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