1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.accessibility;
18 
19 import static android.view.WindowInsets.Type.systemGestures;
20 import static android.view.WindowManager.LayoutParams;
21 
22 import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize;
23 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
24 
25 import static java.lang.Math.abs;
26 
27 import android.animation.ObjectAnimator;
28 import android.animation.PropertyValuesHolder;
29 import android.annotation.MainThread;
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.annotation.UiContext;
33 import android.content.ComponentCallbacks;
34 import android.content.Context;
35 import android.content.pm.ActivityInfo;
36 import android.content.res.Configuration;
37 import android.content.res.Resources;
38 import android.graphics.Insets;
39 import android.graphics.Matrix;
40 import android.graphics.PixelFormat;
41 import android.graphics.PorterDuff;
42 import android.graphics.PorterDuffColorFilter;
43 import android.graphics.Rect;
44 import android.graphics.RectF;
45 import android.graphics.Region;
46 import android.os.Build;
47 import android.os.Bundle;
48 import android.os.Handler;
49 import android.os.RemoteException;
50 import android.os.UserHandle;
51 import android.provider.Settings;
52 import android.util.Log;
53 import android.util.Range;
54 import android.util.Size;
55 import android.util.SparseArray;
56 import android.util.TypedValue;
57 import android.view.Choreographer;
58 import android.view.Display;
59 import android.view.Gravity;
60 import android.view.IWindow;
61 import android.view.IWindowSession;
62 import android.view.LayoutInflater;
63 import android.view.MotionEvent;
64 import android.view.Surface;
65 import android.view.SurfaceControl;
66 import android.view.SurfaceHolder;
67 import android.view.SurfaceView;
68 import android.view.View;
69 import android.view.WindowManager;
70 import android.view.WindowManagerGlobal;
71 import android.view.WindowMetrics;
72 import android.view.accessibility.AccessibilityNodeInfo;
73 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
74 import android.view.accessibility.IRemoteMagnificationAnimationCallback;
75 import android.widget.FrameLayout;
76 import android.widget.ImageView;
77 
78 import androidx.core.math.MathUtils;
79 
80 import com.android.internal.accessibility.common.MagnificationConstants;
81 import com.android.internal.annotations.VisibleForTesting;
82 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
83 import com.android.systemui.R;
84 import com.android.systemui.model.SysUiState;
85 import com.android.systemui.util.settings.SecureSettings;
86 
87 import java.io.PrintWriter;
88 import java.text.NumberFormat;
89 import java.util.Collections;
90 import java.util.Locale;
91 import java.util.function.Supplier;
92 
93 /**
94  * Class to handle adding and removing a window magnification.
95  */
96 class WindowMagnificationController implements View.OnTouchListener, SurfaceHolder.Callback,
97         MirrorWindowControl.MirrorWindowDelegate, MagnificationGestureDetector.OnGestureListener,
98         ComponentCallbacks {
99 
100     private static final String TAG = "WindowMagnificationController";
101     @SuppressWarnings("isloggabletaglength")
102     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE;
103     // Delay to avoid updating state description too frequently.
104     private static final int UPDATE_STATE_DESCRIPTION_DELAY_MS = 100;
105     // It should be consistent with the value defined in WindowMagnificationGestureHandler.
106     private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(
107             MagnificationConstants.SCALE_MIN_VALUE,
108             MagnificationConstants.SCALE_MAX_VALUE);
109     private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f;
110     private static final float ANIMATION_BOUNCE_EFFECT_SCALE = 1.05f;
111     private final SparseArray<Float> mMagnificationSizeScaleOptions = new SparseArray<>();
112 
113     private final Context mContext;
114     private final Resources mResources;
115     private final Handler mHandler;
116     private final Rect mWindowBounds;
117     private final int mDisplayId;
118     @Surface.Rotation
119     @VisibleForTesting
120     int mRotation;
121     private final SurfaceControl.Transaction mTransaction;
122 
123     private final WindowManager mWm;
124 
125     private float mScale;
126 
127     /**
128      * MagnificationFrame represents the bound of {@link #mMirrorSurface} and is constrained
129      * by the {@link #mMagnificationFrameBoundary}.
130      * We use MagnificationFrame to calculate the position of {@link #mMirrorView}.
131      * We combine MagnificationFrame with {@link #mMagnificationFrameOffsetX} and
132      * {@link #mMagnificationFrameOffsetY} to calculate the position of {@link #mSourceBounds}.
133      */
134     private final Rect mMagnificationFrame = new Rect();
135     private final Rect mTmpRect = new Rect();
136 
137     /**
138      * MirrorViewBounds is the bound of the {@link #mMirrorView} which displays the magnified
139      * content.
140      * {@link #mMirrorView}'s center is equal to {@link #mMagnificationFrame}'s center.
141      */
142     private final Rect mMirrorViewBounds = new Rect();
143 
144     /**
145      * SourceBound is the bound of the magnified region which projects the magnified content.
146      * SourceBound's center is equal to the parameters centerX and centerY in
147      * {@link WindowMagnificationController#enableWindowMagnificationInternal(float, float, float)}}
148      * but it is calculated from {@link #mMagnificationFrame}'s center in the runtime.
149      */
150     private final Rect mSourceBounds = new Rect();
151 
152     /**
153      * The relation of centers between {@link #mSourceBounds} and {@link #mMagnificationFrame} is
154      * calculated in {@link #calculateSourceBounds(Rect, float)} and the equations are as following:
155      *      MagnificationFrame = SourceBound (e.g., centerX & centerY) + MagnificationFrameOffset
156      *      SourceBound = MagnificationFrame - MagnificationFrameOffset
157      */
158     private int mMagnificationFrameOffsetX = 0;
159     private int mMagnificationFrameOffsetY = 0;
160 
161     // The root of the mirrored content
162     private SurfaceControl mMirrorSurface;
163 
164     private ImageView mDragView;
165     private ImageView mCloseView;
166     private View mLeftDrag;
167     private View mTopDrag;
168     private View mRightDrag;
169     private View mBottomDrag;
170     private ImageView mTopLeftCornerView;
171     private ImageView mTopRightCornerView;
172     private ImageView mBottomLeftCornerView;
173     private ImageView mBottomRightCornerView;
174     private final Configuration mConfiguration;
175 
176     @NonNull
177     private final WindowMagnifierCallback mWindowMagnifierCallback;
178 
179     private final View.OnLayoutChangeListener mMirrorViewLayoutChangeListener;
180     private final View.OnLayoutChangeListener mMirrorSurfaceViewLayoutChangeListener;
181     private final Runnable mMirrorViewRunnable;
182     private final Runnable mUpdateStateDescriptionRunnable;
183     private final Runnable mWindowInsetChangeRunnable;
184     // MirrorView is the mirror window which displays the magnified content.
185     private View mMirrorView;
186     private View mMirrorBorderView;
187     private SurfaceView mMirrorSurfaceView;
188     private int mMirrorSurfaceMargin;
189     private int mBorderDragSize;
190     private int mOuterBorderSize;
191 
192     /**
193      * How far from the right edge of the screen you need to drag the window before the button
194      * repositions to the other side.
195      */
196     private int mButtonRepositionThresholdFromEdge;
197 
198     // The boundary of magnification frame.
199     private final Rect mMagnificationFrameBoundary = new Rect();
200     // The top Y of the system gesture rect at the bottom. Set to -1 if it is invalid.
201     private int mSystemGestureTop = -1;
202     private int mMinWindowSize;
203 
204     private final WindowMagnificationAnimationController mAnimationController;
205     private final Supplier<IWindowSession> mGlobalWindowSessionSupplier;
206     private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
207     private final MagnificationGestureDetector mGestureDetector;
208     private final int mBounceEffectDuration;
209     private final Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback;
210     private Locale mLocale;
211     private NumberFormat mPercentFormat;
212     private float mBounceEffectAnimationScale;
213     private final SysUiState mSysUiState;
214     // Set it to true when the view is overlapped with the gesture insets at the bottom.
215     private boolean mOverlapWithGestureInsets;
216     private boolean mIsDragging;
217 
218     private static final int MAX_HORIZONTAL_MOVE_ANGLE = 50;
219     private static final int HORIZONTAL = 1;
220     private static final int VERTICAL = 0;
221 
222     @VisibleForTesting
223     static final double HORIZONTAL_LOCK_BASE =
224             Math.tan(Math.toRadians(MAX_HORIZONTAL_MOVE_ANGLE));
225 
226     private boolean mAllowDiagonalScrolling = false;
227     private boolean mEditSizeEnable = false;
228     private boolean mSettingsPanelVisibility = false;
229 
230     @Nullable
231     private final MirrorWindowControl mMirrorWindowControl;
232 
WindowMagnificationController( @iContext 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)233     WindowMagnificationController(
234             @UiContext Context context,
235             @NonNull Handler handler,
236             @NonNull WindowMagnificationAnimationController animationController,
237             SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
238             MirrorWindowControl mirrorWindowControl,
239             SurfaceControl.Transaction transaction,
240             @NonNull WindowMagnifierCallback callback,
241             SysUiState sysUiState,
242             @NonNull Supplier<IWindowSession> globalWindowSessionSupplier,
243             SecureSettings secureSettings) {
244         mContext = context;
245         mHandler = handler;
246         mAnimationController = animationController;
247         mGlobalWindowSessionSupplier = globalWindowSessionSupplier;
248         mAnimationController.setWindowMagnificationController(this);
249         mSfVsyncFrameProvider = sfVsyncFrameProvider;
250         mWindowMagnifierCallback = callback;
251         mSysUiState = sysUiState;
252         mConfiguration = new Configuration(context.getResources().getConfiguration());
253 
254         final Display display = mContext.getDisplay();
255         mDisplayId = mContext.getDisplayId();
256         mRotation = display.getRotation();
257 
258         mWm = context.getSystemService(WindowManager.class);
259         mWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds());
260 
261         mResources = mContext.getResources();
262         mScale = secureSettings.getFloatForUser(
263                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
264                 mResources.getInteger(R.integer.magnification_default_scale),
265                 UserHandle.USER_CURRENT);
266 
267         setupMagnificationSizeScaleOptions();
268 
269         mBounceEffectDuration = mResources.getInteger(
270                 com.android.internal.R.integer.config_shortAnimTime);
271         updateDimensions();
272 
273         final Size windowSize = getDefaultWindowSizeWithWindowBounds(mWindowBounds);
274         setMagnificationFrame(windowSize.getWidth(), windowSize.getHeight(),
275                 mWindowBounds.width() / 2, mWindowBounds.height() / 2);
276         computeBounceAnimationScale();
277 
278         mMirrorWindowControl = mirrorWindowControl;
279         if (mMirrorWindowControl != null) {
280             mMirrorWindowControl.setWindowDelegate(this);
281         }
282         mTransaction = transaction;
283         mGestureDetector =
284                 new MagnificationGestureDetector(mContext, handler, this);
285 
286         // Initialize listeners.
287         mMirrorViewRunnable = () -> {
288             if (mMirrorView != null) {
289                 final Rect oldViewBounds = new Rect(mMirrorViewBounds);
290                 mMirrorView.getBoundsOnScreen(mMirrorViewBounds);
291                 if (oldViewBounds.width() != mMirrorViewBounds.width()
292                         || oldViewBounds.height() != mMirrorViewBounds.height()) {
293                     mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
294                             new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height())));
295                 }
296                 updateSystemUIStateIfNeeded();
297                 mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
298                         mDisplayId, mMirrorViewBounds);
299             }
300         };
301         mMirrorViewLayoutChangeListener =
302                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
303                     if (!mHandler.hasCallbacks(mMirrorViewRunnable)) {
304                         mHandler.post(mMirrorViewRunnable);
305                     }
306                 };
307 
308         mMirrorSurfaceViewLayoutChangeListener =
309                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
310                         mMirrorView.post(this::applyTapExcludeRegion);
311 
312         mMirrorViewGeometryVsyncCallback =
313                 l -> {
314                     if (isActivated() && mMirrorSurface != null && calculateSourceBounds(
315                             mMagnificationFrame, mScale)) {
316                         // The final destination for the magnification surface should be at 0,0
317                         // since the ViewRootImpl's position will change
318                         mTmpRect.set(0, 0, mMagnificationFrame.width(),
319                                 mMagnificationFrame.height());
320                         mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect,
321                                 Surface.ROTATION_0).apply();
322 
323                         // Notify source bounds change when the magnifier is not animating.
324                         if (!mAnimationController.isAnimating()) {
325                             mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId,
326                                     mSourceBounds);
327                         }
328                     }
329                 };
330         mUpdateStateDescriptionRunnable = () -> {
331             if (isActivated()) {
332                 mMirrorView.setStateDescription(formatStateDescription(mScale));
333             }
334         };
335         mWindowInsetChangeRunnable = this::onWindowInsetChanged;
336     }
337 
setupMagnificationSizeScaleOptions()338     private void setupMagnificationSizeScaleOptions() {
339         mMagnificationSizeScaleOptions.clear();
340         mMagnificationSizeScaleOptions.put(MagnificationSize.SMALL, 1.4f);
341         mMagnificationSizeScaleOptions.put(MagnificationSize.MEDIUM, 1.8f);
342         mMagnificationSizeScaleOptions.put(MagnificationSize.LARGE, 2.5f);
343     }
344 
updateDimensions()345     private void updateDimensions() {
346         mMirrorSurfaceMargin = mResources.getDimensionPixelSize(
347                 R.dimen.magnification_mirror_surface_margin);
348         mBorderDragSize = mResources.getDimensionPixelSize(
349                 R.dimen.magnification_border_drag_size);
350         mOuterBorderSize = mResources.getDimensionPixelSize(
351                 R.dimen.magnification_outer_border_margin);
352         mButtonRepositionThresholdFromEdge =
353                 mResources.getDimensionPixelSize(
354                         R.dimen.magnification_button_reposition_threshold_from_edge);
355         mMinWindowSize = mResources.getDimensionPixelSize(
356                 com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
357     }
358 
computeBounceAnimationScale()359     private void computeBounceAnimationScale() {
360         final float windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
361         final float visibleWindowWidth = windowWidth - 2 * mOuterBorderSize;
362         final float animationScaleMax = windowWidth / visibleWindowWidth;
363         mBounceEffectAnimationScale = Math.min(animationScaleMax, ANIMATION_BOUNCE_EFFECT_SCALE);
364     }
365 
updateSystemGestureInsetsTop()366     private boolean updateSystemGestureInsetsTop() {
367         final WindowMetrics windowMetrics = mWm.getCurrentWindowMetrics();
368         final Insets insets = windowMetrics.getWindowInsets().getInsets(systemGestures());
369         final int gestureTop =
370                 insets.bottom != 0 ? windowMetrics.getBounds().bottom - insets.bottom : -1;
371         if (gestureTop != mSystemGestureTop) {
372             mSystemGestureTop = gestureTop;
373             return true;
374         }
375         return false;
376     }
377 
changeMagnificationSize(@agnificationSize int index)378     void changeMagnificationSize(@MagnificationSize int index) {
379         if (!mMagnificationSizeScaleOptions.contains(index)) {
380             return;
381         }
382         final float scale = mMagnificationSizeScaleOptions.get(index, 1.0f);
383         final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3;
384         int size = (int) (initSize * scale);
385         setWindowSize(size, size);
386     }
387 
setEditMagnifierSizeMode(boolean enable)388     void setEditMagnifierSizeMode(boolean enable) {
389         mEditSizeEnable = enable;
390         applyResourcesValues();
391 
392         if (isActivated()) {
393             updateDimensions();
394             applyTapExcludeRegion();
395         }
396     }
397 
setDiagonalScrolling(boolean enable)398     void setDiagonalScrolling(boolean enable) {
399         mAllowDiagonalScrolling = enable;
400     }
401 
402     /**
403      * Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition
404      * animation. If the window magnification is enabling, it runs the animation in reverse.
405      *
406      * @param animationCallback Called when the transition is complete, the given arguments
407      *                          are as same as current values, or the transition is interrupted
408      *                          due to the new transition request.
409      */
deleteWindowMagnification( @ullable IRemoteMagnificationAnimationCallback animationCallback)410     void deleteWindowMagnification(
411             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
412         mAnimationController.deleteWindowMagnification(animationCallback);
413     }
414 
415     /**
416      * Deletes the magnification window.
417      */
deleteWindowMagnification()418     void deleteWindowMagnification() {
419         if (!isActivated()) {
420             return;
421         }
422 
423         if (mMirrorSurface != null) {
424             mTransaction.remove(mMirrorSurface).apply();
425             mMirrorSurface = null;
426         }
427 
428         if (mMirrorSurfaceView != null) {
429             mMirrorSurfaceView.removeOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener);
430         }
431 
432         if (mMirrorView != null) {
433             mHandler.removeCallbacks(mMirrorViewRunnable);
434             mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
435             mWm.removeView(mMirrorView);
436             mMirrorView = null;
437         }
438 
439         if (mMirrorWindowControl != null) {
440             mMirrorWindowControl.destroyControl();
441         }
442         mMirrorViewBounds.setEmpty();
443         mSourceBounds.setEmpty();
444         updateSystemUIStateIfNeeded();
445         setEditMagnifierSizeMode(false);
446 
447         mContext.unregisterComponentCallbacks(this);
448         // Notify source bounds empty when magnification is deleted.
449         mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, new Rect());
450     }
451 
452     @Override
onConfigurationChanged(@onNull Configuration newConfig)453     public void onConfigurationChanged(@NonNull Configuration newConfig) {
454         final int configDiff = newConfig.diff(mConfiguration);
455         mConfiguration.setTo(newConfig);
456         onConfigurationChanged(configDiff);
457     }
458 
459     @Override
onLowMemory()460     public void onLowMemory() {
461     }
462 
463     /**
464      * Called when the configuration has changed, and it updates window magnification UI.
465      *
466      * @param configDiff a bit mask of the differences between the configurations
467      */
onConfigurationChanged(int configDiff)468     void onConfigurationChanged(int configDiff) {
469         if (DEBUG) {
470             Log.d(TAG, "onConfigurationChanged = " + Configuration.configurationDiffToString(
471                     configDiff));
472         }
473         if (configDiff == 0) {
474             return;
475         }
476         if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) {
477             onRotate();
478         }
479 
480         if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) {
481             updateAccessibilityWindowTitleIfNeeded();
482         }
483 
484         boolean reCreateWindow = false;
485         if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) {
486             updateDimensions();
487             computeBounceAnimationScale();
488             reCreateWindow = true;
489         }
490 
491         if ((configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
492             reCreateWindow |= handleScreenSizeChanged();
493         }
494 
495         // Recreate the window again to correct the window appearance due to density or
496         // window size changed not caused by rotation.
497         if (isActivated() && reCreateWindow) {
498             deleteWindowMagnification();
499             enableWindowMagnificationInternal(Float.NaN, Float.NaN, Float.NaN);
500         }
501     }
502 
503     /**
504      * Calculates the magnification frame if the window bounds is changed.
505      * Note that the orientation also changes the wind bounds, so it should be handled first.
506      *
507      * @return {@code true} if the magnification frame is changed with the new window bounds.
508      */
handleScreenSizeChanged()509     private boolean handleScreenSizeChanged() {
510         final Rect oldWindowBounds = new Rect(mWindowBounds);
511         final Rect currentWindowBounds = mWm.getCurrentWindowMetrics().getBounds();
512 
513         if (currentWindowBounds.equals(oldWindowBounds)) {
514             if (DEBUG) {
515                 Log.d(TAG, "handleScreenSizeChanged -- window bounds is not changed");
516             }
517             return false;
518         }
519         mWindowBounds.set(currentWindowBounds);
520         final Size windowSize = getDefaultWindowSizeWithWindowBounds(mWindowBounds);
521         final float newCenterX = (getCenterX()) * mWindowBounds.width() / oldWindowBounds.width();
522         final float newCenterY = (getCenterY()) * mWindowBounds.height() / oldWindowBounds.height();
523 
524         setMagnificationFrame(windowSize.getWidth(), windowSize.getHeight(), (int) newCenterX,
525                 (int) newCenterY);
526         calculateMagnificationFrameBoundary();
527         return true;
528     }
529 
updateSystemUIStateIfNeeded()530     private void updateSystemUIStateIfNeeded() {
531         updateSysUIState(false);
532     }
533 
updateAccessibilityWindowTitleIfNeeded()534     private void updateAccessibilityWindowTitleIfNeeded() {
535         if (!isActivated()) return;
536         LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams();
537         params.accessibilityTitle = getAccessibilityWindowTitle();
538         mWm.updateViewLayout(mMirrorView, params);
539     }
540 
541     /**
542      * Keep MirrorWindow position on the screen unchanged when device rotates 90° clockwise or
543      * anti-clockwise.
544      */
onRotate()545     private void onRotate() {
546         final Display display = mContext.getDisplay();
547         final int oldRotation = mRotation;
548         mRotation = display.getRotation();
549         final int rotationDegree = getDegreeFromRotation(mRotation, oldRotation);
550         if (rotationDegree == 0 || rotationDegree == 180) {
551             Log.w(TAG, "onRotate -- rotate with the device. skip it");
552             return;
553         }
554         final Rect currentWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds());
555         if (currentWindowBounds.width() != mWindowBounds.height()
556                 || currentWindowBounds.height() != mWindowBounds.width()) {
557             Log.w(TAG, "onRotate -- unexpected window height/width");
558             return;
559         }
560 
561         mWindowBounds.set(currentWindowBounds);
562 
563         // Keep MirrorWindow position on the screen unchanged when device rotates 90°
564         // clockwise or anti-clockwise.
565 
566         final Matrix matrix = new Matrix();
567         matrix.setRotate(rotationDegree);
568         if (rotationDegree == 90) {
569             matrix.postTranslate(mWindowBounds.width(), 0);
570         } else if (rotationDegree == 270) {
571             matrix.postTranslate(0, mWindowBounds.height());
572         }
573 
574         final RectF transformedRect = new RectF(mMagnificationFrame);
575         // The window frame is going to be transformed by the rotation matrix.
576         transformedRect.inset(-mMirrorSurfaceMargin, -mMirrorSurfaceMargin);
577         matrix.mapRect(transformedRect);
578         setWindowSizeAndCenter((int) transformedRect.width(), (int) transformedRect.height(),
579                 (int) transformedRect.centerX(), (int) transformedRect.centerY());
580     }
581 
582     /** Returns the rotation degree change of two {@link Surface.Rotation} */
getDegreeFromRotation(@urface.Rotation int newRotation, @Surface.Rotation int oldRotation)583     private int getDegreeFromRotation(@Surface.Rotation int newRotation,
584                                       @Surface.Rotation int oldRotation) {
585         return (oldRotation - newRotation + 4) % 4 * 90;
586     }
587 
createMirrorWindow()588     private void createMirrorWindow() {
589         // The window should be the size the mirrored surface will be but also add room for the
590         // border and the drag handle.
591         int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
592         int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
593 
594         LayoutParams params = new LayoutParams(
595                 windowWidth, windowHeight,
596                 LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
597                 LayoutParams.FLAG_NOT_TOUCH_MODAL
598                         | LayoutParams.FLAG_NOT_FOCUSABLE,
599                 PixelFormat.TRANSPARENT);
600         params.gravity = Gravity.TOP | Gravity.LEFT;
601         params.x = mMagnificationFrame.left - mMirrorSurfaceMargin;
602         params.y = mMagnificationFrame.top - mMirrorSurfaceMargin;
603         params.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
604         params.receiveInsetsIgnoringZOrder = true;
605         params.setTitle(mContext.getString(R.string.magnification_window_title));
606         params.accessibilityTitle = getAccessibilityWindowTitle();
607 
608         mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null);
609         mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view);
610 
611         mMirrorBorderView = mMirrorView.findViewById(R.id.magnification_inner_border);
612 
613         // Allow taps to go through to the mirror SurfaceView below.
614         mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener);
615 
616         mMirrorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
617                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
618                 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
619                 | View.SYSTEM_UI_FLAG_FULLSCREEN
620                 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
621                 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
622         mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
623         mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate());
624         mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> {
625             if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) {
626                 mHandler.post(mWindowInsetChangeRunnable);
627             }
628             return v.onApplyWindowInsets(insets);
629         });
630 
631         mWm.addView(mMirrorView, params);
632 
633         SurfaceHolder holder = mMirrorSurfaceView.getHolder();
634         holder.addCallback(this);
635         holder.setFormat(PixelFormat.RGBA_8888);
636         addDragTouchListeners();
637     }
638 
onWindowInsetChanged()639     private void onWindowInsetChanged() {
640         if (updateSystemGestureInsetsTop()) {
641             updateSystemUIStateIfNeeded();
642         }
643     }
644 
applyTapExcludeRegion()645     private void applyTapExcludeRegion() {
646         // Sometimes this can get posted and run after deleteWindowMagnification() is called.
647         if (mMirrorView == null) return;
648 
649         final Region tapExcludeRegion = calculateTapExclude();
650         final IWindow window = IWindow.Stub.asInterface(mMirrorView.getWindowToken());
651         try {
652             IWindowSession session = mGlobalWindowSessionSupplier.get();
653             session.updateTapExcludeRegion(window, tapExcludeRegion);
654         } catch (RemoteException e) {
655         }
656     }
657 
calculateTapExclude()658     private Region calculateTapExclude() {
659         Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize,
660                 mMirrorView.getWidth() - mBorderDragSize,
661                 mMirrorView.getHeight() - mBorderDragSize);
662 
663         Region tapExcludeRegion = new Region();
664 
665         Rect dragArea = new Rect();
666         mDragView.getHitRect(dragArea);
667 
668         Rect topLeftArea = new Rect();
669         mTopLeftCornerView.getHitRect(topLeftArea);
670 
671         Rect topRightArea = new Rect();
672         mTopRightCornerView.getHitRect(topRightArea);
673 
674         Rect bottomLeftArea = new Rect();
675         mBottomLeftCornerView.getHitRect(bottomLeftArea);
676 
677         Rect bottomRightArea = new Rect();
678         mBottomRightCornerView.getHitRect(bottomRightArea);
679 
680         Rect closeArea = new Rect();
681         mCloseView.getHitRect(closeArea);
682 
683         // add tapExcludeRegion for Drag or close
684         tapExcludeRegion.op(dragArea, Region.Op.UNION);
685         tapExcludeRegion.op(topLeftArea, Region.Op.UNION);
686         tapExcludeRegion.op(topRightArea, Region.Op.UNION);
687         tapExcludeRegion.op(bottomLeftArea, Region.Op.UNION);
688         tapExcludeRegion.op(bottomRightArea, Region.Op.UNION);
689         tapExcludeRegion.op(closeArea, Region.Op.UNION);
690 
691         regionInsideDragBorder.op(tapExcludeRegion, Region.Op.DIFFERENCE);
692 
693         return regionInsideDragBorder;
694     }
695 
getAccessibilityWindowTitle()696     private String getAccessibilityWindowTitle() {
697         return mResources.getString(com.android.internal.R.string.android_system_label);
698     }
699 
showControls()700     private void showControls() {
701         if (mMirrorWindowControl != null) {
702             mMirrorWindowControl.showControl();
703         }
704     }
705 
706     /**
707      * Sets the window frame size with given width and height in pixels without changing the
708      * window center.
709      *
710      * @param width the window frame width in pixels
711      * @param height the window frame height in pixels.
712      */
713     @MainThread
setMagnificationFrameSize(int width, int height)714     private void setMagnificationFrameSize(int width, int height) {
715         setWindowSize(width + 2 * mMirrorSurfaceMargin, height + 2 * mMirrorSurfaceMargin);
716     }
717 
718     /**
719      * Sets the window size with given width and height in pixels without changing the
720      * window center. The width or the height will be clamped in the range
721      * [{@link #mMinWindowSize}, screen width or height].
722      *
723      * @param width the window width in pixels
724      * @param height the window height in pixels.
725      */
setWindowSize(int width, int height)726     public void setWindowSize(int width, int height) {
727         setWindowSizeAndCenter(width, height, Float.NaN, Float.NaN);
728     }
729 
setWindowSizeAndCenter(int width, int height, float centerX, float centerY)730     void setWindowSizeAndCenter(int width, int height, float centerX, float centerY) {
731         width = MathUtils.clamp(width, mMinWindowSize, mWindowBounds.width());
732         height = MathUtils.clamp(height, mMinWindowSize, mWindowBounds.height());
733 
734         if (Float.isNaN(centerX)) {
735             centerX = mMagnificationFrame.centerX();
736         }
737         if (Float.isNaN(centerY)) {
738             centerY = mMagnificationFrame.centerY();
739         }
740 
741         final int frameWidth = width - 2 * mMirrorSurfaceMargin;
742         final int frameHeight = height - 2 * mMirrorSurfaceMargin;
743         setMagnificationFrame(frameWidth, frameHeight, (int) centerX, (int) centerY);
744         calculateMagnificationFrameBoundary();
745         // Correct the frame position to ensure it is inside the boundary.
746         updateMagnificationFramePosition(0, 0);
747         modifyWindowMagnification(true);
748     }
749 
setMagnificationFrame(int width, int height, int centerX, int centerY)750     private void setMagnificationFrame(int width, int height, int centerX, int centerY) {
751         // Sets the initial frame area for the mirror and place it to the given center on the
752         // display.
753         final int initX = centerX - width / 2;
754         final int initY = centerY - height / 2;
755         mMagnificationFrame.set(initX, initY, initX + width, initY + height);
756     }
757 
getDefaultWindowSizeWithWindowBounds(Rect windowBounds)758     private Size getDefaultWindowSizeWithWindowBounds(Rect windowBounds) {
759         int initSize = Math.min(windowBounds.width(), windowBounds.height()) / 2;
760         initSize = Math.min(mResources.getDimensionPixelSize(R.dimen.magnification_max_frame_size),
761                 initSize);
762         initSize += 2 * mMirrorSurfaceMargin;
763         return new Size(initSize, initSize);
764     }
765 
766     /**
767      * This is called once the surfaceView is created so the mirrored content can be placed as a
768      * child of the surfaceView.
769      */
createMirror()770     private void createMirror() {
771         mMirrorSurface = mirrorDisplay(mDisplayId);
772         if (!mMirrorSurface.isValid()) {
773             return;
774         }
775         mTransaction.show(mMirrorSurface)
776                 .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl());
777         modifyWindowMagnification(false);
778     }
779 
780     /**
781      * Mirrors a specified display. The SurfaceControl returned is the root of the mirrored
782      * hierarchy.
783      *
784      * @param displayId The id of the display to mirror
785      * @return The SurfaceControl for the root of the mirrored hierarchy.
786      */
mirrorDisplay(final int displayId)787     private SurfaceControl mirrorDisplay(final int displayId) {
788         try {
789             SurfaceControl outSurfaceControl = new SurfaceControl();
790             WindowManagerGlobal.getWindowManagerService().mirrorDisplay(displayId,
791                     outSurfaceControl);
792             return outSurfaceControl;
793         } catch (RemoteException e) {
794             Log.e(TAG, "Unable to reach window manager", e);
795         }
796         return null;
797     }
798 
addDragTouchListeners()799     private void addDragTouchListeners() {
800         mDragView = mMirrorView.findViewById(R.id.drag_handle);
801         mLeftDrag = mMirrorView.findViewById(R.id.left_handle);
802         mTopDrag = mMirrorView.findViewById(R.id.top_handle);
803         mRightDrag = mMirrorView.findViewById(R.id.right_handle);
804         mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle);
805         mCloseView = mMirrorView.findViewById(R.id.close_button);
806         mTopRightCornerView = mMirrorView.findViewById(R.id.top_right_corner);
807         mTopLeftCornerView = mMirrorView.findViewById(R.id.top_left_corner);
808         mBottomRightCornerView = mMirrorView.findViewById(R.id.bottom_right_corner);
809         mBottomLeftCornerView = mMirrorView.findViewById(R.id.bottom_left_corner);
810 
811         mDragView.setOnTouchListener(this);
812         mLeftDrag.setOnTouchListener(this);
813         mTopDrag.setOnTouchListener(this);
814         mRightDrag.setOnTouchListener(this);
815         mBottomDrag.setOnTouchListener(this);
816         mCloseView.setOnTouchListener(this);
817         mTopLeftCornerView.setOnTouchListener(this);
818         mTopRightCornerView.setOnTouchListener(this);
819         mBottomLeftCornerView.setOnTouchListener(this);
820         mBottomRightCornerView.setOnTouchListener(this);
821     }
822 
823     /**
824      * Modifies the placement of the mirrored content when the position or size of mMirrorView is
825      * updated.
826      *
827      * @param computeWindowSize set to {@code true} to compute window size with
828      * {@link #mMagnificationFrame}.
829      */
modifyWindowMagnification(boolean computeWindowSize)830     private void modifyWindowMagnification(boolean computeWindowSize) {
831         mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback);
832         updateMirrorViewLayout(computeWindowSize);
833     }
834 
835     /**
836      * Updates the layout params of MirrorView based on the size of {@link #mMagnificationFrame}
837      * and translates MirrorView position when the view is moved close to the screen edges;
838      *
839      * @param computeWindowSize set to {@code true} to compute window size with
840      * {@link #mMagnificationFrame}.
841      */
updateMirrorViewLayout(boolean computeWindowSize)842     private void updateMirrorViewLayout(boolean computeWindowSize) {
843         if (!isActivated()) {
844             return;
845         }
846         final int maxMirrorViewX = mWindowBounds.width() - mMirrorView.getWidth();
847         final int maxMirrorViewY = mWindowBounds.height() - mMirrorView.getHeight();
848 
849         LayoutParams params =
850                 (LayoutParams) mMirrorView.getLayoutParams();
851         params.x = mMagnificationFrame.left - mMirrorSurfaceMargin;
852         params.y = mMagnificationFrame.top - mMirrorSurfaceMargin;
853         if (computeWindowSize) {
854             params.width = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
855             params.height = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
856         }
857 
858         // Translates MirrorView position to make MirrorSurfaceView that is inside MirrorView
859         // able to move close to the screen edges.
860         final float translationX;
861         final float translationY;
862         if (params.x < 0) {
863             translationX = Math.max(params.x, -mOuterBorderSize);
864         } else if (params.x > maxMirrorViewX) {
865             translationX = Math.min(params.x - maxMirrorViewX, mOuterBorderSize);
866         } else {
867             translationX = 0;
868         }
869         if (params.y < 0) {
870             translationY = Math.max(params.y, -mOuterBorderSize);
871         } else if (params.y > maxMirrorViewY) {
872             translationY = Math.min(params.y - maxMirrorViewY, mOuterBorderSize);
873         } else {
874             translationY = 0;
875         }
876         mMirrorView.setTranslationX(translationX);
877         mMirrorView.setTranslationY(translationY);
878         mWm.updateViewLayout(mMirrorView, params);
879 
880         // If they are not dragging the handle, we can move the drag handle immediately without
881         // disruption. But if they are dragging it, we avoid moving until the end of the drag.
882         if (!mIsDragging) {
883             mMirrorView.post(this::maybeRepositionButton);
884         }
885     }
886 
887     @Override
onTouch(View v, MotionEvent event)888     public boolean onTouch(View v, MotionEvent event) {
889         if (v == mDragView
890                 || v == mLeftDrag
891                 || v == mTopDrag
892                 || v == mRightDrag
893                 || v == mBottomDrag
894                 || v == mTopLeftCornerView
895                 || v == mTopRightCornerView
896                 || v == mBottomLeftCornerView
897                 || v == mBottomRightCornerView
898                 || v == mCloseView) {
899             return mGestureDetector.onTouch(v, event);
900         }
901         return false;
902     }
903 
updateSysUIStateFlag()904     public void updateSysUIStateFlag() {
905         updateSysUIState(true);
906     }
907 
908     /**
909      * Calculates the desired source bounds. This will be the area under from the center of  the
910      * displayFrame, factoring in scale.
911      *
912      * @return {@code true} if the source bounds is changed.
913      */
calculateSourceBounds(Rect displayFrame, float scale)914     private boolean calculateSourceBounds(Rect displayFrame, float scale) {
915         final Rect oldSourceBounds = mTmpRect;
916         oldSourceBounds.set(mSourceBounds);
917         int halfWidth = displayFrame.width() / 2;
918         int halfHeight = displayFrame.height() / 2;
919         int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale));
920         int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale));
921         int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale));
922         int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale));
923 
924         mSourceBounds.set(left, top, right, bottom);
925 
926         // SourceBound's center is equal to center[X,Y] but calculated from MagnificationFrame's
927         // center. The relation between SourceBound and MagnificationFrame is as following:
928         //          MagnificationFrame = SourceBound (center[X,Y]) + MagnificationFrameOffset
929         //          SourceBound = MagnificationFrame - MagnificationFrameOffset
930         mSourceBounds.offset(-mMagnificationFrameOffsetX, -mMagnificationFrameOffsetY);
931 
932         if (mSourceBounds.left < 0) {
933             mSourceBounds.offsetTo(0, mSourceBounds.top);
934         } else if (mSourceBounds.right > mWindowBounds.width()) {
935             mSourceBounds.offsetTo(mWindowBounds.width() - mSourceBounds.width(),
936                     mSourceBounds.top);
937         }
938 
939         if (mSourceBounds.top < 0) {
940             mSourceBounds.offsetTo(mSourceBounds.left, 0);
941         } else if (mSourceBounds.bottom > mWindowBounds.height()) {
942             mSourceBounds.offsetTo(mSourceBounds.left,
943                     mWindowBounds.height() - mSourceBounds.height());
944         }
945         return !mSourceBounds.equals(oldSourceBounds);
946     }
947 
calculateMagnificationFrameBoundary()948     private void calculateMagnificationFrameBoundary() {
949         // Calculates width and height for magnification frame could exceed out the screen.
950         // TODO : re-calculating again when scale is changed.
951         // The half width of magnification frame.
952         final int halfWidth = mMagnificationFrame.width() / 2;
953         // The half height of magnification frame.
954         final int halfHeight = mMagnificationFrame.height() / 2;
955         // The scaled half width of magnified region.
956         final int scaledWidth = (int) (halfWidth / mScale);
957         // The scaled half height of magnified region.
958         final int scaledHeight = (int) (halfHeight / mScale);
959 
960         // MagnificationFrameBoundary constrain the space of MagnificationFrame, and it also has
961         // to leave enough space for SourceBound to magnify the whole screen space.
962         // However, there is an offset between SourceBound and MagnificationFrame.
963         // The relation between SourceBound and MagnificationFrame is as following:
964         //      SourceBound = MagnificationFrame - MagnificationFrameOffset
965         // Therefore, we have to adjust the exceededBoundary based on the offset.
966         //
967         // We have to increase the offset space for the SourceBound edges which are located in
968         // the MagnificationFrame. For example, if the offsetX and offsetY are negative, which
969         // means SourceBound is at right-bottom size of MagnificationFrame, the left and top
970         // edges of SourceBound are located in MagnificationFrame. So, we have to leave extra
971         // offset space at left and top sides and don't have to leave extra space at right and
972         // bottom sides.
973         final int exceededLeft = Math.max(halfWidth - scaledWidth - mMagnificationFrameOffsetX, 0);
974         final int exceededRight = Math.max(halfWidth - scaledWidth + mMagnificationFrameOffsetX, 0);
975         final int exceededTop = Math.max(halfHeight - scaledHeight - mMagnificationFrameOffsetY, 0);
976         final int exceededBottom = Math.max(halfHeight - scaledHeight + mMagnificationFrameOffsetY,
977                 0);
978 
979         mMagnificationFrameBoundary.set(
980                 -exceededLeft,
981                 -exceededTop,
982                 mWindowBounds.width() + exceededRight,
983                 mWindowBounds.height() + exceededBottom);
984     }
985 
986     /**
987      * Calculates and sets the real position of magnification frame based on the magnified region
988      * should be limited by the region of the display.
989      */
updateMagnificationFramePosition(int xOffset, int yOffset)990     private boolean updateMagnificationFramePosition(int xOffset, int yOffset) {
991         mTmpRect.set(mMagnificationFrame);
992         mTmpRect.offset(xOffset, yOffset);
993 
994         if (mTmpRect.left < mMagnificationFrameBoundary.left) {
995             mTmpRect.offsetTo(mMagnificationFrameBoundary.left, mTmpRect.top);
996         } else if (mTmpRect.right > mMagnificationFrameBoundary.right) {
997             final int leftOffset = mMagnificationFrameBoundary.right - mMagnificationFrame.width();
998             mTmpRect.offsetTo(leftOffset, mTmpRect.top);
999         }
1000 
1001         if (mTmpRect.top < mMagnificationFrameBoundary.top) {
1002             mTmpRect.offsetTo(mTmpRect.left, mMagnificationFrameBoundary.top);
1003         } else if (mTmpRect.bottom > mMagnificationFrameBoundary.bottom) {
1004             final int topOffset = mMagnificationFrameBoundary.bottom - mMagnificationFrame.height();
1005             mTmpRect.offsetTo(mTmpRect.left, topOffset);
1006         }
1007 
1008         if (!mTmpRect.equals(mMagnificationFrame)) {
1009             mMagnificationFrame.set(mTmpRect);
1010             return true;
1011         }
1012         return false;
1013     }
1014 
updateSysUIState(boolean force)1015     private void updateSysUIState(boolean force) {
1016         final boolean overlap = isActivated() && mSystemGestureTop > 0
1017                 && mMirrorViewBounds.bottom > mSystemGestureTop;
1018         if (force || overlap != mOverlapWithGestureInsets) {
1019             mOverlapWithGestureInsets = overlap;
1020             mSysUiState.setFlag(SYSUI_STATE_MAGNIFICATION_OVERLAP, mOverlapWithGestureInsets)
1021                     .commitUpdate(mDisplayId);
1022         }
1023     }
1024 
1025     @Override
surfaceCreated(SurfaceHolder holder)1026     public void surfaceCreated(SurfaceHolder holder) {
1027         createMirror();
1028     }
1029 
1030     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)1031     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
1032     }
1033 
1034     @Override
surfaceDestroyed(SurfaceHolder holder)1035     public void surfaceDestroyed(SurfaceHolder holder) {
1036     }
1037 
1038     @Override
move(int xOffset, int yOffset)1039     public void move(int xOffset, int yOffset) {
1040         moveWindowMagnifier(xOffset, yOffset);
1041         mWindowMagnifierCallback.onMove(mDisplayId);
1042     }
1043 
1044     /**
1045      * Wraps {@link WindowMagnificationController#enableWindowMagnificationInternal(float, float,
1046      * float, float, float)}
1047      * with transition animation. If the window magnification is not enabled, the scale will start
1048      * from 1.0 and the center won't be changed during the animation. If animator is
1049      * {@code STATE_DISABLING}, the animation runs in reverse.
1050      *
1051      * @param scale   The target scale, or {@link Float#NaN} to leave unchanged.
1052      * @param centerX The screen-relative X coordinate around which to center for magnification,
1053      *                or {@link Float#NaN} to leave unchanged.
1054      * @param centerY The screen-relative Y coordinate around which to center for magnification,
1055      *                or {@link Float#NaN} to leave unchanged.
1056      * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset
1057      *                                       between frame position X and centerX
1058      * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset
1059      *                                       between frame position Y and centerY
1060      * @param animationCallback Called when the transition is complete, the given arguments
1061      *                          are as same as current values, or the transition is interrupted
1062      *                          due to the new transition request.
1063      */
enableWindowMagnification(float scale, float centerX, float centerY, float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)1064     public void enableWindowMagnification(float scale, float centerX, float centerY,
1065             float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY,
1066             @Nullable IRemoteMagnificationAnimationCallback animationCallback) {
1067         mAnimationController.enableWindowMagnification(scale, centerX, centerY,
1068                 magnificationFrameOffsetRatioX, magnificationFrameOffsetRatioY, animationCallback);
1069     }
1070 
1071     /**
1072      * Enables window magnification with specified parameters. If the given scale is <strong>less
1073      * than or equal to 1.0f<strong>, then
1074      * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to
1075      * be consistent with the behavior of display magnification.
1076      *
1077      * @param scale   the target scale, or {@link Float#NaN} to leave unchanged
1078      * @param centerX the screen-relative X coordinate around which to center for magnification,
1079      *                or {@link Float#NaN} to leave unchanged.
1080      * @param centerY the screen-relative Y coordinate around which to center for magnification,
1081      *                or {@link Float#NaN} to leave unchanged.
1082      */
enableWindowMagnificationInternal(float scale, float centerX, float centerY)1083     void enableWindowMagnificationInternal(float scale, float centerX, float centerY) {
1084         enableWindowMagnificationInternal(scale, centerX, centerY, Float.NaN, Float.NaN);
1085     }
1086 
1087     /**
1088      * Enables window magnification with specified parameters. If the given scale is <strong>less
1089      * than 1.0f<strong>, then
1090      * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to
1091      * be consistent with the behavior of display magnification.
1092      *
1093      * @param scale   the target scale, or {@link Float#NaN} to leave unchanged
1094      * @param centerX the screen-relative X coordinate around which to center for magnification,
1095      *                or {@link Float#NaN} to leave unchanged.
1096      * @param centerY the screen-relative Y coordinate around which to center for magnification,
1097      *                or {@link Float#NaN} to leave unchanged.
1098      * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset
1099      *                                       between frame position X and centerX,
1100      *                                       or {@link Float#NaN} to leave unchanged.
1101      * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset
1102      *                                       between frame position Y and centerY,
1103      *                                       or {@link Float#NaN} to leave unchanged.
1104      */
enableWindowMagnificationInternal(float scale, float centerX, float centerY, float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY)1105     void enableWindowMagnificationInternal(float scale, float centerX, float centerY,
1106                 float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY) {
1107         if (Float.compare(scale, 1.0f) < 0) {
1108             deleteWindowMagnification();
1109             return;
1110         }
1111         if (!isActivated()) {
1112             onConfigurationChanged(mResources.getConfiguration());
1113             mContext.registerComponentCallbacks(this);
1114         }
1115 
1116         mMagnificationFrameOffsetX = Float.isNaN(magnificationFrameOffsetRatioX)
1117                 ? mMagnificationFrameOffsetX
1118                 : (int) (mMagnificationFrame.width() / 2 * magnificationFrameOffsetRatioX);
1119         mMagnificationFrameOffsetY = Float.isNaN(magnificationFrameOffsetRatioY)
1120                 ? mMagnificationFrameOffsetY
1121                 : (int) (mMagnificationFrame.height() / 2 * magnificationFrameOffsetRatioY);
1122 
1123         // The relation of centers between SourceBound and MagnificationFrame is as following:
1124         // MagnificationFrame = SourceBound (e.g., centerX & centerY) + MagnificationFrameOffset
1125         final float newMagnificationFrameCenterX = centerX + mMagnificationFrameOffsetX;
1126         final float newMagnificationFrameCenterY = centerY + mMagnificationFrameOffsetY;
1127 
1128         final float offsetX = Float.isNaN(centerX) ? 0
1129                 : newMagnificationFrameCenterX - mMagnificationFrame.exactCenterX();
1130         final float offsetY = Float.isNaN(centerY) ? 0
1131                 : newMagnificationFrameCenterY - mMagnificationFrame.exactCenterY();
1132         mScale = Float.isNaN(scale) ? mScale : scale;
1133 
1134         calculateMagnificationFrameBoundary();
1135         updateMagnificationFramePosition((int) offsetX, (int) offsetY);
1136         if (!isActivated()) {
1137             createMirrorWindow();
1138             showControls();
1139             applyResourcesValues();
1140         } else {
1141             modifyWindowMagnification(false);
1142         }
1143     }
1144 
1145     // The magnifier is activated when the window is visible,
1146     // and the window is visible when it is existed.
isActivated()1147     boolean isActivated() {
1148         return mMirrorView != null;
1149     }
1150 
1151     /**
1152      * Sets the scale of the magnified region if it's visible.
1153      *
1154      * @param scale the target scale, or {@link Float#NaN} to leave unchanged
1155      */
setScale(float scale)1156     void setScale(float scale) {
1157         if (mAnimationController.isAnimating() || !isActivated() || mScale == scale) {
1158             return;
1159         }
1160 
1161         enableWindowMagnificationInternal(scale, Float.NaN, Float.NaN);
1162         mHandler.removeCallbacks(mUpdateStateDescriptionRunnable);
1163         mHandler.postDelayed(mUpdateStateDescriptionRunnable, UPDATE_STATE_DESCRIPTION_DELAY_MS);
1164     }
1165 
1166     /**
1167      * Moves the window magnifier with specified offset in pixels unit.
1168      *
1169      * @param offsetX the amount in pixels to offset the window magnifier in the X direction, in
1170      *                current screen pixels.
1171      * @param offsetY the amount in pixels to offset the window magnifier in the Y direction, in
1172      *                current screen pixels.
1173      */
moveWindowMagnifier(float offsetX, float offsetY)1174     void moveWindowMagnifier(float offsetX, float offsetY) {
1175         if (mAnimationController.isAnimating() || mMirrorSurfaceView == null) {
1176             return;
1177         }
1178 
1179         if (!mAllowDiagonalScrolling) {
1180             int direction = selectDirectionForMove(abs(offsetX), abs(offsetY));
1181 
1182             if (direction == HORIZONTAL) {
1183                 offsetY = 0;
1184             } else {
1185                 offsetX = 0;
1186             }
1187         }
1188 
1189         if (updateMagnificationFramePosition((int) offsetX, (int) offsetY)) {
1190             modifyWindowMagnification(false);
1191         }
1192     }
1193 
moveWindowMagnifierToPosition(float positionX, float positionY, IRemoteMagnificationAnimationCallback callback)1194     void moveWindowMagnifierToPosition(float positionX, float positionY,
1195                                        IRemoteMagnificationAnimationCallback callback) {
1196         if (mMirrorSurfaceView == null) {
1197             return;
1198         }
1199         mAnimationController.moveWindowMagnifierToPosition(positionX, positionY, callback);
1200     }
1201 
selectDirectionForMove(float diffX, float diffY)1202     private int selectDirectionForMove(float diffX, float diffY) {
1203         int direction = 0;
1204         float result = diffY / diffX;
1205 
1206         if (result <= HORIZONTAL_LOCK_BASE) {
1207             direction = HORIZONTAL;  // horizontal move
1208         } else {
1209             direction = VERTICAL;  // vertical move
1210         }
1211         return direction;
1212     }
1213 
1214     /**
1215      * Gets the scale.
1216      *
1217      * @return {@link Float#NaN} if the window is invisible.
1218      */
getScale()1219     float getScale() {
1220         return isActivated() ? mScale : Float.NaN;
1221     }
1222 
1223     /**
1224      * Returns the screen-relative X coordinate of the center of the magnified bounds.
1225      *
1226      * @return the X coordinate. {@link Float#NaN} if the window is invisible.
1227      */
getCenterX()1228     float getCenterX() {
1229         return isActivated() ? mMagnificationFrame.exactCenterX() : Float.NaN;
1230     }
1231 
1232     /**
1233      * Returns the screen-relative Y coordinate of the center of the magnified bounds.
1234      *
1235      * @return the Y coordinate. {@link Float#NaN} if the window is invisible.
1236      */
getCenterY()1237     float getCenterY() {
1238         return isActivated() ? mMagnificationFrame.exactCenterY() : Float.NaN;
1239     }
1240 
formatStateDescription(float scale)1241     private CharSequence formatStateDescription(float scale) {
1242         // Cache the locale-appropriate NumberFormat.  Configuration locale is guaranteed
1243         // non-null, so the first time this is called we will always get the appropriate
1244         // NumberFormat, then never regenerate it unless the locale changes on the fly.
1245         final Locale curLocale = mContext.getResources().getConfiguration().getLocales().get(0);
1246         if (!curLocale.equals(mLocale)) {
1247             mLocale = curLocale;
1248             mPercentFormat = NumberFormat.getPercentInstance(curLocale);
1249         }
1250         return mPercentFormat.format(scale);
1251     }
1252 
1253     @Override
onSingleTap(View view)1254     public boolean onSingleTap(View view) {
1255         handleSingleTap(view);
1256         return true;
1257     }
1258 
1259     @Override
onDrag(View view, float offsetX, float offsetY)1260     public boolean onDrag(View view, float offsetX, float offsetY) {
1261         if (mEditSizeEnable) {
1262             return changeWindowSize(view, offsetX, offsetY);
1263         } else {
1264             move((int) offsetX, (int) offsetY);
1265         }
1266         return true;
1267     }
1268 
handleSingleTap(View view)1269     private void handleSingleTap(View view) {
1270         int id = view.getId();
1271         if (id == R.id.drag_handle) {
1272             mWindowMagnifierCallback.onClickSettingsButton(mDisplayId);
1273         } else if (id == R.id.close_button) {
1274             setEditMagnifierSizeMode(false);
1275         } else {
1276             animateBounceEffect();
1277         }
1278     }
1279 
applyResourcesValues()1280     private void applyResourcesValues() {
1281         // Sets the border appearance for the magnifier window
1282         mMirrorBorderView.setBackground(mResources.getDrawable(mEditSizeEnable
1283                 ? R.drawable.accessibility_window_magnification_background_change
1284                 : R.drawable.accessibility_window_magnification_background));
1285 
1286         // Changes the corner radius of the mMirrorSurfaceView
1287         mMirrorSurfaceView.setCornerRadius(
1288                         TypedValue.applyDimension(
1289                                 TypedValue.COMPLEX_UNIT_DIP,
1290                                 mEditSizeEnable ? 16f : 28f,
1291                                 mContext.getResources().getDisplayMetrics()));
1292 
1293         // Sets visibility of components for the magnifier window
1294         if (mEditSizeEnable) {
1295             mDragView.setVisibility(View.GONE);
1296             mCloseView.setVisibility(View.VISIBLE);
1297             mTopRightCornerView.setVisibility(View.VISIBLE);
1298             mTopLeftCornerView.setVisibility(View.VISIBLE);
1299             mBottomRightCornerView.setVisibility(View.VISIBLE);
1300             mBottomLeftCornerView.setVisibility(View.VISIBLE);
1301         } else {
1302             mDragView.setVisibility(View.VISIBLE);
1303             mCloseView.setVisibility(View.GONE);
1304             mTopRightCornerView.setVisibility(View.GONE);
1305             mTopLeftCornerView.setVisibility(View.GONE);
1306             mBottomRightCornerView.setVisibility(View.GONE);
1307             mBottomLeftCornerView.setVisibility(View.GONE);
1308         }
1309     }
1310 
changeWindowSize(View view, float offsetX, float offsetY)1311     private boolean changeWindowSize(View view, float offsetX, float offsetY) {
1312         if (view == mLeftDrag) {
1313             changeMagnificationFrameSize(offsetX, 0, 0, 0);
1314         } else if (view == mRightDrag) {
1315             changeMagnificationFrameSize(0, 0, offsetX, 0);
1316         } else if (view == mTopDrag) {
1317             changeMagnificationFrameSize(0, offsetY, 0, 0);
1318         } else if (view == mBottomDrag) {
1319             changeMagnificationFrameSize(0, 0, 0, offsetY);
1320         } else if (view == mTopLeftCornerView) {
1321             changeMagnificationFrameSize(offsetX, offsetY, 0, 0);
1322         } else if (view == mTopRightCornerView) {
1323             changeMagnificationFrameSize(0, offsetY, offsetX, 0);
1324         } else if (view == mBottomLeftCornerView) {
1325             changeMagnificationFrameSize(offsetX, 0, 0, offsetY);
1326         } else if (view == mBottomRightCornerView) {
1327             changeMagnificationFrameSize(0, 0, offsetX, offsetY);
1328         } else {
1329             return false;
1330         }
1331 
1332         return true;
1333     }
1334 
changeMagnificationFrameSize( float leftOffset, float topOffset, float rightOffset, float bottomOffset)1335     private void changeMagnificationFrameSize(
1336             float leftOffset, float topOffset, float rightOffset,
1337             float bottomOffset) {
1338         boolean bRTL = isRTL(mContext);
1339         final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3;
1340 
1341         final int maxHeightSize = mWindowBounds.height() - 2 * mMirrorSurfaceMargin;
1342         final int maxWidthSize = mWindowBounds.width() - 2 * mMirrorSurfaceMargin;
1343 
1344         Rect tempRect = new Rect();
1345         tempRect.set(mMagnificationFrame);
1346 
1347         if (bRTL) {
1348             tempRect.left += (int) (rightOffset);
1349             tempRect.right += (int) (leftOffset);
1350         } else {
1351             tempRect.right += (int) (rightOffset);
1352             tempRect.left += (int) (leftOffset);
1353         }
1354         tempRect.top += (int) (topOffset);
1355         tempRect.bottom += (int) (bottomOffset);
1356 
1357         if (tempRect.width() < initSize || tempRect.height() < initSize
1358                 || tempRect.width() > maxWidthSize || tempRect.height() > maxHeightSize) {
1359             return;
1360         }
1361         mMagnificationFrame.set(tempRect);
1362 
1363         computeBounceAnimationScale();
1364         calculateMagnificationFrameBoundary();
1365 
1366         modifyWindowMagnification(true);
1367     }
1368 
isRTL(Context context)1369     private static boolean isRTL(Context context) {
1370         Configuration config = context.getResources().getConfiguration();
1371         if (config == null) {
1372             return false;
1373         }
1374         return (config.screenLayout & Configuration.SCREENLAYOUT_LAYOUTDIR_MASK)
1375                 == Configuration.SCREENLAYOUT_LAYOUTDIR_RTL;
1376     }
1377 
1378     @Override
onStart(float x, float y)1379     public boolean onStart(float x, float y) {
1380         mIsDragging = true;
1381         return true;
1382     }
1383 
1384     @Override
onFinish(float x, float y)1385     public boolean onFinish(float x, float y) {
1386         maybeRepositionButton();
1387         mIsDragging = false;
1388         return false;
1389     }
1390 
1391     /** Moves the button to the opposite edge if the frame is against the edge of the screen. */
maybeRepositionButton()1392     private void maybeRepositionButton() {
1393         if (mMirrorView == null) return;
1394 
1395         final float screenEdgeX = mWindowBounds.right - mButtonRepositionThresholdFromEdge;
1396         final FrameLayout.LayoutParams layoutParams =
1397                 (FrameLayout.LayoutParams) mDragView.getLayoutParams();
1398 
1399         mMirrorView.getBoundsOnScreen(mTmpRect);
1400 
1401         final int newGravity;
1402         if (mTmpRect.right >= screenEdgeX) {
1403             newGravity = Gravity.BOTTOM | Gravity.LEFT;
1404         } else {
1405             newGravity = Gravity.BOTTOM | Gravity.RIGHT;
1406         }
1407         if (newGravity != layoutParams.gravity) {
1408             layoutParams.gravity = newGravity;
1409             mDragView.setLayoutParams(layoutParams);
1410             mDragView.post(this::applyTapExcludeRegion);
1411         }
1412     }
1413 
updateDragHandleResourcesIfNeeded(boolean settingsPanelIsShown)1414     void updateDragHandleResourcesIfNeeded(boolean settingsPanelIsShown) {
1415         if (!isActivated()) {
1416             return;
1417         }
1418 
1419         mSettingsPanelVisibility = settingsPanelIsShown;
1420 
1421         mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown
1422                 ? R.drawable.accessibility_window_magnification_drag_handle_background_change
1423                 : R.drawable.accessibility_window_magnification_drag_handle_background));
1424 
1425         PorterDuffColorFilter filter = new PorterDuffColorFilter(
1426                 mContext.getColor(settingsPanelIsShown
1427                         ? R.color.magnification_border_color
1428                         : R.color.magnification_drag_handle_stroke),
1429                 PorterDuff.Mode.SRC_ATOP);
1430 
1431         mDragView.setColorFilter(filter);
1432     }
1433 
animateBounceEffect()1434     private void animateBounceEffect() {
1435         final ObjectAnimator scaleAnimator = ObjectAnimator.ofPropertyValuesHolder(mMirrorView,
1436                 PropertyValuesHolder.ofFloat(View.SCALE_X, 1, mBounceEffectAnimationScale, 1),
1437                 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1, mBounceEffectAnimationScale, 1));
1438         scaleAnimator.setDuration(mBounceEffectDuration);
1439         scaleAnimator.start();
1440     }
1441 
dump(PrintWriter pw)1442     public void dump(PrintWriter pw) {
1443         pw.println("WindowMagnificationController (displayId=" + mDisplayId + "):");
1444         pw.println("      mOverlapWithGestureInsets:" + mOverlapWithGestureInsets);
1445         pw.println("      mScale:" + mScale);
1446         pw.println("      mWindowBounds:" + mWindowBounds);
1447         pw.println("      mMirrorViewBounds:" + (isActivated() ? mMirrorViewBounds : "empty"));
1448         pw.println("      mMagnificationFrameBoundary:"
1449                 + (isActivated() ? mMagnificationFrameBoundary : "empty"));
1450         pw.println("      mMagnificationFrame:"
1451                 + (isActivated() ? mMagnificationFrame : "empty"));
1452         pw.println("      mSourceBounds:"
1453                 + (mSourceBounds.isEmpty() ? "empty" : mSourceBounds));
1454         pw.println("      mSystemGestureTop:" + mSystemGestureTop);
1455         pw.println("      mMagnificationFrameOffsetX:" + mMagnificationFrameOffsetX);
1456         pw.println("      mMagnificationFrameOffsetY:" + mMagnificationFrameOffsetY);
1457     }
1458 
1459     private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate {
1460 
getClickAccessibilityActionLabel()1461         private CharSequence getClickAccessibilityActionLabel() {
1462             if (mEditSizeEnable) {
1463                 // Perform click action to exit edit mode
1464                 return mContext.getResources().getString(
1465                         R.string.magnification_exit_edit_mode_click_label);
1466             }
1467 
1468             return mSettingsPanelVisibility
1469                     ? mContext.getResources().getString(
1470                             R.string.magnification_close_settings_click_label)
1471                     : mContext.getResources().getString(
1472                             R.string.magnification_open_settings_click_label);
1473         }
1474 
1475         @Override
onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info)1476         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
1477             super.onInitializeAccessibilityNodeInfo(host, info);
1478             final AccessibilityAction clickAction = new AccessibilityAction(
1479                     AccessibilityAction.ACTION_CLICK.getId(), getClickAccessibilityActionLabel());
1480             info.addAction(clickAction);
1481             info.setClickable(true);
1482 
1483             info.addAction(
1484                     new AccessibilityAction(R.id.accessibility_action_zoom_in,
1485                             mContext.getString(R.string.accessibility_control_zoom_in)));
1486             info.addAction(new AccessibilityAction(R.id.accessibility_action_zoom_out,
1487                     mContext.getString(R.string.accessibility_control_zoom_out)));
1488 
1489             if (!mEditSizeEnable) {
1490                 info.addAction(new AccessibilityAction(R.id.accessibility_action_move_up,
1491                         mContext.getString(R.string.accessibility_control_move_up)));
1492                 info.addAction(new AccessibilityAction(R.id.accessibility_action_move_down,
1493                         mContext.getString(R.string.accessibility_control_move_down)));
1494                 info.addAction(new AccessibilityAction(R.id.accessibility_action_move_left,
1495                         mContext.getString(R.string.accessibility_control_move_left)));
1496                 info.addAction(new AccessibilityAction(R.id.accessibility_action_move_right,
1497                         mContext.getString(R.string.accessibility_control_move_right)));
1498             } else {
1499                 if ((mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin)
1500                         < mWindowBounds.width()) {
1501                     info.addAction(new AccessibilityAction(
1502                             R.id.accessibility_action_increase_window_width,
1503                             mContext.getString(
1504                                     R.string.accessibility_control_increase_window_width)));
1505                 }
1506                 if ((mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin)
1507                         < mWindowBounds.height()) {
1508                     info.addAction(new AccessibilityAction(
1509                             R.id.accessibility_action_increase_window_height,
1510                             mContext.getString(
1511                                     R.string.accessibility_control_increase_window_height)));
1512                 }
1513                 if ((mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin) > mMinWindowSize) {
1514                     info.addAction(new AccessibilityAction(
1515                             R.id.accessibility_action_decrease_window_width,
1516                             mContext.getString(
1517                                     R.string.accessibility_control_decrease_window_width)));
1518                 }
1519                 if ((mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin) > mMinWindowSize) {
1520                     info.addAction(new AccessibilityAction(
1521                             R.id.accessibility_action_decrease_window_height,
1522                             mContext.getString(
1523                                     R.string.accessibility_control_decrease_window_height)));
1524                 }
1525             }
1526 
1527             info.setContentDescription(mContext.getString(R.string.magnification_window_title));
1528             info.setStateDescription(formatStateDescription(getScale()));
1529         }
1530 
1531         @Override
performAccessibilityAction(View host, int action, Bundle args)1532         public boolean performAccessibilityAction(View host, int action, Bundle args) {
1533             if (performA11yAction(action)) {
1534                 return true;
1535             }
1536             return super.performAccessibilityAction(host, action, args);
1537         }
1538 
performA11yAction(int action)1539         private boolean performA11yAction(int action) {
1540             final float changeWindowSizeAmount = mContext.getResources().getFraction(
1541                     R.fraction.magnification_resize_window_size_amount,
1542                     /* base= */ 1,
1543                     /* pbase= */ 1);
1544 
1545             if (action == AccessibilityAction.ACTION_CLICK.getId()) {
1546                 if (mEditSizeEnable) {
1547                     // When edit mode is enabled, click the magnifier to exit edit mode.
1548                     setEditMagnifierSizeMode(false);
1549                 } else {
1550                     // Simulate tapping the drag view so it opens the Settings.
1551                     handleSingleTap(mDragView);
1552                 }
1553 
1554             } else if (action == R.id.accessibility_action_zoom_in) {
1555                 performScale(mScale + A11Y_CHANGE_SCALE_DIFFERENCE);
1556             } else if (action == R.id.accessibility_action_zoom_out) {
1557                 performScale(mScale - A11Y_CHANGE_SCALE_DIFFERENCE);
1558             } else if (action == R.id.accessibility_action_move_up) {
1559                 move(0, -mSourceBounds.height());
1560             } else if (action == R.id.accessibility_action_move_down) {
1561                 move(0, mSourceBounds.height());
1562             } else if (action == R.id.accessibility_action_move_left) {
1563                 move(-mSourceBounds.width(), 0);
1564             } else if (action == R.id.accessibility_action_move_right) {
1565                 move(mSourceBounds.width(), 0);
1566             } else if (action == R.id.accessibility_action_increase_window_width) {
1567                 int newFrameWidth =
1568                         (int) (mMagnificationFrame.width() * (1 + changeWindowSizeAmount));
1569                 setMagnificationFrameSize(newFrameWidth, mMagnificationFrame.height());
1570             } else if (action == R.id.accessibility_action_increase_window_height) {
1571                 int newFrameHeight =
1572                         (int) (mMagnificationFrame.height() * (1 + changeWindowSizeAmount));
1573                 setMagnificationFrameSize(mMagnificationFrame.width(), newFrameHeight);
1574             } else if (action == R.id.accessibility_action_decrease_window_width) {
1575                 int newFrameWidth =
1576                         (int) (mMagnificationFrame.width() * (1 - changeWindowSizeAmount));
1577                 setMagnificationFrameSize(newFrameWidth, mMagnificationFrame.height());
1578             } else if (action == R.id.accessibility_action_decrease_window_height) {
1579                 int newFrameHeight =
1580                         (int) (mMagnificationFrame.height() * (1 - changeWindowSizeAmount));
1581                 setMagnificationFrameSize(mMagnificationFrame.width(), newFrameHeight);
1582             } else {
1583                 return false;
1584             }
1585 
1586             mWindowMagnifierCallback.onAccessibilityActionPerformed(mDisplayId);
1587             return true;
1588         }
1589 
performScale(float scale)1590         private void performScale(float scale) {
1591             scale = A11Y_ACTION_SCALE_RANGE.clamp(scale);
1592             mWindowMagnifierCallback.onPerformScaleAction(
1593                     mDisplayId, scale, /* updatePersistence= */ true);
1594         }
1595     }
1596 
1597 }