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