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