| /* | |
| * 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. | |
| */ | |
| #define LOG_TAG "InputDeviceMetricsCollector" | |
| #include "InputDeviceMetricsCollector.h" | |
| #include "InputDeviceMetricsSource.h" | |
| #include <android-base/stringprintf.h> | |
| #include <input/PrintTools.h> | |
| namespace android { | |
| using android::base::StringPrintf; | |
| using std::chrono::nanoseconds; | |
| using std::chrono_literals::operator""ns; | |
| namespace { | |
| constexpr nanoseconds DEFAULT_USAGE_SESSION_TIMEOUT = std::chrono::minutes(2); | |
| /** | |
| * 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); | |
| constexpr size_t INTERACTIONS_QUEUE_CAPACITY = 500; | |
| int32_t linuxBusToInputDeviceBusEnum(int32_t linuxBus, bool isUsiStylus) { | |
| if (isUsiStylus) { | |
| // This is a stylus connected over the Universal Stylus Initiative (USI) protocol. | |
| // For metrics purposes, we treat this protocol as a separate bus. | |
| return util::INPUT_DEVICE_USAGE_REPORTED__DEVICE_BUS__USI; | |
| } | |
| // When adding cases to this switch, also add them to the copy of this method in | |
| // TouchpadInputMapper.cpp. | |
| // TODO(b/286394420): deduplicate this method with the one in TouchpadInputMapper.cpp. | |
| 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 MetricsDeviceInfo& info, | |
| 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 id: %d", info.deviceId); | |
| 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); | |
| } | |
| ALOGD_IF(DEBUG, " Uid breakdown:"); | |
| std::vector<int32_t> uids; | |
| std::vector<int32_t> durationsPerUid; | |
| for (auto& [uid, dur] : report.uidBreakdown) { | |
| uids.push_back(uid.val()); | |
| int32_t durMillis = std::chrono::duration_cast<std::chrono::milliseconds>(dur).count(); | |
| durationsPerUid.push_back(durMillis); | |
| ALOGD_IF(DEBUG, " - uid: %s\t duration: %dms", uid.toString().c_str(), | |
| durMillis); | |
| } | |
| util::stats_write(util::INPUTDEVICE_USAGE_REPORTED, info.vendor, info.product, info.version, | |
| linuxBusToInputDeviceBusEnum(info.bus, info.isUsiStylus), durationMillis, | |
| sources, durationsPerSource, uids, durationsPerUid); | |
| } | |
| } 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) | |
| : InputDeviceMetricsCollector(listener, sStatsdLogger, DEFAULT_USAGE_SESSION_TIMEOUT) {} | |
| InputDeviceMetricsCollector::InputDeviceMetricsCollector(InputListenerInterface& listener, | |
| InputDeviceMetricsLogger& logger, | |
| nanoseconds usageSessionTimeout) | |
| : mNextListener(listener), | |
| mLogger(logger), | |
| mUsageSessionTimeout(usageSessionTimeout), | |
| mInteractionsQueue(INTERACTIONS_QUEUE_CAPACITY) {} | |
| void InputDeviceMetricsCollector::notifyInputDevicesChanged( | |
| const NotifyInputDevicesChangedArgs& args) { | |
| { | |
| std::scoped_lock lock(mLock); | |
| reportCompletedSessions(); | |
| onInputDevicesChanged(args.inputDeviceInfos); | |
| } | |
| mNextListener.notify(args); | |
| } | |
| void InputDeviceMetricsCollector::notifyKey(const NotifyKeyArgs& args) { | |
| { | |
| std::scoped_lock lock(mLock); | |
| reportCompletedSessions(); | |
| const SourceProvider getSources = [&args](const MetricsDeviceInfo& info) { | |
| return std::set{getUsageSourceForKeyArgs(info.keyboardType, args)}; | |
| }; | |
| onInputDeviceUsage(DeviceId{args.deviceId}, nanoseconds(args.eventTime), getSources); | |
| } | |
| mNextListener.notify(args); | |
| } | |
| void InputDeviceMetricsCollector::notifyMotion(const NotifyMotionArgs& args) { | |
| { | |
| std::scoped_lock lock(mLock); | |
| reportCompletedSessions(); | |
| onInputDeviceUsage(DeviceId{args.deviceId}, nanoseconds(args.eventTime), | |
| [&args](const auto&) { return getUsageSourcesForMotionArgs(args); }); | |
| } | |
| mNextListener.notify(args); | |
| } | |
| void InputDeviceMetricsCollector::notifySwitch(const NotifySwitchArgs& args) { | |
| { | |
| std::scoped_lock lock(mLock); | |
| reportCompletedSessions(); | |
| } | |
| mNextListener.notify(args); | |
| } | |
| void InputDeviceMetricsCollector::notifySensor(const NotifySensorArgs& args) { | |
| { | |
| std::scoped_lock lock(mLock); | |
| reportCompletedSessions(); | |
| } | |
| mNextListener.notify(args); | |
| } | |
| void InputDeviceMetricsCollector::notifyVibratorState(const NotifyVibratorStateArgs& args) { | |
| { | |
| std::scoped_lock lock(mLock); | |
| reportCompletedSessions(); | |
| } | |
| mNextListener.notify(args); | |
| } | |
| void InputDeviceMetricsCollector::notifyDeviceReset(const NotifyDeviceResetArgs& args) { | |
| { | |
| std::scoped_lock lock(mLock); | |
| reportCompletedSessions(); | |
| } | |
| mNextListener.notify(args); | |
| } | |
| void InputDeviceMetricsCollector::notifyPointerCaptureChanged( | |
| const NotifyPointerCaptureChangedArgs& args) { | |
| { | |
| std::scoped_lock lock(mLock); | |
| reportCompletedSessions(); | |
| } | |
| mNextListener.notify(args); | |
| } | |
| void InputDeviceMetricsCollector::notifyDeviceInteraction(int32_t deviceId, nsecs_t timestamp, | |
| const std::set<Uid>& uids) { | |
| if (isIgnoredInputDeviceId(deviceId)) { | |
| return; | |
| } | |
| std::scoped_lock lock(mLock); | |
| mInteractionsQueue.push(DeviceId{deviceId}, timestamp, uids); | |
| } | |
| void InputDeviceMetricsCollector::dump(std::string& dump) { | |
| std::scoped_lock lock(mLock); | |
| dump += "InputDeviceMetricsCollector:\n"; | |
| dump += " Logged device IDs: " + dumpMapKeys(mLoggedDeviceInfos, &toString) + "\n"; | |
| dump += " Devices with active usage sessions: " + | |
| dumpMapKeys(mActiveUsageSessions, &toString) + "\n"; | |
| } | |
| void InputDeviceMetricsCollector::monitor() { | |
| std::scoped_lock lock(mLock); | |
| } | |
| void InputDeviceMetricsCollector::onInputDevicesChanged(const std::vector<InputDeviceInfo>& infos) { | |
| std::map<DeviceId, MetricsDeviceInfo> newDeviceInfos; | |
| for (const InputDeviceInfo& info : infos) { | |
| if (isIgnoredInputDeviceId(info.getId())) { | |
| continue; | |
| } | |
| const auto& i = info.getIdentifier(); | |
| newDeviceInfos.emplace(info.getId(), | |
| MetricsDeviceInfo{ | |
| .deviceId = info.getId(), | |
| .vendor = i.vendor, | |
| .product = i.product, | |
| .version = i.version, | |
| .bus = i.bus, | |
| .isUsiStylus = info.getUsiVersion().has_value(), | |
| .keyboardType = info.getKeyboardType(), | |
| }); | |
| } | |
| for (auto [deviceId, info] : mLoggedDeviceInfos) { | |
| if (newDeviceInfos.count(deviceId) != 0) { | |
| continue; | |
| } | |
| onInputDeviceRemoved(deviceId, info); | |
| } | |
| std::swap(newDeviceInfos, mLoggedDeviceInfos); | |
| } | |
| void InputDeviceMetricsCollector::onInputDeviceRemoved(DeviceId deviceId, | |
| const MetricsDeviceInfo& info) { | |
| 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(info, 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::onInputDeviceInteraction(const Interaction& interaction) { | |
| auto activeSessionIt = mActiveUsageSessions.find(std::get<DeviceId>(interaction)); | |
| if (activeSessionIt == mActiveUsageSessions.end()) { | |
| return; | |
| } | |
| activeSessionIt->second.recordInteraction(interaction); | |
| } | |
| void InputDeviceMetricsCollector::reportCompletedSessions() { | |
| // Process all pending interactions. | |
| for (auto interaction = mInteractionsQueue.pop(); interaction; | |
| interaction = mInteractionsQueue.pop()) { | |
| onInputDeviceInteraction(*interaction); | |
| } | |
| const auto currentTime = mLogger.getCurrentTime(); | |
| std::vector<DeviceId> completedUsageSessions; | |
| // Process usages for all active session to determine if any sessions have expired. | |
| for (auto& [deviceId, activeSession] : mActiveUsageSessions) { | |
| if (activeSession.checkIfCompletedAt(currentTime)) { | |
| completedUsageSessions.emplace_back(deviceId); | |
| } | |
| } | |
| // Close out and log all expired usage sessions. | |
| 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, 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; | |
| } | |
| void InputDeviceMetricsCollector::ActiveSession::recordInteraction(const Interaction& interaction) { | |
| const auto sessionExpiryTime = mDeviceSession.end + mUsageSessionTimeout; | |
| const auto timestamp = std::get<nanoseconds>(interaction); | |
| if (timestamp >= sessionExpiryTime) { | |
| // This interaction occurred after the device's current active session is set to expire. | |
| // Ignore it. | |
| return; | |
| } | |
| for (Uid uid : std::get<std::set<Uid>>(interaction)) { | |
| auto [activeUidIt, inserted] = mActiveSessionsByUid.try_emplace(uid, timestamp, timestamp); | |
| if (!inserted) { | |
| activeUidIt->second.end = timestamp; | |
| } | |
| } | |
| } | |
| 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); | |
| } | |
| std::vector<Uid> completedUidSessionsForDevice; | |
| for (auto& [uid, session] : mActiveSessionsByUid) { | |
| if (session.end <= sessionExpiryTime) { | |
| completedUidSessionsForDevice.emplace_back(uid); | |
| } | |
| } | |
| for (Uid uid : completedUidSessionsForDevice) { | |
| auto it = mActiveSessionsByUid.find(uid); | |
| const auto& [_, session] = *it; | |
| mUidUsageBreakdown.emplace_back(uid, session.end - session.start); | |
| mActiveSessionsByUid.erase(it); | |
| } | |
| // This active session has expired if there are no more active source sessions tracked. | |
| 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(); | |
| for (const auto& [uid, uidSession] : mActiveSessionsByUid) { | |
| mUidUsageBreakdown.emplace_back(uid, uidSession.end - uidSession.start); | |
| } | |
| mActiveSessionsByUid.clear(); | |
| return {deviceUsageDuration, mSourceUsageBreakdown, mUidUsageBreakdown}; | |
| } | |
| } // namespace android |