diff options
12 files changed, 2048 insertions, 84 deletions
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java index 4840f003da3e..5249fd5253f2 100644 --- a/core/java/android/view/SurfaceControlViewHost.java +++ b/core/java/android/view/SurfaceControlViewHost.java @@ -538,8 +538,8 @@ public class SurfaceControlViewHost { } private void addWindowToken(WindowManager.LayoutParams attrs) { - final WindowManagerImpl wm = - (WindowManagerImpl) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE); + final WindowManager wm = + (WindowManager) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE); attrs.token = wm.getDefaultToken(); } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 427d053f754e..c78826116426 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -6109,4 +6109,12 @@ public interface WindowManager extends ViewManager { throw new UnsupportedOperationException( "getSurfaceControlInputClientToken is not implemented"); } + + /** + * @hide + */ + default @NonNull IBinder getDefaultToken() { + throw new UnsupportedOperationException( + "getDefaultToken is not implemented"); + } } diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index 41d181c1b10c..5072ad755cba 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -458,7 +458,9 @@ public final class WindowManagerImpl implements WindowManager { return null; } - IBinder getDefaultToken() { + @Override + @NonNull + public IBinder getDefaultToken() { return mDefaultToken; } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 7782fd7e6c2a..ef1bf5a5c548 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -62,6 +62,7 @@ import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; import android.view.IWindow; +import android.view.SurfaceControl; import android.view.View; import android.view.accessibility.AccessibilityEvent.EventType; @@ -2404,4 +2405,29 @@ public final class AccessibilityManager { throw re.rethrowFromSystemServer(); } } + + + /** + * Attaches a {@link android.view.SurfaceControl} containing an accessibility overlay to the + * specified display. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW) + public void attachAccessibilityOverlayToDisplay( + int displayId, @NonNull SurfaceControl surfaceControl) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.attachAccessibilityOverlayToDisplay_enforcePermission( + displayId, surfaceControl); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 9c04c27d189a..1c5d29e0ff1e 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -31,6 +31,7 @@ import android.view.accessibility.IMagnificationConnection; import android.view.InputEvent; import android.view.IWindow; import android.view.MagnificationSpec; +import android.view.SurfaceControl; /** * Interface implemented by the AccessibilityManagerService called by @@ -136,4 +137,7 @@ interface IAccessibilityManager { MagnificationSpec magnificationSpec; } WindowTransformationSpec getWindowTransformationSpec(int windowId); + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)") + void attachAccessibilityOverlayToDisplay_enforcePermission(int displayId, in SurfaceControl surfaceControl); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java index 3ca95e11d789..5171a1f22791 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java @@ -19,6 +19,7 @@ package com.android.systemui.accessibility; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; import static com.android.systemui.accessibility.AccessibilityLogger.MagnificationSettingsEvent; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; @@ -28,10 +29,12 @@ import android.annotation.Nullable; import android.content.Context; import android.graphics.Rect; import android.hardware.display.DisplayManager; +import android.os.Binder; import android.os.Handler; import android.util.SparseArray; import android.view.Display; import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IMagnificationConnection; @@ -40,6 +43,7 @@ import android.view.accessibility.IRemoteMagnificationAnimationCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.CoreStartable; +import com.android.systemui.Flags; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.model.SysUiState; @@ -49,6 +53,7 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; +import java.util.function.Supplier; import javax.inject.Inject; @@ -101,19 +106,28 @@ public class Magnification implements CoreStartable, CommandQueue.Callbacks { @Override protected WindowMagnificationController createInstance(Display display) { final Context windowContext = mContext.createWindowContext(display, - TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null); + Flags.createWindowlessWindowMagnifier() + ? TYPE_ACCESSIBILITY_OVERLAY + : TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, + /* options */ null); windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); + + Supplier<SurfaceControlViewHost> scvhSupplier = () -> + Flags.createWindowlessWindowMagnifier() ? new SurfaceControlViewHost(mContext, + mContext.getDisplay(), new Binder(), TAG) : null; + return new WindowMagnificationController( windowContext, mHandler, new WindowMagnificationAnimationController(windowContext), - new SfVsyncFrameCallbackProvider(), - null, + /* mirrorWindowControl= */ null, new SurfaceControl.Transaction(), mWindowMagnifierCallback, mSysUiState, - WindowManagerGlobal::getWindowSession, - mSecureSettings); + mSecureSettings, + scvhSupplier, + new SfVsyncFrameCallbackProvider(), + WindowManagerGlobal::getWindowSession); } } @@ -140,7 +154,7 @@ public class Magnification implements CoreStartable, CommandQueue.Callbacks { @Override protected MagnificationSettingsController createInstance(Display display) { final Context windowContext = mContext.createWindowContext(display, - TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null); + TYPE_ACCESSIBILITY_OVERLAY, /* options */ null); windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); return new MagnificationSettingsController( windowContext, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index dde9f48424ea..33728ac330dc 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -63,23 +63,27 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowMetrics; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.widget.FrameLayout; import android.widget.ImageView; +import androidx.annotation.UiThread; import androidx.core.math.MathUtils; import com.android.internal.accessibility.common.MagnificationConstants; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.systemui.Flags; import com.android.systemui.model.SysUiState; import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; @@ -158,6 +162,15 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private int mMagnificationFrameOffsetX = 0; private int mMagnificationFrameOffsetY = 0; + @Nullable private Supplier<SurfaceControlViewHost> mScvhSupplier; + + /** + * SurfaceControlViewHost is used to control the position of the window containing + * {@link #mMirrorView}. Using SurfaceControlViewHost instead of a regular window enables + * changing the window's position and setting {@link #mMirrorSurface}'s geometry atomically. + */ + private SurfaceControlViewHost mSurfaceControlViewHost; + // The root of the mirrored content private SurfaceControl mMirrorSurface; @@ -236,21 +249,21 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold @UiContext Context context, @NonNull Handler handler, @NonNull WindowMagnificationAnimationController animationController, - SfVsyncFrameCallbackProvider sfVsyncFrameProvider, MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction, @NonNull WindowMagnifierCallback callback, SysUiState sysUiState, - @NonNull Supplier<IWindowSession> globalWindowSessionSupplier, - SecureSettings secureSettings) { + SecureSettings secureSettings, + Supplier<SurfaceControlViewHost> scvhSupplier, + SfVsyncFrameCallbackProvider sfVsyncFrameProvider, + Supplier<IWindowSession> globalWindowSessionSupplier) { mContext = context; mHandler = handler; mAnimationController = animationController; - mGlobalWindowSessionSupplier = globalWindowSessionSupplier; mAnimationController.setWindowMagnificationController(this); - mSfVsyncFrameProvider = sfVsyncFrameProvider; mWindowMagnifierCallback = callback; mSysUiState = sysUiState; + mScvhSupplier = scvhSupplier; mConfiguration = new Configuration(context.getResources().getConfiguration()); mWindowMagnificationSizePrefs = new WindowMagnificationSizePrefs(mContext); @@ -288,22 +301,78 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mTransaction = transaction; mGestureDetector = new MagnificationGestureDetector(mContext, handler, this); + mWindowInsetChangeRunnable = this::onWindowInsetChanged; + mGlobalWindowSessionSupplier = globalWindowSessionSupplier; + mSfVsyncFrameProvider = sfVsyncFrameProvider; // Initialize listeners. - mMirrorViewRunnable = () -> { - if (mMirrorView != null) { - final Rect oldViewBounds = new Rect(mMirrorViewBounds); - mMirrorView.getBoundsOnScreen(mMirrorViewBounds); - if (oldViewBounds.width() != mMirrorViewBounds.width() - || oldViewBounds.height() != mMirrorViewBounds.height()) { - mMirrorView.setSystemGestureExclusionRects(Collections.singletonList( - new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height()))); + if (Flags.createWindowlessWindowMagnifier()) { + mMirrorViewRunnable = new Runnable() { + final Rect mPreviousBounds = new Rect(); + + @Override + public void run() { + if (mMirrorView != null) { + if (mPreviousBounds.width() != mMirrorViewBounds.width() + || mPreviousBounds.height() != mMirrorViewBounds.height()) { + mMirrorView.setSystemGestureExclusionRects(Collections.singletonList( + new Rect(0, 0, mMirrorViewBounds.width(), + mMirrorViewBounds.height()))); + mPreviousBounds.set(mMirrorViewBounds); + } + updateSystemUIStateIfNeeded(); + mWindowMagnifierCallback.onWindowMagnifierBoundsChanged( + mDisplayId, mMirrorViewBounds); + } } - updateSystemUIStateIfNeeded(); - mWindowMagnifierCallback.onWindowMagnifierBoundsChanged( - mDisplayId, mMirrorViewBounds); - } - }; + }; + + mMirrorSurfaceViewLayoutChangeListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + mMirrorView.post(this::applyTapExcludeRegion); + + mMirrorViewGeometryVsyncCallback = null; + } else { + mMirrorViewRunnable = () -> { + if (mMirrorView != null) { + final Rect oldViewBounds = new Rect(mMirrorViewBounds); + mMirrorView.getBoundsOnScreen(mMirrorViewBounds); + if (oldViewBounds.width() != mMirrorViewBounds.width() + || oldViewBounds.height() != mMirrorViewBounds.height()) { + mMirrorView.setSystemGestureExclusionRects(Collections.singletonList( + new Rect(0, 0, + mMirrorViewBounds.width(), mMirrorViewBounds.height()))); + } + updateSystemUIStateIfNeeded(); + mWindowMagnifierCallback.onWindowMagnifierBoundsChanged( + mDisplayId, mMirrorViewBounds); + } + }; + + mMirrorSurfaceViewLayoutChangeListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + mMirrorView.post(this::applyTapExcludeRegion); + + mMirrorViewGeometryVsyncCallback = + l -> { + if (isActivated() && mMirrorSurface != null && calculateSourceBounds( + mMagnificationFrame, mScale)) { + // The final destination for the magnification surface should be at 0,0 + // since the ViewRootImpl's position will change + mTmpRect.set(0, 0, mMagnificationFrame.width(), + mMagnificationFrame.height()); + mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, + Surface.ROTATION_0).apply(); + + // Notify source bounds change when the magnifier is not animating. + if (!mAnimationController.isAnimating()) { + mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, + mSourceBounds); + } + } + }; + } + mMirrorViewLayoutChangeListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { if (!mHandler.hasCallbacks(mMirrorViewRunnable)) { @@ -311,34 +380,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } }; - mMirrorSurfaceViewLayoutChangeListener = - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> - mMirrorView.post(this::applyTapExcludeRegion); - - mMirrorViewGeometryVsyncCallback = - l -> { - if (isActivated() && mMirrorSurface != null && calculateSourceBounds( - mMagnificationFrame, mScale)) { - // The final destination for the magnification surface should be at 0,0 - // since the ViewRootImpl's position will change - mTmpRect.set(0, 0, mMagnificationFrame.width(), - mMagnificationFrame.height()); - mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, - Surface.ROTATION_0).apply(); - - // Notify source bounds change when the magnifier is not animating. - if (!mAnimationController.isAnimating()) { - mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, - mSourceBounds); - } - } - }; mUpdateStateDescriptionRunnable = () -> { if (isActivated()) { mMirrorView.setStateDescription(formatStateDescription(mScale)); } }; - mWindowInsetChangeRunnable = this::onWindowInsetChanged; } private void setupMagnificationSizeScaleOptions() { @@ -448,13 +494,21 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold if (mMirrorView != null) { mHandler.removeCallbacks(mMirrorViewRunnable); mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener); - mWm.removeView(mMirrorView); + if (!Flags.createWindowlessWindowMagnifier()) { + mWm.removeView(mMirrorView); + } mMirrorView = null; } if (mMirrorWindowControl != null) { mMirrorWindowControl.destroyControl(); } + + if (mSurfaceControlViewHost != null) { + mSurfaceControlViewHost.release(); + mSurfaceControlViewHost = null; + } + mMirrorViewBounds.setEmpty(); mSourceBounds.setEmpty(); updateSystemUIStateIfNeeded(); @@ -551,7 +605,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold if (!isActivated()) return; LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); params.accessibilityTitle = getAccessibilityWindowTitle(); - mWm.updateViewLayout(mMirrorView, params); + if (Flags.createWindowlessWindowMagnifier()) { + mSurfaceControlViewHost.relayout(params); + } else { + mWm.updateViewLayout(mMirrorView, params); + } } /** @@ -602,6 +660,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } private void createMirrorWindow() { + if (Flags.createWindowlessWindowMagnifier()) { + createWindowlessMirrorWindow(); + return; + } + // The window should be the size the mirrored surface will be but also add room for the // border and the drag handle. int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; @@ -652,6 +715,68 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold addDragTouchListeners(); } + private void createWindowlessMirrorWindow() { + // The window should be the size the mirrored surface will be but also add room for the + // border and the drag handle. + int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; + int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin; + + // TODO delete TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, it shouldn't be needed anymore + + LayoutParams params = new LayoutParams( + windowWidth, windowHeight, + LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, + LayoutParams.FLAG_NOT_TOUCH_MODAL + | LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT); + params.receiveInsetsIgnoringZOrder = true; + params.setTitle(mContext.getString(R.string.magnification_window_title)); + params.accessibilityTitle = getAccessibilityWindowTitle(); + params.setTrustedOverlay(); + + mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null); + mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view); + + mMirrorBorderView = mMirrorView.findViewById(R.id.magnification_inner_border); + + // Allow taps to go through to the mirror SurfaceView below. + mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener); + + mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener); + mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate()); + mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> { + if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) { + mHandler.post(mWindowInsetChangeRunnable); + } + return v.onApplyWindowInsets(insets); + }); + + mSurfaceControlViewHost = mScvhSupplier.get(); + mSurfaceControlViewHost.setView(mMirrorView, params); + SurfaceControl surfaceControl = mSurfaceControlViewHost + .getSurfacePackage().getSurfaceControl(); + + int x = mMagnificationFrame.left - mMirrorSurfaceMargin; + int y = mMagnificationFrame.top - mMirrorSurfaceMargin; + mTransaction + .setCrop(surfaceControl, new Rect(0, 0, windowWidth, windowHeight)) + .setPosition(surfaceControl, x, y) + .setLayer(surfaceControl, Integer.MAX_VALUE) + .show(surfaceControl) + .apply(); + + mMirrorViewBounds.set(x, y, x + windowWidth, y + windowHeight); + + AccessibilityManager accessibilityManager = mContext + .getSystemService(AccessibilityManager.class); + accessibilityManager.attachAccessibilityOverlayToDisplay(mDisplayId, surfaceControl); + + SurfaceHolder holder = mMirrorSurfaceView.getHolder(); + holder.addCallback(this); + holder.setFormat(PixelFormat.RGBA_8888); + addDragTouchListeners(); + } + private void onWindowInsetChanged() { if (updateSystemGestureInsetsTop()) { updateSystemUIStateIfNeeded(); @@ -659,6 +784,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } private void applyTapExcludeRegion() { + if (Flags.createWindowlessWindowMagnifier()) { + applyTouchableRegion(); + return; + } + // Sometimes this can get posted and run after deleteWindowMagnification() is called. if (mMirrorView == null) return; @@ -709,6 +839,51 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return regionInsideDragBorder; } + private void applyTouchableRegion() { + // Sometimes this can get posted and run after deleteWindowMagnification() is called. + if (mMirrorView == null) return; + + var surfaceControl = mSurfaceControlViewHost.getRootSurfaceControl(); + surfaceControl.setTouchableRegion(calculateTouchableRegion()); + } + + private Region calculateTouchableRegion() { + Region touchableRegion = new Region(0, 0, mMirrorView.getWidth(), mMirrorView.getHeight()); + + Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize, + mMirrorView.getWidth() - mBorderDragSize, + mMirrorView.getHeight() - mBorderDragSize); + touchableRegion.op(regionInsideDragBorder, Region.Op.DIFFERENCE); + + Rect dragArea = new Rect(); + mDragView.getHitRect(dragArea); + + Rect topLeftArea = new Rect(); + mTopLeftCornerView.getHitRect(topLeftArea); + + Rect topRightArea = new Rect(); + mTopRightCornerView.getHitRect(topRightArea); + + Rect bottomLeftArea = new Rect(); + mBottomLeftCornerView.getHitRect(bottomLeftArea); + + Rect bottomRightArea = new Rect(); + mBottomRightCornerView.getHitRect(bottomRightArea); + + Rect closeArea = new Rect(); + mCloseView.getHitRect(closeArea); + + // Add touchable regions for drag and close + touchableRegion.op(dragArea, Region.Op.UNION); + touchableRegion.op(topLeftArea, Region.Op.UNION); + touchableRegion.op(topRightArea, Region.Op.UNION); + touchableRegion.op(bottomLeftArea, Region.Op.UNION); + touchableRegion.op(bottomRightArea, Region.Op.UNION); + touchableRegion.op(closeArea, Region.Op.UNION); + + return touchableRegion; + } + private String getAccessibilityWindowTitle() { return mResources.getString(com.android.internal.R.string.android_system_label); } @@ -852,8 +1027,84 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold * {@link #mMagnificationFrame}. */ private void modifyWindowMagnification(boolean computeWindowSize) { - mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback); - updateMirrorViewLayout(computeWindowSize); + if (Flags.createWindowlessWindowMagnifier()) { + updateMirrorSurfaceGeometry(); + updateWindowlessMirrorViewLayout(computeWindowSize); + } else { + mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback); + updateMirrorViewLayout(computeWindowSize); + } + } + + /** + * Updates {@link #mMirrorSurface}'s geometry. This modifies {@link #mTransaction} but does not + * apply it. + */ + @UiThread + private void updateMirrorSurfaceGeometry() { + if (isActivated() && mMirrorSurface != null + && calculateSourceBounds(mMagnificationFrame, mScale)) { + // The final destination for the magnification surface should be at 0,0 + // since the ViewRootImpl's position will change + mTmpRect.set(0, 0, mMagnificationFrame.width(), mMagnificationFrame.height()); + mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, Surface.ROTATION_0); + + // Notify source bounds change when the magnifier is not animating. + if (!mAnimationController.isAnimating()) { + mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds); + } + } + } + + /** + * Updates the position of {@link mSurfaceControlViewHost} and layout params of MirrorView based + * on the position and size of {@link #mMagnificationFrame}. + * + * @param computeWindowSize set to {@code true} to compute window size with + * {@link #mMagnificationFrame}. + */ + @UiThread + private void updateWindowlessMirrorViewLayout(boolean computeWindowSize) { + if (!isActivated()) { + return; + } + + final int width = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; + final int height = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin; + + final int minX = -mOuterBorderSize; + final int maxX = mWindowBounds.right - width + mOuterBorderSize; + final int x = MathUtils.clamp(mMagnificationFrame.left - mMirrorSurfaceMargin, minX, maxX); + + final int minY = -mOuterBorderSize; + final int maxY = mWindowBounds.bottom - height + mOuterBorderSize; + final int y = MathUtils.clamp(mMagnificationFrame.top - mMirrorSurfaceMargin, minY, maxY); + + if (computeWindowSize) { + LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); + params.width = width; + params.height = height; + mSurfaceControlViewHost.relayout(params); + mTransaction.setCrop(mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(), + new Rect(0, 0, width, height)); + } + + mMirrorViewBounds.set(x, y, x + width, y + height); + mTransaction.setPosition( + mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(), x, y); + if (computeWindowSize) { + mSurfaceControlViewHost.getRootSurfaceControl().applyTransactionOnDraw(mTransaction); + } else { + mTransaction.apply(); + } + + // If they are not dragging the handle, we can move the drag handle immediately without + // disruption. But if they are dragging it, we avoid moving until the end of the drag. + if (!mIsDragging) { + mMirrorView.post(this::maybeRepositionButton); + } + + mMirrorViewRunnable.run(); } /** @@ -1094,7 +1345,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold /** * Enables window magnification with specified parameters. If the given scale is <strong>less - * than or equal to 1.0f<strong>, then + * than or equal to 1.0f</strong>, then * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to * be consistent with the behavior of display magnification. * @@ -1110,7 +1361,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold /** * Enables window magnification with specified parameters. If the given scale is <strong>less - * than 1.0f<strong>, then + * than 1.0f</strong>, then * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to * be consistent with the behavior of display magnification. * @@ -1426,10 +1677,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mDragView.getLayoutParams(); - mMirrorView.getBoundsOnScreen(mTmpRect); - final int newGravity; - if (mTmpRect.right >= screenEdgeX) { + if (mMirrorViewBounds.right >= screenEdgeX) { newGravity = Gravity.BOTTOM | Gravity.LEFT; } else { newGravity = Gravity.BOTTOM | Gravity.RIGHT; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java index 44770fab2d30..b23dfdc68a87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java @@ -16,8 +16,10 @@ package com.android.systemui.accessibility; +import android.annotation.NonNull; import android.graphics.Rect; import android.graphics.Region; +import android.os.IBinder; import android.view.Display; import android.view.View; import android.view.ViewGroup; @@ -89,6 +91,11 @@ public class TestableWindowManager implements WindowManager { return mWindowManager.getMaximumWindowMetrics(); } + @Override + public @NonNull IBinder getDefaultToken() { + return mWindowManager.getDefaultToken(); + } + public View getAttachedView() { return mView; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index d86d12303140..8299acbc2d52 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -21,8 +21,10 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -33,10 +35,16 @@ import android.animation.ValueAnimator; import android.annotation.Nullable; import android.content.Context; import android.graphics.Rect; +import android.os.Binder; import android.os.Handler; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; @@ -46,6 +54,7 @@ import android.view.animation.AccelerateInterpolator; import androidx.test.filters.LargeTest; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.model.SysUiState; @@ -63,7 +72,10 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; @LargeTest @RunWith(AndroidTestingRunner.class) @@ -71,6 +83,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { @Rule public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final float DEFAULT_SCALE = 4.0f; private static final float DEFAULT_CENTER_X = 400.0f; private static final float DEFAULT_CENTER_Y = 500.0f; @@ -107,6 +121,13 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { private TestableWindowManager mWindowManager; private ValueAnimator mValueAnimator; + // This list contains all SurfaceControlViewHosts created during a given test. If the + // magnification window is recreated during a test, the list will contain more than a single + // element. + private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>(); + // The most recently created SurfaceControlViewHost. + private SurfaceControlViewHost mSurfaceControlViewHost; + private SurfaceControl.Transaction mTransaction; @Before public void setUp() throws Exception { @@ -123,10 +144,27 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mValueAnimator = newValueAnimator(); mWindowMagnificationAnimationController = new WindowMagnificationAnimationController( mContext, mValueAnimator); - mController = new SpyWindowMagnificationController(mContext, mHandler, + + Supplier<SurfaceControlViewHost> scvhSupplier = () -> { + mSurfaceControlViewHost = spy(new SurfaceControlViewHost( + mContext, mContext.getDisplay(), new Binder(), "WindowMagnification")); + mSurfaceControlViewHosts.add(mSurfaceControlViewHost); + return mSurfaceControlViewHost; + }; + + mTransaction = spy(new SurfaceControl.Transaction()); + mController = new SpyWindowMagnificationController( + mContext, + mHandler, mWindowMagnificationAnimationController, - mSfVsyncFrameProvider, null, new SurfaceControl.Transaction(), - mWindowMagnifierCallback, mSysUiState, mSecureSettings); + /* mirrorWindowControl= */ null, + mTransaction, + mWindowMagnifierCallback, + mSysUiState, + mSecureSettings, + scvhSupplier, + mSfVsyncFrameProvider); + mSpyController = mController.getSpyController(); } @@ -235,8 +273,52 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y); } + @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) @Test - public void enableWindowMagnificationWithScaleOne_enabled_AnimationAndInvokeCallback() + public void + enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOn_AnimationAndCallbackTrue() + throws RemoteException { + enableWindowMagnificationWithoutAnimation(); + + // Wait for Rects updated. + waitForIdleSync(); + View mirrorView = mSurfaceControlViewHost.getView(); + final float targetScale = 1.0f; + // Move the magnifier to the top left corner, within the boundary + final float targetCenterX = mirrorView.getWidth() / 2.0f; + final float targetCenterY = mirrorView.getHeight() / 2.0f; + + Mockito.reset(mSpyController); + getInstrumentation().runOnMainSync(() -> { + mWindowMagnificationAnimationController.enableWindowMagnification(targetScale, + targetCenterX, targetCenterY, mAnimationCallback); + mCurrentScale.set(mController.getScale()); + mCurrentCenterX.set(mController.getCenterX()); + mCurrentCenterY.set(mController.getCenterY()); + advanceTimeBy(mWaitAnimationDuration); + }); + + verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal( + mScaleCaptor.capture(), + mCenterXCaptor.capture(), mCenterYCaptor.capture(), + mOffsetXCaptor.capture(), mOffsetYCaptor.capture()); + verifyStartValue(mScaleCaptor, mCurrentScale.get()); + verifyStartValue(mCenterXCaptor, mCurrentCenterX.get()); + verifyStartValue(mCenterYCaptor, mCurrentCenterY.get()); + verifyStartValue(mOffsetXCaptor, 0f); + verifyStartValue(mOffsetYCaptor, 0f); + + verifyFinalSpec(targetScale, targetCenterX, targetCenterY); + + verify(mAnimationCallback).onResult(true); + assertEquals(WindowMagnificationAnimationController.STATE_ENABLED, + mWindowMagnificationAnimationController.getState()); + } + + @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) + @Test + public void + enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOff_AnimationAndCallbackTrue() throws RemoteException { enableWindowMagnificationWithoutAnimation(); @@ -475,8 +557,46 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { verify(mAnimationCallback2).onResult(true); } + @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) + @Test + public void enableWindowMagnificationWithOffset_windowlessFlagOn_expectedValues() { + final float offsetRatio = -0.1f; + final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds()); + + Mockito.reset(mSpyController); + getInstrumentation().runOnMainSync(() -> { + mWindowMagnificationAnimationController.enableWindowMagnification(DEFAULT_SCALE, + windowBounds.exactCenterX(), windowBounds.exactCenterY(), + offsetRatio, offsetRatio, mAnimationCallback); + advanceTimeBy(mWaitAnimationDuration); + }); + // Wait for Rects update + waitForIdleSync(); + + final int mirrorSurfaceMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + final int defaultMagnificationWindowSize = + mController.getMagnificationWindowSizeFromIndex( + WindowMagnificationSettings.MagnificationSize.MEDIUM); + final int defaultMagnificationFrameSize = + defaultMagnificationWindowSize - 2 * mirrorSurfaceMargin; + final int expectedOffset = (int) (defaultMagnificationFrameSize / 2 * offsetRatio); + + final float expectedX = (int) (windowBounds.exactCenterX() + expectedOffset + - defaultMagnificationWindowSize / 2); + final float expectedY = (int) (windowBounds.exactCenterY() + expectedOffset + - defaultMagnificationWindowSize / 2); + + // This is called 4 times when (1) first creating WindowlessMirrorWindow (2) SurfaceView is + // created and we place the mirrored content as a child of the SurfaceView + // (3) the animation starts (4) the animation updates + verify(mTransaction, times(4)) + .setPosition(any(SurfaceControl.class), eq(expectedX), eq(expectedY)); + } + + @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) @Test - public void enableWindowMagnificationWithOffset_expectedValues() { + public void enableWindowMagnificationWithOffset_windowlessFlagOff_expectedValues() { final float offsetRatio = -0.1f; final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds()); @@ -876,23 +996,28 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { private static class SpyWindowMagnificationController extends WindowMagnificationController { private WindowMagnificationController mSpyController; - SpyWindowMagnificationController(Context context, Handler handler, + SpyWindowMagnificationController(Context context, + Handler handler, WindowMagnificationAnimationController animationController, - SfVsyncFrameCallbackProvider sfVsyncFrameProvider, - MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction, - WindowMagnifierCallback callback, SysUiState sysUiState, - SecureSettings secureSettings) { + MirrorWindowControl mirrorWindowControl, + SurfaceControl.Transaction transaction, + WindowMagnifierCallback callback, + SysUiState sysUiState, + SecureSettings secureSettings, + Supplier<SurfaceControlViewHost> scvhSupplier, + SfVsyncFrameCallbackProvider sfVsyncFrameProvider) { super( context, handler, animationController, - sfVsyncFrameProvider, mirrorWindowControl, transaction, callback, sysUiState, - WindowManagerGlobal::getWindowSession, - secureSettings); + secureSettings, + scvhSupplier, + sfVsyncFrameProvider, + WindowManagerGlobal::getWindowSession); mSpyController = Mockito.mock(WindowMagnificationController.class); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 04aef82cef43..2225ad6e49d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -68,6 +68,9 @@ import android.graphics.RegionIterator; import android.os.Handler; import android.os.RemoteException; import android.os.SystemClock; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -90,6 +93,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.LargeTest; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.kosmos.KosmosJavaAdapter; @@ -121,10 +125,13 @@ import java.util.concurrent.atomic.AtomicInteger; @LargeTest @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner.class) +@RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) public class WindowMagnificationControllerTest extends SysuiTestCase { @Rule public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000; @Mock @@ -216,13 +223,14 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { mContext, mHandler, mWindowMagnificationAnimationController, - mSfVsyncFrameProvider, mMirrorWindowControl, mTransaction, mWindowMagnifierCallback, mSysUiState, - () -> mWindowSessionSpy, - mSecureSettings); + mSecureSettings, + /* scvhSupplier= */ () -> null, + mSfVsyncFrameProvider, + /* globalWindowSessionSupplier= */ () -> mWindowSessionSpy); verify(mMirrorWindowControl).setWindowDelegate( any(MirrorWindowControl.MirrorWindowDelegate.class)); @@ -270,7 +278,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { mInstrumentation.runOnMainSync( () -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, Float.NaN, /* magnificationFrameOffsetRatioX= */ 0, - /* magnificationFrameOffsetRatioY= */ 0, null)); + /* magnificationFrameOffsetRatioY= */ 0, null)); // Waits for the surface created verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged( @@ -1415,7 +1423,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { } private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x, - float y) { + float y) { return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y); } @@ -1474,4 +1482,4 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { }); } } -} +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java new file mode 100644 index 000000000000..66fb63b6c331 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java @@ -0,0 +1,1502 @@ +/* + * Copyright (C) 2024 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.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_UP; +import static android.view.WindowInsets.Type.systemGestures; +import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; + +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItems; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalAnswers.returnsSecondArg; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.animation.ValueAnimator; +import android.annotation.IdRes; +import android.annotation.Nullable; +import android.app.Instrumentation; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Insets; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Binder; +import android.os.Handler; +import android.os.RemoteException; +import android.os.SystemClock; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableResources; +import android.text.TextUtils; +import android.util.Size; +import android.view.AttachedSurfaceControl; +import android.view.Display; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewRootImpl; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IRemoteMagnificationAnimationCallback; +import android.widget.FrameLayout; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; + +import com.android.systemui.Flags; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.AnimatorTestRule; +import com.android.systemui.kosmos.KosmosJavaAdapter; +import com.android.systemui.model.SysUiState; +import com.android.systemui.res.R; +import com.android.systemui.settings.FakeDisplayTracker; +import com.android.systemui.util.leak.ReferenceTestUtils; +import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.os.FakeHandler; + +import com.google.common.util.concurrent.AtomicDouble; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +@LargeTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +@RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) +public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase { + + @Rule + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000; + @Mock + private MirrorWindowControl mMirrorWindowControl; + @Mock + private WindowMagnifierCallback mWindowMagnifierCallback; + @Mock + IRemoteMagnificationAnimationCallback mAnimationCallback; + @Mock + IRemoteMagnificationAnimationCallback mAnimationCallback2; + + private SurfaceControl.Transaction mTransaction; + @Mock + private SecureSettings mSecureSettings; + + private long mWaitAnimationDuration; + private long mWaitBounceEffectDuration; + + private Handler mHandler; + private TestableWindowManager mWindowManager; + private SysUiState mSysUiState; + private Resources mResources; + private WindowMagnificationAnimationController mWindowMagnificationAnimationController; + private WindowMagnificationController mWindowMagnificationController; + private Instrumentation mInstrumentation; + private final ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0, 1.0f).setDuration(0); + private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); + + private View mSpyView; + private View.OnTouchListener mTouchListener; + + private MotionEventHelper mMotionEventHelper = new MotionEventHelper(); + + // This list contains all SurfaceControlViewHosts created during a given test. If the + // magnification window is recreated during a test, the list will contain more than a single + // element. + private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>(); + // The most recently created SurfaceControlViewHost. + private SurfaceControlViewHost mSurfaceControlViewHost; + private KosmosJavaAdapter mKosmos; + + /** + * return whether window magnification is supported for current test context. + */ + private boolean isWindowModeSupported() { + return getContext().getPackageManager().hasSystemFeature(FEATURE_WINDOW_MAGNIFICATION); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mKosmos = new KosmosJavaAdapter(this); + mContext = Mockito.spy(getContext()); + mHandler = new FakeHandler(TestableLooper.get(this).getLooper()); + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + final WindowManager wm = mContext.getSystemService(WindowManager.class); + mWindowManager = spy(new TestableWindowManager(wm)); + + mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); + mSysUiState = new SysUiState(mDisplayTracker, mKosmos.getSceneContainerPlugin()); + mSysUiState.addCallback(Mockito.mock(SysUiState.SysUiStateCallback.class)); + when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).then( + returnsSecondArg()); + when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then( + returnsSecondArg()); + + mResources = getContext().getOrCreateTestableResources().getResources(); + // prevent the config orientation from undefined, which may cause config.diff method + // neglecting the orientation update. + if (mResources.getConfiguration().orientation == ORIENTATION_UNDEFINED) { + mResources.getConfiguration().orientation = ORIENTATION_PORTRAIT; + } + + // Using the animation duration in WindowMagnificationAnimationController for testing. + mWaitAnimationDuration = mResources.getInteger( + com.android.internal.R.integer.config_longAnimTime); + // Using the bounce effect duration in WindowMagnificationController for testing. + mWaitBounceEffectDuration = mResources.getInteger( + com.android.internal.R.integer.config_shortAnimTime); + + mWindowMagnificationAnimationController = new WindowMagnificationAnimationController( + mContext, mValueAnimator); + Supplier<SurfaceControlViewHost> scvhSupplier = () -> { + mSurfaceControlViewHost = spy(new SurfaceControlViewHost( + mContext, mContext.getDisplay(), new Binder(), "WindowMagnification")); + ViewRootImpl viewRoot = mock(ViewRootImpl.class); + when(mSurfaceControlViewHost.getRootSurfaceControl()).thenReturn(viewRoot); + mSurfaceControlViewHosts.add(mSurfaceControlViewHost); + return mSurfaceControlViewHost; + }; + mTransaction = spy(new SurfaceControl.Transaction()); + mWindowMagnificationController = + new WindowMagnificationController( + mContext, + mHandler, + mWindowMagnificationAnimationController, + mMirrorWindowControl, + mTransaction, + mWindowMagnifierCallback, + mSysUiState, + mSecureSettings, + scvhSupplier, + /* sfVsyncFrameProvider= */ null, + /* globalWindowSessionSupplier= */ null); + + verify(mMirrorWindowControl).setWindowDelegate( + any(MirrorWindowControl.MirrorWindowDelegate.class)); + mSpyView = Mockito.spy(new View(mContext)); + doAnswer((invocation) -> { + mTouchListener = invocation.getArgument(0); + return null; + }).when(mSpyView).setOnTouchListener( + any(View.OnTouchListener.class)); + + // skip test if window magnification is not supported to prevent fail results. (b/279820875) + Assume.assumeTrue(isWindowModeSupported()); + } + + @After + public void tearDown() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.deleteWindowMagnification()); + mValueAnimator.cancel(); + } + + @Test + public void initWindowMagnificationController_checkAllowDiagonalScrollingWithSecureSettings() { + verify(mSecureSettings).getIntForUser( + eq(Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING), + /* def */ eq(1), /* userHandle= */ anyInt()); + assertTrue(mWindowMagnificationController.isDiagonalScrollingEnabled()); + } + + @Test + public void enableWindowMagnification_showControlAndNotifyBoundsChanged() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + verify(mMirrorWindowControl).showControl(); + verify(mWindowMagnifierCallback, + timeout(LAYOUT_CHANGE_TIMEOUT_MS).atLeastOnce()).onWindowMagnifierBoundsChanged( + eq(mContext.getDisplayId()), any(Rect.class)); + } + + @Test + public void enableWindowMagnification_notifySourceBoundsChanged() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, /* magnificationFrameOffsetRatioX= */ 0, + /* magnificationFrameOffsetRatioY= */ 0, null)); + + // Waits for the surface created + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged( + (eq(mContext.getDisplayId())), any()); + } + + @Test + public void enableWindowMagnification_disabled_notifySourceBoundsChanged() { + enableWindowMagnification_notifySourceBoundsChanged(); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.deleteWindowMagnification(null)); + Mockito.reset(mWindowMagnifierCallback); + + enableWindowMagnification_notifySourceBoundsChanged(); + } + + @Test + public void enableWindowMagnification_withAnimation_schedulesFrame() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(2.0f, 10, + 10, /* magnificationFrameOffsetRatioX= */ 0, + /* magnificationFrameOffsetRatioY= */ 0, + Mockito.mock(IRemoteMagnificationAnimationCallback.class)); + }); + advanceTimeBy(LAYOUT_CHANGE_TIMEOUT_MS); + + verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(), + eq(Surface.ROTATION_0)); + } + + @Test + public void moveWindowMagnifier_enabled_notifySourceBoundsChanged() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, 0, 0, null); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifier(10, 10); + }); + + final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class); + verify(mWindowMagnifierCallback, atLeast(2)).onSourceBoundsChanged( + (eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + assertEquals(mWindowMagnificationController.getCenterX(), + sourceBoundsCaptor.getValue().exactCenterX(), 0); + assertEquals(mWindowMagnificationController.getCenterY(), + sourceBoundsCaptor.getValue().exactCenterY(), 0); + } + + @Test + public void enableWindowMagnification_systemGestureExclusionRectsIsSet() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + // Wait for Rects updated. + waitForIdleSync(); + + List<Rect> rects = mSurfaceControlViewHost.getView().getSystemGestureExclusionRects(); + assertFalse(rects.isEmpty()); + } + + @Ignore("The default window size should be constrained after fixing b/288056772") + @Test + public void enableWindowMagnification_LargeScreen_windowSizeIsConstrained() { + final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10; + mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize)); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + final int halfScreenSize = screenSize / 2; + ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams(); + // The frame size should be the half of smaller value of window height/width unless it + //exceed the max frame size. + assertTrue(params.width < halfScreenSize); + assertTrue(params.height < halfScreenSize); + } + + @Test + public void deleteWindowMagnification_destroyControlAndUnregisterComponentCallback() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, + Float.NaN)); + + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.deleteWindowMagnification()); + + verify(mMirrorWindowControl).destroyControl(); + verify(mContext).unregisterComponentCallbacks(mWindowMagnificationController); + } + + @Test + public void deleteWindowMagnification_enableAtTheBottom_overlapFlagIsFalse() { + final WindowManager wm = mContext.getSystemService(WindowManager.class); + final Rect bounds = wm.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + bounds.bottom); + }); + ReferenceTestUtils.waitForCondition(this::hasMagnificationOverlapFlag); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.deleteWindowMagnification(); + }); + + verify(mMirrorWindowControl).destroyControl(); + assertFalse(hasMagnificationOverlapFlag()); + } + + @Test + public void deleteWindowMagnification_notifySourceBoundsChanged() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, + Float.NaN)); + + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.deleteWindowMagnification()); + + // The first time is for notifying magnification enabled and the second time is for + // notifying magnification disabled. + verify(mWindowMagnifierCallback, times(2)).onSourceBoundsChanged( + (eq(mContext.getDisplayId())), any()); + } + + @Test + public void moveMagnifier_schedulesFrame() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + waitForIdleSync(); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.moveWindowMagnifier(100f, 100f)); + + verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(), + eq(Surface.ROTATION_0)); + } + + @Test + public void moveWindowMagnifierToPositionWithAnimation_expectedValuesAndInvokeCallback() + throws RemoteException { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, 0, 0, null); + }); + + final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + final float targetCenterX = sourceBoundsCaptor.getValue().exactCenterX() + 10; + final float targetCenterY = sourceBoundsCaptor.getValue().exactCenterY() + 10; + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifierToPosition( + targetCenterX, targetCenterY, mAnimationCallback); + }); + advanceTimeBy(mWaitAnimationDuration); + + verify(mAnimationCallback, times(1)).onResult(eq(true)); + verify(mAnimationCallback, never()).onResult(eq(false)); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + assertEquals(mWindowMagnificationController.getCenterX(), + sourceBoundsCaptor.getValue().exactCenterX(), 0); + assertEquals(mWindowMagnificationController.getCenterY(), + sourceBoundsCaptor.getValue().exactCenterY(), 0); + assertEquals(mWindowMagnificationController.getCenterX(), targetCenterX, 0); + assertEquals(mWindowMagnificationController.getCenterY(), targetCenterY, 0); + } + + @Test + public void moveWindowMagnifierToPositionMultipleTimes_expectedValuesAndInvokeCallback() + throws RemoteException { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, 0, 0, null); + }); + + final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + final float centerX = sourceBoundsCaptor.getValue().exactCenterX(); + final float centerY = sourceBoundsCaptor.getValue().exactCenterY(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 10, centerY + 10, mAnimationCallback); + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 20, centerY + 20, mAnimationCallback); + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 30, centerY + 30, mAnimationCallback); + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 40, centerY + 40, mAnimationCallback2); + }); + advanceTimeBy(mWaitAnimationDuration); + + // only the last one callback will return true + verify(mAnimationCallback2).onResult(eq(true)); + // the others will return false + verify(mAnimationCallback, times(3)).onResult(eq(false)); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + assertEquals(mWindowMagnificationController.getCenterX(), + sourceBoundsCaptor.getValue().exactCenterX(), 0); + assertEquals(mWindowMagnificationController.getCenterY(), + sourceBoundsCaptor.getValue().exactCenterY(), 0); + assertEquals(mWindowMagnificationController.getCenterX(), centerX + 40, 0); + assertEquals(mWindowMagnificationController.getCenterY(), centerY + 40, 0); + } + + @Test + public void setScale_enabled_expectedValueAndUpdateStateDescription() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(2.0f, + Float.NaN, Float.NaN)); + + mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.setScale(3.0f)); + + assertEquals(3.0f, mWindowMagnificationController.getScale(), 0); + final View mirrorView = mSurfaceControlViewHost.getView(); + assertNotNull(mirrorView); + assertThat(mirrorView.getStateDescription().toString(), containsString("300")); + } + + @Test + public void onConfigurationChanged_disabled_withoutException() { + Display display = Mockito.spy(mContext.getDisplay()); + when(display.getRotation()).thenReturn(Surface.ROTATION_90); + when(mContext.getDisplay()).thenReturn(display); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY); + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_ORIENTATION); + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE); + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE); + }); + } + + @Test + public void onOrientationChanged_enabled_updateDisplayRotationAndCenterStayAtSamePosition() { + final int newRotation = simulateRotateTheDevice(); + final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds()); + final float center = Math.min(windowBounds.exactCenterX(), windowBounds.exactCenterY()); + final float displayWidth = windowBounds.width(); + final PointF magnifiedCenter = new PointF(center, center + 5f); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + magnifiedCenter.x, magnifiedCenter.y); + // Get the center again in case the center we set is out of screen. + magnifiedCenter.set(mWindowMagnificationController.getCenterX(), + mWindowMagnificationController.getCenterY()); + }); + // Rotate the window clockwise 90 degree. + windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom, + windowBounds.right); + mWindowManager.setWindowBounds(windowBounds); + + mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged( + ActivityInfo.CONFIG_ORIENTATION)); + + assertEquals(newRotation, mWindowMagnificationController.mRotation); + final PointF expectedCenter = new PointF(magnifiedCenter.y, + displayWidth - magnifiedCenter.x); + final PointF actualCenter = new PointF(mWindowMagnificationController.getCenterX(), + mWindowMagnificationController.getCenterY()); + assertEquals(expectedCenter, actualCenter); + } + + @Test + public void onOrientationChanged_disabled_updateDisplayRotation() { + final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds()); + // Rotate the window clockwise 90 degree. + windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom, + windowBounds.right); + mWindowManager.setWindowBounds(windowBounds); + final int newRotation = simulateRotateTheDevice(); + + mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged( + ActivityInfo.CONFIG_ORIENTATION)); + + assertEquals(newRotation, mWindowMagnificationController.mRotation); + } + + @Test + public void onScreenSizeAndDensityChanged_enabledAtTheCenterOfScreen_keepSameWindowSizeRatio() { + // The default position is at the center of the screen. + final float expectedRatio = 0.5f; + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + // Screen size and density change + mContext.getResources().getConfiguration().smallestScreenWidthDp = + mContext.getResources().getConfiguration().smallestScreenWidthDp * 2; + final Rect testWindowBounds = new Rect( + mWindowManager.getCurrentWindowMetrics().getBounds()); + testWindowBounds.set(testWindowBounds.left, testWindowBounds.top, + testWindowBounds.right + 100, testWindowBounds.bottom + 100); + mWindowManager.setWindowBounds(testWindowBounds); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE); + }); + + // The ratio of center to window size should be the same. + assertEquals(expectedRatio, + mWindowMagnificationController.getCenterX() / testWindowBounds.width(), + 0); + assertEquals(expectedRatio, + mWindowMagnificationController.getCenterY() / testWindowBounds.height(), + 0); + } + + @Test + public void onScreenChangedToSavedDensity_enabled_restoreSavedMagnifierWindow() { + mContext.getResources().getConfiguration().smallestScreenWidthDp = + mContext.getResources().getConfiguration().smallestScreenWidthDp * 2; + int windowFrameSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + mWindowMagnificationController.mWindowMagnificationSizePrefs.saveSizeForCurrentDensity( + new Size(windowFrameSize, windowFrameSize)); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams(); + assertTrue(params.width == windowFrameSize); + assertTrue(params.height == windowFrameSize); + } + + @Test + public void screenSizeIsChangedToLarge_enabled_defaultWindowSize() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10; + // Screen size and density change + mContext.getResources().getConfiguration().smallestScreenWidthDp = + mContext.getResources().getConfiguration().smallestScreenWidthDp * 2; + mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize)); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE); + }); + + final int defaultWindowSize = + mWindowMagnificationController.getMagnificationWindowSizeFromIndex( + WindowMagnificationSettings.MagnificationSize.MEDIUM); + ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams(); + + assertTrue(params.width == defaultWindowSize); + assertTrue(params.height == defaultWindowSize); + } + + @Test + public void onDensityChanged_enabled_updateDimensionsAndResetWindowMagnification() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + Mockito.reset(mWindowManager); + Mockito.reset(mMirrorWindowControl); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY); + }); + + verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt()); + verify(mSurfaceControlViewHosts.get(0)).release(); + verify(mMirrorWindowControl).destroyControl(); + verify(mSurfaceControlViewHosts.get(1)).setView(any(), any()); + verify(mMirrorWindowControl).showControl(); + } + + @Test + public void onDensityChanged_disabled_updateDimensions() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY); + }); + + verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt()); + } + + @Test + public void initializeA11yNode_enabled_expectedValues() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN, + Float.NaN); + }); + final View mirrorView = mSurfaceControlViewHost.getView(); + assertNotNull(mirrorView); + final AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo(); + + mirrorView.onInitializeAccessibilityNodeInfo(nodeInfo); + + assertNotNull(nodeInfo.getContentDescription()); + assertThat(nodeInfo.getStateDescription().toString(), containsString("250")); + assertThat(nodeInfo.getActionList(), + hasItems(new AccessibilityAction(R.id.accessibility_action_zoom_in, null), + new AccessibilityAction(R.id.accessibility_action_zoom_out, null), + new AccessibilityAction(R.id.accessibility_action_move_right, null), + new AccessibilityAction(R.id.accessibility_action_move_left, null), + new AccessibilityAction(R.id.accessibility_action_move_down, null), + new AccessibilityAction(R.id.accessibility_action_move_up, null))); + } + + @Test + public void performA11yActions_visible_expectedResults() { + final int displayId = mContext.getDisplayId(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(1.5f, Float.NaN, + Float.NaN); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + assertTrue( + mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_out, null)); + // Minimum scale is 1.0. + verify(mWindowMagnifierCallback).onPerformScaleAction( + eq(displayId), /* scale= */ eq(1.0f), /* updatePersistence= */ eq(true)); + + assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_in, null)); + verify(mWindowMagnifierCallback).onPerformScaleAction( + eq(displayId), /* scale= */ eq(2.5f), /* updatePersistence= */ eq(true)); + + // TODO: Verify the final state when the mirror surface is visible. + assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null)); + assertTrue( + mirrorView.performAccessibilityAction(R.id.accessibility_action_move_down, null)); + assertTrue( + mirrorView.performAccessibilityAction(R.id.accessibility_action_move_right, null)); + assertTrue( + mirrorView.performAccessibilityAction(R.id.accessibility_action_move_left, null)); + verify(mWindowMagnifierCallback, times(4)).onMove(eq(displayId)); + + assertTrue(mirrorView.performAccessibilityAction( + AccessibilityAction.ACTION_CLICK.getId(), null)); + verify(mWindowMagnifierCallback).onClickSettingsButton(eq(displayId)); + } + + @Test + public void performA11yActions_visible_notifyAccessibilityActionPerformed() { + final int displayId = mContext.getDisplayId(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN, + Float.NaN); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null); + + verify(mWindowMagnifierCallback).onAccessibilityActionPerformed(eq(displayId)); + } + + @Test + public void windowMagnifierEditMode_performA11yClickAction_exitEditMode() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + View closeButton = getInternalView(R.id.close_button); + View bottomRightCorner = getInternalView(R.id.bottom_right_corner); + View bottomLeftCorner = getInternalView(R.id.bottom_left_corner); + View topRightCorner = getInternalView(R.id.top_right_corner); + View topLeftCorner = getInternalView(R.id.top_left_corner); + + assertEquals(View.VISIBLE, closeButton.getVisibility()); + assertEquals(View.VISIBLE, bottomRightCorner.getVisibility()); + assertEquals(View.VISIBLE, bottomLeftCorner.getVisibility()); + assertEquals(View.VISIBLE, topRightCorner.getVisibility()); + assertEquals(View.VISIBLE, topLeftCorner.getVisibility()); + + final View mirrorView = mSurfaceControlViewHost.getView(); + mInstrumentation.runOnMainSync(() -> + mirrorView.performAccessibilityAction(AccessibilityAction.ACTION_CLICK.getId(), + null)); + + assertEquals(View.GONE, closeButton.getVisibility()); + assertEquals(View.GONE, bottomRightCorner.getVisibility()); + assertEquals(View.GONE, bottomLeftCorner.getVisibility()); + assertEquals(View.GONE, topRightCorner.getVisibility()); + assertEquals(View.GONE, topLeftCorner.getVisibility()); + } + + @Test + + public void windowWidthIsNotMax_performA11yActionIncreaseWidth_windowWidthIncreased() { + final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + final int startingWidth = (int) (windowBounds.width() * 0.8); + final int startingHeight = (int) (windowBounds.height() * 0.8); + final float changeWindowSizeAmount = mContext.getResources().getFraction( + R.fraction.magnification_resize_window_size_amount, + /* base= */ 1, + /* pbase= */ 1); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingWidth, startingHeight); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mirrorView.performAccessibilityAction( + R.id.accessibility_action_increase_window_width, null); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + final int mirrorSurfaceMargin = mResources.getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + // Window width includes the magnifier frame and the margin. Increasing the window size + // will be increasing the amount of the frame size only. + int newWindowWidth = + (int) ((startingWidth - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount)) + + 2 * mirrorSurfaceMargin; + assertEquals(newWindowWidth, actualWindowWidth.get()); + assertEquals(startingHeight, actualWindowHeight.get()); + } + + @Test + public void windowHeightIsNotMax_performA11yActionIncreaseHeight_windowHeightIncreased() { + final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + final int startingWidth = (int) (windowBounds.width() * 0.8); + final int startingHeight = (int) (windowBounds.height() * 0.8); + final float changeWindowSizeAmount = mContext.getResources().getFraction( + R.fraction.magnification_resize_window_size_amount, + /* base= */ 1, + /* pbase= */ 1); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingWidth, startingHeight); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mirrorView.performAccessibilityAction( + R.id.accessibility_action_increase_window_height, null); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + final int mirrorSurfaceMargin = mResources.getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + // Window height includes the magnifier frame and the margin. Increasing the window size + // will be increasing the amount of the frame size only. + int newWindowHeight = + (int) ((startingHeight - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount)) + + 2 * mirrorSurfaceMargin; + assertEquals(startingWidth, actualWindowWidth.get()); + assertEquals(newWindowHeight, actualWindowHeight.get()); + } + + @Test + public void windowWidthIsMax_noIncreaseWindowWidthA11yAction() { + final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + final int startingWidth = windowBounds.width(); + final int startingHeight = windowBounds.height(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingWidth, startingHeight); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AccessibilityNodeInfo accessibilityNodeInfo = + mirrorView.createAccessibilityNodeInfo(); + assertFalse(accessibilityNodeInfo.getActionList().contains( + new AccessibilityAction(R.id.accessibility_action_increase_window_width, null))); + } + + @Test + public void windowHeightIsMax_noIncreaseWindowHeightA11yAction() { + final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + final int startingWidth = windowBounds.width(); + final int startingHeight = windowBounds.height(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingWidth, startingHeight); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AccessibilityNodeInfo accessibilityNodeInfo = + mirrorView.createAccessibilityNodeInfo(); + assertFalse(accessibilityNodeInfo.getActionList().contains( + new AccessibilityAction(R.id.accessibility_action_increase_window_height, null))); + } + + @Test + public void windowWidthIsNotMin_performA11yActionDecreaseWidth_windowWidthDecreased() { + int mMinWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int startingSize = (int) (mMinWindowSize * 1.1); + final float changeWindowSizeAmount = mContext.getResources().getFraction( + R.fraction.magnification_resize_window_size_amount, + /* base= */ 1, + /* pbase= */ 1); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mirrorView.performAccessibilityAction( + R.id.accessibility_action_decrease_window_width, null); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + final int mirrorSurfaceMargin = mResources.getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + // Window width includes the magnifier frame and the margin. Decreasing the window size + // will be decreasing the amount of the frame size only. + int newWindowWidth = + (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount)) + + 2 * mirrorSurfaceMargin; + assertEquals(newWindowWidth, actualWindowWidth.get()); + assertEquals(startingSize, actualWindowHeight.get()); + } + + @Test + public void windowHeightIsNotMin_performA11yActionDecreaseHeight_windowHeightDecreased() { + int mMinWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int startingSize = (int) (mMinWindowSize * 1.1); + final float changeWindowSizeAmount = mContext.getResources().getFraction( + R.fraction.magnification_resize_window_size_amount, + /* base= */ 1, + /* pbase= */ 1); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mirrorView.performAccessibilityAction( + R.id.accessibility_action_decrease_window_height, null); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + final int mirrorSurfaceMargin = mResources.getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + // Window height includes the magnifier frame and the margin. Decreasing the window size + // will be decreasing the amount of the frame size only. + int newWindowHeight = + (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount)) + + 2 * mirrorSurfaceMargin; + assertEquals(startingSize, actualWindowWidth.get()); + assertEquals(newWindowHeight, actualWindowHeight.get()); + } + + @Test + public void windowWidthIsMin_noDecreaseWindowWidthA11yAction() { + int mMinWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int startingSize = mMinWindowSize; + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AccessibilityNodeInfo accessibilityNodeInfo = + mirrorView.createAccessibilityNodeInfo(); + assertFalse(accessibilityNodeInfo.getActionList().contains( + new AccessibilityAction(R.id.accessibility_action_decrease_window_width, null))); + } + + @Test + public void windowHeightIsMin_noDecreaseWindowHeightA11yAction() { + int mMinWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int startingSize = mMinWindowSize; + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AccessibilityNodeInfo accessibilityNodeInfo = + mirrorView.createAccessibilityNodeInfo(); + assertFalse(accessibilityNodeInfo.getActionList().contains( + new AccessibilityAction(R.id.accessibility_action_decrease_window_height, null))); + } + + @Test + public void enableWindowMagnification_hasA11yWindowTitle() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + assertEquals(getContext().getResources().getString( + com.android.internal.R.string.android_system_label), getAccessibilityWindowTitle()); + } + + @Test + public void enableWindowMagnificationWithScaleLessThanOne_enabled_disabled() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(0.9f, Float.NaN, + Float.NaN); + }); + + assertEquals(Float.NaN, mWindowMagnificationController.getScale(), 0); + } + + @Test + public void enableWindowMagnification_rotationIsChanged_updateRotationValue() { + // the config orientation should not be undefined, since it would cause config.diff + // returning 0 and thus the orientation changed would not be detected + assertNotEquals(ORIENTATION_UNDEFINED, mResources.getConfiguration().orientation); + + final Configuration config = mResources.getConfiguration(); + config.orientation = config.orientation == ORIENTATION_LANDSCAPE ? ORIENTATION_PORTRAIT + : ORIENTATION_LANDSCAPE; + final int newRotation = simulateRotateTheDevice(); + + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + assertEquals(newRotation, mWindowMagnificationController.mRotation); + } + + @Test + public void enableWindowMagnification_registerComponentCallback() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, + Float.NaN)); + + verify(mContext).registerComponentCallbacks(mWindowMagnificationController); + } + + @Test + public void onLocaleChanged_enabled_updateA11yWindowTitle() { + final String newA11yWindowTitle = "new a11y window title"; + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + final TestableResources testableResources = getContext().getOrCreateTestableResources(); + testableResources.addOverride(com.android.internal.R.string.android_system_label, + newA11yWindowTitle); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE); + }); + + assertTrue(TextUtils.equals(newA11yWindowTitle, getAccessibilityWindowTitle())); + } + + @Ignore("it's flaky in presubmit but works in abtd, filter for now. b/305654925") + @Test + public void onSingleTap_enabled_scaleAnimates() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onSingleTap(mSpyView); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + + final AtomicDouble maxScaleX = new AtomicDouble(); + advanceTimeBy(mWaitBounceEffectDuration, /* runnableOnEachRefresh= */ () -> { + // For some reason the fancy way doesn't compile... + // maxScaleX.getAndAccumulate(mirrorView.getScaleX(), Math::max); + final double oldMax = maxScaleX.get(); + final double newMax = Math.max(mirrorView.getScaleX(), oldMax); + assertTrue(maxScaleX.compareAndSet(oldMax, newMax)); + }); + + assertTrue(maxScaleX.get() > 1.0); + } + + @Test + public void moveWindowMagnificationToTheBottom_enabledWithGestureInset_overlapFlagIsTrue() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifier(0, bounds.height()); + }); + + ReferenceTestUtils.waitForCondition(() -> hasMagnificationOverlapFlag()); + } + + @Test + public void moveWindowMagnificationToRightEdge_dragHandleMovesToLeftAndUpdatesTapExcludeRegion() + throws RemoteException { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN); + }); + // Wait for Region updated. + waitForIdleSync(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.moveWindowMagnifier(bounds.width(), 0); + }); + // Wait for Region updated. + waitForIdleSync(); + + AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl(); + // Verifying two times in: (1) enable window magnification (2) reposition drag handle + verify(viewRoot, times(2)).setTouchableRegion(any()); + + View dragButton = getInternalView(R.id.drag_handle); + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams(); + assertEquals(Gravity.BOTTOM | Gravity.LEFT, params.gravity); + } + + @Test + public void moveWindowMagnificationToLeftEdge_dragHandleMovesToRightAndUpdatesTapExcludeRegion() + throws RemoteException { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN); + }); + // Wait for Region updated. + waitForIdleSync(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.moveWindowMagnifier(-bounds.width(), 0); + }); + // Wait for Region updated. + waitForIdleSync(); + + AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl(); + // Verifying one times in: (1) enable window magnification + verify(viewRoot).setTouchableRegion(any()); + + View dragButton = getInternalView(R.id.drag_handle); + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams(); + assertEquals(Gravity.BOTTOM | Gravity.RIGHT, params.gravity); + } + + @Test + public void setMinimumWindowSize_enabled_expectedWindowSize() { + final int minimumWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int expectedWindowHeight = minimumWindowSize; + final int expectedWindowWidth = minimumWindowSize; + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight); + actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width); + + }); + + assertEquals(expectedWindowHeight, actualWindowHeight.get()); + assertEquals(expectedWindowWidth, actualWindowWidth.get()); + } + + @Test + public void setMinimumWindowSizeThenEnable_expectedWindowSize() { + final int minimumWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int expectedWindowHeight = minimumWindowSize; + final int expectedWindowWidth = minimumWindowSize; + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight); + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN); + actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(expectedWindowHeight, actualWindowHeight.get()); + assertEquals(expectedWindowWidth, actualWindowWidth.get()); + } + + @Test + public void setWindowSizeLessThanMin_enabled_minimumWindowSize() { + final int minimumWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSize(minimumWindowSize - 10, + minimumWindowSize - 10); + actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(minimumWindowSize, actualWindowHeight.get()); + assertEquals(minimumWindowSize, actualWindowWidth.get()); + } + + @Test + public void setWindowSizeLargerThanScreenSize_enabled_windowSizeIsScreenSize() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSize(bounds.width() + 10, bounds.height() + 10); + actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(bounds.height(), actualWindowHeight.get()); + assertEquals(bounds.width(), actualWindowWidth.get()); + } + + @Test + public void changeMagnificationSize_expectedWindowSize() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + + final float magnificationScaleLarge = 2.5f; + final int initSize = Math.min(bounds.width(), bounds.height()) / 3; + final int magnificationSize = (int) (initSize * magnificationScaleLarge) + - (int) (initSize * magnificationScaleLarge) % 2; + + final int expectedWindowHeight = magnificationSize; + final int expectedWindowWidth = magnificationSize; + + mInstrumentation.runOnMainSync( + () -> + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.changeMagnificationSize( + WindowMagnificationSettings.MagnificationSize.LARGE); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(expectedWindowHeight, actualWindowHeight.get()); + assertEquals(expectedWindowWidth, actualWindowWidth.get()); + } + + @Test + public void editModeOnDragCorner_resizesWindow() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + + final int startingSize = (int) (bounds.width() / 2); + + mInstrumentation.runOnMainSync( + () -> + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + waitForIdleSync(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController + .onDrag(getInternalView(R.id.bottom_right_corner), 2f, 1f); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(startingSize + 1, actualWindowHeight.get()); + assertEquals(startingSize + 2, actualWindowWidth.get()); + } + + @Test + public void editModeOnDragEdge_resizesWindowInOnlyOneDirection() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + + final int startingSize = (int) (bounds.width() / 2f); + + mInstrumentation.runOnMainSync( + () -> + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + mWindowMagnificationController + .onDrag(getInternalView(R.id.bottom_handle), 2f, 1f); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + assertEquals(startingSize + 1, actualWindowHeight.get()); + assertEquals(startingSize, actualWindowWidth.get()); + } + + @Test + public void setWindowCenterOutOfScreen_enabled_magnificationCenterIsInsideTheScreen() { + + final int minimumWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + final AtomicInteger magnificationCenterX = new AtomicInteger(); + final AtomicInteger magnificationCenterY = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSizeAndCenter(minimumWindowSize, + minimumWindowSize, bounds.right, bounds.bottom); + magnificationCenterX.set((int) mWindowMagnificationController.getCenterX()); + magnificationCenterY.set((int) mWindowMagnificationController.getCenterY()); + }); + + assertTrue(magnificationCenterX.get() < bounds.right); + assertTrue(magnificationCenterY.get() < bounds.bottom); + } + + @Test + public void performSingleTap_DragHandle() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.enableWindowMagnificationInternal( + 1.5f, bounds.centerX(), bounds.centerY()); + }); + View dragButton = getInternalView(R.id.drag_handle); + + // Perform a single-tap + final long downTime = SystemClock.uptimeMillis(); + dragButton.dispatchTouchEvent( + obtainMotionEvent(downTime, 0, ACTION_DOWN, 100, 100)); + dragButton.dispatchTouchEvent( + obtainMotionEvent(downTime, downTime, ACTION_UP, 100, 100)); + + verify(mSurfaceControlViewHost).setView(any(View.class), any()); + } + + private <T extends View> T getInternalView(@IdRes int idRes) { + View mirrorView = mSurfaceControlViewHost.getView(); + T view = mirrorView.findViewById(idRes); + assertNotNull(view); + return view; + } + + private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x, + float y) { + return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y); + } + + private CharSequence getAccessibilityWindowTitle() { + final View mirrorView = mSurfaceControlViewHost.getView(); + if (mirrorView == null) { + return null; + } + WindowManager.LayoutParams layoutParams = + (WindowManager.LayoutParams) mirrorView.getLayoutParams(); + return layoutParams.accessibilityTitle; + } + + private boolean hasMagnificationOverlapFlag() { + return (mSysUiState.getFlags() & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0; + } + + private void setSystemGestureInsets() { + final WindowInsets testInsets = new WindowInsets.Builder() + .setInsets(systemGestures(), Insets.of(0, 0, 0, 10)) + .build(); + mWindowManager.setWindowInsets(testInsets); + } + + private int updateMirrorSurfaceMarginDimension() { + return mContext.getResources().getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + } + + @Surface.Rotation + private int simulateRotateTheDevice() { + final Display display = Mockito.spy(mContext.getDisplay()); + final int currentRotation = display.getRotation(); + final int newRotation = (currentRotation + 1) % 4; + when(display.getRotation()).thenReturn(newRotation); + when(mContext.getDisplay()).thenReturn(display); + return newRotation; + } + + // advance time based on the device frame refresh rate + private void advanceTimeBy(long timeDelta) { + advanceTimeBy(timeDelta, /* runnableOnEachRefresh= */ null); + } + + // advance time based on the device frame refresh rate, and trigger runnable on each refresh + private void advanceTimeBy(long timeDelta, @Nullable Runnable runnableOnEachRefresh) { + final float frameRate = mContext.getDisplay().getRefreshRate(); + final int timeSlot = (int) (1000 / frameRate); + int round = (int) Math.ceil((double) timeDelta / timeSlot); + for (; round >= 0; round--) { + mInstrumentation.runOnMainSync(() -> { + mAnimatorTestRule.advanceTimeBy(timeSlot); + if (runnableOnEachRefresh != null) { + runnableOnEachRefresh.run(); + } + }); + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 57c05396992e..63784ba61150 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -16,6 +16,7 @@ package com.android.server.accessibility; +import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON; import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER; import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER_CLIENT; @@ -5724,6 +5725,21 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @Override + public void attachAccessibilityOverlayToDisplay_enforcePermission( + int displayId, SurfaceControl sc) { + mContext.enforceCallingPermission( + INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay_enforcePermission"); + mMainHandler.sendMessage( + obtainMessage( + AccessibilityManagerService::attachAccessibilityOverlayToDisplayInternal, + this, + -1, + displayId, + sc, + null)); + } + + @Override public void attachAccessibilityOverlayToDisplay( int interactionId, int displayId, @@ -5759,12 +5775,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub t.close(); result = AccessibilityService.OVERLAY_RESULT_SUCCESS; } - // Send the result back to the service. - try { - callback.sendAttachOverlayResult(result, interactionId); - } catch (RemoteException re) { - Slog.e(LOG_TAG, "Exception while attaching overlay.", re); - // the other side will time out + + if (callback != null) { + // Send the result back to the service. + try { + callback.sendAttachOverlayResult(result, interactionId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Exception while attaching overlay.", re); + // the other side will time out + } } } } |