1 /*
2  * Copyright (C) 2016 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 
17 package android.accessibilityservice.cts;
18 
19 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.launchActivityAndWaitForItToBeOnscreen;
20 
21 import static androidx.test.InstrumentationRegistry.getInstrumentation;
22 
23 import static com.android.compatibility.common.util.TestUtils.waitOn;
24 
25 import static org.junit.Assert.assertEquals;
26 import static org.junit.Assert.assertFalse;
27 import static org.junit.Assert.assertNotNull;
28 import static org.junit.Assert.assertTrue;
29 import static org.mockito.Mockito.any;
30 import static org.mockito.Mockito.anyFloat;
31 import static org.mockito.Mockito.eq;
32 import static org.mockito.Mockito.mock;
33 import static org.mockito.Mockito.timeout;
34 import static org.mockito.Mockito.verify;
35 
36 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule;
37 import android.accessibility.cts.common.InstrumentedAccessibilityService;
38 import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule;
39 import android.accessibility.cts.common.ShellCommandBuilder;
40 import android.accessibilityservice.AccessibilityService.MagnificationController;
41 import android.accessibilityservice.AccessibilityService.MagnificationController.OnMagnificationChangedListener;
42 import android.accessibilityservice.AccessibilityServiceInfo;
43 import android.accessibilityservice.cts.activities.AccessibilityWindowQueryActivity;
44 import android.app.Activity;
45 import android.app.Instrumentation;
46 import android.app.UiAutomation;
47 import android.graphics.Point;
48 import android.graphics.Rect;
49 import android.graphics.Region;
50 import android.platform.test.annotations.AppModeFull;
51 import android.util.DisplayMetrics;
52 import android.view.View;
53 import android.view.ViewGroup;
54 import android.view.accessibility.AccessibilityNodeInfo;
55 import android.widget.Button;
56 
57 import androidx.test.rule.ActivityTestRule;
58 import androidx.test.runner.AndroidJUnit4;
59 
60 import org.junit.Before;
61 import org.junit.Rule;
62 import org.junit.Test;
63 import org.junit.rules.RuleChain;
64 import org.junit.runner.RunWith;
65 
66 import java.util.concurrent.atomic.AtomicBoolean;
67 
68 /**
69  * Class for testing {@link AccessibilityServiceInfo}.
70  */
71 @AppModeFull
72 @RunWith(AndroidJUnit4.class)
73 public class AccessibilityMagnificationTest {
74 
75     /** Maximum timeout when waiting for a magnification callback. */
76     public static final int LISTENER_TIMEOUT_MILLIS = 500;
77     public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED =
78             "accessibility_display_magnification_enabled";
79     private StubMagnificationAccessibilityService mService;
80     private Instrumentation mInstrumentation;
81 
82     private AccessibilityDumpOnFailureRule mDumpOnFailureRule =
83             new AccessibilityDumpOnFailureRule();
84 
85     private final ActivityTestRule<AccessibilityWindowQueryActivity> mActivityRule =
86             new ActivityTestRule<>(AccessibilityWindowQueryActivity.class, false, false);
87 
88     private InstrumentedAccessibilityServiceTestRule<InstrumentedAccessibilityService>
89             mInstrumentedAccessibilityServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
90                     InstrumentedAccessibilityService.class, false);
91 
92     private InstrumentedAccessibilityServiceTestRule<StubMagnificationAccessibilityService>
93             mMagnificationAccessibilityServiceRule = new InstrumentedAccessibilityServiceTestRule<>(
94                     StubMagnificationAccessibilityService.class, false);
95 
96     @Rule
97     public final RuleChain mRuleChain = RuleChain
98             .outerRule(mActivityRule)
99             .around(mMagnificationAccessibilityServiceRule)
100             .around(mInstrumentedAccessibilityServiceRule)
101             .around(mDumpOnFailureRule);
102 
103     @Before
setUp()104     public void setUp() throws Exception {
105         ShellCommandBuilder.create(getInstrumentation())
106                 .deleteSecureSetting(ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED)
107                 .run();
108         mInstrumentation = getInstrumentation();
109         // Starting the service will force the accessibility subsystem to examine its settings, so
110         // it will update magnification in the process to disable it.
111         mService = mMagnificationAccessibilityServiceRule.enableService();
112     }
113 
114     @Test
testSetScale()115     public void testSetScale() {
116         final MagnificationController controller = mService.getMagnificationController();
117         final float scale = 2.0f;
118         final AtomicBoolean result = new AtomicBoolean();
119 
120         mService.runOnServiceSync(() -> result.set(controller.setScale(scale, false)));
121 
122         assertTrue("Failed to set scale", result.get());
123         assertEquals("Failed to apply scale", scale, controller.getScale(), 0f);
124 
125         mService.runOnServiceSync(() -> result.set(controller.reset(false)));
126 
127         assertTrue("Failed to reset", result.get());
128         assertEquals("Failed to apply reset", 1.0f, controller.getScale(), 0f);
129     }
130 
131     @Test
testSetScaleAndCenter()132     public void testSetScaleAndCenter() {
133         final MagnificationController controller = mService.getMagnificationController();
134         final Region region = controller.getMagnificationRegion();
135         final Rect bounds = region.getBounds();
136         final float scale = 2.0f;
137         final float x = bounds.left + (bounds.width() / 4.0f);
138         final float y = bounds.top + (bounds.height() / 4.0f);
139         final AtomicBoolean setScale = new AtomicBoolean();
140         final AtomicBoolean setCenter = new AtomicBoolean();
141         final AtomicBoolean result = new AtomicBoolean();
142 
143         mService.runOnServiceSync(() -> {
144             setScale.set(controller.setScale(scale, false));
145             setCenter.set(controller.setCenter(x, y, false));
146         });
147 
148         assertTrue("Failed to set scale", setScale.get());
149         assertEquals("Failed to apply scale", scale, controller.getScale(), 0f);
150 
151         assertTrue("Failed to set center", setCenter.get());
152         assertEquals("Failed to apply center X", x, controller.getCenterX(), 5.0f);
153         assertEquals("Failed to apply center Y", y, controller.getCenterY(), 5.0f);
154 
155         mService.runOnServiceSync(() -> result.set(controller.reset(false)));
156 
157         assertTrue("Failed to reset", result.get());
158         assertEquals("Failed to apply reset", 1.0f, controller.getScale(), 0f);
159     }
160 
161     @Test
testListener()162     public void testListener() {
163         final MagnificationController controller = mService.getMagnificationController();
164         final OnMagnificationChangedListener listener = mock(OnMagnificationChangedListener.class);
165         controller.addListener(listener);
166 
167         try {
168             final float scale = 2.0f;
169             final AtomicBoolean result = new AtomicBoolean();
170 
171             mService.runOnServiceSync(() -> result.set(controller.setScale(scale, false)));
172 
173             assertTrue("Failed to set scale", result.get());
174             verify(listener, timeout(LISTENER_TIMEOUT_MILLIS).atLeastOnce()).onMagnificationChanged(
175                     eq(controller), any(Region.class), eq(scale), anyFloat(), anyFloat());
176 
177             mService.runOnServiceSync(() -> result.set(controller.reset(false)));
178 
179             assertTrue("Failed to reset", result.get());
180             verify(listener, timeout(LISTENER_TIMEOUT_MILLIS).atLeastOnce()).onMagnificationChanged(
181                     eq(controller), any(Region.class), eq(1.0f), anyFloat(), anyFloat());
182         } finally {
183             controller.removeListener(listener);
184         }
185     }
186 
187     @Test
testMagnificationServiceShutsDownWhileMagnifying_shouldReturnTo1x()188     public void testMagnificationServiceShutsDownWhileMagnifying_shouldReturnTo1x() {
189         final MagnificationController controller = mService.getMagnificationController();
190         mService.runOnServiceSync(() -> controller.setScale(2.0f, false));
191 
192         mService.runOnServiceSync(() -> mService.disableSelf());
193         mService = null;
194         InstrumentedAccessibilityService service =
195                 mInstrumentedAccessibilityServiceRule.enableService();
196         final MagnificationController controller2 = service.getMagnificationController();
197         assertEquals("Magnification must reset when a service dies",
198                 1.0f, controller2.getScale(), 0f);
199     }
200 
201     @Test
testGetMagnificationRegion_whenCanControlMagnification_shouldNotBeEmpty()202     public void testGetMagnificationRegion_whenCanControlMagnification_shouldNotBeEmpty() {
203         final MagnificationController controller = mService.getMagnificationController();
204         Region magnificationRegion = controller.getMagnificationRegion();
205         assertFalse("Magnification region should not be empty when "
206                  + "magnification is being actively controlled", magnificationRegion.isEmpty());
207     }
208 
209     @Test
testGetMagnificationRegion_whenCantControlMagnification_shouldBeEmpty()210     public void testGetMagnificationRegion_whenCantControlMagnification_shouldBeEmpty() {
211         mService.runOnServiceSync(() -> mService.disableSelf());
212         mService = null;
213         InstrumentedAccessibilityService service =
214                 mInstrumentedAccessibilityServiceRule.enableService();
215         final MagnificationController controller = service.getMagnificationController();
216         Region magnificationRegion = controller.getMagnificationRegion();
217         assertTrue("Magnification region should be empty when magnification "
218                 + "is not being actively controlled", magnificationRegion.isEmpty());
219     }
220 
221     @Test
testGetMagnificationRegion_whenMagnificationGesturesEnabled_shouldNotBeEmpty()222     public void testGetMagnificationRegion_whenMagnificationGesturesEnabled_shouldNotBeEmpty() {
223         ShellCommandBuilder.create(mInstrumentation)
224                 .putSecureSetting(ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED, "1")
225                 .run();
226         mService.runOnServiceSync(() -> mService.disableSelf());
227         mService = null;
228         InstrumentedAccessibilityService service =
229                 mInstrumentedAccessibilityServiceRule.enableService();
230         try {
231             final MagnificationController controller = service.getMagnificationController();
232             Region magnificationRegion = controller.getMagnificationRegion();
233             assertFalse("Magnification region should not be empty when magnification "
234                     + "gestures are active", magnificationRegion.isEmpty());
235         } finally {
236             ShellCommandBuilder.create(mInstrumentation)
237                     .deleteSecureSetting(ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED)
238                     .run();
239         }
240     }
241 
242     @Test
testAnimatingMagnification()243     public void testAnimatingMagnification() throws InterruptedException {
244         final MagnificationController controller = mService.getMagnificationController();
245         final int timeBetweenAnimationChanges = 100;
246 
247         final float scale1 = 5.0f;
248         final float x1 = 500;
249         final float y1 = 1000;
250 
251         final float scale2 = 4.0f;
252         final float x2 = 500;
253         final float y2 = 1500;
254 
255         final float scale3 = 2.1f;
256         final float x3 = 700;
257         final float y3 = 700;
258 
259         for (int i = 0; i < 5; i++) {
260             mService.runOnServiceSync(() -> {
261                 controller.setScale(scale1, true);
262                 controller.setCenter(x1, y1, true);
263             });
264 
265             Thread.sleep(timeBetweenAnimationChanges);
266 
267             mService.runOnServiceSync(() -> {
268                 controller.setScale(scale2, true);
269                 controller.setCenter(x2, y2, true);
270             });
271 
272             Thread.sleep(timeBetweenAnimationChanges);
273 
274             mService.runOnServiceSync(() -> {
275                 controller.setScale(scale3, true);
276                 controller.setCenter(x3, y3, true);
277             });
278 
279             Thread.sleep(timeBetweenAnimationChanges);
280         }
281     }
282 
283     @Test
testA11yNodeInfoVisibility_whenOutOfMagnifiedArea_shouldVisible()284     public void testA11yNodeInfoVisibility_whenOutOfMagnifiedArea_shouldVisible()
285             throws Exception{
286         final UiAutomation uiAutomation = mInstrumentation.getUiAutomation(
287                 UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
288         final Activity activity = launchActivityAndWaitForItToBeOnscreen(
289                 mInstrumentation, uiAutomation, mActivityRule);
290         final MagnificationController controller = mService.getMagnificationController();
291         final Rect magnifyBounds = controller.getMagnificationRegion().getBounds();
292         final float scale = 8.0f;
293         final Button button = activity.findViewById(R.id.button1);
294         adjustViewBoundsIfNeeded(button, scale, magnifyBounds);
295 
296         final AccessibilityNodeInfo buttonNode = uiAutomation.getRootInActiveWindow()
297                 .findAccessibilityNodeInfosByViewId(
298                         "android.accessibilityservice.cts:id/button1").get(0);
299         assertNotNull("Can't find button on the screen", buttonNode);
300         assertTrue("Button should be visible", buttonNode.isVisibleToUser());
301 
302         // Get right-bottom center position
303         final float centerX = magnifyBounds.left + (((float) magnifyBounds.width() / (2.0f * scale))
304                 * ((2.0f * scale) - 1.0f));
305         final float centerY = magnifyBounds.top + (((float) magnifyBounds.height() / (2.0f * scale))
306                 * ((2.0f * scale) - 1.0f));
307         try {
308             waitOnMagnificationChanged(controller, scale, centerX, centerY);
309             // Waiting for UI refresh
310             mInstrumentation.waitForIdleSync();
311             buttonNode.refresh();
312 
313             final Rect boundsInScreen = new Rect();
314             final DisplayMetrics displayMetrics = new DisplayMetrics();
315             buttonNode.getBoundsInScreen(boundsInScreen);
316             activity.getDisplay().getMetrics(displayMetrics);
317             final Rect displayRect = new Rect(0, 0,
318                     displayMetrics.widthPixels, displayMetrics.heightPixels);
319             // The boundsInScreen of button is adjusted to outside of screen by framework,
320             // for example, Rect(-xxx, -xxx, -xxx, -xxx). Intersection of button and screen
321             // should be empty.
322             assertFalse("Button shouldn't be on the screen, screen is " + displayRect
323                             + ", button bounds is " + boundsInScreen,
324                     Rect.intersects(displayRect, boundsInScreen));
325             assertTrue("Button should be visible", buttonNode.isVisibleToUser());
326         } finally {
327             mService.runOnServiceSync(() -> controller.reset(false));
328         }
329     }
330 
waitOnMagnificationChanged(MagnificationController controller, float newScale, float newCenterX, float newCenterY)331     private void waitOnMagnificationChanged(MagnificationController controller, float newScale,
332             float newCenterX, float newCenterY) {
333         final Object waitLock = new Object();
334         final AtomicBoolean notified = new AtomicBoolean();
335         final OnMagnificationChangedListener listener = (c, region, scale, centerX, centerY) -> {
336             final float delta = 5.0f;
337             synchronized (waitLock) {
338                 if (newScale == scale
339                         && (centerX > newCenterX - delta) && (centerY > newCenterY - delta)) {
340                     notified.set(true);
341                     waitLock.notifyAll();
342                 }
343             }
344         };
345         controller.addListener(listener);
346         try {
347             final AtomicBoolean setScale = new AtomicBoolean();
348             final AtomicBoolean setCenter = new AtomicBoolean();
349             mService.runOnServiceSync(() -> {
350                 setScale.set(controller.setScale(newScale, false));
351                 setCenter.set(controller.setCenter(newCenterX, newCenterY, false));
352             });
353             assertTrue("Failed to set scale", setScale.get());
354             assertEquals("Failed to apply scale", newScale, controller.getScale(), 0f);
355             assertTrue("Failed to set center", setCenter.get());
356             waitOn(waitLock, () -> notified.get(), LISTENER_TIMEOUT_MILLIS,
357                     "waitOnMagnificationChanged");
358         } finally {
359             controller.removeListener(listener);
360         }
361     }
362 
363     /**
364      * Adjust top-left view bounds if it's still in the magnified viewport after sets magnification
365      * scale and move centers to bottom-right.
366      */
adjustViewBoundsIfNeeded(View topLeftview, float scale, Rect magnifyBounds)367     private void adjustViewBoundsIfNeeded(View topLeftview, float scale, Rect magnifyBounds) {
368         final Point magnifyViewportTopLeft = new Point();
369         magnifyViewportTopLeft.x = (int)((scale - 1.0f) * ((float) magnifyBounds.width() / scale));
370         magnifyViewportTopLeft.y = (int)((scale - 1.0f) * ((float) magnifyBounds.height() / scale));
371         magnifyViewportTopLeft.offset(magnifyBounds.left, magnifyBounds.top);
372 
373         final int[] viewLocation = new int[2];
374         topLeftview.getLocationOnScreen(viewLocation);
375         final Rect viewBounds = new Rect(viewLocation[0], viewLocation[1],
376                 viewLocation[0] + topLeftview.getWidth(),
377                 viewLocation[1] + topLeftview.getHeight());
378         if (viewBounds.right < magnifyViewportTopLeft.x
379                 && viewBounds.bottom < magnifyViewportTopLeft.y) {
380             // no need
381             return;
382         }
383 
384         final ViewGroup.LayoutParams layoutParams = topLeftview.getLayoutParams();
385         if (viewBounds.right >= magnifyViewportTopLeft.x) {
386             layoutParams.width = topLeftview.getWidth() - 1
387                     - (viewBounds.right - magnifyViewportTopLeft.x);
388             assertTrue("Needs to fix layout", layoutParams.width > 0);
389         }
390         if (viewBounds.bottom >= magnifyViewportTopLeft.y) {
391             layoutParams.height = topLeftview.getHeight() - 1
392                     - (viewBounds.bottom - magnifyViewportTopLeft.y);
393             assertTrue("Needs to fix layout", layoutParams.height > 0);
394         }
395         mInstrumentation.runOnMainSync(() -> topLeftview.setLayoutParams(layoutParams));
396         // Waiting for UI refresh
397         mInstrumentation.waitForIdleSync();
398     }
399 }
400