Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 58bda65d authored by Yeabkal Wubshit's avatar Yeabkal Wubshit
Browse files

Rotary encoder rotation count telemetry

Implements a telemetry using the Telemetry Express API to log full
rotations on rotary encoder devices. By default, logs are disabled for
rotations. A rotary input device can change the minimum logged rotation
value via the `rotary_encoder.min_rotations_to_log` IDC property, by
setting it to a positive integer value.

Bug: 370353565
Test: atest RotaryEncoderInputMapperTest
Test: manual with custom logs
Flag: com.android.input.flags.rotary_input_telemetry

Change-Id: I5162b0d343936ac8049c24835cd8e57d44643516
parent 0f2f4523
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -207,3 +207,10 @@ flag {
  description: "Allow user to enable key repeats or configure timeout before key repeat and key repeat delay rates."
  bug: "336585002"
}

flag {
  name: "rotary_input_telemetry"
  namespace: "wear_frameworks"
  description: "Enable telemetry for rotary input"
  bug: "370353565"
}
+4 −0
Original line number Diff line number Diff line
@@ -90,10 +90,14 @@ cc_defaults {
        "libstatslog",
        "libstatspull",
        "libutils",
        "libstatssocket",
    ],
    static_libs: [
        "libchrome-gestures",
        "libui-types",
        "libexpresslog",
        "libtextclassifier_hash_static",
        "libstatslog_express",
    ],
    header_libs: [
        "libbatteryservice_headers",
+51 −2
Original line number Diff line number Diff line
@@ -20,6 +20,8 @@

#include "RotaryEncoderInputMapper.h"

#include <Counter.h>
#include <com_android_input_flags.h>
#include <utils/Timers.h>
#include <optional>

@@ -27,14 +29,26 @@

namespace android {

using android::expresslog::Counter;

constexpr float kDefaultResolution = 0;
constexpr float kDefaultScaleFactor = 1.0f;
constexpr int32_t kDefaultMinRotationsToLog = 3;

RotaryEncoderInputMapper::RotaryEncoderInputMapper(InputDeviceContext& deviceContext,
                                                   const InputReaderConfiguration& readerConfig)
      : RotaryEncoderInputMapper(deviceContext, readerConfig,
                                 Counter::logIncrement /* telemetryLogCounter */) {}

RotaryEncoderInputMapper::RotaryEncoderInputMapper(
        InputDeviceContext& deviceContext, const InputReaderConfiguration& readerConfig,
        std::function<void(const char*, int64_t)> telemetryLogCounter)
      : InputMapper(deviceContext, readerConfig),
        mSource(AINPUT_SOURCE_ROTARY_ENCODER),
        mScalingFactor(kDefaultScaleFactor),
        mOrientation(ui::ROTATION_0) {}
        mResolution(kDefaultResolution),
        mOrientation(ui::ROTATION_0),
        mTelemetryLogCounter(telemetryLogCounter) {}

RotaryEncoderInputMapper::~RotaryEncoderInputMapper() {}

@@ -51,6 +65,7 @@ void RotaryEncoderInputMapper::populateDeviceInfo(InputDeviceInfo& info) {
        if (!res.has_value()) {
            ALOGW("Rotary Encoder device configuration file didn't specify resolution!\n");
        }
        mResolution = res.value_or(kDefaultResolution);
        std::optional<float> scalingFactor = config.getFloat("device.scalingFactor");
        if (!scalingFactor.has_value()) {
            ALOGW("Rotary Encoder device configuration file didn't specify scaling factor,"
@@ -59,7 +74,22 @@ void RotaryEncoderInputMapper::populateDeviceInfo(InputDeviceInfo& info) {
        }
        mScalingFactor = scalingFactor.value_or(kDefaultScaleFactor);
        info.addMotionRange(AMOTION_EVENT_AXIS_SCROLL, mSource, -1.0f, 1.0f, 0.0f, 0.0f,
                            res.value_or(0.0f) * mScalingFactor);
                            mResolution * mScalingFactor);

        if (com::android::input::flags::rotary_input_telemetry()) {
            mMinRotationsToLog = config.getInt("rotary_encoder.min_rotations_to_log");
            if (!mMinRotationsToLog.has_value()) {
                ALOGI("Rotary Encoder device configuration file didn't specify min log rotation.");
            } else if (*mMinRotationsToLog <= 0) {
                ALOGE("Rotary Encoder device configuration specified non-positive min log rotation "
                      ": %d. Telemetry logging of rotations disabled.",
                      *mMinRotationsToLog);
                mMinRotationsToLog = {};
            } else {
                ALOGD("Rotary Encoder telemetry enabled. mMinRotationsToLog=%d",
                      *mMinRotationsToLog);
            }
        }
    }
}

@@ -121,10 +151,29 @@ std::list<NotifyArgs> RotaryEncoderInputMapper::process(const RawEvent& rawEvent
    return out;
}

void RotaryEncoderInputMapper::logScroll(float scroll) {
    if (mResolution <= 0 || !mMinRotationsToLog) return;

    mUnloggedScrolls += fabs(scroll);

    // unitsPerRotation = (2 * PI * radians) * (units per radian (i.e. resolution))
    const float unitsPerRotation = 2 * M_PI * mResolution;
    const float scrollsPerMinRotationsToLog = *mMinRotationsToLog * unitsPerRotation;
    const int32_t numMinRotationsToLog =
            static_cast<int32_t>(mUnloggedScrolls / scrollsPerMinRotationsToLog);
    mUnloggedScrolls = std::fmod(mUnloggedScrolls, scrollsPerMinRotationsToLog);
    if (numMinRotationsToLog) {
        mTelemetryLogCounter("input.value_rotary_input_device_full_rotation_count",
                             numMinRotationsToLog * (*mMinRotationsToLog));
    }
}

std::list<NotifyArgs> RotaryEncoderInputMapper::sync(nsecs_t when, nsecs_t readTime) {
    std::list<NotifyArgs> out;

    float scroll = mRotaryEncoderScrollAccumulator.getRelativeVWheel();
    logScroll(scroll);

    if (mSlopController) {
        scroll = mSlopController->consumeEvent(when, scroll);
    }
+26 −0
Original line number Diff line number Diff line
@@ -46,13 +46,39 @@ private:

    int32_t mSource;
    float mScalingFactor;
    /** Units per rotation, provided via the `device.res` IDC property. */
    float mResolution;
    ui::Rotation mOrientation;
    /**
     * The minimum number of rotations to log for telemetry.
     * Provided via `rotary_encoder.min_rotations_to_log` IDC property. If no value is provided in
     * the IDC file, or if a non-positive value is provided, the telemetry will be disabled, and
     * this value is set to the empty optional.
     */
    std::optional<int32_t> mMinRotationsToLog;
    /**
     * A function to log count for telemetry.
     * The char* is the logging key, and the int64_t is the value to log.
     * Abstracting the actual logging APIs via this function is helpful for simple unit testing.
     */
    std::function<void(const char*, int64_t)> mTelemetryLogCounter;
    ui::LogicalDisplayId mDisplayId = ui::LogicalDisplayId::INVALID;
    std::unique_ptr<SlopController> mSlopController;

    /** Amount of raw scrolls (pre-slop) not yet logged for telemetry. */
    float mUnloggedScrolls = 0;

    explicit RotaryEncoderInputMapper(InputDeviceContext& deviceContext,
                                      const InputReaderConfiguration& readerConfig);

    /** This is a test constructor that allows injecting the expresslog Counter logic. */
    RotaryEncoderInputMapper(InputDeviceContext& deviceContext,
                             const InputReaderConfiguration& readerConfig,
                             std::function<void(const char*, int64_t)> expressLogCounter);
    [[nodiscard]] std::list<NotifyArgs> sync(nsecs_t when, nsecs_t readTime);

    /** Logs a given amount of scroll for telemetry. */
    void logScroll(float scroll);
};

} // namespace android
+149 −0
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@

#include <android-base/logging.h>
#include <android_companion_virtualdevice_flags.h>
#include <com_android_input_flags.h>
#include <flag_macros.h>
#include <gtest/gtest.h>
#include <input/DisplayViewport.h>
#include <linux/input-event-codes.h>
@@ -100,6 +102,15 @@ protected:
        EXPECT_CALL(mMockEventHub, hasRelativeAxis(EVENTHUB_ID, REL_HWHEEL_HI_RES))
                .WillRepeatedly(Return(false));
    }

    std::map<const char*, int64_t> mTelemetryLogCounts;

    /**
     * A fake function for telemetry logging.
     * Records the log counts in the `mTelemetryLogCounts` map.
     */
    std::function<void(const char*, int64_t)> mTelemetryLogCounter =
            [this](const char* key, int64_t value) { mTelemetryLogCounts[key] += value; };
};

