| /* |
| * 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.accessibility.WindowMagnificationSettings.MagnificationSize; |
| import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; |
| |
| import static java.lang.Math.abs; |
| |
| import android.animation.ObjectAnimator; |
| import android.animation.PropertyValuesHolder; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.UiContext; |
| import android.content.ComponentCallbacks; |
| import android.content.Context; |
| import android.content.pm.ActivityInfo; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.graphics.Insets; |
| import android.graphics.Matrix; |
| import android.graphics.PixelFormat; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffColorFilter; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.Region; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.util.Log; |
| import android.util.Range; |
| import android.util.Size; |
| import android.util.SparseArray; |
| import android.util.TypedValue; |
| 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 android.view.accessibility.IRemoteMagnificationAnimationCallback; |
| import android.widget.FrameLayout; |
| import android.widget.ImageView; |
| |
| import androidx.core.math.MathUtils; |
| |
| 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.util.settings.SecureSettings; |
| |
| import java.io.PrintWriter; |
| import java.text.NumberFormat; |
| import java.util.Collections; |
| import java.util.Locale; |
| import java.util.function.Supplier; |
| |
| /** |
| * Class to handle adding and removing a window magnification. |
| */ |
| class WindowMagnificationController implements View.OnTouchListener, SurfaceHolder.Callback, |
| MirrorWindowControl.MirrorWindowDelegate, MagnificationGestureDetector.OnGestureListener, |
| ComponentCallbacks { |
| |
| private static final String TAG = "WindowMagnificationController"; |
| @SuppressWarnings("isloggabletaglength") |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE; |
| // 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<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(1.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 SparseArray<Float> mMagnificationSizeScaleOptions = new SparseArray<>(); |
| |
| private final Context mContext; |
| private final Resources mResources; |
| private final Handler mHandler; |
| private final Rect mWindowBounds; |
| private final int mDisplayId; |
| @Surface.Rotation |
| @VisibleForTesting |
| int mRotation; |
| private final SurfaceControl.Transaction mTransaction; |
| |
| private final WindowManager mWm; |
| |
| private float mScale; |
| |
| /** |
| * MagnificationFrame represents the bound of {@link #mMirrorSurface} and is constrained |
| * by the {@link #mMagnificationFrameBoundary}. |
| * We use MagnificationFrame to calculate the position of {@link #mMirrorView}. |
| * We combine MagnificationFrame with {@link #mMagnificationFrameOffsetX} and |
| * {@link #mMagnificationFrameOffsetY} to calculate the position of {@link #mSourceBounds}. |
| */ |
| private final Rect mMagnificationFrame = new Rect(); |
| private final Rect mTmpRect = new Rect(); |
| |
| /** |
| * MirrorViewBounds is the bound of the {@link #mMirrorView} which displays the magnified |
| * content. |
| * {@link #mMirrorView}'s center is equal to {@link #mMagnificationFrame}'s center. |
| */ |
| private final Rect mMirrorViewBounds = new Rect(); |
| |
| /** |
| * SourceBound is the bound of the magnified region which projects the magnified content. |
| * SourceBound's center is equal to the parameters centerX and centerY in |
| * {@link WindowMagnificationController#enableWindowMagnificationInternal(float, float, float)}} |
| * but it is calculated from {@link #mMagnificationFrame}'s center in the runtime. |
| */ |
| private final Rect mSourceBounds = new Rect(); |
| |
| /** |
| * The relation of centers between {@link #mSourceBounds} and {@link #mMagnificationFrame} is |
| * calculated in {@link #calculateSourceBounds(Rect, float)} and the equations are as following: |
| * MagnificationFrame = SourceBound (e.g., centerX & centerY) + MagnificationFrameOffset |
| * SourceBound = MagnificationFrame - MagnificationFrameOffset |
| */ |
| private int mMagnificationFrameOffsetX = 0; |
| private int mMagnificationFrameOffsetY = 0; |
| |
| // The root of the mirrored content |
| private SurfaceControl mMirrorSurface; |
| |
| private ImageView mDragView; |
| private ImageView mCloseView; |
| private View mLeftDrag; |
| private View mTopDrag; |
| private View mRightDrag; |
| private View mBottomDrag; |
| private ImageView mTopLeftCornerView; |
| private ImageView mTopRightCornerView; |
| private ImageView mBottomLeftCornerView; |
| private ImageView mBottomRightCornerView; |
| private final Configuration mConfiguration; |
| |
| @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; |
| // MirrorView is the mirror window which displays the magnified content. |
| private View mMirrorView; |
| private View mMirrorBorderView; |
| private SurfaceView mMirrorSurfaceView; |
| private int mMirrorSurfaceMargin; |
| private int mBorderDragSize; |
| private int mOuterBorderSize; |
| |
| /** |
| * How far from the right edge of the screen you need to drag the window before the button |
| * repositions to the other side. |
| */ |
| private int mButtonRepositionThresholdFromEdge; |
| |
| // 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 int mMinWindowSize; |
| |
| private final WindowMagnificationAnimationController mAnimationController; |
| private final Supplier<IWindowSession> mGlobalWindowSessionSupplier; |
| private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; |
| private final MagnificationGestureDetector mGestureDetector; |
| private final int mBounceEffectDuration; |
| private final Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback; |
| private Locale mLocale; |
| private NumberFormat mPercentFormat; |
| private float mBounceEffectAnimationScale; |
| private final SysUiState mSysUiState; |
| // Set it to true when the view is overlapped with the gesture insets at the bottom. |
| private boolean mOverlapWithGestureInsets; |
| private boolean mIsDragging; |
| |
| private static final int MAX_HORIZONTAL_MOVE_ANGLE = 50; |
| private static final int HORIZONTAL = 1; |
| private static final int VERTICAL = 0; |
| |
| @VisibleForTesting |
| static final double HORIZONTAL_LOCK_BASE = |
| Math.tan(Math.toRadians(MAX_HORIZONTAL_MOVE_ANGLE)); |
| |
| private boolean mAllowDiagonalScrolling = false; |
| private boolean mEditSizeEnable = false; |
| |
| @Nullable |
| private final MirrorWindowControl mMirrorWindowControl; |
| |
| WindowMagnificationController( |
| @UiContext Context context, |
| @NonNull Handler handler, |
| @NonNull WindowMagnificationAnimationController animationController, |
| SfVsyncFrameCallbackProvider sfVsyncFrameProvider, |
| MirrorWindowControl mirrorWindowControl, |
| SurfaceControl.Transaction transaction, |
| @NonNull WindowMagnifierCallback callback, |
| SysUiState sysUiState, |
| @NonNull Supplier<IWindowSession> globalWindowSessionSupplier, |
| SecureSettings secureSettings) { |
| mContext = context; |
| mHandler = handler; |
| mAnimationController = animationController; |
| mGlobalWindowSessionSupplier = globalWindowSessionSupplier; |
| mAnimationController.setWindowMagnificationController(this); |
| mSfVsyncFrameProvider = sfVsyncFrameProvider; |
| mWindowMagnifierCallback = callback; |
| mSysUiState = sysUiState; |
| mConfiguration = new Configuration(context.getResources().getConfiguration()); |
| |
| final Display display = mContext.getDisplay(); |
| mDisplayId = mContext.getDisplayId(); |
| mRotation = display.getRotation(); |
| |
| mWm = context.getSystemService(WindowManager.class); |
| mWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds()); |
| |
| mResources = mContext.getResources(); |
| mScale = secureSettings.getFloatForUser( |
| Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, |
| mResources.getInteger(R.integer.magnification_default_scale), |
| UserHandle.USER_CURRENT); |
| |
| setupMagnificationSizeScaleOptions(); |
| |
| mBounceEffectDuration = mResources.getInteger( |
| com.android.internal.R.integer.config_shortAnimTime); |
| updateDimensions(); |
| |
| final Size windowSize = getDefaultWindowSizeWithWindowBounds(mWindowBounds); |
| setMagnificationFrame(windowSize.getWidth(), windowSize.getHeight(), |
| mWindowBounds.width() / 2, mWindowBounds.height() / 2); |
| 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) -> |
| mMirrorView.post(this::applyTapExcludeRegion); |
| |
| mMirrorViewGeometryVsyncCallback = |
| l -> { |
| if (isActivated() && 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(); |
| |
| // Notify source bounds change when the magnifier is not animating. |
| if (!mAnimationController.isAnimating()) { |
| mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, |
| mSourceBounds); |
| } |
| } |
| }; |
| mUpdateStateDescriptionRunnable = () -> { |
| if (isActivated()) { |
| mMirrorView.setStateDescription(formatStateDescription(mScale)); |
| } |
| }; |
| mWindowInsetChangeRunnable = this::onWindowInsetChanged; |
| } |
| |
| private void setupMagnificationSizeScaleOptions() { |
| mMagnificationSizeScaleOptions.clear(); |
| mMagnificationSizeScaleOptions.put(MagnificationSize.SMALL, 1.4f); |
| mMagnificationSizeScaleOptions.put(MagnificationSize.MEDIUM, 1.8f); |
| mMagnificationSizeScaleOptions.put(MagnificationSize.LARGE, 2.5f); |
| } |
| |
| private void updateDimensions() { |
| mMirrorSurfaceMargin = mResources.getDimensionPixelSize( |
| R.dimen.magnification_mirror_surface_margin); |
| mBorderDragSize = mResources.getDimensionPixelSize( |
| R.dimen.magnification_border_drag_size); |
| mOuterBorderSize = mResources.getDimensionPixelSize( |
| R.dimen.magnification_outer_border_margin); |
| mButtonRepositionThresholdFromEdge = |
| mResources.getDimensionPixelSize( |
| R.dimen.magnification_button_reposition_threshold_from_edge); |
| mMinWindowSize = mResources.getDimensionPixelSize( |
| com.android.internal.R.dimen.accessibility_window_magnifier_min_size); |
| } |
| |
| 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; |
| } |
| |
| void changeMagnificationSize(@MagnificationSize int index) { |
| if (!mMagnificationSizeScaleOptions.contains(index)) { |
| return; |
| } |
| final float scale = mMagnificationSizeScaleOptions.get(index, 1.0f); |
| final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3; |
| int size = (int) (initSize * scale); |
| setWindowSize(size, size); |
| } |
| |
| void setEditMagnifierSizeMode(boolean enable) { |
| mEditSizeEnable = enable; |
| applyResourcesValues(); |
| |
| if (isActivated()) { |
| updateDimensions(); |
| applyTapExcludeRegion(); |
| } |
| } |
| |
| void setDiagonalScrolling(boolean enable) { |
| mAllowDiagonalScrolling = enable; |
| } |
| |
| /** |
| * Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition |
| * animation. If the window magnification is enabling, it runs the animation in reverse. |
| * |
| * @param animationCallback Called when the transition is complete, the given arguments |
| * are as same as current values, or the transition is interrupted |
| * due to the new transition request. |
| */ |
| void deleteWindowMagnification( |
| @Nullable IRemoteMagnificationAnimationCallback animationCallback) { |
| mAnimationController.deleteWindowMagnification(animationCallback); |
| } |
| |
| /** |
| * Deletes the magnification window. |
| */ |
| void deleteWindowMagnification() { |
| if (!isActivated()) { |
| return; |
| } |
| |
| 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(); |
| mSourceBounds.setEmpty(); |
| updateSystemUIStateIfNeeded(); |
| setEditMagnifierSizeMode(false); |
| |
| mContext.unregisterComponentCallbacks(this); |
| // Notify source bounds empty when magnification is deleted. |
| mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, new Rect()); |
| } |
| |
| @Override |
| public void onConfigurationChanged(@NonNull Configuration newConfig) { |
| final int configDiff = newConfig.diff(mConfiguration); |
| mConfiguration.setTo(newConfig); |
| onConfigurationChanged(configDiff); |
| } |
| |
| @Override |
| public void onLowMemory() { |
| } |
| |
| /** |
| * 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 (DEBUG) { |
| Log.d(TAG, "onConfigurationChanged = " + Configuration.configurationDiffToString( |
| configDiff)); |
| } |
| if (configDiff == 0) { |
| return; |
| } |
| if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) { |
| onRotate(); |
| } |
| |
| if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) { |
| updateAccessibilityWindowTitleIfNeeded(); |
| } |
| |
| boolean reCreateWindow = false; |
| if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) { |
| updateDimensions(); |
| computeBounceAnimationScale(); |
| reCreateWindow = true; |
| } |
| |
| if ((configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) { |
| reCreateWindow |= handleScreenSizeChanged(); |
| } |
| |
| // Recreate the window again to correct the window appearance due to density or |
| // window size changed not caused by rotation. |
| if (isActivated() && reCreateWindow) { |
| deleteWindowMagnification(); |
| enableWindowMagnificationInternal(Float.NaN, Float.NaN, Float.NaN); |
| } |
| } |
| |
| /** |
| * Calculates the magnification frame if the window bounds is changed. |
| * Note that the orientation also changes the wind bounds, so it should be handled first. |
| * |
| * @return {@code true} if the magnification frame is changed with the new window bounds. |
| */ |
| private boolean handleScreenSizeChanged() { |
| final Rect oldWindowBounds = new Rect(mWindowBounds); |
| final Rect currentWindowBounds = mWm.getCurrentWindowMetrics().getBounds(); |
| |
| if (currentWindowBounds.equals(oldWindowBounds)) { |
| if (DEBUG) { |
| Log.d(TAG, "handleScreenSizeChanged -- window bounds is not changed"); |
| } |
| return false; |
| } |
| mWindowBounds.set(currentWindowBounds); |
| final Size windowSize = getDefaultWindowSizeWithWindowBounds(mWindowBounds); |
| final float newCenterX = (getCenterX()) * mWindowBounds.width() / oldWindowBounds.width(); |
| final float newCenterY = (getCenterY()) * mWindowBounds.height() / oldWindowBounds.height(); |
| |
| setMagnificationFrame(windowSize.getWidth(), windowSize.getHeight(), (int) newCenterX, |
| (int) newCenterY); |
| calculateMagnificationFrameBoundary(); |
| return true; |
| } |
| |
| private void updateSystemUIStateIfNeeded() { |
| updateSysUIState(false); |
| } |
| |
| private void updateAccessibilityWindowTitleIfNeeded() { |
| if (!isActivated()) return; |
| LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); |
| params.accessibilityTitle = getAccessibilityWindowTitle(); |
| mWm.updateViewLayout(mMirrorView, params); |
| } |
| |
| /** |
| * Keep MirrorWindow position on the screen unchanged when device rotates 90° clockwise or |
| * anti-clockwise. |
| */ |
| private void onRotate() { |
| final Display display = mContext.getDisplay(); |
| final int oldRotation = mRotation; |
| mRotation = display.getRotation(); |
| final int rotationDegree = getDegreeFromRotation(mRotation, oldRotation); |
| if (rotationDegree == 0 || rotationDegree == 180) { |
| Log.w(TAG, "onRotate -- rotate with the device. skip it"); |
| return; |
| } |
| final Rect currentWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds()); |
| if (currentWindowBounds.width() != mWindowBounds.height() |
| || currentWindowBounds.height() != mWindowBounds.width()) { |
| Log.w(TAG, "onRotate -- unexpected window height/width"); |
| return; |
| } |
| |
| mWindowBounds.set(currentWindowBounds); |
| |
| // Keep MirrorWindow position on the screen unchanged when device rotates 90° |
| // clockwise or anti-clockwise. |
| |
| 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()); |
| } |
| |
| final RectF transformedRect = new RectF(mMagnificationFrame); |
| // The window frame is going to be transformed by the rotation matrix. |
| transformedRect.inset(-mMirrorSurfaceMargin, -mMirrorSurfaceMargin); |
| matrix.mapRect(transformedRect); |
| setWindowSizeAndCenter((int) transformedRect.width(), (int) transformedRect.height(), |
| (int) transformedRect.centerX(), (int) transformedRect.centerY()); |
| } |
| |
| /** Returns the rotation degree change of two {@link Surface.Rotation} */ |
| private int getDegreeFromRotation(@Surface.Rotation int newRotation, |
| @Surface.Rotation int oldRotation) { |
| return (oldRotation - newRotation + 4) % 4 * 90; |
| } |
| |
| 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); |
| |
| mMirrorBorderView = mMirrorView.findViewById(R.id.magnification_inner_border); |
| |
| // 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() { |
| // Sometimes this can get posted and run after deleteWindowMagnification() is called. |
| if (mMirrorView == null) return; |
| |
| final Region tapExcludeRegion = calculateTapExclude(); |
| final IWindow window = IWindow.Stub.asInterface(mMirrorView.getWindowToken()); |
| try { |
| IWindowSession session = mGlobalWindowSessionSupplier.get(); |
| session.updateTapExcludeRegion(window, tapExcludeRegion); |
| } catch (RemoteException e) { |
| } |
| } |
| |
| private Region calculateTapExclude() { |
| Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize, |
| mMirrorView.getWidth() - mBorderDragSize, |
| mMirrorView.getHeight() - mBorderDragSize); |
| |
| Region tapExcludeRegion = new Region(); |
| |
| Rect dragArea = new Rect(); |
| mDragView.getHitRect(dragArea); |
| |
| Rect topLeftArea = new Rect(); |
| mTopLeftCornerView.getHitRect(topLeftArea); |
| |
| Rect topRightArea = new Rect(); |
| mTopRightCornerView.getHitRect(topRightArea); |
| |
| Rect bottomLeftArea = new Rect(); |
| mBottomLeftCornerView.getHitRect(bottomLeftArea); |
| |
| Rect bottomRightArea = new Rect(); |
| mBottomRightCornerView.getHitRect(bottomRightArea); |
| |
| Rect closeArea = new Rect(); |
| mCloseView.getHitRect(closeArea); |
| |
| // add tapExcludeRegion for Drag or close |
| tapExcludeRegion.op(dragArea, Region.Op.UNION); |
| tapExcludeRegion.op(topLeftArea, Region.Op.UNION); |
| tapExcludeRegion.op(topRightArea, Region.Op.UNION); |
| tapExcludeRegion.op(bottomLeftArea, Region.Op.UNION); |
| tapExcludeRegion.op(bottomRightArea, Region.Op.UNION); |
| tapExcludeRegion.op(closeArea, Region.Op.UNION); |
| |
| regionInsideDragBorder.op(tapExcludeRegion, 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(); |
| } |
| } |
| |
| /** |
| * Sets the window size with given width and height in pixels without changing the |
| * window center. The width or the height will be clamped in the range |
| * [{@link #mMinWindowSize}, screen width or height]. |
| * |
| * @param width the window width in pixels |
| * @param height the window height in pixels. |
| */ |
| public void setWindowSize(int width, int height) { |
| setWindowSizeAndCenter(width, height, Float.NaN, Float.NaN); |
| } |
| |
| void setWindowSizeAndCenter(int width, int height, float centerX, float centerY) { |
| width = MathUtils.clamp(width, mMinWindowSize, mWindowBounds.width()); |
| height = MathUtils.clamp(height, mMinWindowSize, mWindowBounds.height()); |
| |
| if (Float.isNaN(centerX)) { |
| centerX = mMagnificationFrame.centerX(); |
| } |
| if (Float.isNaN(centerY)) { |
| centerY = mMagnificationFrame.centerY(); |
| } |
| |
| final int frameWidth = width - 2 * mMirrorSurfaceMargin; |
| final int frameHeight = height - 2 * mMirrorSurfaceMargin; |
| setMagnificationFrame(frameWidth, frameHeight, (int) centerX, (int) centerY); |
| calculateMagnificationFrameBoundary(); |
| // Correct the frame position to ensure it is inside the boundary. |
| updateMagnificationFramePosition(0, 0); |
| modifyWindowMagnification(true); |
| } |
| |
| private void setMagnificationFrame(int width, int height, int centerX, int centerY) { |
| // Sets the initial frame area for the mirror and place it to the given center on the |
| // display. |
| final int initX = centerX - width / 2; |
| final int initY = centerY - height / 2; |
| mMagnificationFrame.set(initX, initY, initX + width, initY + height); |
| } |
| |
| private Size getDefaultWindowSizeWithWindowBounds(Rect windowBounds) { |
| int initSize = Math.min(windowBounds.width(), windowBounds.height()) / 2; |
| initSize = Math.min(mResources.getDimensionPixelSize(R.dimen.magnification_max_frame_size), |
| initSize); |
| initSize += 2 * mMirrorSurfaceMargin; |
| return new Size(initSize, 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 = mirrorDisplay(mDisplayId); |
| if (!mMirrorSurface.isValid()) { |
| return; |
| } |
| mTransaction.show(mMirrorSurface) |
| .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl()); |
| modifyWindowMagnification(false); |
| } |
| |
| /** |
| * Mirrors a specified display. The SurfaceControl returned is the root of the mirrored |
| * hierarchy. |
| * |
| * @param displayId The id of the display to mirror |
| * @return The SurfaceControl for the root of the mirrored hierarchy. |
| */ |
| private SurfaceControl mirrorDisplay(final int displayId) { |
| try { |
| SurfaceControl outSurfaceControl = new SurfaceControl(); |
| WindowManagerGlobal.getWindowManagerService().mirrorDisplay(displayId, |
| outSurfaceControl); |
| return outSurfaceControl; |
| } catch (RemoteException e) { |
| Log.e(TAG, "Unable to reach window manager", e); |
| } |
| return null; |
| } |
| |
| 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); |
| mCloseView = mMirrorView.findViewById(R.id.close_button); |
| mTopRightCornerView = mMirrorView.findViewById(R.id.top_right_corner); |
| mTopLeftCornerView = mMirrorView.findViewById(R.id.top_left_corner); |
| mBottomRightCornerView = mMirrorView.findViewById(R.id.bottom_right_corner); |
| mBottomLeftCornerView = mMirrorView.findViewById(R.id.bottom_left_corner); |
| |
| mDragView.setOnTouchListener(this); |
| mLeftDrag.setOnTouchListener(this); |
| mTopDrag.setOnTouchListener(this); |
| mRightDrag.setOnTouchListener(this); |
| mBottomDrag.setOnTouchListener(this); |
| mCloseView.setOnTouchListener(this); |
| mTopLeftCornerView.setOnTouchListener(this); |
| mTopRightCornerView.setOnTouchListener(this); |
| mBottomLeftCornerView.setOnTouchListener(this); |
| mBottomRightCornerView.setOnTouchListener(this); |
| } |
| |
| /** |
| * Modifies the placement of the mirrored content when the position or size of mMirrorView is |
| * updated. |
| * |
| * @param computeWindowSize set to {@code true} to compute window size with |
| * {@link #mMagnificationFrame}. |
| */ |
| private void modifyWindowMagnification(boolean computeWindowSize) { |
| mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback); |
| updateMirrorViewLayout(computeWindowSize); |
| } |
| |
| /** |
| * Updates the layout params of MirrorView based on the size of {@link #mMagnificationFrame} |
| * and translates MirrorView position when the view is moved close to the screen edges; |
| * |
| * @param computeWindowSize set to {@code true} to compute window size with |
| * {@link #mMagnificationFrame}. |
| */ |
| private void updateMirrorViewLayout(boolean computeWindowSize) { |
| if (!isActivated()) { |
| 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; |
| if (computeWindowSize) { |
| params.width = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; |
| params.height = mMagnificationFrame.height() + 2 * 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); |
| |
| // If they are not dragging the handle, we can move the drag handle immediately without |
| // disruption. But if they are dragging it, we avoid moving until the end of the drag. |
| if (!mIsDragging) { |
| mMirrorView.post(this::maybeRepositionButton); |
| } |
| } |
| |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| if (v == mDragView |
| || v == mLeftDrag |
| || v == mTopDrag |
| || v == mRightDrag |
| || v == mBottomDrag |
| || v == mTopLeftCornerView |
| || v == mTopRightCornerView |
| || v == mBottomLeftCornerView |
| || v == mBottomRightCornerView |
| || v == mCloseView) { |
| return mGestureDetector.onTouch(v, 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. |
| * |
| * @return {@code true} if the source bounds is changed. |
| */ |
| private boolean calculateSourceBounds(Rect displayFrame, float scale) { |
| final Rect oldSourceBounds = mTmpRect; |
| oldSourceBounds.set(mSourceBounds); |
| 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); |
| |
| // SourceBound's center is equal to center[X,Y] but calculated from MagnificationFrame's |
| // center. The relation between SourceBound and MagnificationFrame is as following: |
| // MagnificationFrame = SourceBound (center[X,Y]) + MagnificationFrameOffset |
| // SourceBound = MagnificationFrame - MagnificationFrameOffset |
| mSourceBounds.offset(-mMagnificationFrameOffsetX, -mMagnificationFrameOffsetY); |
| |
| if (mSourceBounds.left < 0) { |
| mSourceBounds.offsetTo(0, mSourceBounds.top); |
| } else if (mSourceBounds.right > mWindowBounds.width()) { |
| mSourceBounds.offsetTo(mWindowBounds.width() - mSourceBounds.width(), |
| mSourceBounds.top); |
| } |
| |
| if (mSourceBounds.top < 0) { |
| mSourceBounds.offsetTo(mSourceBounds.left, 0); |
| } else if (mSourceBounds.bottom > mWindowBounds.height()) { |
| mSourceBounds.offsetTo(mSourceBounds.left, |
| mWindowBounds.height() - mSourceBounds.height()); |
| } |
| return !mSourceBounds.equals(oldSourceBounds); |
| } |
| |
| private void calculateMagnificationFrameBoundary() { |
| // 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); |
| |
| // MagnificationFrameBoundary constrain the space of MagnificationFrame, and it also has |
| // to leave enough space for SourceBound to magnify the whole screen space. |
| // However, there is an offset between SourceBound and MagnificationFrame. |
| // The relation between SourceBound and MagnificationFrame is as following: |
| // SourceBound = MagnificationFrame - MagnificationFrameOffset |
| // Therefore, we have to adjust the exceededBoundary based on the offset. |
| // |
| // We have to increase the offset space for the SourceBound edges which are located in |
| // the MagnificationFrame. For example, if the offsetX and offsetY are negative, which |
| // means SourceBound is at right-bottom size of MagnificationFrame, the left and top |
| // edges of SourceBound are located in MagnificationFrame. So, we have to leave extra |
| // offset space at left and top sides and don't have to leave extra space at right and |
| // bottom sides. |
| final int exceededLeft = Math.max(halfWidth - scaledWidth - mMagnificationFrameOffsetX, 0); |
| final int exceededRight = Math.max(halfWidth - scaledWidth + mMagnificationFrameOffsetX, 0); |
| final int exceededTop = Math.max(halfHeight - scaledHeight - mMagnificationFrameOffsetY, 0); |
| final int exceededBottom = Math.max(halfHeight - scaledHeight + mMagnificationFrameOffsetY, |
| 0); |
| |
| mMagnificationFrameBoundary.set( |
| -exceededLeft, |
| -exceededTop, |
| mWindowBounds.width() + exceededRight, |
| mWindowBounds.height() + exceededBottom); |
| } |
| |
| /** |
| * 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 = isActivated() && 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); |
| mWindowMagnifierCallback.onMove(mDisplayId); |
| } |
| |
| /** |
| * Wraps {@link WindowMagnificationController#enableWindowMagnificationInternal(float, float, |
| * float, float, float)} |
| * with transition animation. If the window magnification is not enabled, the scale will start |
| * from 1.0 and the center won't be changed during the animation. If animator is |
| * {@code STATE_DISABLING}, the animation runs in reverse. |
| * |
| * @param scale The target scale, or {@link Float#NaN} to leave unchanged. |
| * @param centerX The screen-relative X coordinate around which to center for magnification, |
| * or {@link Float#NaN} to leave unchanged. |
| * @param centerY The screen-relative Y coordinate around which to center for magnification, |
| * or {@link Float#NaN} to leave unchanged. |
| * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset |
| * between frame position X and centerX |
| * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset |
| * between frame position Y and centerY |
| * @param animationCallback Called when the transition is complete, the given arguments |
| * are as same as current values, or the transition is interrupted |
| * due to the new transition request. |
| */ |
| public void enableWindowMagnification(float scale, float centerX, float centerY, |
| float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, |
| @Nullable IRemoteMagnificationAnimationCallback animationCallback) { |
| mAnimationController.enableWindowMagnification(scale, centerX, centerY, |
| magnificationFrameOffsetRatioX, magnificationFrameOffsetRatioY, animationCallback); |
| } |
| |
| /** |
| * Enables window magnification with specified parameters. If the given scale is <strong>less |
| * than or equal to 1.0f<strong>, then |
| * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to |
| * be consistent with the behavior of display magnification. |
| * |
| * @param scale the target scale, or {@link Float#NaN} to leave unchanged |
| * @param centerX the screen-relative X coordinate around which to center for magnification, |
| * or {@link Float#NaN} to leave unchanged. |
| * @param centerY the screen-relative Y coordinate around which to center for magnification, |
| * or {@link Float#NaN} to leave unchanged. |
| */ |
| void enableWindowMagnificationInternal(float scale, float centerX, float centerY) { |
| enableWindowMagnificationInternal(scale, centerX, centerY, Float.NaN, Float.NaN); |
| } |
| |
| /** |
| * Enables window magnification with specified parameters. If the given scale is <strong>less |
| * than 1.0f<strong>, then |
| * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to |
| * be consistent with the behavior of display magnification. |
| * |
| * @param scale the target scale, or {@link Float#NaN} to leave unchanged |
| * @param centerX the screen-relative X coordinate around which to center for magnification, |
| * or {@link Float#NaN} to leave unchanged. |
| * @param centerY the screen-relative Y coordinate around which to center for magnification, |
| * or {@link Float#NaN} to leave unchanged. |
| * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset |
| * between frame position X and centerX, |
| * or {@link Float#NaN} to leave unchanged. |
| * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset |
| * between frame position Y and centerY, |
| * or {@link Float#NaN} to leave unchanged. |
| */ |
| void enableWindowMagnificationInternal(float scale, float centerX, float centerY, |
| float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY) { |
| if (Float.compare(scale, 1.0f) < 0) { |
| deleteWindowMagnification(); |
| return; |
| } |
| if (!isActivated()) { |
| onConfigurationChanged(mResources.getConfiguration()); |
| mContext.registerComponentCallbacks(this); |
| } |
| |
| mMagnificationFrameOffsetX = Float.isNaN(magnificationFrameOffsetRatioX) |
| ? mMagnificationFrameOffsetX |
| : (int) (mMagnificationFrame.width() / 2 * magnificationFrameOffsetRatioX); |
| mMagnificationFrameOffsetY = Float.isNaN(magnificationFrameOffsetRatioY) |
| ? mMagnificationFrameOffsetY |
| : (int) (mMagnificationFrame.height() / 2 * magnificationFrameOffsetRatioY); |
| |
| // The relation of centers between SourceBound and MagnificationFrame is as following: |
| // MagnificationFrame = SourceBound (e.g., centerX & centerY) + MagnificationFrameOffset |
| final float newMagnificationFrameCenterX = centerX + mMagnificationFrameOffsetX; |
| final float newMagnificationFrameCenterY = centerY + mMagnificationFrameOffsetY; |
| |
| final float offsetX = Float.isNaN(centerX) ? 0 |
| : newMagnificationFrameCenterX - mMagnificationFrame.exactCenterX(); |
| final float offsetY = Float.isNaN(centerY) ? 0 |
| : newMagnificationFrameCenterY - mMagnificationFrame.exactCenterY(); |
| mScale = Float.isNaN(scale) ? mScale : scale; |
| |
| calculateMagnificationFrameBoundary(); |
| updateMagnificationFramePosition((int) offsetX, (int) offsetY); |
| if (!isActivated()) { |
| createMirrorWindow(); |
| showControls(); |
| applyResourcesValues(); |
| } else { |
| modifyWindowMagnification(false); |
| } |
| } |
| |
| // The magnifier is activated when the window is visible, |
| // and the window is visible when it is existed. |
| boolean isActivated() { |
| return mMirrorView != null; |
| } |
| |
| /** |
| * 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 (mAnimationController.isAnimating() || !isActivated() || mScale == scale) { |
| return; |
| } |
| |
| enableWindowMagnificationInternal(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 (mAnimationController.isAnimating() || mMirrorSurfaceView == null) { |
| return; |
| } |
| |
| if (!mAllowDiagonalScrolling) { |
| int direction = selectDirectionForMove(abs(offsetX), abs(offsetY)); |
| |
| if (direction == HORIZONTAL) { |
| offsetY = 0; |
| } else { |
| offsetX = 0; |
| } |
| } |
| |
| if (updateMagnificationFramePosition((int) offsetX, (int) offsetY)) { |
| modifyWindowMagnification(false); |
| } |
| } |
| |
| void moveWindowMagnifierToPosition(float positionX, float positionY, |
| IRemoteMagnificationAnimationCallback callback) { |
| if (mMirrorSurfaceView == null) { |
| return; |
| } |
| mAnimationController.moveWindowMagnifierToPosition(positionX, positionY, callback); |
| } |
| |
| private int selectDirectionForMove(float diffX, float diffY) { |
| int direction = 0; |
| float result = diffY / diffX; |
| |
| if (result <= HORIZONTAL_LOCK_BASE) { |
| direction = HORIZONTAL; // horizontal move |
| } else { |
| direction = VERTICAL; // vertical move |
| } |
| return direction; |
| } |
| |
| /** |
| * Gets the scale. |
| * |
| * @return {@link Float#NaN} if the window is invisible. |
| */ |
| float getScale() { |
| return isActivated() ? 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 isActivated() ? 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 isActivated() ? mMagnificationFrame.exactCenterY() : Float.NaN; |
| } |
| |
| 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(View view) { |
| handleSingleTap(view); |
| return true; |
| } |
| |
| @Override |
| public boolean onDrag(View view, float offsetX, float offsetY) { |
| if (mEditSizeEnable) { |
| return changeWindowSize(view, offsetX, offsetY); |
| } else { |
| move((int) offsetX, (int) offsetY); |
| } |
| return true; |
| } |
| |
| private void handleSingleTap(View view) { |
| int id = view.getId(); |
| if (id == R.id.drag_handle) { |
| mWindowMagnifierCallback.onClickSettingsButton(mDisplayId); |
| } else if (id == R.id.close_button) { |
| setEditMagnifierSizeMode(false); |
| } else { |
| animateBounceEffect(); |
| } |
| } |
| |
| private void applyResourcesValues() { |
| // Sets the border appearance for the magnifier window |
| mMirrorBorderView.setBackground(mResources.getDrawable(mEditSizeEnable |
| ? R.drawable.accessibility_window_magnification_background_change |
| : R.drawable.accessibility_window_magnification_background)); |
| |
| // Changes the corner radius of the mMirrorSurfaceView |
| mMirrorSurfaceView.setCornerRadius( |
| TypedValue.applyDimension( |
| TypedValue.COMPLEX_UNIT_DIP, |
| mEditSizeEnable ? 16f : 28f, |
| mContext.getResources().getDisplayMetrics())); |
| |
| // Sets visibility of components for the magnifier window |
| if (mEditSizeEnable) { |
| mDragView.setVisibility(View.GONE); |
| mCloseView.setVisibility(View.VISIBLE); |
| mTopRightCornerView.setVisibility(View.VISIBLE); |
| mTopLeftCornerView.setVisibility(View.VISIBLE); |
| mBottomRightCornerView.setVisibility(View.VISIBLE); |
| mBottomLeftCornerView.setVisibility(View.VISIBLE); |
| } else { |
| mDragView.setVisibility(View.VISIBLE); |
| mCloseView.setVisibility(View.GONE); |
| mTopRightCornerView.setVisibility(View.GONE); |
| mTopLeftCornerView.setVisibility(View.GONE); |
| mBottomRightCornerView.setVisibility(View.GONE); |
| mBottomLeftCornerView.setVisibility(View.GONE); |
| } |
| } |
| |
| private boolean changeWindowSize(View view, float offsetX, float offsetY) { |
| if (view == mLeftDrag) { |
| changeMagnificationFrameSize(offsetX, 0, 0, 0); |
| } else if (view == mRightDrag) { |
| changeMagnificationFrameSize(0, 0, offsetX, 0); |
| } else if (view == mTopDrag) { |
| changeMagnificationFrameSize(0, offsetY, 0, 0); |
| } else if (view == mBottomDrag) { |
| changeMagnificationFrameSize(0, 0, 0, offsetY); |
| } else if (view == mTopLeftCornerView) { |
| changeMagnificationFrameSize(offsetX, offsetY, 0, 0); |
| } else if (view == mTopRightCornerView) { |
| changeMagnificationFrameSize(0, offsetY, offsetX, 0); |
| } else if (view == mBottomLeftCornerView) { |
| changeMagnificationFrameSize(offsetX, 0, 0, offsetY); |
| } else if (view == mBottomRightCornerView) { |
| changeMagnificationFrameSize(0, 0, offsetX, offsetY); |
| } else { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| private void changeMagnificationFrameSize( |
| float leftOffset, float topOffset, float rightOffset, |
| float bottomOffset) { |
| boolean bRTL = isRTL(mContext); |
| final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3; |
| |
| final int maxHeightSize = mWindowBounds.height() - 2 * mMirrorSurfaceMargin; |
| final int maxWidthSize = mWindowBounds.width() - 2 * mMirrorSurfaceMargin; |
| |
| Rect tempRect = new Rect(); |
| tempRect.set(mMagnificationFrame); |
| |
| if (bRTL) { |
| tempRect.left += (int) (rightOffset); |
| tempRect.right += (int) (leftOffset); |
| } else { |
| tempRect.right += (int) (rightOffset); |
| tempRect.left += (int) (leftOffset); |
| } |
| tempRect.top += (int) (topOffset); |
| tempRect.bottom += (int) (bottomOffset); |
| |
| if (tempRect.width() < initSize || tempRect.height() < initSize |
| || tempRect.width() > maxWidthSize || tempRect.height() > maxHeightSize) { |
| return; |
| } |
| mMagnificationFrame.set(tempRect); |
| |
| computeBounceAnimationScale(); |
| calculateMagnificationFrameBoundary(); |
| |
| modifyWindowMagnification(true); |
| } |
| |
| private static boolean isRTL(Context context) { |
| Configuration config = context.getResources().getConfiguration(); |
| if (config == null) { |
| return false; |
| } |
| return (config.screenLayout & Configuration.SCREENLAYOUT_LAYOUTDIR_MASK) |
| == Configuration.SCREENLAYOUT_LAYOUTDIR_RTL; |
| } |
| |
| @Override |
| public boolean onStart(float x, float y) { |
| mIsDragging = true; |
| return true; |
| } |
| |
| @Override |
| public boolean onFinish(float x, float y) { |
| maybeRepositionButton(); |
| mIsDragging = false; |
| return false; |
| } |
| |
| /** Moves the button to the opposite edge if the frame is against the edge of the screen. */ |
| private void maybeRepositionButton() { |
| if (mMirrorView == null) return; |
| |
| final float screenEdgeX = mWindowBounds.right - mButtonRepositionThresholdFromEdge; |
| final FrameLayout.LayoutParams layoutParams = |
| (FrameLayout.LayoutParams) mDragView.getLayoutParams(); |
| |
| mMirrorView.getBoundsOnScreen(mTmpRect); |
| |
| final int newGravity; |
| if (mTmpRect.right >= screenEdgeX) { |
| newGravity = Gravity.BOTTOM | Gravity.LEFT; |
| } else { |
| newGravity = Gravity.BOTTOM | Gravity.RIGHT; |
| } |
| if (newGravity != layoutParams.gravity) { |
| layoutParams.gravity = newGravity; |
| mDragView.setLayoutParams(layoutParams); |
| mDragView.post(this::applyTapExcludeRegion); |
| } |
| } |
| |
| void updateDragHandleResourcesIfNeeded(boolean settingsPanelIsShown) { |
| if (!isActivated()) { |
| return; |
| } |
| |
| mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown |
| ? R.drawable.accessibility_window_magnification_drag_handle_background_change |
| : R.drawable.accessibility_window_magnification_drag_handle_background)); |
| |
| PorterDuffColorFilter filter = new PorterDuffColorFilter( |
| mContext.getColor(settingsPanelIsShown |
| ? R.color.magnification_border_color |
| : R.color.magnification_drag_handle_stroke), |
| PorterDuff.Mode.SRC_ATOP); |
| |
| mDragView.setColorFilter(filter); |
| } |
| |
| 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(" mWindowBounds:" + mWindowBounds); |
| pw.println(" mMirrorViewBounds:" + (isActivated() ? mMirrorViewBounds : "empty")); |
| pw.println(" mMagnificationFrameBoundary:" |
| + (isActivated() ? mMagnificationFrameBoundary : "empty")); |
| pw.println(" mMagnificationFrame:" |
| + (isActivated() ? mMagnificationFrame : "empty")); |
| pw.println(" mSourceBounds:" |
| + (mSourceBounds.isEmpty() ? "empty" : mSourceBounds)); |
| pw.println(" mSystemGestureTop:" + mSystemGestureTop); |
| pw.println(" mMagnificationFrameOffsetX:" + mMagnificationFrameOffsetX); |
| pw.println(" mMagnificationFrameOffsetY:" + mMagnificationFrameOffsetY); |
| } |
| |
| private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate { |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(host, info); |
| final AccessibilityAction clickAction = new AccessibilityAction( |
| AccessibilityAction.ACTION_CLICK.getId(), mContext.getResources().getString( |
| R.string.magnification_open_settings_click_label)); |
| info.addAction(clickAction); |
| info.setClickable(true); |
| 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 == AccessibilityAction.ACTION_CLICK.getId()) { |
| // Simulate tapping the drag view so it opens the Settings. |
| handleSingleTap(mDragView); |
| } else 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; |
| } |
| } |
| } |