Home | History | Annotate | Download | only in cts
      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