diff options
-rw-r--r-- | include/input/InputDevice.h | 3 | ||||
-rw-r--r-- | include/input/PrintTools.h | 14 | ||||
-rw-r--r-- | services/inputflinger/InputDeviceMetricsCollector.cpp | 152 | ||||
-rw-r--r-- | services/inputflinger/InputDeviceMetricsCollector.h | 45 | ||||
-rw-r--r-- | services/inputflinger/tests/Android.bp | 1 | ||||
-rw-r--r-- | services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp | 264 |
6 files changed, 478 insertions, 1 deletions
diff --git a/include/input/InputDevice.h b/include/input/InputDevice.h index 1a40fdb90c..ffb2cb1fc1 100644 --- a/include/input/InputDevice.h +++ b/include/input/InputDevice.h @@ -68,6 +68,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 */ 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/services/inputflinger/InputDeviceMetricsCollector.cpp b/services/inputflinger/InputDeviceMetricsCollector.cpp index c6089c8d2a..50f336b952 100644 --- a/services/inputflinger/InputDeviceMetricsCollector.cpp +++ b/services/inputflinger/InputDeviceMetricsCollector.cpp @@ -17,52 +17,202 @@ #define LOG_TAG "InputDeviceMetricsCollector" #include "InputDeviceMetricsCollector.h" +#include <android-base/stringprintf.h> +#include <input/PrintTools.h> +#include <linux/input.h> +#include <statslog.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, + nanoseconds sessionDuration) override { + const int32_t durationMillis = + std::chrono::duration_cast<std::chrono::milliseconds>(sessionDuration).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); + + util::stats_write(util::INPUTDEVICE_USAGE_REPORTED, identifier.vendor, identifier.product, + identifier.version, linuxBusToInputDeviceBusEnum(identifier.bus), + durationMillis, /*usage_sources=*/empty, + /*usage_durations_per_source=*/empty, /*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 + 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) { + processUsages(); + onInputDevicesChanged(args.inputDeviceInfos); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyConfigurationChanged( const NotifyConfigurationChangedArgs& args) { + processUsages(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyKey(const NotifyKeyArgs& args) { + processUsages(); + onInputDeviceUsage(DeviceId{args.deviceId}, nanoseconds(args.eventTime)); + mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyMotion(const NotifyMotionArgs& args) { + processUsages(); + onInputDeviceUsage(DeviceId{args.deviceId}, nanoseconds(args.eventTime)); + mNextListener.notify(args); } void InputDeviceMetricsCollector::notifySwitch(const NotifySwitchArgs& args) { + processUsages(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifySensor(const NotifySensorArgs& args) { + processUsages(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyVibratorState(const NotifyVibratorStateArgs& args) { + processUsages(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyDeviceReset(const NotifyDeviceResetArgs& args) { + processUsages(); mNextListener.notify(args); } void InputDeviceMetricsCollector::notifyPointerCaptureChanged( const NotifyPointerCaptureChangedArgs& args) { + processUsages(); 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, InputDeviceIdentifier> newDeviceIds; + + for (const InputDeviceInfo& info : infos) { + if (isIgnoredInputDeviceId(info.getId())) { + continue; + } + newDeviceIds.emplace(info.getId(), info.getIdentifier()); + } + + for (auto [deviceId, identifier] : mLoggedDeviceInfos) { + if (newDeviceIds.count(deviceId) != 0) { + continue; + } + onInputDeviceRemoved(deviceId, identifier); + } + + std::swap(newDeviceIds, mLoggedDeviceInfos); +} + +void InputDeviceMetricsCollector::onInputDeviceRemoved(DeviceId deviceId, + const InputDeviceIdentifier& identifier) { + // Report usage for that device if there is an active session. + auto it = mActiveUsageSessions.find(deviceId); + if (it != mActiveUsageSessions.end()) { + mLogger.logInputDeviceUsageReported(identifier, it->second.end - it->second.start); + mActiveUsageSessions.erase(it); + } + // We don't remove this from mLoggedDeviceInfos because it will be updated in + // onInputDevicesChanged(). +} + +void InputDeviceMetricsCollector::onInputDeviceUsage(DeviceId deviceId, nanoseconds eventTime) { + if (mLoggedDeviceInfos.count(deviceId) == 0) { + // Do not track usage for devices that are not logged. + return; + } + + auto [it, inserted] = mActiveUsageSessions.try_emplace(deviceId, eventTime, eventTime); + if (!inserted) { + it->second.end = eventTime; + } +} + +void InputDeviceMetricsCollector::processUsages() { + const auto usageSessionExpiryTime = mLogger.getCurrentTime() - mUsageSessionTimeout; + + std::vector<DeviceId> completedUsageSessions; + + for (const auto& [deviceId, usageSession] : mActiveUsageSessions) { + if (usageSession.end <= usageSessionExpiryTime) { + completedUsageSessions.emplace_back(deviceId); + } + } + + for (DeviceId deviceId : completedUsageSessions) { + const auto it = mLoggedDeviceInfos.find(deviceId); + LOG_ALWAYS_FATAL_IF(it == mLoggedDeviceInfos.end()); + + const auto& session = mActiveUsageSessions[deviceId]; + mLogger.logInputDeviceUsageReported(it->second, session.end - session.start); + + mActiveUsageSessions.erase(deviceId); + } } } // namespace android diff --git a/services/inputflinger/InputDeviceMetricsCollector.h b/services/inputflinger/InputDeviceMetricsCollector.h index c95907540a..c9daa92f2a 100644 --- a/services/inputflinger/InputDeviceMetricsCollector.h +++ b/services/inputflinger/InputDeviceMetricsCollector.h @@ -18,6 +18,12 @@ #include "InputListener.h" +#include <ftl/mixins.h> +#include <input/InputDevice.h> +#include <chrono> +#include <map> +#include <vector> + namespace android { /** @@ -34,11 +40,24 @@ public: virtual void dump(std::string& dump) = 0; }; +/** The logging interface for the metrics collector, injected for testing. */ +class InputDeviceMetricsLogger { +public: + virtual std::chrono::nanoseconds getCurrentTime() = 0; + virtual void logInputDeviceUsageReported(const InputDeviceIdentifier&, + std::chrono::nanoseconds duration) = 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 +72,32 @@ 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 std::string toString(const DeviceId& id) { + return std::to_string(ftl::to_underlying(id)); + } + + std::map<DeviceId, InputDeviceIdentifier> mLoggedDeviceInfos; + + struct UsageSession { + std::chrono::nanoseconds start; + std::chrono::nanoseconds end; + }; + // The input devices that currently have active usage sessions. + std::map<DeviceId, UsageSession> mActiveUsageSessions; + + void onInputDevicesChanged(const std::vector<InputDeviceInfo>& infos); + void onInputDeviceRemoved(DeviceId deviceId, const InputDeviceIdentifier& identifier); + void onInputDeviceUsage(DeviceId deviceId, std::chrono::nanoseconds eventTime); + void processUsages(); }; } // namespace android diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp index 52277ff078..ec67a1d1c4 100644 --- a/services/inputflinger/tests/Android.bp +++ b/services/inputflinger/tests/Android.bp @@ -47,6 +47,7 @@ cc_test { "FocusResolver_test.cpp", "GestureConverter_test.cpp", "HardwareStateConverter_test.cpp", + "InputDeviceMetricsCollector_test.cpp", "InputMapperTest.cpp", "InputProcessor_test.cpp", "InputProcessorConverter_test.cpp", diff --git a/services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp b/services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp new file mode 100644 index 0000000000..d82e4266db --- /dev/null +++ b/services/inputflinger/tests/InputDeviceMetricsCollector_test.cpp @@ -0,0 +1,264 @@ +/* + * 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 "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 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"; + +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) { + auto info = InputDeviceInfo(); + info.initialize(id, /*generation=*/1, /*controllerNumber=*/1, getIdentifier(id), "alias", + /*isExternal=*/false, /*hasMic=*/false, ADISPLAY_ID_NONE); + info.addSource(AINPUT_SOURCE_TOUCHSCREEN); + return info; +} + +} // namespace + +// --- InputDeviceMetricsCollectorTest --- + +class InputDeviceMetricsCollectorTest : public testing::Test, InputDeviceMetricsLogger { +protected: + TestInputListener mTestListener; + InputDeviceMetricsCollector mMetricsCollector{mTestListener, *this, USAGE_TIMEOUT}; + + void assertUsageLogged(InputDeviceIdentifier identifier, nanoseconds duration) { + ASSERT_GE(mLoggedUsageSessions.size(), 1u); + const auto& session = *mLoggedUsageSessions.begin(); + ASSERT_EQ(identifier, std::get<InputDeviceIdentifier>(session)); + ASSERT_EQ(duration, std::get<nanoseconds>(session)); + mLoggedUsageSessions.erase(mLoggedUsageSessions.begin()); + } + + void assertUsageNotLogged() { ASSERT_TRUE(mLoggedUsageSessions.empty()); } + + void setCurrentTime(nanoseconds time) { mCurrentTime = time; } + + NotifyMotionArgs generateMotionArgs(int32_t deviceId) { + PointerProperties pointerProperties{}; + pointerProperties.id = 0; + pointerProperties.toolType = ToolType::FINGER; + + PointerCoords pointerCoords{}; + pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_X, 100); + pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_Y, 200); + + return {/*id=*/0, + mCurrentTime.count(), + /*readTime=*/0, + deviceId, + AINPUT_SOURCE_TOUCHSCREEN, + /*displayId=*/0, + POLICY_FLAG_PASS_TO_USER, + AMOTION_EVENT_ACTION_MOVE, + /*actionButton=*/0, + /*flags=*/0, + AMETA_NONE, + /*buttonState=*/0, + MotionClassification::NONE, + AMOTION_EVENT_EDGE_FLAG_NONE, + /*pointerCount=*/1, + &pointerProperties, + &pointerCoords, + /*xPrecision=*/0, + /*yPrecision=*/0, + AMOTION_EVENT_INVALID_CURSOR_POSITION, + AMOTION_EVENT_INVALID_CURSOR_POSITION, + mCurrentTime.count(), + /*videoFrames=*/{}}; + } + +private: + std::vector<std::tuple<InputDeviceIdentifier, nanoseconds>> mLoggedUsageSessions; + nanoseconds mCurrentTime{TIME}; + + nanoseconds getCurrentTime() override { return mCurrentTime; } + + void logInputDeviceUsageReported(const InputDeviceIdentifier& identifier, + nanoseconds duration) override { + mLoggedUsageSessions.emplace_back(identifier, duration); + } +}; + +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()); +} + +} // namespace android |