Merge changes from topic "magnification_switch" into sc-dev

* changes:
  Makes the movable region of magnification button sticking to the edges
  Makes magnification switch UI never overlap navigationBar and statusBar
This commit is contained in:
Minche Li
2021-05-20 04:39:03 +00:00
committed by Android (Google) Code Review
5 changed files with 354 additions and 142 deletions

View File

@@ -1293,7 +1293,7 @@
<dimen name="magnification_drag_view_size">36dp</dimen> <dimen name="magnification_drag_view_size">36dp</dimen>
<dimen name="magnification_controls_size">90dp</dimen> <dimen name="magnification_controls_size">90dp</dimen>
<dimen name="magnification_switch_button_size">48dp</dimen> <dimen name="magnification_switch_button_size">48dp</dimen>
<dimen name="magnification_switch_button_padding">6dp</dimen> <dimen name="magnification_switch_button_margin">16dp</dimen>
<dimen name="magnifier_left_right_controls_width">35dp</dimen> <dimen name="magnifier_left_right_controls_width">35dp</dimen>
<dimen name="magnifier_left_right_controls_height">45dp</dimen> <dimen name="magnifier_left_right_controls_height">45dp</dimen>
<dimen name="magnifier_up_down_controls_width">45dp</dimen> <dimen name="magnifier_up_down_controls_width">45dp</dimen>

View File

@@ -22,14 +22,17 @@ import android.annotation.NonNull;
import android.annotation.UiContext; import android.annotation.UiContext;
import android.content.Context; import android.content.Context;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.graphics.Insets;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Bundle; import android.os.Bundle;
import android.os.UserHandle; import android.os.UserHandle;
import android.provider.Settings; import android.provider.Settings;
import android.util.MathUtils;
import android.view.Gravity; import android.view.Gravity;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.WindowManager.LayoutParams; import android.view.WindowManager.LayoutParams;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
@@ -38,6 +41,7 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.ImageView; import android.widget.ImageView;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.systemui.R; import com.android.systemui.R;
import java.util.Collections; import java.util.Collections;
@@ -45,8 +49,8 @@ import java.util.Collections;
/** /**
* Shows/hides a {@link android.widget.ImageView} on the screen and changes the values of * Shows/hides a {@link android.widget.ImageView} on the screen and changes the values of
* {@link Settings.Secure#ACCESSIBILITY_MAGNIFICATION_MODE} when the UI is toggled. * {@link Settings.Secure#ACCESSIBILITY_MAGNIFICATION_MODE} when the UI is toggled.
* The button icon is movable by dragging. And the button UI would automatically be dismissed after * The button icon is movable by dragging and it would not overlap navigation bar window.
* displaying for a period of time. * And the button UI would automatically be dismissed after displaying for a period of time.
*/ */
class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureListener { class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureListener {
@@ -64,25 +68,30 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
private final AccessibilityManager mAccessibilityManager; private final AccessibilityManager mAccessibilityManager;
private final WindowManager mWindowManager; private final WindowManager mWindowManager;
private final ImageView mImageView; private final ImageView mImageView;
private final Runnable mWindowInsetChangeRunnable;
private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
private int mMagnificationMode = Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; private int mMagnificationMode = Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN;
private final LayoutParams mParams; private final LayoutParams mParams;
@VisibleForTesting
final Rect mDraggableWindowBounds = new Rect();
private boolean mIsVisible = false; private boolean mIsVisible = false;
private final MagnificationGestureDetector mGestureDetector; private final MagnificationGestureDetector mGestureDetector;
private boolean mSingleTapDetected = false; private boolean mSingleTapDetected = false;
private boolean mToLeftScreenEdge = false;
MagnificationModeSwitch(@UiContext Context context) { MagnificationModeSwitch(@UiContext Context context) {
this(context, createView(context)); this(context, createView(context), new SfVsyncFrameCallbackProvider());
} }
@VisibleForTesting @VisibleForTesting
MagnificationModeSwitch(Context context, @NonNull ImageView imageView) { MagnificationModeSwitch(Context context, @NonNull ImageView imageView,
SfVsyncFrameCallbackProvider sfVsyncFrameProvider) {
mContext = context; mContext = context;
mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class);
mWindowManager = (WindowManager) mContext.getSystemService( mWindowManager = mContext.getSystemService(WindowManager.class);
Context.WINDOW_SERVICE); mSfVsyncFrameProvider = sfVsyncFrameProvider;
mParams = createLayoutParams(context); mParams = createLayoutParams(context);
mImageView = imageView; mImageView = imageView;
applyResourcesValues();
mImageView.setOnTouchListener(this::onTouch); mImageView.setOnTouchListener(this::onTouch);
mImageView.setAccessibilityDelegate(new View.AccessibilityDelegate() { mImageView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
@Override @Override
@@ -107,6 +116,15 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
return super.performAccessibilityAction(host, action, args); return super.performAccessibilityAction(host, action, args);
} }
}); });
mWindowInsetChangeRunnable = this::onWindowInsetChanged;
mImageView.setOnApplyWindowInsetsListener((v, insets) -> {
// Adds a pending post check to avoiding redundant calculation because this callback
// is sent frequently when the switch icon window dragged by the users.
if (!mImageView.getHandler().hasCallbacks(mWindowInsetChangeRunnable)) {
mImageView.getHandler().post(mWindowInsetChangeRunnable);
}
return v.onApplyWindowInsets(insets);
});
mFadeInAnimationTask = () -> { mFadeInAnimationTask = () -> {
mImageView.animate() mImageView.animate()
@@ -133,11 +151,17 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
return mContext.getResources().getString(stringId); return mContext.getResources().getString(stringId);
} }
private void applyResourcesValues() { private void applyResourcesValuesWithDensityChanged() {
final int padding = mContext.getResources().getDimensionPixelSize( final int size = mContext.getResources().getDimensionPixelSize(
R.dimen.magnification_switch_button_padding); R.dimen.magnification_switch_button_size);
mImageView.setPadding(padding, padding, padding, padding); mParams.height = size;
mImageView.setImageResource(getIconResId(mMagnificationMode)); mParams.width = size;
if (mIsVisible) {
stickToScreenEdge(mToLeftScreenEdge);
// Reset button to make its window layer always above the mirror window.
removeButton();
showButton(mMagnificationMode, /* resetPosition= */false);
}
} }
private boolean onTouch(View v, MotionEvent event) { private boolean onTouch(View v, MotionEvent event) {
@@ -168,6 +192,12 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
@Override @Override
public boolean onFinish(float xOffset, float yOffset) { public boolean onFinish(float xOffset, float yOffset) {
if (mIsVisible) {
final int windowWidth = mWindowManager.getCurrentWindowMetrics().getBounds().width();
final int halfWindowWidth = windowWidth / 2;
mToLeftScreenEdge = (mParams.x < halfWindowWidth);
stickToScreenEdge(mToLeftScreenEdge);
}
if (!mSingleTapDetected) { if (!mSingleTapDetected) {
showButton(mMagnificationMode); showButton(mMagnificationMode);
} }
@@ -175,10 +205,18 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
return true; return true;
} }
private void stickToScreenEdge(boolean toLeftScreenEdge) {
mParams.x = toLeftScreenEdge
? mDraggableWindowBounds.left : mDraggableWindowBounds.right;
updateButtonViewLayoutIfNeeded();
}
private void moveButton(float offsetX, float offsetY) { private void moveButton(float offsetX, float offsetY) {
mParams.x -= offsetX; mSfVsyncFrameProvider.postFrameCallback(l -> {
mParams.y -= offsetY; mParams.x += offsetX;
mWindowManager.updateViewLayout(mImageView, mParams); mParams.y += offsetY;
updateButtonViewLayoutIfNeeded();
});
} }
void removeButton() { void removeButton() {
@@ -193,16 +231,32 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
mImageView.setAlpha(0f); mImageView.setAlpha(0f);
mWindowManager.removeView(mImageView); mWindowManager.removeView(mImageView);
mIsVisible = false; mIsVisible = false;
mParams.x = 0;
mParams.y = 0;
} }
void showButton(int mode) { void showButton(int mode) {
showButton(mode, true);
}
/**
* Shows magnification switch button for the specified magnification mode.
* When the button is going to be visible by calling this method, the layout position can be
* reset depending on the flag.
*
* @param mode The magnification mode
* @param resetPosition if the button position needs be reset
*/
private void showButton(int mode, boolean resetPosition) {
if (mMagnificationMode != mode) { if (mMagnificationMode != mode) {
mMagnificationMode = mode; mMagnificationMode = mode;
mImageView.setImageResource(getIconResId(mode)); mImageView.setImageResource(getIconResId(mode));
} }
if (!mIsVisible) { if (!mIsVisible) {
if (resetPosition) {
mDraggableWindowBounds.set(getDraggableWindowBounds());
mParams.x = mDraggableWindowBounds.right;
mParams.y = mDraggableWindowBounds.bottom;
mToLeftScreenEdge = false;
}
mWindowManager.addView(mImageView, mParams); mWindowManager.addView(mImageView, mParams);
// Exclude magnification switch button from system gesture area. // Exclude magnification switch button from system gesture area.
setSystemGestureExclusion(); setSystemGestureExclusion();
@@ -229,12 +283,7 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
void onConfigurationChanged(int configDiff) { void onConfigurationChanged(int configDiff) {
if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) { if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) {
applyResourcesValues(); applyResourcesValuesWithDensityChanged();
if (mIsVisible) {
mWindowManager.updateViewLayout(mImageView, mParams);
// Exclude magnification switch button from system gesture area.
setSystemGestureExclusion();
}
return; return;
} }
if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) { if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) {
@@ -243,6 +292,25 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
} }
} }
private void onWindowInsetChanged() {
final Rect newBounds = getDraggableWindowBounds();
if (mDraggableWindowBounds.equals(newBounds)) {
return;
}
mDraggableWindowBounds.set(newBounds);
stickToScreenEdge(mToLeftScreenEdge);
}
private void updateButtonViewLayoutIfNeeded() {
if (mIsVisible) {
mParams.x = MathUtils.constrain(mParams.x, mDraggableWindowBounds.left,
mDraggableWindowBounds.right);
mParams.y = MathUtils.constrain(mParams.y, mDraggableWindowBounds.top,
mDraggableWindowBounds.bottom);
mWindowManager.updateViewLayout(mImageView, mParams);
}
}
private void updateAccessibilityWindowTitle() { private void updateAccessibilityWindowTitle() {
mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext); mParams.accessibilityTitle = getAccessibilityWindowTitle(mContext);
if (mIsVisible) { if (mIsVisible) {
@@ -283,17 +351,34 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
} }
private static LayoutParams createLayoutParams(Context context) { private static LayoutParams createLayoutParams(Context context) {
final int size = context.getResources().getDimensionPixelSize(
R.dimen.magnification_switch_button_size);
final LayoutParams params = new LayoutParams( final LayoutParams params = new LayoutParams(
LayoutParams.WRAP_CONTENT, size,
LayoutParams.WRAP_CONTENT, size,
LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
LayoutParams.FLAG_NOT_FOCUSABLE, LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSPARENT); PixelFormat.TRANSPARENT);
params.gravity = Gravity.BOTTOM | Gravity.RIGHT; params.gravity = Gravity.TOP | Gravity.LEFT;
params.accessibilityTitle = getAccessibilityWindowTitle(context); params.accessibilityTitle = getAccessibilityWindowTitle(context);
return params; return params;
} }
private Rect getDraggableWindowBounds() {
final int layoutMargin = mContext.getResources().getDimensionPixelSize(
R.dimen.magnification_switch_button_margin);
final Rect boundRect = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds());
final Insets systemBars =
mWindowManager.getCurrentWindowMetrics().getWindowInsets()
.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
final Rect insets = new Rect(layoutMargin,
systemBars.top + layoutMargin,
mParams.width + layoutMargin,
mParams.height + layoutMargin + systemBars.bottom);
boundRect.inset(insets);
return boundRect;
}
private static String getAccessibilityWindowTitle(Context context) { private static String getAccessibilityWindowTitle(Context context) {
return context.getString(com.android.internal.R.string.android_system_label); return context.getString(com.android.internal.R.string.android_system_label);
} }
@@ -305,5 +390,4 @@ class MagnificationModeSwitch implements MagnificationGestureDetector.OnGestureL
new Rect(0, 0, mImageView.getWidth(), mImageView.getHeight()))); new Rect(0, 0, mImageView.getWidth(), mImageView.getHeight())));
}); });
} }
} }

