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