/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.accessibility; import static android.view.WindowInsets.Type.systemGestures; import static android.view.WindowManager.LayoutParams; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiContext; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Resources; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.util.Log; import android.util.Range; import android.view.Choreographer; import android.view.Display; import android.view.Gravity; import android.view.IWindow; import android.view.IWindowSession; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowMetrics; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.R; import com.android.systemui.model.SysUiState; import com.android.systemui.shared.system.WindowManagerWrapper; import java.io.PrintWriter; import java.text.NumberFormat; import java.util.Collections; import java.util.Locale; /** * Class to handle adding and removing a window magnification. */ class WindowMagnificationController implements View.OnTouchListener, SurfaceHolder.Callback, MirrorWindowControl.MirrorWindowDelegate, MagnificationGestureDetector.OnGestureListener { private static final String TAG = "WindowMagnificationController"; // Delay to avoid updating state description too frequently. private static final int UPDATE_STATE_DESCRIPTION_DELAY_MS = 100; // It should be consistent with the value defined in WindowMagnificationGestureHandler. private static final Range A11Y_ACTION_SCALE_RANGE = new Range<>(2.0f, 8.0f); private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f; private static final float ANIMATION_BOUNCE_EFFECT_SCALE = 1.05f; private final Context mContext; private final Resources mResources; private final Handler mHandler; private Rect mWindowBounds; private final int mDisplayId; @Surface.Rotation @VisibleForTesting int mRotation; private final Rect mMagnificationFrame = new Rect(); private final SurfaceControl.Transaction mTransaction; private final WindowManager mWm; private float mScale; private final Rect mTmpRect = new Rect(); private final Rect mMirrorViewBounds = new Rect(); private final Rect mSourceBounds = new Rect(); // The root of the mirrored content private SurfaceControl mMirrorSurface; private View mDragView; private View mLeftDrag; private View mTopDrag; private View mRightDrag; private View mBottomDrag; @NonNull private final WindowMagnifierCallback mWindowMagnifierCallback; private final View.OnLayoutChangeListener mMirrorViewLayoutChangeListener; private final View.OnLayoutChangeListener mMirrorSurfaceViewLayoutChangeListener; private final Runnable mMirrorViewRunnable; private final Runnable mUpdateStateDescriptionRunnable; private final Runnable mWindowInsetChangeRunnable; private View mMirrorView; private SurfaceView mMirrorSurfaceView; private int mMirrorSurfaceMargin; private int mBorderDragSize; private int mDragViewSize; private int mOuterBorderSize; // The boundary of magnification frame. private final Rect mMagnificationFrameBoundary = new Rect(); // The top Y of the system gesture rect at the bottom. Set to -1 if it is invalid. private int mSystemGestureTop = -1; private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; private final MagnificationGestureDetector mGestureDetector; private final int mBounceEffectDuration; private Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback; private Locale mLocale; private NumberFormat mPercentFormat; private float mBounceEffectAnimationScale; private SysUiState mSysUiState; // Set it to true when the view is overlapped with the gesture insets at the bottom. private boolean mOverlapWithGestureInsets; @Nullable private MirrorWindowControl mMirrorWindowControl; WindowMagnificationController(@UiContext Context context, @NonNull Handler handler, SfVsyncFrameCallbackProvider sfVsyncFrameProvider, MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction, @NonNull WindowMagnifierCallback callback, SysUiState sysUiState) { mContext = context; mHandler = handler; mSfVsyncFrameProvider = sfVsyncFrameProvider; mWindowMagnifierCallback = callback; mSysUiState = sysUiState; final Display display = mContext.getDisplay(); mDisplayId = mContext.getDisplayId(); mRotation = display.getRotation(); mWm = context.getSystemService(WindowManager.class); mWindowBounds = mWm.getCurrentWindowMetrics().getBounds(); mResources = mContext.getResources(); mScale = mResources.getInteger(R.integer.magnification_default_scale); mBounceEffectDuration = mResources.getInteger( com.android.internal.R.integer.config_shortAnimTime); updateDimensions(); setInitialStartBounds(); computeBounceAnimationScale(); mMirrorWindowControl = mirrorWindowControl; if (mMirrorWindowControl != null) { mMirrorWindowControl.setWindowDelegate(this); } mTransaction = transaction; mGestureDetector = new MagnificationGestureDetector(mContext, handler, this); // Initialize listeners. mMirrorViewRunnable = () -> { if (mMirrorView != null) { final Rect oldViewBounds = new Rect(mMirrorViewBounds); mMirrorView.getBoundsOnScreen(mMirrorViewBounds); if (oldViewBounds.width() != mMirrorViewBounds.width() || oldViewBounds.height() != mMirrorViewBounds.height()) { mMirrorView.setSystemGestureExclusionRects(Collections.singletonList( new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height()))); } updateSystemUIStateIfNeeded(); mWindowMagnifierCallback.onWindowMagnifierBoundsChanged( mDisplayId, mMirrorViewBounds); } }; mMirrorViewLayoutChangeListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { if (!mHandler.hasCallbacks(mMirrorViewRunnable)) { mHandler.post(mMirrorViewRunnable); } }; mMirrorSurfaceViewLayoutChangeListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> applyTapExcludeRegion(); mMirrorViewGeometryVsyncCallback = l -> { if (isWindowVisible() && mMirrorSurface != null) { calculateSourceBounds(mMagnificationFrame, mScale); // The final destination for the magnification surface should be at 0,0 // since the ViewRootImpl's position will change mTmpRect.set(0, 0, mMagnificationFrame.width(), mMagnificationFrame.height()); mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, Surface.ROTATION_0).apply(); mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds); } }; mUpdateStateDescriptionRunnable = () -> { if (isWindowVisible()) { mMirrorView.setStateDescription(formatStateDescription(mScale)); } }; mWindowInsetChangeRunnable = this::onWindowInsetChanged; } private void updateDimensions() { mMirrorSurfaceMargin = mResources.getDimensionPixelSize( R.dimen.magnification_mirror_surface_margin); mBorderDragSize = mResources.getDimensionPixelSize( R.dimen.magnification_border_drag_size); mDragViewSize = mResources.getDimensionPixelSize( R.dimen.magnification_drag_view_size); mOuterBorderSize = mResources.getDimensionPixelSize( R.dimen.magnification_outer_border_margin); } private void computeBounceAnimationScale() { final float windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; final float visibleWindowWidth = windowWidth - 2 * mOuterBorderSize; final float animationScaleMax = windowWidth / visibleWindowWidth; mBounceEffectAnimationScale = Math.min(animationScaleMax, ANIMATION_BOUNCE_EFFECT_SCALE); } private boolean updateSystemGestureInsetsTop() { final WindowMetrics windowMetrics = mWm.getCurrentWindowMetrics(); final Insets insets = windowMetrics.getWindowInsets().getInsets(systemGestures()); final int gestureTop = insets.bottom != 0 ? windowMetrics.getBounds().bottom - insets.bottom : -1; if (gestureTop != mSystemGestureTop) { mSystemGestureTop = gestureTop; return true; } return false; } /** * Deletes the magnification window. */ void deleteWindowMagnification() { if (mMirrorSurface != null) { mTransaction.remove(mMirrorSurface).apply(); mMirrorSurface = null; } if (mMirrorSurfaceView != null) { mMirrorSurfaceView.removeOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener); } if (mMirrorView != null) { mHandler.removeCallbacks(mMirrorViewRunnable); mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener); mWm.removeView(mMirrorView); mMirrorView = null; } if (mMirrorWindowControl != null) { mMirrorWindowControl.destroyControl(); } mMirrorViewBounds.setEmpty(); updateSystemUIStateIfNeeded(); } /** * Called when the configuration has changed, and it updates window magnification UI. * * @param configDiff a bit mask of the differences between the configurations */ void onConfigurationChanged(int configDiff) { if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) { updateDimensions(); computeBounceAnimationScale(); if (isWindowVisible()) { deleteWindowMagnification(); enableWindowMagnification(Float.NaN, Float.NaN, Float.NaN); } } else if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) { onRotate(); } else if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) { updateAccessibilityWindowTitleIfNeeded(); } } private void updateSystemUIStateIfNeeded() { updateSysUIState(false); } private void updateAccessibilityWindowTitleIfNeeded() { if (!isWindowVisible()) return; LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); params.accessibilityTitle = getAccessibilityWindowTitle(); mWm.updateViewLayout(mMirrorView, params); } /** Handles MirrorWindow position when the device rotation changed. */ private void onRotate() { final Display display = mContext.getDisplay(); final int oldRotation = mRotation; mWindowBounds = mWm.getCurrentWindowMetrics().getBounds(); setMagnificationFrameBoundary(); mRotation = display.getRotation(); if (!isWindowVisible()) { return; } // Keep MirrorWindow position on the screen unchanged when device rotates 90° // clockwise or anti-clockwise. final int rotationDegree = getDegreeFromRotation(mRotation, oldRotation); final Matrix matrix = new Matrix(); matrix.setRotate(rotationDegree); if (rotationDegree == 90) { matrix.postTranslate(mWindowBounds.width(), 0); } else if (rotationDegree == 270) { matrix.postTranslate(0, mWindowBounds.height()); } else { Log.w(TAG, "Invalid rotation change. " + rotationDegree); return; } // The rect of MirrorView is going to be transformed. LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); mTmpRect.set(params.x, params.y, params.x + params.width, params.y + params.height); final RectF transformedRect = new RectF(mTmpRect); matrix.mapRect(transformedRect); moveWindowMagnifier(transformedRect.left - mTmpRect.left, transformedRect.top - mTmpRect.top); } /** Returns the rotation degree change of two {@link Surface.Rotation} */ private int getDegreeFromRotation(@Surface.Rotation int newRotation, @Surface.Rotation int oldRotation) { final int rotationDiff = oldRotation - newRotation; final int degree = (rotationDiff + 4) % 4 * 90; return degree; } private void createMirrorWindow() { // The window should be the size the mirrored surface will be but also add room for the // border and the drag handle. int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin; LayoutParams params = new LayoutParams( windowWidth, windowHeight, LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); params.gravity = Gravity.TOP | Gravity.LEFT; params.x = mMagnificationFrame.left - mMirrorSurfaceMargin; params.y = mMagnificationFrame.top - mMirrorSurfaceMargin; params.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; params.receiveInsetsIgnoringZOrder = true; params.setTitle(mContext.getString(R.string.magnification_window_title)); params.accessibilityTitle = getAccessibilityWindowTitle(); mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null); mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view); // Allow taps to go through to the mirror SurfaceView below. mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener); mMirrorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener); mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate()); mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> { if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) { mHandler.post(mWindowInsetChangeRunnable); } return v.onApplyWindowInsets(insets); }); mWm.addView(mMirrorView, params); SurfaceHolder holder = mMirrorSurfaceView.getHolder(); holder.addCallback(this); holder.setFormat(PixelFormat.RGBA_8888); addDragTouchListeners(); } private void onWindowInsetChanged() { if (updateSystemGestureInsetsTop()) { updateSystemUIStateIfNeeded(); } } private void applyTapExcludeRegion() { final Region tapExcludeRegion = calculateTapExclude(); final IWindow window = IWindow.Stub.asInterface(mMirrorView.getWindowToken()); try { IWindowSession session = WindowManagerGlobal.getWindowSession(); session.updateTapExcludeRegion(window, tapExcludeRegion); } catch (RemoteException e) { } } private Region calculateTapExclude() { Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize, mMirrorView.getWidth() - mBorderDragSize, mMirrorView.getHeight() - mBorderDragSize); Rect dragArea = new Rect(mMirrorView.getWidth() - mDragViewSize - mBorderDragSize, mMirrorView.getHeight() - mDragViewSize - mBorderDragSize, mMirrorView.getWidth(), mMirrorView.getHeight()); regionInsideDragBorder.op(dragArea, Region.Op.DIFFERENCE); return regionInsideDragBorder; } private String getAccessibilityWindowTitle() { return mResources.getString(com.android.internal.R.string.android_system_label); } private void showControls() { if (mMirrorWindowControl != null) { mMirrorWindowControl.showControl(); } } private void setInitialStartBounds() { // Sets the initial frame area for the mirror and places it in the center of the display. final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 2 + 2 * mMirrorSurfaceMargin; final int initX = mWindowBounds.width() / 2 - initSize / 2; final int initY = mWindowBounds.height() / 2 - initSize / 2; mMagnificationFrame.set(initX, initY, initX + initSize, initY + initSize); } /** * This is called once the surfaceView is created so the mirrored content can be placed as a * child of the surfaceView. */ private void createMirror() { mMirrorSurface = WindowManagerWrapper.getInstance().mirrorDisplay(mDisplayId); if (!mMirrorSurface.isValid()) { return; } mTransaction.show(mMirrorSurface) .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl()); modifyWindowMagnification(mTransaction); } private void addDragTouchListeners() { mDragView = mMirrorView.findViewById(R.id.drag_handle); mLeftDrag = mMirrorView.findViewById(R.id.left_handle); mTopDrag = mMirrorView.findViewById(R.id.top_handle); mRightDrag = mMirrorView.findViewById(R.id.right_handle); mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle); mDragView.setOnTouchListener(this); mLeftDrag.setOnTouchListener(this); mTopDrag.setOnTouchListener(this); mRightDrag.setOnTouchListener(this); mBottomDrag.setOnTouchListener(this); } /** * Modifies the placement of the mirrored content when the position of mMirrorView is updated. */ private void modifyWindowMagnification(SurfaceControl.Transaction t) { mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback); updateMirrorViewLayout(); } /** * Updates the layout params of MirrorView and translates MirrorView position when the view is * moved close to the screen edges. */ private void updateMirrorViewLayout() { if (!isWindowVisible()) { return; } final int maxMirrorViewX = mWindowBounds.width() - mMirrorView.getWidth(); final int maxMirrorViewY = mWindowBounds.height() - mMirrorView.getHeight(); LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); params.x = mMagnificationFrame.left - mMirrorSurfaceMargin; params.y = mMagnificationFrame.top - mMirrorSurfaceMargin; // Translates MirrorView position to make MirrorSurfaceView that is inside MirrorView // able to move close to the screen edges. final float translationX; final float translationY; if (params.x < 0) { translationX = Math.max(params.x, -mOuterBorderSize); } else if (params.x > maxMirrorViewX) { translationX = Math.min(params.x - maxMirrorViewX, mOuterBorderSize); } else { translationX = 0; } if (params.y < 0) { translationY = Math.max(params.y, -mOuterBorderSize); } else if (params.y > maxMirrorViewY) { translationY = Math.min(params.y - maxMirrorViewY, mOuterBorderSize); } else { translationY = 0; } mMirrorView.setTranslationX(translationX); mMirrorView.setTranslationY(translationY); mWm.updateViewLayout(mMirrorView, params); } @Override public boolean onTouch(View v, MotionEvent event) { if (v == mDragView || v == mLeftDrag || v == mTopDrag || v == mRightDrag || v == mBottomDrag) { return mGestureDetector.onTouch(event); } return false; } public void updateSysUIStateFlag() { updateSysUIState(true); } /** * Calculates the desired source bounds. This will be the area under from the center of the * displayFrame, factoring in scale. */ private void calculateSourceBounds(Rect displayFrame, float scale) { int halfWidth = displayFrame.width() / 2; int halfHeight = displayFrame.height() / 2; int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale)); int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale)); int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale)); int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale)); mSourceBounds.set(left, top, right, bottom); } private void setMagnificationFrameBoundary() { // Calculates width and height for magnification frame could exceed out the screen. // TODO : re-calculating again when scale is changed. // The half width of magnification frame. final int halfWidth = mMagnificationFrame.width() / 2; // The half height of magnification frame. final int halfHeight = mMagnificationFrame.height() / 2; // The scaled half width of magnified region. final int scaledWidth = (int) (halfWidth / mScale); // The scaled half height of magnified region. final int scaledHeight = (int) (halfHeight / mScale); final int exceededWidth = halfWidth - scaledWidth; final int exceededHeight = halfHeight - scaledHeight; mMagnificationFrameBoundary.set(-exceededWidth, -exceededHeight, mWindowBounds.width() + exceededWidth, mWindowBounds.height() + exceededHeight); } /** * Calculates and sets the real position of magnification frame based on the magnified region * should be limited by the region of the display. */ private boolean updateMagnificationFramePosition(int xOffset, int yOffset) { mTmpRect.set(mMagnificationFrame); mTmpRect.offset(xOffset, yOffset); if (mTmpRect.left < mMagnificationFrameBoundary.left) { mTmpRect.offsetTo(mMagnificationFrameBoundary.left, mTmpRect.top); } else if (mTmpRect.right > mMagnificationFrameBoundary.right) { final int leftOffset = mMagnificationFrameBoundary.right - mMagnificationFrame.width(); mTmpRect.offsetTo(leftOffset, mTmpRect.top); } if (mTmpRect.top < mMagnificationFrameBoundary.top) { mTmpRect.offsetTo(mTmpRect.left, mMagnificationFrameBoundary.top); } else if (mTmpRect.bottom > mMagnificationFrameBoundary.bottom) { final int topOffset = mMagnificationFrameBoundary.bottom - mMagnificationFrame.height(); mTmpRect.offsetTo(mTmpRect.left, topOffset); } if (!mTmpRect.equals(mMagnificationFrame)) { mMagnificationFrame.set(mTmpRect); return true; } return false; } private void updateSysUIState(boolean force) { final boolean overlap = isWindowVisible() && mSystemGestureTop > 0 && mMirrorViewBounds.bottom > mSystemGestureTop; if (force || overlap != mOverlapWithGestureInsets) { mOverlapWithGestureInsets = overlap; mSysUiState.setFlag(SYSUI_STATE_MAGNIFICATION_OVERLAP, mOverlapWithGestureInsets) .commitUpdate(mDisplayId); } } @Override public void surfaceCreated(SurfaceHolder holder) { createMirror(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { } @Override public void move(int xOffset, int yOffset) { moveWindowMagnifier(xOffset, yOffset); } /** * Enables window magnification with specified parameters. * * @param scale the target scale, or {@link Float#NaN} to leave unchanged * @param centerX the screen-relative X coordinate around which to center, * or {@link Float#NaN} to leave unchanged. * @param centerY the screen-relative Y coordinate around which to center, * or {@link Float#NaN} to leave unchanged. */ void enableWindowMagnification(float scale, float centerX, float centerY) { final float offsetX = Float.isNaN(centerX) ? 0 : centerX - mMagnificationFrame.exactCenterX(); final float offsetY = Float.isNaN(centerY) ? 0 : centerY - mMagnificationFrame.exactCenterY(); mScale = Float.isNaN(scale) ? mScale : scale; setMagnificationFrameBoundary(); updateMagnificationFramePosition((int) offsetX, (int) offsetY); if (!isWindowVisible()) { createMirrorWindow(); showControls(); } else { modifyWindowMagnification(mTransaction); } } /** * Sets the scale of the magnified region if it's visible. * * @param scale the target scale, or {@link Float#NaN} to leave unchanged */ void setScale(float scale) { if (!isWindowVisible() || mScale == scale) { return; } enableWindowMagnification(scale, Float.NaN, Float.NaN); mHandler.removeCallbacks(mUpdateStateDescriptionRunnable); mHandler.postDelayed(mUpdateStateDescriptionRunnable, UPDATE_STATE_DESCRIPTION_DELAY_MS); } /** * Moves the window magnifier with specified offset in pixels unit. * * @param offsetX the amount in pixels to offset the window magnifier in the X direction, in * current screen pixels. * @param offsetY the amount in pixels to offset the window magnifier in the Y direction, in * current screen pixels. */ void moveWindowMagnifier(float offsetX, float offsetY) { if (mMirrorSurfaceView == null) { return; } if (updateMagnificationFramePosition((int) offsetX, (int) offsetY)) { modifyWindowMagnification(mTransaction); } } /** * Gets the scale. * * @return {@link Float#NaN} if the window is invisible. */ float getScale() { return isWindowVisible() ? mScale : Float.NaN; } /** * Returns the screen-relative X coordinate of the center of the magnified bounds. * * @return the X coordinate. {@link Float#NaN} if the window is invisible. */ float getCenterX() { return isWindowVisible() ? mMagnificationFrame.exactCenterX() : Float.NaN; } /** * Returns the screen-relative Y coordinate of the center of the magnified bounds. * * @return the Y coordinate. {@link Float#NaN} if the window is invisible. */ float getCenterY() { return isWindowVisible() ? mMagnificationFrame.exactCenterY() : Float.NaN; } //The window is visible when it is existed. private boolean isWindowVisible() { return mMirrorView != null; } private CharSequence formatStateDescription(float scale) { // Cache the locale-appropriate NumberFormat. Configuration locale is guaranteed // non-null, so the first time this is called we will always get the appropriate // NumberFormat, then never regenerate it unless the locale changes on the fly. final Locale curLocale = mContext.getResources().getConfiguration().getLocales().get(0); if (!curLocale.equals(mLocale)) { mLocale = curLocale; mPercentFormat = NumberFormat.getPercentInstance(curLocale); } return mPercentFormat.format(scale); } @Override public boolean onSingleTap() { animateBounceEffect(); return true; } @Override public boolean onDrag(float offsetX, float offsetY) { moveWindowMagnifier(offsetX, offsetY); return true; } @Override public boolean onStart(float x, float y) { return true; } @Override public boolean onFinish(float x, float y) { return false; } private void animateBounceEffect() { final ObjectAnimator scaleAnimator = ObjectAnimator.ofPropertyValuesHolder(mMirrorView, PropertyValuesHolder.ofFloat(View.SCALE_X, 1, mBounceEffectAnimationScale, 1), PropertyValuesHolder.ofFloat(View.SCALE_Y, 1, mBounceEffectAnimationScale, 1)); scaleAnimator.setDuration(mBounceEffectDuration); scaleAnimator.start(); } public void dump(PrintWriter pw) { pw.println("WindowMagnificationController (displayId=" + mDisplayId + "):"); pw.println(" mOverlapWithGestureInsets:" + mOverlapWithGestureInsets); pw.println(" mScale:" + mScale); pw.println(" mMirrorViewBounds:" + (isWindowVisible() ? mMirrorViewBounds : "empty")); pw.println(" mSystemGestureTop:" + mSystemGestureTop); } private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate { @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); info.addAction( new AccessibilityAction(R.id.accessibility_action_zoom_in, mContext.getString(R.string.accessibility_control_zoom_in))); info.addAction(new AccessibilityAction(R.id.accessibility_action_zoom_out, mContext.getString(R.string.accessibility_control_zoom_out))); info.addAction(new AccessibilityAction(R.id.accessibility_action_move_up, mContext.getString(R.string.accessibility_control_move_up))); info.addAction(new AccessibilityAction(R.id.accessibility_action_move_down, mContext.getString(R.string.accessibility_control_move_down))); info.addAction(new AccessibilityAction(R.id.accessibility_action_move_left, mContext.getString(R.string.accessibility_control_move_left))); info.addAction(new AccessibilityAction(R.id.accessibility_action_move_right, mContext.getString(R.string.accessibility_control_move_right))); info.setContentDescription(mContext.getString(R.string.magnification_window_title)); info.setStateDescription(formatStateDescription(getScale())); } @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { if (performA11yAction(action)) { return true; } return super.performAccessibilityAction(host, action, args); } private boolean performA11yAction(int action) { if (action == R.id.accessibility_action_zoom_in) { final float scale = mScale + A11Y_CHANGE_SCALE_DIFFERENCE; mWindowMagnifierCallback.onPerformScaleAction(mDisplayId, A11Y_ACTION_SCALE_RANGE.clamp(scale)); } else if (action == R.id.accessibility_action_zoom_out) { final float scale = mScale - A11Y_CHANGE_SCALE_DIFFERENCE; mWindowMagnifierCallback.onPerformScaleAction(mDisplayId, A11Y_ACTION_SCALE_RANGE.clamp(scale)); } else if (action == R.id.accessibility_action_move_up) { move(0, -mSourceBounds.height()); } else if (action == R.id.accessibility_action_move_down) { move(0, mSourceBounds.height()); } else if (action == R.id.accessibility_action_move_left) { move(-mSourceBounds.width(), 0); } else if (action == R.id.accessibility_action_move_right) { move(mSourceBounds.width(), 0); } else { return false; } mWindowMagnifierCallback.onAccessibilityActionPerformed(mDisplayId); return true; } } }