View File

@@ -22,6 +22,7 @@ import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.ACTION_UP;
import static android.view.WindowInsets.Type.systemBars;
import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK;
import static com.android.systemui.accessibility.MagnificationModeSwitch.DEFAULT_FADE_OUT_ANIMATION_DELAY_MS; import static com.android.systemui.accessibility.MagnificationModeSwitch.DEFAULT_FADE_OUT_ANIMATION_DELAY_MS;
@@ -33,6 +34,7 @@ import static junit.framework.Assert.assertNotNull;
import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
@@ -41,19 +43,27 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.content.Context; import android.content.Context;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
import android.os.UserHandle; import android.os.UserHandle;
import android.provider.Settings; import android.provider.Settings;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.Choreographer;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewConfiguration; import android.view.ViewConfiguration;
import android.view.ViewPropertyAnimator; import android.view.ViewPropertyAnimator;
import android.view.WindowInsets;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo;
@@ -61,6 +71,7 @@ import android.widget.ImageView;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
@@ -69,6 +80,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
@@ -77,6 +89,7 @@ import java.util.List;
@SmallTest @SmallTest
@RunWith(AndroidTestingRunner.class) @RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class MagnificationModeSwitchTest extends SysuiTestCase { public class MagnificationModeSwitchTest extends SysuiTestCase {
private static final float FADE_IN_ALPHA = 1f; private static final float FADE_IN_ALPHA = 1f;
@@ -86,7 +99,10 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
@Mock @Mock
private AccessibilityManager mAccessibilityManager; private AccessibilityManager mAccessibilityManager;
@Mock @Mock
private WindowManager mWindowManager; private SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
@Mock
private Handler mHandler;
private TestableWindowManager mWindowManager;
private ViewPropertyAnimator mViewPropertyAnimator; private ViewPropertyAnimator mViewPropertyAnimator;
private MagnificationModeSwitch mMagnificationModeSwitch; private MagnificationModeSwitch mMagnificationModeSwitch;
private View.OnTouchListener mTouchListener; private View.OnTouchListener mTouchListener;
@@ -96,10 +112,8 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
WindowManager wm = mContext.getSystemService(WindowManager.class); final WindowManager wm = mContext.getSystemService(WindowManager.class);
doAnswer(invocation -> mWindowManager = spy(new TestableWindowManager(wm));
wm.getMaximumWindowMetrics()
).when(mWindowManager).getMaximumWindowMetrics();
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager); mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager);
mSpyImageView = Mockito.spy(new ImageView(mContext)); mSpyImageView = Mockito.spy(new ImageView(mContext));
@@ -110,7 +124,14 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
return null; return null;
}).when(mSpyImageView).setOnTouchListener( }).when(mSpyImageView).setOnTouchListener(
any(View.OnTouchListener.class)); any(View.OnTouchListener.class));
mMagnificationModeSwitch = new MagnificationModeSwitch(mContext, mSpyImageView); doAnswer(invocation -> {
Choreographer.FrameCallback callback = invocation.getArgument(0);
callback.doFrame(0);
return null;
}).when(mSfVsyncFrameProvider).postFrameCallback(
any(Choreographer.FrameCallback.class));
mMagnificationModeSwitch = new MagnificationModeSwitch(mContext, mSpyImageView,
mSfVsyncFrameProvider);
assertNotNull(mTouchListener); assertNotNull(mTouchListener);
} }
@@ -178,19 +199,64 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
} }
@Test @Test
public void onConfigurationChanged_buttonIsShowing_updateResourcesAndLayout() { public void onDensityChanged_buttonIsShowing_updateResourcesAndLayout() {
mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
resetAndStubMockImageViewAndAnimator(); resetAndStubMockImageViewAndAnimator();
mMagnificationModeSwitch.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY); mMagnificationModeSwitch.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY);
verify(mSpyImageView).setPadding(anyInt(), anyInt(), anyInt(), anyInt()); InOrder inOrder = Mockito.inOrder(mWindowManager);
verify(mSpyImageView).setImageResource( inOrder.verify(mWindowManager).updateViewLayout(eq(mSpyImageView), any());
getIconResId(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN)); inOrder.verify(mWindowManager).removeView(eq(mSpyImageView));
verify(mWindowManager).updateViewLayout(eq(mSpyImageView), any()); inOrder.verify(mWindowManager).addView(eq(mSpyImageView), any());
verify(mSpyImageView).setSystemGestureExclusionRects(any(List.class)); verify(mSpyImageView).setSystemGestureExclusionRects(any(List.class));
} }
@Test
public void onApplyWindowInsetsWithBoundsChange_buttonIsShowing_updateLayoutPosition() {
mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
mMagnificationModeSwitch.mDraggableWindowBounds.inset(10, 10);
mSpyImageView.onApplyWindowInsets(WindowInsets.CONSUMED);
verify(mWindowManager).updateViewLayout(eq(mSpyImageView),
any(WindowManager.LayoutParams.class));
assertLayoutPosition(/* toLeftScreenEdge= */ false);
}
@Test
public void onApplyWindowInsetsWithWindowInsetsChange_buttonIsShowing_draggableBoundsChanged() {
mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
final Rect oldDraggableBounds = new Rect(mMagnificationModeSwitch.mDraggableWindowBounds);
mWindowManager.setWindowInsets(new WindowInsets.Builder()
.setInsetsIgnoringVisibility(systemBars(), Insets.of(0, 20, 0, 20))
.build());
mSpyImageView.onApplyWindowInsets(WindowInsets.CONSUMED);
assertNotEquals(oldDraggableBounds, mMagnificationModeSwitch.mDraggableWindowBounds);
}
@Test
public void onDraggingGestureFinish_buttonIsShowing_stickToRightEdge() {
final int windowHalfWidth =
mWindowManager.getCurrentWindowMetrics().getBounds().width() / 2;
mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
// Drag button to right side on screen
final int offset = ViewConfiguration.get(mContext).getScaledTouchSlop() + 10;
final long downTime = SystemClock.uptimeMillis();
mTouchListener.onTouch(mSpyImageView, obtainMotionEvent(
downTime, 0, ACTION_DOWN, 100, 100));
mTouchListener.onTouch(mSpyImageView,
obtainMotionEvent(downTime, downTime, ACTION_MOVE, windowHalfWidth - offset, 100));
mTouchListener.onTouch(mSpyImageView, obtainMotionEvent(
downTime, downTime, ACTION_UP, windowHalfWidth - offset, 100));
assertLayoutPosition(/* toLeftScreenEdge= */false);
}
@Test @Test
public void performSingleTap_fullscreenMode_removeViewAndChangeSettingsValue() { public void performSingleTap_fullscreenMode_removeViewAndChangeSettingsValue() {
mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
@@ -344,12 +410,12 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
public void showButton_hasAccessibilityWindowTitle() { public void showButton_hasAccessibilityWindowTitle() {
mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW);
ArgumentCaptor<WindowManager.LayoutParams> paramsArgumentCaptor = ArgumentCaptor.forClass( final WindowManager.LayoutParams layoutPrams =
WindowManager.LayoutParams.class); mWindowManager.getLayoutParamsFromAttachedView();
verify(mWindowManager).addView(eq(mSpyImageView), paramsArgumentCaptor.capture()); assertNotNull(layoutPrams);
assertEquals(getContext().getResources().getString( assertEquals(getContext().getResources().getString(
com.android.internal.R.string.android_system_label), com.android.internal.R.string.android_system_label),
paramsArgumentCaptor.getValue().accessibilityTitle); layoutPrams.accessibilityTitle);
} }
@Test @Test
@@ -361,10 +427,10 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
com.android.internal.R.string.android_system_label, newA11yWindowTitle); com.android.internal.R.string.android_system_label, newA11yWindowTitle);
mMagnificationModeSwitch.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE); mMagnificationModeSwitch.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE);
ArgumentCaptor<WindowManager.LayoutParams> paramsArgumentCaptor = ArgumentCaptor.forClass( final WindowManager.LayoutParams layoutParams =
WindowManager.LayoutParams.class); mWindowManager.getLayoutParamsFromAttachedView();
verify(mWindowManager).updateViewLayout(eq(mSpyImageView), paramsArgumentCaptor.capture()); assertNotNull(layoutParams);
assertEquals(newA11yWindowTitle, paramsArgumentCaptor.getValue().accessibilityTitle); assertEquals(newA11yWindowTitle, layoutParams.accessibilityTitle);
} }
private void assertModeUnchanged(int expectedMode) { private void assertModeUnchanged(int expectedMode) {
@@ -374,7 +440,7 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
} }
private void assertShowFadingAnimation(float alpha) { private void assertShowFadingAnimation(float alpha) {
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); final ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
if (alpha == FADE_IN_ALPHA) { // Fade-in if (alpha == FADE_IN_ALPHA) { // Fade-in
verify(mSpyImageView).postOnAnimation(runnableCaptor.capture()); verify(mSpyImageView).postOnAnimation(runnableCaptor.capture());
} else { // Fade-out } else { // Fade-out
@@ -392,6 +458,13 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
private void resetAndStubMockImageViewAndAnimator() { private void resetAndStubMockImageViewAndAnimator() {
resetAndStubMockAnimator(); resetAndStubMockAnimator();
Mockito.reset(mSpyImageView); Mockito.reset(mSpyImageView);
final Handler handler = mock(Handler.class);
when(mSpyImageView.getHandler()).thenReturn(handler);
doAnswer(invocation -> {
final Runnable runnable = invocation.getArgument(0);
runnable.run();
return null;
}).when(handler).post(any(Runnable.class));
doAnswer(invocation -> { doAnswer(invocation -> {
final Runnable runnable = invocation.getArgument(0); final Runnable runnable = invocation.getArgument(0);
runnable.run(); runnable.run();
@@ -440,4 +513,16 @@ public class MagnificationModeSwitchTest extends SysuiTestCase {
mFadeOutAnimation.run(); mFadeOutAnimation.run();
mFadeOutAnimation = null; mFadeOutAnimation = null;
} }
private void assertLayoutPosition(boolean toLeftScreenEdge) {
final int expectedX =
toLeftScreenEdge ? mMagnificationModeSwitch.mDraggableWindowBounds.left
: mMagnificationModeSwitch.mDraggableWindowBounds.right;
final int expectedY = mMagnificationModeSwitch.mDraggableWindowBounds.bottom;
final WindowManager.LayoutParams layoutParams =
mWindowManager.getLayoutParamsFromAttachedView();
assertNotNull(layoutParams);
assertEquals(expectedX, layoutParams.x);
assertEquals(expectedY, layoutParams.y);
}
} }

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2021 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 com.android.systemui.accessibility;
import static android.view.WindowInsets.Type.systemGestures;
import android.graphics.Insets;
import android.graphics.Region;
import android.view.Display;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
public class TestableWindowManager implements WindowManager {
private final WindowManager mWindowManager;
private View mView;
private WindowInsets mWindowInsets = null;
TestableWindowManager(WindowManager windowManager) {
mWindowManager = windowManager;
}
@Override
public Display getDefaultDisplay() {
return mWindowManager.getDefaultDisplay();
}
@Override
public void removeViewImmediate(View view) {
mWindowManager.removeViewImmediate(view);
}
@Override
public void requestAppKeyboardShortcuts(WindowManager.KeyboardShortcutsReceiver receiver,
int deviceId) {
mWindowManager.requestAppKeyboardShortcuts(receiver, deviceId);
}
@Override
public Region getCurrentImeTouchRegion() {
return mWindowManager.getCurrentImeTouchRegion();
}
@Override
public void addView(View view, ViewGroup.LayoutParams params) {
mView = view;
mWindowManager.addView(view, params);
}
@Override
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
mWindowManager.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mView = null;
mWindowManager.removeView(view);
}
@Override
public WindowMetrics getCurrentWindowMetrics() {
final Insets systemGesturesInsets = Insets.of(0, 0, 0, 10);
final WindowInsets insets = new WindowInsets.Builder()
.setInsets(systemGestures(), systemGesturesInsets)
.build();
final WindowMetrics windowMetrics = new WindowMetrics(
mWindowManager.getCurrentWindowMetrics().getBounds(),
mWindowInsets == null ? insets : mWindowInsets);
return windowMetrics;
}
@Override
public WindowMetrics getMaximumWindowMetrics() {
return mWindowManager.getMaximumWindowMetrics();
}
public View getAttachedView() {
return mView;
}
public WindowManager.LayoutParams getLayoutParamsFromAttachedView() {
if (mView == null) {
return null;
}
return (WindowManager.LayoutParams) mView.getLayoutParams();
}
public void setWindowInsets(WindowInsets insets) {
mWindowInsets = insets;
}
}

View File

@@ -17,7 +17,6 @@
package com.android.systemui.accessibility; package com.android.systemui.accessibility;
import static android.view.Choreographer.FrameCallback; import static android.view.Choreographer.FrameCallback;
import static android.view.WindowInsets.Type.systemGestures;
import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
@@ -46,9 +45,7 @@ import android.app.Instrumentation;
import android.content.Context; import android.content.Context;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
@@ -58,10 +55,7 @@ import android.view.Display;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceControl; import android.view.SurfaceControl;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo;
import androidx.test.InstrumentationRegistry; import androidx.test.InstrumentationRegistry;
@@ -96,14 +90,13 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
private MirrorWindowControl mMirrorWindowControl; private MirrorWindowControl mMirrorWindowControl;
@Mock @Mock
private WindowMagnifierCallback mWindowMagnifierCallback; private WindowMagnifierCallback mWindowMagnifierCallback;
@Mock (answer = Answers.RETURNS_DEEP_STUBS) @Mock(answer = Answers.RETURNS_DEEP_STUBS)
private SurfaceControl.Transaction mTransaction; private SurfaceControl.Transaction mTransaction;
private WindowManager mWindowManager; private TestableWindowManager mWindowManager;
private SysUiState mSysUiState = new SysUiState(); private SysUiState mSysUiState = new SysUiState();
private Resources mResources; private Resources mResources;
private WindowMagnificationController mWindowMagnificationController; private WindowMagnificationController mWindowMagnificationController;
private Instrumentation mInstrumentation; private Instrumentation mInstrumentation;
private View mMirrorView;
@Before @Before
public void setUp() { public void setUp() {
@@ -157,7 +150,9 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onWindowMagnifierBoundsChanged( timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onWindowMagnifierBoundsChanged(
eq(mContext.getDisplayId()), boundsCaptor.capture()); eq(mContext.getDisplayId()), boundsCaptor.capture());
final Rect actualBounds = new Rect(); final Rect actualBounds = new Rect();
mMirrorView.getBoundsOnScreen(actualBounds); final View mirrorView = mWindowManager.getAttachedView();
assertNotNull(mirrorView);
mirrorView.getBoundsOnScreen(actualBounds);
assertEquals(actualBounds, boundsCaptor.getValue()); assertEquals(actualBounds, boundsCaptor.getValue());
} }
@@ -218,7 +213,9 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
ArgumentCaptor<Runnable> runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); ArgumentCaptor<Runnable> runnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class);
verify(mHandler).postDelayed(runnableArgumentCaptor.capture(), anyLong()); verify(mHandler).postDelayed(runnableArgumentCaptor.capture(), anyLong());
runnableArgumentCaptor.getValue().run(); runnableArgumentCaptor.getValue().run();
assertThat(mMirrorView.getStateDescription().toString(), containsString("300")); final View mirrorView = mWindowManager.getAttachedView();
assertNotNull(mirrorView);
assertThat(mirrorView.getStateDescription().toString(), containsString("300"));
} }
@Test @Test
@@ -300,10 +297,11 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
mWindowMagnificationController.enableWindowMagnification(2.5f, Float.NaN, mWindowMagnificationController.enableWindowMagnification(2.5f, Float.NaN,
Float.NaN); Float.NaN);
}); });
assertNotNull(mMirrorView); final View mirrorView = mWindowManager.getAttachedView();
assertNotNull(mirrorView);
final AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo(); final AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo();
mMirrorView.onInitializeAccessibilityNodeInfo(nodeInfo); mirrorView.onInitializeAccessibilityNodeInfo(nodeInfo);
assertNotNull(nodeInfo.getContentDescription()); assertNotNull(nodeInfo.getContentDescription());
assertThat(nodeInfo.getStateDescription().toString(), containsString("250")); assertThat(nodeInfo.getStateDescription().toString(), containsString("250"));
@@ -323,24 +321,24 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
mWindowMagnificationController.enableWindowMagnification(2.5f, Float.NaN, mWindowMagnificationController.enableWindowMagnification(2.5f, Float.NaN,
Float.NaN); Float.NaN);
}); });
assertNotNull(mMirrorView);
final View mirrorView = mWindowManager.getAttachedView();
assertTrue( assertTrue(
mMirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_out, null)); mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_out, null));
// Minimum scale is 2.0. // Minimum scale is 2.0.
verify(mWindowMagnifierCallback).onPerformScaleAction(eq(displayId), eq(2.0f)); verify(mWindowMagnifierCallback).onPerformScaleAction(eq(displayId), eq(2.0f));
assertTrue(mMirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_in, null)); assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_in, null));
verify(mWindowMagnifierCallback).onPerformScaleAction(eq(displayId), eq(3.5f)); verify(mWindowMagnifierCallback).onPerformScaleAction(eq(displayId), eq(3.5f));
// TODO: Verify the final state when the mirror surface is visible. // TODO: Verify the final state when the mirror surface is visible.
assertTrue(mMirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null)); assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null));
assertTrue( assertTrue(
mMirrorView.performAccessibilityAction(R.id.accessibility_action_move_down, null)); mirrorView.performAccessibilityAction(R.id.accessibility_action_move_down, null));
assertTrue( assertTrue(
mMirrorView.performAccessibilityAction(R.id.accessibility_action_move_right, null)); mirrorView.performAccessibilityAction(R.id.accessibility_action_move_right, null));
assertTrue( assertTrue(
mMirrorView.performAccessibilityAction(R.id.accessibility_action_move_left, null)); mirrorView.performAccessibilityAction(R.id.accessibility_action_move_left, null));
} }
@Test @Test
@@ -351,7 +349,8 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
Float.NaN); Float.NaN);
}); });
mMirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null); final View mirrorView = mWindowManager.getAttachedView();
mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null);
verify(mWindowMagnifierCallback).onAccessibilityActionPerformed(eq(displayId)); verify(mWindowMagnifierCallback).onAccessibilityActionPerformed(eq(displayId));
} }
@@ -363,12 +362,8 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
Float.NaN); Float.NaN);
}); });
ArgumentCaptor<WindowManager.LayoutParams> paramsArgumentCaptor = ArgumentCaptor.forClass(
WindowManager.LayoutParams.class);
verify(mWindowManager).addView(eq(mMirrorView), paramsArgumentCaptor.capture());
assertEquals(getContext().getResources().getString( assertEquals(getContext().getResources().getString(
com.android.internal.R.string.android_system_label), com.android.internal.R.string.android_system_label), getAccessibilityWindowTitle());
paramsArgumentCaptor.getValue().accessibilityTitle);
} }
@Test @Test
@@ -400,15 +395,15 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
mWindowMagnificationController.onSingleTap(); mWindowMagnificationController.onSingleTap();
}); });
final View mirrorView = mWindowManager.getAttachedView();
final long timeout = SystemClock.uptimeMillis() + 1000; final long timeout = SystemClock.uptimeMillis() + 1000;
while (SystemClock.uptimeMillis() < timeout) { while (SystemClock.uptimeMillis() < timeout) {
SystemClock.sleep(10); SystemClock.sleep(10);
if (Float.compare(1.0f, mirrorView.getScaleX()) < 0) {
if (Float.compare(1.0f, mMirrorView.getScaleX()) < 0) {
return; return;
} }
} }
fail("mMirrorView scale is not changed"); fail("MirrorView scale is not changed");
} }
@Test @Test
@@ -428,77 +423,16 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
} }
private CharSequence getAccessibilityWindowTitle() { private CharSequence getAccessibilityWindowTitle() {
if (mMirrorView == null) { final View mirrorView = mWindowManager.getAttachedView();
if (mirrorView == null) {
return null; return null;
} }
WindowManager.LayoutParams layoutParams = WindowManager.LayoutParams layoutParams =
(WindowManager.LayoutParams) mMirrorView.getLayoutParams(); (WindowManager.LayoutParams) mirrorView.getLayoutParams();
return layoutParams.accessibilityTitle; return layoutParams.accessibilityTitle;
} }
private boolean hasMagnificationOverlapFlag() { private boolean hasMagnificationOverlapFlag() {
return (mSysUiState.getFlags() & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0; return (mSysUiState.getFlags() & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0;
} }
private class TestableWindowManager implements WindowManager {
private final WindowManager mWindowManager;
TestableWindowManager(WindowManager windowManager) {
mWindowManager = windowManager;
}
@Override
public Display getDefaultDisplay() {
return mWindowManager.getDefaultDisplay();
}
@Override
public void removeViewImmediate(View view) {
mWindowManager.removeViewImmediate(view);
}
@Override
public void requestAppKeyboardShortcuts(KeyboardShortcutsReceiver receiver, int deviceId) {
mWindowManager.requestAppKeyboardShortcuts(receiver, deviceId);
}
@Override
public Region getCurrentImeTouchRegion() {
return mWindowManager.getCurrentImeTouchRegion();
}
@Override
public void addView(View view, ViewGroup.LayoutParams params) {
mMirrorView = view;
mWindowManager.addView(view, params);
}
@Override
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
mWindowManager.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mMirrorView = null;
mWindowManager.removeView(view);
}
@Override
public WindowMetrics getCurrentWindowMetrics() {
final Insets systemGesturesInsets = Insets.of(0, 0, 0, 10);
final WindowInsets insets = new WindowInsets.Builder()
.setInsets(systemGestures(), systemGesturesInsets)
.build();
final WindowMetrics windowMetrics = new WindowMetrics(
mWindowManager.getCurrentWindowMetrics().getBounds(), insets);
return windowMetrics;
}
@Override
public WindowMetrics getMaximumWindowMetrics() {
return mWindowManager.getMaximumWindowMetrics();
}
}
} }