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