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

Commit c5766da6 authored by Yeabkal Wubshit's avatar Yeabkal Wubshit
Browse files

Create InputDevice ViewBehavior

ViewBehavior will be a static inner class for InputDevice, providing
View-related behaviors pertaining to an InputDevice. We have added an
API in InputDevice to return its ViewBehavior.

This will be helpful if different InputDevice properties affect the way
an input- generated MotionEvent should be handled by a View. For
example, we have implemented a "shouldSmoothScroll" API in this change.
This API is to tell Views processing motions generated by the
InputDevice to prefer animating scrolls caused by such motions, to
deliver a better quality scrolling UX than compared to a regular scroll
that jumps by X pixels in a single frame.

Note that, a single ViewBehavior instance contains all behaviors for a
single InputDevice. This is unlike the other inner static class of
InputDevice: MotionRange. This decision of making a single ViewBehavior
contain all behaviors is made because not all View behaviors may depend
on source+axis (e.g. there could be a behavior that is globally
applicable to the whole InputDevice).

Bug: 246946631
Test: unit tests, manual
Change-Id: I65cf7d6556e26d199c9021f53bdae183f21d3c6d
parent 04d99941
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -20266,6 +20266,7 @@ package android.hardware.input {
    method @Nullable public android.hardware.input.HostUsiVersion getHostUsiVersion(@NonNull android.view.Display);
    method @Nullable public android.view.InputDevice getInputDevice(int);
    method public int[] getInputDeviceIds();
    method @FlaggedApi("com.android.input.flags.input_device_view_behavior_api") @Nullable public android.view.InputDevice.ViewBehavior getInputDeviceViewBehavior(int);
    method @FloatRange(from=0, to=1) public float getMaximumObscuringOpacityForTouch();
    method public boolean isStylusPointerIconEnabled();
    method public void registerInputDeviceListener(android.hardware.input.InputManager.InputDeviceListener, android.os.Handler);
@@ -50437,6 +50438,10 @@ package android.view {
    method public boolean isFromSource(int);
  }
  @FlaggedApi("com.android.input.flags.input_device_view_behavior_api") public static final class InputDevice.ViewBehavior {
    method @FlaggedApi("com.android.input.flags.input_device_view_behavior_api") public boolean shouldSmoothScroll(int, int);
  }
  public abstract class InputEvent implements android.os.Parcelable {
    method public int describeContents();
    method public final android.view.InputDevice getDevice();
+19 −0
Original line number Diff line number Diff line
@@ -16,9 +16,11 @@

package android.hardware.input;

import static com.android.input.flags.Flags.FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API;
import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag;

import android.Manifest;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -293,6 +295,23 @@ public final class InputManager {
        return mGlobal.getInputDevice(id);
    }

    /**
     * Gets the {@link InputDevice.ViewBehavior} of the input device with a given {@code id}.
     *
     * <p>Use this API to query a fresh view behavior instance whenever the input device
     * changes.
     *
     * @param deviceId the id of the input device whose view behavior is being requested.
     * @return the view behavior of the input device with the provided id, or {@code null} if there
     *      is not input device with the provided id.
     */
    @FlaggedApi(FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API)
    @Nullable
    public InputDevice.ViewBehavior getInputDeviceViewBehavior(int deviceId) {
        InputDevice device = getInputDevice(deviceId);
        return device == null ? null : device.getViewBehavior();
    }

    /**
     * Gets information about the input device with the specified descriptor.
     * @param descriptor The input device descriptor.
+120 −0
Original line number Diff line number Diff line
@@ -16,7 +16,10 @@

package android.view;

import static com.android.input.flags.Flags.FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API;

import android.Manifest;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -28,6 +31,7 @@ import android.hardware.BatteryState;
import android.hardware.SensorManager;
import android.hardware.input.HostUsiVersion;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
import android.hardware.input.InputManagerGlobal;
import android.hardware.lights.LightsManager;
import android.icu.util.ULocale;
@@ -90,6 +94,8 @@ public final class InputDevice implements Parcelable {
    private final int mAssociatedDisplayId;
    private final ArrayList<MotionRange> mMotionRanges = new ArrayList<MotionRange>();

    private final ViewBehavior mViewBehavior = new ViewBehavior(this);

    @GuardedBy("mMotionRanges")
    private Vibrator mVibrator; // guarded by mMotionRanges during initialization

@@ -539,6 +545,8 @@ public final class InputDevice implements Parcelable {
            addMotionRange(in.readInt(), in.readInt(), in.readFloat(), in.readFloat(),
                    in.readFloat(), in.readFloat(), in.readFloat());
        }

        mViewBehavior.mShouldSmoothScroll = in.readBoolean();
    }

    /**
@@ -571,6 +579,7 @@ public final class InputDevice implements Parcelable {
        private int mUsiVersionMinor = -1;
        private int mAssociatedDisplayId = Display.INVALID_DISPLAY;
        private List<MotionRange> mMotionRanges = new ArrayList<>();
        private boolean mShouldSmoothScroll;

        /** @see InputDevice#getId() */
        public Builder setId(int id) {
@@ -706,6 +715,16 @@ public final class InputDevice implements Parcelable {
            return this;
        }

        /**
         * Sets the view behavior for smooth scrolling ({@code false} by default).
         *
         * @see ViewBehavior#shouldSmoothScroll(int, int)
         */
        public Builder setShouldSmoothScroll(boolean shouldSmoothScroll) {
            mShouldSmoothScroll = shouldSmoothScroll;
            return this;
        }

        /** Build {@link InputDevice}. */
        public InputDevice build() {
            InputDevice device = new InputDevice(
@@ -745,6 +764,8 @@ public final class InputDevice implements Parcelable {
                        range.getResolution());
            }

            device.setShouldSmoothScroll(mShouldSmoothScroll);

            return device;
        }
    }
@@ -1123,6 +1144,22 @@ public final class InputDevice implements Parcelable {
        return mMotionRanges;
    }

    /**
     * Provides the {@link ViewBehavior} for the device.
     *
     * <p>This behavior is designed to be obtained using the
     * {@link InputManager#getInputDeviceViewBehavior(int)} API, to allow associating the behavior
     * with a {@link Context} (since input device is not associated with a context).
     * The ability to associate the behavior with a context opens capabilities like linking the
     * behavior to user settings, for example.
     *
     * @hide
     */
    @NonNull
    public ViewBehavior getViewBehavior() {
        return mViewBehavior;
    }

    // Called from native code.
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private void addMotionRange(int axis, int source,
@@ -1130,6 +1167,11 @@ public final class InputDevice implements Parcelable {
        mMotionRanges.add(new MotionRange(axis, source, min, max, flat, fuzz, resolution));
    }

    // Called from native code.
    private void setShouldSmoothScroll(boolean shouldSmoothScroll) {
        mViewBehavior.mShouldSmoothScroll = shouldSmoothScroll;
    }

    /**
     * Returns the Bluetooth address of this input device, if known.
     *
@@ -1447,6 +1489,82 @@ public final class InputDevice implements Parcelable {
        }
    }

    /**
     * Provides information on how views processing {@link MotionEvent}s generated by this input
     * device should respond to the events. Use {@link InputManager#getInputDeviceViewBehavior(int)}
     * to get an instance of the view behavior for an input device.
     *
     * <p>See an example below how a {@link View} can use this class to determine and apply the
     * scrolling behavior for a generic {@link MotionEvent}.
     *
     * <pre>{@code
     *     public boolean onGenericMotionEvent(MotionEvent event) {
     *         InputManager manager = context.getSystemService(InputManager.class);
     *         ViewBehavior viewBehavior = manager.getInputDeviceViewBehavior(event.getDeviceId());
     *         // Assume a helper function that tells us which axis to use for scrolling purpose.
     *         int axis = getScrollAxisForGenericMotionEvent(event);
     *         int source = event.getSource();
     *
     *         boolean shouldSmoothScroll =
     *                 viewBehavior != null && viewBehavior.shouldSmoothScroll(axis, source);
     *         // Proceed to running the scrolling logic...
     *     }
     * }</pre>
     *
     * @see InputManager#getInputDeviceViewBehavior(int)
     */
    @FlaggedApi(FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API)
    public static final class ViewBehavior {
        private static final boolean DEFAULT_SHOULD_SMOOTH_SCROLL = false;

        private final InputDevice mInputDevice;

        // TODO(b/246946631): implement support for InputDevices to adjust this configuration
        // by axis and source. When implemented, the axis/source specific config will take
        // precedence over this global config.
        /** A global smooth scroll configuration applying to all motion axis and input source. */
        private boolean mShouldSmoothScroll = DEFAULT_SHOULD_SMOOTH_SCROLL;

        /** @hide */
        public ViewBehavior(@NonNull InputDevice inputDevice) {
            mInputDevice = inputDevice;
        }

        /**
         * Returns whether a view should smooth scroll when scrolling due to a {@link MotionEvent}
         * generated by the input device.
         *
         * <p>Smooth scroll in this case refers to a scroll that animates the transition between
         * the starting and ending positions of the scroll. When this method returns {@code true},
         * views should try to animate a scroll generated by this device at the given axis and with
         * the given source to produce a good scroll user experience. If this method returns
         * {@code false}, animating scrolls is not necessary.
         *
         * <p>If the input device does not have a {@link MotionRange} with the provided axis and
         * source, this method returns {@code false}.
         *
         * @param axis the {@link MotionEvent} axis whose value is used to get the scroll extent.
         * @param source the {link InputDevice} source from which the {@link MotionEvent} that
         *      triggers the scroll came.
         * @return {@code true} if smooth scrolling should be used for the scroll, or {@code false}
         *      if smooth scrolling is not necessary, or if the provided axis and source combination
         *      is not available for the input device.
         */
        @FlaggedApi(FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API)
        public boolean shouldSmoothScroll(int axis, int source) {
            // Note: although we currently do not use axis and source in computing the return value,
            // we will keep the API params to avoid further public API changes when we start
            // supporting axis/source configuration. Also, having these params lets OEMs provide
            // their custom implementation of the API that depends on axis and source.

            // TODO(b/246946631): speed up computation using caching of results.
            if (mInputDevice.getMotionRange(axis, source) == null) {
                return false;
            }
            return mShouldSmoothScroll;
        }
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        mKeyCharacterMap.writeToParcel(out, flags);
@@ -1484,6 +1602,8 @@ public final class InputDevice implements Parcelable {
            out.writeFloat(range.mFuzz);
            out.writeFloat(range.mResolution);
        }

        out.writeBoolean(mViewBehavior.mShouldSmoothScroll);
    }

    @Override
+18 −4
Original line number Diff line number Diff line
@@ -14,17 +14,16 @@
 * limitations under the License.
 */

#include <input/Input.h>
#include "android_view_InputDevice.h"

#include <android_runtime/AndroidRuntime.h>
#include <com_android_input_flags.h>
#include <input/Input.h>
#include <jni.h>
#include <nativehelper/JNIHelp.h>

#include <nativehelper/ScopedLocalRef.h>

#include "android_view_InputDevice.h"
#include "android_view_KeyCharacterMap.h"

#include "core_jni_helpers.h"

namespace android {
@@ -34,6 +33,7 @@ static struct {

    jmethodID ctor;
    jmethodID addMotionRange;
    jmethodID setShouldSmoothScroll;
} gInputDeviceClassInfo;

jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& deviceInfo) {
@@ -103,6 +103,18 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi
        }
    }

    if (com::android::input::flags::input_device_view_behavior_api()) {
        const InputDeviceViewBehavior& viewBehavior = deviceInfo.getViewBehavior();
        std::optional<bool> defaultSmoothScroll = viewBehavior.shouldSmoothScroll;
        if (defaultSmoothScroll.has_value()) {
            env->CallVoidMethod(inputDeviceObj.get(), gInputDeviceClassInfo.setShouldSmoothScroll,
                                *defaultSmoothScroll);
            if (env->ExceptionCheck()) {
                return NULL;
            }
        }
    }

    return env->NewLocalRef(inputDeviceObj.get());
}

@@ -118,6 +130,8 @@ int register_android_view_InputDevice(JNIEnv* env)

    gInputDeviceClassInfo.addMotionRange =
            GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "addMotionRange", "(IIFFFFF)V");
    gInputDeviceClassInfo.setShouldSmoothScroll =
            GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "setShouldSmoothScroll", "(Z)V");
    return 0;
}

