blob: ef3e4cda811161ef856620802f93021199fae449 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.widget.cts;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import android.widget.Magnifier;
import android.widget.ScrollView;
import androidx.test.annotation.UiThreadTest;
import androidx.test.filters.SmallTest;
import androidx.test.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.PollingCheck;
import com.android.compatibility.common.util.WidgetTestUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Tests for {@link Magnifier}.
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
public class MagnifierTest {
private static final String TIME_LIMIT_EXCEEDED =
"Completing the magnifier operation took too long";
private static final float PIXEL_COMPARISON_DELTA = 1f;
private static final float WINDOW_ELEVATION = 10f;
private Activity mActivity;
private LinearLayout mLayout;
private View mView;
private Magnifier mMagnifier;
private DisplayMetrics mDisplayMetrics;
@Rule
public ActivityTestRule<MagnifierCtsActivity> mActivityRule =
new ActivityTestRule<>(MagnifierCtsActivity.class);
@Before
public void setup() throws Throwable {
mActivity = mActivityRule.getActivity();
PollingCheck.waitFor(mActivity::hasWindowFocus);
mDisplayMetrics = mActivity.getResources().getDisplayMetrics();
// Do not run the tests, unless the device screen is big enough to fit a magnifier
// having the default size.
assumeTrue(isScreenBigEnough());
mLayout = mActivity.findViewById(R.id.magnifier_activity_centered_view_layout);
mView = mActivity.findViewById(R.id.magnifier_centered_view);
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mLayout, null);
mMagnifier = new Magnifier.Builder(mView)
.setSize(mView.getWidth() / 2, mView.getHeight() / 2)
.build();
mActivityRule.runOnUiThread(() -> {
// Elevate the application window to have non-zero insets inside surface.
mActivityRule.getActivity().getWindow().setElevation(WINDOW_ELEVATION);
});
}
private boolean isScreenBigEnough() {
// Get the size of the screen in dp.
final float dpScreenWidth = mDisplayMetrics.widthPixels / mDisplayMetrics.density;
final float dpScreenHeight = mDisplayMetrics.heightPixels / mDisplayMetrics.density;
// Get the size of the magnifier window in dp.
final PointF dpMagnifier = Magnifier.getMagnifierDefaultSize();
return dpScreenWidth >= dpMagnifier.x * 1.1 && dpScreenHeight >= dpMagnifier.y * 1.1;
}
//***** Tests for constructor *****//
@Test
public void testConstructor() {
new Magnifier(new View(mActivity));
}
@Test(expected = NullPointerException.class)
public void testConstructor_NPE() {
new Magnifier(null);
}
//***** Tests for builder *****//
@Test
public void testBuilder_setsPropertiesCorrectly_whenTheyAreValid() {
final int magnifierWidth = 90;
final int magnifierHeight = 120;
final float zoom = 1.5f;
final int sourceToMagnifierHorizontalOffset = 10;
final int sourceToMagnifierVerticalOffset = -100;
final float cornerRadius = 20.0f;
final float elevation = 15.0f;
final boolean enableClipping = false;
final Drawable overlay = new ColorDrawable(Color.BLUE);
final Magnifier.Builder builder = new Magnifier.Builder(mView)
.setSize(magnifierWidth, magnifierHeight)
.setInitialZoom(zoom)
.setDefaultSourceToMagnifierOffset(sourceToMagnifierHorizontalOffset,
sourceToMagnifierVerticalOffset)
.setCornerRadius(cornerRadius)
.setInitialZoom(zoom)
.setElevation(elevation)
.setOverlay(overlay)
.setClippingEnabled(enableClipping);
final Magnifier magnifier = builder.build();
assertEquals(magnifierWidth, magnifier.getWidth());
assertEquals(magnifierHeight, magnifier.getHeight());
assertEquals(zoom, magnifier.getZoom(), 0f);
assertEquals(Math.round(magnifierWidth / zoom), magnifier.getSourceWidth());
assertEquals(Math.round(magnifierHeight / zoom), magnifier.getSourceHeight());
assertEquals(sourceToMagnifierHorizontalOffset,
magnifier.getDefaultHorizontalSourceToMagnifierOffset());
assertEquals(sourceToMagnifierVerticalOffset,
magnifier.getDefaultVerticalSourceToMagnifierOffset());
assertEquals(cornerRadius, magnifier.getCornerRadius(), 0f);
assertEquals(elevation, magnifier.getElevation(), 0f);
assertEquals(enableClipping, magnifier.isClippingEnabled());
assertEquals(overlay, magnifier.getOverlay());
}
@Test(expected = NullPointerException.class)
public void testBuilder_throwsException_whenViewIsNull() {
new Magnifier.Builder(null);
}
@Test(expected = IllegalArgumentException.class)
public void testBuilder_throwsException_whenWidthIsInvalid() {
new Magnifier.Builder(mView).setSize(0, 10);
}
@Test(expected = IllegalArgumentException.class)
public void testBuilder_throwsException_whenHeightIsInvalid() {
new Magnifier.Builder(mView).setSize(10, 0);
}
@Test(expected = IllegalArgumentException.class)
public void testBuilder_throwsException_whenZoomIsZero() {
new Magnifier.Builder(mView).setInitialZoom(0f);
}
@Test(expected = IllegalArgumentException.class)
public void testBuilder_throwsException_whenZoomIsNegative() {
new Magnifier.Builder(mView).setInitialZoom(-1f);
}
@Test(expected = IllegalArgumentException.class)
public void testBuilder_throwsException_whenElevationIsInvalid() {
new Magnifier.Builder(mView).setElevation(-1f);
}
@Test(expected = IllegalArgumentException.class)
public void testBuilder_throwsException_whenCornerRadiusIsNegative() {
new Magnifier.Builder(mView).setCornerRadius(-1f);
}
//***** Tests for default parameters *****//
private int dpToPixelSize(float dp) {
return (int) (dp * mDisplayMetrics.density + 0.5f);
}
private float dpToPixel(float dp) {
return dp * mDisplayMetrics.density;
}
@Test
public void testMagnifierDefaultParameters_withDeprecatedConstructor() {
final Magnifier magnifier = new Magnifier(mView);
final int width = dpToPixelSize(100f);
assertEquals(width, magnifier.getWidth());
final int height = dpToPixelSize(48f);
assertEquals(height, magnifier.getHeight());
final float elevation = dpToPixel(4f);
assertEquals(elevation, magnifier.getElevation(), 0.01f);
final float zoom = 1.25f;
assertEquals(zoom, magnifier.getZoom(), 0.01f);
final int verticalOffset = -dpToPixelSize(42f);
assertEquals(verticalOffset, magnifier.getDefaultVerticalSourceToMagnifierOffset());
final int horizontalOffset = dpToPixelSize(0f);
assertEquals(horizontalOffset, magnifier.getDefaultHorizontalSourceToMagnifierOffset());
final Context deviceDefaultContext = new ContextThemeWrapper(mView.getContext(),
android.R.style.Theme_DeviceDefault);
final TypedArray ta = deviceDefaultContext.obtainStyledAttributes(
new int[]{android.R.attr.dialogCornerRadius});
final float dialogCornerRadius = ta.getDimension(0, 0);
ta.recycle();
assertEquals(dialogCornerRadius, magnifier.getCornerRadius(), 0.01f);
final boolean isClippingEnabled = true;
assertEquals(isClippingEnabled, magnifier.isClippingEnabled());
final int overlayColor = 0x0EFFFFFF;
assertEquals(overlayColor, ((ColorDrawable) magnifier.getOverlay()).getColor());
}
@Test
public void testMagnifierDefaultParameters_withBuilder() {
final Magnifier magnifier = new Magnifier.Builder(mView).build();
final int width = dpToPixelSize(100f);
assertEquals(width, magnifier.getWidth());
final int height = dpToPixelSize(48f);
assertEquals(height, magnifier.getHeight());
final float elevation = dpToPixel(4f);
assertEquals(elevation, magnifier.getElevation(), 0.01f);
final float zoom = 1.25f;
assertEquals(zoom, magnifier.getZoom(), 0.01f);
final int verticalOffset = -dpToPixelSize(42f);
assertEquals(verticalOffset, magnifier.getDefaultVerticalSourceToMagnifierOffset());
final int horizontalOffset = dpToPixelSize(0f);
assertEquals(horizontalOffset, magnifier.getDefaultHorizontalSourceToMagnifierOffset());
final float dialogCornerRadius = dpToPixel(2f);
assertEquals(dialogCornerRadius, magnifier.getCornerRadius(), 0.01f);
final boolean isClippingEnabled = true;
assertEquals(isClippingEnabled, magnifier.isClippingEnabled());
final int overlayColor = 0x00FFFFFF;
assertEquals(overlayColor, ((ColorDrawable) magnifier.getOverlay()).getColor());
}
@Test
@UiThreadTest
public void testSizeAndZoom_areValid() {
mMagnifier = new Magnifier(mView);
// Size should be positive.
assertTrue(mMagnifier.getWidth() > 0);
assertTrue(mMagnifier.getHeight() > 0);
// Source size should be positive.
assertTrue(mMagnifier.getSourceWidth() > 0);
assertTrue(mMagnifier.getSourceHeight() > 0);
// The magnified view region should be zoomed in, not out.
assertTrue(mMagnifier.getZoom() > 1.0f);
}
//***** Tests for #show() *****//
@Test
public void testShow() throws Throwable {
final float xCenter = mView.getWidth() / 2f;
final float yCenter = mView.getHeight() / 2f;
showMagnifier(xCenter, yCenter);
final int[] viewLocationInWindow = new int[2];
mView.getLocationInWindow(viewLocationInWindow);
// Check the coordinates of the content being copied.
final Point sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(xCenter + viewLocationInWindow[0],
sourcePosition.x + mMagnifier.getSourceWidth() / 2f, PIXEL_COMPARISON_DELTA);
assertEquals(yCenter + viewLocationInWindow[1],
sourcePosition.y + mMagnifier.getSourceHeight() / 2f, PIXEL_COMPARISON_DELTA);
// Check the coordinates of the magnifier.
final Point magnifierPosition = mMagnifier.getPosition();
assertNotNull(magnifierPosition);
assertEquals(sourcePosition.x + mMagnifier.getDefaultHorizontalSourceToMagnifierOffset()
- mMagnifier.getWidth() / 2f + mMagnifier.getSourceWidth() / 2f,
magnifierPosition.x, PIXEL_COMPARISON_DELTA);
assertEquals(sourcePosition.y + mMagnifier.getDefaultVerticalSourceToMagnifierOffset()
- mMagnifier.getHeight() / 2f + mMagnifier.getSourceHeight() / 2f,
magnifierPosition.y, PIXEL_COMPARISON_DELTA);
}
@Test
public void testShow_doesNotCrash_whenCalledWithExtremeCoordinates() throws Throwable {
showMagnifier(Integer.MIN_VALUE, Integer.MIN_VALUE);
showMagnifier(Integer.MIN_VALUE, Integer.MAX_VALUE);
showMagnifier(Integer.MAX_VALUE, Integer.MIN_VALUE);
showMagnifier(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
@Test
public void testShow_withDecoupledMagnifierPosition() throws Throwable {
final float xCenter = mView.getWidth() / 2;
final float yCenter = mView.getHeight() / 2;
final int xMagnifier = -20;
final int yMagnifier = -10;
showMagnifier(xCenter, yCenter, xMagnifier, yMagnifier);
final int[] viewLocationInWindow = new int[2];
mView.getLocationInWindow(viewLocationInWindow);
final Point magnifierPosition = mMagnifier.getPosition();
assertNotNull(magnifierPosition);
assertEquals(
viewLocationInWindow[0] + xMagnifier - mMagnifier.getWidth() / 2,
magnifierPosition.x, PIXEL_COMPARISON_DELTA);
assertEquals(
viewLocationInWindow[1] + yMagnifier - mMagnifier.getHeight() / 2,
magnifierPosition.y, PIXEL_COMPARISON_DELTA);
}
@Test
public void testShow_whenPixelCopyFails() throws Throwable {
WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> {
mActivity.setContentView(R.layout.magnifier_activity_centered_surfaceview_layout);
}, false /*forceLayout*/);
final View view = mActivity.findViewById(R.id.magnifier_centered_view);
runOnUiThreadAndWaitForCompletion(() -> mMagnifier = new Magnifier.Builder(view).build());
// The PixelCopy will fail as no draw has been done so far to the SurfaceView.
showMagnifier(0f, 0f);
assertNull(mMagnifier.getPosition());
assertNull(mMagnifier.getSourcePosition());
assertNull(mMagnifier.getContent());
}
//***** Tests for #dismiss() *****//
@Test
public void testDismiss_doesNotCrash() throws Throwable {
showMagnifier(0, 0);
final CountDownLatch latch = new CountDownLatch(1);
mActivityRule.runOnUiThread(() -> {
mMagnifier.dismiss();
mMagnifier.dismiss();
mMagnifier.show(0, 0);
mMagnifier.dismiss();
mMagnifier.dismiss();
latch.countDown();
});
assertTrue(TIME_LIMIT_EXCEEDED, latch.await(2, TimeUnit.SECONDS));
}
//***** Tests for #update() *****//
@Test
public void testUpdate_doesNotCrash() throws Throwable {
showMagnifier(0, 0);
final CountDownLatch latch = new CountDownLatch(1);
mActivityRule.runOnUiThread(() -> {
mMagnifier.update();
mMagnifier.update();
mMagnifier.show(10, 10);
mMagnifier.update();
mMagnifier.update();
mMagnifier.dismiss();
mMagnifier.update();
latch.countDown();
});
assertTrue(TIME_LIMIT_EXCEEDED, latch.await(2, TimeUnit.SECONDS));
}
@Test
public void testMagnifierContent_refreshesAfterUpdate() throws Throwable {
prepareFourQuadrantsScenario();
// Show the magnifier at the center of the activity.
showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2);
final Bitmap initialBitmap = mMagnifier.getContent()
.copy(mMagnifier.getContent().getConfig(), true);
assertFourQuadrants(initialBitmap);
// Make the one quadrant white.
final View quadrant1 =
mActivity.findViewById(R.id.magnifier_activity_four_quadrants_layout_quadrant_1);
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, quadrant1, () -> {
quadrant1.setBackground(null);
});
// Update the magnifier.
runAndWaitForMagnifierOperationComplete(mMagnifier::update);
final Bitmap newBitmap = mMagnifier.getContent();
assertFourQuadrants(newBitmap);
assertFalse(newBitmap.sameAs(initialBitmap));
}
//***** Tests for the position of the magnifier *****//
@Test
public void testWindowPosition_isClampedInsideMainApplicationWindow_topLeft() throws Throwable {
prepareFourQuadrantsScenario();
// Magnify the center of the activity in a magnifier outside bounds.
showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2,
-mMagnifier.getWidth(), -mMagnifier.getHeight());
// The window should have been positioned to the top left of the activity,
// such that it does not overlap system insets.
final Insets systemInsets = mLayout.getRootWindowInsets().getSystemWindowInsets();
final Point magnifierCoords = mMagnifier.getPosition();
assertNotNull(magnifierCoords);
assertEquals(systemInsets.left, magnifierCoords.x, PIXEL_COMPARISON_DELTA);
assertEquals(systemInsets.top, magnifierCoords.y, PIXEL_COMPARISON_DELTA);
}
@Test
public void testWindowPosition_isClampedInsideMainApplicationWindow_bottomRight()
throws Throwable {
prepareFourQuadrantsScenario();
// Magnify the center of the activity in a magnifier outside bounds.
showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2,
mLayout.getRootView().getWidth() + mMagnifier.getWidth(),
mLayout.getRootView().getHeight() + mMagnifier.getHeight());
// The window should have been positioned to the bottom right of the activity.
final Insets systemInsets = mLayout.getRootWindowInsets().getSystemWindowInsets();
final Point magnifierCoords = mMagnifier.getPosition();
assertNotNull(magnifierCoords);
assertEquals(mLayout.getRootView().getWidth()
- systemInsets.right - mMagnifier.getWidth(),
magnifierCoords.x, PIXEL_COMPARISON_DELTA);
assertEquals(mLayout.getRootView().getHeight()
- systemInsets.bottom - mMagnifier.getHeight(),
magnifierCoords.y, PIXEL_COMPARISON_DELTA);
}
@Test
public void testWindowPosition_isNotClamped_whenClampingFlagIsOff_topLeft() throws Throwable {
prepareFourQuadrantsScenario();
mMagnifier = new Magnifier.Builder(mLayout)
.setClippingEnabled(false)
.build();
// Magnify the center of the activity in a magnifier outside bounds.
showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2,
-mMagnifier.getWidth(), -mMagnifier.getHeight());
// The window should have not been clamped.
final Point magnifierCoords = mMagnifier.getPosition();
final int[] magnifiedViewPosition = new int[2];
mLayout.getLocationInWindow(magnifiedViewPosition);
assertNotNull(magnifierCoords);
assertEquals(magnifiedViewPosition[0] - 3 * mMagnifier.getWidth() / 2, magnifierCoords.x,
PIXEL_COMPARISON_DELTA);
assertEquals(magnifiedViewPosition[1] - 3 * mMagnifier.getHeight() / 2, magnifierCoords.y,
PIXEL_COMPARISON_DELTA);
}
@Test
public void testWindowPosition_isNotClamped_whenClampingFlagIsOff_bottomRight()
throws Throwable {
prepareFourQuadrantsScenario();
mMagnifier = new Magnifier.Builder(mLayout)
.setClippingEnabled(false)
.setSize(40, 40)
.build();
// Magnify the center of the activity in a magnifier outside bounds.
showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2,
mLayout.getRootView().getWidth() + mMagnifier.getWidth(),
mLayout.getRootView().getHeight() + mMagnifier.getHeight());
// The window should have not been clamped.
final Point magnifierCoords = mMagnifier.getPosition();
final int[] magnifiedViewPosition = new int[2];
mLayout.getLocationInWindow(magnifiedViewPosition);
assertNotNull(magnifierCoords);
assertEquals(magnifiedViewPosition[0] + mLayout.getRootView().getWidth()
+ mMagnifier.getWidth() / 2, magnifierCoords.x, PIXEL_COMPARISON_DELTA);
assertEquals(magnifiedViewPosition[1] + mLayout.getRootView().getHeight()
+ mMagnifier.getHeight() / 2, magnifierCoords.y, PIXEL_COMPARISON_DELTA);
}
@Test
public void testWindowPosition_isCorrect_whenADefaultContentToMagnifierOffsetIsUsed()
throws Throwable {
prepareFourQuadrantsScenario();
final int horizontalOffset = 5;
final int verticalOffset = -10;
mMagnifier = new Magnifier.Builder(mLayout)
.setSize(20, 10) /* make magnifier small to avoid having it clamped */
.setDefaultSourceToMagnifierOffset(horizontalOffset, verticalOffset)
.build();
// Magnify the center of the activity in a magnifier outside bounds.
showMagnifier(mLayout.getWidth() / 2, mLayout.getHeight() / 2);
final Point magnifierCoords = mMagnifier.getPosition();
final Point sourceCoords = mMagnifier.getSourcePosition();
assertNotNull(magnifierCoords);
assertEquals(sourceCoords.x + mMagnifier.getSourceWidth() / 2f + horizontalOffset,
magnifierCoords.x + mMagnifier.getWidth() / 2f, PIXEL_COMPARISON_DELTA);
assertEquals(sourceCoords.y + mMagnifier.getSourceHeight() / 2f + verticalOffset,
magnifierCoords.y + mMagnifier.getHeight() / 2f, PIXEL_COMPARISON_DELTA);
}
@Test
@UiThreadTest
public void testWindowPosition_isNull_whenMagnifierIsNotShowing() {
mMagnifier = new Magnifier.Builder(mLayout)
.setSize(20, 10) /* make magnifier small to avoid having it clamped */
.build();
// No #show has been requested, so the position should be null.
assertNull(mMagnifier.getPosition());
// #show should make the position not null.
mMagnifier.show(0, 0);
assertNotNull(mMagnifier.getPosition());
// #dismiss should make the position null.
mMagnifier.dismiss();
assertNull(mMagnifier.getPosition());
}
//***** Tests for the position of the content copied to the magnifier *****//
@Test
@UiThreadTest
public void testSourcePosition_isNull_whenMagnifierIsNotShowing() {
mMagnifier = new Magnifier.Builder(mLayout)
.setSize(20, 10) /* make magnifier small to avoid having it clamped */
.build();
// No #show has been requested, so the source position should be null.
assertNull(mMagnifier.getSourcePosition());
// #show should make the source position not null.
mMagnifier.show(0, 0);
assertNotNull(mMagnifier.getSourcePosition());
// #dismiss should make the source position null.
mMagnifier.dismiss();
assertNull(mMagnifier.getSourcePosition());
}
@Test
public void testSourcePosition_respectsMaxVisibleBounds_inHorizontalScrollableContainer()
throws Throwable {
WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> {
mActivity.setContentView(R.layout.magnifier_activity_scrollable_views_layout);
}, false /*forceLayout*/);
final View view = mActivity
.findViewById(R.id.magnifier_activity_horizontally_scrolled_view);
final HorizontalScrollView container = (HorizontalScrollView) mActivity
.findViewById(R.id.horizontal_scroll_container);
final Magnifier.Builder builder = new Magnifier.Builder(view)
.setSize(100, 100)
.setInitialZoom(20f) /* 5x5 source size */
.setSourceBounds(
Magnifier.SOURCE_BOUND_MAX_VISIBLE,
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
Magnifier.SOURCE_BOUND_MAX_VISIBLE,
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE
);
runOnUiThreadAndWaitForCompletion(() -> {
mMagnifier = builder.build();
// Scroll halfway horizontally.
container.scrollTo(view.getWidth() / 2, 0);
});
final int[] containerPosition = new int[2];
container.getLocationInWindow(containerPosition);
// Try to copy from an x to the left of the currently visible region.
showMagnifier(view.getWidth() / 4, 0);
Point sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(containerPosition[0], sourcePosition.x, PIXEL_COMPARISON_DELTA);
// Try to copy from an x to the right of the currently visible region.
showMagnifier(3 * view.getWidth() / 4, 0);
sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(containerPosition[0] + container.getWidth() - mMagnifier.getSourceWidth(),
sourcePosition.x, PIXEL_COMPARISON_DELTA);
}
@Test
public void testSourcePosition_respectsMaxVisibleBounds_inVerticalScrollableContainer()
throws Throwable {
WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> {
mActivity.setContentView(R.layout.magnifier_activity_scrollable_views_layout);
}, false /*forceLayout*/);
final View view = mActivity.findViewById(R.id.magnifier_activity_vertically_scrolled_view);
final ScrollView container = (ScrollView) mActivity
.findViewById(R.id.vertical_scroll_container);
final Magnifier.Builder builder = new Magnifier.Builder(view)
.setSize(100, 100)
.setInitialZoom(10f) /* 10x10 source size */
.setSourceBounds(
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
Magnifier.SOURCE_BOUND_MAX_VISIBLE,
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
Magnifier.SOURCE_BOUND_MAX_VISIBLE
);
runOnUiThreadAndWaitForCompletion(() -> {
mMagnifier = builder.build();
// Scroll halfway vertically.
container.scrollTo(0, view.getHeight() / 2);
});
final int[] containerPosition = new int[2];
container.getLocationInWindow(containerPosition);
// Try to copy from an y above the currently visible region.
showMagnifier(0, view.getHeight() / 4);
Point sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(containerPosition[1], sourcePosition.y, PIXEL_COMPARISON_DELTA);
// Try to copy from an x below the currently visible region.
showMagnifier(0, 3 * view.getHeight() / 4);
sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(containerPosition[1] + container.getHeight() - mMagnifier.getSourceHeight(),
sourcePosition.y, PIXEL_COMPARISON_DELTA);
}
@Test
public void testSourcePosition_respectsMaxInSurfaceBounds() throws Throwable {
WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> {
mActivity.setContentView(R.layout.magnifier_activity_centered_view_layout);
}, false /*forceLayout*/);
final View view = mActivity.findViewById(R.id.magnifier_centered_view);
final Magnifier.Builder builder = new Magnifier.Builder(view)
.setSize(100, 100)
.setInitialZoom(5f) /* 20x20 source size */
.setSourceBounds(
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE
);
runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build());
final int[] viewPosition = new int[2];
view.getLocationInWindow(viewPosition);
// Copy content centered on relative position (0, 0) and expect the top left
// corner of the source NOT to have been pulled to coincide with (0, 0) of the view.
showMagnifier(0, 0);
Point sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(viewPosition[0] - mMagnifier.getSourceWidth() / 2, sourcePosition.x,
PIXEL_COMPARISON_DELTA);
assertEquals(viewPosition[1] - mMagnifier.getSourceHeight() / 2, sourcePosition.y,
PIXEL_COMPARISON_DELTA);
// Copy content centered on the bottom right corner of the view and expect the top left
// corner of the source NOT to have been pulled inside the view.
showMagnifier(view.getWidth(), view.getHeight());
sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(viewPosition[0] + view.getWidth() - mMagnifier.getSourceWidth() / 2,
sourcePosition.x, PIXEL_COMPARISON_DELTA);
assertEquals(viewPosition[1] + view.getHeight() - mMagnifier.getSourceHeight() / 2,
sourcePosition.y, PIXEL_COMPARISON_DELTA);
final int[] viewPositionInSurface = new int[2];
view.getLocationInSurface(viewPositionInSurface);
// Copy content centered on the top left corner of the main app surface and expect the top
// left corner of the source to have been pulled to the top left corner of the surface.
showMagnifier(-viewPositionInSurface[0], -viewPositionInSurface[1]);
sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(0, sourcePosition.x - viewPosition[0] + viewPositionInSurface[0],
PIXEL_COMPARISON_DELTA);
assertEquals(0, sourcePosition.y - viewPosition[1] + viewPositionInSurface[1],
PIXEL_COMPARISON_DELTA);
// Copy content below and to the right of the bottom right corner of the main app surface
// and expect the source to have been pulled inside the surface at its bottom right.
showMagnifier(2 * view.getRootView().getWidth(), 2 * view.getRootView().getHeight());
sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertTrue(
sourcePosition.x < 2 * view.getRootView().getWidth() - mMagnifier.getSourceWidth());
assertTrue(sourcePosition.x > view.getRootView().getWidth() - mMagnifier.getSourceWidth());
assertTrue(sourcePosition.y
< 2 * view.getRootView().getHeight() - mMagnifier.getSourceHeight());
assertTrue(sourcePosition.y
> view.getRootView().getHeight() - mMagnifier.getSourceHeight());
}
@Test
public void testSourcePosition_respectsMaxInSurfaceBounds_forSurfaceView() throws Throwable {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> {
mActivity.setContentView(R.layout.magnifier_activity_centered_surfaceview_layout);
}, false /* forceLayout */);
WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> {
// Draw something in the SurfaceView for the Magnifier to copy.
final View view = mActivity.findViewById(R.id.magnifier_centered_view);
final SurfaceHolder surfaceHolder = ((SurfaceView) view).getHolder();
final Canvas canvas = surfaceHolder.lockHardwareCanvas();
canvas.drawColor(Color.BLUE);
surfaceHolder.unlockCanvasAndPost(canvas);
}, false /* forceLayout */);
final View view = mActivity.findViewById(R.id.magnifier_centered_view);
final Magnifier.Builder builder = new Magnifier.Builder(view)
.setSize(100, 100)
.setInitialZoom(5f) /* 20x20 source size */
.setSourceBounds(
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE,
Magnifier.SOURCE_BOUND_MAX_IN_SURFACE
);
runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build());
// Copy content centered on relative position (0, 0) and expect the top left
// corner of the source to have been pulled to coincide with (0, 0) of the view
// (since the view coincides with the surface content is copied from).
showMagnifier(0, 0);
Point sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(0, sourcePosition.x, PIXEL_COMPARISON_DELTA);
assertEquals(0, sourcePosition.y, PIXEL_COMPARISON_DELTA);
// Copy content centered on the bottom right corner of the view and expect the top left
// corner of the source to have been pulled inside the surface view.
showMagnifier(view.getWidth(), view.getHeight());
sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(view.getWidth() - mMagnifier.getSourceWidth(), sourcePosition.x);
assertEquals(view.getHeight() - mMagnifier.getSourceHeight(), sourcePosition.y);
// Copy content from the center of the surface view and expect no clamping to be done.
showMagnifier(view.getWidth() / 2, view.getHeight() / 2);
sourcePosition = mMagnifier.getSourcePosition();
assertNotNull(sourcePosition);
assertEquals(view.getWidth() / 2 - mMagnifier.getSourceWidth() / 2, sourcePosition.x,
PIXEL_COMPARISON_DELTA);
assertEquals(view.getHeight() / 2 - mMagnifier.getSourceHeight() / 2, sourcePosition.y,
PIXEL_COMPARISON_DELTA);
}
@Test
public void testSourceBounds_areAdjustedWhenInvalid() throws Throwable {
WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> {
mActivity.setContentView(R.layout.magnifier_activity_centered_view_layout);
}, false /*forceLayout*/);
final View view = mActivity.findViewById(R.id.magnifier_centered_view);
final Insets systemInsets = view.getRootWindowInsets().getSystemWindowInsets();
final Magnifier.Builder builder = new Magnifier.Builder(view)
.setSize(2 * view.getWidth() + systemInsets.right,
2 * view.getHeight() + systemInsets.bottom)
.setInitialZoom(1f) /* source double the size of the view + right/bottom insets */
.setSourceBounds(/* invalid bounds */
Magnifier.SOURCE_BOUND_MAX_VISIBLE,
Magnifier.SOURCE_BOUND_MAX_VISIBLE,
Magnifier.SOURCE_BOUND_MAX_VISIBLE,
Magnifier.SOURCE_BOUND_MAX_VISIBLE
);
runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build());
final int[] viewPosition = new int[2];
view.getLocationInWindow(viewPosition);
// Make sure that the left and top bounds are respected, since this is possible
// for this source size, when the view is centered.
showMagnifier(0, 0);
Point sourcePosition = mMagnifier.getSourcePosition();
assertEquals(viewPosition[0], sourcePosition.x, PIXEL_COMPARISON_DELTA);
assertEquals(viewPosition[1], sourcePosition.y, PIXEL_COMPARISON_DELTA);
// Move the magnified view to the top left of the screen, and make sure that
// the top and left bounds are still respected.
mActivityRule.runOnUiThread(() -> {
final LinearLayout layout =
mActivity.findViewById(R.id.magnifier_activity_centered_view_layout);
layout.setGravity(Gravity.TOP | Gravity.LEFT);
});
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, view, null);
view.getLocationInWindow(viewPosition);
showMagnifier(0, 0);
sourcePosition = mMagnifier.getSourcePosition();
assertEquals(viewPosition[0], sourcePosition.x, PIXEL_COMPARISON_DELTA);
assertEquals(viewPosition[1], sourcePosition.y, PIXEL_COMPARISON_DELTA);
// Move the magnified view to the bottom right of the layout, and expect the top and left
// bounds to have been shifted such that the source sits inside the surface.
mActivityRule.runOnUiThread(() -> {
final LinearLayout layout =
mActivity.findViewById(R.id.magnifier_activity_centered_view_layout);
layout.setGravity(Gravity.BOTTOM | Gravity.RIGHT);
});
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, view, null);
view.getLocationInSurface(viewPosition);
showMagnifier(0, 0);
sourcePosition = mMagnifier.getSourcePosition();
assertEquals(viewPosition[0] - view.getWidth(), sourcePosition.x, PIXEL_COMPARISON_DELTA);
assertEquals(viewPosition[1] - view.getHeight(), sourcePosition.y, PIXEL_COMPARISON_DELTA);
}
//***** Tests for zoom change *****//
@Test
public void testZoomChange() throws Throwable {
// Setup.
final View view = new View(mActivity);
final int width = 300;
final int height = 270;
final Magnifier.Builder builder = new Magnifier.Builder(view)
.setSize(width, height)
.setInitialZoom(1.0f);
mMagnifier = builder.build();
final float newZoom = 1.5f;
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, view, () -> {
mLayout.addView(view, new LayoutParams(200, 200));
mMagnifier.setZoom(newZoom);
});
assertEquals((int) (width / newZoom), mMagnifier.getSourceWidth());
assertEquals((int) (height / newZoom), mMagnifier.getSourceHeight());
// Show.
showMagnifier(200, 200);
// Check bitmap size.
assertNotNull(mMagnifier.getOriginalContent());
assertEquals((int) (width / newZoom), mMagnifier.getOriginalContent().getWidth());
assertEquals((int) (height / newZoom), mMagnifier.getOriginalContent().getHeight());
}
@Test(expected = IllegalArgumentException.class)
public void testZoomChange_throwsException_whenZoomIsZero() {
final View view = new View(mActivity);
new Magnifier(view).setZoom(0f);
}
@Test(expected = IllegalArgumentException.class)
public void testZoomChange_throwsException_whenZoomIsNegative() {
final View view = new View(mActivity);
new Magnifier(view).setZoom(-1f);
}
//***** Tests for overlay *****//
@Test
public void testOverlay_isDrawn() throws Throwable {
final Magnifier.Builder builder = new Magnifier.Builder(mView)
.setSize(50, 50)
.setOverlay(new ColorDrawable(Color.BLUE));
runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build());
showMagnifier(0, 0);
// Assert that the content has the correct size and is all blue.
final Bitmap content = mMagnifier.getContent();
assertNotNull(content);
assertEquals(mMagnifier.getWidth(), content.getWidth());
assertEquals(mMagnifier.getHeight(), content.getHeight());
for (int i = 0; i < content.getWidth(); ++i) {
for (int j = 0; j < content.getHeight(); ++j) {
assertEquals(Color.BLUE, content.getPixel(i, j));
}
}
}
@Test
public void testOverlay_redrawsOnInvalidation() throws Throwable {
final ColorDrawable overlay = new ColorDrawable(Color.BLUE);
final Magnifier.Builder builder = new Magnifier.Builder(mView)
.setSize(50, 50)
.setOverlay(overlay);
runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build());
showMagnifier(0, 0);
overlay.setColor(Color.WHITE);
// Assert that the content has the correct size and is all blue.
final Bitmap content = mMagnifier.getContent();
assertNotNull(content);
assertEquals(mMagnifier.getWidth(), content.getWidth());
assertEquals(mMagnifier.getHeight(), content.getHeight());
for (int i = 0; i < content.getWidth(); ++i) {
for (int j = 0; j < content.getHeight(); ++j) {
assertEquals(Color.WHITE, content.getPixel(i, j));
}
}
}
@Test
public void testOverlay_isNotVisible_whenSetToNull() throws Throwable {
final Magnifier.Builder builder = new Magnifier.Builder(mView)
.setSize(50, 50)
.setInitialZoom(10f) /* 5x5 source size */
.setOverlay(null);
runOnUiThreadAndWaitForCompletion(() -> mMagnifier = builder.build());
showMagnifier(mView.getWidth() / 2, mView.getHeight() / 2);
// Assert that the content has the correct size and is all the view color.
final Bitmap content = mMagnifier.getContent();
assertNotNull(content);
assertEquals(mMagnifier.getWidth(), content.getWidth());
assertEquals(mMagnifier.getHeight(), content.getHeight());
final int viewColor = mView.getContext().getResources().getColor(
android.R.color.holo_blue_bright, null);
for (int i = 0; i < content.getWidth(); ++i) {
for (int j = 0; j < content.getHeight(); ++j) {
assertEquals(viewColor, content.getPixel(i, j));
}
}
}
//***** Helper methods / classes *****//
private void showMagnifier(float sourceX, float sourceY) throws Throwable {
runAndWaitForMagnifierOperationComplete(() -> mMagnifier.show(sourceX, sourceY));
}
private void showMagnifier(float sourceX, float sourceY, float magnifierX, float magnifierY)
throws Throwable {
runAndWaitForMagnifierOperationComplete(() -> mMagnifier.show(sourceX, sourceY,
magnifierX, magnifierY));
}
private void runAndWaitForMagnifierOperationComplete(final Runnable lambda) throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
mMagnifier.setOnOperationCompleteCallback(latch::countDown);
mActivityRule.runOnUiThread(lambda);
assertTrue(TIME_LIMIT_EXCEEDED, latch.await(2, TimeUnit.SECONDS));
}
private void runOnUiThreadAndWaitForCompletion(final Runnable lambda) throws Throwable {
final CountDownLatch latch = new CountDownLatch(1);
mActivityRule.runOnUiThread(() -> {
lambda.run();
latch.countDown();
});
assertTrue(TIME_LIMIT_EXCEEDED, latch.await(2, TimeUnit.SECONDS));
}
/**
* Sets the activity to contain four equal quadrants coloured differently and
* instantiates a magnifier. This method should not be called on the UI thread.
*/
private void prepareFourQuadrantsScenario() throws Throwable {
WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, () -> {
mActivity.setContentView(R.layout.magnifier_activity_four_quadrants_layout);
mLayout = mActivity.findViewById(R.id.magnifier_activity_four_quadrants_layout);
mMagnifier = new Magnifier(mLayout);
}, false /*forceLayout*/);
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mLayout, null);
}
/**
* Asserts that the current bitmap contains four different dominant colors, which
* are (almost) equally distributed. The test takes into account an amount of
* noise, possible consequence of upscaling and filtering the magnified content.
*
* @param bitmap the bitmap to be checked
*/
private void assertFourQuadrants(final Bitmap bitmap) {
final int expectedQuadrants = 4;
final int totalPixels = bitmap.getWidth() * bitmap.getHeight();
final Map<Integer, Integer> colorCount = new HashMap<>();
for (int x = 0; x < bitmap.getWidth(); ++x) {
for (int y = 0; y < bitmap.getHeight(); ++y) {
final int currentColor = bitmap.getPixel(x, y);
colorCount.put(currentColor, colorCount.getOrDefault(currentColor, 0) + 1);
}
}
assertTrue(colorCount.size() >= expectedQuadrants);
final List<Integer> counts = new ArrayList<>(colorCount.values());
Collections.sort(counts);
int quadrantsTotal = 0;
for (int i = counts.size() - expectedQuadrants; i < counts.size(); ++i) {
quadrantsTotal += counts.get(i);
}
assertTrue(1.0f * (totalPixels - quadrantsTotal) / totalPixels <= 0.1f);
for (int i = counts.size() - expectedQuadrants; i < counts.size(); ++i) {
final float proportion = 1.0f
* Math.abs(expectedQuadrants * counts.get(i) - quadrantsTotal) / quadrantsTotal;
assertTrue(proportion <= 0.1f);
}
}
}