Merge "Prediction for motion events"
This commit is contained in:
committed by
Android (Google) Code Review
commit
e76ea25c20
116
core/java/android/view/MotionPredictor.java
Normal file
116
core/java/android/view/MotionPredictor.java
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package android.view;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
|
||||
import libcore.util.NativeAllocationRegistry;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Calculates motion predictions.
|
||||
*
|
||||
* Add motions here to get predicted events!
|
||||
* @hide
|
||||
*/
|
||||
// Acts as a pass-through to the native MotionPredictor object.
|
||||
// Do not store any state in this Java layer, or add any business logic here. All of the
|
||||
// implementation details should go into the native MotionPredictor.
|
||||
// The context / resource access must be here rather than in native layer due to the lack of the
|
||||
// corresponding native API surface.
|
||||
public final class MotionPredictor {
|
||||
|
||||
private static class RegistryHolder {
|
||||
public static final NativeAllocationRegistry REGISTRY =
|
||||
NativeAllocationRegistry.createMalloced(
|
||||
MotionPredictor.class.getClassLoader(),
|
||||
nativeGetNativeMotionPredictorFinalizer());
|
||||
}
|
||||
|
||||
// Pointer to the native object.
|
||||
private final long mPtr;
|
||||
private final Context mContext;
|
||||
|
||||
/**
|
||||
* Create a new MotionPredictor for the provided {@link Context}.
|
||||
* @param context The context for the predictions
|
||||
*/
|
||||
public MotionPredictor(@NonNull Context context) {
|
||||
mContext = context;
|
||||
final int offsetNanos = mContext.getResources().getInteger(
|
||||
com.android.internal.R.integer.config_motionPredictionOffsetNanos);
|
||||
mPtr = nativeInitialize(offsetNanos);
|
||||
RegistryHolder.REGISTRY.registerNativeAllocation(this, mPtr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a movement so that in the future, a prediction for the current gesture can be
|
||||
* generated. Ensure to add all motions from the gesture of interest to generate the correct
|
||||
* prediction.
|
||||
* @param event The received event
|
||||
*/
|
||||
public void record(@NonNull MotionEvent event) {
|
||||
nativeRecord(mPtr, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get predicted events for all gestures that have been provided to the 'record' function.
|
||||
* If events from multiple devices were sent to 'record', this will produce a separate
|
||||
* {@link MotionEvent} for each device id. The returned list may be empty if no predictions for
|
||||
* any of the added events are available.
|
||||
* Predictions may not reach the requested timestamp if the confidence in the prediction results
|
||||
* is low.
|
||||
*
|
||||
* @param predictionTimeNanos The time that the prediction should target, in the
|
||||
* {@link android.os.SystemClock#uptimeMillis} time base, but in nanoseconds.
|
||||
*
|
||||
* @return the list of predicted motion events, for each device id. Ensure to check the
|
||||
* historical data in addition to the latest ({@link MotionEvent#getX getX()},
|
||||
* {@link MotionEvent#getY getY()}) coordinates for smoothest prediction curves. Empty list is
|
||||
* returned if predictions are not supported, or not possible for the current set of gestures.
|
||||
*/
|
||||
@NonNull
|
||||
public List<MotionEvent> predict(long predictionTimeNanos) {
|
||||
return Arrays.asList(nativePredict(mPtr, predictionTimeNanos));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this device supports motion predictions for the given source type.
|
||||
*
|
||||
* @param deviceId The input device id
|
||||
* @param source The source of input events
|
||||
* @return True if the current device supports predictions, false otherwise.
|
||||
*/
|
||||
public boolean isPredictionAvailable(int deviceId, int source) {
|
||||
// Device-specific override
|
||||
if (!mContext.getResources().getBoolean(
|
||||
com.android.internal.R.bool.config_enableMotionPrediction)) {
|
||||
return false;
|
||||
}
|
||||
return nativeIsPredictionAvailable(mPtr, deviceId, source);
|
||||
}
|
||||
|
||||
private static native long nativeInitialize(int offsetNanos);
|
||||
private static native void nativeRecord(long nativePtr, MotionEvent event);
|
||||
private static native MotionEvent[] nativePredict(long nativePtr, long predictionTimeNanos);
|
||||
private static native boolean nativeIsPredictionAvailable(long nativePtr, int deviceId,
|
||||
int source);
|
||||
private static native long nativeGetNativeMotionPredictorFinalizer();
|
||||
}
|
||||
@@ -129,6 +129,7 @@ cc_library_shared {
|
||||
"android_view_KeyCharacterMap.cpp",
|
||||
"android_view_KeyEvent.cpp",
|
||||
"android_view_MotionEvent.cpp",
|
||||
"android_view_MotionPredictor.cpp",
|
||||
"android_view_PointerIcon.cpp",
|
||||
"android_view_Surface.cpp",
|
||||
"android_view_SurfaceControl.cpp",
|
||||
@@ -283,6 +284,7 @@ cc_library_shared {
|
||||
"libhwui",
|
||||
"libmediandk",
|
||||
"libpermission",
|
||||
"libPlatformProperties",
|
||||
"libsensor",
|
||||
"libinput",
|
||||
"libcamera_client",
|
||||
|
||||
@@ -187,6 +187,7 @@ extern int register_android_view_InputQueue(JNIEnv* env);
|
||||
extern int register_android_view_KeyCharacterMap(JNIEnv *env);
|
||||
extern int register_android_view_KeyEvent(JNIEnv* env);
|
||||
extern int register_android_view_MotionEvent(JNIEnv* env);
|
||||
extern int register_android_view_MotionPredictor(JNIEnv* env);
|
||||
extern int register_android_view_PointerIcon(JNIEnv* env);
|
||||
extern int register_android_view_VelocityTracker(JNIEnv* env);
|
||||
extern int register_android_view_VerifiedKeyEvent(JNIEnv* env);
|
||||
@@ -1643,6 +1644,7 @@ static const RegJNIRec gRegJNI[] = {
|
||||
REG_JNI(register_android_view_InputQueue),
|
||||
REG_JNI(register_android_view_KeyEvent),
|
||||
REG_JNI(register_android_view_MotionEvent),
|
||||
REG_JNI(register_android_view_MotionPredictor),
|
||||
REG_JNI(register_android_view_PointerIcon),
|
||||
REG_JNI(register_android_view_VelocityTracker),
|
||||
REG_JNI(register_android_view_VerifiedKeyEvent),
|
||||
|
||||
@@ -97,7 +97,6 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi
|
||||
return env->NewLocalRef(inputDeviceObj.get());
|
||||
}
|
||||
|
||||
|
||||
int register_android_view_InputDevice(JNIEnv* env)
|
||||
{
|
||||
gInputDeviceClassInfo.clazz = FindClassOrDie(env, "android/view/InputDevice");
|
||||
@@ -108,9 +107,8 @@ int register_android_view_InputDevice(JNIEnv* env)
|
||||
"String;ZIILandroid/view/KeyCharacterMap;Ljava/"
|
||||
"lang/String;Ljava/lang/String;ZZZZZZ)V");
|
||||
|
||||
gInputDeviceClassInfo.addMotionRange = GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz,
|
||||
"addMotionRange", "(IIFFFFF)V");
|
||||
|
||||
gInputDeviceClassInfo.addMotionRange =
|
||||
GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "addMotionRange", "(IIFFFFF)V");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,20 @@ jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent& ev
|
||||
return eventObj;
|
||||
}
|
||||
|
||||
jobject android_view_MotionEvent_obtainFromNative(JNIEnv* env, std::unique_ptr<MotionEvent> event) {
|
||||
if (event == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
jobject eventObj =
|
||||
env->CallStaticObjectMethod(gMotionEventClassInfo.clazz, gMotionEventClassInfo.obtain);
|
||||
if (env->ExceptionCheck() || !eventObj) {
|
||||
LOGE_EX(env);
|
||||
LOG_ALWAYS_FATAL("An exception occurred while obtaining a Java motion event.");
|
||||
}
|
||||
android_view_MotionEvent_setNativePtr(env, eventObj, event.release());
|
||||
return eventObj;
|
||||
}
|
||||
|
||||
status_t android_view_MotionEvent_recycle(JNIEnv* env, jobject eventObj) {
|
||||
env->CallVoidMethod(eventObj, gMotionEventClassInfo.recycle);
|
||||
if (env->ExceptionCheck()) {
|
||||
|
||||
@@ -28,6 +28,11 @@ class MotionEvent;
|
||||
* Returns NULL on error. */
|
||||
extern jobject android_view_MotionEvent_obtainAsCopy(JNIEnv* env, const MotionEvent& event);
|
||||
|
||||
/* Obtains an instance of a Java MotionEvent object, taking over the ownership of the provided
|
||||
* native MotionEvent instance. Crashes on error. */
|
||||
extern jobject android_view_MotionEvent_obtainFromNative(JNIEnv* env,
|
||||
std::unique_ptr<MotionEvent> event);
|
||||
|
||||
/* Gets the underlying native MotionEvent instance within a DVM MotionEvent object.
|
||||
* Returns NULL if the event is NULL or if it is uninitialized. */
|
||||
extern MotionEvent* android_view_MotionEvent_getNativePtr(JNIEnv* env, jobject eventObj);
|
||||
|
||||
100
core/jni/android_view_MotionPredictor.cpp
Normal file
100
core/jni/android_view_MotionPredictor.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 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 "MotionPredictor-JNI"
|
||||
|
||||
#include <input/Input.h>
|
||||
#include <input/MotionPredictor.h>
|
||||
|
||||
#include "android_view_MotionEvent.h"
|
||||
#include "core_jni_converters.h"
|
||||
#include "core_jni_helpers.h"
|
||||
|
||||
/**
|
||||
* This file is a bridge from Java to native for MotionPredictor class.
|
||||
* It should be pass-through only. Do not store any state or put any business logic into this file.
|
||||
*/
|
||||
|
||||
namespace android {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
static struct {
|
||||
jclass clazz;
|
||||
} gMotionEventClassInfo;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
static void release(void* ptr) {
|
||||
delete reinterpret_cast<MotionPredictor*>(ptr);
|
||||
}
|
||||
|
||||
static jlong android_view_MotionPredictor_nativeGetNativeMotionPredictorFinalizer(JNIEnv* env,
|
||||
jclass clazz) {
|
||||
return reinterpret_cast<jlong>(release);
|
||||
}
|
||||
|
||||
static jlong android_view_MotionPredictor_nativeInitialize(JNIEnv* env, jclass clazz,
|
||||
jint offsetNanos) {
|
||||
const nsecs_t offset = static_cast<nsecs_t>(offsetNanos);
|
||||
return reinterpret_cast<jlong>(new MotionPredictor(offset));
|
||||
}
|
||||
|
||||
static void android_view_MotionPredictor_nativeRecord(JNIEnv* env, jclass clazz, jlong ptr,
|
||||
jobject event) {
|
||||
MotionPredictor* predictor = reinterpret_cast<MotionPredictor*>(ptr);
|
||||
MotionEvent* motionEvent = android_view_MotionEvent_getNativePtr(env, event);
|
||||
predictor->record(*motionEvent);
|
||||
}
|
||||
|
||||
static jobject android_view_MotionPredictor_nativePredict(JNIEnv* env, jclass clazz, jlong ptr,
|
||||
jlong predictionTimeNanos) {
|
||||
MotionPredictor* predictor = reinterpret_cast<MotionPredictor*>(ptr);
|
||||
return toJavaArray(env, predictor->predict(static_cast<nsecs_t>(predictionTimeNanos)),
|
||||
gMotionEventClassInfo.clazz, &android_view_MotionEvent_obtainFromNative);
|
||||
}
|
||||
|
||||
static jboolean android_view_MotionPredictor_nativeIsPredictionAvailable(JNIEnv* env, jclass clazz,
|
||||
jlong ptr, jint deviceId,
|
||||
jint source) {
|
||||
MotionPredictor* predictor = reinterpret_cast<MotionPredictor*>(ptr);
|
||||
return predictor->isPredictionAvailable(static_cast<int32_t>(deviceId),
|
||||
static_cast<int32_t>(source));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
static const std::array<JNINativeMethod, 5> gMotionPredictorMethods{{
|
||||
/* name, signature, funcPtr */
|
||||
{"nativeInitialize", "(I)J", (void*)android_view_MotionPredictor_nativeInitialize},
|
||||
{"nativeGetNativeMotionPredictorFinalizer", "()J",
|
||||
(void*)android_view_MotionPredictor_nativeGetNativeMotionPredictorFinalizer},
|
||||
{"nativeRecord", "(JLandroid/view/MotionEvent;)V",
|
||||
(void*)android_view_MotionPredictor_nativeRecord},
|
||||
{"nativePredict", "(JJ)[Landroid/view/MotionEvent;",
|
||||
(void*)android_view_MotionPredictor_nativePredict},
|
||||
{"nativeIsPredictionAvailable", "(JII)Z",
|
||||
(void*)android_view_MotionPredictor_nativeIsPredictionAvailable},
|
||||
}};
|
||||
|
||||
int register_android_view_MotionPredictor(JNIEnv* env) {
|
||||
jclass motionEventClazz = FindClassOrDie(env, "android/view/MotionEvent");
|
||||
gMotionEventClassInfo.clazz = MakeGlobalRefOrDie(env, motionEventClazz);
|
||||
return RegisterMethodsOrDie(env, "android/view/MotionPredictor", gMotionPredictorMethods.data(),
|
||||
gMotionPredictorMethods.size());
|
||||
}
|
||||
|
||||
} // namespace android
|
||||
32
core/jni/core_jni_converters.h
Normal file
32
core/jni/core_jni_converters.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <nativehelper/scoped_local_ref.h>
|
||||
|
||||
template <class T>
|
||||
static jobject toJavaArray(JNIEnv* env, std::vector<T>&& list, jclass clazz,
|
||||
jobject (*convert)(JNIEnv* env, T)) {
|
||||
jobjectArray arr = env->NewObjectArray(list.size(), clazz, nullptr);
|
||||
LOG_ALWAYS_FATAL_IF(arr == nullptr);
|
||||
for (size_t i = 0; i < list.size(); i++) {
|
||||
T& t = list[i];
|
||||
ScopedLocalRef<jobject> javaObj(env, convert(env, std::move(t)));
|
||||
env->SetObjectArrayElement(arr, i, javaObj.get());
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
@@ -2344,6 +2344,24 @@
|
||||
display, this value should be true. -->
|
||||
<bool name="config_perDisplayFocusEnabled">false</bool>
|
||||
|
||||
<!-- Whether the system enables motion prediction. Only enable this after confirming that the
|
||||
model works well on your device. To enable system-based prediction, set this value to true.
|
||||
-->
|
||||
<bool name="config_enableMotionPrediction">true</bool>
|
||||
|
||||
<!-- Additional offset to use for motion prediction, in nanoseconds. A positive number indicates
|
||||
that the prediction will take place further in the future. For example, suppose a
|
||||
MotionEvent arrives with timestamp t=1, and the current expected presentation time is t=2.
|
||||
Typically, the prediction will target the presentation time, t=2. If you'd like to make
|
||||
prediction more aggressive, you could set the offset to a positive number.
|
||||
Setting the offset to 1 here would mean that the prediction will be done for time t=3.
|
||||
A negative number may also be provided, to make the prediction less aggressive. In general,
|
||||
the offset here should represent some built-in hardware delays that may not be accounted
|
||||
for by the "expected present time". See also:
|
||||
https://developer.android.com/reference/android/view/
|
||||
Choreographer.FrameTimeline#getExpectedPresentationTimeNanos() -->
|
||||
<integer name="config_motionPredictionOffsetNanos">0</integer>
|
||||
|
||||
<!-- Whether a software navigation bar should be shown. NOTE: in the future this may be
|
||||
autodetected from the Configuration. -->
|
||||
<bool name="config_showNavigationBar">false</bool>
|
||||
|
||||
@@ -1684,6 +1684,8 @@
|
||||
<java-symbol type="bool" name="config_lockUiMode" />
|
||||
<java-symbol type="bool" name="config_reverseDefaultRotation" />
|
||||
<java-symbol type="bool" name="config_perDisplayFocusEnabled" />
|
||||
<java-symbol type="bool" name="config_enableMotionPrediction" />
|
||||
<java-symbol type="integer" name="config_motionPredictionOffsetNanos" />
|
||||
<java-symbol type="bool" name="config_showNavigationBar" />
|
||||
<java-symbol type="bool" name="config_supportAutoRotation" />
|
||||
<java-symbol type="bool" name="config_dockedStackDividerFreeSnapMode" />
|
||||
|
||||
@@ -18,6 +18,7 @@ android_test {
|
||||
static_libs: [
|
||||
"androidx.test.ext.junit",
|
||||
"androidx.test.rules",
|
||||
"mockito-target-minus-junit4",
|
||||
"services.core.unboosted",
|
||||
"testables",
|
||||
"truth-prebuilt",
|
||||
|
||||
138
tests/Input/src/com/android/test/input/MotionPredictorTest.kt
Normal file
138
tests/Input/src/com/android/test/input/MotionPredictorTest.kt
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package com.android.test.input
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.os.SystemProperties
|
||||
import android.view.InputDevice
|
||||
import android.view.MotionEvent
|
||||
import android.view.MotionEvent.ACTION_DOWN
|
||||
import android.view.MotionEvent.ACTION_MOVE
|
||||
import android.view.MotionEvent.PointerCoords
|
||||
import android.view.MotionEvent.PointerProperties
|
||||
import android.view.MotionPredictor
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.`when`
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
private fun getStylusMotionEvent(
|
||||
eventTime: Duration,
|
||||
action: Int,
|
||||
x: Float,
|
||||
y: Float,
|
||||
): MotionEvent{
|
||||
// One-time: send a DOWN event
|
||||
val pointerCount = 1
|
||||
val properties = arrayOfNulls<MotionEvent.PointerProperties>(pointerCount)
|
||||
val coords = arrayOfNulls<MotionEvent.PointerCoords>(pointerCount)
|
||||
|
||||
for (i in 0 until pointerCount) {
|
||||
properties[i] = PointerProperties()
|
||||
properties[i]!!.id = i
|
||||
properties[i]!!.toolType = MotionEvent.TOOL_TYPE_STYLUS
|
||||
coords[i] = PointerCoords()
|
||||
coords[i]!!.x = x
|
||||
coords[i]!!.y = y
|
||||
}
|
||||
|
||||
return MotionEvent.obtain(/*downTime=*/0, eventTime.toMillis(), action, properties.size,
|
||||
properties, coords, /*metaState=*/0, /*buttonState=*/0,
|
||||
/*xPrecision=*/0f, /*yPrecision=*/0f, /*deviceId=*/0, /*edgeFlags=*/0,
|
||||
InputDevice.SOURCE_STYLUS, /*flags=*/0)
|
||||
}
|
||||
|
||||
private fun getPredictionContext(offset: Duration, enablePrediction: Boolean): Context {
|
||||
val context = mock(Context::class.java)
|
||||
val resources: Resources = mock(Resources::class.java)
|
||||
`when`(context.getResources()).thenReturn(resources)
|
||||
`when`(resources.getInteger(
|
||||
com.android.internal.R.integer.config_motionPredictionOffsetNanos)).thenReturn(
|
||||
offset.toNanos().toInt())
|
||||
`when`(resources.getBoolean(
|
||||
com.android.internal.R.bool.config_enableMotionPrediction)).thenReturn(enablePrediction)
|
||||
return context
|
||||
}
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@SmallTest
|
||||
class MotionPredictorTest {
|
||||
private val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
val initialPropertyValue = SystemProperties.get("persist.input.enable_motion_prediction")
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
instrumentation.uiAutomation.executeShellCommand(
|
||||
"setprop persist.input.enable_motion_prediction true")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
instrumentation.uiAutomation.executeShellCommand(
|
||||
"setprop persist.input.enable_motion_prediction $initialPropertyValue")
|
||||
}
|
||||
|
||||
/**
|
||||
* In a typical usage, app will send the event to the predictor and then call .predict to draw
|
||||
* a prediction. Here, we send 2 events to the predictor and check the returned event.
|
||||
* Input:
|
||||
* t = 0 x = 0 y = 0
|
||||
* t = 1 x = 1 y = 2
|
||||
* Output (expected):
|
||||
* t = 3 x = 3 y = 6
|
||||
*
|
||||
* Historical data is ignored for simplicity.
|
||||
*/
|
||||
@Test
|
||||
fun testPredictedCoordinatesAndTime() {
|
||||
val context = getPredictionContext(
|
||||
/*offset=*/Duration.ofMillis(1), /*enablePrediction=*/true)
|
||||
val predictor = MotionPredictor(context)
|
||||
var eventTime = Duration.ofMillis(0)
|
||||
val downEvent = getStylusMotionEvent(eventTime, ACTION_DOWN, /*x=*/0f, /*y=*/0f)
|
||||
// ACTION_DOWN t=0 x=0 y=0
|
||||
predictor.record(downEvent)
|
||||
|
||||
eventTime += Duration.ofMillis(1)
|
||||
val moveEvent = getStylusMotionEvent(eventTime, ACTION_MOVE, /*x=*/1f, /*y=*/2f)
|
||||
// ACTION_MOVE t=1 x=1 y=2
|
||||
predictor.record(moveEvent)
|
||||
|
||||
val predicted = predictor.predict(Duration.ofMillis(2).toNanos())
|
||||
assertEquals(1, predicted.size)
|
||||
val event = predicted[0]
|
||||
assertNotNull(event)
|
||||
|
||||
// Prediction will happen for t=3 (2 + 1, since offset is 1 and present time is 2)
|
||||
assertEquals(3, event.eventTime)
|
||||
assertEquals(3f, event.x, /*delta=*/0.001f)
|
||||
assertEquals(6f, event.y, /*delta=*/0.001f)
|
||||
}
|
||||
}
|
||||
31
tests/MotionPrediction/Android.bp
Normal file
31
tests/MotionPrediction/Android.bp
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Copyright (C) 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.
|
||||
//
|
||||
|
||||
package {
|
||||
// See: http://go/android-license-faq
|
||||
// A large-scale-change added 'default_applicable_licenses' to import
|
||||
// all of the 'license_kinds' from "frameworks_base_license"
|
||||
// to get the below license kinds:
|
||||
// SPDX-license-identifier-Apache-2.0
|
||||
default_applicable_licenses: ["frameworks_base_license"],
|
||||
}
|
||||
|
||||
android_app {
|
||||
name: "MotionPrediction",
|
||||
srcs: ["**/*.kt"],
|
||||
platform_apis: true,
|
||||
certificate: "platform",
|
||||
}
|
||||
34
tests/MotionPrediction/AndroidManifest.xml
Normal file
34
tests/MotionPrediction/AndroidManifest.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="test.motionprediction">
|
||||
|
||||
<application android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
1
tests/MotionPrediction/OWNERS
Normal file
1
tests/MotionPrediction/OWNERS
Normal file
@@ -0,0 +1 @@
|
||||
include platform/frameworks/base:/INPUT_OWNERS
|
||||
32
tests/MotionPrediction/res/layout/activity_main.xml
Normal file
32
tests/MotionPrediction/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 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.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="test.motionprediction.MainActivity">
|
||||
|
||||
<test.motionprediction.DrawingView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/output" />
|
||||
|
||||
</LinearLayout>
|
||||
BIN
tests/MotionPrediction/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
tests/MotionPrediction/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
tests/MotionPrediction/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
tests/MotionPrediction/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
tests/MotionPrediction/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
tests/MotionPrediction/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
tests/MotionPrediction/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
tests/MotionPrediction/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
BIN
tests/MotionPrediction/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
tests/MotionPrediction/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
20
tests/MotionPrediction/res/values-w820dp/dimens.xml
Normal file
20
tests/MotionPrediction/res/values-w820dp/dimens.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<!-- Copyright (C) 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.
|
||||
-->
|
||||
<resources>
|
||||
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
||||
(such as screen margins) for screens with more than 820dp of available width. This
|
||||
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
|
||||
<dimen name="activity_horizontal_margin">64dp</dimen>
|
||||
</resources>
|
||||
20
tests/MotionPrediction/res/values/colors.xml
Normal file
20
tests/MotionPrediction/res/values/colors.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 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.
|
||||
-->
|
||||
<resources>
|
||||
<color name="colorPrimary">#3F51B5</color>
|
||||
<color name="colorPrimaryDark">#303F9F</color>
|
||||
<color name="colorAccent">#FF4081</color>
|
||||
</resources>
|
||||
19
tests/MotionPrediction/res/values/dimens.xml
Normal file
19
tests/MotionPrediction/res/values/dimens.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<!-- Copyright (C) 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.
|
||||
-->
|
||||
<resources>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
</resources>
|
||||
17
tests/MotionPrediction/res/values/strings.xml
Normal file
17
tests/MotionPrediction/res/values/strings.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- Copyright (C) 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.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">Motion Prediction</string>
|
||||
</resources>
|
||||
23
tests/MotionPrediction/res/values/styles.xml
Normal file
23
tests/MotionPrediction/res/values/styles.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<!-- Copyright (C) 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.
|
||||
-->
|
||||
<resources>
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="@android:style/Theme.Material.Light.DarkActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="android:colorAccent">@color/colorAccent</item>
|
||||
</style>
|
||||
</resources>
|
||||
106
tests/MotionPrediction/src/test/motionprediction/DrawingView.kt
Normal file
106
tests/MotionPrediction/src/test/motionprediction/DrawingView.kt
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package test.motionprediction
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.MotionEvent.ACTION_DOWN
|
||||
import android.view.MotionPredictor
|
||||
import android.view.View
|
||||
|
||||
import java.util.Vector
|
||||
|
||||
private fun drawLine(canvas: Canvas, from: MotionEvent, to: MotionEvent, paint: Paint) {
|
||||
canvas.apply {
|
||||
val x0 = from.getX()
|
||||
val y0 = from.getY()
|
||||
val x1 = to.getX()
|
||||
val y1 = to.getY()
|
||||
// TODO: handle historical data
|
||||
drawLine(x0, y0, x1, y1, paint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the current stroke and predicted values
|
||||
*/
|
||||
class DrawingView(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||
private val TAG = "DrawingView"
|
||||
|
||||
val events: MutableMap<Int, Vector<MotionEvent>> = mutableMapOf<Int, Vector<MotionEvent>>()
|
||||
|
||||
var isPredictionAvailable = false
|
||||
private val predictor = MotionPredictor(getContext())
|
||||
|
||||
private var predictionPaint = Paint()
|
||||
private var realPaint = Paint()
|
||||
|
||||
init {
|
||||
setBackgroundColor(Color.WHITE)
|
||||
predictionPaint.color = Color.BLACK
|
||||
predictionPaint.setStrokeWidth(5f)
|
||||
realPaint.color = Color.RED
|
||||
realPaint.setStrokeWidth(5f)
|
||||
}
|
||||
|
||||
private fun addEvent(event: MotionEvent) {
|
||||
if (event.getActionMasked() == ACTION_DOWN) {
|
||||
events.remove(event.deviceId)
|
||||
}
|
||||
var vec = events.getOrPut(event.deviceId) { Vector<MotionEvent>() }
|
||||
vec.add(MotionEvent.obtain(event))
|
||||
predictor.record(event)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
public override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
isPredictionAvailable = predictor.isPredictionAvailable(event.getDeviceId(),
|
||||
event.getSource())
|
||||
addEvent(event)
|
||||
return true
|
||||
}
|
||||
|
||||
public override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
if (!isPredictionAvailable) {
|
||||
canvas.apply {
|
||||
drawRect(0f, 0f, 200f, 200f, realPaint)
|
||||
}
|
||||
}
|
||||
|
||||
var eventTime = 0L
|
||||
|
||||
// Draw real events
|
||||
for ((_, vec) in events ) {
|
||||
for (i in 1 until vec.size) {
|
||||
drawLine(canvas, vec[i - 1], vec[i], realPaint)
|
||||
}
|
||||
eventTime = vec.lastElement().eventTime
|
||||
}
|
||||
|
||||
// Draw predictions. Convert to nanos and hardcode to +20ms into the future
|
||||
val predictionList = predictor.predict(eventTime * 1000000 + 20000000)
|
||||
for (prediction in predictionList) {
|
||||
val realEvents = events.get(prediction.deviceId)!!
|
||||
drawLine(canvas, realEvents[realEvents.size - 1], prediction, predictionPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*/
|
||||
|
||||
package test.motionprediction
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
|
||||
class MainActivity : Activity() {
|
||||
val TAG = "MotionPrediction"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user