| /* |
| * 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::notifyConfigurationChanged( |
| const NotifyConfigurationChangedArgs& args) { |
| { |
| std::scoped_lock lock(mLock); |
| reportCompletedSessions(); |
| } |
| 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 |