diff options
48 files changed, 3350 insertions, 515 deletions
diff --git a/include/android/input.h b/include/android/input.h index a45f065dd0..9a0eb4d838 100644 --- a/include/android/input.h +++ b/include/android/input.h @@ -781,6 +781,8 @@ enum { * * These values are relative to the state from the last event, not accumulated, so developers * should make sure to process this axis value for all batched historical events. + * + * This axis is only set on the first pointer in a motion event. */ AMOTION_EVENT_AXIS_GESTURE_X_OFFSET = 48, /** @@ -797,6 +799,8 @@ enum { * * These values are relative to the state from the last event, not accumulated, so developers * should make sure to process this axis value for all batched historical events. + * + * This axis is only set on the first pointer in a motion event. */ AMOTION_EVENT_AXIS_GESTURE_SCROLL_X_DISTANCE = 50, /** @@ -815,16 +819,29 @@ enum { * * These values are relative to the state from the last event, not accumulated, so developers * should make sure to process this axis value for all batched historical events. + * + * This axis is only set on the first pointer in a motion event. */ AMOTION_EVENT_AXIS_GESTURE_PINCH_SCALE_FACTOR = 52, /** + * Axis constant: the number of fingers being used in a multi-finger swipe gesture. + * + * - For a touch pad, reports the number of fingers being used in a multi-finger swipe gesture + * (with CLASSIFICATION_MULTI_FINGER_SWIPE). + * + * Since CLASSIFICATION_MULTI_FINGER_SWIPE is a hidden API, so is this axis. It is only set on + * the first pointer in a motion event. + */ + AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT = 53, + + /** * Note: This is not an "Axis constant". It does not represent any axis, nor should it be used * to represent any axis. It is a constant holding the value of the largest defined axis value, * to make some computations (like iterating through all possible axes) cleaner. * Please update the value accordingly if you add a new axis. */ - AMOTION_EVENT_MAXIMUM_VALID_AXIS_VALUE = AMOTION_EVENT_AXIS_GESTURE_PINCH_SCALE_FACTOR, + AMOTION_EVENT_MAXIMUM_VALID_AXIS_VALUE = AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT, // NOTE: If you add a new axis here you must also add it to several other files. // Refer to frameworks/base/core/java/android/view/MotionEvent.java for the full list. diff --git a/include/input/Input.h b/include/input/Input.h index fe0c775fd3..527a47741c 100644 --- a/include/input/Input.h +++ b/include/input/Input.h @@ -242,6 +242,19 @@ enum class ToolType { ftl_last = PALM, }; +/** + * The state of the key. This should have 1:1 correspondence with the values of anonymous enum + * defined in input.h + */ +enum class KeyState { + UNKNOWN = AKEY_STATE_UNKNOWN, + UP = AKEY_STATE_UP, + DOWN = AKEY_STATE_DOWN, + VIRTUAL = AKEY_STATE_VIRTUAL, + ftl_first = UNKNOWN, + ftl_last = VIRTUAL, +}; + bool isStylusToolType(ToolType toolType); /* diff --git a/include/input/InputDevice.h b/include/input/InputDevice.h index 1a40fdb90c..b7751f704a 100644 --- a/include/input/InputDevice.h +++ b/include/input/InputDevice.h @@ -18,8 +18,10 @@ #include <android/sensor.h> #include <ftl/flags.h> +#include <ftl/mixins.h> #include <input/Input.h> #include <input/KeyCharacterMap.h> +#include <set> #include <unordered_map> #include <vector> @@ -68,6 +70,9 @@ struct InputDeviceIdentifier { * while conforming to the filename limitations. */ std::string getCanonicalName() const; + + bool operator==(const InputDeviceIdentifier&) const = default; + bool operator!=(const InputDeviceIdentifier&) const = default; }; /* Types of input device sensors. Keep sync with core/java/android/hardware/Sensor.java */ @@ -179,11 +184,24 @@ struct InputDeviceSensorInfo { int32_t id; }; +struct BrightnessLevel : ftl::DefaultConstructible<BrightnessLevel, std::uint8_t>, + ftl::Equatable<BrightnessLevel>, + ftl::Orderable<BrightnessLevel>, + ftl::Addable<BrightnessLevel> { + using DefaultConstructible::DefaultConstructible; +}; + struct InputDeviceLightInfo { explicit InputDeviceLightInfo(std::string name, int32_t id, InputDeviceLightType type, ftl::Flags<InputDeviceLightCapability> capabilityFlags, - int32_t ordinal) - : name(name), id(id), type(type), capabilityFlags(capabilityFlags), ordinal(ordinal) {} + int32_t ordinal, + std::set<BrightnessLevel> preferredBrightnessLevels) + : name(name), + id(id), + type(type), + capabilityFlags(capabilityFlags), + ordinal(ordinal), + preferredBrightnessLevels(std::move(preferredBrightnessLevels)) {} // Name string of the light. std::string name; // Light id @@ -194,6 +212,8 @@ struct InputDeviceLightInfo { ftl::Flags<InputDeviceLightCapability> capabilityFlags; // Ordinal of the light int32_t ordinal; + // Custom brightness levels for the light + std::set<BrightnessLevel> preferredBrightnessLevels; }; struct InputDeviceBatteryInfo { diff --git a/include/input/PrintTools.h b/include/input/PrintTools.h index 02bc2010db..0ca6fa30ce 100644 --- a/include/input/PrintTools.h +++ b/include/input/PrintTools.h @@ -88,6 +88,20 @@ std::string dumpMap(const std::map<K, V>& map, std::string (*keyToString)(const } /** + * Convert map keys to string. The keys of the map should be integral type. + */ +template <typename K, typename V> +std::string dumpMapKeys(const std::map<K, V>& map, + std::string (*keyToString)(const K&) = constToString) { + std::string out; + for (const auto& [k, _] : map) { + out += out.empty() ? "{" : ", "; + out += keyToString(k); + } + return out.empty() ? "{}" : (out + "}"); +} + +/** * Convert a vector to a string. The values of the vector should be of a type supported by * constToString. */ diff --git a/libs/input/InputEventLabels.cpp b/libs/input/InputEventLabels.cpp index f99a7d640e..1c7cc129bd 100644 --- a/libs/input/InputEventLabels.cpp +++ b/libs/input/InputEventLabels.cpp @@ -404,7 +404,8 @@ namespace android { DEFINE_AXIS(GESTURE_Y_OFFSET), \ DEFINE_AXIS(GESTURE_SCROLL_X_DISTANCE), \ DEFINE_AXIS(GESTURE_SCROLL_Y_DISTANCE), \ - DEFINE_AXIS(GESTURE_PINCH_SCALE_FACTOR) + DEFINE_AXIS(GESTURE_PINCH_SCALE_FACTOR), \ + DEFINE_AXIS(GESTURE_SWIPE_FINGER_COUNT) // NOTE: If you add new LEDs here, you must also add them to Input.h #define LEDS_SEQUENCE \ diff --git a/libs/ui/Gralloc5.cpp b/libs/ui/Gralloc5.cpp index 21068394d2..c3b2d3d808 100644 --- a/libs/ui/Gralloc5.cpp +++ b/libs/ui/Gralloc5.cpp @@ -343,14 +343,17 @@ status_t Gralloc5Mapper::validateBufferSize(buffer_handle_t bufferHandle, uint32 return BAD_VALUE; } } - { - auto value = getStandardMetadata<StandardMetadataType::USAGE>(mMapper, bufferHandle); - if (static_cast<BufferUsage>(usage) != value) { - ALOGW("Usage didn't match, expected %" PRIu64 " got %" PRId64, usage, - static_cast<int64_t>(value.value_or(BufferUsage::CPU_READ_NEVER))); - return BAD_VALUE; - } - } + // TODO: This can false-positive fail if the allocator adjusted the USAGE bits internally + // Investigate further & re-enable or remove, but for now ignoring usage should be OK + (void)usage; + // { + // auto value = getStandardMetadata<StandardMetadataType::USAGE>(mMapper, bufferHandle); + // if (static_cast<BufferUsage>(usage) != value) { + // ALOGW("Usage didn't match, expected %" PRIu64 " got %" PRId64, usage, + // static_cast<int64_t>(value.value_or(BufferUsage::CPU_READ_NEVER))); + // return BAD_VALUE; + // } + // } { auto value = getStandardMetadata<StandardMetadataType::STRIDE>(mMapper, bufferHandle); if (stride != value) { diff --git a/libs/ultrahdr/fuzzer/Android.bp b/libs/ultrahdr/fuzzer/Android.bp new file mode 100644 index 0000000000..27b38c3590 --- /dev/null +++ b/libs/ultrahdr/fuzzer/Android.bp @@ -0,0 +1,65 @@ +// Copyright 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_native_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_native_license"], +} + +cc_defaults { + name: "ultrahdr_fuzzer_defaults", + host_supported: true, + static_libs: ["liblog"], + target: { + darwin: { + enabled: false, + }, + }, + fuzz_config: { + cc: [ + "android-media-fuzzing-reports@google.com", + ], + description: "The fuzzers target the APIs of jpeg hdr", + service_privilege: "constrained", + users: "multi_user", + }, +} + +cc_fuzz { + name: "ultrahdr_enc_fuzzer", + defaults: ["ultrahdr_fuzzer_defaults"], + srcs: [ + "ultrahdr_enc_fuzzer.cpp", + ], + shared_libs: [ + "libimage_io", + "libjpeg", + "liblog", + ], + static_libs: [ + "libjpegdecoder", + "libjpegencoder", + "libultrahdr", + "libutils", + ], + fuzz_config: { + fuzzed_code_usage: "future_version", + vector: "local_no_privileges_required", + }, +} + diff --git a/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp b/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp new file mode 100644 index 0000000000..472699bb9e --- /dev/null +++ b/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp @@ -0,0 +1,286 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// System include files +#include <fuzzer/FuzzedDataProvider.h> +#include <algorithm> +#include <iostream> +#include <random> +#include <vector> + +// User include files +#include "ultrahdr/gainmapmath.h" +#include "ultrahdr/jpegencoderhelper.h" +#include "utils/Log.h" + +using namespace android::ultrahdr; + +// constants +const int kMinWidth = 8; +const int kMaxWidth = 7680; + +const int kMinHeight = 8; +const int kMaxHeight = 4320; + +const int kScaleFactor = 4; + +const int kJpegBlock = 16; + +// Color gamuts for image data, sync with ultrahdr.h +const int kCgMin = ULTRAHDR_COLORGAMUT_UNSPECIFIED + 1; +const int kCgMax = ULTRAHDR_COLORGAMUT_MAX; + +// Transfer functions for image data, sync with ultrahdr.h +const int kTfMin = ULTRAHDR_TF_UNSPECIFIED + 1; +const int kTfMax = ULTRAHDR_TF_MAX; + +// Transfer functions for image data, sync with ultrahdr.h +const int kOfMin = ULTRAHDR_OUTPUT_UNSPECIFIED + 1; +const int kOfMax = ULTRAHDR_OUTPUT_MAX; + +// quality factor +const int kQfMin = 0; +const int kQfMax = 100; + +// seed +const unsigned kSeed = 0x7ab7; + +class JpegHDRFuzzer { +public: + JpegHDRFuzzer(const uint8_t* data, size_t size) : mFdp(data, size){}; + void process(); + void fillP010Buffer(uint16_t* data, int width, int height, int stride); + void fill420Buffer(uint8_t* data, int size); + +private: + FuzzedDataProvider mFdp; +}; + +void JpegHDRFuzzer::fillP010Buffer(uint16_t* data, int width, int height, int stride) { + uint16_t* tmp = data; + std::vector<uint16_t> buffer(16); + for (int i = 0; i < buffer.size(); i++) { + buffer[i] = mFdp.ConsumeIntegralInRange<int>(0, (1 << 10) - 1); + } + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i += buffer.size()) { + memcpy(data + i, buffer.data(), std::min((int)buffer.size(), (width - i))); + std::shuffle(buffer.begin(), buffer.end(), std::default_random_engine(kSeed)); + } + tmp += stride; + } +} + +void JpegHDRFuzzer::fill420Buffer(uint8_t* data, int size) { + std::vector<uint8_t> buffer(16); + mFdp.ConsumeData(buffer.data(), buffer.size()); + for (int i = 0; i < size; i += buffer.size()) { + memcpy(data + i, buffer.data(), std::min((int)buffer.size(), (size - i))); + std::shuffle(buffer.begin(), buffer.end(), std::default_random_engine(kSeed)); + } +} + +void JpegHDRFuzzer::process() { + while (mFdp.remaining_bytes()) { + struct jpegr_uncompressed_struct p010Img {}; + struct jpegr_uncompressed_struct yuv420Img {}; + struct jpegr_uncompressed_struct grayImg {}; + struct jpegr_compressed_struct jpegImgR {}; + struct jpegr_compressed_struct jpegImg {}; + struct jpegr_compressed_struct jpegGainMap {}; + + // which encode api to select + int muxSwitch = mFdp.ConsumeIntegralInRange<int>(0, 4); + + // quality factor + int quality = mFdp.ConsumeIntegralInRange<int>(kQfMin, kQfMax); + + // hdr_tf + auto tf = static_cast<ultrahdr_transfer_function>( + mFdp.ConsumeIntegralInRange<int>(kTfMin, kTfMax)); + + // p010 Cg + auto p010Cg = + static_cast<ultrahdr_color_gamut>(mFdp.ConsumeIntegralInRange<int>(kCgMin, kCgMax)); + + // 420 Cg + auto yuv420Cg = + static_cast<ultrahdr_color_gamut>(mFdp.ConsumeIntegralInRange<int>(kCgMin, kCgMax)); + + // hdr_of + auto of = static_cast<ultrahdr_output_format>( + mFdp.ConsumeIntegralInRange<int>(kOfMin, kOfMax)); + + int width = mFdp.ConsumeIntegralInRange<int>(kMinWidth, kMaxWidth); + width = (width >> 1) << 1; + + int height = mFdp.ConsumeIntegralInRange<int>(kMinHeight, kMaxHeight); + height = (height >> 1) << 1; + + std::unique_ptr<uint16_t[]> bufferY = nullptr; + std::unique_ptr<uint16_t[]> bufferUV = nullptr; + std::unique_ptr<uint8_t[]> yuv420ImgRaw = nullptr; + std::unique_ptr<uint8_t[]> grayImgRaw = nullptr; + if (muxSwitch != 4) { + // init p010 image + bool isUVContiguous = mFdp.ConsumeBool(); + bool hasYStride = mFdp.ConsumeBool(); + int yStride = hasYStride ? mFdp.ConsumeIntegralInRange<int>(width, width + 128) : width; + p010Img.width = width; + p010Img.height = height; + p010Img.colorGamut = p010Cg; + p010Img.luma_stride = hasYStride ? yStride : 0; + int bppP010 = 2; + if (isUVContiguous) { + size_t p010Size = yStride * height * 3 / 2; + bufferY = std::make_unique<uint16_t[]>(p010Size); + p010Img.data = bufferY.get(); + p010Img.chroma_data = nullptr; + p010Img.chroma_stride = 0; + fillP010Buffer(bufferY.get(), width, height, yStride); + fillP010Buffer(bufferY.get() + yStride * height, width, height / 2, yStride); + } else { + int uvStride = mFdp.ConsumeIntegralInRange<int>(width, width + 128); + size_t p010YSize = yStride * height; + bufferY = std::make_unique<uint16_t[]>(p010YSize); + p010Img.data = bufferY.get(); + fillP010Buffer(bufferY.get(), width, height, yStride); + size_t p010UVSize = uvStride * p010Img.height / 2; + bufferUV = std::make_unique<uint16_t[]>(p010UVSize); + p010Img.chroma_data = bufferUV.get(); + p010Img.chroma_stride = uvStride; + fillP010Buffer(bufferUV.get(), width, height / 2, uvStride); + } + } else { + int map_width = width / kScaleFactor; + int map_height = height / kScaleFactor; + map_width = static_cast<size_t>(floor((map_width + kJpegBlock - 1) / kJpegBlock)) * + kJpegBlock; + map_height = ((map_height + 1) >> 1) << 1; + // init 400 image + grayImg.width = map_width; + grayImg.height = map_height; + grayImg.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED; + + const size_t graySize = map_width * map_height; + grayImgRaw = std::make_unique<uint8_t[]>(graySize); + grayImg.data = grayImgRaw.get(); + fill420Buffer(grayImgRaw.get(), graySize); + grayImg.chroma_data = nullptr; + grayImg.luma_stride = 0; + grayImg.chroma_stride = 0; + } + + if (muxSwitch > 0) { + // init 420 image + yuv420Img.width = width; + yuv420Img.height = height; + yuv420Img.colorGamut = yuv420Cg; + + const size_t yuv420Size = (yuv420Img.width * yuv420Img.height * 3) / 2; + yuv420ImgRaw = std::make_unique<uint8_t[]>(yuv420Size); + yuv420Img.data = yuv420ImgRaw.get(); + fill420Buffer(yuv420ImgRaw.get(), yuv420Size); + yuv420Img.chroma_data = nullptr; + yuv420Img.luma_stride = 0; + yuv420Img.chroma_stride = 0; + } + + // dest + // 2 * p010 size as input data is random, DCT compression might not behave as expected + jpegImgR.maxLength = std::max(8 * 1024 /* min size 8kb */, width * height * 3 * 2); + auto jpegImgRaw = std::make_unique<uint8_t[]>(jpegImgR.maxLength); + jpegImgR.data = jpegImgRaw.get(); + +//#define DUMP_PARAM +#ifdef DUMP_PARAM + std::cout << "Api Select " << muxSwitch << std::endl; + std::cout << "image dimensions " << width << " x " << height << std::endl; + std::cout << "p010 color gamut " << p010Img.colorGamut << std::endl; + std::cout << "p010 luma stride " << p010Img.luma_stride << std::endl; + std::cout << "p010 chroma stride " << p010Img.chroma_stride << std::endl; + std::cout << "420 color gamut " << yuv420Img.colorGamut << std::endl; + std::cout << "quality factor " << quality << std::endl; +#endif + + JpegR jpegHdr; + android::status_t status = android::UNKNOWN_ERROR; + if (muxSwitch == 0) { // api 0 + jpegImgR.length = 0; + status = jpegHdr.encodeJPEGR(&p010Img, tf, &jpegImgR, quality, nullptr); + } else if (muxSwitch == 1) { // api 1 + jpegImgR.length = 0; + status = jpegHdr.encodeJPEGR(&p010Img, &yuv420Img, tf, &jpegImgR, quality, nullptr); + } else { + // compressed img + JpegEncoderHelper encoder; + if (encoder.compressImage(yuv420Img.data, yuv420Img.width, yuv420Img.height, quality, + nullptr, 0)) { + jpegImg.length = encoder.getCompressedImageSize(); + jpegImg.maxLength = jpegImg.length; + jpegImg.data = encoder.getCompressedImagePtr(); + jpegImg.colorGamut = yuv420Cg; + + if (muxSwitch == 2) { // api 2 + jpegImgR.length = 0; + status = jpegHdr.encodeJPEGR(&p010Img, &yuv420Img, &jpegImg, tf, &jpegImgR); + } else if (muxSwitch == 3) { // api 3 + jpegImgR.length = 0; + status = jpegHdr.encodeJPEGR(&p010Img, &jpegImg, tf, &jpegImgR); + } else if (muxSwitch == 4) { // api 4 + jpegImgR.length = 0; + JpegEncoderHelper gainMapEncoder; + if (gainMapEncoder.compressImage(grayImg.data, grayImg.width, grayImg.height, + quality, nullptr, 0, true)) { + jpegGainMap.length = gainMapEncoder.getCompressedImageSize(); + jpegGainMap.maxLength = jpegImg.length; + jpegGainMap.data = gainMapEncoder.getCompressedImagePtr(); + jpegGainMap.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED; + ultrahdr_metadata_struct metadata; + metadata.version = "1.3.1"; + if (tf == ULTRAHDR_TF_HLG) { + metadata.maxContentBoost = kHlgMaxNits / kSdrWhiteNits; + } else if (tf == ULTRAHDR_TF_PQ) { + metadata.maxContentBoost = kPqMaxNits / kSdrWhiteNits; + } else { + metadata.maxContentBoost = 0; + } + metadata.minContentBoost = 1.0f; + status = jpegHdr.encodeJPEGR(&jpegImg, &jpegGainMap, &metadata, &jpegImgR); + } + } + } + } + if (status == android::OK) { + jpegr_uncompressed_struct decodedJpegR; + auto decodedRaw = std::make_unique<uint8_t[]>(width * height * 8); + decodedJpegR.data = decodedRaw.get(); + jpegHdr.decodeJPEGR(&jpegImgR, &decodedJpegR, + mFdp.ConsumeFloatingPointInRange<float>(1.0, FLT_MAX), nullptr, of, + nullptr, nullptr); + std::vector<uint8_t> iccData(0); + std::vector<uint8_t> exifData(0); + jpegr_info_struct info{0, 0, &iccData, &exifData}; + jpegHdr.getJPEGRInfo(&jpegImgR, &info); + } + } +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + JpegHDRFuzzer fuzzHandle(data, size); + fuzzHandle.process(); + return 0; +} diff --git a/libs/ultrahdr/include/ultrahdr/jpegr.h b/libs/ultrahdr/include/ultrahdr/jpegr.h index 00b66aeaf6..1f9bd0f930 100644 --- a/libs/ultrahdr/include/ultrahdr/jpegr.h +++ b/libs/ultrahdr/include/ultrahdr/jpegr.h @@ -17,6 +17,7 @@ #ifndef ANDROID_ULTRAHDR_JPEGR_H #define ANDROID_ULTRAHDR_JPEGR_H +#include "jpegencoderhelper.h" #include "jpegrerrorcode.h" #include "ultrahdr.h" @@ -312,11 +313,11 @@ private: * This method is called in the encoding pipeline. It will encode the gain map. * * @param uncompressed_gain_map uncompressed gain map - * @param dest encoded recover map + * @param resource to compress gain map * @return NO_ERROR if encoding succeeds, error code if error occurs. */ status_t compressGainMap(jr_uncompressed_ptr uncompressed_gain_map, - jr_compressed_ptr dest); + JpegEncoderHelper* jpeg_encoder); /* * This methoud is called to separate primary image and gain map image from JPEGR diff --git a/libs/ultrahdr/jpegr.cpp b/libs/ultrahdr/jpegr.cpp index 5ebca399a9..c250aa02fd 100644 --- a/libs/ultrahdr/jpegr.cpp +++ b/libs/ultrahdr/jpegr.cpp @@ -244,11 +244,13 @@ status_t JpegR::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, std::unique_ptr<uint8_t[]> map_data; map_data.reset(reinterpret_cast<uint8_t*>(map.data)); + JpegEncoderHelper jpeg_encoder_gainmap; + JPEGR_CHECK(compressGainMap(&map, &jpeg_encoder_gainmap)); jpegr_compressed_struct compressed_map; - compressed_map.maxLength = map.width * map.height; - unique_ptr<uint8_t[]> compressed_map_data = make_unique<uint8_t[]>(compressed_map.maxLength); - compressed_map.data = compressed_map_data.get(); - JPEGR_CHECK(compressGainMap(&map, &compressed_map)); + compressed_map.maxLength = jpeg_encoder_gainmap.getCompressedImageSize(); + compressed_map.length = compressed_map.maxLength; + compressed_map.data = jpeg_encoder_gainmap.getCompressedImagePtr(); + compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED; sp<DataStruct> icc = IccHelper::writeIccProfile(ULTRAHDR_TF_SRGB, uncompressed_yuv_420_image.colorGamut); @@ -301,11 +303,13 @@ status_t JpegR::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, std::unique_ptr<uint8_t[]> map_data; map_data.reset(reinterpret_cast<uint8_t*>(map.data)); + JpegEncoderHelper jpeg_encoder_gainmap; + JPEGR_CHECK(compressGainMap(&map, &jpeg_encoder_gainmap)); jpegr_compressed_struct compressed_map; - compressed_map.maxLength = map.width * map.height; - unique_ptr<uint8_t[]> compressed_map_data = make_unique<uint8_t[]>(compressed_map.maxLength); - compressed_map.data = compressed_map_data.get(); - JPEGR_CHECK(compressGainMap(&map, &compressed_map)); + compressed_map.maxLength = jpeg_encoder_gainmap.getCompressedImageSize(); + compressed_map.length = compressed_map.maxLength; + compressed_map.data = jpeg_encoder_gainmap.getCompressedImagePtr(); + compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED; sp<DataStruct> icc = IccHelper::writeIccProfile(ULTRAHDR_TF_SRGB, uncompressed_yuv_420_image->colorGamut); @@ -356,11 +360,13 @@ status_t JpegR::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, std::unique_ptr<uint8_t[]> map_data; map_data.reset(reinterpret_cast<uint8_t*>(map.data)); + JpegEncoderHelper jpeg_encoder_gainmap; + JPEGR_CHECK(compressGainMap(&map, &jpeg_encoder_gainmap)); jpegr_compressed_struct compressed_map; - compressed_map.maxLength = map.width * map.height; - unique_ptr<uint8_t[]> compressed_map_data = make_unique<uint8_t[]>(compressed_map.maxLength); - compressed_map.data = compressed_map_data.get(); - JPEGR_CHECK(compressGainMap(&map, &compressed_map)); + compressed_map.maxLength = jpeg_encoder_gainmap.getCompressedImageSize(); + compressed_map.length = compressed_map.maxLength; + compressed_map.data = jpeg_encoder_gainmap.getCompressedImagePtr(); + compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED; JPEGR_CHECK(appendGainMap(compressed_jpeg_image, &compressed_map, nullptr, &metadata, dest)); @@ -407,11 +413,13 @@ status_t JpegR::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image, std::unique_ptr<uint8_t[]> map_data; map_data.reset(reinterpret_cast<uint8_t*>(map.data)); + JpegEncoderHelper jpeg_encoder_gainmap; + JPEGR_CHECK(compressGainMap(&map, &jpeg_encoder_gainmap)); jpegr_compressed_struct compressed_map; - compressed_map.maxLength = map.width * map.height; - unique_ptr<uint8_t[]> compressed_map_data = make_unique<uint8_t[]>(compressed_map.maxLength); - compressed_map.data = compressed_map_data.get(); - JPEGR_CHECK(compressGainMap(&map, &compressed_map)); + compressed_map.maxLength = jpeg_encoder_gainmap.getCompressedImageSize(); + compressed_map.length = compressed_map.maxLength; + compressed_map.data = jpeg_encoder_gainmap.getCompressedImagePtr(); + compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED; JPEGR_CHECK(appendGainMap(compressed_jpeg_image, &compressed_map, nullptr, &metadata, dest)); @@ -604,30 +612,21 @@ status_t JpegR::decodeJPEGR(jr_compressed_ptr compressed_jpegr_image, } status_t JpegR::compressGainMap(jr_uncompressed_ptr uncompressed_gain_map, - jr_compressed_ptr dest) { - if (uncompressed_gain_map == nullptr || dest == nullptr) { + JpegEncoderHelper* jpeg_encoder) { + if (uncompressed_gain_map == nullptr || jpeg_encoder == nullptr) { return ERROR_JPEGR_INVALID_NULL_PTR; } - JpegEncoderHelper jpeg_encoder; - if (!jpeg_encoder.compressImage(uncompressed_gain_map->data, - uncompressed_gain_map->width, - uncompressed_gain_map->height, - kMapCompressQuality, - nullptr, - 0, - true /* isSingleChannel */)) { + if (!jpeg_encoder->compressImage(uncompressed_gain_map->data, + uncompressed_gain_map->width, + uncompressed_gain_map->height, + kMapCompressQuality, + nullptr, + 0, + true /* isSingleChannel */)) { return ERROR_JPEGR_ENCODE_ERROR; } - if (dest->maxLength < jpeg_encoder.getCompressedImageSize()) { - return ERROR_JPEGR_BUFFER_TOO_SMALL; - } - - memcpy(dest->data, jpeg_encoder.getCompressedImagePtr(), jpeg_encoder.getCompressedImageSize()); - dest->length = jpeg_encoder.getCompressedImageSize(); - dest->colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED; - return NO_ERROR; } diff --git a/libs/ultrahdr/tests/jpegr_test.cpp b/libs/ultrahdr/tests/jpegr_test.cpp index 5f5c19684b..d482ea1f79 100644 --- a/libs/ultrahdr/tests/jpegr_test.cpp +++ b/libs/ultrahdr/tests/jpegr_test.cpp @@ -89,6 +89,51 @@ static bool loadFile(const char filename[], void*& result, int* fileLength) { return true; } +static bool loadP010Image(const char *filename, jr_uncompressed_ptr img, + bool isUVContiguous) { + int fd = open(filename, O_CLOEXEC); + if (fd < 0) { + return false; + } + const int bpp = 2; + int lumaStride = img->luma_stride == 0 ? img->width : img->luma_stride; + int lumaSize = bpp * lumaStride * img->height; + int chromaSize = bpp * (img->height / 2) * + (isUVContiguous ? lumaStride : img->chroma_stride); + img->data = malloc(lumaSize + (isUVContiguous ? chromaSize : 0)); + if (img->data == nullptr) { + ALOGE("loadP010Image(): failed to allocate memory for luma data."); + return false; + } + uint8_t *mem = static_cast<uint8_t *>(img->data); + for (int i = 0; i < img->height; i++) { + if (read(fd, mem, img->width * bpp) != img->width * bpp) { + close(fd); + return false; + } + mem += lumaStride * bpp; + } + int chromaStride = lumaStride; + if (!isUVContiguous) { + img->chroma_data = malloc(chromaSize); + if (img->chroma_data == nullptr) { + ALOGE("loadP010Image(): failed to allocate memory for chroma data."); + return false; + } + mem = static_cast<uint8_t *>(img->chroma_data); + chromaStride = img->chroma_stride; + } + for (int i = 0; i < img->height / 2; i++) { + if (read(fd, mem, img->width * bpp) != img->width * bpp) { + close(fd); + return false; + } + mem += chromaStride * bpp; + } + close(fd); + return true; +} + class JpegRTest : public testing::Test { public: JpegRTest(); @@ -98,10 +143,11 @@ protected: virtual void SetUp(); virtual void TearDown(); - struct jpegr_uncompressed_struct mRawP010Image; - struct jpegr_uncompressed_struct mRawP010ImageWithStride; - struct jpegr_uncompressed_struct mRawYuv420Image; - struct jpegr_compressed_struct mJpegImage; + struct jpegr_uncompressed_struct mRawP010Image{}; + struct jpegr_uncompressed_struct mRawP010ImageWithStride{}; + struct jpegr_uncompressed_struct mRawP010ImageWithChromaData{}; + struct jpegr_uncompressed_struct mRawYuv420Image{}; + struct jpegr_compressed_struct mJpegImage{}; }; JpegRTest::JpegRTest() {} @@ -110,7 +156,11 @@ JpegRTest::~JpegRTest() {} void JpegRTest::SetUp() {} void JpegRTest::TearDown() { free(mRawP010Image.data); + free(mRawP010Image.chroma_data); free(mRawP010ImageWithStride.data); + free(mRawP010ImageWithStride.chroma_data); + free(mRawP010ImageWithChromaData.data); + free(mRawP010ImageWithChromaData.chroma_data); free(mRawYuv420Image.data); free(mJpegImage.data); } @@ -286,6 +336,8 @@ TEST_F(JpegRTest, encodeAPI0ForInvalidArgs) { &mRawP010ImageWithStride, ultrahdr_transfer_function::ULTRAHDR_TF_HLG, &jpegR, DEFAULT_JPEG_QUALITY, nullptr)) << "fail, API allows bad chroma stride"; + mRawP010ImageWithStride.chroma_data = nullptr; + free(jpegR.data); } @@ -734,6 +786,7 @@ TEST_F(JpegRTest, encodeAPI3ForInvalidArgs) { EXPECT_NE(OK, jpegRCodec.encodeJPEGR( &mRawP010ImageWithStride, &jpegR, ultrahdr_transfer_function::ULTRAHDR_TF_HLG, &jpegR)) << "fail, API allows bad chroma stride"; + mRawP010ImageWithStride.chroma_data = nullptr; // bad compressed image EXPECT_NE(OK, jpegRCodec.encodeJPEGR( @@ -831,6 +884,81 @@ TEST_F(JpegRTest, writeXmpThenRead) { EXPECT_FLOAT_EQ(metadata_expected.minContentBoost, metadata_read.minContentBoost); } +/* Test Encode API-0 */ +TEST_F(JpegRTest, encodeFromP010) { + int ret; + + mRawP010Image.width = TEST_IMAGE_WIDTH; + mRawP010Image.height = TEST_IMAGE_HEIGHT; + mRawP010Image.colorGamut = ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_BT2100; + // Load input files. + if (!loadP010Image(RAW_P010_IMAGE, &mRawP010Image, true)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + + JpegR jpegRCodec; + + jpegr_compressed_struct jpegR; + jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t); + jpegR.data = malloc(jpegR.maxLength); + ret = jpegRCodec.encodeJPEGR( + &mRawP010Image, ultrahdr_transfer_function::ULTRAHDR_TF_HLG, &jpegR, DEFAULT_JPEG_QUALITY, + nullptr); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + + mRawP010ImageWithStride.width = TEST_IMAGE_WIDTH; + mRawP010ImageWithStride.height = TEST_IMAGE_HEIGHT; + mRawP010ImageWithStride.luma_stride = TEST_IMAGE_WIDTH + 128; + mRawP010ImageWithStride.colorGamut = ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_BT2100; + // Load input files. + if (!loadP010Image(RAW_P010_IMAGE, &mRawP010ImageWithStride, true)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + + jpegr_compressed_struct jpegRWithStride; + jpegRWithStride.maxLength = jpegR.length; + jpegRWithStride.data = malloc(jpegRWithStride.maxLength); + ret = jpegRCodec.encodeJPEGR( + &mRawP010ImageWithStride, ultrahdr_transfer_function::ULTRAHDR_TF_HLG, &jpegRWithStride, + DEFAULT_JPEG_QUALITY, nullptr); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + ASSERT_EQ(jpegR.length, jpegRWithStride.length) + << "Same input is yielding different output"; + ASSERT_EQ(0, memcmp(jpegR.data, jpegRWithStride.data, jpegR.length)) + << "Same input is yielding different output"; + + mRawP010ImageWithChromaData.width = TEST_IMAGE_WIDTH; + mRawP010ImageWithChromaData.height = TEST_IMAGE_HEIGHT; + mRawP010ImageWithChromaData.luma_stride = TEST_IMAGE_WIDTH + 64; + mRawP010ImageWithChromaData.chroma_stride = TEST_IMAGE_WIDTH + 256; + mRawP010ImageWithChromaData.colorGamut = ultrahdr_color_gamut::ULTRAHDR_COLORGAMUT_BT2100; + // Load input files. + if (!loadP010Image(RAW_P010_IMAGE, &mRawP010ImageWithChromaData, false)) { + FAIL() << "Load file " << RAW_P010_IMAGE << " failed"; + } + jpegr_compressed_struct jpegRWithChromaData; + jpegRWithChromaData.maxLength = jpegR.length; + jpegRWithChromaData.data = malloc(jpegRWithChromaData.maxLength); + ret = jpegRCodec.encodeJPEGR( + &mRawP010ImageWithChromaData, ultrahdr_transfer_function::ULTRAHDR_TF_HLG, + &jpegRWithChromaData, DEFAULT_JPEG_QUALITY, nullptr); + if (ret != OK) { + FAIL() << "Error code is " << ret; + } + ASSERT_EQ(jpegR.length, jpegRWithChromaData.length) + << "Same input is yielding different output"; + ASSERT_EQ(0, memcmp(jpegR.data, jpegRWithChromaData.data, jpegR.length)) + << "Same input is yielding different output"; + + free(jpegR.data); + free(jpegRWithStride.data); + free(jpegRWithChromaData.data); +} + /* Test Encode API-0 and decode */ TEST_F(JpegRTest, encodeFromP010ThenDecode) { int ret; diff --git a/opengl/libs/EGL/Loader.cpp b/opengl/libs/EGL/Loader.cpp index 6cfb608dd5..2c3ce16f66 100644 --- a/opengl/libs/EGL/Loader.cpp +++ b/opengl/libs/EGL/Loader.cpp @@ -21,6 +21,7 @@ #include <android-base/properties.h> #include <android/dlext.h> +#include <cutils/properties.h> #include <dirent.h> #include <dlfcn.h> #include <graphicsenv/GraphicsEnv.h> @@ -261,7 +262,10 @@ void* Loader::open(egl_connection_t* cnx) hnd = attempt_to_load_system_driver(cnx, nullptr, true); } - if (!hnd && !failToLoadFromDriverSuffixProperty) { + if (!hnd && !failToLoadFromDriverSuffixProperty && + property_get_int32("ro.vendor.api_level", 0) < __ANDROID_API_U__) { + // Still can't find the graphics drivers with the exact name. This time try to use wildcard + // matching if the device is launched before Android 14. hnd = attempt_to_load_system_driver(cnx, nullptr, false); } diff --git a/services/inputflinger/BlockingQueue.h b/services/inputflinger/BlockingQueue.h index fe3728789d..56938480f0 100644 --- a/services/inputflinger/BlockingQueue.h +++ b/services/inputflinger/BlockingQueue.h @@ -16,15 +16,17 @@ #pragma once -#include "android-base/thread_annotations.h" #include <condition_variable> +#include <list> #include <mutex> -#include <vector> +#include <optional> +#include "android-base/thread_annotations.h" namespace android { /** - * A FIFO queue that stores up to <i>capacity</i> objects. + * A thread-safe FIFO queue. This list-backed queue stores up to <i>capacity</i> objects if + * a capacity is provided at construction, and is otherwise unbounded. * Objects can always be added. Objects are added immediately. * If the queue is full, new objects cannot be added. * @@ -33,13 +35,13 @@ namespace android { template <class T> class BlockingQueue { public: - BlockingQueue(size_t capacity) : mCapacity(capacity) { - mQueue.reserve(mCapacity); - }; + explicit BlockingQueue() = default; + + explicit BlockingQueue(size_t capacity) : mCapacity(capacity){}; /** * Retrieve and remove the oldest object. - * Blocks execution while queue is empty. + * Blocks execution indefinitely while queue is empty. */ T pop() { std::unique_lock lock(mLock); @@ -51,26 +53,62 @@ public: }; /** + * Retrieve and remove the oldest object. + * Blocks execution for the given duration while queue is empty, and returns std::nullopt + * if the queue was empty for the entire duration. + */ + std::optional<T> popWithTimeout(std::chrono::nanoseconds duration) { + std::unique_lock lock(mLock); + android::base::ScopedLockAssertion assumeLock(mLock); + if (!mHasElements.wait_for(lock, duration, + [this]() REQUIRES(mLock) { return !this->mQueue.empty(); })) { + return {}; + } + T t = std::move(mQueue.front()); + mQueue.erase(mQueue.begin()); + return t; + }; + + /** * Add a new object to the queue. * Does not block. * Return true if an element was successfully added. * Return false if the queue is full. */ bool push(T&& t) { - { + { // acquire lock std::scoped_lock lock(mLock); - if (mQueue.size() == mCapacity) { + if (mCapacity && mQueue.size() == mCapacity) { return false; } mQueue.push_back(std::move(t)); - } + } // release lock + mHasElements.notify_one(); + return true; + }; + + /** + * Construct a new object into the queue. + * Does not block. + * Return true if an element was successfully added. + * Return false if the queue is full. + */ + template <class... Args> + bool emplace(Args&&... args) { + { // acquire lock + std::scoped_lock lock(mLock); + if (mCapacity && mQueue.size() == mCapacity) { + return false; + } + mQueue.emplace_back(args...); + } // release lock mHasElements.notify_one(); return true; }; - void erase(const std::function<bool(const T&)>& lambda) { + void erase_if(const std::function<bool(const T&)>& pred) { std::scoped_lock lock(mLock); - std::erase_if(mQueue, [&lambda](const auto& t) { return lambda(t); }); + std::erase_if(mQueue, pred); } /** @@ -93,7 +131,7 @@ public: } private: - const size_t mCapacity; + const std::optional<size_t> mCapacity; /** * Used to signal that mQueue is non-empty. */ @@ -102,7 +140,7 @@ private: * Lock for accessing and waiting on elements. */ std::mutex mLock; - std::vector<T> mQueue GUARDED_BY(mLock); + std::list<T> mQueue GUARDED_BY(mLock); }; } // namespace android diff --git a/services/inputflinger/InputCommonConverter.cpp b/services/inputflinger/InputCommonConverter.cpp index 2437d0fcfc..781288076c 100644 --- a/services/inputflinger/InputCommonConverter.cpp +++ b/services/inputflinger/InputCommonConverter.cpp @@ -258,12 +258,12 @@ static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_13) == common static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_14) == common::Axis::GENERIC_14); static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_15) == common::Axis::GENERIC_15); static_assert(static_cast<common::Axis>(AMOTION_EVENT_AXIS_GENERIC_16) == common::Axis::GENERIC_16); -// TODO(b/251196347): add GESTURE_{X,Y}_OFFSET, GESTURE_SCROLL_{X,Y}_DISTANCE, and -// GESTURE_PINCH_SCALE_FACTOR. +// TODO(b/251196347): add GESTURE_{X,Y}_OFFSET, GESTURE_SCROLL_{X,Y}_DISTANCE, +// GESTURE_PINCH_SCALE_FACTOR, and GESTURE_SWIPE_FINGER_COUNT. // If you added a new axis, consider whether this should also be exposed as a HAL axis. Update the // static_assert below and add the new axis here, or leave a comment summarizing your decision. static_assert(static_cast<common::Axis>(AMOTION_EVENT_MAXIMUM_VALID_AXIS_VALUE) == - static_cast<common::Axis>(AMOTION_EVENT_AXIS_GESTURE_PINCH_SCALE_FACTOR)); + static_cast<common::Axis>(AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT)); static common::VideoFrame getHalVideoFrame(const TouchVideoFrame& frame) { common::VideoFrame out; diff --git a/services/inputflinger/InputDeviceMetricsCollector.cpp b/services/inputflinger/InputDeviceMetricsCollector.cpp index c6089c8d2a..3e25cc3d66 100644 --- a/services/inputflinger/InputDeviceMetricsCollector.cpp +++ b/services/inputflinger/InputDeviceMetricsCollector.cpp @@ -17,52 +17,362 @@ #define LOG_TAG "InputDeviceMetricsCollector" #include "InputDeviceMetricsCollector.h" +#include "KeyCodeClassifications.h" + +#include <android-base/stringprintf.h> +#include <input/PrintTools.h> +#include <linux/input.h> + namespace android { +using android::base::StringPrintf; +using std::chrono::nanoseconds; + +namespace { + +constexpr nanoseconds DEFAULT_USAGE_SESSION_TIMEOUT = std::chrono::seconds(5); + +/** + * Log debug messages about metrics events logged to statsd. + * Enable this via "adb shell setprop log.tag.InputDeviceMetricsCollector DEBUG" (requires restart) + */ +const bool DEBUG = __android_log_is_loggable(ANDROID_LOG_DEBUG, LOG_TAG, ANDROID_LOG_INFO); + +int32_t linuxBusToInputDeviceBusEnum(int32_t linuxBus) { + switch (linuxBus) { + case BUS_USB: + return util::INPUT_DEVICE_USAGE_REPORTED__DEVICE_BUS__USB; + case BUS_BLUETOOTH: + return util::INPUT_DEVICE_USAGE_REPORTED__DEVICE_BUS__BLUETOOTH; + default: + return util::INPUT_DEVICE_USAGE_REPORTED__DEVICE_BUS__OTHER; + } +} + +class : public InputDeviceMetricsLogger { + nanoseconds getCurrentTime() override { return nanoseconds(systemTime(SYSTEM_TIME_MONOTONIC)); } + + void logInputDeviceUsageReported(const InputDeviceIdentifier& identifier, + const DeviceUsageReport& report) override { + const int32_t durationMillis = + std::chrono::duration_cast<std::chrono::milliseconds>(report.usageDuration).count(); + const static std::vector<int32_t> empty; + + ALOGD_IF(DEBUG, "Usage session reported for device: %s", identifier.name.c_str()); + ALOGD_IF(DEBUG, " Total duration: %dms", durationMillis); + ALOGD_IF(DEBUG, " Source breakdown:"); + + std::vector<int32_t> sources; + std::vector<int32_t> durationsPerSource; + for (auto& [src, dur] : report.sourceBreakdown) { + sources.push_back(ftl::to_underlying(src)); + int32_t durMillis = std::chrono::duration_cast<std::chrono::milliseconds>(dur).count(); + durationsPerSource.emplace_back(durMillis); + ALOGD_IF(DEBUG, " - usageSource: %s\t duration: %dms", + ftl::enum_string(src).c_str(), durMillis); + } + + util::stats_write(util::INPUTDEVICE_USAGE_REPORTED, identifier.vendor, identifier.product, + identifier.version, linuxBusToInputDeviceBusEnum(identifier.bus), + durationMillis, sources, durationsPerSource, /*uids=*/empty, + /*usage_durations_per_uid=*/empty); + } +} sStatsdLogger; + +bool isIgnoredInputDeviceId(int32_t deviceId) { + switch (deviceId) { + case INVALID_INPUT_DEVICE_ID: + case VIRTUAL_KEYBOARD_ID: + return true; + default: + return false; + } +} + +} // namespace + +InputDeviceUsageSource getUsageSourceForKeyArgs(const InputDeviceInfo& info, + const NotifyKeyArgs& keyArgs) { + if (!isFromSource(keyArgs.source, AINPUT_SOURCE_KEYBOARD)) { + return InputDeviceUsageSource::UNKNOWN; + } + + if (isFromSource(keyArgs.source, AINPUT_SOURCE_DPAD) && + DPAD_ALL_KEYCODES.count(keyArgs.keyCode) != 0) { + return InputDeviceUsageSource::DPAD; + } + + if (isFromSource(keyArgs.source, AINPUT_SOURCE_GAMEPAD) && + GAMEPAD_KEYCODES.count(keyArgs.keyCode) != 0) { + return InputDeviceUsageSource::GAMEPAD; + } + + if (info.getKeyboardType() == AINPUT_KEYBOARD_TYPE_ALPHABETIC) { + return InputDeviceUsageSource::KEYBOARD; + } + + return InputDeviceUsageSource::BUTTONS; +} + +std::set<InputDeviceUsageSource> getUsageSourcesForMotionArgs(const NotifyMotionArgs& motionArgs) { + LOG_ALWAYS_FATAL_IF(motionArgs.pointerCount < 1, "Received motion args without pointers"); + std::set<InputDeviceUsageSource> sources; + + for (uint32_t i = 0; i < motionArgs.pointerCount; i++) { + const auto toolType = motionArgs.pointerProperties[i].toolType; + if (isFromSource(motionArgs.source, AINPUT_SOURCE_MOUSE)) { + if (toolType == ToolType::MOUSE) { + sources.emplace(InputDeviceUsageSource::MOUSE); + continue; + } + if (toolType == ToolType::FINGER) { + sources.emplace(InputDeviceUsageSource::TOUCHPAD); + continue; + } + if (isStylusToolType(toolType)) { + sources.emplace(InputDeviceUsageSource::STYLUS_INDIRECT); + continue; + } + } + if (isFromSource(motionArgs.source, AINPUT_SOURCE_MOUSE_RELATIVE) && + toolType == ToolType::MOUSE) { + sources.emplace(InputDeviceUsageSource::MOUSE_CAPTURED); + continue; + } + if (isFromSource(motionArgs.source, AINPUT_SOURCE_TOUCHPAD) && + toolType == ToolType::FINGER) { + sources.emplace(InputDeviceUsageSource::TOUCHPAD_CAPTURED); + continue; + } + if (isFromSource(motionArgs.source, AINPUT_SOURCE_BLUETOOTH_STYLUS) && + isStylusToolType(toolType)) { + sources.emplace(InputDeviceUsageSource::STYLUS_FUSED); + continue; + } + if (isFromSource(motionArgs.source, AINPUT_SOURCE_STYLUS) && isStylusToolType(toolType)) { + sources.emplace(InputDeviceUsageSource::STYLUS_DIRECT); + continue; + } + if (isFromSource(motionArgs.source, AINPUT_SOURCE_TOUCH_NAVIGATION)) { + sources.emplace(InputDeviceUsageSource::TOUCH_NAVIGATION); + continue; + } + if (isFromSource(motionArgs.source, AINPUT_SOURCE_JOYSTICK)) { + sources.emplace(InputDeviceUsageSource::JOYSTICK); + continue; + } + if (isFromSource(motionArgs.source, AINPUT_SOURCE_ROTARY_ENCODER)) { + sources.emplace(InputDeviceUsageSource::ROTARY_ENCODER); + continue; + } + if (isFromSource(motionArgs.source, AINPUT_SOURCE_TRACKBALL)) { + sources.emplace(InputDeviceUsageSource::TRACKBALL); + continue; + } + if (isFromSource(motionArgs.source, AINPUT_SOURCE_TOUCHSCREEN)) { + sources.emplace(InputDeviceUsageSource::TOUCHSCREEN); + continue; + } + sources.emplace(InputDeviceUsageSource::UNKNOWN); + } + + return sources; +} + +// --- InputDeviceMetricsCollector --- + InputDeviceMetricsCollector::InputDeviceMetricsCollector(InputListenerInterface& listener) - : mNextListener(listener){}; + : InputDeviceMetricsCollector(listener, sStatsdLogger, DEFAULT_USAGE_SESSION_TIMEOUT) {} + +InputDeviceMetricsCollector::InputDeviceMetricsCollector(InputListenerInterface& listener, + InputDeviceMetricsLogger& logger, + nanoseconds usageSessionTimeout) + : mNextListener(listener), mLogger(logger), mUsageSessionTimeout(usageSessionTimeout) {} void InputDeviceMetricsCollector::notifyInputDevicesChanged( const NotifyInputDevicesChangedArgs& args) { + reportCompletedSessions(); + onInputDevicesChanged(args.inputDeviceInfos); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyConfigurationChanged( const NotifyConfigurationChangedArgs& args) { + reportCompletedSessions(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyKey(const NotifyKeyArgs& args) { + reportCompletedSessions(); + const SourceProvider getSources = [&args](const InputDeviceInfo& info) { + return std::set{getUsageSourceForKeyArgs(info, args)}; + }; + onInputDeviceUsage(DeviceId{args.deviceId}, nanoseconds(args.eventTime), getSources); + mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyMotion(const NotifyMotionArgs& args) { + reportCompletedSessions(); + onInputDeviceUsage(DeviceId{args.deviceId}, nanoseconds(args.eventTime), + [&args](const auto&) { return getUsageSourcesForMotionArgs(args); }); + mNextListener.notify(args); } void InputDeviceMetricsCollector::notifySwitch(const NotifySwitchArgs& args) { + reportCompletedSessions(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifySensor(const NotifySensorArgs& args) { + reportCompletedSessions(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyVibratorState(const NotifyVibratorStateArgs& args) { + reportCompletedSessions(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyDeviceReset(const NotifyDeviceResetArgs& args) { + reportCompletedSessions(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyPointerCaptureChanged( const NotifyPointerCaptureChangedArgs& args) { + reportCompletedSessions(); mNextListener.notify(args); } void InputDeviceMetricsCollector::dump(std::string& dump) { dump += "InputDeviceMetricsCollector:\n"; + + dump += " Logged device IDs: " + dumpMapKeys(mLoggedDeviceInfos, &toString) + "\n"; + dump += " Devices with active usage sessions: " + + dumpMapKeys(mActiveUsageSessions, &toString) + "\n"; +} + +void InputDeviceMetricsCollector::onInputDevicesChanged(const std::vector<InputDeviceInfo>& infos) { + std::map<DeviceId, InputDeviceInfo> newDeviceInfos; + + for (const InputDeviceInfo& info : infos) { + if (isIgnoredInputDeviceId(info.getId())) { + continue; + } + newDeviceInfos.emplace(info.getId(), info); + } + + for (auto [deviceId, info] : mLoggedDeviceInfos) { + if (newDeviceInfos.count(deviceId) != 0) { + continue; + } + onInputDeviceRemoved(deviceId, info.getIdentifier()); + } + + std::swap(newDeviceInfos, mLoggedDeviceInfos); +} + +void InputDeviceMetricsCollector::onInputDeviceRemoved(DeviceId deviceId, + const InputDeviceIdentifier& identifier) { + auto it = mActiveUsageSessions.find(deviceId); + if (it == mActiveUsageSessions.end()) { + return; + } + // Report usage for that device if there is an active session. + auto& [_, activeSession] = *it; + mLogger.logInputDeviceUsageReported(identifier, activeSession.finishSession()); + mActiveUsageSessions.erase(it); + + // We don't remove this from mLoggedDeviceInfos because it will be updated in + // onInputDevicesChanged(). +} + +void InputDeviceMetricsCollector::onInputDeviceUsage(DeviceId deviceId, nanoseconds eventTime, + const SourceProvider& getSources) { + auto infoIt = mLoggedDeviceInfos.find(deviceId); + if (infoIt == mLoggedDeviceInfos.end()) { + // Do not track usage for devices that are not logged. + return; + } + + auto [sessionIt, _] = + mActiveUsageSessions.try_emplace(deviceId, mUsageSessionTimeout, eventTime); + for (InputDeviceUsageSource source : getSources(infoIt->second)) { + sessionIt->second.recordUsage(eventTime, source); + } +} + +void InputDeviceMetricsCollector::reportCompletedSessions() { + const auto currentTime = mLogger.getCurrentTime(); + + std::vector<DeviceId> completedUsageSessions; + + for (auto& [deviceId, activeSession] : mActiveUsageSessions) { + if (activeSession.checkIfCompletedAt(currentTime)) { + completedUsageSessions.emplace_back(deviceId); + } + } + + for (DeviceId deviceId : completedUsageSessions) { + const auto infoIt = mLoggedDeviceInfos.find(deviceId); + LOG_ALWAYS_FATAL_IF(infoIt == mLoggedDeviceInfos.end()); + + auto activeSessionIt = mActiveUsageSessions.find(deviceId); + LOG_ALWAYS_FATAL_IF(activeSessionIt == mActiveUsageSessions.end()); + auto& [_, activeSession] = *activeSessionIt; + mLogger.logInputDeviceUsageReported(infoIt->second.getIdentifier(), + activeSession.finishSession()); + mActiveUsageSessions.erase(activeSessionIt); + } +} + +// --- InputDeviceMetricsCollector::ActiveSession --- + +InputDeviceMetricsCollector::ActiveSession::ActiveSession(nanoseconds usageSessionTimeout, + nanoseconds startTime) + : mUsageSessionTimeout(usageSessionTimeout), mDeviceSession({startTime, startTime}) {} + +void InputDeviceMetricsCollector::ActiveSession::recordUsage(nanoseconds eventTime, + InputDeviceUsageSource source) { + // We assume that event times for subsequent events are always monotonically increasing for each + // input device. + auto [activeSourceIt, inserted] = + mActiveSessionsBySource.try_emplace(source, eventTime, eventTime); + if (!inserted) { + activeSourceIt->second.end = eventTime; + } + mDeviceSession.end = eventTime; +} + +bool InputDeviceMetricsCollector::ActiveSession::checkIfCompletedAt(nanoseconds timestamp) { + const auto sessionExpiryTime = timestamp - mUsageSessionTimeout; + std::vector<InputDeviceUsageSource> completedSourceSessionsForDevice; + for (auto& [source, session] : mActiveSessionsBySource) { + if (session.end <= sessionExpiryTime) { + completedSourceSessionsForDevice.emplace_back(source); + } + } + for (InputDeviceUsageSource source : completedSourceSessionsForDevice) { + auto it = mActiveSessionsBySource.find(source); + const auto& [_, session] = *it; + mSourceUsageBreakdown.emplace_back(source, session.end - session.start); + mActiveSessionsBySource.erase(it); + } + return mActiveSessionsBySource.empty(); +} + +InputDeviceMetricsLogger::DeviceUsageReport +InputDeviceMetricsCollector::ActiveSession::finishSession() { + const auto deviceUsageDuration = mDeviceSession.end - mDeviceSession.start; + + for (const auto& [source, sourceSession] : mActiveSessionsBySource) { + mSourceUsageBreakdown.emplace_back(source, sourceSession.end - sourceSession.start); + } + mActiveSessionsBySource.clear(); + + return {deviceUsageDuration, mSourceUsageBreakdown}; } } // namespace android diff --git a/services/inputflinger/InputDeviceMetricsCollector.h b/services/inputflinger/InputDeviceMetricsCollector.h index c95907540a..e2e79e45ee 100644 --- a/services/inputflinger/InputDeviceMetricsCollector.h +++ b/services/inputflinger/InputDeviceMetricsCollector.h @@ -17,6 +17,16 @@ #pragma once #include "InputListener.h" +#include "NotifyArgs.h" + +#include <ftl/mixins.h> +#include <input/InputDevice.h> +#include <statslog.h> +#include <chrono> +#include <functional> +#include <map> +#include <set> +#include <vector> namespace android { @@ -34,11 +44,73 @@ public: virtual void dump(std::string& dump) = 0; }; +/** + * Enum representation of the InputDeviceUsageSource. + */ +enum class InputDeviceUsageSource : int32_t { + UNKNOWN = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__UNKNOWN, + BUTTONS = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__BUTTONS, + KEYBOARD = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__KEYBOARD, + DPAD = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__DPAD, + GAMEPAD = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__GAMEPAD, + JOYSTICK = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__JOYSTICK, + MOUSE = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__MOUSE, + MOUSE_CAPTURED = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__MOUSE_CAPTURED, + TOUCHPAD = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TOUCHPAD, + TOUCHPAD_CAPTURED = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TOUCHPAD_CAPTURED, + ROTARY_ENCODER = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__ROTARY_ENCODER, + STYLUS_DIRECT = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__STYLUS_DIRECT, + STYLUS_INDIRECT = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__STYLUS_INDIRECT, + STYLUS_FUSED = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__STYLUS_FUSED, + TOUCH_NAVIGATION = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TOUCH_NAVIGATION, + TOUCHSCREEN = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TOUCHSCREEN, + TRACKBALL = util::INPUT_DEVICE_USAGE_REPORTED__USAGE_SOURCES__TRACKBALL, + + ftl_first = UNKNOWN, + ftl_last = TRACKBALL, +}; + +/** Returns the InputDeviceUsageSource that corresponds to the key event. */ +InputDeviceUsageSource getUsageSourceForKeyArgs(const InputDeviceInfo&, const NotifyKeyArgs&); + +/** Returns the InputDeviceUsageSources that correspond to the motion event. */ +std::set<InputDeviceUsageSource> getUsageSourcesForMotionArgs(const NotifyMotionArgs&); + +/** The logging interface for the metrics collector, injected for testing. */ +class InputDeviceMetricsLogger { +public: + virtual std::chrono::nanoseconds getCurrentTime() = 0; + + // Describes the breakdown of an input device usage session by its usage sources. + // An input device can have more than one usage source. For example, some game controllers have + // buttons, joysticks, and touchpads. We track usage by these sources to get a better picture of + // the device usage. The source breakdown of a 10 minute usage session could look like this: + // { {GAMEPAD, <9 mins>}, {TOUCHPAD, <2 mins>}, {TOUCHPAD, <3 mins>} } + // This would indicate that the GAMEPAD source was used first, and that source usage session + // lasted for 9 mins. During that time, the TOUCHPAD was used for 2 mins, until its source + // usage session expired. The TOUCHPAD was then used again later for another 3 mins. + using SourceUsageBreakdown = + std::vector<std::pair<InputDeviceUsageSource, std::chrono::nanoseconds /*duration*/>>; + + struct DeviceUsageReport { + std::chrono::nanoseconds usageDuration; + SourceUsageBreakdown sourceBreakdown; + }; + + virtual void logInputDeviceUsageReported(const InputDeviceIdentifier&, + const DeviceUsageReport&) = 0; + virtual ~InputDeviceMetricsLogger() = default; +}; + class InputDeviceMetricsCollector : public InputDeviceMetricsCollectorInterface { public: explicit InputDeviceMetricsCollector(InputListenerInterface& listener); ~InputDeviceMetricsCollector() override = default; + // Test constructor + InputDeviceMetricsCollector(InputListenerInterface& listener, InputDeviceMetricsLogger& logger, + std::chrono::nanoseconds usageSessionTimeout); + void notifyInputDevicesChanged(const NotifyInputDevicesChangedArgs& args) override; void notifyConfigurationChanged(const NotifyConfigurationChangedArgs& args) override; void notifyKey(const NotifyKeyArgs& args) override; @@ -53,6 +125,51 @@ public: private: InputListenerInterface& mNextListener; + InputDeviceMetricsLogger& mLogger; + const std::chrono::nanoseconds mUsageSessionTimeout; + + // Type-safe wrapper for input device id. + struct DeviceId : ftl::Constructible<DeviceId, std::int32_t>, + ftl::Equatable<DeviceId>, + ftl::Orderable<DeviceId> { + using Constructible::Constructible; + }; + static inline std::string toString(const DeviceId& id) { + return std::to_string(ftl::to_underlying(id)); + } + + std::map<DeviceId, InputDeviceInfo> mLoggedDeviceInfos; + + class ActiveSession { + public: + explicit ActiveSession(std::chrono::nanoseconds usageSessionTimeout, + std::chrono::nanoseconds startTime); + void recordUsage(std::chrono::nanoseconds eventTime, InputDeviceUsageSource source); + bool checkIfCompletedAt(std::chrono::nanoseconds timestamp); + InputDeviceMetricsLogger::DeviceUsageReport finishSession(); + + private: + struct UsageSession { + std::chrono::nanoseconds start{}; + std::chrono::nanoseconds end{}; + }; + + const std::chrono::nanoseconds mUsageSessionTimeout; + UsageSession mDeviceSession{}; + + std::map<InputDeviceUsageSource, UsageSession> mActiveSessionsBySource{}; + InputDeviceMetricsLogger::SourceUsageBreakdown mSourceUsageBreakdown{}; + }; + + // The input devices that currently have active usage sessions. + std::map<DeviceId, ActiveSession> mActiveUsageSessions; + + void onInputDevicesChanged(const std::vector<InputDeviceInfo>& infos); + void onInputDeviceRemoved(DeviceId deviceId, const InputDeviceIdentifier& identifier); + using SourceProvider = std::function<std::set<InputDeviceUsageSource>(const InputDeviceInfo&)>; + void onInputDeviceUsage(DeviceId deviceId, std::chrono::nanoseconds eventTime, + const SourceProvider& getSources); + void reportCompletedSessions(); }; } // namespace android diff --git a/services/inputflinger/InputProcessor.cpp b/services/inputflinger/InputProcessor.cpp index 7a84be93b1..6dd267ce8f 100644 --- a/services/inputflinger/InputProcessor.cpp +++ b/services/inputflinger/InputProcessor.cpp @@ -322,7 +322,7 @@ void MotionClassifier::reset() { void MotionClassifier::reset(const NotifyDeviceResetArgs& args) { int32_t deviceId = args.deviceId; // Clear the pending events right away, to avoid unnecessary work done by the HAL. - mEvents.erase([deviceId](const ClassifierEvent& event) { + mEvents.erase_if([deviceId](const ClassifierEvent& event) { std::optional<int32_t> eventDeviceId = event.getDeviceId(); return eventDeviceId && (*eventDeviceId == deviceId); }); diff --git a/services/inputflinger/SyncQueue.h b/services/inputflinger/SyncQueue.h new file mode 100644 index 0000000000..62efd5598a --- /dev/null +++ b/services/inputflinger/SyncQueue.h @@ -0,0 +1,53 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <utils/threads.h> +#include <list> +#include <mutex> +#include <optional> + +namespace android { + +/** A thread-safe FIFO queue. */ +template <class T> +class SyncQueue { +public: + /** Retrieve and remove the oldest object. Returns std::nullopt if the queue is empty. */ + std::optional<T> pop() { + std::scoped_lock lock(mLock); + if (mQueue.empty()) { + return {}; + } + T t = std::move(mQueue.front()); + mQueue.erase(mQueue.begin()); + return t; + }; + + /** Add a new object to the queue. */ + template <class... Args> + void push(Args&&... args) { + std::scoped_lock lock(mLock); + mQueue.emplace_back(args...); + }; + +private: + std::mutex mLock; + std::list<T> mQueue GUARDED_BY(mLock); +}; + +} // namespace android diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp index bdd45dc9b4..0cc7cfbcc8 100644 --- a/services/inputflinger/dispatcher/InputDispatcher.cpp +++ b/services/inputflinger/dispatcher/InputDispatcher.cpp @@ -5659,14 +5659,6 @@ void InputDispatcher::dumpDispatchStateLocked(std::string& dump) const { } else { dump += INDENT "Displays: <none>\n"; } - dump += INDENT "Window Infos:\n"; - dump += StringPrintf(INDENT2 "vsync id: %" PRId64 "\n", mWindowInfosVsyncId); - dump += StringPrintf(INDENT2 "timestamp (ns): %" PRId64 "\n", mWindowInfosTimestamp); - dump += "\n"; - dump += StringPrintf(INDENT2 "max update delay (ns): %" PRId64 "\n", mMaxWindowInfosDelay); - dump += StringPrintf(INDENT2 "max update delay vsync id: %" PRId64 "\n", - mMaxWindowInfosDelayVsyncId); - dump += "\n"; if (!mGlobalMonitorsByDisplay.empty()) { for (const auto& [displayId, monitors] : mGlobalMonitorsByDisplay) { @@ -6708,15 +6700,6 @@ void InputDispatcher::onWindowInfosChanged(const gui::WindowInfosUpdate& update) for (const auto& [displayId, handles] : handlesPerDisplay) { setInputWindowsLocked(handles, displayId); } - - mWindowInfosVsyncId = update.vsyncId; - mWindowInfosTimestamp = update.timestamp; - - int64_t delay = systemTime() - update.timestamp; - if (delay > mMaxWindowInfosDelay) { - mMaxWindowInfosDelay = delay; - mMaxWindowInfosDelayVsyncId = update.vsyncId; - } } // Wake up poll loop since it may need to make new input dispatching choices. mLooper->wake(); diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h index 0e9cfeffe4..8ca01b7a09 100644 --- a/services/inputflinger/dispatcher/InputDispatcher.h +++ b/services/inputflinger/dispatcher/InputDispatcher.h @@ -204,11 +204,6 @@ private: const IdGenerator mIdGenerator; - int64_t mWindowInfosVsyncId GUARDED_BY(mLock); - int64_t mWindowInfosTimestamp GUARDED_BY(mLock); - int64_t mMaxWindowInfosDelay GUARDED_BY(mLock) = -1; - int64_t mMaxWindowInfosDelayVsyncId GUARDED_BY(mLock) = -1; - // With each iteration, InputDispatcher nominally processes one queued event, // a timeout, or a response from an input consumer. // This method should only be called on the input dispatcher's own thread. diff --git a/services/inputflinger/include/KeyCodeClassifications.h b/services/inputflinger/include/KeyCodeClassifications.h new file mode 100644 index 0000000000..a09b02e17b --- /dev/null +++ b/services/inputflinger/include/KeyCodeClassifications.h @@ -0,0 +1,55 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <android/input.h> +#include <set> + +namespace android { + +/** The set of all Android key codes that are required for a device to be classified as a D-pad. */ +static const std::set<int32_t> DPAD_REQUIRED_KEYCODES = { + AKEYCODE_DPAD_UP, AKEYCODE_DPAD_DOWN, AKEYCODE_DPAD_LEFT, + AKEYCODE_DPAD_RIGHT, AKEYCODE_DPAD_CENTER, +}; + +/** The set of all Android key codes that correspond to D-pad keys. */ +static const std::set<int32_t> DPAD_ALL_KEYCODES = { + AKEYCODE_DPAD_UP, AKEYCODE_DPAD_DOWN, AKEYCODE_DPAD_LEFT, + AKEYCODE_DPAD_RIGHT, AKEYCODE_DPAD_CENTER, AKEYCODE_DPAD_UP_LEFT, + AKEYCODE_DPAD_UP_RIGHT, AKEYCODE_DPAD_DOWN_LEFT, AKEYCODE_DPAD_DOWN_RIGHT, +}; + +/** The set of all Android key codes that correspond to gamepad buttons. */ +static const std::set<int32_t> GAMEPAD_KEYCODES = { + AKEYCODE_BUTTON_A, AKEYCODE_BUTTON_B, AKEYCODE_BUTTON_C, // + AKEYCODE_BUTTON_X, AKEYCODE_BUTTON_Y, AKEYCODE_BUTTON_Z, // + AKEYCODE_BUTTON_L1, AKEYCODE_BUTTON_R1, // + AKEYCODE_BUTTON_L2, AKEYCODE_BUTTON_R2, // + AKEYCODE_BUTTON_THUMBL, AKEYCODE_BUTTON_THUMBR, // + AKEYCODE_BUTTON_START, AKEYCODE_BUTTON_SELECT, AKEYCODE_BUTTON_MODE, // +}; + +/** The set of all Android key codes that correspond to buttons (bit-switches) on a stylus. */ +static const std::set<int32_t> STYLUS_BUTTON_KEYCODES = { + AKEYCODE_STYLUS_BUTTON_PRIMARY, + AKEYCODE_STYLUS_BUTTON_SECONDARY, + AKEYCODE_STYLUS_BUTTON_TERTIARY, + AKEYCODE_STYLUS_BUTTON_TAIL, +}; + +} // namespace android diff --git a/services/inputflinger/reader/EventHub.cpp b/services/inputflinger/reader/EventHub.cpp index 0354164155..04747cce83 100644 --- a/services/inputflinger/reader/EventHub.cpp +++ b/services/inputflinger/reader/EventHub.cpp @@ -58,6 +58,8 @@ #include "EventHub.h" +#include "KeyCodeClassifications.h" + #define INDENT " " #define INDENT2 " " #define INDENT3 " " @@ -189,14 +191,6 @@ static std::string sha1(const std::string& in) { return out; } -/* The set of all Android key codes that correspond to buttons (bit-switches) on a stylus. */ -static constexpr std::array<int32_t, 4> STYLUS_BUTTON_KEYCODES = { - AKEYCODE_STYLUS_BUTTON_PRIMARY, - AKEYCODE_STYLUS_BUTTON_SECONDARY, - AKEYCODE_STYLUS_BUTTON_TERTIARY, - AKEYCODE_STYLUS_BUTTON_TAIL, -}; - /** * Return true if name matches "v4l-touch*" */ @@ -2060,15 +2054,6 @@ void EventHub::scanDevicesLocked() { // ---------------------------------------------------------------------------- -static const int32_t GAMEPAD_KEYCODES[] = { - AKEYCODE_BUTTON_A, AKEYCODE_BUTTON_B, AKEYCODE_BUTTON_C, // - AKEYCODE_BUTTON_X, AKEYCODE_BUTTON_Y, AKEYCODE_BUTTON_Z, // - AKEYCODE_BUTTON_L1, AKEYCODE_BUTTON_R1, // - AKEYCODE_BUTTON_L2, AKEYCODE_BUTTON_R2, // - AKEYCODE_BUTTON_THUMBL, AKEYCODE_BUTTON_THUMBR, // - AKEYCODE_BUTTON_START, AKEYCODE_BUTTON_SELECT, AKEYCODE_BUTTON_MODE, // -}; - status_t EventHub::registerFdForEpoll(int fd) { // TODO(b/121395353) - consider adding EPOLLRDHUP struct epoll_event eventItem = {}; @@ -2391,31 +2376,23 @@ void EventHub::openDeviceLocked(const std::string& devicePath) { device->classes |= InputDeviceClass::ALPHAKEY; } - // See if this device has a DPAD. - if (device->hasKeycodeLocked(AKEYCODE_DPAD_UP) && - device->hasKeycodeLocked(AKEYCODE_DPAD_DOWN) && - device->hasKeycodeLocked(AKEYCODE_DPAD_LEFT) && - device->hasKeycodeLocked(AKEYCODE_DPAD_RIGHT) && - device->hasKeycodeLocked(AKEYCODE_DPAD_CENTER)) { + // See if this device has a D-pad. + if (std::all_of(DPAD_REQUIRED_KEYCODES.begin(), DPAD_REQUIRED_KEYCODES.end(), + [&](int32_t keycode) { return device->hasKeycodeLocked(keycode); })) { device->classes |= InputDeviceClass::DPAD; } // See if this device has a gamepad. - for (size_t i = 0; i < sizeof(GAMEPAD_KEYCODES) / sizeof(GAMEPAD_KEYCODES[0]); i++) { - if (device->hasKeycodeLocked(GAMEPAD_KEYCODES[i])) { - device->classes |= InputDeviceClass::GAMEPAD; - break; - } + if (std::any_of(GAMEPAD_KEYCODES.begin(), GAMEPAD_KEYCODES.end(), + [&](int32_t keycode) { return device->hasKeycodeLocked(keycode); })) { + device->classes |= InputDeviceClass::GAMEPAD; } // See if this device has any stylus buttons that we would want to fuse with touch data. - if (!device->classes.any(InputDeviceClass::TOUCH | InputDeviceClass::TOUCH_MT)) { - for (int32_t keycode : STYLUS_BUTTON_KEYCODES) { - if (device->hasKeycodeLocked(keycode)) { - device->classes |= InputDeviceClass::EXTERNAL_STYLUS; - break; - } - } + if (!device->classes.any(InputDeviceClass::TOUCH | InputDeviceClass::TOUCH_MT) && + std::any_of(STYLUS_BUTTON_KEYCODES.begin(), STYLUS_BUTTON_KEYCODES.end(), + [&](int32_t keycode) { return device->hasKeycodeLocked(keycode); })) { + device->classes |= InputDeviceClass::EXTERNAL_STYLUS; } } diff --git a/services/inputflinger/reader/controller/PeripheralController.cpp b/services/inputflinger/reader/controller/PeripheralController.cpp index a380b5eadf..eabf591dbf 100644 --- a/services/inputflinger/reader/controller/PeripheralController.cpp +++ b/services/inputflinger/reader/controller/PeripheralController.cpp @@ -16,8 +16,10 @@ #include <locale> #include <regex> -#include <set> +#include <sstream> +#include <string> +#include <android/sysprop/InputProperties.sysprop.h> #include <ftl/enum.h> #include "../Macros.h" @@ -45,6 +47,10 @@ static inline int32_t toArgb(int32_t brightness, int32_t red, int32_t green, int return (brightness & 0xff) << 24 | (red & 0xff) << 16 | (green & 0xff) << 8 | (blue & 0xff); } +static inline bool isKeyboardBacklightCustomLevelsEnabled() { + return sysprop::InputProperties::enable_keyboard_backlight_custom_levels().value_or(true); +} + /** * Input controller owned by InputReader device, implements the native API for querying input * lights, getting and setting the lights brightness and color, by interacting with EventHub @@ -272,11 +278,43 @@ void PeripheralController::populateDeviceInfo(InputDeviceInfo* deviceInfo) { for (const auto& [lightId, light] : mLights) { // Input device light doesn't support ordinal, always pass 1. InputDeviceLightInfo lightInfo(light->name, light->id, light->type, light->capabilityFlags, - /*ordinal=*/1); + /*ordinal=*/1, getPreferredBrightnessLevels(light.get())); deviceInfo->addLightInfo(lightInfo); } } +// TODO(b/281822656): Move to constructor and add as a parameter to avoid parsing repeatedly. +// Need to change lifecycle of Peripheral controller so that Input device configuration map is +// available at construction time before moving this logic to constructor. +std::set<BrightnessLevel> PeripheralController::getPreferredBrightnessLevels( + const Light* light) const { + std::set<BrightnessLevel> levels; + if (!isKeyboardBacklightCustomLevelsEnabled() || + light->type != InputDeviceLightType::KEYBOARD_BACKLIGHT) { + return levels; + } + std::optional<std::string> keyboardBacklightLevels = + mDeviceContext.getConfiguration().getString("keyboard.backlight.brightnessLevels"); + if (!keyboardBacklightLevels) { + return levels; + } + std::stringstream ss(*keyboardBacklightLevels); + while (ss.good()) { + std::string substr; + std::getline(ss, substr, ','); + char* end; + int32_t value = static_cast<int32_t>(strtol(substr.c_str(), &end, 10)); + if (*end != '\0' || value < 0 || value > 255) { + ALOGE("Error parsing keyboard backlight brightness levels, provided levels = %s", + keyboardBacklightLevels->c_str()); + levels.clear(); + break; + } + levels.insert(BrightnessLevel(value)); + } + return levels; +} + void PeripheralController::dump(std::string& dump) { dump += INDENT2 "Input Controller:\n"; if (!mLights.empty()) { @@ -550,5 +588,4 @@ std::optional<int32_t> PeripheralController::getLightPlayerId(int32_t lightId) { int32_t PeripheralController::getEventHubId() const { return getDeviceContext().getEventHubId(); } - } // namespace android diff --git a/services/inputflinger/reader/controller/PeripheralController.h b/services/inputflinger/reader/controller/PeripheralController.h index 8ac42c3792..07ade7c931 100644 --- a/services/inputflinger/reader/controller/PeripheralController.h +++ b/services/inputflinger/reader/controller/PeripheralController.h @@ -76,6 +76,7 @@ private: virtual void dump(std::string& dump) {} + void configureSuggestedBrightnessLevels(); std::optional<std::int32_t> getRawLightBrightness(int32_t rawLightId); void setRawLightBrightness(int32_t rawLightId, int32_t brightness); }; @@ -152,6 +153,8 @@ private: // Battery map from battery ID to battery std::unordered_map<int32_t, std::unique_ptr<Battery>> mBatteries; + + std::set<BrightnessLevel> getPreferredBrightnessLevels(const Light* light) const; }; } // namespace android diff --git a/services/inputflinger/reader/include/InputDevice.h b/services/inputflinger/reader/include/InputDevice.h index 0b8a608891..2f8e5bd6cf 100644 --- a/services/inputflinger/reader/include/InputDevice.h +++ b/services/inputflinger/reader/include/InputDevice.h @@ -74,7 +74,7 @@ public: } inline bool hasMic() const { return mHasMic; } - inline bool isIgnored() { return !getMapperCount(); } + inline bool isIgnored() { return !getMapperCount() && !mController; } bool isEnabled(); [[nodiscard]] std::list<NotifyArgs> setEnabled(bool enabled, nsecs_t when); diff --git a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp index 7eca6fa0b4..108882121d 100644 --- a/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp +++ b/services/inputflinger/reader/mapper/gestures/GestureConverter.cpp @@ -385,6 +385,8 @@ NotifyMotionArgs GestureConverter::endScroll(nsecs_t when, nsecs_t readTime) { } mDownTime = when; + mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT, + fingerCount); out.push_back(makeMotionArgs(when, readTime, AMOTION_EVENT_ACTION_DOWN, /* actionButton= */ 0, mButtonState, /* pointerCount= */ 1, mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition, @@ -441,6 +443,7 @@ NotifyMotionArgs GestureConverter::endScroll(nsecs_t when, nsecs_t readTime) { /* actionButton= */ 0, mButtonState, /* pointerCount= */ 1, mFingerProps.data(), mFakeFingerCoords.data(), xCursorPosition, yCursorPosition)); + mFakeFingerCoords[0].setAxisValue(AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT, 0); mCurrentClassification = MotionClassification::NONE; mSwipeFingerCount = 0; return out; diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp index 52277ff078..6438a144cd 100644 --- a/services/inputflinger/tests/Android.bp +++ b/services/inputflinger/tests/Android.bp @@ -40,6 +40,7 @@ cc_test { "AnrTracker_test.cpp", "BlockingQueue_test.cpp", "CapturedTouchpadEventConverter_test.cpp", + "CursorInputMapper_test.cpp", "EventHub_test.cpp", "FakeEventHub.cpp", "FakeInputReaderPolicy.cpp", @@ -47,6 +48,7 @@ cc_test { "FocusResolver_test.cpp", "GestureConverter_test.cpp", "HardwareStateConverter_test.cpp", + "InputDeviceMetricsCollector_test.cpp", "InputMapperTest.cpp", "InputProcessor_test.cpp", "InputProcessorConverter_test.cpp", @@ -57,7 +59,9 @@ cc_test { "NotifyArgs_test.cpp", "PreferStylusOverTouch_test.cpp", "PropertyProvider_test.cpp", + "SyncQueue_test.cpp", "TestInputListener.cpp", + "TouchpadInputMapper_test.cpp", "UinputDevice.cpp", "UnwantedInteractionBlocker_test.cpp", ], diff --git a/services/inputflinger/tests/BlockingQueue_test.cpp b/services/inputflinger/tests/BlockingQueue_test.cpp index fd9d9d5bd3..754a5c451e 100644 --- a/services/inputflinger/tests/BlockingQueue_test.cpp +++ b/services/inputflinger/tests/BlockingQueue_test.cpp @@ -22,6 +22,7 @@ namespace android { +using std::chrono_literals::operator""ns; // --- BlockingQueueTest --- @@ -34,6 +35,14 @@ TEST(BlockingQueueTest, Queue_AddAndRemove) { ASSERT_TRUE(queue.push(1)); ASSERT_EQ(queue.pop(), 1); + + ASSERT_TRUE(queue.emplace(2)); + ASSERT_EQ(queue.popWithTimeout(0ns), 2); + + ASSERT_TRUE(queue.push(3)); + ASSERT_EQ(queue.popWithTimeout(100ns), 3); + + ASSERT_EQ(std::nullopt, queue.popWithTimeout(0ns)); } /** @@ -87,7 +96,7 @@ TEST(BlockingQueueTest, Queue_Erases) { queue.push(3); queue.push(4); // Erase elements 2 and 4 - queue.erase([](int element) { return element == 2 || element == 4; }); + queue.erase_if([](int element) { return element == 2 || element == 4; }); // Should no longer receive elements 2 and 4 ASSERT_EQ(1, queue.pop()); ASSERT_EQ(3, queue.pop()); @@ -138,5 +147,9 @@ TEST(BlockingQueueTest, Queue_BlocksWhileWaitingForElements) { ASSERT_TRUE(hasReceivedElement); } +TEST(BlockingQueueTest, Queue_TimesOut) { + BlockingQueue<int> queue; + ASSERT_EQ(std::nullopt, queue.popWithTimeout(1ns)); +} } // namespace android diff --git a/services/inputflinger/tests/CursorInputMapper_test.cpp b/services/inputflinger/tests/CursorInputMapper_test.cpp new file mode 100644 index 0000000000..6774b1793f --- /dev/null +++ b/services/inputflinger/tests/CursorInputMapper_test.cpp @@ -0,0 +1,105 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CursorInputMapper.h" + +#include <android-base/logging.h> +#include <gtest/gtest.h> + +#include "FakePointerController.h" +#include "InputMapperTest.h" +#include "InterfaceMocks.h" +#include "TestInputListenerMatchers.h" + +#define TAG "CursorInputMapper_test" + +namespace android { + +using testing::Return; +using testing::VariantWith; +constexpr auto ACTION_DOWN = AMOTION_EVENT_ACTION_DOWN; +constexpr auto ACTION_MOVE = AMOTION_EVENT_ACTION_MOVE; +constexpr auto ACTION_UP = AMOTION_EVENT_ACTION_UP; +constexpr auto BUTTON_PRESS = AMOTION_EVENT_ACTION_BUTTON_PRESS; +constexpr auto BUTTON_RELEASE = AMOTION_EVENT_ACTION_BUTTON_RELEASE; +constexpr auto HOVER_MOVE = AMOTION_EVENT_ACTION_HOVER_MOVE; + +/** + * Unit tests for CursorInputMapper. + * This class is named 'CursorInputMapperUnitTest' to avoid name collision with the existing + * 'CursorInputMapperTest'. If all of the CursorInputMapper tests are migrated here, the name + * can be simplified to 'CursorInputMapperTest'. + * TODO(b/283812079): move CursorInputMapper tests here. + */ +class CursorInputMapperUnitTest : public InputMapperUnitTest { +protected: + void SetUp() override { + InputMapperUnitTest::SetUp(); + + // Current scan code state - all keys are UP by default + setScanCodeState(KeyState::UP, + {BTN_LEFT, BTN_RIGHT, BTN_MIDDLE, BTN_BACK, BTN_SIDE, BTN_FORWARD, + BTN_EXTRA, BTN_TASK}); + EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_WHEEL)) + .WillRepeatedly(Return(false)); + EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_HWHEEL)) + .WillRepeatedly(Return(false)); + + EXPECT_CALL(mMockInputReaderContext, bumpGeneration()).WillRepeatedly(Return(1)); + + mMapper = createInputMapper<CursorInputMapper>(*mDeviceContext, mReaderConfiguration); + } +}; + +/** + * Move the mouse and then click the button. Check whether HOVER_EXIT is generated when hovering + * ends. Currently, it is not. + */ +TEST_F(CursorInputMapperUnitTest, HoverAndLeftButtonPress) { + std::list<NotifyArgs> args; + + // Move the cursor a little + args += process(EV_REL, REL_X, 10); + args += process(EV_REL, REL_Y, 20); + args += process(EV_SYN, SYN_REPORT, 0); + ASSERT_THAT(args, ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(HOVER_MOVE)))); + + // Now click the mouse button + args.clear(); + args += process(EV_KEY, BTN_LEFT, 1); + args += process(EV_SYN, SYN_REPORT, 0); + ASSERT_THAT(args, + ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_DOWN)), + VariantWith<NotifyMotionArgs>(WithMotionAction(BUTTON_PRESS)))); + + // Move some more. + args.clear(); + args += process(EV_REL, REL_X, 10); + args += process(EV_REL, REL_Y, 20); + args += process(EV_SYN, SYN_REPORT, 0); + ASSERT_THAT(args, ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_MOVE)))); + + // Release the button + args.clear(); + args += process(EV_KEY, BTN_LEFT, 0); + args += process(EV_SYN, SYN_REPORT, 0); + ASSERT_THAT(args, + ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(BUTTON_RELEASE)), + VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_UP)), + VariantWith<NotifyMotionArgs>(WithMotionAction(HOVER_MOVE)))); +} + +} // namespace android diff --git a/services/inputflinger/tests/EventBuilders.h b/services/inputflinger/tests/EventBuilders.h new file mode 100644 index 0000000000..606a57d942 --- /dev/null +++ b/services/inputflinger/tests/EventBuilders.h @@ -0,0 +1,362 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <NotifyArgs.h> +#include <android/input.h> +#include <attestation/HmacKeyManager.h> +#include <input/Input.h> +#include <vector> + +namespace android { + +// An arbitrary device id. +static constexpr uint32_t DEFAULT_DEVICE_ID = 1; + +// The default policy flags to use for event injection by tests. +static constexpr uint32_t DEFAULT_POLICY_FLAGS = POLICY_FLAG_FILTERED | POLICY_FLAG_PASS_TO_USER; + +class PointerBuilder { +public: + PointerBuilder(int32_t id, ToolType toolType) { + mProperties.clear(); + mProperties.id = id; + mProperties.toolType = toolType; + mCoords.clear(); + } + + PointerBuilder& x(float x) { return axis(AMOTION_EVENT_AXIS_X, x); } + + PointerBuilder& y(float y) { return axis(AMOTION_EVENT_AXIS_Y, y); } + + PointerBuilder& axis(int32_t axis, float value) { + mCoords.setAxisValue(axis, value); + return *this; + } + + PointerProperties buildProperties() const { return mProperties; } + + PointerCoords buildCoords() const { return mCoords; } + +private: + PointerProperties mProperties; + PointerCoords mCoords; +}; + +class MotionEventBuilder { +public: + MotionEventBuilder(int32_t action, int32_t source) { + mAction = action; + mSource = source; + mEventTime = systemTime(SYSTEM_TIME_MONOTONIC); + mDownTime = mEventTime; + } + + MotionEventBuilder& deviceId(int32_t deviceId) { + mDeviceId = deviceId; + return *this; + } + + MotionEventBuilder& downTime(nsecs_t downTime) { + mDownTime = downTime; + return *this; + } + + MotionEventBuilder& eventTime(nsecs_t eventTime) { + mEventTime = eventTime; + return *this; + } + + MotionEventBuilder& displayId(int32_t displayId) { + mDisplayId = displayId; + return *this; + } + + MotionEventBuilder& actionButton(int32_t actionButton) { + mActionButton = actionButton; + return *this; + } + + MotionEventBuilder& buttonState(int32_t buttonState) { + mButtonState = buttonState; + return *this; + } + + MotionEventBuilder& rawXCursorPosition(float rawXCursorPosition) { + mRawXCursorPosition = rawXCursorPosition; + return *this; + } + + MotionEventBuilder& rawYCursorPosition(float rawYCursorPosition) { + mRawYCursorPosition = rawYCursorPosition; + return *this; + } + + MotionEventBuilder& pointer(PointerBuilder pointer) { + mPointers.push_back(pointer); + return *this; + } + + MotionEventBuilder& addFlag(uint32_t flags) { + mFlags |= flags; + return *this; + } + + MotionEvent build() { + std::vector<PointerProperties> pointerProperties; + std::vector<PointerCoords> pointerCoords; + for (const PointerBuilder& pointer : mPointers) { + pointerProperties.push_back(pointer.buildProperties()); + pointerCoords.push_back(pointer.buildCoords()); + } + + // Set mouse cursor position for the most common cases to avoid boilerplate. + if (mSource == AINPUT_SOURCE_MOUSE && + !MotionEvent::isValidCursorPosition(mRawXCursorPosition, mRawYCursorPosition)) { + mRawXCursorPosition = pointerCoords[0].getX(); + mRawYCursorPosition = pointerCoords[0].getY(); + } + + MotionEvent event; + static const ui::Transform kIdentityTransform; + event.initialize(InputEvent::nextId(), mDeviceId, mSource, mDisplayId, INVALID_HMAC, + mAction, mActionButton, mFlags, /*edgeFlags=*/0, AMETA_NONE, mButtonState, + MotionClassification::NONE, kIdentityTransform, + /*xPrecision=*/0, /*yPrecision=*/0, mRawXCursorPosition, + mRawYCursorPosition, kIdentityTransform, mDownTime, mEventTime, + mPointers.size(), pointerProperties.data(), pointerCoords.data()); + return event; + } + +private: + int32_t mAction; + int32_t mDeviceId{DEFAULT_DEVICE_ID}; + int32_t mSource; + nsecs_t mDownTime; + nsecs_t mEventTime; + int32_t mDisplayId{ADISPLAY_ID_DEFAULT}; + int32_t mActionButton{0}; + int32_t mButtonState{0}; + int32_t mFlags{0}; + float mRawXCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION}; + float mRawYCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION}; + + std::vector<PointerBuilder> mPointers; +}; + +class MotionArgsBuilder { +public: + MotionArgsBuilder(int32_t action, int32_t source) { + mAction = action; + mSource = source; + mEventTime = systemTime(SYSTEM_TIME_MONOTONIC); + mDownTime = mEventTime; + } + + MotionArgsBuilder& deviceId(int32_t deviceId) { + mDeviceId = deviceId; + return *this; + } + + MotionArgsBuilder& downTime(nsecs_t downTime) { + mDownTime = downTime; + return *this; + } + + MotionArgsBuilder& eventTime(nsecs_t eventTime) { + mEventTime = eventTime; + return *this; + } + + MotionArgsBuilder& displayId(int32_t displayId) { + mDisplayId = displayId; + return *this; + } + + MotionArgsBuilder& policyFlags(int32_t policyFlags) { + mPolicyFlags = policyFlags; + return *this; + } + + MotionArgsBuilder& actionButton(int32_t actionButton) { + mActionButton = actionButton; + return *this; + } + + MotionArgsBuilder& buttonState(int32_t buttonState) { + mButtonState = buttonState; + return *this; + } + + MotionArgsBuilder& rawXCursorPosition(float rawXCursorPosition) { + mRawXCursorPosition = rawXCursorPosition; + return *this; + } + + MotionArgsBuilder& rawYCursorPosition(float rawYCursorPosition) { + mRawYCursorPosition = rawYCursorPosition; + return *this; + } + + MotionArgsBuilder& pointer(PointerBuilder pointer) { + mPointers.push_back(pointer); + return *this; + } + + MotionArgsBuilder& addFlag(uint32_t flags) { + mFlags |= flags; + return *this; + } + + MotionArgsBuilder& classification(MotionClassification classification) { + mClassification = classification; + return *this; + } + + NotifyMotionArgs build() { + std::vector<PointerProperties> pointerProperties; + std::vector<PointerCoords> pointerCoords; + for (const PointerBuilder& pointer : mPointers) { + pointerProperties.push_back(pointer.buildProperties()); + pointerCoords.push_back(pointer.buildCoords()); + } + + // Set mouse cursor position for the most common cases to avoid boilerplate. + if (mSource == AINPUT_SOURCE_MOUSE && + !MotionEvent::isValidCursorPosition(mRawXCursorPosition, mRawYCursorPosition)) { + mRawXCursorPosition = pointerCoords[0].getX(); + mRawYCursorPosition = pointerCoords[0].getY(); + } + + return {InputEvent::nextId(), + mEventTime, + /*readTime=*/mEventTime, + mDeviceId, + mSource, + mDisplayId, + mPolicyFlags, + mAction, + mActionButton, + mFlags, + AMETA_NONE, + mButtonState, + mClassification, + /*edgeFlags=*/0, + static_cast<uint32_t>(mPointers.size()), + pointerProperties.data(), + pointerCoords.data(), + /*xPrecision=*/0, + /*yPrecision=*/0, + mRawXCursorPosition, + mRawYCursorPosition, + mDownTime, + /*videoFrames=*/{}}; + } + +private: + int32_t mAction; + int32_t mDeviceId{DEFAULT_DEVICE_ID}; + uint32_t mSource; + nsecs_t mDownTime; + nsecs_t mEventTime; + int32_t mDisplayId{ADISPLAY_ID_DEFAULT}; + uint32_t mPolicyFlags = DEFAULT_POLICY_FLAGS; + int32_t mActionButton{0}; + int32_t mButtonState{0}; + int32_t mFlags{0}; + MotionClassification mClassification{MotionClassification::NONE}; + float mRawXCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION}; + float mRawYCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION}; + + std::vector<PointerBuilder> mPointers; +}; + +class KeyArgsBuilder { +public: + KeyArgsBuilder(int32_t action, int32_t source) { + mAction = action; + mSource = source; + mEventTime = systemTime(SYSTEM_TIME_MONOTONIC); + mDownTime = mEventTime; + } + + KeyArgsBuilder& deviceId(int32_t deviceId) { + mDeviceId = deviceId; + return *this; + } + + KeyArgsBuilder& downTime(nsecs_t downTime) { + mDownTime = downTime; + return *this; + } + + KeyArgsBuilder& eventTime(nsecs_t eventTime) { + mEventTime = eventTime; + return *this; + } + + KeyArgsBuilder& displayId(int32_t displayId) { + mDisplayId = displayId; + return *this; + } + + KeyArgsBuilder& policyFlags(int32_t policyFlags) { + mPolicyFlags = policyFlags; + return *this; + } + + KeyArgsBuilder& addFlag(uint32_t flags) { + mFlags |= flags; + return *this; + } + + KeyArgsBuilder& keyCode(int32_t keyCode) { + mKeyCode = keyCode; + return *this; + } + + NotifyKeyArgs build() const { + return {InputEvent::nextId(), + mEventTime, + /*readTime=*/mEventTime, + mDeviceId, + mSource, + mDisplayId, + mPolicyFlags, + mAction, + mFlags, + mKeyCode, + mScanCode, + mMetaState, + mDownTime}; + } + +private: + int32_t mAction; + int32_t mDeviceId = DEFAULT_DEVICE_ID; + uint32_t mSource; + nsecs_t mDownTime; + nsecs_t mEventTime; + int32_t mDisplayId{ADISPLAY_ID_DEFAULT}; + uint32_t mPolicyFlags = DEFAULT_POLICY_FLAGS; + int32_t mFlags{0}; + int32_t mKeyCode{AKEYCODE_UNKNOWN}; + int32_t mScanCode{0}; + int32_t mMetaState{AMETA_NONE}; +}; + +} // namespace android diff --git a/services/inputflinger/tests/GestureConverter_test.cpp b/services/inputflinger/tests/GestureConverter_test.cpp index f115fff128..a3994f08e0 100644 --- a/services/inputflinger/tests/GestureConverter_test.cpp +++ b/services/inputflinger/tests/GestureConverter_test.cpp @@ -395,7 +395,7 @@ TEST_F(GestureConverterTest, ThreeFingerSwipe_ClearsClassificationAfterGesture) WithMotionClassification(MotionClassification::NONE)); } -TEST_F(GestureConverterTest, ThreeFingerSwipe_ClearsOffsetsAfterGesture) { +TEST_F(GestureConverterTest, ThreeFingerSwipe_ClearsGestureAxesAfterGesture) { InputDeviceContext deviceContext(*mDevice, EVENTHUB_ID); GestureConverter converter(*mReader->getContext(), deviceContext, DEVICE_ID); @@ -412,7 +412,8 @@ TEST_F(GestureConverterTest, ThreeFingerSwipe_ClearsOffsetsAfterGesture) { GESTURES_ZOOM_START); args = converter.handleGesture(ARBITRARY_TIME, READ_TIME, pinchGesture); ASSERT_FALSE(args.empty()); - EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()), WithGestureOffset(0, 0, EPSILON)); + EXPECT_THAT(std::get<NotifyMotionArgs>(args.front()), + AllOf(WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(0))); } TEST_F(GestureConverterTest, ThreeFingerSwipe_Vertical) { @@ -433,6 +434,7 @@ TEST_F(GestureConverterTest, ThreeFingerSwipe_Vertical) { NotifyMotionArgs arg = std::get<NotifyMotionArgs>(args.front()); ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithGestureOffset(0, 0, EPSILON), + WithGestureSwipeFingerCount(3), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(1u), WithToolType(ToolType::FINGER))); PointerCoords finger0Start = arg.pointerCoords[0]; @@ -441,7 +443,7 @@ TEST_F(GestureConverterTest, ThreeFingerSwipe_Vertical) { ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN | 1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(3), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(2u), WithToolType(ToolType::FINGER))); PointerCoords finger1Start = arg.pointerCoords[1]; @@ -450,7 +452,7 @@ TEST_F(GestureConverterTest, ThreeFingerSwipe_Vertical) { ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN | 2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(3), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(3u), WithToolType(ToolType::FINGER))); PointerCoords finger2Start = arg.pointerCoords[2]; @@ -459,7 +461,7 @@ TEST_F(GestureConverterTest, ThreeFingerSwipe_Vertical) { arg = std::get<NotifyMotionArgs>(args.front()); ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), - WithGestureOffset(0, -0.01, EPSILON), + WithGestureOffset(0, -0.01, EPSILON), WithGestureSwipeFingerCount(3), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(3u), WithToolType(ToolType::FINGER))); EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX()); @@ -476,7 +478,7 @@ TEST_F(GestureConverterTest, ThreeFingerSwipe_Vertical) { arg = std::get<NotifyMotionArgs>(args.front()); ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), - WithGestureOffset(0, -0.005, EPSILON), + WithGestureOffset(0, -0.005, EPSILON), WithGestureSwipeFingerCount(3), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(3u), WithToolType(ToolType::FINGER))); EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX()); @@ -492,19 +494,20 @@ TEST_F(GestureConverterTest, ThreeFingerSwipe_Vertical) { ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()), AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP | 2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(3), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(3u), WithToolType(ToolType::FINGER))); args.pop_front(); ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()), AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP | 1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(3), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(2u), WithToolType(ToolType::FINGER))); args.pop_front(); ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()), AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithGestureOffset(0, 0, EPSILON), + WithGestureSwipeFingerCount(3), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(1u), WithToolType(ToolType::FINGER))); } @@ -600,6 +603,7 @@ TEST_F(GestureConverterTest, FourFingerSwipe_Horizontal) { NotifyMotionArgs arg = std::get<NotifyMotionArgs>(args.front()); ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_DOWN), WithGestureOffset(0, 0, EPSILON), + WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(1u), WithToolType(ToolType::FINGER))); PointerCoords finger0Start = arg.pointerCoords[0]; @@ -608,7 +612,7 @@ TEST_F(GestureConverterTest, FourFingerSwipe_Horizontal) { ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN | 1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(2u), WithToolType(ToolType::FINGER))); PointerCoords finger1Start = arg.pointerCoords[1]; @@ -617,7 +621,7 @@ TEST_F(GestureConverterTest, FourFingerSwipe_Horizontal) { ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN | 2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(3u), WithToolType(ToolType::FINGER))); PointerCoords finger2Start = arg.pointerCoords[2]; @@ -626,7 +630,7 @@ TEST_F(GestureConverterTest, FourFingerSwipe_Horizontal) { ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_DOWN | 3 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(4u), WithToolType(ToolType::FINGER))); PointerCoords finger3Start = arg.pointerCoords[3]; @@ -635,7 +639,7 @@ TEST_F(GestureConverterTest, FourFingerSwipe_Horizontal) { arg = std::get<NotifyMotionArgs>(args.front()); ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), - WithGestureOffset(0.01, 0, EPSILON), + WithGestureOffset(0.01, 0, EPSILON), WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(4u), WithToolType(ToolType::FINGER))); EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX() + 10); @@ -654,7 +658,7 @@ TEST_F(GestureConverterTest, FourFingerSwipe_Horizontal) { arg = std::get<NotifyMotionArgs>(args.front()); ASSERT_THAT(arg, AllOf(WithMotionAction(AMOTION_EVENT_ACTION_MOVE), - WithGestureOffset(0.005, 0, EPSILON), + WithGestureOffset(0.005, 0, EPSILON), WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(4u), WithToolType(ToolType::FINGER))); EXPECT_EQ(arg.pointerCoords[0].getX(), finger0Start.getX() + 15); @@ -672,26 +676,27 @@ TEST_F(GestureConverterTest, FourFingerSwipe_Horizontal) { ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()), AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP | 3 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(4u), WithToolType(ToolType::FINGER))); args.pop_front(); ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()), AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP | 2 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(3u), WithToolType(ToolType::FINGER))); args.pop_front(); ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()), AllOf(WithMotionAction(AMOTION_EVENT_ACTION_POINTER_UP | 1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT), - WithGestureOffset(0, 0, EPSILON), + WithGestureOffset(0, 0, EPSILON), WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(2u), WithToolType(ToolType::FINGER))); args.pop_front(); ASSERT_THAT(std::get<NotifyMotionArgs>(args.front()), AllOf(WithMotionAction(AMOTION_EVENT_ACTION_UP), WithGestureOffset(0, 0, EPSILON), + WithGestureSwipeFingerCount(4), WithMotionClassification(MotionClassification::MULTI_FINGER_SWIPE), WithPointerCount(1u), WithToolType(ToolType::FINGER))); } diff --git a/services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp b/services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp new file mode 100644 index 0000000000..e38f88c046 --- /dev/null +++ b/services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp @@ -0,0 +1,625 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../InputDeviceMetricsCollector.h" + +#include <gtest/gtest.h> +#include <gui/constants.h> +#include <linux/input.h> +#include <array> +#include <tuple> + +#include "EventBuilders.h" +#include "TestInputListener.h" + +namespace android { + +using std::chrono_literals::operator""ns; +using std::chrono::nanoseconds; + +namespace { + +constexpr auto USAGE_TIMEOUT = 8765309ns; +constexpr auto TIME = 999999ns; +constexpr auto ALL_USAGE_SOURCES = ftl::enum_range<InputDeviceUsageSource>(); + +constexpr int32_t DEVICE_ID = 3; +constexpr int32_t DEVICE_ID_2 = 4; +constexpr int32_t VID = 0xFEED; +constexpr int32_t PID = 0xDEAD; +constexpr int32_t VERSION = 0xBEEF; +const std::string DEVICE_NAME = "Half Dome"; +const std::string LOCATION = "California"; +const std::string UNIQUE_ID = "Yosemite"; +constexpr uint32_t TOUCHSCREEN = AINPUT_SOURCE_TOUCHSCREEN; +constexpr uint32_t STYLUS = AINPUT_SOURCE_STYLUS; +constexpr uint32_t KEY_SOURCES = + AINPUT_SOURCE_KEYBOARD | AINPUT_SOURCE_DPAD | AINPUT_SOURCE_GAMEPAD; +constexpr int32_t POINTER_1_DOWN = + AMOTION_EVENT_ACTION_POINTER_DOWN | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT); + +InputDeviceIdentifier getIdentifier(int32_t id = DEVICE_ID) { + InputDeviceIdentifier identifier; + identifier.name = DEVICE_NAME + "_" + std::to_string(id); + identifier.location = LOCATION; + identifier.uniqueId = UNIQUE_ID; + identifier.vendor = VID; + identifier.product = PID; + identifier.version = VERSION; + identifier.bus = BUS_USB; + return identifier; +} + +InputDeviceInfo generateTestDeviceInfo(int32_t id = DEVICE_ID, + uint32_t sources = TOUCHSCREEN | STYLUS, + bool isAlphabetic = false) { + auto info = InputDeviceInfo(); + info.initialize(id, /*generation=*/1, /*controllerNumber=*/1, getIdentifier(id), "alias", + /*isExternal=*/false, /*hasMic=*/false, ADISPLAY_ID_NONE); + info.addSource(sources); + info.setKeyboardType(isAlphabetic ? AINPUT_KEYBOARD_TYPE_ALPHABETIC + : AINPUT_KEYBOARD_TYPE_NON_ALPHABETIC); + return info; +} + +const InputDeviceInfo ALPHABETIC_KEYBOARD_INFO = + generateTestDeviceInfo(DEVICE_ID, KEY_SOURCES, /*isAlphabetic=*/true); +const InputDeviceInfo NON_ALPHABETIC_KEYBOARD_INFO = + generateTestDeviceInfo(DEVICE_ID, KEY_SOURCES, /*isAlphabetic=*/false); + +} // namespace + +// --- InputDeviceMetricsCollectorDeviceClassificationTest --- + +class DeviceClassificationFixture : public ::testing::Test, + public ::testing::WithParamInterface<InputDeviceUsageSource> {}; + +TEST_P(DeviceClassificationFixture, ValidClassifications) { + const InputDeviceUsageSource usageSource = GetParam(); + + // Use a switch to ensure a test is added for all source classifications. + switch (usageSource) { + case InputDeviceUsageSource::UNKNOWN: { + ASSERT_EQ(InputDeviceUsageSource::UNKNOWN, + getUsageSourceForKeyArgs(generateTestDeviceInfo(), + KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, TOUCHSCREEN) + .build())); + + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::UNKNOWN}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, AINPUT_SOURCE_KEYBOARD) + .pointer(PointerBuilder(/*id=*/1, ToolType::PALM) + .x(100) + .y(200)) + .build())); + break; + } + + case InputDeviceUsageSource::BUTTONS: { + ASSERT_EQ(InputDeviceUsageSource::BUTTONS, + getUsageSourceForKeyArgs(NON_ALPHABETIC_KEYBOARD_INFO, + KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES) + .keyCode(AKEYCODE_STYLUS_BUTTON_TAIL) + .build())); + break; + } + + case InputDeviceUsageSource::KEYBOARD: { + ASSERT_EQ(InputDeviceUsageSource::KEYBOARD, + getUsageSourceForKeyArgs(ALPHABETIC_KEYBOARD_INFO, + KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES) + .build())); + break; + } + + case InputDeviceUsageSource::DPAD: { + ASSERT_EQ(InputDeviceUsageSource::DPAD, + getUsageSourceForKeyArgs(NON_ALPHABETIC_KEYBOARD_INFO, + KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES) + .keyCode(AKEYCODE_DPAD_CENTER) + .build())); + + ASSERT_EQ(InputDeviceUsageSource::DPAD, + getUsageSourceForKeyArgs(ALPHABETIC_KEYBOARD_INFO, + KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES) + .keyCode(AKEYCODE_DPAD_CENTER) + .build())); + break; + } + + case InputDeviceUsageSource::GAMEPAD: { + ASSERT_EQ(InputDeviceUsageSource::GAMEPAD, + getUsageSourceForKeyArgs(NON_ALPHABETIC_KEYBOARD_INFO, + KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES) + .keyCode(AKEYCODE_BUTTON_A) + .build())); + + ASSERT_EQ(InputDeviceUsageSource::GAMEPAD, + getUsageSourceForKeyArgs(ALPHABETIC_KEYBOARD_INFO, + KeyArgsBuilder(AKEY_EVENT_ACTION_DOWN, KEY_SOURCES) + .keyCode(AKEYCODE_BUTTON_A) + .build())); + break; + } + + case InputDeviceUsageSource::JOYSTICK: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::JOYSTICK}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, AINPUT_SOURCE_JOYSTICK) + .pointer(PointerBuilder(/*id=*/1, ToolType::UNKNOWN) + .axis(AMOTION_EVENT_AXIS_GAS, 1.f)) + .build())); + break; + } + + case InputDeviceUsageSource::MOUSE: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::MOUSE}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_HOVER_MOVE, + AINPUT_SOURCE_MOUSE) + .pointer(PointerBuilder(/*id=*/1, ToolType::MOUSE) + .x(100) + .y(200)) + .build())); + break; + } + + case InputDeviceUsageSource::MOUSE_CAPTURED: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::MOUSE_CAPTURED}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, + AINPUT_SOURCE_MOUSE_RELATIVE) + .pointer(PointerBuilder(/*id=*/1, ToolType::MOUSE) + .x(100) + .y(200) + .axis(AMOTION_EVENT_AXIS_RELATIVE_X, 100) + .axis(AMOTION_EVENT_AXIS_RELATIVE_Y, 200)) + .build())); + break; + } + + case InputDeviceUsageSource::TOUCHPAD: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCHPAD}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, AINPUT_SOURCE_MOUSE) + .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER) + .x(100) + .y(200)) + .build())); + break; + } + + case InputDeviceUsageSource::TOUCHPAD_CAPTURED: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCHPAD_CAPTURED}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, AINPUT_SOURCE_TOUCHPAD) + .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER) + .x(100) + .y(200) + .axis(AMOTION_EVENT_AXIS_RELATIVE_X, 1) + .axis(AMOTION_EVENT_AXIS_RELATIVE_Y, 2)) + .build())); + break; + } + + case InputDeviceUsageSource::ROTARY_ENCODER: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::ROTARY_ENCODER}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_SCROLL, + AINPUT_SOURCE_ROTARY_ENCODER) + .pointer(PointerBuilder(/*id=*/1, ToolType::UNKNOWN) + .axis(AMOTION_EVENT_AXIS_SCROLL, 10) + .axis(AMOTION_EVENT_AXIS_VSCROLL, 10)) + .build())); + break; + } + + case InputDeviceUsageSource::STYLUS_DIRECT: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::STYLUS_DIRECT}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_HOVER_ENTER, + STYLUS | TOUCHSCREEN) + .pointer(PointerBuilder(/*id=*/1, ToolType::STYLUS) + .x(100) + .y(200)) + .build())); + break; + } + + case InputDeviceUsageSource::STYLUS_INDIRECT: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::STYLUS_INDIRECT}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_HOVER_ENTER, + STYLUS | TOUCHSCREEN | AINPUT_SOURCE_MOUSE) + .pointer(PointerBuilder(/*id=*/1, ToolType::STYLUS) + .x(100) + .y(200)) + .build())); + break; + } + + case InputDeviceUsageSource::STYLUS_FUSED: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::STYLUS_FUSED}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_HOVER_ENTER, + AINPUT_SOURCE_BLUETOOTH_STYLUS | TOUCHSCREEN) + .pointer(PointerBuilder(/*id=*/1, ToolType::STYLUS) + .x(100) + .y(200)) + .build())); + break; + } + + case InputDeviceUsageSource::TOUCH_NAVIGATION: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCH_NAVIGATION}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_MOVE, + AINPUT_SOURCE_TOUCH_NAVIGATION) + .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER) + .x(100) + .y(200)) + .build())); + break; + } + + case InputDeviceUsageSource::TOUCHSCREEN: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCHSCREEN}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(POINTER_1_DOWN, TOUCHSCREEN) + .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER) + .x(100) + .y(200)) + .pointer(PointerBuilder(/*id=*/2, ToolType::FINGER) + .x(300) + .y(400)) + .build())); + break; + } + + case InputDeviceUsageSource::TRACKBALL: { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TRACKBALL}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(AMOTION_EVENT_ACTION_SCROLL, + AINPUT_SOURCE_TRACKBALL) + .pointer(PointerBuilder(/*id=*/1, ToolType::UNKNOWN) + .axis(AMOTION_EVENT_AXIS_VSCROLL, 100) + .axis(AMOTION_EVENT_AXIS_HSCROLL, 200)) + .build())); + break; + } + } +} + +INSTANTIATE_TEST_SUITE_P(InputDeviceMetricsCollectorDeviceClassificationTest, + DeviceClassificationFixture, + ::testing::ValuesIn(ALL_USAGE_SOURCES.begin(), ALL_USAGE_SOURCES.end()), + [](const testing::TestParamInfo<InputDeviceUsageSource>& testParamInfo) { + return ftl::enum_string(testParamInfo.param); + }); + +TEST(InputDeviceMetricsCollectorDeviceClassificationTest, MixedClassificationTouchscreenStylus) { + std::set<InputDeviceUsageSource> srcs{InputDeviceUsageSource::TOUCHSCREEN, + InputDeviceUsageSource::STYLUS_DIRECT}; + ASSERT_EQ(srcs, + getUsageSourcesForMotionArgs( + MotionArgsBuilder(POINTER_1_DOWN, TOUCHSCREEN | STYLUS) + .pointer(PointerBuilder(/*id=*/1, ToolType::FINGER).x(100).y(200)) + .pointer(PointerBuilder(/*id=*/2, ToolType::STYLUS).x(300).y(400)) + .build())); +} + +// --- InputDeviceMetricsCollectorTest --- + +class InputDeviceMetricsCollectorTest : public testing::Test, public InputDeviceMetricsLogger { +protected: + TestInputListener mTestListener; + InputDeviceMetricsCollector mMetricsCollector{mTestListener, *this, USAGE_TIMEOUT}; + + void assertUsageLogged(const InputDeviceIdentifier& identifier, nanoseconds duration, + std::optional<SourceUsageBreakdown> sourceBreakdown = {}) { + ASSERT_GE(mLoggedUsageSessions.size(), 1u); + const auto& [loggedIdentifier, report] = *mLoggedUsageSessions.begin(); + ASSERT_EQ(identifier, loggedIdentifier); + ASSERT_EQ(duration, report.usageDuration); + if (sourceBreakdown) { + ASSERT_EQ(sourceBreakdown, report.sourceBreakdown); + } + mLoggedUsageSessions.erase(mLoggedUsageSessions.begin()); + } + + void assertUsageNotLogged() { ASSERT_TRUE(mLoggedUsageSessions.empty()); } + + void setCurrentTime(nanoseconds time) { mCurrentTime = time; } + + NotifyMotionArgs generateMotionArgs(int32_t deviceId, + uint32_t source = AINPUT_SOURCE_TOUCHSCREEN, + std::vector<ToolType> toolTypes = {ToolType::FINGER}) { + MotionArgsBuilder builder(AMOTION_EVENT_ACTION_MOVE, source); + for (size_t i = 0; i < toolTypes.size(); i++) { + builder.pointer(PointerBuilder(i, toolTypes[i])); + } + return builder.deviceId(deviceId) + .eventTime(mCurrentTime.count()) + .downTime(mCurrentTime.count()) + .build(); + } + +private: + std::vector<std::tuple<InputDeviceIdentifier, DeviceUsageReport>> mLoggedUsageSessions; + nanoseconds mCurrentTime{TIME}; + + nanoseconds getCurrentTime() override { return mCurrentTime; } + + void logInputDeviceUsageReported(const InputDeviceIdentifier& identifier, + const DeviceUsageReport& report) override { + mLoggedUsageSessions.emplace_back(identifier, report); + } +}; + +TEST_F(InputDeviceMetricsCollectorTest, DontLogUsageWhenDeviceNotRegistered) { + // Device was used. + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + mTestListener.assertNotifyMotionWasCalled(); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Device was used again after the usage timeout expired, but we still don't log usage. + setCurrentTime(TIME + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + mTestListener.assertNotifyMotionWasCalled(); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); +} + +TEST_F(InputDeviceMetricsCollectorTest, DontLogUsageForIgnoredDevices) { + constexpr static std::array<int32_t, 2> ignoredDevices{ + {INVALID_INPUT_DEVICE_ID, VIRTUAL_KEYBOARD_ID}}; + + for (int32_t ignoredDeviceId : ignoredDevices) { + mMetricsCollector.notifyInputDevicesChanged( + {/*id=*/0, {generateTestDeviceInfo(ignoredDeviceId)}}); + + // Device was used. + mMetricsCollector.notifyMotion(generateMotionArgs(ignoredDeviceId)); + mTestListener.assertNotifyMotionWasCalled(); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Device was used again after the usage timeout expired, but we still don't log usage. + setCurrentTime(TIME + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(ignoredDeviceId)); + mTestListener.assertNotifyMotionWasCalled(); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Remove the ignored device, and ensure we still don't log usage. + mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {}}); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + } +} + +TEST_F(InputDeviceMetricsCollectorTest, LogsSingleEventUsageSession) { + mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo()}}); + + // Device was used. + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Device was used again after the usage timeout. + setCurrentTime(TIME + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + // The usage session has zero duration because it consisted of only one event. + ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(), 0ns)); +} + +TEST_F(InputDeviceMetricsCollectorTest, LogsMultipleEventUsageSession) { + mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo()}}); + + // Device was used. + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Device was used again after some time. + setCurrentTime(TIME + 21ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + + setCurrentTime(TIME + 42ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + + // Device was used again after the usage timeout. + setCurrentTime(TIME + 42ns + 2 * USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(), 42ns)); +} + +TEST_F(InputDeviceMetricsCollectorTest, RemovingDeviceEndsUsageSession) { + mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo()}}); + + // Device was used. + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Device was used again after some time. + setCurrentTime(TIME + 21ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + + // The device was removed before the usage timeout expired. + setCurrentTime(TIME + 42ns); + mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {}}); + ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(), 21ns)); +} + +TEST_F(InputDeviceMetricsCollectorTest, TracksUsageFromDifferentDevicesIndependently) { + mMetricsCollector.notifyInputDevicesChanged( + {/*id=*/0, {generateTestDeviceInfo(), generateTestDeviceInfo(DEVICE_ID_2)}}); + + // Device 1 was used. + setCurrentTime(TIME); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + setCurrentTime(TIME + 100ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Device 2 was used. + setCurrentTime(TIME + 200ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2)); + setCurrentTime(TIME + 400ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2)); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Device 1 was used after its usage timeout expired. Its usage session is reported. + setCurrentTime(TIME + 300ns + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(DEVICE_ID), 100ns)); + + // Device 2 was used. + setCurrentTime(TIME + 350ns + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2)); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Device 1 was used. + setCurrentTime(TIME + 500ns + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Device 2 is not used for a while, but Device 1 is used again. + setCurrentTime(TIME + 400ns + (2 * USAGE_TIMEOUT)); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + // Since Device 2's usage session ended, its usage should be reported. + ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(DEVICE_ID_2), 150ns + USAGE_TIMEOUT)); + + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); +} + +TEST_F(InputDeviceMetricsCollectorTest, BreakdownUsageBySource) { + mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo()}}); + InputDeviceMetricsLogger::SourceUsageBreakdown expectedSourceBreakdown; + + // Use touchscreen. + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN)); + setCurrentTime(TIME + 100ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN)); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Use a stylus with the same input device. + setCurrentTime(TIME + 200ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, STYLUS, {ToolType::STYLUS})); + setCurrentTime(TIME + 400ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, STYLUS, {ToolType::STYLUS})); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Touchscreen was used again after its usage timeout expired. + // This should be tracked as a separate usage of the source in the breakdown. + setCurrentTime(TIME + 300ns + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID)); + expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::TOUCHSCREEN, 100ns); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Continue stylus and touchscreen usages. + setCurrentTime(TIME + 350ns + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, STYLUS, {ToolType::STYLUS})); + setCurrentTime(TIME + 450ns + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN)); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Touchscreen was used after the stylus's usage timeout expired. + // The stylus usage should be tracked in the source breakdown. + setCurrentTime(TIME + 400ns + USAGE_TIMEOUT + USAGE_TIMEOUT); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN)); + expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::STYLUS_DIRECT, + 150ns + USAGE_TIMEOUT); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Remove all devices to force the usage session to be logged. + setCurrentTime(TIME + 500ns + USAGE_TIMEOUT); + mMetricsCollector.notifyInputDevicesChanged({}); + expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::TOUCHSCREEN, + 100ns + USAGE_TIMEOUT); + // Verify that only one usage session was logged for the device, and that session was broken + // down by source correctly. + ASSERT_NO_FATAL_FAILURE(assertUsageLogged(getIdentifier(), + 400ns + USAGE_TIMEOUT + USAGE_TIMEOUT, + expectedSourceBreakdown)); + + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); +} + +TEST_F(InputDeviceMetricsCollectorTest, BreakdownUsageBySource_TrackSourceByDevice) { + mMetricsCollector.notifyInputDevicesChanged( + {/*id=*/0, {generateTestDeviceInfo(DEVICE_ID), generateTestDeviceInfo(DEVICE_ID_2)}}); + InputDeviceMetricsLogger::SourceUsageBreakdown expectedSourceBreakdown1; + InputDeviceMetricsLogger::SourceUsageBreakdown expectedSourceBreakdown2; + + // Use both devices, with different sources. + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN)); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2, STYLUS, {ToolType::STYLUS})); + setCurrentTime(TIME + 100ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN)); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID_2, STYLUS, {ToolType::STYLUS})); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Remove all devices to force the usage session to be logged. + mMetricsCollector.notifyInputDevicesChanged({}); + expectedSourceBreakdown1.emplace_back(InputDeviceUsageSource::TOUCHSCREEN, 100ns); + expectedSourceBreakdown2.emplace_back(InputDeviceUsageSource::STYLUS_DIRECT, 100ns); + ASSERT_NO_FATAL_FAILURE( + assertUsageLogged(getIdentifier(DEVICE_ID), 100ns, expectedSourceBreakdown1)); + ASSERT_NO_FATAL_FAILURE( + assertUsageLogged(getIdentifier(DEVICE_ID_2), 100ns, expectedSourceBreakdown2)); + + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); +} + +TEST_F(InputDeviceMetricsCollectorTest, BreakdownUsageBySource_MultiSourceEvent) { + mMetricsCollector.notifyInputDevicesChanged({/*id=*/0, {generateTestDeviceInfo(DEVICE_ID)}}); + InputDeviceMetricsLogger::SourceUsageBreakdown expectedSourceBreakdown; + + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, // + {ToolType::STYLUS})); + setCurrentTime(TIME + 100ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, // + {ToolType::STYLUS, ToolType::FINGER})); + setCurrentTime(TIME + 200ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, // + {ToolType::STYLUS, ToolType::FINGER})); + setCurrentTime(TIME + 300ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, // + {ToolType::FINGER})); + setCurrentTime(TIME + 400ns); + mMetricsCollector.notifyMotion(generateMotionArgs(DEVICE_ID, TOUCHSCREEN | STYLUS, // + {ToolType::FINGER})); + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); + + // Remove all devices to force the usage session to be logged. + mMetricsCollector.notifyInputDevicesChanged({}); + expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::STYLUS_DIRECT, 200ns); + expectedSourceBreakdown.emplace_back(InputDeviceUsageSource::TOUCHSCREEN, 300ns); + ASSERT_NO_FATAL_FAILURE( + assertUsageLogged(getIdentifier(DEVICE_ID), 400ns, expectedSourceBreakdown)); + + ASSERT_NO_FATAL_FAILURE(assertUsageNotLogged()); +} + +} // namespace android diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp index 3660f81b92..e6b73af3e9 100644 --- a/services/inputflinger/tests/InputDispatcher_test.cpp +++ b/services/inputflinger/tests/InputDispatcher_test.cpp @@ -15,6 +15,7 @@ */ #include "../dispatcher/InputDispatcher.h" +#include "EventBuilders.h" #include <android-base/properties.h> #include <android-base/silent_death_test.h> @@ -51,7 +52,7 @@ using testing::AllOf; static constexpr nsecs_t ARBITRARY_TIME = 1234; // An arbitrary device id. -static constexpr int32_t DEVICE_ID = 1; +static constexpr int32_t DEVICE_ID = DEFAULT_DEVICE_ID; static constexpr int32_t SECOND_DEVICE_ID = 2; // An arbitrary display id. @@ -97,9 +98,6 @@ static constexpr int32_t WINDOW_UID = 1001; static constexpr int32_t SECONDARY_WINDOW_PID = 1010; static constexpr int32_t SECONDARY_WINDOW_UID = 1012; -// The default policy flags to use for event injection by tests. -static constexpr uint32_t DEFAULT_POLICY_FLAGS = POLICY_FLAG_FILTERED | POLICY_FLAG_PASS_TO_USER; - // An arbitrary pid of the gesture monitor window static constexpr int32_t MONITOR_PID = 2001; @@ -1468,247 +1466,6 @@ static InputEventInjectionResult injectKeyUp(const std::unique_ptr<InputDispatch return injectKey(dispatcher, AKEY_EVENT_ACTION_UP, /*repeatCount=*/0, displayId); } -class PointerBuilder { -public: - PointerBuilder(int32_t id, ToolType toolType) { - mProperties.clear(); - mProperties.id = id; - mProperties.toolType = toolType; - mCoords.clear(); - } - - PointerBuilder& x(float x) { return axis(AMOTION_EVENT_AXIS_X, x); } - - PointerBuilder& y(float y) { return axis(AMOTION_EVENT_AXIS_Y, y); } - - PointerBuilder& axis(int32_t axis, float value) { - mCoords.setAxisValue(axis, value); - return *this; - } - - PointerProperties buildProperties() const { return mProperties; } - - PointerCoords buildCoords() const { return mCoords; } - -private: - PointerProperties mProperties; - PointerCoords mCoords; -}; - -class MotionEventBuilder { -public: - MotionEventBuilder(int32_t action, int32_t source) { - mAction = action; - mSource = source; - mEventTime = systemTime(SYSTEM_TIME_MONOTONIC); - mDownTime = mEventTime; - } - - MotionEventBuilder& deviceId(int32_t deviceId) { - mDeviceId = deviceId; - return *this; - } - - MotionEventBuilder& downTime(nsecs_t downTime) { - mDownTime = downTime; - return *this; - } - - MotionEventBuilder& eventTime(nsecs_t eventTime) { - mEventTime = eventTime; - return *this; - } - - MotionEventBuilder& displayId(int32_t displayId) { - mDisplayId = displayId; - return *this; - } - - MotionEventBuilder& actionButton(int32_t actionButton) { - mActionButton = actionButton; - return *this; - } - - MotionEventBuilder& buttonState(int32_t buttonState) { - mButtonState = buttonState; - return *this; - } - - MotionEventBuilder& rawXCursorPosition(float rawXCursorPosition) { - mRawXCursorPosition = rawXCursorPosition; - return *this; - } - - MotionEventBuilder& rawYCursorPosition(float rawYCursorPosition) { - mRawYCursorPosition = rawYCursorPosition; - return *this; - } - - MotionEventBuilder& pointer(PointerBuilder pointer) { - mPointers.push_back(pointer); - return *this; - } - - MotionEventBuilder& addFlag(uint32_t flags) { - mFlags |= flags; - return *this; - } - - MotionEvent build() { - std::vector<PointerProperties> pointerProperties; - std::vector<PointerCoords> pointerCoords; - for (const PointerBuilder& pointer : mPointers) { - pointerProperties.push_back(pointer.buildProperties()); - pointerCoords.push_back(pointer.buildCoords()); - } - - // Set mouse cursor position for the most common cases to avoid boilerplate. - if (mSource == AINPUT_SOURCE_MOUSE && - !MotionEvent::isValidCursorPosition(mRawXCursorPosition, mRawYCursorPosition)) { - mRawXCursorPosition = pointerCoords[0].getX(); - mRawYCursorPosition = pointerCoords[0].getY(); - } - - MotionEvent event; - ui::Transform identityTransform; - event.initialize(InputEvent::nextId(), mDeviceId, mSource, mDisplayId, INVALID_HMAC, - mAction, mActionButton, mFlags, /* edgeFlags */ 0, AMETA_NONE, - mButtonState, MotionClassification::NONE, identityTransform, - /* xPrecision */ 0, /* yPrecision */ 0, mRawXCursorPosition, - mRawYCursorPosition, identityTransform, mDownTime, mEventTime, - mPointers.size(), pointerProperties.data(), pointerCoords.data()); - - return event; - } - -private: - int32_t mAction; - int32_t mDeviceId = DEVICE_ID; - int32_t mSource; - nsecs_t mDownTime; - nsecs_t mEventTime; - int32_t mDisplayId{ADISPLAY_ID_DEFAULT}; - int32_t mActionButton{0}; - int32_t mButtonState{0}; - int32_t mFlags{0}; - float mRawXCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION}; - float mRawYCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION}; - - std::vector<PointerBuilder> mPointers; -}; - -class MotionArgsBuilder { -public: - MotionArgsBuilder(int32_t action, int32_t source) { - mAction = action; - mSource = source; - mEventTime = systemTime(SYSTEM_TIME_MONOTONIC); - mDownTime = mEventTime; - } - - MotionArgsBuilder& deviceId(int32_t deviceId) { - mDeviceId = deviceId; - return *this; - } - - MotionArgsBuilder& downTime(nsecs_t downTime) { - mDownTime = downTime; - return *this; - } - - MotionArgsBuilder& eventTime(nsecs_t eventTime) { - mEventTime = eventTime; - return *this; - } - - MotionArgsBuilder& displayId(int32_t displayId) { - mDisplayId = displayId; - return *this; - } - - MotionArgsBuilder& policyFlags(int32_t policyFlags) { - mPolicyFlags = policyFlags; - return *this; - } - - MotionArgsBuilder& actionButton(int32_t actionButton) { - mActionButton = actionButton; - return *this; - } - - MotionArgsBuilder& buttonState(int32_t buttonState) { - mButtonState = buttonState; - return *this; - } - - MotionArgsBuilder& rawXCursorPosition(float rawXCursorPosition) { - mRawXCursorPosition = rawXCursorPosition; - return *this; - } - - MotionArgsBuilder& rawYCursorPosition(float rawYCursorPosition) { - mRawYCursorPosition = rawYCursorPosition; - return *this; - } - - MotionArgsBuilder& pointer(PointerBuilder pointer) { - mPointers.push_back(pointer); - return *this; - } - - MotionArgsBuilder& addFlag(uint32_t flags) { - mFlags |= flags; - return *this; - } - - MotionArgsBuilder& classification(MotionClassification classification) { - mClassification = classification; - return *this; - } - - NotifyMotionArgs build() { - std::vector<PointerProperties> pointerProperties; - std::vector<PointerCoords> pointerCoords; - for (const PointerBuilder& pointer : mPointers) { - pointerProperties.push_back(pointer.buildProperties()); - pointerCoords.push_back(pointer.buildCoords()); - } - - // Set mouse cursor position for the most common cases to avoid boilerplate. - if (mSource == AINPUT_SOURCE_MOUSE && - !MotionEvent::isValidCursorPosition(mRawXCursorPosition, mRawYCursorPosition)) { - mRawXCursorPosition = pointerCoords[0].getX(); - mRawYCursorPosition = pointerCoords[0].getY(); - } - - NotifyMotionArgs args(InputEvent::nextId(), mEventTime, /*readTime=*/mEventTime, mDeviceId, - mSource, mDisplayId, mPolicyFlags, mAction, mActionButton, mFlags, - AMETA_NONE, mButtonState, mClassification, /*edgeFlags=*/0, - mPointers.size(), pointerProperties.data(), pointerCoords.data(), - /*xPrecision=*/0, /*yPrecision=*/0, mRawXCursorPosition, - mRawYCursorPosition, mDownTime, /*videoFrames=*/{}); - - return args; - } - -private: - int32_t mAction; - int32_t mDeviceId = DEVICE_ID; - int32_t mSource; - nsecs_t mDownTime; - nsecs_t mEventTime; - int32_t mDisplayId{ADISPLAY_ID_DEFAULT}; - int32_t mPolicyFlags = DEFAULT_POLICY_FLAGS; - int32_t mActionButton{0}; - int32_t mButtonState{0}; - int32_t mFlags{0}; - MotionClassification mClassification{MotionClassification::NONE}; - float mRawXCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION}; - float mRawYCursorPosition{AMOTION_EVENT_INVALID_CURSOR_POSITION}; - - std::vector<PointerBuilder> mPointers; -}; - static InputEventInjectionResult injectMotionEvent( const std::unique_ptr<InputDispatcher>& dispatcher, const MotionEvent& event, std::chrono::milliseconds injectionTimeout = INJECT_EVENT_TIMEOUT, diff --git a/services/inputflinger/tests/InputMapperTest.cpp b/services/inputflinger/tests/InputMapperTest.cpp index ad48a79731..0eee2b9be4 100644 --- a/services/inputflinger/tests/InputMapperTest.cpp +++ b/services/inputflinger/tests/InputMapperTest.cpp @@ -22,6 +22,74 @@ namespace android { +using testing::Return; + +void InputMapperUnitTest::SetUp() { + mFakePointerController = std::make_shared<FakePointerController>(); + mFakePointerController->setBounds(0, 0, 800 - 1, 480 - 1); + mFakePointerController->setPosition(400, 240); + + EXPECT_CALL(mMockInputReaderContext, getPointerController(DEVICE_ID)) + .WillRepeatedly(Return(mFakePointerController)); + + EXPECT_CALL(mMockInputReaderContext, getEventHub()).WillRepeatedly(Return(&mMockEventHub)); + InputDeviceIdentifier identifier; + identifier.name = "device"; + identifier.location = "USB1"; + identifier.bus = 0; + + EXPECT_CALL(mMockEventHub, getDeviceIdentifier(EVENTHUB_ID)).WillRepeatedly(Return(identifier)); + mDevice = std::make_unique<InputDevice>(&mMockInputReaderContext, DEVICE_ID, + /*generation=*/2, identifier); + mDeviceContext = std::make_unique<InputDeviceContext>(*mDevice, EVENTHUB_ID); +} + +void InputMapperUnitTest::setupAxis(int axis, bool valid, int32_t min, int32_t max, + int32_t resolution) { + EXPECT_CALL(mMockEventHub, getAbsoluteAxisInfo(EVENTHUB_ID, axis, testing::_)) + .WillRepeatedly([=](int32_t, int32_t, RawAbsoluteAxisInfo* outAxisInfo) { + outAxisInfo->valid = valid; + outAxisInfo->minValue = min; + outAxisInfo->maxValue = max; + outAxisInfo->flat = 0; + outAxisInfo->fuzz = 0; + outAxisInfo->resolution = resolution; + return valid ? OK : -1; + }); +} + +void InputMapperUnitTest::expectScanCodes(bool present, std::set<int> scanCodes) { + for (const auto& scanCode : scanCodes) { + EXPECT_CALL(mMockEventHub, hasScanCode(EVENTHUB_ID, scanCode)) + .WillRepeatedly(testing::Return(present)); + } +} + +void InputMapperUnitTest::setScanCodeState(KeyState state, std::set<int> scanCodes) { + for (const auto& scanCode : scanCodes) { + EXPECT_CALL(mMockEventHub, getScanCodeState(EVENTHUB_ID, scanCode)) + .WillRepeatedly(testing::Return(static_cast<int>(state))); + } +} + +void InputMapperUnitTest::setKeyCodeState(KeyState state, std::set<int> keyCodes) { + for (const auto& keyCode : keyCodes) { + EXPECT_CALL(mMockEventHub, getKeyCodeState(EVENTHUB_ID, keyCode)) + .WillRepeatedly(testing::Return(static_cast<int>(state))); + } +} + +std::list<NotifyArgs> InputMapperUnitTest::process(int32_t type, int32_t code, int32_t value) { + RawEvent event; + event.when = systemTime(SYSTEM_TIME_MONOTONIC); + event.readTime = event.when; + event.deviceId = mMapper->getDeviceContext().getEventHubId(); + event.type = type; + event.code = code; + event.value = value; + return mMapper->process(&event); +} + const char* InputMapperTest::DEVICE_NAME = "device"; const char* InputMapperTest::DEVICE_LOCATION = "USB1"; const ftl::Flags<InputDeviceClass> InputMapperTest::DEVICE_CLASSES = diff --git a/services/inputflinger/tests/InputMapperTest.h b/services/inputflinger/tests/InputMapperTest.h index 2b6655c45e..909bd9c056 100644 --- a/services/inputflinger/tests/InputMapperTest.h +++ b/services/inputflinger/tests/InputMapperTest.h @@ -23,16 +23,48 @@ #include <InputMapper.h> #include <NotifyArgs.h> #include <ftl/flags.h> +#include <gmock/gmock.h> #include <utils/StrongPointer.h> #include "FakeEventHub.h" #include "FakeInputReaderPolicy.h" #include "InstrumentedInputReader.h" +#include "InterfaceMocks.h" #include "TestConstants.h" #include "TestInputListener.h" namespace android { +class InputMapperUnitTest : public testing::Test { +protected: + static constexpr int32_t EVENTHUB_ID = 1; + static constexpr int32_t DEVICE_ID = END_RESERVED_ID + 1000; + virtual void SetUp() override; + + void setupAxis(int axis, bool valid, int32_t min, int32_t max, int32_t resolution); + + void expectScanCodes(bool present, std::set<int> scanCodes); + + void setScanCodeState(KeyState state, std::set<int> scanCodes); + + void setKeyCodeState(KeyState state, std::set<int> keyCodes); + + std::list<NotifyArgs> process(int32_t type, int32_t code, int32_t value); + + MockEventHubInterface mMockEventHub; + std::shared_ptr<FakePointerController> mFakePointerController; + MockInputReaderContext mMockInputReaderContext; + std::unique_ptr<InputDevice> mDevice; + + std::unique_ptr<InputDeviceContext> mDeviceContext; + InputReaderConfiguration mReaderConfiguration; + // The mapper should be created by the subclasses. + std::unique_ptr<InputMapper> mMapper; +}; + +/** + * Deprecated - use InputMapperUnitTest instead. + */ class InputMapperTest : public testing::Test { protected: static const char* DEVICE_NAME; diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp index bfb371f02a..327513d8f5 100644 --- a/services/inputflinger/tests/InputReader_test.cpp +++ b/services/inputflinger/tests/InputReader_test.cpp @@ -11070,6 +11070,101 @@ TEST_F(LightControllerTest, MonoKeyboardBacklight) { ASSERT_EQ(controller.getLightColor(lights[0].id).value_or(-1), LIGHT_BRIGHTNESS); } +TEST_F(LightControllerTest, Ignore_MonoLight_WithPreferredBacklightLevels) { + RawLightInfo infoMono = {.id = 1, + .name = "mono_light", + .maxBrightness = 255, + .flags = InputLightClass::BRIGHTNESS, + .path = ""}; + mFakeEventHub->addRawLightInfo(infoMono.id, std::move(infoMono)); + mFakeEventHub->addConfigurationProperty(EVENTHUB_ID, "keyboard.backlight.brightnessLevels", + "0,100,200"); + + PeripheralController& controller = addControllerAndConfigure<PeripheralController>(); + std::list<NotifyArgs> unused = + mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(), + /*changes=*/{}); + + InputDeviceInfo info; + controller.populateDeviceInfo(&info); + std::vector<InputDeviceLightInfo> lights = info.getLights(); + ASSERT_EQ(1U, lights.size()); + ASSERT_EQ(0U, lights[0].preferredBrightnessLevels.size()); +} + +TEST_F(LightControllerTest, KeyboardBacklight_WithNoPreferredBacklightLevels) { + RawLightInfo infoMono = {.id = 1, + .name = "mono_keyboard_backlight", + .maxBrightness = 255, + .flags = InputLightClass::BRIGHTNESS | + InputLightClass::KEYBOARD_BACKLIGHT, + .path = ""}; + mFakeEventHub->addRawLightInfo(infoMono.id, std::move(infoMono)); + + PeripheralController& controller = addControllerAndConfigure<PeripheralController>(); + std::list<NotifyArgs> unused = + mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(), + /*changes=*/{}); + + InputDeviceInfo info; + controller.populateDeviceInfo(&info); + std::vector<InputDeviceLightInfo> lights = info.getLights(); + ASSERT_EQ(1U, lights.size()); + ASSERT_EQ(0U, lights[0].preferredBrightnessLevels.size()); +} + +TEST_F(LightControllerTest, KeyboardBacklight_WithPreferredBacklightLevels) { + RawLightInfo infoMono = {.id = 1, + .name = "mono_keyboard_backlight", + .maxBrightness = 255, + .flags = InputLightClass::BRIGHTNESS | + InputLightClass::KEYBOARD_BACKLIGHT, + .path = ""}; + mFakeEventHub->addRawLightInfo(infoMono.id, std::move(infoMono)); + mFakeEventHub->addConfigurationProperty(EVENTHUB_ID, "keyboard.backlight.brightnessLevels", + "0,100,200"); + + PeripheralController& controller = addControllerAndConfigure<PeripheralController>(); + std::list<NotifyArgs> unused = + mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(), + /*changes=*/{}); + + InputDeviceInfo info; + controller.populateDeviceInfo(&info); + std::vector<InputDeviceLightInfo> lights = info.getLights(); + ASSERT_EQ(1U, lights.size()); + ASSERT_EQ(3U, lights[0].preferredBrightnessLevels.size()); + std::set<BrightnessLevel>::iterator it = lights[0].preferredBrightnessLevels.begin(); + ASSERT_EQ(BrightnessLevel(0), *it); + std::advance(it, 1); + ASSERT_EQ(BrightnessLevel(100), *it); + std::advance(it, 1); + ASSERT_EQ(BrightnessLevel(200), *it); +} + +TEST_F(LightControllerTest, KeyboardBacklight_WithWrongPreferredBacklightLevels) { + RawLightInfo infoMono = {.id = 1, + .name = "mono_keyboard_backlight", + .maxBrightness = 255, + .flags = InputLightClass::BRIGHTNESS | + InputLightClass::KEYBOARD_BACKLIGHT, + .path = ""}; + mFakeEventHub->addRawLightInfo(infoMono.id, std::move(infoMono)); + mFakeEventHub->addConfigurationProperty(EVENTHUB_ID, "keyboard.backlight.brightnessLevels", + "0,100,200,300,400,500"); + + PeripheralController& controller = addControllerAndConfigure<PeripheralController>(); + std::list<NotifyArgs> unused = + mDevice->configure(ARBITRARY_TIME, mFakePolicy->getReaderConfiguration(), + /*changes=*/{}); + + InputDeviceInfo info; + controller.populateDeviceInfo(&info); + std::vector<InputDeviceLightInfo> lights = info.getLights(); + ASSERT_EQ(1U, lights.size()); + ASSERT_EQ(0U, lights[0].preferredBrightnessLevels.size()); +} + TEST_F(LightControllerTest, RGBLight) { RawLightInfo infoRed = {.id = 1, .name = "red", diff --git a/services/inputflinger/tests/InterfaceMocks.h b/services/inputflinger/tests/InterfaceMocks.h new file mode 100644 index 0000000000..d720a902dc --- /dev/null +++ b/services/inputflinger/tests/InterfaceMocks.h @@ -0,0 +1,146 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <android-base/logging.h> +#include <gmock/gmock.h> + +namespace android { + +class MockInputReaderContext : public InputReaderContext { +public: + MOCK_METHOD(void, updateGlobalMetaState, (), (override)); + int32_t getGlobalMetaState() override { return 0; }; + + MOCK_METHOD(void, disableVirtualKeysUntil, (nsecs_t time), (override)); + MOCK_METHOD(bool, shouldDropVirtualKey, (nsecs_t now, int32_t keyCode, int32_t scanCode), + (override)); + + MOCK_METHOD(void, fadePointer, (), (override)); + MOCK_METHOD(std::shared_ptr<PointerControllerInterface>, getPointerController, + (int32_t deviceId), (override)); + + MOCK_METHOD(void, requestTimeoutAtTime, (nsecs_t when), (override)); + MOCK_METHOD(int32_t, bumpGeneration, (), (override)); + + MOCK_METHOD(void, getExternalStylusDevices, (std::vector<InputDeviceInfo> & outDevices), + (override)); + MOCK_METHOD(std::list<NotifyArgs>, dispatchExternalStylusState, (const StylusState& outState), + (override)); + + MOCK_METHOD(InputReaderPolicyInterface*, getPolicy, (), (override)); + MOCK_METHOD(EventHubInterface*, getEventHub, (), (override)); + + int32_t getNextId() override { return 1; }; + + MOCK_METHOD(void, updateLedMetaState, (int32_t metaState), (override)); + MOCK_METHOD(int32_t, getLedMetaState, (), (override)); +}; + +class MockEventHubInterface : public EventHubInterface { +public: + MOCK_METHOD(ftl::Flags<InputDeviceClass>, getDeviceClasses, (int32_t deviceId), (const)); + MOCK_METHOD(InputDeviceIdentifier, getDeviceIdentifier, (int32_t deviceId), (const)); + MOCK_METHOD(int32_t, getDeviceControllerNumber, (int32_t deviceId), (const)); + MOCK_METHOD(std::optional<PropertyMap>, getConfiguration, (int32_t deviceId), (const)); + MOCK_METHOD(status_t, getAbsoluteAxisInfo, + (int32_t deviceId, int axis, RawAbsoluteAxisInfo* outAxisInfo), (const)); + MOCK_METHOD(bool, hasRelativeAxis, (int32_t deviceId, int axis), (const)); + MOCK_METHOD(bool, hasInputProperty, (int32_t deviceId, int property), (const)); + MOCK_METHOD(bool, hasMscEvent, (int32_t deviceId, int mscEvent), (const)); + MOCK_METHOD(void, addKeyRemapping, (int32_t deviceId, int fromKeyCode, int toKeyCode), (const)); + MOCK_METHOD(status_t, mapKey, + (int32_t deviceId, int scanCode, int usageCode, int32_t metaState, + int32_t* outKeycode, int32_t* outMetaState, uint32_t* outFlags), + (const)); + MOCK_METHOD(status_t, mapAxis, (int32_t deviceId, int scanCode, AxisInfo* outAxisInfo), + (const)); + MOCK_METHOD(void, setExcludedDevices, (const std::vector<std::string>& devices)); + MOCK_METHOD(std::vector<RawEvent>, getEvents, (int timeoutMillis)); + MOCK_METHOD(std::vector<TouchVideoFrame>, getVideoFrames, (int32_t deviceId)); + MOCK_METHOD((base::Result<std::pair<InputDeviceSensorType, int32_t>>), mapSensor, + (int32_t deviceId, int32_t absCode), (const, override)); + MOCK_METHOD(std::vector<int32_t>, getRawBatteryIds, (int32_t deviceId), (const, override)); + MOCK_METHOD(std::optional<RawBatteryInfo>, getRawBatteryInfo, + (int32_t deviceId, int32_t BatteryId), (const, override)); + MOCK_METHOD(std::vector<int32_t>, getRawLightIds, (int32_t deviceId), (const, override)); + MOCK_METHOD(std::optional<RawLightInfo>, getRawLightInfo, (int32_t deviceId, int32_t lightId), + (const, override)); + MOCK_METHOD(std::optional<int32_t>, getLightBrightness, (int32_t deviceId, int32_t lightId), + (const, override)); + MOCK_METHOD(void, setLightBrightness, (int32_t deviceId, int32_t lightId, int32_t brightness), + (override)); + MOCK_METHOD((std::optional<std::unordered_map<LightColor, int32_t>>), getLightIntensities, + (int32_t deviceId, int32_t lightId), (const, override)); + MOCK_METHOD(void, setLightIntensities, + (int32_t deviceId, int32_t lightId, + (std::unordered_map<LightColor, int32_t>)intensities), + (override)); + + MOCK_METHOD(std::optional<RawLayoutInfo>, getRawLayoutInfo, (int32_t deviceId), + (const, override)); + MOCK_METHOD(int32_t, getScanCodeState, (int32_t deviceId, int32_t scanCode), (const, override)); + MOCK_METHOD(int32_t, getKeyCodeState, (int32_t deviceId, int32_t keyCode), (const, override)); + MOCK_METHOD(int32_t, getSwitchState, (int32_t deviceId, int32_t sw), (const, override)); + + MOCK_METHOD(status_t, getAbsoluteAxisValue, (int32_t deviceId, int32_t axis, int32_t* outValue), + (const, override)); + MOCK_METHOD(int32_t, getKeyCodeForKeyLocation, (int32_t deviceId, int32_t locationKeyCode), + (const, override)); + MOCK_METHOD(bool, markSupportedKeyCodes, + (int32_t deviceId, const std::vector<int32_t>& keyCodes, uint8_t* outFlags), + (const, override)); + + MOCK_METHOD(bool, hasScanCode, (int32_t deviceId, int32_t scanCode), (const, override)); + + MOCK_METHOD(bool, hasKeyCode, (int32_t deviceId, int32_t keyCode), (const, override)); + + MOCK_METHOD(bool, hasLed, (int32_t deviceId, int32_t led), (const, override)); + + MOCK_METHOD(void, setLedState, (int32_t deviceId, int32_t led, bool on), (override)); + + MOCK_METHOD(void, getVirtualKeyDefinitions, + (int32_t deviceId, std::vector<VirtualKeyDefinition>& outVirtualKeys), + (const, override)); + + MOCK_METHOD(const std::shared_ptr<KeyCharacterMap>, getKeyCharacterMap, (int32_t deviceId), + (const, override)); + + MOCK_METHOD(bool, setKeyboardLayoutOverlay, + (int32_t deviceId, std::shared_ptr<KeyCharacterMap> map), (override)); + + MOCK_METHOD(void, vibrate, (int32_t deviceId, const VibrationElement& effect), (override)); + MOCK_METHOD(void, cancelVibrate, (int32_t deviceId), (override)); + + MOCK_METHOD(std::vector<int32_t>, getVibratorIds, (int32_t deviceId), (const, override)); + MOCK_METHOD(std::optional<int32_t>, getBatteryCapacity, (int32_t deviceId, int32_t batteryId), + (const, override)); + + MOCK_METHOD(std::optional<int32_t>, getBatteryStatus, (int32_t deviceId, int32_t batteryId), + (const, override)); + MOCK_METHOD(void, requestReopenDevices, (), (override)); + MOCK_METHOD(void, wake, (), (override)); + + MOCK_METHOD(void, dump, (std::string & dump), (const, override)); + MOCK_METHOD(void, monitor, (), (const, override)); + MOCK_METHOD(bool, isDeviceEnabled, (int32_t deviceId), (const, override)); + MOCK_METHOD(status_t, enableDevice, (int32_t deviceId), (override)); + MOCK_METHOD(status_t, disableDevice, (int32_t deviceId), (override)); + MOCK_METHOD(void, sysfsNodeChanged, (const std::string& sysfsNodePath), (override)); +}; + +} // namespace android diff --git a/services/inputflinger/tests/SyncQueue_test.cpp b/services/inputflinger/tests/SyncQueue_test.cpp new file mode 100644 index 0000000000..af2f961ed4 --- /dev/null +++ b/services/inputflinger/tests/SyncQueue_test.cpp @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../SyncQueue.h" + +#include <gtest/gtest.h> +#include <thread> + +namespace android { + +// --- SyncQueueTest --- + +// Validate basic pop and push operation. +TEST(SyncQueueTest, AddAndRemove) { + SyncQueue<int> queue; + + queue.push(1); + ASSERT_EQ(queue.pop(), 1); + + queue.push(3); + ASSERT_EQ(queue.pop(), 3); + + ASSERT_EQ(std::nullopt, queue.pop()); +} + +// Make sure the queue maintains FIFO order. +// Add elements and remove them, and check the order. +TEST(SyncQueueTest, isFIFO) { + SyncQueue<int> queue; + + constexpr int numItems = 10; + for (int i = 0; i < numItems; i++) { + queue.push(static_cast<int>(i)); + } + for (int i = 0; i < numItems; i++) { + ASSERT_EQ(queue.pop(), static_cast<int>(i)); + } +} + +TEST(SyncQueueTest, AllowsMultipleThreads) { + SyncQueue<int> queue; + + // Test with a large number of items to increase likelihood that threads overlap + constexpr int numItems = 100; + + // Fill queue from a different thread + std::thread fillQueue([&queue]() { + for (int i = 0; i < numItems; i++) { + queue.push(static_cast<int>(i)); + } + }); + + // Make sure all elements are received in correct order + for (int i = 0; i < numItems; i++) { + // Since popping races with the thread that's filling the queue, + // keep popping until we get something back + std::optional<int> popped; + do { + popped = queue.pop(); + } while (!popped); + ASSERT_EQ(popped, static_cast<int>(i)); + } + + fillQueue.join(); +} + +} // namespace android diff --git a/services/inputflinger/tests/TestInputListenerMatchers.h b/services/inputflinger/tests/TestInputListenerMatchers.h index db6f2548e8..01b79ca5da 100644 --- a/services/inputflinger/tests/TestInputListenerMatchers.h +++ b/services/inputflinger/tests/TestInputListenerMatchers.h @@ -23,6 +23,8 @@ #include <gtest/gtest.h> #include <input/Input.h> +#include "TestConstants.h" + namespace android { MATCHER_P(WithMotionAction, action, "MotionEvent with specified action") { @@ -136,6 +138,15 @@ MATCHER_P2(WithGesturePinchScaleFactor, factor, epsilon, return fabs(argScaleFactor - factor) <= epsilon; } +MATCHER_P(WithGestureSwipeFingerCount, count, + "InputEvent with specified touchpad swipe finger count") { + const auto argFingerCount = + arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_GESTURE_SWIPE_FINGER_COUNT); + *result_listener << "expected gesture swipe finger count " << count << " but got " + << argFingerCount; + return fabs(argFingerCount - count) <= EPSILON; +} + MATCHER_P(WithPressure, pressure, "InputEvent with specified pressure") { const auto argPressure = arg.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_PRESSURE); *result_listener << "expected pressure " << pressure << ", but got " << argPressure; diff --git a/services/inputflinger/tests/TouchpadInputMapper_test.cpp b/services/inputflinger/tests/TouchpadInputMapper_test.cpp new file mode 100644 index 0000000000..92cd462c9a --- /dev/null +++ b/services/inputflinger/tests/TouchpadInputMapper_test.cpp @@ -0,0 +1,155 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TouchpadInputMapper.h" + +#include <android-base/logging.h> +#include <gtest/gtest.h> + +#include <thread> +#include "FakePointerController.h" +#include "InputMapperTest.h" +#include "InterfaceMocks.h" +#include "TestInputListenerMatchers.h" + +#define TAG "TouchpadInputMapper_test" + +namespace android { + +using testing::Return; +using testing::VariantWith; +constexpr auto ACTION_DOWN = AMOTION_EVENT_ACTION_DOWN; +constexpr auto ACTION_UP = AMOTION_EVENT_ACTION_UP; +constexpr auto BUTTON_PRESS = AMOTION_EVENT_ACTION_BUTTON_PRESS; +constexpr auto BUTTON_RELEASE = AMOTION_EVENT_ACTION_BUTTON_RELEASE; +constexpr auto HOVER_MOVE = AMOTION_EVENT_ACTION_HOVER_MOVE; + +/** + * Unit tests for TouchpadInputMapper. + */ +class TouchpadInputMapperTest : public InputMapperUnitTest { +protected: + void SetUp() override { + InputMapperUnitTest::SetUp(); + + // Present scan codes: BTN_TOUCH and BTN_TOOL_FINGER + expectScanCodes(/*present=*/true, + {BTN_LEFT, BTN_RIGHT, BTN_TOOL_FINGER, BTN_TOOL_QUINTTAP, BTN_TOUCH, + BTN_TOOL_DOUBLETAP, BTN_TOOL_TRIPLETAP, BTN_TOOL_QUADTAP}); + // Missing scan codes that the mapper checks for. + expectScanCodes(/*present=*/false, + {BTN_TOOL_PEN, BTN_TOOL_RUBBER, BTN_TOOL_BRUSH, BTN_TOOL_PENCIL, + BTN_TOOL_AIRBRUSH}); + + // Current scan code state - all keys are UP by default + setScanCodeState(KeyState::UP, {BTN_TOUCH, BTN_STYLUS, + BTN_STYLUS2, BTN_0, + BTN_TOOL_FINGER, BTN_TOOL_PEN, + BTN_TOOL_RUBBER, BTN_TOOL_BRUSH, + BTN_TOOL_PENCIL, BTN_TOOL_AIRBRUSH, + BTN_TOOL_MOUSE, BTN_TOOL_LENS, + BTN_TOOL_DOUBLETAP, BTN_TOOL_TRIPLETAP, + BTN_TOOL_QUADTAP, BTN_TOOL_QUINTTAP, + BTN_LEFT, BTN_RIGHT, + BTN_MIDDLE, BTN_BACK, + BTN_SIDE, BTN_FORWARD, + BTN_EXTRA, BTN_TASK}); + + setKeyCodeState(KeyState::UP, + {AKEYCODE_STYLUS_BUTTON_PRIMARY, AKEYCODE_STYLUS_BUTTON_SECONDARY}); + + // Key mappings + EXPECT_CALL(mMockEventHub, + mapKey(EVENTHUB_ID, BTN_LEFT, /*usageCode=*/0, /*metaState=*/0, testing::_, + testing::_, testing::_)) + .WillRepeatedly(Return(NAME_NOT_FOUND)); + + // Input properties - only INPUT_PROP_BUTTONPAD + EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, INPUT_PROP_BUTTONPAD)) + .WillRepeatedly(Return(true)); + EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, INPUT_PROP_SEMI_MT)) + .WillRepeatedly(Return(false)); + + // Axes that the device has + setupAxis(ABS_MT_SLOT, /*valid=*/true, /*min=*/0, /*max=*/4, /*resolution=*/0); + setupAxis(ABS_MT_POSITION_X, /*valid=*/true, /*min=*/0, /*max=*/2000, /*resolution=*/24); + setupAxis(ABS_MT_POSITION_Y, /*valid=*/true, /*min=*/0, /*max=*/1000, /*resolution=*/24); + setupAxis(ABS_MT_PRESSURE, /*valid=*/true, /*min*/ 0, /*max=*/255, /*resolution=*/0); + // Axes that the device does not have + setupAxis(ABS_MT_ORIENTATION, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); + setupAxis(ABS_MT_TOUCH_MAJOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); + setupAxis(ABS_MT_TOUCH_MINOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); + setupAxis(ABS_MT_WIDTH_MAJOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); + setupAxis(ABS_MT_WIDTH_MINOR, /*valid=*/false, /*min=*/0, /*max=*/0, /*resolution=*/0); + + EXPECT_CALL(mMockEventHub, getAbsoluteAxisValue(EVENTHUB_ID, ABS_MT_SLOT, testing::_)) + .WillRepeatedly([](int32_t eventHubId, int32_t, int32_t* outValue) { + *outValue = 0; + return OK; + }); + mMapper = createInputMapper<TouchpadInputMapper>(*mDeviceContext, mReaderConfiguration); + } +}; + +/** + * Start moving the finger and then click the left touchpad button. Check whether HOVER_EXIT is + * generated when hovering stops. Currently, it is not. + * In the current implementation, HOVER_MOVE and ACTION_DOWN events are not sent out right away, + * but only after the button is released. + */ +TEST_F(TouchpadInputMapperTest, HoverAndLeftButtonPress) { + std::list<NotifyArgs> args; + + args += process(EV_ABS, ABS_MT_TRACKING_ID, 1); + args += process(EV_KEY, BTN_TOUCH, 1); + setScanCodeState(KeyState::DOWN, {BTN_TOOL_FINGER}); + args += process(EV_KEY, BTN_TOOL_FINGER, 1); + args += process(EV_ABS, ABS_MT_POSITION_X, 50); + args += process(EV_ABS, ABS_MT_POSITION_Y, 50); + args += process(EV_ABS, ABS_MT_PRESSURE, 1); + args += process(EV_SYN, SYN_REPORT, 0); + ASSERT_THAT(args, testing::IsEmpty()); + + // Without this sleep, the test fails. + // TODO(b/284133337): Figure out whether this can be removed + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + args += process(EV_KEY, BTN_LEFT, 1); + setScanCodeState(KeyState::DOWN, {BTN_LEFT}); + args += process(EV_SYN, SYN_REPORT, 0); + + args += process(EV_KEY, BTN_LEFT, 0); + setScanCodeState(KeyState::UP, {BTN_LEFT}); + args += process(EV_SYN, SYN_REPORT, 0); + ASSERT_THAT(args, + ElementsAre(VariantWith<NotifyMotionArgs>(WithMotionAction(HOVER_MOVE)), + VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_DOWN)), + VariantWith<NotifyMotionArgs>(WithMotionAction(BUTTON_PRESS)), + VariantWith<NotifyMotionArgs>(WithMotionAction(BUTTON_RELEASE)), + VariantWith<NotifyMotionArgs>(WithMotionAction(ACTION_UP)))); + + // Liftoff + args.clear(); + args += process(EV_ABS, ABS_MT_PRESSURE, 0); + args += process(EV_ABS, ABS_MT_TRACKING_ID, -1); + args += process(EV_KEY, BTN_TOUCH, 0); + setScanCodeState(KeyState::UP, {BTN_TOOL_FINGER}); + args += process(EV_KEY, BTN_TOOL_FINGER, 0); + args += process(EV_SYN, SYN_REPORT, 0); + ASSERT_THAT(args, testing::IsEmpty()); +} + +} // namespace android diff --git a/services/inputflinger/tests/fuzzers/BlockingQueueFuzzer.cpp b/services/inputflinger/tests/fuzzers/BlockingQueueFuzzer.cpp index d2595bfc9d..e9016bb0c6 100644 --- a/services/inputflinger/tests/fuzzers/BlockingQueueFuzzer.cpp +++ b/services/inputflinger/tests/fuzzers/BlockingQueueFuzzer.cpp @@ -47,12 +47,21 @@ extern "C" int LLVMFuzzerTestOneInput(uint8_t *data, size_t size) { filled > numPops ? filled -= numPops : filled = 0; }, [&]() -> void { + // Pops blocks if it is empty, so only pop up to num elements inserted. + size_t numPops = fdp.ConsumeIntegralInRange<size_t>(0, filled); + for (size_t i = 0; i < numPops; i++) { + queue.popWithTimeout( + std::chrono::nanoseconds{fdp.ConsumeIntegral<int64_t>()}); + } + filled > numPops ? filled -= numPops : filled = 0; + }, + [&]() -> void { queue.clear(); filled = 0; }, [&]() -> void { int32_t eraseElement = fdp.ConsumeIntegral<int32_t>(); - queue.erase([&](int32_t element) { + queue.erase_if([&](int32_t element) { if (element == eraseElement) { filled--; return true; diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp index 95911d890c..61f2d54300 100644 --- a/services/surfaceflinger/SurfaceFlinger.cpp +++ b/services/surfaceflinger/SurfaceFlinger.cpp @@ -2527,7 +2527,7 @@ bool SurfaceFlinger::commit(TimePoint frameTime, VsyncId vsyncId, TimePoint expe } updateCursorAsync(); - updateInputFlinger(vsyncId); + updateInputFlinger(vsyncId, frameTime); if (mLayerTracingEnabled && !mLayerTracing.flagIsSet(LayerTracing::TRACE_COMPOSITION)) { // This will block and tracing should only be enabled for debugging. @@ -3721,7 +3721,7 @@ void SurfaceFlinger::commitTransactionsLocked(uint32_t transactionFlags) { doCommitTransactions(); } -void SurfaceFlinger::updateInputFlinger(VsyncId vsyncId) { +void SurfaceFlinger::updateInputFlinger(VsyncId vsyncId, TimePoint frameTime) { if (!mInputFlinger || (!mUpdateInputInfo && mInputWindowCommands.empty())) { return; } @@ -3733,8 +3733,6 @@ void SurfaceFlinger::updateInputFlinger(VsyncId vsyncId) { if (mUpdateInputInfo) { mUpdateInputInfo = false; updateWindowInfo = true; - mLastInputFlingerUpdateVsyncId = vsyncId; - mLastInputFlingerUpdateTimestamp = systemTime(); buildWindowInfos(windowInfos, displayInfos); } @@ -3756,17 +3754,18 @@ void SurfaceFlinger::updateInputFlinger(VsyncId vsyncId) { inputWindowCommands = std::move(mInputWindowCommands), inputFlinger = mInputFlinger, this, - visibleWindowsChanged]() { + visibleWindowsChanged, vsyncId, frameTime]() { ATRACE_NAME("BackgroundExecutor::updateInputFlinger"); if (updateWindowInfo) { mWindowInfosListenerInvoker - ->windowInfosChanged(std::move(windowInfos), std::move(displayInfos), + ->windowInfosChanged(gui::WindowInfosUpdate{std::move(windowInfos), + std::move(displayInfos), + ftl::to_underlying(vsyncId), + frameTime.ns()}, std::move( inputWindowCommands.windowInfosReportedListeners), /* forceImmediateCall= */ visibleWindowsChanged || - !inputWindowCommands.focusRequests.empty(), - mLastInputFlingerUpdateVsyncId, - mLastInputFlingerUpdateTimestamp); + !inputWindowCommands.focusRequests.empty()); } else { // If there are listeners but no changes to input windows, call the listeners // immediately. @@ -6141,27 +6140,14 @@ void SurfaceFlinger::dumpAllLocked(const DumpArgs& args, const std::string& comp result.append("\n"); result.append("Window Infos:\n"); - StringAppendF(&result, " input flinger update vsync id: %" PRId64 "\n", - ftl::to_underlying(mLastInputFlingerUpdateVsyncId)); - StringAppendF(&result, " input flinger update timestamp (ns): %" PRId64 "\n", - mLastInputFlingerUpdateTimestamp); + auto windowInfosDebug = mWindowInfosListenerInvoker->getDebugInfo(); + StringAppendF(&result, " max send vsync id: %" PRId64 "\n", + ftl::to_underlying(windowInfosDebug.maxSendDelayVsyncId)); + StringAppendF(&result, " max send delay (ns): %" PRId64 " ns\n", + windowInfosDebug.maxSendDelayDuration); + StringAppendF(&result, " unsent messages: %" PRIu32 "\n", + windowInfosDebug.pendingMessageCount); result.append("\n"); - - if (VsyncId unsentVsyncId = mWindowInfosListenerInvoker->getUnsentMessageVsyncId(); - unsentVsyncId != VsyncId()) { - StringAppendF(&result, " unsent input flinger update vsync id: %" PRId64 "\n", - ftl::to_underlying(unsentVsyncId)); - StringAppendF(&result, " unsent input flinger update timestamp (ns): %" PRId64 "\n", - mWindowInfosListenerInvoker->getUnsentMessageTimestamp()); - result.append("\n"); - } - - if (uint32_t pendingMessages = mWindowInfosListenerInvoker->getPendingMessageCount(); - pendingMessages != 0) { - StringAppendF(&result, " pending input flinger calls: %" PRIu32 "\n", - mWindowInfosListenerInvoker->getPendingMessageCount()); - result.append("\n"); - } } mat4 SurfaceFlinger::calculateColorMatrix(float saturation) { diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h index 6b9ba8c32c..b7d2047671 100644 --- a/services/surfaceflinger/SurfaceFlinger.h +++ b/services/surfaceflinger/SurfaceFlinger.h @@ -718,7 +718,7 @@ private: void updateLayerHistory(const frontend::LayerSnapshot& snapshot); frontend::Update flushLifecycleUpdates() REQUIRES(kMainThreadContext); - void updateInputFlinger(VsyncId); + void updateInputFlinger(VsyncId vsyncId, TimePoint frameTime); void persistDisplayBrightness(bool needsComposite) REQUIRES(kMainThreadContext); void buildWindowInfos(std::vector<gui::WindowInfo>& outWindowInfos, std::vector<gui::DisplayInfo>& outDisplayInfos); @@ -1250,9 +1250,6 @@ private: VsyncId mLastCommittedVsyncId; - VsyncId mLastInputFlingerUpdateVsyncId; - nsecs_t mLastInputFlingerUpdateTimestamp; - // If blurs should be enabled on this device. bool mSupportsBlur = false; std::atomic<uint32_t> mFrameMissedCount = 0; diff --git a/services/surfaceflinger/WindowInfosListenerInvoker.cpp b/services/surfaceflinger/WindowInfosListenerInvoker.cpp index b2885fb9b4..20699ef123 100644 --- a/services/surfaceflinger/WindowInfosListenerInvoker.cpp +++ b/services/surfaceflinger/WindowInfosListenerInvoker.cpp @@ -16,8 +16,11 @@ #include <ftl/small_vector.h> #include <gui/ISurfaceComposer.h> +#include <gui/TraceUtils.h> #include <gui/WindowInfosUpdate.h> +#include <scheduler/Time.h> +#include "BackgroundExecutor.h" #include "WindowInfosListenerInvoker.h" namespace android { @@ -26,7 +29,7 @@ using gui::DisplayInfo; using gui::IWindowInfosListener; using gui::WindowInfo; -using WindowInfosListenerVector = ftl::SmallVector<const sp<IWindowInfosListener>, 3>; +using WindowInfosListenerVector = ftl::SmallVector<const sp<gui::IWindowInfosListener>, 3>; struct WindowInfosReportedListenerInvoker : gui::BnWindowInfosReportedListener, IBinder::DeathRecipient { @@ -86,45 +89,19 @@ void WindowInfosListenerInvoker::binderDied(const wp<IBinder>& who) { } void WindowInfosListenerInvoker::windowInfosChanged( - std::vector<WindowInfo> windowInfos, std::vector<DisplayInfo> displayInfos, - WindowInfosReportedListenerSet reportedListeners, bool forceImmediateCall, VsyncId vsyncId, - nsecs_t timestamp) { - reportedListeners.insert(sp<WindowInfosListenerInvoker>::fromExisting(this)); - auto callListeners = [this, windowInfos = std::move(windowInfos), - displayInfos = std::move(displayInfos), vsyncId, - timestamp](WindowInfosReportedListenerSet reportedListeners) mutable { - WindowInfosListenerVector windowInfosListeners; - { - std::scoped_lock lock(mListenersMutex); - for (const auto& [_, listener] : mWindowInfosListeners) { - windowInfosListeners.push_back(listener); - } - } - - auto reportedInvoker = - sp<WindowInfosReportedListenerInvoker>::make(windowInfosListeners, - std::move(reportedListeners)); - - gui::WindowInfosUpdate update(std::move(windowInfos), std::move(displayInfos), - ftl::to_underlying(vsyncId), timestamp); - - for (const auto& listener : windowInfosListeners) { - sp<IBinder> asBinder = IInterface::asBinder(listener); - - // linkToDeath is used here to ensure that the windowInfosReportedListeners - // are called even if one of the windowInfosListeners dies before - // calling onWindowInfosReported. - asBinder->linkToDeath(reportedInvoker); + gui::WindowInfosUpdate update, WindowInfosReportedListenerSet reportedListeners, + bool forceImmediateCall) { + WindowInfosListenerVector listeners; + { + std::scoped_lock lock{mMessagesMutex}; - auto status = listener->onWindowInfosChanged(update, reportedInvoker); - if (!status.isOk()) { - reportedInvoker->onWindowInfosReported(); - } + if (!mDelayInfo) { + mDelayInfo = DelayInfo{ + .vsyncId = update.vsyncId, + .frameTime = update.timestamp, + }; } - }; - { - std::scoped_lock lock(mMessagesMutex); // If there are unacked messages and this isn't a forced call, then return immediately. // If a forced window infos change doesn't happen first, the update will be sent after // the WindowInfosReportedListeners are called. If a forced window infos change happens or @@ -132,44 +109,87 @@ void WindowInfosListenerInvoker::windowInfosChanged( // will be dropped and the listeners will only be called with the latest info. This is done // to reduce the amount of binder memory used. if (mActiveMessageCount > 0 && !forceImmediateCall) { - mWindowInfosChangedDelayed = std::move(callListeners); - mUnsentVsyncId = vsyncId; - mUnsentTimestamp = timestamp; - mReportedListenersDelayed.merge(reportedListeners); + mDelayedUpdate = std::move(update); + mReportedListeners.merge(reportedListeners); + return; + } + + if (mDelayedUpdate) { + mDelayedUpdate.reset(); + } + + { + std::scoped_lock lock{mListenersMutex}; + for (const auto& [_, listener] : mWindowInfosListeners) { + listeners.push_back(listener); + } + } + if (CC_UNLIKELY(listeners.empty())) { + mReportedListeners.merge(reportedListeners); + mDelayInfo.reset(); return; } - mWindowInfosChangedDelayed = nullptr; - mUnsentVsyncId = VsyncId(); - mUnsentTimestamp = -1; - reportedListeners.merge(mReportedListenersDelayed); + reportedListeners.insert(sp<WindowInfosListenerInvoker>::fromExisting(this)); + reportedListeners.merge(mReportedListeners); + mReportedListeners.clear(); + mActiveMessageCount++; + updateMaxSendDelay(); + mDelayInfo.reset(); } - callListeners(std::move(reportedListeners)); -} -binder::Status WindowInfosListenerInvoker::onWindowInfosReported() { - std::function<void(WindowInfosReportedListenerSet)> callListeners; - WindowInfosReportedListenerSet reportedListeners; + auto reportedInvoker = + sp<WindowInfosReportedListenerInvoker>::make(listeners, std::move(reportedListeners)); - { - std::scoped_lock lock{mMessagesMutex}; - mActiveMessageCount--; - if (!mWindowInfosChangedDelayed || mActiveMessageCount > 0) { - return binder::Status::ok(); - } + for (const auto& listener : listeners) { + sp<IBinder> asBinder = IInterface::asBinder(listener); - mActiveMessageCount++; - callListeners = std::move(mWindowInfosChangedDelayed); - mWindowInfosChangedDelayed = nullptr; - mUnsentVsyncId = VsyncId(); - mUnsentTimestamp = -1; - reportedListeners = std::move(mReportedListenersDelayed); - mReportedListenersDelayed.clear(); + // linkToDeath is used here to ensure that the windowInfosReportedListeners + // are called even if one of the windowInfosListeners dies before + // calling onWindowInfosReported. + asBinder->linkToDeath(reportedInvoker); + + auto status = listener->onWindowInfosChanged(update, reportedInvoker); + if (!status.isOk()) { + reportedInvoker->onWindowInfosReported(); + } } +} - callListeners(std::move(reportedListeners)); +binder::Status WindowInfosListenerInvoker::onWindowInfosReported() { + BackgroundExecutor::getInstance().sendCallbacks({[this]() { + gui::WindowInfosUpdate update; + { + std::scoped_lock lock{mMessagesMutex}; + mActiveMessageCount--; + if (!mDelayedUpdate || mActiveMessageCount > 0) { + return; + } + update = std::move(*mDelayedUpdate); + mDelayedUpdate.reset(); + } + windowInfosChanged(std::move(update), {}, false); + }}); return binder::Status::ok(); } +WindowInfosListenerInvoker::DebugInfo WindowInfosListenerInvoker::getDebugInfo() { + std::scoped_lock lock{mMessagesMutex}; + updateMaxSendDelay(); + mDebugInfo.pendingMessageCount = mActiveMessageCount; + return mDebugInfo; +} + +void WindowInfosListenerInvoker::updateMaxSendDelay() { + if (!mDelayInfo) { + return; + } + nsecs_t delay = TimePoint::now().ns() - mDelayInfo->frameTime; + if (delay > mDebugInfo.maxSendDelayDuration) { + mDebugInfo.maxSendDelayDuration = delay; + mDebugInfo.maxSendDelayVsyncId = VsyncId{mDelayInfo->vsyncId}; + } +} + } // namespace android diff --git a/services/surfaceflinger/WindowInfosListenerInvoker.h b/services/surfaceflinger/WindowInfosListenerInvoker.h index ade607fc99..bc465a3a2b 100644 --- a/services/surfaceflinger/WindowInfosListenerInvoker.h +++ b/services/surfaceflinger/WindowInfosListenerInvoker.h @@ -16,6 +16,7 @@ #pragma once +#include <optional> #include <unordered_set> #include <android/gui/BnWindowInfosReportedListener.h> @@ -40,26 +41,18 @@ public: void addWindowInfosListener(sp<gui::IWindowInfosListener>); void removeWindowInfosListener(const sp<gui::IWindowInfosListener>& windowInfosListener); - void windowInfosChanged(std::vector<gui::WindowInfo>, std::vector<gui::DisplayInfo>, + void windowInfosChanged(gui::WindowInfosUpdate update, WindowInfosReportedListenerSet windowInfosReportedListeners, - bool forceImmediateCall, VsyncId vsyncId, nsecs_t timestamp); + bool forceImmediateCall); binder::Status onWindowInfosReported() override; - VsyncId getUnsentMessageVsyncId() { - std::scoped_lock lock(mMessagesMutex); - return mUnsentVsyncId; - } - - nsecs_t getUnsentMessageTimestamp() { - std::scoped_lock lock(mMessagesMutex); - return mUnsentTimestamp; - } - - uint32_t getPendingMessageCount() { - std::scoped_lock lock(mMessagesMutex); - return mActiveMessageCount; - } + struct DebugInfo { + VsyncId maxSendDelayVsyncId; + nsecs_t maxSendDelayDuration; + uint32_t pendingMessageCount; + }; + DebugInfo getDebugInfo(); protected: void binderDied(const wp<IBinder>& who) override; @@ -73,11 +66,16 @@ private: std::mutex mMessagesMutex; uint32_t mActiveMessageCount GUARDED_BY(mMessagesMutex) = 0; - std::function<void(WindowInfosReportedListenerSet)> mWindowInfosChangedDelayed - GUARDED_BY(mMessagesMutex); - VsyncId mUnsentVsyncId GUARDED_BY(mMessagesMutex); - nsecs_t mUnsentTimestamp GUARDED_BY(mMessagesMutex) = -1; - WindowInfosReportedListenerSet mReportedListenersDelayed; + std::optional<gui::WindowInfosUpdate> mDelayedUpdate GUARDED_BY(mMessagesMutex); + WindowInfosReportedListenerSet mReportedListeners; + + DebugInfo mDebugInfo GUARDED_BY(mMessagesMutex); + struct DelayInfo { + int64_t vsyncId; + nsecs_t frameTime; + }; + std::optional<DelayInfo> mDelayInfo GUARDED_BY(mMessagesMutex); + void updateMaxSendDelay() REQUIRES(mMessagesMutex); }; } // namespace android diff --git a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h index 534a8f3b87..8e208bc538 100644 --- a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h +++ b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h @@ -590,7 +590,7 @@ public: mFlinger->binderDied(display); mFlinger->onFirstRef(); - mFlinger->updateInputFlinger(VsyncId{0}); + mFlinger->updateInputFlinger(VsyncId{}, TimePoint{}); mFlinger->updateCursorAsync(); mutableScheduler().setVsyncConfig({.sfOffset = mFdp.ConsumeIntegral<nsecs_t>(), diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp index 84a8529cda..86af303744 100644 --- a/services/surfaceflinger/tests/unittests/Android.bp +++ b/services/surfaceflinger/tests/unittests/Android.bp @@ -139,6 +139,7 @@ cc_test { "VSyncReactorTest.cpp", "VsyncConfigurationTest.cpp", "VsyncScheduleTest.cpp", + "WindowInfosListenerInvokerTest.cpp", ], } diff --git a/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp b/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp new file mode 100644 index 0000000000..af4971b063 --- /dev/null +++ b/services/surfaceflinger/tests/unittests/WindowInfosListenerInvokerTest.cpp @@ -0,0 +1,244 @@ +#include <android/gui/BnWindowInfosListener.h> +#include <gtest/gtest.h> +#include <gui/SurfaceComposerClient.h> +#include <gui/WindowInfosUpdate.h> +#include <condition_variable> + +#include "BackgroundExecutor.h" +#include "WindowInfosListenerInvoker.h" +#include "android/gui/IWindowInfosReportedListener.h" + +namespace android { + +class WindowInfosListenerInvokerTest : public testing::Test { +protected: + WindowInfosListenerInvokerTest() : mInvoker(sp<WindowInfosListenerInvoker>::make()) {} + + ~WindowInfosListenerInvokerTest() { + std::mutex mutex; + std::condition_variable cv; + bool flushComplete = false; + // Flush the BackgroundExecutor thread to ensure any scheduled tasks are complete. + // Otherwise, references those tasks hold may go out of scope before they are done + // executing. + BackgroundExecutor::getInstance().sendCallbacks({[&]() { + std::scoped_lock lock{mutex}; + flushComplete = true; + cv.notify_one(); + }}); + std::unique_lock<std::mutex> lock{mutex}; + cv.wait(lock, [&]() { return flushComplete; }); + } + + sp<WindowInfosListenerInvoker> mInvoker; +}; + +using WindowInfosUpdateConsumer = std::function<void(const gui::WindowInfosUpdate&, + const sp<gui::IWindowInfosReportedListener>&)>; + +class Listener : public gui::BnWindowInfosListener { +public: + Listener(WindowInfosUpdateConsumer consumer) : mConsumer(std::move(consumer)) {} + + binder::Status onWindowInfosChanged( + const gui::WindowInfosUpdate& update, + const sp<gui::IWindowInfosReportedListener>& reportedListener) override { + mConsumer(update, reportedListener); + return binder::Status::ok(); + } + +private: + WindowInfosUpdateConsumer mConsumer; +}; + +// Test that WindowInfosListenerInvoker#windowInfosChanged calls a single window infos listener. +TEST_F(WindowInfosListenerInvokerTest, callsSingleListener) { + std::mutex mutex; + std::condition_variable cv; + + int callCount = 0; + + mInvoker->addWindowInfosListener( + sp<Listener>::make([&](const gui::WindowInfosUpdate&, + const sp<gui::IWindowInfosReportedListener>& reportedListener) { + std::scoped_lock lock{mutex}; + callCount++; + cv.notify_one(); + + reportedListener->onWindowInfosReported(); + })); + + BackgroundExecutor::getInstance().sendCallbacks( + {[this]() { mInvoker->windowInfosChanged({}, {}, false); }}); + + std::unique_lock<std::mutex> lock{mutex}; + cv.wait(lock, [&]() { return callCount == 1; }); + EXPECT_EQ(callCount, 1); +} + +// Test that WindowInfosListenerInvoker#windowInfosChanged calls multiple window infos listeners. +TEST_F(WindowInfosListenerInvokerTest, callsMultipleListeners) { + std::mutex mutex; + std::condition_variable cv; + + int callCount = 0; + const int expectedCallCount = 3; + + for (int i = 0; i < expectedCallCount; i++) { + mInvoker->addWindowInfosListener(sp<Listener>::make( + [&](const gui::WindowInfosUpdate&, + const sp<gui::IWindowInfosReportedListener>& reportedListener) { + std::scoped_lock lock{mutex}; + callCount++; + if (callCount == expectedCallCount) { + cv.notify_one(); + } + + reportedListener->onWindowInfosReported(); + })); + } + + BackgroundExecutor::getInstance().sendCallbacks( + {[&]() { mInvoker->windowInfosChanged({}, {}, false); }}); + + std::unique_lock<std::mutex> lock{mutex}; + cv.wait(lock, [&]() { return callCount == expectedCallCount; }); + EXPECT_EQ(callCount, expectedCallCount); +} + +// Test that WindowInfosListenerInvoker#windowInfosChanged delays sending a second message until +// after the WindowInfosReportedListener is called. +TEST_F(WindowInfosListenerInvokerTest, delaysUnackedCall) { + std::mutex mutex; + std::condition_variable cv; + + int callCount = 0; + + // Simulate a slow ack by not calling the WindowInfosReportedListener. + mInvoker->addWindowInfosListener(sp<Listener>::make( + [&](const gui::WindowInfosUpdate&, const sp<gui::IWindowInfosReportedListener>&) { + std::scoped_lock lock{mutex}; + callCount++; + cv.notify_one(); + })); + + BackgroundExecutor::getInstance().sendCallbacks({[&]() { + mInvoker->windowInfosChanged({}, {}, false); + mInvoker->windowInfosChanged({}, {}, false); + }}); + + { + std::unique_lock lock{mutex}; + cv.wait(lock, [&]() { return callCount == 1; }); + } + EXPECT_EQ(callCount, 1); + + // Ack the first message. + mInvoker->onWindowInfosReported(); + + { + std::unique_lock lock{mutex}; + cv.wait(lock, [&]() { return callCount == 2; }); + } + EXPECT_EQ(callCount, 2); +} + +// Test that WindowInfosListenerInvoker#windowInfosChanged immediately sends a second message when +// forceImmediateCall is true. +TEST_F(WindowInfosListenerInvokerTest, sendsForcedMessage) { + std::mutex mutex; + std::condition_variable cv; + + int callCount = 0; + const int expectedCallCount = 2; + + // Simulate a slow ack by not calling the WindowInfosReportedListener. + mInvoker->addWindowInfosListener(sp<Listener>::make( + [&](const gui::WindowInfosUpdate&, const sp<gui::IWindowInfosReportedListener>&) { + std::scoped_lock lock{mutex}; + callCount++; + if (callCount == expectedCallCount) { + cv.notify_one(); + } + })); + + BackgroundExecutor::getInstance().sendCallbacks({[&]() { + mInvoker->windowInfosChanged({}, {}, false); + mInvoker->windowInfosChanged({}, {}, true); + }}); + + { + std::unique_lock lock{mutex}; + cv.wait(lock, [&]() { return callCount == expectedCallCount; }); + } + EXPECT_EQ(callCount, expectedCallCount); +} + +// Test that WindowInfosListenerInvoker#windowInfosChanged skips old messages when more than one +// message is delayed. +TEST_F(WindowInfosListenerInvokerTest, skipsDelayedMessage) { + std::mutex mutex; + std::condition_variable cv; + + int64_t lastUpdateId = -1; + + // Simulate a slow ack by not calling the WindowInfosReportedListener. + mInvoker->addWindowInfosListener( + sp<Listener>::make([&](const gui::WindowInfosUpdate& update, + const sp<gui::IWindowInfosReportedListener>&) { + std::scoped_lock lock{mutex}; + lastUpdateId = update.vsyncId; + cv.notify_one(); + })); + + BackgroundExecutor::getInstance().sendCallbacks({[&]() { + mInvoker->windowInfosChanged({{}, {}, /* vsyncId= */ 1, 0}, {}, false); + mInvoker->windowInfosChanged({{}, {}, /* vsyncId= */ 2, 0}, {}, false); + mInvoker->windowInfosChanged({{}, {}, /* vsyncId= */ 3, 0}, {}, false); + }}); + + { + std::unique_lock lock{mutex}; + cv.wait(lock, [&]() { return lastUpdateId == 1; }); + } + EXPECT_EQ(lastUpdateId, 1); + + // Ack the first message. The third update should be sent. + mInvoker->onWindowInfosReported(); + + { + std::unique_lock lock{mutex}; + cv.wait(lock, [&]() { return lastUpdateId == 3; }); + } + EXPECT_EQ(lastUpdateId, 3); +} + +// Test that WindowInfosListenerInvoker#windowInfosChanged immediately calls listener after a call +// where no listeners were configured. +TEST_F(WindowInfosListenerInvokerTest, noListeners) { + std::mutex mutex; + std::condition_variable cv; + + int callCount = 0; + + // Test that calling windowInfosChanged without any listeners doesn't cause the next call to be + // delayed. + BackgroundExecutor::getInstance().sendCallbacks({[&]() { + mInvoker->windowInfosChanged({}, {}, false); + mInvoker->addWindowInfosListener(sp<Listener>::make( + [&](const gui::WindowInfosUpdate&, const sp<gui::IWindowInfosReportedListener>&) { + std::scoped_lock lock{mutex}; + callCount++; + cv.notify_one(); + })); + mInvoker->windowInfosChanged({}, {}, false); + }}); + + { + std::unique_lock lock{mutex}; + cv.wait(lock, [&]() { return callCount == 1; }); + } + EXPECT_EQ(callCount, 1); +} + +} // namespace android |