1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.test.input 18 19 import android.content.Context 20 import android.content.res.Resources 21 import android.os.SystemProperties 22 import android.view.InputDevice 23 import android.view.MotionEvent 24 import android.view.MotionEvent.ACTION_DOWN 25 import android.view.MotionEvent.ACTION_MOVE 26 import android.view.MotionEvent.PointerCoords 27 import android.view.MotionEvent.PointerProperties 28 import android.view.MotionPredictor 29 30 import androidx.test.ext.junit.runners.AndroidJUnit4 31 import androidx.test.filters.SmallTest 32 import androidx.test.platform.app.InstrumentationRegistry 33 34 import org.junit.After 35 import org.junit.Assert.assertEquals 36 import org.junit.Assert.assertNotNull 37 import org.junit.Before 38 import org.junit.Test 39 import org.junit.runner.RunWith 40 import org.mockito.Mockito.mock 41 import org.mockito.Mockito.`when` 42 43 import java.time.Duration 44 45 private fun getStylusMotionEvent( 46 eventTime: Duration, 47 action: Int, 48 x: Float, 49 y: Float, 50 ): MotionEvent{ 51 val pointerCount = 1 52 val properties = arrayOfNulls<MotionEvent.PointerProperties>(pointerCount) 53 val coords = arrayOfNulls<MotionEvent.PointerCoords>(pointerCount) 54 55 for (i in 0 until pointerCount) { 56 properties[i] = PointerProperties() 57 properties[i]!!.id = i 58 properties[i]!!.toolType = MotionEvent.TOOL_TYPE_STYLUS 59 coords[i] = PointerCoords() 60 coords[i]!!.x = x 61 coords[i]!!.y = y 62 } 63 64 return MotionEvent.obtain(/*downTime=*/0, eventTime.toMillis(), action, properties.size, 65 properties, coords, /*metaState=*/0, /*buttonState=*/0, 66 /*xPrecision=*/0f, /*yPrecision=*/0f, /*deviceId=*/0, /*edgeFlags=*/0, 67 InputDevice.SOURCE_STYLUS, /*flags=*/0) 68 } 69 70 private fun getPredictionContext(offset: Duration, enablePrediction: Boolean): Context { 71 val context = mock(Context::class.java) 72 val resources: Resources = mock(Resources::class.java) 73 `when`(context.getResources()).thenReturn(resources) 74 `when`(resources.getInteger( 75 com.android.internal.R.integer.config_motionPredictionOffsetNanos)).thenReturn( 76 offset.toNanos().toInt()) 77 `when`(resources.getBoolean( 78 com.android.internal.R.bool.config_enableMotionPrediction)).thenReturn(enablePrediction) 79 return context 80 } 81 82 @RunWith(AndroidJUnit4::class) 83 @SmallTest 84 class MotionPredictorTest { 85 private val instrumentation = InstrumentationRegistry.getInstrumentation() 86 val initialPropertyValue = SystemProperties.get("persist.input.enable_motion_prediction") 87 88 @Before 89 fun setUp() { 90 instrumentation.uiAutomation.executeShellCommand( 91 "setprop persist.input.enable_motion_prediction true") 92 } 93 94 @After 95 fun tearDown() { 96 instrumentation.uiAutomation.executeShellCommand( 97 "setprop persist.input.enable_motion_prediction $initialPropertyValue") 98 } 99 100 /** 101 * In a typical usage, app will send the event to the predictor and then call .predict to draw 102 * a prediction. Here, we send 2 events to the predictor and check the returned event. 103 * Input: 104 * t = 0 x = 0 y = 0 105 * t = 4 x = 10 y = 20 106 * Output (expected): 107 * t = 12 x = 30 y = 60 ± error 108 * 109 * Historical data is ignored for simplicity. 110 */ 111 @Test 112 fun testPredictedCoordinatesAndTime() { 113 val context = getPredictionContext( 114 /*offset=*/Duration.ofMillis(1), /*enablePrediction=*/true) 115 val predictor = MotionPredictor(context) 116 var eventTime = Duration.ofMillis(0) 117 val downEvent = getStylusMotionEvent(eventTime, ACTION_DOWN, /*x=*/0f, /*y=*/0f) 118 // ACTION_DOWN t=0 x=0 y=0 119 predictor.record(downEvent) 120 121 eventTime += Duration.ofMillis(4) 122 val moveEvent = getStylusMotionEvent(eventTime, ACTION_MOVE, /*x=*/10f, /*y=*/20f) 123 // ACTION_MOVE t=1 x=1 y=2 124 predictor.record(moveEvent) 125 126 val predicted = predictor.predict(Duration.ofMillis(8).toNanos()) 127 assertNotNull(predicted) 128 129 // Prediction will happen for t=12 (since it is the next input interval after the requested 130 // time, 8, plus the model offset, 1). 131 assertEquals(12, predicted!!.eventTime) 132 assertEquals(30f, predicted.x, /*delta=*/10f) 133 assertEquals(60f, predicted.y, /*delta=*/15f) 134 } 135 } 136