1 /* 2 * Copyright (C) 2017 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.widget.cts; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertNull; 23 import static org.junit.Assert.assertTrue; 24 import static org.junit.Assume.assumeTrue; 25 26 import android.Manifest; 27 import android.app.Activity; 28 import android.content.Context; 29 import android.content.res.TypedArray; 30 import android.graphics.Bitmap; 31 import android.graphics.Canvas; 32 import android.graphics.Color; 33 import android.graphics.Insets; 34 import android.graphics.Point; 35 import android.graphics.PointF; 36 import android.graphics.drawable.ColorDrawable; 37 import android.graphics.drawable.Drawable; 38 import android.util.DisplayMetrics; 39 import android.view.ContextThemeWrapper; 40 import android.view.Gravity; 41 import android.view.SurfaceHolder; 42 import android.view.SurfaceView; 43 import android.view.View; 44 import android.widget.HorizontalScrollView; 45 import android.widget.LinearLayout; 46 import android.widget.LinearLayout.LayoutParams; 47 import android.widget.Magnifier; 48 import android.widget.ScrollView; 49 50 import androidx.test.InstrumentationRegistry; 51 import androidx.test.annotation.UiThreadTest; 52 import androidx.test.filters.SmallTest; 53 import androidx.test.rule.ActivityTestRule; 54 import androidx.test.runner.AndroidJUnit4; 55 56 import com.android.compatibility.common.util.AdoptShellPermissionsRule; 57 import com.android.compatibility.common.util.WidgetTestUtils; 58 import com.android.compatibility.common.util.WindowUtil; 59 60 import org.junit.Before; 61 import org.junit.Rule; 62 import org.junit.Test; 63 import org.junit.runner.RunWith; 64 65 import java.util.ArrayList; 66 import java.util.Collections; 67 import java.util.HashMap; 68 import java.util.List; 69 import java.util.Map; 70 import java.util.concurrent.CountDownLatch; 71 import java.util.concurrent.TimeUnit; 72 73 /** 74 * Tests for {@link Magnifier}. 75 */ 76 @SmallTest 77 @RunWith(AndroidJUnit4.class) 78 public class MagnifierTest { 79 private static final String TIME_LIMIT_EXCEEDED = 80 "Completing the magnifier operation took too long"; 81 private static final float PIXEL_COMPARISON_DELTA = 1f; 82 private static final float WINDOW_ELEVATION = 10f; 83 84 private Activity mActivity; 85 private LinearLayout mLayout; 86 private View mView; 87 private Magnifier mMagnifier; 88 private DisplayMetrics mDisplayMetrics; 89 90 @Rule(order = 0) 91 public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule( 92 androidx.test.platform.app.InstrumentationRegistry 93 .getInstrumentation().getUiAutomation(), 94 Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX); 95 96 @Rule(order = 1) 97 public ActivityTestRule<MagnifierCtsActivity> mActivityRule = 98 new ActivityTestRule<>(MagnifierCtsActivity.class); 99 100 @Before setup()101 public void setup() throws Throwable { 102 mActivity = mActivityRule.getActivity(); 103 WindowUtil.waitForFocus(mActivity); 104 105 mDisplayMetrics = mActivity.getResources().getDisplayMetrics(); 106 // Do not run the tests, unless the device screen is big enough to fit a magnifier 107 // having the default size. 108 assumeTrue(isScreenBigEnough()); 109 110 mLayout = mActivity.findViewById(R.id.magnifier_activity_centered_view_layout); 111 mView = mActivity.findViewById(R.id.magnifier_centered_view); 112 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mLayout, null); 113 114 mMagnifier = new Magnifier.Builder(mView) 115 .setSize(mView.getWidth() / 2, mView.getHeight() / 2) 116 .build(); 117 mActivityRule.runOnUiThread(() -> { 118 // Elevate the application window to have non-zero insets inside surface. 119 mActivityRule.getActivity().getWindow().setElevation(WINDOW_ELEVATION); 120 }); 121 } 122 isScreenBigEnough()123 private boolean isScreenBigEnough() { 124 // Get the size of the screen in dp. 125 final float dpScreenWidth = mDisplayMetrics.widthPixels / mDisplayMetrics.density; 126 final float dpScreenHeight = mDisplayMetrics.heightPixels / mDisplayMetrics.density; 127 // Get the size of the magnifier window in dp. 128 final PointF dpMagnifier = Magnifier.getMagnifierDefaultSize(); 129 130 return dpScreenWidth >= dpMagnifier.x * 1.1 && dpScreenHeight >= dpMagnifier.y * 1.1; 131 } 132 133 //***** Tests for constructor *****// 134 135 @Test testConstructor()136 public void testConstructor() { 137 new Magnifier(new View(mActivity)); 138 } 139 140 @Test(expected = NullPointerException.class) testConstructor_NPE()141 public void testConstructor_NPE() { 142 new Magnifier(null); 143 } 144 145 //***** Tests for builder *****// 146 147 @Test testBuilder_setsPropertiesCorrectly_whenTheyAreValid()148 public void testBuilder_setsPropertiesCorrectly_whenTheyAreValid() { 149 final int magnifierWidth = 90; 150 final int magnifierHeight = 120; 151 final float zoom = 1.5f; 152 final int sourceToMagnifierHorizontalOffset = 10; 153 final int sourceToMagnifierVerticalOffset = -100; 154 final float cornerRadius = 20.0f; 155 final float elevation = 15.0f; 156 final boolean enableClipping = false; 157 final Drawable overlay = new ColorDrawable(Color.BLUE); 158 159 final Magnifier.Builder builder = new Magnifier.Builder(mView) 160 .setSize(magnifierWidth, magnifierHeight) 161 .setInitialZoom(zoom) 162 .setDefaultSourceToMagnifierOffset(sourceToMagnifierHorizontalOffset, 163 sourceToMagnifierVerticalOffset) 164 .setCornerRadius(cornerRadius) 165 .setInitialZoom(zoom) 166 .setElevation(elevation) 167 .setOverlay(overlay) 168 .setClippingEnabled(enableClipping); 169 final Magnifier magnifier = builder.build(); 170 171 assertEquals(magnifierWidth, magnifier.getWidth()); 172 assertEquals(magnifierHeight, magnifier.getHeight()); 173 assertEquals(zoom, magnifier.getZoom(), 0f); 174 assertEquals(Math.round(magnifierWidth / zoom), magnifier.getSourceWidth()); 175 assertEquals(Math.round(magnifierHeight / zoom), magnifier.getSourceHeight()); 176 assertEquals(sourceToMagnifierHorizontalOffset, 177 magnifier.getDefaultHorizontalSourceToMagnifierOffset()); 178 assertEquals(sourceToMagnifierVerticalOffset, 179 magnifier.getDefaultVerticalSourceToMagnifierOffset()); 180 assertEquals(cornerRadius, magnifier.getCornerRadius(), 0f); 181 assertEquals(elevation, magnifier.getElevation(), 0f); 182 assertEquals(enableClipping, magnifier.isClippingEnabled()); 183 assertEquals(overlay, magnifier.getOverlay()); 184 } 185 186 @Test(expected = NullPointerException.class) testBuilder_throwsException_whenViewIsNull()187 public void testBuilder_throwsException_whenViewIsNull() { 188 new Magnifier.Builder(null); 189 } 190 191 @Test(expected = IllegalArgumentException.class) testBuilder_throwsException_whenWidthIsInvalid()192 public void testBuilder_throwsException_whenWidthIsInvalid() { 193 new Magnifier.Builder(mView).setSize(0, 10); 194 } 195 196 @Test(expected = IllegalArgumentException.class) testBuilder_throwsException_whenHeightIsInvalid()197 public void testBuilder_throwsException_whenHeightIsInvalid() { 198 new Magnifier.Builder(mView).setSize(10, 0); 199 } 200 201 @Test(expected = IllegalArgumentException.class) testBuilder_throwsException_whenZoomIsZero()202 public void testBuilder_throwsException_whenZoomIsZero() { 203 new Magnifier.Builder(mView).setInitialZoom(0f); 204 } 205 206 @Test(expected = IllegalArgumentException.class) testBuilder_throwsException_whenZoomIsNegative()207 public void testBuilder_throwsException_whenZoomIsNegative() { 208 new Magnifier.Builder(mView).setInitialZoom(-1f); 209 } 210 211 @Test(expected = IllegalArgumentException.class) testBuilder_throwsException_whenElevationIsInvalid()212 public void testBuilder_throwsException_whenElevationIsInvalid() { 213 new Magnifier.Builder(mView).setElevation(-1f); 214 } 215 216 @Test(expected = IllegalArgumentException.class) testBuilder_throwsException_whenCornerRadiusIsNegative()217 public void testBuilder_throwsException_whenCornerRadiusIsNegative() { 218 new Magnifier.Builder(mView).setCornerRadius(-1f); 219 } 220 221 //***** Tests for default parameters *****// 222 dpToPixelSize(float dp)223 private int dpToPixelSize(float dp) { 224 return (int) (dp * mDisplayMetrics.density + 0.5f); 225 } 226 dpToPixel(float dp)227 private float dpToPixel(float dp) { 228 return dp * mDisplayMetrics.density; 229 } 230 231 @Test testMagnifierDefaultParameters_withDeprecatedConstructor()232 public void testMagnifierDefaultParameters_withDeprecatedConstructor() { 233 final Magnifier magnifier = new Magnifier(mView); 234 235 final int width = dpToPixelSize(100f); 236 assertEquals(width, magnifier.getWidth()); 237 final int height = dpToPixelSize(48f); 238 assertEquals(height, magnifier.getHeight()); 239 final float elevation = dpToPixel(4f); 240 assertEquals(elevation, magnifier.getElevation(), 0.01f); 241 final float zoom = 1.25f; 242 assertEquals(zoom, magnifier.getZoom(), 0.01f); 243 final int verticalOffset = -dpToPixelSize(42f); 244 assertEquals(verticalOffset, magnifier.getDefaultVerticalSourceToMagnifierOffset()); 245 final int horizontalOffset = dpToPixelSize(0f); 246 assertEquals(horizontalOffset, magnifier.getDefaultHorizontalSourceToMagnifierOffset()); 247 final Context deviceDefaultContext = new ContextThemeWrapper(mView.getContext(), 248 android.R.style.Theme_DeviceDefault); 249 final TypedArray ta = deviceDefaultContext.obtainStyledAttributes( 250 new int[]{android.R.attr.dialogCornerRadius}); 251 final float dialogCornerRadius = ta.getDimension(0, 0); 252 ta.recycle(); 253 assertEquals(dialogCornerRadius, magnifier.getCornerRadius(), 0.01f); 254 final boolean isClippingEnabled = true; 255 assertEquals(isClippingEnabled, magnifier.isClippingEnabled()); 256 final int overlayColor = 0x0EFFFFFF; 257 assertEquals(overlayColor, ((ColorDrawable) magnifier.getOverlay()).getColor()); 258 } 259 260 @Test testMagnifierDefaultParameters_withBuilder()261 public void testMagnifierDefaultParameters_withBuilder() { 262 final Magnifier magnifier = new Magnifier.Builder(mView).build(); 263 264 final int width = dpToPixelSize(100f); 265 assertEquals(width, magnifier.getWidth()); 266 final int height = dpToPixelSize(48f); 267 assertEquals(height, magnifier.getHeight()); 268 final float elevation = dpToPixel(4f); 269 assertEquals(elevation, magnifier.getElevation(), 0.01f); 270 final float zoom = 1.25f; 271 assertEquals(zoom, magnifier.getZoom(), 0.01f); 272 final int verticalOffset = -dpToPixelSize(42f); 273 assertEquals(verticalOffset, magnifier.getDefaultVerticalSourceToMagnifierOffset()); 274 final int horizontalOffset = dpToPixelSize(0f); 275 assertEquals(horizontalOffset, magnifier.getDefaultHorizontalSourceToMagnifierOffset()); 276 final float dialogCornerRadius = dpToPixel(2f); 277 assertEquals(dialogCornerRadius, magnifier.getCornerRadius(), 0.01f); 278 final boolean isClippingEnabled = true; 279 assertEquals(isClippingEnabled, magnifier.isClippingEnabled()); 280 final int overlayColor = 0x00FFFFFF; 281 assertEquals(overlayColor, ((ColorDrawable) magnifier.getOverlay()).getColor()); 282 } 283 284 @Test 285 @UiThreadTest testSizeAndZoom_areValid()286 public void testSizeAndZoom_areValid() { 287 mMagnifier = new Magnifier(mView); 288 // Size should be positive. 289 assertTrue(mMagnifier.getWidth() > 0); 290 assertTrue(mMagnifier.getHeight() > 0); 291 // Source size should be positive. 292 assertTrue(mMagnifier.getSourceWidth() > 0); 293 assertTrue(mMagnifier.getSourceHeight() > 0); 294 // The magnified view region should be zoomed in, not out. 295 assertTrue(mMagnifier.getZoom() > 1.0f); 296 } 297 298 299 //***** Tests for #show() *****// 300 301 @Test testShow()302 public void testShow() throws Throwable { 303 final float xCenter = mView.getWidth() / 2f; 304 final float yCenter = mView.getHeight() / 2f; 305 showMagnifier(xCenter, yCenter); 306 307 final int[] viewLocationInWindow = new int[2]; 308 mView.getLocationInWindow(viewLocationInWindow); 309 310 // Check the coordinates of the content being copied. 311 final Point sourcePosition = mMagnifier.getSourcePosition(); 312 assertNotNull(sourcePosition); 313 assertEquals(xCenter + viewLocationInWindow[0], 314 sourcePosition.x + mMagnifier.getSourceWidth() / 2f, PIXEL_COMPARISON_DELTA); 315 assertEquals(yCenter + viewLocationInWindow[1], 316 sourcePosition.y + mMagnifier.getSourceHeight() / 2f, PIXEL_COMPARISON_DELTA); 317 318 // Check the coordinates of the magnifier. 319 final Point magnifierPosition = mMagnifier.getPosition(); 320 assertNotNull(magnifierPosition); 321 assertEquals(sourcePosition.x + mMagnifier.getDefaultHorizontalSourceToMagnifierOffset() 322 - mMagnifier.getWidth() / 2f + mMagnifier.getSourceWidth() / 2f, 323 magnifierPosition.x, PIXEL_COMPARISON_DELTA); 324 assertEquals(sourcePosition.y + mMagnifier.getDefaultVerticalSourceToMagnifierOffset() 325 - mMagnifier.getHeight() / 2f + mMagnifier.getSourceHeight() / 2f, 326 magnifierPosition.y, PIXEL_COMPARISON_DELTA); 327 } 328 329 @Test testShow_doesNotCrash_whenCalledWithExtremeCoordinates()330 public void testShow_doesNotCrash_whenCalledWithExtremeCoordinates() throws Throwable { 331 showMagnifier(Integer.MIN_VALUE, Integer.MIN_VALUE); 332 showMagnifier(Integer.MIN_VALUE, Integer.MAX_VALUE); 333 showMagnifier(Integer.MAX_VALUE, Integer.MIN_VALUE); 334 showMagnifier(Integer.MAX_VALUE, Integer.MAX_VALUE); 335 } 336 337 @Test testShow_withDecoupledMagnifierPosition()338 public void testShow_withDecoupledMagnifierPosition() throws Throwable { 339 final float xCenter = mView.getWidth() / 2; 340 final float yCenter = mView.getHeight() / 2; 341 342 final int xMagnifier = -20; 343 final int yMagnifier = -10; 344 showMagnifier(xCenter, yCenter, xMagnifier, yMagnifier); 345 346 final int[] viewLocationInWindow = new int[2]; 347 mView.getLocationInWindow(viewLocationInWindow); 348 final Point magnifierPosition = mMagnifier.getPosition(); 349 assertNotNull(magnifierPosition); 350 assertEquals( 351 viewLocationInWindow[0] + xMagnifier - mMagnifier.getWidth() / 2, 352 magnifierPosition.x, PIXEL_COMPARISON_DELTA); 353 assertEquals( 354 viewLocationInWindow[1] + yMagnifier - mMagnifier.getHeight() / 2, 355 magnifierPosition.y, PIXEL_COMPARISON_DELTA); 356 } 357 358 @Test testShow_whenPixelCopyFails()359 public void testShow_whenPixelCopyFails() throws Throwable { 360 WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> { 361 mActivity.setContentView(R.layout.magnifier_activity_centered_surfaceview_layout); 362 }, false /*forceLayout*/); 363 final View view = mActivity.findViewById(R.id.magnifier_centered_view); 364 365 runOnUiThreadAndWaitForCompletion(() -> mMagnifier = new Magnifier.Builder(view).build()); 366 // The PixelCopy will fail as no draw has been done so far to the SurfaceView. 367 showMagnifier(0f, 0f); 368 369 assertNull(mMagnifier.getPosition()); 370 assertNull(mMagnifier.getSourcePosition()); 371 assertNull(mMagnifier.getContent()); 372 } 373 374 //***** Tests for #dismiss() *****// 375 376 @Test testDismiss_doesNotCrash()377 public void testDismiss_doesNotCrash() throws Throwable { 378 showMagnifier(0, 0); 379 final CountDownLatch latch = new CountDownLatch(1); 380 mActivityRule.runOnUiThread(() -> { 381 mMagnifier.dismiss(); 382 mMagnifier.dismiss(); 383 mMagnifier.show(0, 0); 384 mMagnifier.dismiss(); 385 mMagnifier.dismiss(); 386 latch.countDown(); 387 }); 388 assertTrue(TIME_LIMIT_EXCEEDED, latch.await(2, TimeUnit.SECONDS)); 389 } 390 391 //***** Tests for #update() *****// 392 393 @Test testUpdate_doesNotCrash()394 public void testUpdate_doesNotCrash() throws Throwable { 395 showMagnifier(0, 0); 396 final CountDownLatch latch = new CountDownLatch(1); 397 mActivityRule.runOnUiThread(() -> { 398 mMagnifier.update(); 399 mMagnifier.update(); 400 mMagnifier.show(10, 10); 401 mMagnifier.update(); 402 mMagnifier.update(); 403 mMagnifier.dismiss(); 404 mMagnifier.update(); 405 latch.countDown(); 406 }); 407 assertTrue(TIME_LIMIT_EXCEEDED, latch.await(2, TimeUnit.SECONDS)); 408 } 409 410 @Test testMagnifierContent_refreshesAfterUpdate()411 public void testMagnifierContent_refreshesAfterUpdate() throws Throwable { 412 prepareFourQuadrantsScenario(); 413 414 // Show the magnifier at the center of the activity. 415 showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2); 416 417 final Bitmap initialBitmap = mMagnifier.getContent() 418 .copy(mMagnifier.getContent().getConfig(), true); 419 assertFourQuadrants(initialBitmap); 420 421 // Make the one quadrant white. 422 final View quadrant1 = 423 mActivity.findViewById(R.id.magnifier_activity_four_quadrants_layout_quadrant_1); 424 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, quadrant1, () -> { 425 quadrant1.setBackground(null); 426 }); 427 428 // Update the magnifier. 429 runAndWaitForMagnifierOperationComplete(mMagnifier::update); 430 431 final Bitmap newBitmap = mMagnifier.getContent(); 432 assertFourQuadrants(newBitmap); 433 assertFalse(newBitmap.sameAs(initialBitmap)); 434 } 435 436 //***** Tests for the position of the magnifier *****// 437 438 @Test testWindowPosition_isClampedInsideMainApplicationWindow_topLeft()439 public void testWindowPosition_isClampedInsideMainApplicationWindow_topLeft() throws Throwable { 440 prepareFourQuadrantsScenario(); 441 442 // Magnify the center of the activity in a magnifier outside bounds. 443 showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2, 444 -mMagnifier.getWidth(), -mMagnifier.getHeight()); 445 446 // The window should have been positioned to the top left of the activity, 447 // such that it does not overlap system insets. 448 final Insets systemInsets = mLayout.getRootWindowInsets().getSystemWindowInsets(); 449 final Point magnifierCoords = mMagnifier.getPosition(); 450 assertNotNull(magnifierCoords); 451 assertEquals(systemInsets.left, magnifierCoords.x, PIXEL_COMPARISON_DELTA); 452 assertEquals(systemInsets.top, magnifierCoords.y, PIXEL_COMPARISON_DELTA); 453 } 454 455 @Test testWindowPosition_isClampedInsideMainApplicationWindow_bottomRight()456 public void testWindowPosition_isClampedInsideMainApplicationWindow_bottomRight() 457 throws Throwable { 458 prepareFourQuadrantsScenario(); 459 460 // Magnify the center of the activity in a magnifier outside bounds. 461 showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2, 462 mLayout.getRootView().getWidth() + mMagnifier.getWidth(), 463 mLayout.getRootView().getHeight() + mMagnifier.getHeight()); 464 465 // The window should have been positioned to the bottom right of the activity. 466 final Insets systemInsets = mLayout.getRootWindowInsets().getSystemWindowInsets(); 467 final Point magnifierCoords = mMagnifier.getPosition(); 468 assertNotNull(magnifierCoords); 469 assertEquals(mLayout.getRootView().getWidth() 470 - systemInsets.right - mMagnifier.getWidth(), 471 magnifierCoords.x, PIXEL_COMPARISON_DELTA); 472 assertEquals(mLayout.getRootView().getHeight() 473 - systemInsets.bottom - mMagnifier.getHeight(), 474 magnifierCoords.y, PIXEL_COMPARISON_DELTA); 475 } 476 477 @Test testWindowPosition_isNotClamped_whenClampingFlagIsOff_topLeft()478 public void testWindowPosition_isNotClamped_whenClampingFlagIsOff_topLeft() throws Throwable { 479 prepareFourQuadrantsScenario(); 480 mMagnifier = new Magnifier.Builder(mLayout) 481 .setClippingEnabled(false) 482 .build(); 483 484 // Magnify the center of the activity in a magnifier outside bounds. 485 showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2, 486 -mMagnifier.getWidth(), -mMagnifier.getHeight()); 487 488 // The window should have not been clamped. 489 final Point magnifierCoords = mMagnifier.getPosition(); 490 final int[] magnifiedViewPosition = new int[2]; 491 mLayout.getLocationInWindow(magnifiedViewPosition); 492 assertNotNull(magnifierCoords); 493 assertEquals(magnifiedViewPosition[0] - 3 * mMagnifier.getWidth() / 2, magnifierCoords.x, 494 PIXEL_COMPARISON_DELTA); 495 assertEquals(magnifiedViewPosition[1] - 3 * mMagnifier.getHeight() / 2, magnifierCoords.y, 496 PIXEL_COMPARISON_DELTA); 497 } 498 499 @Test testWindowPosition_isNotClamped_whenClampingFlagIsOff_bottomRight()500 public void testWindowPosition_isNotClamped_whenClampingFlagIsOff_bottomRight() 501 throws Throwable { 502 prepareFourQuadrantsScenario(); 503 mMagnifier = new Magnifier.Builder(mLayout) 504 .setClippingEnabled(false) 505 .setSize(40, 40) 506 .build(); 507 508 // Magnify the center of the activity in a magnifier outside bounds. 509 showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2, 510 mLayout.getRootView().getWidth() + mMagnifier.getWidth(), 511 mLayout.getRootView().getHeight() + mMagnifier.getHeight()); 512 513 // The window should have not been clamped. 514 final Point magnifierCoords = mMagnifier.getPosition(); 515 final int[] magnifiedViewPosition = new int[2]; 516 mLayout.getLocationInWindow(magnifiedViewPosition); 517 assertNotNull(magnifierCoords); 518 assertEquals(magnifiedViewPosition[0] + mLayout.getRootView().getWidth() 519 + mMagnifier.getWidth() / 2, magnifierCoords.x, PIXEL_COMPARISON_DELTA); 520 assertEquals(magnifiedViewPosition[1] + mLayout.getRootView().getHeight() 521 + mMagnifier.getHeight() / 2, magnifierCoords.y, PIXEL_COMPARISON_DELTA); 522 } 523 524 @Test testWindowPosition_isCorrect_whenADefaultContentToMagnifierOffsetIsUsed()525 public void testWindowPosition_isCorrect_whenADefaultContentToMagnifierOffsetIsUsed() 526 throws Throwable { 527 prepareFourQuadrantsScenario(); 528 final int horizontalOffset = 5; 529 final int verticalOffset = -10; 530 mMagnifier = new Magnifier.Builder(mLayout) 531 .setSize(20, 10) /* make magnifier small to avoid having it clamped */ 532 .setDefaultSourceToMagnifierOffset(horizontalOffset, verticalOffset) 533 .build(); 534 535 // Magnify the center of the activity in a magnifier outside bounds. 536 showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2); 537 538 final Point magnifierCoords = mMagnifier.getPosition(); 539 final Point sourceCoords = mMagnifier.getSourcePosition(); 540 assertNotNull(magnifierCoords); 541 assertEquals(sourceCoords.x + mMagnifier.getSourceWidth() / 2f + horizontalOffset, 542 magnifierCoords.x + mMagnifier.getWidth() / 2f, PIXEL_COMPARISON_DELTA); 543 assertEquals(sourceCoords.y + mMagnifier.getSourceHeight() / 2f + verticalOffset, 544 magnifierCoords.y + mMagnifier.getHeight() / 2f, PIXEL_COMPARISON_DELTA); 545 } 546 547 @Test 548 @UiThreadTest testWindowPosition_isNull_whenMagnifierIsNotShowing()549 public void testWindowPosition_isNull_whenMagnifierIsNotShowing() { 550 mMagnifier = new Magnifier.Builder(mLayout) 551 .setSize(20, 10) /* make magnifier small to avoid having it clamped */ 552 .build(); 553 554 // No #show has been requested, so the position should be null. 555 assertNull(mMagnifier.getPosition()); 556 // #show should make the position not null. 557 mMagnifier.show(0, 0); 558 assertNotNull(mMagnifier.getPosition()); 559 // #dismiss should make the position null. 560 mMagnifier.dismiss(); 561 assertNull(mMagnifier.getPosition()); 562 } 563 564 //***** Tests for the position of the content copied to the magnifier *****// 565 566 @Test 567 @UiThreadTest testSourcePosition_isNull_whenMagnifierIsNotShowing()568 public void testSourcePosition_isNull_whenMagnifierIsNotShowing() { 569 mMagnifier = new Magnifier.Builder(mLayout) 570 .setSize(20, 10) /* make magnifier small to avoid having it clamped */ 571 .build(); 572 573 // No #show has been requested, so the source position should be null. 574 assertNull(mMagnifier.getSourcePosition()); 575 // #show should make the source position not null. 576 mMagnifier.show(0, 0); 577 assertNotNull(mMagnifier.getSourcePosition()); 578 // #dismiss should make the source position null. 579 mMagnifier.dismiss(); 580 assertNull(mMagnifier.getSourcePosition()); 581 } 582 583 @Test testSourcePosition_respectsMaxVisibleBounds_inHorizontalScrollableContainer()584 public void testSourcePosition_respectsMaxVisibleBounds_inHorizontalScrollableContainer() 585 throws Throwable { 586 WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> { 587 mActivity.setContentView(R.layout.magnifier_activity_scrollable_views_layout); 588 }, false /*forceLayout*/); 589 final View view = mActivity 590 .findViewById(R.id.magnifier_activity_horizontally_scrolled_view); 591 final HorizontalScrollView container = (HorizontalScrollView) mActivity 592 .findViewById(R.id.horizontal_scroll_container); 593 final Magnifier.Builder builder = new Magnifier.Builder(view) 594 .setSize(100, 100) 595 .setInitialZoom(20f) /* 5x5 source size */ 596 .setSourceBounds( 597 Magnifier.SOURCE_BOUND_MAX_VISIBLE, 598 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, 599 Magnifier.SOURCE_BOUND_MAX_VISIBLE, 600 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE 601 ); 602 603 runOnUiThreadAndWaitForCompletion(() -> { 604 mMagnifier = builder.build(); 605 // Scroll halfway horizontally. 606 container.scrollTo(view.getWidth() / 2, 0); 607 }); 608 609 final int[] containerPosition = new int[2]; 610 container.getLocationInWindow(containerPosition); 611 612 // Try to copy from an x to the left of the currently visible region. 613 showMagnifier(view.getWidth() / 4, 0); 614 Point sourcePosition = mMagnifier.getSourcePosition(); 615 assertNotNull(sourcePosition); 616 assertEquals(containerPosition[0], sourcePosition.x, PIXEL_COMPARISON_DELTA); 617 618 // Try to copy from an x to the right of the currently visible region. 619 showMagnifier(3 * view.getWidth() / 4, 0); 620 sourcePosition = mMagnifier.getSourcePosition(); 621 assertNotNull(sourcePosition); 622 assertEquals(containerPosition[0] + container.getWidth() - mMagnifier.getSourceWidth(), 623 sourcePosition.x, PIXEL_COMPARISON_DELTA); 624 } 625 626 @Test testSourcePosition_respectsMaxVisibleBounds_inVerticalScrollableContainer()627 public void testSourcePosition_respectsMaxVisibleBounds_inVerticalScrollableContainer() 628 throws Throwable { 629 WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> { 630 mActivity.setContentView(R.layout.magnifier_activity_scrollable_views_layout); 631 }, false /*forceLayout*/); 632 final View view = mActivity.findViewById(R.id.magnifier_activity_vertically_scrolled_view); 633 final ScrollView container = (ScrollView) mActivity 634 .findViewById(R.id.vertical_scroll_container); 635 final Magnifier.Builder builder = new Magnifier.Builder(view) 636 .setSize(100, 100) 637 .setInitialZoom(10f) /* 10x10 source size */ 638 .setSourceBounds( 639 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, 640 Magnifier.SOURCE_BOUND_MAX_VISIBLE, 641 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, 642 Magnifier.SOURCE_BOUND_MAX_VISIBLE 643 ); 644 645 runOnUiThreadAndWaitForCompletion(() -> { 646 mMagnifier = builder.build(); 647 // Scroll halfway vertically. 648 container.scrollTo(0, view.getHeight() / 2); 649 }); 650 651 final int[] containerPosition = new int[2]; 652 container.getLocationInWindow(containerPosition); 653 654 // Try to copy from an y above the currently visible region. 655 showMagnifier(0, view.getHeight() / 4); 656 Point sourcePosition = mMagnifier.getSourcePosition(); 657 assertNotNull(sourcePosition); 658 assertEquals(containerPosition[1], sourcePosition.y, PIXEL_COMPARISON_DELTA); 659 660 // Try to copy from an x below the currently visible region. 661 showMagnifier(0, 3 * view.getHeight() / 4); 662 sourcePosition = mMagnifier.getSourcePosition(); 663 assertNotNull(sourcePosition); 664 assertEquals(containerPosition[1] + container.getHeight() - mMagnifier.getSourceHeight(), 665 sourcePosition.y, PIXEL_COMPARISON_DELTA); 666 } 667 668 @Test testSourcePosition_respectsMaxInSurfaceBounds()669 public void testSourcePosition_respectsMaxInSurfaceBounds() throws Throwable { 670 WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> { 671 mActivity.setContentView(R.layout.magnifier_activity_centered_view_layout); 672 }, false /*forceLayout*/); 673 final View view = mActivity.findViewById(R.id.magnifier_centered_view); 674 final Magnifier.Builder builder = new Magnifier.Builder(view) 675 .setSize(100, 100) 676 .setInitialZoom(5f) /* 20x20 source size */ 677 .setSourceBounds( 678 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, 679 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, 680 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, 681 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE 682 ); 683 684 runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build()); 685 686 final int[] viewPosition = new int[2]; 687 view.getLocationInWindow(viewPosition); 688 689 // Copy content centered on relative position (0, 0) and expect the top left 690 // corner of the source NOT to have been pulled to coincide with (0, 0) of the view. 691 showMagnifier(0, 0); 692 Point sourcePosition = mMagnifier.getSourcePosition(); 693 assertNotNull(sourcePosition); 694 assertEquals(viewPosition[0] - mMagnifier.getSourceWidth() / 2, sourcePosition.x, 695 PIXEL_COMPARISON_DELTA); 696 assertEquals(viewPosition[1] - mMagnifier.getSourceHeight() / 2, sourcePosition.y, 697 PIXEL_COMPARISON_DELTA); 698 699 // Copy content centered on the bottom right corner of the view and expect the top left 700 // corner of the source NOT to have been pulled inside the view. 701 showMagnifier(view.getWidth(), view.getHeight()); 702 sourcePosition = mMagnifier.getSourcePosition(); 703 assertNotNull(sourcePosition); 704 assertEquals(viewPosition[0] + view.getWidth() - mMagnifier.getSourceWidth() / 2, 705 sourcePosition.x, PIXEL_COMPARISON_DELTA); 706 assertEquals(viewPosition[1] + view.getHeight() - mMagnifier.getSourceHeight() / 2, 707 sourcePosition.y, PIXEL_COMPARISON_DELTA); 708 709 final int[] viewPositionInSurface = new int[2]; 710 view.getLocationInSurface(viewPositionInSurface); 711 // Copy content centered on the top left corner of the main app surface and expect the top 712 // left corner of the source to have been pulled to the top left corner of the surface. 713 showMagnifier(-viewPositionInSurface[0], -viewPositionInSurface[1]); 714 sourcePosition = mMagnifier.getSourcePosition(); 715 assertNotNull(sourcePosition); 716 assertEquals(0, sourcePosition.x - viewPosition[0] + viewPositionInSurface[0], 717 PIXEL_COMPARISON_DELTA); 718 assertEquals(0, sourcePosition.y - viewPosition[1] + viewPositionInSurface[1], 719 PIXEL_COMPARISON_DELTA); 720 721 // Copy content below and to the right of the bottom right corner of the main app surface 722 // and expect the source to have been pulled inside the surface at its bottom right. 723 showMagnifier(2 * view.getRootView().getWidth(), 2 * view.getRootView().getHeight()); 724 sourcePosition = mMagnifier.getSourcePosition(); 725 assertNotNull(sourcePosition); 726 assertTrue( 727 sourcePosition.x < 2 * view.getRootView().getWidth() - mMagnifier.getSourceWidth()); 728 assertTrue(sourcePosition.x > view.getRootView().getWidth() - mMagnifier.getSourceWidth()); 729 assertTrue(sourcePosition.y 730 < 2 * view.getRootView().getHeight() - mMagnifier.getSourceHeight()); 731 assertTrue(sourcePosition.y 732 > view.getRootView().getHeight() - mMagnifier.getSourceHeight()); 733 } 734 735 @Test testSourcePosition_respectsMaxInSurfaceBounds_forSurfaceView()736 public void testSourcePosition_respectsMaxInSurfaceBounds_forSurfaceView() throws Throwable { 737 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 738 WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> { 739 mActivity.setContentView(R.layout.magnifier_activity_centered_surfaceview_layout); 740 }, false /* forceLayout */); 741 mActivityRule.runOnUiThread(() -> { 742 // Draw something in the SurfaceView for the Magnifier to copy. 743 final View view = mActivity.findViewById(R.id.magnifier_centered_view); 744 final SurfaceHolder surfaceHolder = ((SurfaceView) view).getHolder(); 745 final Canvas canvas = surfaceHolder.lockHardwareCanvas(); 746 canvas.drawColor(Color.BLUE); 747 surfaceHolder.unlockCanvasAndPost(canvas); 748 }); 749 final View view = mActivity.findViewById(R.id.magnifier_centered_view); 750 final Magnifier.Builder builder = new Magnifier.Builder(view) 751 .setSize(100, 100) 752 .setInitialZoom(5f) /* 20x20 source size */ 753 .setSourceBounds( 754 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, 755 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, 756 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, 757 Magnifier.SOURCE_BOUND_MAX_IN_SURFACE 758 ); 759 760 runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build()); 761 762 // Copy content centered on relative position (0, 0) and expect the top left 763 // corner of the source to have been pulled to coincide with (0, 0) of the view 764 // (since the view coincides with the surface content is copied from). 765 showMagnifier(0, 0); 766 Point sourcePosition = mMagnifier.getSourcePosition(); 767 assertNotNull(sourcePosition); 768 assertEquals(0, sourcePosition.x, PIXEL_COMPARISON_DELTA); 769 assertEquals(0, sourcePosition.y, PIXEL_COMPARISON_DELTA); 770 771 // Copy content centered on the bottom right corner of the view and expect the top left 772 // corner of the source to have been pulled inside the surface view. 773 showMagnifier(view.getWidth(), view.getHeight()); 774 sourcePosition = mMagnifier.getSourcePosition(); 775 assertNotNull(sourcePosition); 776 assertEquals(view.getWidth() - mMagnifier.getSourceWidth(), sourcePosition.x); 777 assertEquals(view.getHeight() - mMagnifier.getSourceHeight(), sourcePosition.y); 778 779 // Copy content from the center of the surface view and expect no clamping to be done. 780 showMagnifier(view.getWidth() / 2, view.getHeight() / 2); 781 sourcePosition = mMagnifier.getSourcePosition(); 782 assertNotNull(sourcePosition); 783 assertEquals(view.getWidth() / 2 - mMagnifier.getSourceWidth() / 2, sourcePosition.x, 784 PIXEL_COMPARISON_DELTA); 785 assertEquals(view.getHeight() / 2 - mMagnifier.getSourceHeight() / 2, sourcePosition.y, 786 PIXEL_COMPARISON_DELTA); 787 } 788 789 @Test testSourceBounds_areAdjustedWhenInvalid()790 public void testSourceBounds_areAdjustedWhenInvalid() throws Throwable { 791 WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> { 792 mActivity.setContentView(R.layout.magnifier_activity_centered_view_layout); 793 }, false /*forceLayout*/); 794 final View view = mActivity.findViewById(R.id.magnifier_centered_view); 795 final Insets systemInsets = view.getRootWindowInsets().getSystemWindowInsets(); 796 final Magnifier.Builder builder = new Magnifier.Builder(view) 797 .setSize(2 * view.getWidth() + systemInsets.right, 798 2 * view.getHeight() + systemInsets.bottom) 799 .setInitialZoom(1f) /* source double the size of the view + right/bottom insets */ 800 .setSourceBounds(/* invalid bounds */ 801 Magnifier.SOURCE_BOUND_MAX_VISIBLE, 802 Magnifier.SOURCE_BOUND_MAX_VISIBLE, 803 Magnifier.SOURCE_BOUND_MAX_VISIBLE, 804 Magnifier.SOURCE_BOUND_MAX_VISIBLE 805 ); 806 807 runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build()); 808 809 final int[] viewPosition = new int[2]; 810 view.getLocationInWindow(viewPosition); 811 812 // Make sure that the left and top bounds are respected, since this is possible 813 // for this source size, when the view is centered. 814 showMagnifier(0, 0); 815 Point sourcePosition = mMagnifier.getSourcePosition(); 816 assertEquals(viewPosition[0], sourcePosition.x, PIXEL_COMPARISON_DELTA); 817 assertEquals(viewPosition[1], sourcePosition.y, PIXEL_COMPARISON_DELTA); 818 819 // Move the magnified view to the top left of the screen, and make sure that 820 // the top and left bounds are still respected. 821 mActivityRule.runOnUiThread(() -> { 822 final LinearLayout layout = 823 mActivity.findViewById(R.id.magnifier_activity_centered_view_layout); 824 layout.setGravity(Gravity.TOP | Gravity.LEFT); 825 }); 826 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, view, null); 827 view.getLocationInWindow(viewPosition); 828 829 showMagnifier(0, 0); 830 sourcePosition = mMagnifier.getSourcePosition(); 831 assertEquals(viewPosition[0], sourcePosition.x, PIXEL_COMPARISON_DELTA); 832 assertEquals(viewPosition[1], sourcePosition.y, PIXEL_COMPARISON_DELTA); 833 834 // Move the magnified view to the bottom right of the layout, and expect the top and left 835 // bounds to have been shifted such that the source sits inside the surface. 836 mActivityRule.runOnUiThread(() -> { 837 final LinearLayout layout = 838 mActivity.findViewById(R.id.magnifier_activity_centered_view_layout); 839 layout.setGravity(Gravity.BOTTOM | Gravity.RIGHT); 840 }); 841 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, view, null); 842 view.getLocationInSurface(viewPosition); 843 844 showMagnifier(0, 0); 845 sourcePosition = mMagnifier.getSourcePosition(); 846 assertEquals(viewPosition[0] - view.getWidth(), sourcePosition.x, PIXEL_COMPARISON_DELTA); 847 assertEquals(viewPosition[1] - view.getHeight(), sourcePosition.y, PIXEL_COMPARISON_DELTA); 848 } 849 850 //***** Tests for zoom change *****// 851 852 @Test testZoomChange()853 public void testZoomChange() throws Throwable { 854 // Setup. 855 final View view = new View(mActivity); 856 final int width = 300; 857 final int height = 270; 858 final Magnifier.Builder builder = new Magnifier.Builder(view) 859 .setSize(width, height) 860 .setInitialZoom(1.0f); 861 mMagnifier = builder.build(); 862 final float newZoom = 1.5f; 863 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, view, () -> { 864 mLayout.addView(view, new LayoutParams(200, 200)); 865 mMagnifier.setZoom(newZoom); 866 }); 867 assertEquals((int) (width / newZoom), mMagnifier.getSourceWidth()); 868 assertEquals((int) (height / newZoom), mMagnifier.getSourceHeight()); 869 870 // Show. 871 showMagnifier(200, 200); 872 873 // Check bitmap size. 874 assertNotNull(mMagnifier.getOriginalContent()); 875 assertEquals((int) (width / newZoom), mMagnifier.getOriginalContent().getWidth()); 876 assertEquals((int) (height / newZoom), mMagnifier.getOriginalContent().getHeight()); 877 } 878 879 @Test(expected = IllegalArgumentException.class) testZoomChange_throwsException_whenZoomIsZero()880 public void testZoomChange_throwsException_whenZoomIsZero() { 881 final View view = new View(mActivity); 882 new Magnifier(view).setZoom(0f); 883 } 884 885 @Test(expected = IllegalArgumentException.class) testZoomChange_throwsException_whenZoomIsNegative()886 public void testZoomChange_throwsException_whenZoomIsNegative() { 887 final View view = new View(mActivity); 888 new Magnifier(view).setZoom(-1f); 889 } 890 891 //***** Tests for overlay *****// 892 893 @Test testOverlay_isDrawn()894 public void testOverlay_isDrawn() throws Throwable { 895 final Magnifier.Builder builder = new Magnifier.Builder(mView) 896 .setSize(50, 50) 897 .setOverlay(new ColorDrawable(Color.BLUE)); 898 runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build()); 899 900 showMagnifier(0, 0); 901 // Assert that the content has the correct size and is all blue. 902 final Bitmap content = mMagnifier.getContent(); 903 assertNotNull(content); 904 assertEquals(mMagnifier.getWidth(), content.getWidth()); 905 assertEquals(mMagnifier.getHeight(), content.getHeight()); 906 for (int i = 0; i < content.getWidth(); ++i) { 907 for (int j = 0; j < content.getHeight(); ++j) { 908 assertEquals(Color.BLUE, content.getPixel(i, j)); 909 } 910 } 911 } 912 913 @Test testOverlay_redrawsOnInvalidation()914 public void testOverlay_redrawsOnInvalidation() throws Throwable { 915 final ColorDrawable overlay = new ColorDrawable(Color.BLUE); 916 final Magnifier.Builder builder = new Magnifier.Builder(mView) 917 .setSize(50, 50) 918 .setOverlay(overlay); 919 runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build()); 920 921 showMagnifier(0, 0); 922 overlay.setColor(Color.WHITE); 923 // Assert that the content has the correct size and is all blue. 924 final Bitmap content = mMagnifier.getContent(); 925 assertNotNull(content); 926 assertEquals(mMagnifier.getWidth(), content.getWidth()); 927 assertEquals(mMagnifier.getHeight(), content.getHeight()); 928 for (int i = 0; i < content.getWidth(); ++i) { 929 for (int j = 0; j < content.getHeight(); ++j) { 930 assertEquals(Color.WHITE, content.getPixel(i, j)); 931 } 932 } 933 } 934 935 @Test testOverlay_isNotVisible_whenSetToNull()936 public void testOverlay_isNotVisible_whenSetToNull() throws Throwable { 937 final Magnifier.Builder builder = new Magnifier.Builder(mView) 938 .setSize(50, 50) 939 .setInitialZoom(10f) /* 5x5 source size */ 940 .setOverlay(null); 941 runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build()); 942 943 showMagnifier(mView.getWidth() / 2, mView.getHeight() / 2); 944 // Assert that the content has the correct size and is all the view color. 945 final Bitmap content = mMagnifier.getContent(); 946 assertNotNull(content); 947 assertEquals(mMagnifier.getWidth(), content.getWidth()); 948 assertEquals(mMagnifier.getHeight(), content.getHeight()); 949 final int viewColor = mView.getContext().getResources().getColor( 950 android.R.color.holo_blue_bright, null); 951 for (int i = 0; i < content.getWidth(); ++i) { 952 for (int j = 0; j < content.getHeight(); ++j) { 953 assertEquals(viewColor, content.getPixel(i, j)); 954 } 955 } 956 } 957 958 //***** Helper methods / classes *****// 959 showMagnifier(float sourceX, float sourceY)960 private void showMagnifier(float sourceX, float sourceY) throws Throwable { 961 runAndWaitForMagnifierOperationComplete(() -> mMagnifier.show(sourceX, sourceY)); 962 } 963 showMagnifier(float sourceX, float sourceY, float magnifierX, float magnifierY)964 private void showMagnifier(float sourceX, float sourceY, float magnifierX, float magnifierY) 965 throws Throwable { 966 runAndWaitForMagnifierOperationComplete(() -> mMagnifier.show(sourceX, sourceY, 967 magnifierX, magnifierY)); 968 } 969 runAndWaitForMagnifierOperationComplete(final Runnable lambda)970 private void runAndWaitForMagnifierOperationComplete(final Runnable lambda) throws Throwable { 971 final CountDownLatch latch = new CountDownLatch(1); 972 mMagnifier.setOnOperationCompleteCallback(latch::countDown); 973 mActivityRule.runOnUiThread(lambda); 974 assertTrue(TIME_LIMIT_EXCEEDED, latch.await(2, TimeUnit.SECONDS)); 975 } 976 runOnUiThreadAndWaitForCompletion(final Runnable lambda)977 private void runOnUiThreadAndWaitForCompletion(final Runnable lambda) throws Throwable { 978 final CountDownLatch latch = new CountDownLatch(1); 979 mActivityRule.runOnUiThread(() -> { 980 lambda.run(); 981 latch.countDown(); 982 }); 983 assertTrue(TIME_LIMIT_EXCEEDED, latch.await(2, TimeUnit.SECONDS)); 984 } 985 986 /** 987 * Sets the activity to contain four equal quadrants coloured differently and 988 * instantiates a magnifier. This method should not be called on the UI thread. 989 */ prepareFourQuadrantsScenario()990 private void prepareFourQuadrantsScenario() throws Throwable { 991 WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> { 992 mActivity.setContentView(R.layout.magnifier_activity_four_quadrants_layout); 993 mLayout = mActivity.findViewById(R.id.magnifier_activity_four_quadrants_layout); 994 mMagnifier = new Magnifier(mLayout); 995 }, false /*forceLayout*/); 996 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mLayout, null); 997 } 998 999 /** 1000 * Asserts that the current bitmap contains four different dominant colors, which 1001 * are (almost) equally distributed. The test takes into account an amount of 1002 * noise, possible consequence of upscaling and filtering the magnified content. 1003 * 1004 * @param bitmap the bitmap to be checked 1005 */ assertFourQuadrants(final Bitmap bitmap)1006 private void assertFourQuadrants(final Bitmap bitmap) { 1007 final int expectedQuadrants = 4; 1008 final int totalPixels = bitmap.getWidth() * bitmap.getHeight(); 1009 1010 final Map<Integer, Integer> colorCount = new HashMap<>(); 1011 for (int x = 0; x < bitmap.getWidth(); ++x) { 1012 for (int y = 0; y < bitmap.getHeight(); ++y) { 1013 final int currentColor = bitmap.getPixel(x, y); 1014 colorCount.put(currentColor, colorCount.getOrDefault(currentColor, 0) + 1); 1015 } 1016 } 1017 assertTrue(colorCount.size() >= expectedQuadrants); 1018 1019 final List<Integer> counts = new ArrayList<>(colorCount.values()); 1020 Collections.sort(counts); 1021 1022 int quadrantsTotal = 0; 1023 for (int i = counts.size() - expectedQuadrants; i < counts.size(); ++i) { 1024 quadrantsTotal += counts.get(i); 1025 } 1026 assertTrue(1.0f * (totalPixels - quadrantsTotal) / totalPixels <= 0.1f); 1027 1028 for (int i = counts.size() - expectedQuadrants; i < counts.size(); ++i) { 1029 final float proportion = 1.0f 1030 * Math.abs(expectedQuadrants * counts.get(i) - quadrantsTotal) / quadrantsTotal; 1031 assertTrue(proportion <= 0.1f); 1032 } 1033 } 1034 } 1035