• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.server.accessibility.integration
17 
18 import android.Manifest
19 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule
20 import android.accessibility.cts.common.InstrumentedAccessibilityService
21 import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule
22 import android.accessibilityservice.AccessibilityService
23 import android.accessibilityservice.AccessibilityServiceInfo
24 import android.accessibilityservice.MagnificationConfig
25 import android.app.Activity
26 import android.app.Instrumentation
27 import android.app.UiAutomation
28 import android.companion.virtual.VirtualDeviceManager
29 import android.graphics.PointF
30 import android.hardware.display.DisplayManager
31 import android.hardware.display.VirtualDisplay
32 import android.hardware.input.InputManager
33 import android.hardware.input.VirtualMouse
34 import android.hardware.input.VirtualMouseConfig
35 import android.hardware.input.VirtualMouseRelativeEvent
36 import android.os.Handler
37 import android.os.Looper
38 import android.os.OutcomeReceiver
39 import android.platform.test.annotations.RequiresFlagsEnabled
40 import android.testing.PollingCheck
41 import android.view.Display
42 import android.view.InputDevice
43 import android.view.MotionEvent
44 import android.virtualdevice.cts.common.VirtualDeviceRule
45 import androidx.test.ext.junit.runners.AndroidJUnit4
46 import androidx.test.platform.app.InstrumentationRegistry
47 import com.android.server.accessibility.Flags
48 import com.google.common.truth.Truth.assertThat
49 import java.util.concurrent.CompletableFuture
50 import java.util.concurrent.CountDownLatch
51 import java.util.concurrent.TimeUnit
52 import kotlin.math.abs
53 import kotlin.time.Duration.Companion.milliseconds
54 import kotlin.time.Duration.Companion.seconds
55 import org.junit.After
56 import org.junit.Before
57 import org.junit.Rule
58 import org.junit.Test
59 import org.junit.rules.RuleChain
60 import org.junit.runner.RunWith
61 
62 // Convenient extension functions for float.
63 private const val EPS = 0.00001f
Floatnull64 private fun Float.nearEq(other: Float) = abs(this - other) < EPS
65 private fun PointF.nearEq(other: PointF) = this.x.nearEq(other.x) && this.y.nearEq(other.y)
66 
67 /** End-to-end tests for full screen magnification following mouse cursor. */
68 @RunWith(AndroidJUnit4::class)
69 @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_WITH_POINTER_MOTION_FILTER)
70 class FullScreenMagnificationMouseFollowingTest {
71 
72     private lateinit var instrumentation: Instrumentation
73     private lateinit var uiAutomation: UiAutomation
74 
75     private val magnificationAccessibilityServiceRule =
76         InstrumentedAccessibilityServiceTestRule<TestMagnificationAccessibilityService>(
77             TestMagnificationAccessibilityService::class.java, false
78         )
79     private lateinit var service: TestMagnificationAccessibilityService
80 
81     // virtualDeviceRule tears down `virtualDevice` and `virtualDisplay`.
82     // Note that CheckFlagsRule is a part of VirtualDeviceRule. See its javadoc.
83     val virtualDeviceRule: VirtualDeviceRule =
84         VirtualDeviceRule.withAdditionalPermissions(Manifest.permission.MANAGE_ACTIVITY_TASKS)
85     private lateinit var virtualDevice: VirtualDeviceManager.VirtualDevice
86     private lateinit var virtualDisplay: VirtualDisplay
87 
88     // Once created, it's our responsibility to close the mouse.
89     private lateinit var virtualMouse: VirtualMouse
90 
91     @get:Rule
92     val ruleChain: RuleChain =
93         RuleChain.outerRule(virtualDeviceRule)
94             .around(magnificationAccessibilityServiceRule)
95             .around(AccessibilityDumpOnFailureRule())
96 
97     @Before
98     fun setUp() {
99         instrumentation = InstrumentationRegistry.getInstrumentation()
100         uiAutomation =
101             instrumentation.getUiAutomation(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES)
102         uiAutomation.serviceInfo =
103             uiAutomation.serviceInfo!!.apply {
104                 flags = flags or AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
105             }
106 
107         prepareVirtualDevices()
108 
109         launchTestActivityFullscreen(virtualDisplay.display.displayId)
110 
111         service = magnificationAccessibilityServiceRule.enableService()
112         service.observingDisplayId = virtualDisplay.display.displayId
113     }
114 
115     @After
116     fun cleanUp() {
117         if (this::virtualMouse.isInitialized) {
118             virtualMouse.close()
119         }
120     }
121 
122     // Note on continuous movement:
123     // Assume that the entire display is magnified, and the zoom level is z.
124     // In continuous movement, mouse speed relative to the unscaled physical display is the same as
125     // unmagnified speed. While, when a cursor moves from the left edge to the right edge of the
126     // screen, the magnification center moves from the left bound to the right bound, which is
127     // (display width) * (z - 1) / z.
128     //
129     // Similarly, when the mouse cursor moves by d in unscaled, display coordinates,
130     // the magnification center moves by d * (z - 1) / z.
131 
132     @Test
133     fun testContinuous_toBottomRight() {
134         ensureMouseAtCenter()
135 
136         val controller = service.getMagnificationController(virtualDisplay.display.displayId)
137 
138         scaleTo(controller, 2f)
139         assertMagnification(controller, scale = 2f, CENTER_X, CENTER_Y)
140 
141         // Move cursor by (10, 15)
142         // This will move magnification center by (5, 7.5)
143         sendMouseMove(10f, 15f)
144         assertCursorLocation(CENTER_X + 10, CENTER_Y + 15)
145         assertMagnification(controller, scale = 2f, CENTER_X + 5, CENTER_Y + 7.5f)
146 
147         // Move cursor to the rest of the way to the edge.
148         sendMouseMove(DISPLAY_WIDTH - 10, DISPLAY_HEIGHT - 15)
149         assertCursorLocation(DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1)
150         assertMagnification(controller, scale = 2f, DISPLAY_WIDTH * 3 / 4, DISPLAY_HEIGHT * 3 / 4)
151 
152         // Move cursor further won't move the magnification.
153         sendMouseMove(100f, 100f)
154         assertCursorLocation(DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1)
155     }
156 
157     @Test
158     fun testContinuous_toTopLeft() {
159         ensureMouseAtCenter()
160 
161         val controller = service.getMagnificationController(virtualDisplay.display.displayId)
162 
163         scaleTo(controller, 3f)
164         assertMagnification(controller, scale = 3f, CENTER_X, CENTER_Y)
165 
166         // Move cursor by (-30, -15)
167         // This will move magnification center by (-20, -10)
168         sendMouseMove(-30f, -15f)
169         assertCursorLocation(CENTER_X - 30, CENTER_Y - 15)
170         assertMagnification(controller, scale = 3f, CENTER_X - 20, CENTER_Y - 10)
171 
172         // Move cursor to the rest of the way to the edge.
173         sendMouseMove(-CENTER_X + 30, -CENTER_Y + 15)
174         assertCursorLocation(0f, 0f)
175         assertMagnification(controller, scale = 3f, DISPLAY_WIDTH / 6, DISPLAY_HEIGHT / 6)
176 
177         // Move cursor further won't move the magnification.
178         sendMouseMove(-100f, -100f)
179         assertCursorLocation(0f, 0f)
180         assertMagnification(controller, scale = 3f, DISPLAY_WIDTH / 6, DISPLAY_HEIGHT / 6)
181     }
182 
183     private fun ensureMouseAtCenter() {
184         val displayCenter = PointF(320f, 240f)
185         val cursorLocation = virtualMouse.cursorPosition
186         if (!cursorLocation.nearEq(displayCenter)) {
187             sendMouseMove(displayCenter.x - cursorLocation.x, displayCenter.y - cursorLocation.y)
188             assertCursorLocation(320f, 240f)
189         }
190     }
191 
192     private fun sendMouseMove(dx: Float, dy: Float) {
193         virtualMouse.sendRelativeEvent(
194             VirtualMouseRelativeEvent.Builder().setRelativeX(dx).setRelativeY(dy).build()
195         )
196     }
197 
198     /**
199      * Asserts that the cursor location is at the specified coordinates. The coordinates
200      * are in the non-scaled, display coordinates.
201      */
202     private fun assertCursorLocation(x: Float, y: Float) {
203         PollingCheck.check("Wait for the cursor at ($x, $y)", CURSOR_TIMEOUT.inWholeMilliseconds) {
204             service.lastObservedCursorLocation?.let { it.x.nearEq(x) && it.y.nearEq(y) } ?: false
205         }
206     }
207 
208     private fun scaleTo(controller: AccessibilityService.MagnificationController, scale: Float) {
209         val config =
210             MagnificationConfig.Builder()
211                 .setActivated(true)
212                 .setMode(MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN)
213                 .setScale(scale)
214                 .build()
215         val setResult = BooleanArray(1)
216         service.runOnServiceSync { setResult[0] = controller.setMagnificationConfig(config, false) }
217         assertThat(setResult[0]).isTrue()
218     }
219 
220     private fun assertMagnification(
221         controller: AccessibilityService.MagnificationController,
222         scale: Float = Float.NaN, centerX: Float = Float.NaN, centerY: Float = Float.NaN
223     ) {
224         PollingCheck.check(
225             "Wait for the magnification to scale=$scale, centerX=$centerX, centerY=$centerY",
226             MAGNIFICATION_TIMEOUT.inWholeMilliseconds
227         ) check@{
228             val actual = controller.getMagnificationConfig() ?: return@check false
229             actual.isActivated &&
230                 (actual.mode == MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN) &&
231                 (scale.isNaN() || scale.nearEq(actual.scale)) &&
232                 (centerX.isNaN() || centerX.nearEq(actual.centerX)) &&
233                 (centerY.isNaN() || centerY.nearEq(actual.centerY))
234         }
235     }
236 
237     /**
238      * Sets up a virtual display and a virtual mouse for the test. The virtual mouse is associated
239      * with the virtual display.
240      */
241     private fun prepareVirtualDevices() {
242         val deviceLatch = CountDownLatch(1)
243         val im = instrumentation.context.getSystemService(InputManager::class.java)
244         val inputDeviceListener =
245             object : InputManager.InputDeviceListener {
246                 override fun onInputDeviceAdded(deviceId: Int) {
247                     onInputDeviceChanged(deviceId)
248                 }
249 
250                 override fun onInputDeviceRemoved(deviceId: Int) {}
251 
252                 override fun onInputDeviceChanged(deviceId: Int) {
253                     val device = im.getInputDevice(deviceId) ?: return
254                     if (device.vendorId == VIRTUAL_MOUSE_VENDOR_ID &&
255                         device.productId == VIRTUAL_MOUSE_PRODUCT_ID
256                     ) {
257                         deviceLatch.countDown()
258                     }
259                 }
260             }
261         im.registerInputDeviceListener(inputDeviceListener, Handler(Looper.getMainLooper()))
262 
263         virtualDevice = virtualDeviceRule.createManagedVirtualDevice()
264         virtualDisplay =
265             virtualDeviceRule.createManagedVirtualDisplay(
266                 virtualDevice,
267                 VirtualDeviceRule
268                     .createDefaultVirtualDisplayConfigBuilder(
269                         DISPLAY_WIDTH.toInt(),
270                         DISPLAY_HEIGHT.toInt()
271                     )
272                     .setFlags(
273                         DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
274                             or DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED
275                             or DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
276                     )
277             )!!
278         virtualMouse =
279             virtualDevice.createVirtualMouse(
280                 VirtualMouseConfig.Builder()
281                     .setVendorId(VIRTUAL_MOUSE_VENDOR_ID)
282                     .setProductId(VIRTUAL_MOUSE_PRODUCT_ID)
283                     .setAssociatedDisplayId(virtualDisplay.display.displayId)
284                     .setInputDeviceName("VirtualMouse")
285                     .build()
286             )
287 
288         deviceLatch.await(UI_IDLE_GLOBAL_TIMEOUT.inWholeSeconds, TimeUnit.SECONDS)
289         im.unregisterInputDeviceListener(inputDeviceListener)
290     }
291 
292     /**
293      * Launches a test (empty) activity and makes it fullscreen on the specified display. This
294      * ensures that system bars are hidden and the full screen magnification enlarges the entire
295      * display.
296      */
297     private fun launchTestActivityFullscreen(displayId: Int) {
298         val future = CompletableFuture<Void?>()
299         val fullscreenCallback =
300             object : OutcomeReceiver<Void, Throwable> {
301                 override fun onResult(result: Void?) {
302                     future.complete(null)
303                 }
304 
305                 override fun onError(error: Throwable) {
306                     future.completeExceptionally(error)
307                 }
308             }
309 
310         val activity =
311             virtualDeviceRule.startActivityOnDisplaySync<TestActivity>(
312                 displayId,
313                 TestActivity::class.java
314             )
315         instrumentation.runOnMainSync {
316             activity.requestFullscreenMode(
317                 Activity.FULLSCREEN_MODE_REQUEST_ENTER,
318                 fullscreenCallback
319             )
320         }
321         future.get(UI_IDLE_GLOBAL_TIMEOUT.inWholeSeconds, TimeUnit.SECONDS)
322 
323         uiAutomation.waitForIdle(
324             UI_IDLE_TIMEOUT.inWholeMilliseconds, UI_IDLE_GLOBAL_TIMEOUT.inWholeMilliseconds
325         )
326     }
327 
328     class TestMagnificationAccessibilityService : InstrumentedAccessibilityService() {
329         private val lock = Any()
330 
331         var observingDisplayId = Display.INVALID_DISPLAY
332             set(v) {
333                 synchronized(lock) { field = v }
334             }
335 
336         var lastObservedCursorLocation: PointF? = null
337             private set
338             get() {
339                 synchronized(lock) {
340                     return field
341                 }
342             }
343 
344         override fun onServiceConnected() {
345             serviceInfo =
346                 getServiceInfo()!!.apply { setMotionEventSources(InputDevice.SOURCE_MOUSE) }
347 
348             super.onServiceConnected()
349         }
350 
351         override fun onMotionEvent(event: MotionEvent) {
352             super.onMotionEvent(event)
353 
354             synchronized(lock) {
355                 if (event.displayId == observingDisplayId) {
356                     lastObservedCursorLocation = PointF(event.x, event.y)
357                 }
358             }
359         }
360     }
361 
362     class TestActivity : Activity()
363 
364     companion object {
365         private const val VIRTUAL_MOUSE_VENDOR_ID = 123
366         private const val VIRTUAL_MOUSE_PRODUCT_ID = 456
367 
368         private val CURSOR_TIMEOUT = 1.seconds
369         private val MAGNIFICATION_TIMEOUT = 3.seconds
370         private val UI_IDLE_TIMEOUT = 500.milliseconds
371         private val UI_IDLE_GLOBAL_TIMEOUT = 5.seconds
372 
373         private const val DISPLAY_WIDTH = 640.0f
374         private const val DISPLAY_HEIGHT = 480.0f
375         private const val CENTER_X = DISPLAY_WIDTH / 2f
376         private const val CENTER_Y = DISPLAY_HEIGHT / 2f
377     }
378 }
379