blob: 71f37176c3ddc4ef656e5bdc9945b5d64bf808b5 [file] [log] [blame]
/*
* 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 "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,
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
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)
: 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