TEST_F(RotaryEncoderInputMapperTest, ConfigureDisplayIdWithAssociatedViewport) {
@@ -187,4 +198,142 @@ TEST_F(RotaryEncoderInputMapperTest, HighResScrollIgnoresRegularScroll) {
                              WithMotionAction(AMOTION_EVENT_ACTION_SCROLL), WithScroll(0.5f)))));
}

TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, RotaryInputTelemetryFlagOff_NoRotationLogging,
                  REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(com::android::input::flags,
                                                       rotary_input_telemetry))) {
    mPropertyMap.addProperty("device.res", "3");
    mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
                                                          mTelemetryLogCounter);
    InputDeviceInfo info;
    mMapper->populateDeviceInfo(info);

    std::list<NotifyArgs> args;
    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 70);
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);

    ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
              mTelemetryLogCounts.end());
}

TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, ZeroResolution_NoRotationLogging,
                  REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
                                                      rotary_input_telemetry))) {
    mPropertyMap.addProperty("device.res", "-3");
    mPropertyMap.addProperty("rotary_encoder.min_rotations_to_log", "2");
    mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
                                                          mTelemetryLogCounter);
    InputDeviceInfo info;
    mMapper->populateDeviceInfo(info);

    std::list<NotifyArgs> args;
    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 700);
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);

    ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
              mTelemetryLogCounts.end());
}

TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, NegativeMinLogRotation_NoRotationLogging,
                  REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
                                                      rotary_input_telemetry))) {
    mPropertyMap.addProperty("device.res", "3");
    mPropertyMap.addProperty("rotary_encoder.min_rotations_to_log", "-2");
    mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
                                                          mTelemetryLogCounter);
    InputDeviceInfo info;
    mMapper->populateDeviceInfo(info);

    std::list<NotifyArgs> args;
    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 700);
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);

    ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
              mTelemetryLogCounts.end());
}

TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, ZeroMinLogRotation_NoRotationLogging,
                  REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
                                                      rotary_input_telemetry))) {
    mPropertyMap.addProperty("device.res", "3");
    mPropertyMap.addProperty("rotary_encoder.min_rotations_to_log", "0");
    mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
                                                          mTelemetryLogCounter);
    InputDeviceInfo info;
    mMapper->populateDeviceInfo(info);

    std::list<NotifyArgs> args;
    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 700);
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);

    ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
              mTelemetryLogCounts.end());
}

TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, NoMinLogRotation_NoRotationLogging,
                  REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
                                                      rotary_input_telemetry))) {
    // 3 units per radian, 2 * M_PI * 3 = ~18.85 units per rotation.
    mPropertyMap.addProperty("device.res", "3");
    mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
                                                          mTelemetryLogCounter);
    InputDeviceInfo info;
    mMapper->populateDeviceInfo(info);

    std::list<NotifyArgs> args;
    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 700);
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);

    ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
              mTelemetryLogCounts.end());
}

TEST_F_WITH_FLAGS(RotaryEncoderInputMapperTest, RotationLogging,
                  REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(com::android::input::flags,
                                                      rotary_input_telemetry))) {
    // 3 units per radian, 2 * M_PI * 3 = ~18.85 units per rotation.
    // Multiples of `unitsPerRoation`, to easily follow the assertions below.
    // [18.85, 37.7, 56.55, 75.4, 94.25, 113.1, 131.95, 150.8]
    mPropertyMap.addProperty("device.res", "3");
    mPropertyMap.addProperty("rotary_encoder.min_rotations_to_log", "2");

    mMapper = createInputMapper<RotaryEncoderInputMapper>(*mDeviceContext, mReaderConfiguration,
                                                          mTelemetryLogCounter);
    InputDeviceInfo info;
    mMapper->populateDeviceInfo(info);

    std::list<NotifyArgs> args;
    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 15); // total scroll = 15
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
    ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
              mTelemetryLogCounts.end());

    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 13); // total scroll = 28
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
    // Expect 0 since `min_rotations_to_log` = 2, and total scroll 28 only has 1 rotation.
    ASSERT_EQ(mTelemetryLogCounts.find("input.value_rotary_input_device_full_rotation_count"),
              mTelemetryLogCounts.end());

    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, 10); // total scroll = 38
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
    // Total scroll includes >= `min_rotations_to_log` (2), expect log.
    ASSERT_EQ(mTelemetryLogCounts["input.value_rotary_input_device_full_rotation_count"], 2);

    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, -22); // total scroll = 60
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
    // Expect no additional telemetry. Total rotation is 3, and total unlogged rotation is 1, which
    // is less than `min_rotations_to_log`.
    ASSERT_EQ(mTelemetryLogCounts["input.value_rotary_input_device_full_rotation_count"], 2);

    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, -16); // total scroll = 76
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
    // Total unlogged rotation >= `min_rotations_to_log` (2), so expect 2 more logged rotation.
    ASSERT_EQ(mTelemetryLogCounts["input.value_rotary_input_device_full_rotation_count"], 4);

    args += process(ARBITRARY_TIME, EV_REL, REL_WHEEL, -76); // total scroll = 152
    args += process(ARBITRARY_TIME, EV_SYN, SYN_REPORT, 0);
    // Total unlogged scroll >= 4*`min_rotations_to_log`. Expect *all* unlogged rotations to be
    // logged, even if that's more than multiple of `min_rotations_to_log`.
    ASSERT_EQ(mTelemetryLogCounts["input.value_rotary_input_device_full_rotation_count"], 8);
}

} // namespace android
 No newline at end of file