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.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP;
23 
24 import android.animation.ObjectAnimator;
25 import android.animation.PropertyValuesHolder;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.annotation.UiContext;
29 import android.content.Context;
30 import android.content.pm.ActivityInfo;
31 import android.content.res.Configuration;
32 import android.content.res.Resources;
33 import android.graphics.Insets;
34 import android.graphics.Matrix;
35 import android.graphics.PixelFormat;
36 import android.graphics.Rect;
37 import android.graphics.RectF;
38 import android.graphics.Region;
39 import android.os.Build;
40 import android.os.Bundle;
41 import android.os.Handler;
42 import android.os.RemoteException;
43 import android.util.Log;
44 import android.util.Range;
45 import android.view.Choreographer;
46 import android.view.Display;
47 import android.view.Gravity;
48 import android.view.IWindow;
49 import android.view.IWindowSession;
50 import android.view.LayoutInflater;
51 import android.view.MotionEvent;
52 import android.view.Surface;
53 import android.view.SurfaceControl;
54 import android.view.SurfaceHolder;
55 import android.view.SurfaceView;
56 import android.view.View;
57 import android.view.WindowManager;
58 import android.view.WindowManagerGlobal;
59 import android.view.WindowMetrics;
60 import android.view.accessibility.AccessibilityNodeInfo;
61 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
62 
63 import com.android.internal.annotations.VisibleForTesting;
64 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
65 import com.android.systemui.R;
66 import com.android.systemui.model.SysUiState;
67 import com.android.systemui.shared.system.WindowManagerWrapper;
68 
69 import java.io.PrintWriter;
70 import java.text.NumberFormat;
71 import java.util.Collections;
72 import java.util.Locale;
73 
74 /**
75  * Class to handle adding and removing a window magnification.
76  */
77 class WindowMagnificationController implements View.OnTouchListener, SurfaceHolder.Callback,
78         MirrorWindowControl.MirrorWindowDelegate, MagnificationGestureDetector.OnGestureListener {
79 
80     private static final String TAG = "WindowMagnificationController";
81     @SuppressWarnings("isloggabletaglength")
82     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE;
83     // Delay to avoid updating state description too frequently.
84     private static final int UPDATE_STATE_DESCRIPTION_DELAY_MS = 100;
85     // It should be consistent with the value defined in WindowMagnificationGestureHandler.
86     private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(2.0f, 8.0f);
87     private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f;
88     private static final float ANIMATION_BOUNCE_EFFECT_SCALE = 1.05f;
89     private final Context mContext;
90     private final Resources mResources;
91     private final Handler mHandler;
92     private Rect mWindowBounds;
93     private final int mDisplayId;
94     @Surface.Rotation
95     @VisibleForTesting
96     int mRotation;
97     private final Rect mMagnificationFrame = new Rect();
98     private final SurfaceControl.Transaction mTransaction;
99 
100     private final WindowManager mWm;
101 
102     private float mScale;
103 
104     private final Rect mTmpRect = new Rect();
105     private final Rect mMirrorViewBounds = new Rect();
106     private final Rect mSourceBounds = new Rect();
107 
108     // The root of the mirrored content
109     private SurfaceControl mMirrorSurface;
110 
111     private View mDragView;
112     private View mLeftDrag;
113     private View mTopDrag;
114     private View mRightDrag;
115     private View mBottomDrag;
116 
117     @NonNull
118     private final WindowMagnifierCallback mWindowMagnifierCallback;
119 
120     private final View.OnLayoutChangeListener mMirrorViewLayoutChangeListener;
121     private final View.OnLayoutChangeListener mMirrorSurfaceViewLayoutChangeListener;
122     private final Runnable mMirrorViewRunnable;
123     private final Runnable mUpdateStateDescriptionRunnable;
124     private final Runnable mWindowInsetChangeRunnable;
125     private View mMirrorView;
126     private SurfaceView mMirrorSurfaceView;
127     private int mMirrorSurfaceMargin;
128     private int mBorderDragSize;
129     private int mDragViewSize;
130     private int mOuterBorderSize;
131     // The boundary of magnification frame.
132     private final Rect mMagnificationFrameBoundary = new Rect();
133     // The top Y of the system gesture rect at the bottom. Set to -1 if it is invalid.
134     private int mSystemGestureTop = -1;
135 
136     private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
137     private final MagnificationGestureDetector mGestureDetector;
138     private final int mBounceEffectDuration;
139     private Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback;
140     private Locale mLocale;
141     private NumberFormat mPercentFormat;
142     private float mBounceEffectAnimationScale;
143     private SysUiState mSysUiState;
144     // Set it to true when the view is overlapped with the gesture insets at the bottom.
145     private boolean mOverlapWithGestureInsets;
146 
147     @Nullable
148     private MirrorWindowControl mMirrorWindowControl;
149 
WindowMagnificationController(@iContext Context context, @NonNull Handler handler, SfVsyncFrameCallbackProvider sfVsyncFrameProvider, MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction, @NonNull WindowMagnifierCallback callback, SysUiState sysUiState)150     WindowMagnificationController(@UiContext Context context, @NonNull Handler handler,
151             SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
152             MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction,
153             @NonNull WindowMagnifierCallback callback, SysUiState sysUiState) {
154         mContext = context;
155         mHandler = handler;
156         mSfVsyncFrameProvider = sfVsyncFrameProvider;
157         mWindowMagnifierCallback = callback;
158         mSysUiState = sysUiState;
159 
160         final Display display = mContext.getDisplay();
161         mDisplayId = mContext.getDisplayId();
162         mRotation = display.getRotation();
163 
164         mWm = context.getSystemService(WindowManager.class);
165         mWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds());
166 
167         mResources = mContext.getResources();
168         mScale = mResources.getInteger(R.integer.magnification_default_scale);
169         mBounceEffectDuration = mResources.getInteger(
170                 com.android.internal.R.integer.config_shortAnimTime);
171         updateDimensions();
172         setMagnificationFrameWith(mWindowBounds, mWindowBounds.width() / 2,
173                 mWindowBounds.height() / 2);
174         computeBounceAnimationScale();
175 
176         mMirrorWindowControl = mirrorWindowControl;
177         if (mMirrorWindowControl != null) {
178             mMirrorWindowControl.setWindowDelegate(this);
179         }
180         mTransaction = transaction;
181         mGestureDetector =
182                 new MagnificationGestureDetector(mContext, handler, this);
183 
184         // Initialize listeners.
185         mMirrorViewRunnable = () -> {
186             if (mMirrorView != null) {
187                 final Rect oldViewBounds = new Rect(mMirrorViewBounds);
188                 mMirrorView.getBoundsOnScreen(mMirrorViewBounds);
189                 if (oldViewBounds.width() != mMirrorViewBounds.width()
190                         || oldViewBounds.height() != mMirrorViewBounds.height()) {
191                     mMirrorView.setSystemGestureExclusionRects(Collections.singletonList(
192                             new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height())));
193                 }
194                 updateSystemUIStateIfNeeded();
195                 mWindowMagnifierCallback.onWindowMagnifierBoundsChanged(
196                         mDisplayId, mMirrorViewBounds);
197             }
198         };
199         mMirrorViewLayoutChangeListener =
200                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
201                     if (!mHandler.hasCallbacks(mMirrorViewRunnable)) {
202                         mHandler.post(mMirrorViewRunnable);
203                     }
204                 };
205 
206         mMirrorSurfaceViewLayoutChangeListener =
207                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
208                         -> applyTapExcludeRegion();
209 
210         mMirrorViewGeometryVsyncCallback =
211                 l -> {
212                     if (isWindowVisible() && mMirrorSurface != null) {
213                         calculateSourceBounds(mMagnificationFrame, mScale);
214                         // The final destination for the magnification surface should be at 0,0
215                         // since the ViewRootImpl's position will change
216                         mTmpRect.set(0, 0, mMagnificationFrame.width(),
217                                 mMagnificationFrame.height());
218                         mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect,
219                                 Surface.ROTATION_0).apply();
220                         mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds);
221                     }
222                 };
223         mUpdateStateDescriptionRunnable = () -> {
224             if (isWindowVisible()) {
225                 mMirrorView.setStateDescription(formatStateDescription(mScale));
226             }
227         };
228         mWindowInsetChangeRunnable = this::onWindowInsetChanged;
229     }
230 
updateDimensions()231     private void updateDimensions() {
232         mMirrorSurfaceMargin = mResources.getDimensionPixelSize(
233                 R.dimen.magnification_mirror_surface_margin);
234         mBorderDragSize = mResources.getDimensionPixelSize(
235                 R.dimen.magnification_border_drag_size);
236         mDragViewSize = mResources.getDimensionPixelSize(
237                 R.dimen.magnification_drag_view_size);
238         mOuterBorderSize = mResources.getDimensionPixelSize(
239                 R.dimen.magnification_outer_border_margin);
240     }
241 
computeBounceAnimationScale()242     private void computeBounceAnimationScale() {
243         final float windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
244         final float visibleWindowWidth = windowWidth - 2 * mOuterBorderSize;
245         final float animationScaleMax = windowWidth / visibleWindowWidth;
246         mBounceEffectAnimationScale = Math.min(animationScaleMax, ANIMATION_BOUNCE_EFFECT_SCALE);
247     }
248 
updateSystemGestureInsetsTop()249     private boolean updateSystemGestureInsetsTop() {
250         final WindowMetrics windowMetrics = mWm.getCurrentWindowMetrics();
251         final Insets insets = windowMetrics.getWindowInsets().getInsets(systemGestures());
252         final int gestureTop =
253                 insets.bottom != 0 ? windowMetrics.getBounds().bottom - insets.bottom : -1;
254         if (gestureTop != mSystemGestureTop) {
255             mSystemGestureTop = gestureTop;
256             return true;
257         }
258         return false;
259     }
260 
261     /**
262      * Deletes the magnification window.
263      */
deleteWindowMagnification()264     void deleteWindowMagnification() {
265         if (mMirrorSurface != null) {
266             mTransaction.remove(mMirrorSurface).apply();
267             mMirrorSurface = null;
268         }
269 
270         if (mMirrorSurfaceView != null) {
271             mMirrorSurfaceView.removeOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener);
272         }
273 
274         if (mMirrorView != null) {
275             mHandler.removeCallbacks(mMirrorViewRunnable);
276             mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
277             mWm.removeView(mMirrorView);
278             mMirrorView = null;
279         }
280 
281         if (mMirrorWindowControl != null) {
282             mMirrorWindowControl.destroyControl();
283         }
284         mMirrorViewBounds.setEmpty();
285         updateSystemUIStateIfNeeded();
286     }
287 
288     /**
289      * Called when the configuration has changed, and it updates window magnification UI.
290      *
291      * @param configDiff a bit mask of the differences between the configurations
292      */
onConfigurationChanged(int configDiff)293     void onConfigurationChanged(int configDiff) {
294         if (DEBUG) {
295             Log.d(TAG, "onConfigurationChanged = " + Configuration.configurationDiffToString(
296                     configDiff));
297         }
298         if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) {
299             onRotate();
300         }
301 
302         if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) {
303             updateAccessibilityWindowTitleIfNeeded();
304         }
305 
306         boolean reCreateWindow = false;
307         if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) {
308             updateDimensions();
309             computeBounceAnimationScale();
310             reCreateWindow = true;
311         }
312 
313         if ((configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
314             reCreateWindow |= handleScreenSizeChanged();
315         }
316 
317         // Recreate the window again to correct the window appearance due to density or
318         // window size changed not caused by rotation.
319         if (isWindowVisible() && reCreateWindow) {
320             deleteWindowMagnification();
321             enableWindowMagnification(Float.NaN, Float.NaN, Float.NaN);
322         }
323     }
324 
325     /**
326      * Calculates the magnification frame if the window bounds is changed.
327      * Note that the orientation also changes the wind bounds, so it should be handled first.
328      *
329      * @return {@code true} if the magnification frame is changed with the new window bounds.
330      */
handleScreenSizeChanged()331     private boolean handleScreenSizeChanged() {
332         final Rect oldWindowBounds = new Rect(mWindowBounds);
333         final Rect currentWindowBounds = mWm.getCurrentWindowMetrics().getBounds();
334 
335         if (currentWindowBounds.equals(oldWindowBounds)) {
336             if (DEBUG) {
337                 Log.d(TAG, "updateMagnificationFrame -- window bounds is not changed");
338             }
339             return false;
340         }
341         mWindowBounds.set(currentWindowBounds);
342         final float newCenterX = (getCenterX()) * mWindowBounds.width() / oldWindowBounds.width();
343         final float newCenterY = (getCenterY()) * mWindowBounds.height() / oldWindowBounds.height();
344         setMagnificationFrameWith(mWindowBounds, (int) newCenterX, (int) newCenterY);
345         calculateMagnificationFrameBoundary();
346         return true;
347     }
348 
updateSystemUIStateIfNeeded()349     private void updateSystemUIStateIfNeeded() {
350         updateSysUIState(false);
351     }
352 
updateAccessibilityWindowTitleIfNeeded()353     private void updateAccessibilityWindowTitleIfNeeded() {
354         if (!isWindowVisible()) return;
355         LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams();
356         params.accessibilityTitle = getAccessibilityWindowTitle();
357         mWm.updateViewLayout(mMirrorView, params);
358     }
359 
360     /**
361      * Keep MirrorWindow position on the screen unchanged when device rotates 90° clockwise or
362      * anti-clockwise.
363      */
onRotate()364     private void onRotate() {
365         final Display display = mContext.getDisplay();
366         final int oldRotation = mRotation;
367         mRotation = display.getRotation();
368         final int rotationDegree = getDegreeFromRotation(mRotation, oldRotation);
369         if (rotationDegree == 0 || rotationDegree == 180) {
370             Log.w(TAG, "onRotate -- rotate with the device. skip it");
371             return;
372         }
373         final Rect currentWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds());
374         if (currentWindowBounds.width() != mWindowBounds.height()
375                 || currentWindowBounds.height() != mWindowBounds.width()) {
376             Log.w(TAG, "onRotate -- unexpected window height/width");
377             return;
378         }
379 
380         mWindowBounds.set(currentWindowBounds);
381 
382         calculateMagnificationFrameBoundary();
383 
384         if (!isWindowVisible()) {
385             return;
386         }
387         // Keep MirrorWindow position on the screen unchanged when device rotates 90°
388         // clockwise or anti-clockwise.
389 
390         final Matrix matrix = new Matrix();
391         matrix.setRotate(rotationDegree);
392         if (rotationDegree == 90) {
393             matrix.postTranslate(mWindowBounds.width(), 0);
394         } else if (rotationDegree == 270) {
395             matrix.postTranslate(0, mWindowBounds.height());
396         }
397         // The rect of MirrorView is going to be transformed.
398         LayoutParams params =
399                 (LayoutParams) mMirrorView.getLayoutParams();
400         mTmpRect.set(params.x, params.y, params.x + params.width, params.y + params.height);
401         final RectF transformedRect = new RectF(mTmpRect);
402         matrix.mapRect(transformedRect);
403         moveWindowMagnifier(transformedRect.left - mTmpRect.left,
404                 transformedRect.top - mTmpRect.top);
405     }
406 
407     /** Returns the rotation degree change of two {@link Surface.Rotation} */
getDegreeFromRotation(@urface.Rotation int newRotation, @Surface.Rotation int oldRotation)408     private int getDegreeFromRotation(@Surface.Rotation int newRotation,
409             @Surface.Rotation int oldRotation) {
410         final int rotationDiff = oldRotation - newRotation;
411         final int degree = (rotationDiff + 4) % 4 * 90;
412         return degree;
413     }
414 
createMirrorWindow()415     private void createMirrorWindow() {
416         // The window should be the size the mirrored surface will be but also add room for the
417         // border and the drag handle.
418         int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin;
419         int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin;
420 
421         LayoutParams params = new LayoutParams(
422                 windowWidth, windowHeight,
423                 LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY,
424                 LayoutParams.FLAG_NOT_TOUCH_MODAL
425                         | LayoutParams.FLAG_NOT_FOCUSABLE,
426                 PixelFormat.TRANSPARENT);
427         params.gravity = Gravity.TOP | Gravity.LEFT;
428         params.x = mMagnificationFrame.left - mMirrorSurfaceMargin;
429         params.y = mMagnificationFrame.top - mMirrorSurfaceMargin;
430         params.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
431         params.receiveInsetsIgnoringZOrder = true;
432         params.setTitle(mContext.getString(R.string.magnification_window_title));
433         params.accessibilityTitle = getAccessibilityWindowTitle();
434 
435         mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null);
436         mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view);
437 
438         // Allow taps to go through to the mirror SurfaceView below.
439         mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener);
440 
441         mMirrorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
442                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
443                 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
444                 | View.SYSTEM_UI_FLAG_FULLSCREEN
445                 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
446                 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
447         mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener);
448         mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate());
449         mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> {
450             if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) {
451                 mHandler.post(mWindowInsetChangeRunnable);
452             }
453             return v.onApplyWindowInsets(insets);
454         });
455 
456         mWm.addView(mMirrorView, params);
457 
458         SurfaceHolder holder = mMirrorSurfaceView.getHolder();
459         holder.addCallback(this);
460         holder.setFormat(PixelFormat.RGBA_8888);
461         addDragTouchListeners();
462     }
463 
onWindowInsetChanged()464     private void onWindowInsetChanged() {
465         if (updateSystemGestureInsetsTop()) {
466             updateSystemUIStateIfNeeded();
467         }
468     }
469 
applyTapExcludeRegion()470     private void applyTapExcludeRegion() {
471         final Region tapExcludeRegion = calculateTapExclude();
472         final IWindow window = IWindow.Stub.asInterface(mMirrorView.getWindowToken());
473         try {
474             IWindowSession session = WindowManagerGlobal.getWindowSession();
475             session.updateTapExcludeRegion(window, tapExcludeRegion);
476         } catch (RemoteException e) {
477         }
478     }
479 
calculateTapExclude()480     private Region calculateTapExclude() {
481         Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize,
482                 mMirrorView.getWidth() - mBorderDragSize,
483                 mMirrorView.getHeight() - mBorderDragSize);
484         Rect dragArea = new Rect(mMirrorView.getWidth() - mDragViewSize - mBorderDragSize,
485                 mMirrorView.getHeight() - mDragViewSize - mBorderDragSize,
486                 mMirrorView.getWidth(), mMirrorView.getHeight());
487         regionInsideDragBorder.op(dragArea, Region.Op.DIFFERENCE);
488         return regionInsideDragBorder;
489     }
490 
getAccessibilityWindowTitle()491     private String getAccessibilityWindowTitle() {
492         return mResources.getString(com.android.internal.R.string.android_system_label);
493     }
494 
showControls()495     private void showControls() {
496         if (mMirrorWindowControl != null) {
497             mMirrorWindowControl.showControl();
498         }
499     }
500 
setMagnificationFrameWith(Rect windowBounds, int centerX, int centerY)501     private void setMagnificationFrameWith(Rect windowBounds, int centerX, int centerY) {
502         // Sets the initial frame area for the mirror and place it to the given center on the
503         // display.
504         int initSize = Math.min(windowBounds.width(), windowBounds.height()) / 2;
505         initSize = Math.min(mResources.getDimensionPixelSize(R.dimen.magnification_max_frame_size),
506                 initSize);
507         initSize += 2 * mMirrorSurfaceMargin;
508         final int initX = centerX - initSize / 2;
509         final int initY = centerY - initSize / 2;
510         mMagnificationFrame.set(initX, initY, initX + initSize, initY + initSize);
511     }
512 
513     /**
514      * This is called once the surfaceView is created so the mirrored content can be placed as a
515      * child of the surfaceView.
516      */
createMirror()517     private void createMirror() {
518         mMirrorSurface = WindowManagerWrapper.getInstance().mirrorDisplay(mDisplayId);
519         if (!mMirrorSurface.isValid()) {
520             return;
521         }
522         mTransaction.show(mMirrorSurface)
523                 .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl());
524 
525         modifyWindowMagnification(mTransaction);
526     }
527 
addDragTouchListeners()528     private void addDragTouchListeners() {
529         mDragView = mMirrorView.findViewById(R.id.drag_handle);
530         mLeftDrag = mMirrorView.findViewById(R.id.left_handle);
531         mTopDrag = mMirrorView.findViewById(R.id.top_handle);
532         mRightDrag = mMirrorView.findViewById(R.id.right_handle);
533         mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle);
534 
535         mDragView.setOnTouchListener(this);
536         mLeftDrag.setOnTouchListener(this);
537         mTopDrag.setOnTouchListener(this);
538         mRightDrag.setOnTouchListener(this);
539         mBottomDrag.setOnTouchListener(this);
540     }
541 
542     /**
543      * Modifies the placement of the mirrored content when the position of mMirrorView is updated.
544      */
modifyWindowMagnification(SurfaceControl.Transaction t)545     private void modifyWindowMagnification(SurfaceControl.Transaction t) {
546         mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback);
547         updateMirrorViewLayout();
548 
549     }
550 
551     /**
552      * Updates the layout params of MirrorView and translates MirrorView position when the view is
553      * moved close to the screen edges.
554      */
updateMirrorViewLayout()555     private void updateMirrorViewLayout() {
556         if (!isWindowVisible()) {
557             return;
558         }
559         final int maxMirrorViewX = mWindowBounds.width() - mMirrorView.getWidth();
560         final int maxMirrorViewY = mWindowBounds.height() - mMirrorView.getHeight();
561 
562         LayoutParams params =
563                 (LayoutParams) mMirrorView.getLayoutParams();
564         params.x = mMagnificationFrame.left - mMirrorSurfaceMargin;
565         params.y = mMagnificationFrame.top - mMirrorSurfaceMargin;
566 
567         // Translates MirrorView position to make MirrorSurfaceView that is inside MirrorView
568         // able to move close to the screen edges.
569         final float translationX;
570         final float translationY;
571         if (params.x < 0) {
572             translationX = Math.max(params.x, -mOuterBorderSize);
573         } else if (params.x > maxMirrorViewX) {
574             translationX = Math.min(params.x - maxMirrorViewX, mOuterBorderSize);
575         } else {
576             translationX = 0;
577         }
578         if (params.y < 0) {
579             translationY = Math.max(params.y, -mOuterBorderSize);
580         } else if (params.y > maxMirrorViewY) {
581             translationY = Math.min(params.y - maxMirrorViewY, mOuterBorderSize);
582         } else {
583             translationY = 0;
584         }
585         mMirrorView.setTranslationX(translationX);
586         mMirrorView.setTranslationY(translationY);
587         mWm.updateViewLayout(mMirrorView, params);
588     }
589 
590     @Override
onTouch(View v, MotionEvent event)591     public boolean onTouch(View v, MotionEvent event) {
592         if (v == mDragView || v == mLeftDrag || v == mTopDrag || v == mRightDrag
593                 || v == mBottomDrag) {
594             return mGestureDetector.onTouch(event);
595         }
596         return false;
597     }
598 
updateSysUIStateFlag()599     public void updateSysUIStateFlag() {
600         updateSysUIState(true);
601     }
602 
603     /**
604      * Calculates the desired source bounds. This will be the area under from the center of  the
605      * displayFrame, factoring in scale.
606      */
calculateSourceBounds(Rect displayFrame, float scale)607     private void calculateSourceBounds(Rect displayFrame, float scale) {
608         int halfWidth = displayFrame.width() / 2;
609         int halfHeight = displayFrame.height() / 2;
610         int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale));
611         int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale));
612         int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale));
613         int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale));
614         mSourceBounds.set(left, top, right, bottom);
615     }
616 
calculateMagnificationFrameBoundary()617     private void calculateMagnificationFrameBoundary() {
618         // Calculates width and height for magnification frame could exceed out the screen.
619         // TODO : re-calculating again when scale is changed.
620         // The half width of magnification frame.
621         final int halfWidth = mMagnificationFrame.width() / 2;
622         // The half height of magnification frame.
623         final int halfHeight = mMagnificationFrame.height() / 2;
624         // The scaled half width of magnified region.
625         final int scaledWidth = (int) (halfWidth / mScale);
626         // The scaled half height of magnified region.
627         final int scaledHeight = (int) (halfHeight / mScale);
628         final int exceededWidth = halfWidth - scaledWidth;
629         final int exceededHeight = halfHeight - scaledHeight;
630 
631         mMagnificationFrameBoundary.set(-exceededWidth, -exceededHeight,
632                 mWindowBounds.width() + exceededWidth, mWindowBounds.height() + exceededHeight);
633     }
634 
635     /**
636      * Calculates and sets the real position of magnification frame based on the magnified region
637      * should be limited by the region of the display.
638      */
updateMagnificationFramePosition(int xOffset, int yOffset)639     private boolean updateMagnificationFramePosition(int xOffset, int yOffset) {
640         mTmpRect.set(mMagnificationFrame);
641         mTmpRect.offset(xOffset, yOffset);
642 
643         if (mTmpRect.left < mMagnificationFrameBoundary.left) {
644             mTmpRect.offsetTo(mMagnificationFrameBoundary.left, mTmpRect.top);
645         } else if (mTmpRect.right > mMagnificationFrameBoundary.right) {
646             final int leftOffset = mMagnificationFrameBoundary.right - mMagnificationFrame.width();
647             mTmpRect.offsetTo(leftOffset, mTmpRect.top);
648         }
649 
650         if (mTmpRect.top < mMagnificationFrameBoundary.top) {
651             mTmpRect.offsetTo(mTmpRect.left, mMagnificationFrameBoundary.top);
652         } else if (mTmpRect.bottom > mMagnificationFrameBoundary.bottom) {
653             final int topOffset = mMagnificationFrameBoundary.bottom - mMagnificationFrame.height();
654             mTmpRect.offsetTo(mTmpRect.left, topOffset);
655         }
656 
657         if (!mTmpRect.equals(mMagnificationFrame)) {
658             mMagnificationFrame.set(mTmpRect);
659             return true;
660         }
661         return false;
662     }
663 
updateSysUIState(boolean force)664     private void updateSysUIState(boolean force) {
665         final boolean overlap = isWindowVisible() && mSystemGestureTop > 0
666                 && mMirrorViewBounds.bottom > mSystemGestureTop;
667         if (force || overlap != mOverlapWithGestureInsets) {
668             mOverlapWithGestureInsets = overlap;
669             mSysUiState.setFlag(SYSUI_STATE_MAGNIFICATION_OVERLAP, mOverlapWithGestureInsets)
670                     .commitUpdate(mDisplayId);
671         }
672     }
673 
674     @Override
surfaceCreated(SurfaceHolder holder)675     public void surfaceCreated(SurfaceHolder holder) {
676         createMirror();
677     }
678 
679     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)680     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
681     }
682 
683     @Override
surfaceDestroyed(SurfaceHolder holder)684     public void surfaceDestroyed(SurfaceHolder holder) {
685     }
686 
687     @Override
move(int xOffset, int yOffset)688     public void move(int xOffset, int yOffset) {
689         moveWindowMagnifier(xOffset, yOffset);
690     }
691 
692     /**
693      * Enables window magnification with specified parameters.
694      *
695      * @param scale   the target scale, or {@link Float#NaN} to leave unchanged
696      * @param centerX the screen-relative X coordinate around which to center,
697      *                or {@link Float#NaN} to leave unchanged.
698      * @param centerY the screen-relative Y coordinate around which to center,
699      *                or {@link Float#NaN} to leave unchanged.
700      */
enableWindowMagnification(float scale, float centerX, float centerY)701     void enableWindowMagnification(float scale, float centerX, float centerY) {
702         final float offsetX = Float.isNaN(centerX) ? 0
703                 : centerX - mMagnificationFrame.exactCenterX();
704         final float offsetY = Float.isNaN(centerY) ? 0
705                 : centerY - mMagnificationFrame.exactCenterY();
706         mScale = Float.isNaN(scale) ? mScale : scale;
707 
708         calculateMagnificationFrameBoundary();
709         updateMagnificationFramePosition((int) offsetX, (int) offsetY);
710         if (!isWindowVisible()) {
711             createMirrorWindow();
712             showControls();
713         } else {
714             modifyWindowMagnification(mTransaction);
715         }
716     }
717 
718     /**
719      * Sets the scale of the magnified region if it's visible.
720      *
721      * @param scale the target scale, or {@link Float#NaN} to leave unchanged
722      */
setScale(float scale)723     void setScale(float scale) {
724         if (!isWindowVisible() || mScale == scale) {
725             return;
726         }
727         enableWindowMagnification(scale, Float.NaN, Float.NaN);
728         mHandler.removeCallbacks(mUpdateStateDescriptionRunnable);
729         mHandler.postDelayed(mUpdateStateDescriptionRunnable, UPDATE_STATE_DESCRIPTION_DELAY_MS);
730     }
731 
732     /**
733      * Moves the window magnifier with specified offset in pixels unit.
734      *
735      * @param offsetX the amount in pixels to offset the window magnifier in the X direction, in
736      *                current screen pixels.
737      * @param offsetY the amount in pixels to offset the window magnifier in the Y direction, in
738      *                current screen pixels.
739      */
moveWindowMagnifier(float offsetX, float offsetY)740     void moveWindowMagnifier(float offsetX, float offsetY) {
741         if (mMirrorSurfaceView == null) {
742             return;
743         }
744         if (updateMagnificationFramePosition((int) offsetX, (int) offsetY)) {
745             modifyWindowMagnification(mTransaction);
746         }
747     }
748 
749     /**
750      * Gets the scale.
751      *
752      * @return {@link Float#NaN} if the window is invisible.
753      */
getScale()754     float getScale() {
755         return isWindowVisible() ? mScale : Float.NaN;
756     }
757 
758     /**
759      * Returns the screen-relative X coordinate of the center of the magnified bounds.
760      *
761      * @return the X coordinate. {@link Float#NaN} if the window is invisible.
762      */
getCenterX()763     float getCenterX() {
764         return isWindowVisible() ? mMagnificationFrame.exactCenterX() : Float.NaN;
765     }
766 
767     /**
768      * Returns the screen-relative Y coordinate of the center of the magnified bounds.
769      *
770      * @return the Y coordinate. {@link Float#NaN} if the window is invisible.
771      */
getCenterY()772     float getCenterY() {
773         return isWindowVisible() ? mMagnificationFrame.exactCenterY() : Float.NaN;
774     }
775 
776     //The window is visible when it is existed.
isWindowVisible()777     private boolean isWindowVisible() {
778         return mMirrorView != null;
779     }
780 
formatStateDescription(float scale)781     private CharSequence formatStateDescription(float scale) {
782         // Cache the locale-appropriate NumberFormat.  Configuration locale is guaranteed
783         // non-null, so the first time this is called we will always get the appropriate
784         // NumberFormat, then never regenerate it unless the locale changes on the fly.
785         final Locale curLocale = mContext.getResources().getConfiguration().getLocales().get(0);
786         if (!curLocale.equals(mLocale)) {
787             mLocale = curLocale;
788             mPercentFormat = NumberFormat.getPercentInstance(curLocale);
789         }
790         return mPercentFormat.format(scale);
791     }
792 
793     @Override
onSingleTap()794     public boolean onSingleTap() {
795         animateBounceEffect();
796         return true;
797     }
798 
799     @Override
onDrag(float offsetX, float offsetY)800     public boolean onDrag(float offsetX, float offsetY) {
801         moveWindowMagnifier(offsetX, offsetY);
802         return true;
803     }
804 
805     @Override
onStart(float x, float y)806     public boolean onStart(float x, float y) {
807         return true;
808     }
809 
810     @Override
onFinish(float x, float y)811     public boolean onFinish(float x, float y) {
812         return false;
813     }
814 
animateBounceEffect()815     private void animateBounceEffect() {
816         final ObjectAnimator scaleAnimator = ObjectAnimator.ofPropertyValuesHolder(mMirrorView,
817                 PropertyValuesHolder.ofFloat(View.SCALE_X, 1, mBounceEffectAnimationScale, 1),
818                 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1, mBounceEffectAnimationScale, 1));
819         scaleAnimator.setDuration(mBounceEffectDuration);
820         scaleAnimator.start();
821     }
822 
dump(PrintWriter pw)823     public void dump(PrintWriter pw) {
824         pw.println("WindowMagnificationController (displayId=" + mDisplayId + "):");
825         pw.println("      mOverlapWithGestureInsets:" + mOverlapWithGestureInsets);
826         pw.println("      mScale:" + mScale);
827         pw.println("      mMirrorViewBounds:" + (isWindowVisible() ? mMirrorViewBounds : "empty"));
828         pw.println("      mSourceBounds:"
829                  + (isWindowVisible() ? mSourceBounds : "empty"));
830         pw.println("      mSystemGestureTop:" + mSystemGestureTop);
831     }
832 
833     private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate {
834 
835         @Override
onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info)836         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
837             super.onInitializeAccessibilityNodeInfo(host, info);
838             info.addAction(
839                     new AccessibilityAction(R.id.accessibility_action_zoom_in,
840                             mContext.getString(R.string.accessibility_control_zoom_in)));
841             info.addAction(new AccessibilityAction(R.id.accessibility_action_zoom_out,
842                     mContext.getString(R.string.accessibility_control_zoom_out)));
843             info.addAction(new AccessibilityAction(R.id.accessibility_action_move_up,
844                     mContext.getString(R.string.accessibility_control_move_up)));
845             info.addAction(new AccessibilityAction(R.id.accessibility_action_move_down,
846                     mContext.getString(R.string.accessibility_control_move_down)));
847             info.addAction(new AccessibilityAction(R.id.accessibility_action_move_left,
848                     mContext.getString(R.string.accessibility_control_move_left)));
849             info.addAction(new AccessibilityAction(R.id.accessibility_action_move_right,
850                     mContext.getString(R.string.accessibility_control_move_right)));
851 
852             info.setContentDescription(mContext.getString(R.string.magnification_window_title));
853             info.setStateDescription(formatStateDescription(getScale()));
854         }
855 
856         @Override
performAccessibilityAction(View host, int action, Bundle args)857         public boolean performAccessibilityAction(View host, int action, Bundle args) {
858             if (performA11yAction(action)) {
859                 return true;
860             }
861             return super.performAccessibilityAction(host, action, args);
862         }
863 
performA11yAction(int action)864         private boolean performA11yAction(int action) {
865             if (action == R.id.accessibility_action_zoom_in) {
866                 final float scale = mScale + A11Y_CHANGE_SCALE_DIFFERENCE;
867                 mWindowMagnifierCallback.onPerformScaleAction(mDisplayId,
868                         A11Y_ACTION_SCALE_RANGE.clamp(scale));
869             } else if (action == R.id.accessibility_action_zoom_out) {
870                 final float scale = mScale - A11Y_CHANGE_SCALE_DIFFERENCE;
871                 mWindowMagnifierCallback.onPerformScaleAction(mDisplayId,
872                         A11Y_ACTION_SCALE_RANGE.clamp(scale));
873             } else if (action == R.id.accessibility_action_move_up) {
874                 move(0, -mSourceBounds.height());
875             } else if (action == R.id.accessibility_action_move_down) {
876                 move(0, mSourceBounds.height());
877             } else if (action == R.id.accessibility_action_move_left) {
878                 move(-mSourceBounds.width(), 0);
879             } else if (action == R.id.accessibility_action_move_right) {
880                 move(mSourceBounds.width(), 0);
881             } else {
882                 return false;
883             }
884             mWindowMagnifierCallback.onAccessibilityActionPerformed(mDisplayId);
885             return true;
886         }
887     }
888 }
889