+10 −3
Original line number Diff line number Diff line
@@ -67,8 +67,14 @@ public class InputDeviceTest {
        assertEquals("keyCharacterMap not equal", keyCharacterMap, outKeyCharacterMap);

        for (int j = 0; j < device.getMotionRanges().size(); j++) {
            assertMotionRangeEquals(device.getMotionRanges().get(j),
                    outDevice.getMotionRanges().get(j));
            InputDevice.MotionRange motionRange = device.getMotionRanges().get(j);
            assertMotionRangeEquals(motionRange, outDevice.getMotionRanges().get(j));

            int axis = motionRange.getAxis();
            int source = motionRange.getSource();
            assertEquals(
                    device.getViewBehavior().shouldSmoothScroll(axis, source),
                    outDevice.getViewBehavior().shouldSmoothScroll(axis, source));
        }
    }

@@ -93,7 +99,8 @@ public class InputDeviceTest {
                .setHasBattery(true)
                .setKeyboardLanguageTag("en-US")
                .setKeyboardLayoutType("qwerty")
                .setUsiVersion(new HostUsiVersion(2, 0));
                .setUsiVersion(new HostUsiVersion(2, 0))
                .setShouldSmoothScroll(true);

        for (int i = 0; i < 30; i++) {
            deviceBuilder.addMotionRange(