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