All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.systemui.accessibility.WindowMagnificationController Maven / Gradle / Ivy

/*
 * 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;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy