diff --git a/core/api/current.txt b/core/api/current.txt index b17e3343666e626c8fb46ada1397415a5f410ff6..2465ffeef67fb515a9d9d69c490e68558dcc588f 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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(); diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 4ebbde732747b054ea18c31ffcfb1f5ba493c790..db992cdd20dbc34cd7460b104d71ffaff59a6ef0 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -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. diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index f2c3abc8edb4755437b35e6a1b1b2dd2f34e8e56..891e2a2d4b2048631380fdc0830862d18befe307 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -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 diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp index 239c6260800bc9e0591b4c41228cd27c61bc1ad3..aae0da9006a24c2ec4099ba2fd8af448802ffa07 100644 --- a/core/jni/android_view_InputDevice.cpp +++ b/core/jni/android_view_InputDevice.cpp @@ -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; } diff --git a/tests/Input/src/com/android/test/input/InputDeviceTest.java b/tests/Input/src/com/android/test/input/InputDeviceTest.java index 5434c82b07bde7e1a60bb2ee3dc86e943f1ed1f7..5f1bc8748db8f260ea1bb8f30eeffaee93fececa 100644 --- a/tests/Input/src/com/android/test/input/InputDeviceTest.java +++ b/tests/Input/src/com/android/test/input/InputDeviceTest.java @@ -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(