1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.accessibility; 18 19 import static android.view.WindowInsets.Type.systemGestures; 20 import static android.view.WindowManager.LayoutParams; 21 22 import static com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize; 23 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; 24 25 import static java.lang.Math.abs; 26 27 import android.animation.ObjectAnimator; 28 import android.animation.PropertyValuesHolder; 29 import android.annotation.MainThread; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.annotation.UiContext; 33 import android.content.ComponentCallbacks; 34 import android.content.Context; 35 import android.content.pm.ActivityInfo; 36 import android.content.res.Configuration; 37 import android.content.res.Resources; 38 import android.graphics.Insets; 39 import android.graphics.Matrix; 40 import android.graphics.PixelFormat; 41 import android.graphics.PorterDuff; 42 import android.graphics.PorterDuffColorFilter; 43 import android.graphics.Rect; 44 import android.graphics.RectF; 45 import android.graphics.Region; 46 import android.os.Build; 47 import android.os.Bundle; 48 import android.os.Handler; 49 import android.os.RemoteException; 50 import android.os.UserHandle; 51 import android.provider.Settings; 52 import android.util.Log; 53 import android.util.Range; 54 import android.util.Size; 55 import android.util.SparseArray; 56 import android.util.TypedValue; 57 import android.view.Choreographer; 58 import android.view.Display; 59 import android.view.Gravity; 60 import android.view.IWindow; 61 import android.view.IWindowSession; 62 import android.view.LayoutInflater; 63 import android.view.MotionEvent; 64 import android.view.Surface; 65 import android.view.SurfaceControl; 66 import android.view.SurfaceHolder; 67 import android.view.SurfaceView; 68 import android.view.View; 69 import android.view.WindowManager; 70 import android.view.WindowManagerGlobal; 71 import android.view.WindowMetrics; 72 import android.view.accessibility.AccessibilityNodeInfo; 73 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 74 import android.view.accessibility.IRemoteMagnificationAnimationCallback; 75 import android.widget.FrameLayout; 76 import android.widget.ImageView; 77 78 import androidx.core.math.MathUtils; 79 80 import com.android.internal.accessibility.common.MagnificationConstants; 81 import com.android.internal.annotations.VisibleForTesting; 82 import com.android.internal.graphics.SfVsyncFrameCallbackProvider; 83 import com.android.systemui.R; 84 import com.android.systemui.model.SysUiState; 85 import com.android.systemui.util.settings.SecureSettings; 86 87 import java.io.PrintWriter; 88 import java.text.NumberFormat; 89 import java.util.Collections; 90 import java.util.Locale; 91 import java.util.function.Supplier; 92 93 /** 94 * Class to handle adding and removing a window magnification. 95 */ 96 class WindowMagnificationController implements View.OnTouchListener, SurfaceHolder.Callback, 97 MirrorWindowControl.MirrorWindowDelegate, MagnificationGestureDetector.OnGestureListener, 98 ComponentCallbacks { 99 100 private static final String TAG = "WindowMagnificationController"; 101 @SuppressWarnings("isloggabletaglength") 102 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE; 103 // Delay to avoid updating state description too frequently. 104 private static final int UPDATE_STATE_DESCRIPTION_DELAY_MS = 100; 105 // It should be consistent with the value defined in WindowMagnificationGestureHandler. 106 private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>( 107 MagnificationConstants.SCALE_MIN_VALUE, 108 MagnificationConstants.SCALE_MAX_VALUE); 109 private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f; 110 private static final float ANIMATION_BOUNCE_EFFECT_SCALE = 1.05f; 111 private final SparseArray<Float> mMagnificationSizeScaleOptions = new SparseArray<>(); 112 113 private final Context mContext; 114 private final Resources mResources; 115 private final Handler mHandler; 116 private final Rect mWindowBounds; 117 private final int mDisplayId; 118 @Surface.Rotation 119 @VisibleForTesting 120 int mRotation; 121 private final SurfaceControl.Transaction mTransaction; 122 123 private final WindowManager mWm; 124 125 private float mScale; 126 127 /** 128 * MagnificationFrame represents the bound of {@link #mMirrorSurface} and is constrained 129 * by the {@link #mMagnificationFrameBoundary}. 130 * We use MagnificationFrame to calculate the position of {@link #mMirrorView}. 131 * We combine MagnificationFrame with {@link #mMagnificationFrameOffsetX} and 132 * {@link #mMagnificationFrameOffsetY} to calculate the position of {@link #mSourceBounds}. 133 */ 134 private final Rect mMagnificationFrame = new Rect(); 135 private final Rect mTmpRect = new Rect(); 136 137 /** 138 * MirrorViewBounds is the bound of the {@link #mMirrorView} which displays the magnified 139 * content. 140 * {@link #mMirrorView}'s center is equal to {@link #mMagnificationFrame}'s center. 141 */ 142 private final Rect mMirrorViewBounds = new Rect(); 143 144 /** 145 * SourceBound is the bound of the magnified region which projects the magnified content. 146 * SourceBound's center is equal to the parameters centerX and centerY in 147 * {@link WindowMagnificationController#enableWindowMagnificationInternal(float, float, float)}} 148 * but it is calculated from {@link #mMagnificationFrame}'s center in the runtime. 149 */ 150 private final Rect mSourceBounds = new Rect(); 151 152 /** 153 * The relation of centers between {@link #mSourceBounds} and {@link #mMagnificationFrame} is 154 * calculated in {@link #calculateSourceBounds(Rect, float)} and the equations are as following: 155 * MagnificationFrame = SourceBound (e.g., centerX & centerY) + MagnificationFrameOffset 156 * SourceBound = MagnificationFrame - MagnificationFrameOffset 157 */ 158 private int mMagnificationFrameOffsetX = 0; 159 private int mMagnificationFrameOffsetY = 0; 160 161 // The root of the mirrored content 162 private SurfaceControl mMirrorSurface; 163 164 private ImageView mDragView; 165 private ImageView mCloseView; 166 private View mLeftDrag; 167 private View mTopDrag; 168 private View mRightDrag; 169 private View mBottomDrag; 170 private ImageView mTopLeftCornerView; 171 private ImageView mTopRightCornerView; 172 private ImageView mBottomLeftCornerView; 173 private ImageView mBottomRightCornerView; 174 private final Configuration mConfiguration; 175 176 @NonNull 177 private final WindowMagnifierCallback mWindowMagnifierCallback; 178 179 private final View.OnLayoutChangeListener mMirrorViewLayoutChangeListener; 180 private final View.OnLayoutChangeListener mMirrorSurfaceViewLayoutChangeListener; 181 private final Runnable mMirrorViewRunnable; 182 private final Runnable mUpdateStateDescriptionRunnable; 183 private final Runnable mWindowInsetChangeRunnable; 184 // MirrorView is the mirror window which displays the magnified content. 185 private View mMirrorView; 186 private View mMirrorBorderView; 187 private SurfaceView mMirrorSurfaceView; 188 private int mMirrorSurfaceMargin; 189 private int mBorderDragSize; 190 private int mOuterBorderSize; 191 192 /** 193 * How far from the right edge of the screen you need to drag the window before the button 194 * repositions to the other side. 195 */ 196 private int mButtonRepositionThresholdFromEdge; 197 198 // The boundary of magnification frame. 199 private final Rect mMagnificationFrameBoundary = new Rect(); 200 // The top Y of the system gesture rect at the bottom. Set to -1 if it is invalid. 201 private int mSystemGestureTop = -1; 202 private int mMinWindowSize; 203 204 private final WindowMagnificationAnimationController mAnimationController; 205 private final Supplier<IWindowSession> mGlobalWindowSessionSupplier; 206 private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; 207 private final MagnificationGestureDetector mGestureDetector; 208 private final int mBounceEffectDuration; 209 private final Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback; 210 private Locale mLocale; 211 private NumberFormat mPercentFormat; 212 private float mBounceEffectAnimationScale; 213 private final SysUiState mSysUiState; 214 // Set it to true when the view is overlapped with the gesture insets at the bottom. 215 private boolean mOverlapWithGestureInsets; 216 private boolean mIsDragging; 217 218 private static final int MAX_HORIZONTAL_MOVE_ANGLE = 50; 219 private static final int HORIZONTAL = 1; 220 private static final int VERTICAL = 0; 221 222 @VisibleForTesting 223 static final double HORIZONTAL_LOCK_BASE = 224 Math.tan(Math.toRadians(MAX_HORIZONTAL_MOVE_ANGLE)); 225 226 private boolean mAllowDiagonalScrolling = false; 227 private boolean mEditSizeEnable = false; 228 private boolean mSettingsPanelVisibility = false; 229 230 @Nullable 231 private final MirrorWindowControl mMirrorWindowControl; 232 WindowMagnificationController( @iContext 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)233 WindowMagnificationController( 234 @UiContext Context context, 235 @NonNull Handler handler, 236 @NonNull WindowMagnificationAnimationController animationController, 237 SfVsyncFrameCallbackProvider sfVsyncFrameProvider, 238 MirrorWindowControl mirrorWindowControl, 239 SurfaceControl.Transaction transaction, 240 @NonNull WindowMagnifierCallback callback, 241 SysUiState sysUiState, 242 @NonNull Supplier<IWindowSession> globalWindowSessionSupplier, 243 SecureSettings secureSettings) { 244 mContext = context; 245 mHandler = handler; 246 mAnimationController = animationController; 247 mGlobalWindowSessionSupplier = globalWindowSessionSupplier; 248 mAnimationController.setWindowMagnificationController(this); 249 mSfVsyncFrameProvider = sfVsyncFrameProvider; 250 mWindowMagnifierCallback = callback; 251 mSysUiState = sysUiState; 252 mConfiguration = new Configuration(context.getResources().getConfiguration()); 253 254 final Display display = mContext.getDisplay(); 255 mDisplayId = mContext.getDisplayId(); 256 mRotation = display.getRotation(); 257 258 mWm = context.getSystemService(WindowManager.class); 259 mWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds()); 260 261 mResources = mContext.getResources(); 262 mScale = secureSettings.getFloatForUser( 263 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 264 mResources.getInteger(R.integer.magnification_default_scale), 265 UserHandle.USER_CURRENT); 266 267 setupMagnificationSizeScaleOptions(); 268 269 mBounceEffectDuration = mResources.getInteger( 270 com.android.internal.R.integer.config_shortAnimTime); 271 updateDimensions(); 272 273 final Size windowSize = getDefaultWindowSizeWithWindowBounds(mWindowBounds); 274 setMagnificationFrame(windowSize.getWidth(), windowSize.getHeight(), 275 mWindowBounds.width() / 2, mWindowBounds.height() / 2); 276 computeBounceAnimationScale(); 277 278 mMirrorWindowControl = mirrorWindowControl; 279 if (mMirrorWindowControl != null) { 280 mMirrorWindowControl.setWindowDelegate(this); 281 } 282 mTransaction = transaction; 283 mGestureDetector = 284 new MagnificationGestureDetector(mContext, handler, this); 285 286 // Initialize listeners. 287 mMirrorViewRunnable = () -> { 288 if (mMirrorView != null) { 289 final Rect oldViewBounds = new Rect(mMirrorViewBounds); 290 mMirrorView.getBoundsOnScreen(mMirrorViewBounds); 291 if (oldViewBounds.width() != mMirrorViewBounds.width() 292 || oldViewBounds.height() != mMirrorViewBounds.height()) { 293 mMirrorView.setSystemGestureExclusionRects(Collections.singletonList( 294 new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height()))); 295 } 296 updateSystemUIStateIfNeeded(); 297 mWindowMagnifierCallback.onWindowMagnifierBoundsChanged( 298 mDisplayId, mMirrorViewBounds); 299 } 300 }; 301 mMirrorViewLayoutChangeListener = 302 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 303 if (!mHandler.hasCallbacks(mMirrorViewRunnable)) { 304 mHandler.post(mMirrorViewRunnable); 305 } 306 }; 307 308 mMirrorSurfaceViewLayoutChangeListener = 309 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> 310 mMirrorView.post(this::applyTapExcludeRegion); 311 312 mMirrorViewGeometryVsyncCallback = 313 l -> { 314 if (isActivated() && mMirrorSurface != null && calculateSourceBounds( 315 mMagnificationFrame, mScale)) { 316 // The final destination for the magnification surface should be at 0,0 317 // since the ViewRootImpl's position will change 318 mTmpRect.set(0, 0, mMagnificationFrame.width(), 319 mMagnificationFrame.height()); 320 mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, 321 Surface.ROTATION_0).apply(); 322 323 // Notify source bounds change when the magnifier is not animating. 324 if (!mAnimationController.isAnimating()) { 325 mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, 326 mSourceBounds); 327 } 328 } 329 }; 330 mUpdateStateDescriptionRunnable = () -> { 331 if (isActivated()) { 332 mMirrorView.setStateDescription(formatStateDescription(mScale)); 333 } 334 }; 335 mWindowInsetChangeRunnable = this::onWindowInsetChanged; 336 } 337 setupMagnificationSizeScaleOptions()338 private void setupMagnificationSizeScaleOptions() { 339 mMagnificationSizeScaleOptions.clear(); 340 mMagnificationSizeScaleOptions.put(MagnificationSize.SMALL, 1.4f); 341 mMagnificationSizeScaleOptions.put(MagnificationSize.MEDIUM, 1.8f); 342 mMagnificationSizeScaleOptions.put(MagnificationSize.LARGE, 2.5f); 343 } 344 updateDimensions()345 private void updateDimensions() { 346 mMirrorSurfaceMargin = mResources.getDimensionPixelSize( 347 R.dimen.magnification_mirror_surface_margin); 348 mBorderDragSize = mResources.getDimensionPixelSize( 349 R.dimen.magnification_border_drag_size); 350 mOuterBorderSize = mResources.getDimensionPixelSize( 351 R.dimen.magnification_outer_border_margin); 352 mButtonRepositionThresholdFromEdge = 353 mResources.getDimensionPixelSize( 354 R.dimen.magnification_button_reposition_threshold_from_edge); 355 mMinWindowSize = mResources.getDimensionPixelSize( 356 com.android.internal.R.dimen.accessibility_window_magnifier_min_size); 357 } 358 computeBounceAnimationScale()359 private void computeBounceAnimationScale() { 360 final float windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; 361 final float visibleWindowWidth = windowWidth - 2 * mOuterBorderSize; 362 final float animationScaleMax = windowWidth / visibleWindowWidth; 363 mBounceEffectAnimationScale = Math.min(animationScaleMax, ANIMATION_BOUNCE_EFFECT_SCALE); 364 } 365 updateSystemGestureInsetsTop()366 private boolean updateSystemGestureInsetsTop() { 367 final WindowMetrics windowMetrics = mWm.getCurrentWindowMetrics(); 368 final Insets insets = windowMetrics.getWindowInsets().getInsets(systemGestures()); 369 final int gestureTop = 370 insets.bottom != 0 ? windowMetrics.getBounds().bottom - insets.bottom : -1; 371 if (gestureTop != mSystemGestureTop) { 372 mSystemGestureTop = gestureTop; 373 return true; 374 } 375 return false; 376 } 377 changeMagnificationSize(@agnificationSize int index)378 void changeMagnificationSize(@MagnificationSize int index) { 379 if (!mMagnificationSizeScaleOptions.contains(index)) { 380 return; 381 } 382 final float scale = mMagnificationSizeScaleOptions.get(index, 1.0f); 383 final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3; 384 int size = (int) (initSize * scale); 385 setWindowSize(size, size); 386 } 387 setEditMagnifierSizeMode(boolean enable)388 void setEditMagnifierSizeMode(boolean enable) { 389 mEditSizeEnable = enable; 390 applyResourcesValues(); 391 392 if (isActivated()) { 393 updateDimensions(); 394 applyTapExcludeRegion(); 395 } 396 } 397 setDiagonalScrolling(boolean enable)398 void setDiagonalScrolling(boolean enable) { 399 mAllowDiagonalScrolling = enable; 400 } 401 402 /** 403 * Wraps {@link WindowMagnificationController#deleteWindowMagnification()}} with transition 404 * animation. If the window magnification is enabling, it runs the animation in reverse. 405 * 406 * @param animationCallback Called when the transition is complete, the given arguments 407 * are as same as current values, or the transition is interrupted 408 * due to the new transition request. 409 */ deleteWindowMagnification( @ullable IRemoteMagnificationAnimationCallback animationCallback)410 void deleteWindowMagnification( 411 @Nullable IRemoteMagnificationAnimationCallback animationCallback) { 412 mAnimationController.deleteWindowMagnification(animationCallback); 413 } 414 415 /** 416 * Deletes the magnification window. 417 */ deleteWindowMagnification()418 void deleteWindowMagnification() { 419 if (!isActivated()) { 420 return; 421 } 422 423 if (mMirrorSurface != null) { 424 mTransaction.remove(mMirrorSurface).apply(); 425 mMirrorSurface = null; 426 } 427 428 if (mMirrorSurfaceView != null) { 429 mMirrorSurfaceView.removeOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener); 430 } 431 432 if (mMirrorView != null) { 433 mHandler.removeCallbacks(mMirrorViewRunnable); 434 mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener); 435 mWm.removeView(mMirrorView); 436 mMirrorView = null; 437 } 438 439 if (mMirrorWindowControl != null) { 440 mMirrorWindowControl.destroyControl(); 441 } 442 mMirrorViewBounds.setEmpty(); 443 mSourceBounds.setEmpty(); 444 updateSystemUIStateIfNeeded(); 445 setEditMagnifierSizeMode(false); 446 447 mContext.unregisterComponentCallbacks(this); 448 // Notify source bounds empty when magnification is deleted. 449 mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, new Rect()); 450 } 451 452 @Override onConfigurationChanged(@onNull Configuration newConfig)453 public void onConfigurationChanged(@NonNull Configuration newConfig) { 454 final int configDiff = newConfig.diff(mConfiguration); 455 mConfiguration.setTo(newConfig); 456 onConfigurationChanged(configDiff); 457 } 458 459 @Override onLowMemory()460 public void onLowMemory() { 461 } 462 463 /** 464 * Called when the configuration has changed, and it updates window magnification UI. 465 * 466 * @param configDiff a bit mask of the differences between the configurations 467 */ onConfigurationChanged(int configDiff)468 void onConfigurationChanged(int configDiff) { 469 if (DEBUG) { 470 Log.d(TAG, "onConfigurationChanged = " + Configuration.configurationDiffToString( 471 configDiff)); 472 } 473 if (configDiff == 0) { 474 return; 475 } 476 if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) { 477 onRotate(); 478 } 479 480 if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) { 481 updateAccessibilityWindowTitleIfNeeded(); 482 } 483 484 boolean reCreateWindow = false; 485 if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) { 486 updateDimensions(); 487 computeBounceAnimationScale(); 488 reCreateWindow = true; 489 } 490 491 if ((configDiff & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) { 492 reCreateWindow |= handleScreenSizeChanged(); 493 } 494 495 // Recreate the window again to correct the window appearance due to density or 496 // window size changed not caused by rotation. 497 if (isActivated() && reCreateWindow) { 498 deleteWindowMagnification(); 499 enableWindowMagnificationInternal(Float.NaN, Float.NaN, Float.NaN); 500 } 501 } 502 503 /** 504 * Calculates the magnification frame if the window bounds is changed. 505 * Note that the orientation also changes the wind bounds, so it should be handled first. 506 * 507 * @return {@code true} if the magnification frame is changed with the new window bounds. 508 */ handleScreenSizeChanged()509 private boolean handleScreenSizeChanged() { 510 final Rect oldWindowBounds = new Rect(mWindowBounds); 511 final Rect currentWindowBounds = mWm.getCurrentWindowMetrics().getBounds(); 512 513 if (currentWindowBounds.equals(oldWindowBounds)) { 514 if (DEBUG) { 515 Log.d(TAG, "handleScreenSizeChanged -- window bounds is not changed"); 516 } 517 return false; 518 } 519 mWindowBounds.set(currentWindowBounds); 520 final Size windowSize = getDefaultWindowSizeWithWindowBounds(mWindowBounds); 521 final float newCenterX = (getCenterX()) * mWindowBounds.width() / oldWindowBounds.width(); 522 final float newCenterY = (getCenterY()) * mWindowBounds.height() / oldWindowBounds.height(); 523 524 setMagnificationFrame(windowSize.getWidth(), windowSize.getHeight(), (int) newCenterX, 525 (int) newCenterY); 526 calculateMagnificationFrameBoundary(); 527 return true; 528 } 529 updateSystemUIStateIfNeeded()530 private void updateSystemUIStateIfNeeded() { 531 updateSysUIState(false); 532 } 533 updateAccessibilityWindowTitleIfNeeded()534 private void updateAccessibilityWindowTitleIfNeeded() { 535 if (!isActivated()) return; 536 LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); 537 params.accessibilityTitle = getAccessibilityWindowTitle(); 538 mWm.updateViewLayout(mMirrorView, params); 539 } 540 541 /** 542 * Keep MirrorWindow position on the screen unchanged when device rotates 90° clockwise or 543 * anti-clockwise. 544 */ onRotate()545 private void onRotate() { 546 final Display display = mContext.getDisplay(); 547 final int oldRotation = mRotation; 548 mRotation = display.getRotation(); 549 final int rotationDegree = getDegreeFromRotation(mRotation, oldRotation); 550 if (rotationDegree == 0 || rotationDegree == 180) { 551 Log.w(TAG, "onRotate -- rotate with the device. skip it"); 552 return; 553 } 554 final Rect currentWindowBounds = new Rect(mWm.getCurrentWindowMetrics().getBounds()); 555 if (currentWindowBounds.width() != mWindowBounds.height() 556 || currentWindowBounds.height() != mWindowBounds.width()) { 557 Log.w(TAG, "onRotate -- unexpected window height/width"); 558 return; 559 } 560 561 mWindowBounds.set(currentWindowBounds); 562 563 // Keep MirrorWindow position on the screen unchanged when device rotates 90° 564 // clockwise or anti-clockwise. 565 566 final Matrix matrix = new Matrix(); 567 matrix.setRotate(rotationDegree); 568 if (rotationDegree == 90) { 569 matrix.postTranslate(mWindowBounds.width(), 0); 570 } else if (rotationDegree == 270) { 571 matrix.postTranslate(0, mWindowBounds.height()); 572 } 573 574 final RectF transformedRect = new RectF(mMagnificationFrame); 575 // The window frame is going to be transformed by the rotation matrix. 576 transformedRect.inset(-mMirrorSurfaceMargin, -mMirrorSurfaceMargin); 577 matrix.mapRect(transformedRect); 578 setWindowSizeAndCenter((int) transformedRect.width(), (int) transformedRect.height(), 579 (int) transformedRect.centerX(), (int) transformedRect.centerY()); 580 } 581 582 /** Returns the rotation degree change of two {@link Surface.Rotation} */ getDegreeFromRotation(@urface.Rotation int newRotation, @Surface.Rotation int oldRotation)583 private int getDegreeFromRotation(@Surface.Rotation int newRotation, 584 @Surface.Rotation int oldRotation) { 585 return (oldRotation - newRotation + 4) % 4 * 90; 586 } 587 createMirrorWindow()588 private void createMirrorWindow() { 589 // The window should be the size the mirrored surface will be but also add room for the 590 // border and the drag handle. 591 int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; 592 int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin; 593 594 LayoutParams params = new LayoutParams( 595 windowWidth, windowHeight, 596 LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, 597 LayoutParams.FLAG_NOT_TOUCH_MODAL 598 | LayoutParams.FLAG_NOT_FOCUSABLE, 599 PixelFormat.TRANSPARENT); 600 params.gravity = Gravity.TOP | Gravity.LEFT; 601 params.x = mMagnificationFrame.left - mMirrorSurfaceMargin; 602 params.y = mMagnificationFrame.top - mMirrorSurfaceMargin; 603 params.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; 604 params.receiveInsetsIgnoringZOrder = true; 605 params.setTitle(mContext.getString(R.string.magnification_window_title)); 606 params.accessibilityTitle = getAccessibilityWindowTitle(); 607 608 mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null); 609 mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view); 610 611 mMirrorBorderView = mMirrorView.findViewById(R.id.magnification_inner_border); 612 613 // Allow taps to go through to the mirror SurfaceView below. 614 mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener); 615 616 mMirrorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE 617 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 618 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 619 | View.SYSTEM_UI_FLAG_FULLSCREEN 620 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY 621 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); 622 mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener); 623 mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate()); 624 mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> { 625 if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) { 626 mHandler.post(mWindowInsetChangeRunnable); 627 } 628 return v.onApplyWindowInsets(insets); 629 }); 630 631 mWm.addView(mMirrorView, params); 632 633 SurfaceHolder holder = mMirrorSurfaceView.getHolder(); 634 holder.addCallback(this); 635 holder.setFormat(PixelFormat.RGBA_8888); 636 addDragTouchListeners(); 637 } 638 onWindowInsetChanged()639 private void onWindowInsetChanged() { 640 if (updateSystemGestureInsetsTop()) { 641 updateSystemUIStateIfNeeded(); 642 } 643 } 644 applyTapExcludeRegion()645 private void applyTapExcludeRegion() { 646 // Sometimes this can get posted and run after deleteWindowMagnification() is called. 647 if (mMirrorView == null) return; 648 649 final Region tapExcludeRegion = calculateTapExclude(); 650 final IWindow window = IWindow.Stub.asInterface(mMirrorView.getWindowToken()); 651 try { 652 IWindowSession session = mGlobalWindowSessionSupplier.get(); 653 session.updateTapExcludeRegion(window, tapExcludeRegion); 654 } catch (RemoteException e) { 655 } 656 } 657 calculateTapExclude()658 private Region calculateTapExclude() { 659 Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize, 660 mMirrorView.getWidth() - mBorderDragSize, 661 mMirrorView.getHeight() - mBorderDragSize); 662 663 Region tapExcludeRegion = new Region(); 664 665 Rect dragArea = new Rect(); 666 mDragView.getHitRect(dragArea); 667 668 Rect topLeftArea = new Rect(); 669 mTopLeftCornerView.getHitRect(topLeftArea); 670 671 Rect topRightArea = new Rect(); 672 mTopRightCornerView.getHitRect(topRightArea); 673 674 Rect bottomLeftArea = new Rect(); 675 mBottomLeftCornerView.getHitRect(bottomLeftArea); 676 677 Rect bottomRightArea = new Rect(); 678 mBottomRightCornerView.getHitRect(bottomRightArea); 679 680 Rect closeArea = new Rect(); 681 mCloseView.getHitRect(closeArea); 682 683 // add tapExcludeRegion for Drag or close 684 tapExcludeRegion.op(dragArea, Region.Op.UNION); 685 tapExcludeRegion.op(topLeftArea, Region.Op.UNION); 686 tapExcludeRegion.op(topRightArea, Region.Op.UNION); 687 tapExcludeRegion.op(bottomLeftArea, Region.Op.UNION); 688 tapExcludeRegion.op(bottomRightArea, Region.Op.UNION); 689 tapExcludeRegion.op(closeArea, Region.Op.UNION); 690 691 regionInsideDragBorder.op(tapExcludeRegion, Region.Op.DIFFERENCE); 692 693 return regionInsideDragBorder; 694 } 695 getAccessibilityWindowTitle()696 private String getAccessibilityWindowTitle() { 697 return mResources.getString(com.android.internal.R.string.android_system_label); 698 } 699 showControls()700 private void showControls() { 701 if (mMirrorWindowControl != null) { 702 mMirrorWindowControl.showControl(); 703 } 704 } 705 706 /** 707 * Sets the window frame size with given width and height in pixels without changing the 708 * window center. 709 * 710 * @param width the window frame width in pixels 711 * @param height the window frame height in pixels. 712 */ 713 @MainThread setMagnificationFrameSize(int width, int height)714 private void setMagnificationFrameSize(int width, int height) { 715 setWindowSize(width + 2 * mMirrorSurfaceMargin, height + 2 * mMirrorSurfaceMargin); 716 } 717 718 /** 719 * Sets the window size with given width and height in pixels without changing the 720 * window center. The width or the height will be clamped in the range 721 * [{@link #mMinWindowSize}, screen width or height]. 722 * 723 * @param width the window width in pixels 724 * @param height the window height in pixels. 725 */ setWindowSize(int width, int height)726 public void setWindowSize(int width, int height) { 727 setWindowSizeAndCenter(width, height, Float.NaN, Float.NaN); 728 } 729 setWindowSizeAndCenter(int width, int height, float centerX, float centerY)730 void setWindowSizeAndCenter(int width, int height, float centerX, float centerY) { 731 width = MathUtils.clamp(width, mMinWindowSize, mWindowBounds.width()); 732 height = MathUtils.clamp(height, mMinWindowSize, mWindowBounds.height()); 733 734 if (Float.isNaN(centerX)) { 735 centerX = mMagnificationFrame.centerX(); 736 } 737 if (Float.isNaN(centerY)) { 738 centerY = mMagnificationFrame.centerY(); 739 } 740 741 final int frameWidth = width - 2 * mMirrorSurfaceMargin; 742 final int frameHeight = height - 2 * mMirrorSurfaceMargin; 743 setMagnificationFrame(frameWidth, frameHeight, (int) centerX, (int) centerY); 744 calculateMagnificationFrameBoundary(); 745 // Correct the frame position to ensure it is inside the boundary. 746 updateMagnificationFramePosition(0, 0); 747 modifyWindowMagnification(true); 748 } 749 setMagnificationFrame(int width, int height, int centerX, int centerY)750 private void setMagnificationFrame(int width, int height, int centerX, int centerY) { 751 // Sets the initial frame area for the mirror and place it to the given center on the 752 // display. 753 final int initX = centerX - width / 2; 754 final int initY = centerY - height / 2; 755 mMagnificationFrame.set(initX, initY, initX + width, initY + height); 756 } 757 getDefaultWindowSizeWithWindowBounds(Rect windowBounds)758 private Size getDefaultWindowSizeWithWindowBounds(Rect windowBounds) { 759 int initSize = Math.min(windowBounds.width(), windowBounds.height()) / 2; 760 initSize = Math.min(mResources.getDimensionPixelSize(R.dimen.magnification_max_frame_size), 761 initSize); 762 initSize += 2 * mMirrorSurfaceMargin; 763 return new Size(initSize, initSize); 764 } 765 766 /** 767 * This is called once the surfaceView is created so the mirrored content can be placed as a 768 * child of the surfaceView. 769 */ createMirror()770 private void createMirror() { 771 mMirrorSurface = mirrorDisplay(mDisplayId); 772 if (!mMirrorSurface.isValid()) { 773 return; 774 } 775 mTransaction.show(mMirrorSurface) 776 .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl()); 777 modifyWindowMagnification(false); 778 } 779 780 /** 781 * Mirrors a specified display. The SurfaceControl returned is the root of the mirrored 782 * hierarchy. 783 * 784 * @param displayId The id of the display to mirror 785 * @return The SurfaceControl for the root of the mirrored hierarchy. 786 */ mirrorDisplay(final int displayId)787 private SurfaceControl mirrorDisplay(final int displayId) { 788 try { 789 SurfaceControl outSurfaceControl = new SurfaceControl(); 790 WindowManagerGlobal.getWindowManagerService().mirrorDisplay(displayId, 791 outSurfaceControl); 792 return outSurfaceControl; 793 } catch (RemoteException e) { 794 Log.e(TAG, "Unable to reach window manager", e); 795 } 796 return null; 797 } 798 addDragTouchListeners()799 private void addDragTouchListeners() { 800 mDragView = mMirrorView.findViewById(R.id.drag_handle); 801 mLeftDrag = mMirrorView.findViewById(R.id.left_handle); 802 mTopDrag = mMirrorView.findViewById(R.id.top_handle); 803 mRightDrag = mMirrorView.findViewById(R.id.right_handle); 804 mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle); 805 mCloseView = mMirrorView.findViewById(R.id.close_button); 806 mTopRightCornerView = mMirrorView.findViewById(R.id.top_right_corner); 807 mTopLeftCornerView = mMirrorView.findViewById(R.id.top_left_corner); 808 mBottomRightCornerView = mMirrorView.findViewById(R.id.bottom_right_corner); 809 mBottomLeftCornerView = mMirrorView.findViewById(R.id.bottom_left_corner); 810 811 mDragView.setOnTouchListener(this); 812 mLeftDrag.setOnTouchListener(this); 813 mTopDrag.setOnTouchListener(this); 814 mRightDrag.setOnTouchListener(this); 815 mBottomDrag.setOnTouchListener(this); 816 mCloseView.setOnTouchListener(this); 817 mTopLeftCornerView.setOnTouchListener(this); 818 mTopRightCornerView.setOnTouchListener(this); 819 mBottomLeftCornerView.setOnTouchListener(this); 820 mBottomRightCornerView.setOnTouchListener(this); 821 } 822 823 /** 824 * Modifies the placement of the mirrored content when the position or size of mMirrorView is 825 * updated. 826 * 827 * @param computeWindowSize set to {@code true} to compute window size with 828 * {@link #mMagnificationFrame}. 829 */ modifyWindowMagnification(boolean computeWindowSize)830 private void modifyWindowMagnification(boolean computeWindowSize) { 831 mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback); 832 updateMirrorViewLayout(computeWindowSize); 833 } 834 835 /** 836 * Updates the layout params of MirrorView based on the size of {@link #mMagnificationFrame} 837 * and translates MirrorView position when the view is moved close to the screen edges; 838 * 839 * @param computeWindowSize set to {@code true} to compute window size with 840 * {@link #mMagnificationFrame}. 841 */ updateMirrorViewLayout(boolean computeWindowSize)842 private void updateMirrorViewLayout(boolean computeWindowSize) { 843 if (!isActivated()) { 844 return; 845 } 846 final int maxMirrorViewX = mWindowBounds.width() - mMirrorView.getWidth(); 847 final int maxMirrorViewY = mWindowBounds.height() - mMirrorView.getHeight(); 848 849 LayoutParams params = 850 (LayoutParams) mMirrorView.getLayoutParams(); 851 params.x = mMagnificationFrame.left - mMirrorSurfaceMargin; 852 params.y = mMagnificationFrame.top - mMirrorSurfaceMargin; 853 if (computeWindowSize) { 854 params.width = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; 855 params.height = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin; 856 } 857 858 // Translates MirrorView position to make MirrorSurfaceView that is inside MirrorView 859 // able to move close to the screen edges. 860 final float translationX; 861 final float translationY; 862 if (params.x < 0) { 863 translationX = Math.max(params.x, -mOuterBorderSize); 864 } else if (params.x > maxMirrorViewX) { 865 translationX = Math.min(params.x - maxMirrorViewX, mOuterBorderSize); 866 } else { 867 translationX = 0; 868 } 869 if (params.y < 0) { 870 translationY = Math.max(params.y, -mOuterBorderSize); 871 } else if (params.y > maxMirrorViewY) { 872 translationY = Math.min(params.y - maxMirrorViewY, mOuterBorderSize); 873 } else { 874 translationY = 0; 875 } 876 mMirrorView.setTranslationX(translationX); 877 mMirrorView.setTranslationY(translationY); 878 mWm.updateViewLayout(mMirrorView, params); 879 880 // If they are not dragging the handle, we can move the drag handle immediately without 881 // disruption. But if they are dragging it, we avoid moving until the end of the drag. 882 if (!mIsDragging) { 883 mMirrorView.post(this::maybeRepositionButton); 884 } 885 } 886 887 @Override onTouch(View v, MotionEvent event)888 public boolean onTouch(View v, MotionEvent event) { 889 if (v == mDragView 890 || v == mLeftDrag 891 || v == mTopDrag 892 || v == mRightDrag 893 || v == mBottomDrag 894 || v == mTopLeftCornerView 895 || v == mTopRightCornerView 896 || v == mBottomLeftCornerView 897 || v == mBottomRightCornerView 898 || v == mCloseView) { 899 return mGestureDetector.onTouch(v, event); 900 } 901 return false; 902 } 903 updateSysUIStateFlag()904 public void updateSysUIStateFlag() { 905 updateSysUIState(true); 906 } 907 908 /** 909 * Calculates the desired source bounds. This will be the area under from the center of the 910 * displayFrame, factoring in scale. 911 * 912 * @return {@code true} if the source bounds is changed. 913 */ calculateSourceBounds(Rect displayFrame, float scale)914 private boolean calculateSourceBounds(Rect displayFrame, float scale) { 915 final Rect oldSourceBounds = mTmpRect; 916 oldSourceBounds.set(mSourceBounds); 917 int halfWidth = displayFrame.width() / 2; 918 int halfHeight = displayFrame.height() / 2; 919 int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale)); 920 int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale)); 921 int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale)); 922 int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale)); 923 924 mSourceBounds.set(left, top, right, bottom); 925 926 // SourceBound's center is equal to center[X,Y] but calculated from MagnificationFrame's 927 // center. The relation between SourceBound and MagnificationFrame is as following: 928 // MagnificationFrame = SourceBound (center[X,Y]) + MagnificationFrameOffset 929 // SourceBound = MagnificationFrame - MagnificationFrameOffset 930 mSourceBounds.offset(-mMagnificationFrameOffsetX, -mMagnificationFrameOffsetY); 931 932 if (mSourceBounds.left < 0) { 933 mSourceBounds.offsetTo(0, mSourceBounds.top); 934 } else if (mSourceBounds.right > mWindowBounds.width()) { 935 mSourceBounds.offsetTo(mWindowBounds.width() - mSourceBounds.width(), 936 mSourceBounds.top); 937 } 938 939 if (mSourceBounds.top < 0) { 940 mSourceBounds.offsetTo(mSourceBounds.left, 0); 941 } else if (mSourceBounds.bottom > mWindowBounds.height()) { 942 mSourceBounds.offsetTo(mSourceBounds.left, 943 mWindowBounds.height() - mSourceBounds.height()); 944 } 945 return !mSourceBounds.equals(oldSourceBounds); 946 } 947 calculateMagnificationFrameBoundary()948 private void calculateMagnificationFrameBoundary() { 949 // Calculates width and height for magnification frame could exceed out the screen. 950 // TODO : re-calculating again when scale is changed. 951 // The half width of magnification frame. 952 final int halfWidth = mMagnificationFrame.width() / 2; 953 // The half height of magnification frame. 954 final int halfHeight = mMagnificationFrame.height() / 2; 955 // The scaled half width of magnified region. 956 final int scaledWidth = (int) (halfWidth / mScale); 957 // The scaled half height of magnified region. 958 final int scaledHeight = (int) (halfHeight / mScale); 959 960 // MagnificationFrameBoundary constrain the space of MagnificationFrame, and it also has 961 // to leave enough space for SourceBound to magnify the whole screen space. 962 // However, there is an offset between SourceBound and MagnificationFrame. 963 // The relation between SourceBound and MagnificationFrame is as following: 964 // SourceBound = MagnificationFrame - MagnificationFrameOffset 965 // Therefore, we have to adjust the exceededBoundary based on the offset. 966 // 967 // We have to increase the offset space for the SourceBound edges which are located in 968 // the MagnificationFrame. For example, if the offsetX and offsetY are negative, which 969 // means SourceBound is at right-bottom size of MagnificationFrame, the left and top 970 // edges of SourceBound are located in MagnificationFrame. So, we have to leave extra 971 // offset space at left and top sides and don't have to leave extra space at right and 972 // bottom sides. 973 final int exceededLeft = Math.max(halfWidth - scaledWidth - mMagnificationFrameOffsetX, 0); 974 final int exceededRight = Math.max(halfWidth - scaledWidth + mMagnificationFrameOffsetX, 0); 975 final int exceededTop = Math.max(halfHeight - scaledHeight - mMagnificationFrameOffsetY, 0); 976 final int exceededBottom = Math.max(halfHeight - scaledHeight + mMagnificationFrameOffsetY, 977 0); 978 979 mMagnificationFrameBoundary.set( 980 -exceededLeft, 981 -exceededTop, 982 mWindowBounds.width() + exceededRight, 983 mWindowBounds.height() + exceededBottom); 984 } 985 986 /** 987 * Calculates and sets the real position of magnification frame based on the magnified region 988 * should be limited by the region of the display. 989 */ updateMagnificationFramePosition(int xOffset, int yOffset)990 private boolean updateMagnificationFramePosition(int xOffset, int yOffset) { 991 mTmpRect.set(mMagnificationFrame); 992 mTmpRect.offset(xOffset, yOffset); 993 994 if (mTmpRect.left < mMagnificationFrameBoundary.left) { 995 mTmpRect.offsetTo(mMagnificationFrameBoundary.left, mTmpRect.top); 996 } else if (mTmpRect.right > mMagnificationFrameBoundary.right) { 997 final int leftOffset = mMagnificationFrameBoundary.right - mMagnificationFrame.width(); 998 mTmpRect.offsetTo(leftOffset, mTmpRect.top); 999 } 1000 1001 if (mTmpRect.top < mMagnificationFrameBoundary.top) { 1002 mTmpRect.offsetTo(mTmpRect.left, mMagnificationFrameBoundary.top); 1003 } else if (mTmpRect.bottom > mMagnificationFrameBoundary.bottom) { 1004 final int topOffset = mMagnificationFrameBoundary.bottom - mMagnificationFrame.height(); 1005 mTmpRect.offsetTo(mTmpRect.left, topOffset); 1006 } 1007 1008 if (!mTmpRect.equals(mMagnificationFrame)) { 1009 mMagnificationFrame.set(mTmpRect); 1010 return true; 1011 } 1012 return false; 1013 } 1014 updateSysUIState(boolean force)1015 private void updateSysUIState(boolean force) { 1016 final boolean overlap = isActivated() && mSystemGestureTop > 0 1017 && mMirrorViewBounds.bottom > mSystemGestureTop; 1018 if (force || overlap != mOverlapWithGestureInsets) { 1019 mOverlapWithGestureInsets = overlap; 1020 mSysUiState.setFlag(SYSUI_STATE_MAGNIFICATION_OVERLAP, mOverlapWithGestureInsets) 1021 .commitUpdate(mDisplayId); 1022 } 1023 } 1024 1025 @Override surfaceCreated(SurfaceHolder holder)1026 public void surfaceCreated(SurfaceHolder holder) { 1027 createMirror(); 1028 } 1029 1030 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)1031 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 1032 } 1033 1034 @Override surfaceDestroyed(SurfaceHolder holder)1035 public void surfaceDestroyed(SurfaceHolder holder) { 1036 } 1037 1038 @Override move(int xOffset, int yOffset)1039 public void move(int xOffset, int yOffset) { 1040 moveWindowMagnifier(xOffset, yOffset); 1041 mWindowMagnifierCallback.onMove(mDisplayId); 1042 } 1043 1044 /** 1045 * Wraps {@link WindowMagnificationController#enableWindowMagnificationInternal(float, float, 1046 * float, float, float)} 1047 * with transition animation. If the window magnification is not enabled, the scale will start 1048 * from 1.0 and the center won't be changed during the animation. If animator is 1049 * {@code STATE_DISABLING}, the animation runs in reverse. 1050 * 1051 * @param scale The target scale, or {@link Float#NaN} to leave unchanged. 1052 * @param centerX The screen-relative X coordinate around which to center for magnification, 1053 * or {@link Float#NaN} to leave unchanged. 1054 * @param centerY The screen-relative Y coordinate around which to center for magnification, 1055 * or {@link Float#NaN} to leave unchanged. 1056 * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset 1057 * between frame position X and centerX 1058 * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset 1059 * between frame position Y and centerY 1060 * @param animationCallback Called when the transition is complete, the given arguments 1061 * are as same as current values, or the transition is interrupted 1062 * due to the new transition request. 1063 */ enableWindowMagnification(float scale, float centerX, float centerY, float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, @Nullable IRemoteMagnificationAnimationCallback animationCallback)1064 public void enableWindowMagnification(float scale, float centerX, float centerY, 1065 float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY, 1066 @Nullable IRemoteMagnificationAnimationCallback animationCallback) { 1067 mAnimationController.enableWindowMagnification(scale, centerX, centerY, 1068 magnificationFrameOffsetRatioX, magnificationFrameOffsetRatioY, animationCallback); 1069 } 1070 1071 /** 1072 * Enables window magnification with specified parameters. If the given scale is <strong>less 1073 * than or equal to 1.0f<strong>, then 1074 * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to 1075 * be consistent with the behavior of display magnification. 1076 * 1077 * @param scale the target scale, or {@link Float#NaN} to leave unchanged 1078 * @param centerX the screen-relative X coordinate around which to center for magnification, 1079 * or {@link Float#NaN} to leave unchanged. 1080 * @param centerY the screen-relative Y coordinate around which to center for magnification, 1081 * or {@link Float#NaN} to leave unchanged. 1082 */ enableWindowMagnificationInternal(float scale, float centerX, float centerY)1083 void enableWindowMagnificationInternal(float scale, float centerX, float centerY) { 1084 enableWindowMagnificationInternal(scale, centerX, centerY, Float.NaN, Float.NaN); 1085 } 1086 1087 /** 1088 * Enables window magnification with specified parameters. If the given scale is <strong>less 1089 * than 1.0f<strong>, then 1090 * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to 1091 * be consistent with the behavior of display magnification. 1092 * 1093 * @param scale the target scale, or {@link Float#NaN} to leave unchanged 1094 * @param centerX the screen-relative X coordinate around which to center for magnification, 1095 * or {@link Float#NaN} to leave unchanged. 1096 * @param centerY the screen-relative Y coordinate around which to center for magnification, 1097 * or {@link Float#NaN} to leave unchanged. 1098 * @param magnificationFrameOffsetRatioX Indicate the X coordinate offset 1099 * between frame position X and centerX, 1100 * or {@link Float#NaN} to leave unchanged. 1101 * @param magnificationFrameOffsetRatioY Indicate the Y coordinate offset 1102 * between frame position Y and centerY, 1103 * or {@link Float#NaN} to leave unchanged. 1104 */ enableWindowMagnificationInternal(float scale, float centerX, float centerY, float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY)1105 void enableWindowMagnificationInternal(float scale, float centerX, float centerY, 1106 float magnificationFrameOffsetRatioX, float magnificationFrameOffsetRatioY) { 1107 if (Float.compare(scale, 1.0f) < 0) { 1108 deleteWindowMagnification(); 1109 return; 1110 } 1111 if (!isActivated()) { 1112 onConfigurationChanged(mResources.getConfiguration()); 1113 mContext.registerComponentCallbacks(this); 1114 } 1115 1116 mMagnificationFrameOffsetX = Float.isNaN(magnificationFrameOffsetRatioX) 1117 ? mMagnificationFrameOffsetX 1118 : (int) (mMagnificationFrame.width() / 2 * magnificationFrameOffsetRatioX); 1119 mMagnificationFrameOffsetY = Float.isNaN(magnificationFrameOffsetRatioY) 1120 ? mMagnificationFrameOffsetY 1121 : (int) (mMagnificationFrame.height() / 2 * magnificationFrameOffsetRatioY); 1122 1123 // The relation of centers between SourceBound and MagnificationFrame is as following: 1124 // MagnificationFrame = SourceBound (e.g., centerX & centerY) + MagnificationFrameOffset 1125 final float newMagnificationFrameCenterX = centerX + mMagnificationFrameOffsetX; 1126 final float newMagnificationFrameCenterY = centerY + mMagnificationFrameOffsetY; 1127 1128 final float offsetX = Float.isNaN(centerX) ? 0 1129 : newMagnificationFrameCenterX - mMagnificationFrame.exactCenterX(); 1130 final float offsetY = Float.isNaN(centerY) ? 0 1131 : newMagnificationFrameCenterY - mMagnificationFrame.exactCenterY(); 1132 mScale = Float.isNaN(scale) ? mScale : scale; 1133 1134 calculateMagnificationFrameBoundary(); 1135 updateMagnificationFramePosition((int) offsetX, (int) offsetY); 1136 if (!isActivated()) { 1137 createMirrorWindow(); 1138 showControls(); 1139 applyResourcesValues(); 1140 } else { 1141 modifyWindowMagnification(false); 1142 } 1143 } 1144 1145 // The magnifier is activated when the window is visible, 1146 // and the window is visible when it is existed. isActivated()1147 boolean isActivated() { 1148 return mMirrorView != null; 1149 } 1150 1151 /** 1152 * Sets the scale of the magnified region if it's visible. 1153 * 1154 * @param scale the target scale, or {@link Float#NaN} to leave unchanged 1155 */ setScale(float scale)1156 void setScale(float scale) { 1157 if (mAnimationController.isAnimating() || !isActivated() || mScale == scale) { 1158 return; 1159 } 1160 1161 enableWindowMagnificationInternal(scale, Float.NaN, Float.NaN); 1162 mHandler.removeCallbacks(mUpdateStateDescriptionRunnable); 1163 mHandler.postDelayed(mUpdateStateDescriptionRunnable, UPDATE_STATE_DESCRIPTION_DELAY_MS); 1164 } 1165 1166 /** 1167 * Moves the window magnifier with specified offset in pixels unit. 1168 * 1169 * @param offsetX the amount in pixels to offset the window magnifier in the X direction, in 1170 * current screen pixels. 1171 * @param offsetY the amount in pixels to offset the window magnifier in the Y direction, in 1172 * current screen pixels. 1173 */ moveWindowMagnifier(float offsetX, float offsetY)1174 void moveWindowMagnifier(float offsetX, float offsetY) { 1175 if (mAnimationController.isAnimating() || mMirrorSurfaceView == null) { 1176 return; 1177 } 1178 1179 if (!mAllowDiagonalScrolling) { 1180 int direction = selectDirectionForMove(abs(offsetX), abs(offsetY)); 1181 1182 if (direction == HORIZONTAL) { 1183 offsetY = 0; 1184 } else { 1185 offsetX = 0; 1186 } 1187 } 1188 1189 if (updateMagnificationFramePosition((int) offsetX, (int) offsetY)) { 1190 modifyWindowMagnification(false); 1191 } 1192 } 1193 moveWindowMagnifierToPosition(float positionX, float positionY, IRemoteMagnificationAnimationCallback callback)1194 void moveWindowMagnifierToPosition(float positionX, float positionY, 1195 IRemoteMagnificationAnimationCallback callback) { 1196 if (mMirrorSurfaceView == null) { 1197 return; 1198 } 1199 mAnimationController.moveWindowMagnifierToPosition(positionX, positionY, callback); 1200 } 1201 selectDirectionForMove(float diffX, float diffY)1202 private int selectDirectionForMove(float diffX, float diffY) { 1203 int direction = 0; 1204 float result = diffY / diffX; 1205 1206 if (result <= HORIZONTAL_LOCK_BASE) { 1207 direction = HORIZONTAL; // horizontal move 1208 } else { 1209 direction = VERTICAL; // vertical move 1210 } 1211 return direction; 1212 } 1213 1214 /** 1215 * Gets the scale. 1216 * 1217 * @return {@link Float#NaN} if the window is invisible. 1218 */ getScale()1219 float getScale() { 1220 return isActivated() ? mScale : Float.NaN; 1221 } 1222 1223 /** 1224 * Returns the screen-relative X coordinate of the center of the magnified bounds. 1225 * 1226 * @return the X coordinate. {@link Float#NaN} if the window is invisible. 1227 */ getCenterX()1228 float getCenterX() { 1229 return isActivated() ? mMagnificationFrame.exactCenterX() : Float.NaN; 1230 } 1231 1232 /** 1233 * Returns the screen-relative Y coordinate of the center of the magnified bounds. 1234 * 1235 * @return the Y coordinate. {@link Float#NaN} if the window is invisible. 1236 */ getCenterY()1237 float getCenterY() { 1238 return isActivated() ? mMagnificationFrame.exactCenterY() : Float.NaN; 1239 } 1240 formatStateDescription(float scale)1241 private CharSequence formatStateDescription(float scale) { 1242 // Cache the locale-appropriate NumberFormat. Configuration locale is guaranteed 1243 // non-null, so the first time this is called we will always get the appropriate 1244 // NumberFormat, then never regenerate it unless the locale changes on the fly. 1245 final Locale curLocale = mContext.getResources().getConfiguration().getLocales().get(0); 1246 if (!curLocale.equals(mLocale)) { 1247 mLocale = curLocale; 1248 mPercentFormat = NumberFormat.getPercentInstance(curLocale); 1249 } 1250 return mPercentFormat.format(scale); 1251 } 1252 1253 @Override onSingleTap(View view)1254 public boolean onSingleTap(View view) { 1255 handleSingleTap(view); 1256 return true; 1257 } 1258 1259 @Override onDrag(View view, float offsetX, float offsetY)1260 public boolean onDrag(View view, float offsetX, float offsetY) { 1261 if (mEditSizeEnable) { 1262 return changeWindowSize(view, offsetX, offsetY); 1263 } else { 1264 move((int) offsetX, (int) offsetY); 1265 } 1266 return true; 1267 } 1268 handleSingleTap(View view)1269 private void handleSingleTap(View view) { 1270 int id = view.getId(); 1271 if (id == R.id.drag_handle) { 1272 mWindowMagnifierCallback.onClickSettingsButton(mDisplayId); 1273 } else if (id == R.id.close_button) { 1274 setEditMagnifierSizeMode(false); 1275 } else { 1276 animateBounceEffect(); 1277 } 1278 } 1279 applyResourcesValues()1280 private void applyResourcesValues() { 1281 // Sets the border appearance for the magnifier window 1282 mMirrorBorderView.setBackground(mResources.getDrawable(mEditSizeEnable 1283 ? R.drawable.accessibility_window_magnification_background_change 1284 : R.drawable.accessibility_window_magnification_background)); 1285 1286 // Changes the corner radius of the mMirrorSurfaceView 1287 mMirrorSurfaceView.setCornerRadius( 1288 TypedValue.applyDimension( 1289 TypedValue.COMPLEX_UNIT_DIP, 1290 mEditSizeEnable ? 16f : 28f, 1291 mContext.getResources().getDisplayMetrics())); 1292 1293 // Sets visibility of components for the magnifier window 1294 if (mEditSizeEnable) { 1295 mDragView.setVisibility(View.GONE); 1296 mCloseView.setVisibility(View.VISIBLE); 1297 mTopRightCornerView.setVisibility(View.VISIBLE); 1298 mTopLeftCornerView.setVisibility(View.VISIBLE); 1299 mBottomRightCornerView.setVisibility(View.VISIBLE); 1300 mBottomLeftCornerView.setVisibility(View.VISIBLE); 1301 } else { 1302 mDragView.setVisibility(View.VISIBLE); 1303 mCloseView.setVisibility(View.GONE); 1304 mTopRightCornerView.setVisibility(View.GONE); 1305 mTopLeftCornerView.setVisibility(View.GONE); 1306 mBottomRightCornerView.setVisibility(View.GONE); 1307 mBottomLeftCornerView.setVisibility(View.GONE); 1308 } 1309 } 1310 changeWindowSize(View view, float offsetX, float offsetY)1311 private boolean changeWindowSize(View view, float offsetX, float offsetY) { 1312 if (view == mLeftDrag) { 1313 changeMagnificationFrameSize(offsetX, 0, 0, 0); 1314 } else if (view == mRightDrag) { 1315 changeMagnificationFrameSize(0, 0, offsetX, 0); 1316 } else if (view == mTopDrag) { 1317 changeMagnificationFrameSize(0, offsetY, 0, 0); 1318 } else if (view == mBottomDrag) { 1319 changeMagnificationFrameSize(0, 0, 0, offsetY); 1320 } else if (view == mTopLeftCornerView) { 1321 changeMagnificationFrameSize(offsetX, offsetY, 0, 0); 1322 } else if (view == mTopRightCornerView) { 1323 changeMagnificationFrameSize(0, offsetY, offsetX, 0); 1324 } else if (view == mBottomLeftCornerView) { 1325 changeMagnificationFrameSize(offsetX, 0, 0, offsetY); 1326 } else if (view == mBottomRightCornerView) { 1327 changeMagnificationFrameSize(0, 0, offsetX, offsetY); 1328 } else { 1329 return false; 1330 } 1331 1332 return true; 1333 } 1334 changeMagnificationFrameSize( float leftOffset, float topOffset, float rightOffset, float bottomOffset)1335 private void changeMagnificationFrameSize( 1336 float leftOffset, float topOffset, float rightOffset, 1337 float bottomOffset) { 1338 boolean bRTL = isRTL(mContext); 1339 final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 3; 1340 1341 final int maxHeightSize = mWindowBounds.height() - 2 * mMirrorSurfaceMargin; 1342 final int maxWidthSize = mWindowBounds.width() - 2 * mMirrorSurfaceMargin; 1343 1344 Rect tempRect = new Rect(); 1345 tempRect.set(mMagnificationFrame); 1346 1347 if (bRTL) { 1348 tempRect.left += (int) (rightOffset); 1349 tempRect.right += (int) (leftOffset); 1350 } else { 1351 tempRect.right += (int) (rightOffset); 1352 tempRect.left += (int) (leftOffset); 1353 } 1354 tempRect.top += (int) (topOffset); 1355 tempRect.bottom += (int) (bottomOffset); 1356 1357 if (tempRect.width() < initSize || tempRect.height() < initSize 1358 || tempRect.width() > maxWidthSize || tempRect.height() > maxHeightSize) { 1359 return; 1360 } 1361 mMagnificationFrame.set(tempRect); 1362 1363 computeBounceAnimationScale(); 1364 calculateMagnificationFrameBoundary(); 1365 1366 modifyWindowMagnification(true); 1367 } 1368 isRTL(Context context)1369 private static boolean isRTL(Context context) { 1370 Configuration config = context.getResources().getConfiguration(); 1371 if (config == null) { 1372 return false; 1373 } 1374 return (config.screenLayout & Configuration.SCREENLAYOUT_LAYOUTDIR_MASK) 1375 == Configuration.SCREENLAYOUT_LAYOUTDIR_RTL; 1376 } 1377 1378 @Override onStart(float x, float y)1379 public boolean onStart(float x, float y) { 1380 mIsDragging = true; 1381 return true; 1382 } 1383 1384 @Override onFinish(float x, float y)1385 public boolean onFinish(float x, float y) { 1386 maybeRepositionButton(); 1387 mIsDragging = false; 1388 return false; 1389 } 1390 1391 /** Moves the button to the opposite edge if the frame is against the edge of the screen. */ maybeRepositionButton()1392 private void maybeRepositionButton() { 1393 if (mMirrorView == null) return; 1394 1395 final float screenEdgeX = mWindowBounds.right - mButtonRepositionThresholdFromEdge; 1396 final FrameLayout.LayoutParams layoutParams = 1397 (FrameLayout.LayoutParams) mDragView.getLayoutParams(); 1398 1399 mMirrorView.getBoundsOnScreen(mTmpRect); 1400 1401 final int newGravity; 1402 if (mTmpRect.right >= screenEdgeX) { 1403 newGravity = Gravity.BOTTOM | Gravity.LEFT; 1404 } else { 1405 newGravity = Gravity.BOTTOM | Gravity.RIGHT; 1406 } 1407 if (newGravity != layoutParams.gravity) { 1408 layoutParams.gravity = newGravity; 1409 mDragView.setLayoutParams(layoutParams); 1410 mDragView.post(this::applyTapExcludeRegion); 1411 } 1412 } 1413 updateDragHandleResourcesIfNeeded(boolean settingsPanelIsShown)1414 void updateDragHandleResourcesIfNeeded(boolean settingsPanelIsShown) { 1415 if (!isActivated()) { 1416 return; 1417 } 1418 1419 mSettingsPanelVisibility = settingsPanelIsShown; 1420 1421 mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown 1422 ? R.drawable.accessibility_window_magnification_drag_handle_background_change 1423 : R.drawable.accessibility_window_magnification_drag_handle_background)); 1424 1425 PorterDuffColorFilter filter = new PorterDuffColorFilter( 1426 mContext.getColor(settingsPanelIsShown 1427 ? R.color.magnification_border_color 1428 : R.color.magnification_drag_handle_stroke), 1429 PorterDuff.Mode.SRC_ATOP); 1430 1431 mDragView.setColorFilter(filter); 1432 } 1433 animateBounceEffect()1434 private void animateBounceEffect() { 1435 final ObjectAnimator scaleAnimator = ObjectAnimator.ofPropertyValuesHolder(mMirrorView, 1436 PropertyValuesHolder.ofFloat(View.SCALE_X, 1, mBounceEffectAnimationScale, 1), 1437 PropertyValuesHolder.ofFloat(View.SCALE_Y, 1, mBounceEffectAnimationScale, 1)); 1438 scaleAnimator.setDuration(mBounceEffectDuration); 1439 scaleAnimator.start(); 1440 } 1441 dump(PrintWriter pw)1442 public void dump(PrintWriter pw) { 1443 pw.println("WindowMagnificationController (displayId=" + mDisplayId + "):"); 1444 pw.println(" mOverlapWithGestureInsets:" + mOverlapWithGestureInsets); 1445 pw.println(" mScale:" + mScale); 1446 pw.println(" mWindowBounds:" + mWindowBounds); 1447 pw.println(" mMirrorViewBounds:" + (isActivated() ? mMirrorViewBounds : "empty")); 1448 pw.println(" mMagnificationFrameBoundary:" 1449 + (isActivated() ? mMagnificationFrameBoundary : "empty")); 1450 pw.println(" mMagnificationFrame:" 1451 + (isActivated() ? mMagnificationFrame : "empty")); 1452 pw.println(" mSourceBounds:" 1453 + (mSourceBounds.isEmpty() ? "empty" : mSourceBounds)); 1454 pw.println(" mSystemGestureTop:" + mSystemGestureTop); 1455 pw.println(" mMagnificationFrameOffsetX:" + mMagnificationFrameOffsetX); 1456 pw.println(" mMagnificationFrameOffsetY:" + mMagnificationFrameOffsetY); 1457 } 1458 1459 private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate { 1460 getClickAccessibilityActionLabel()1461 private CharSequence getClickAccessibilityActionLabel() { 1462 if (mEditSizeEnable) { 1463 // Perform click action to exit edit mode 1464 return mContext.getResources().getString( 1465 R.string.magnification_exit_edit_mode_click_label); 1466 } 1467 1468 return mSettingsPanelVisibility 1469 ? mContext.getResources().getString( 1470 R.string.magnification_close_settings_click_label) 1471 : mContext.getResources().getString( 1472 R.string.magnification_open_settings_click_label); 1473 } 1474 1475 @Override onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info)1476 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 1477 super.onInitializeAccessibilityNodeInfo(host, info); 1478 final AccessibilityAction clickAction = new AccessibilityAction( 1479 AccessibilityAction.ACTION_CLICK.getId(), getClickAccessibilityActionLabel()); 1480 info.addAction(clickAction); 1481 info.setClickable(true); 1482 1483 info.addAction( 1484 new AccessibilityAction(R.id.accessibility_action_zoom_in, 1485 mContext.getString(R.string.accessibility_control_zoom_in))); 1486 info.addAction(new AccessibilityAction(R.id.accessibility_action_zoom_out, 1487 mContext.getString(R.string.accessibility_control_zoom_out))); 1488 1489 if (!mEditSizeEnable) { 1490 info.addAction(new AccessibilityAction(R.id.accessibility_action_move_up, 1491 mContext.getString(R.string.accessibility_control_move_up))); 1492 info.addAction(new AccessibilityAction(R.id.accessibility_action_move_down, 1493 mContext.getString(R.string.accessibility_control_move_down))); 1494 info.addAction(new AccessibilityAction(R.id.accessibility_action_move_left, 1495 mContext.getString(R.string.accessibility_control_move_left))); 1496 info.addAction(new AccessibilityAction(R.id.accessibility_action_move_right, 1497 mContext.getString(R.string.accessibility_control_move_right))); 1498 } else { 1499 if ((mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin) 1500 < mWindowBounds.width()) { 1501 info.addAction(new AccessibilityAction( 1502 R.id.accessibility_action_increase_window_width, 1503 mContext.getString( 1504 R.string.accessibility_control_increase_window_width))); 1505 } 1506 if ((mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin) 1507 < mWindowBounds.height()) { 1508 info.addAction(new AccessibilityAction( 1509 R.id.accessibility_action_increase_window_height, 1510 mContext.getString( 1511 R.string.accessibility_control_increase_window_height))); 1512 } 1513 if ((mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin) > mMinWindowSize) { 1514 info.addAction(new AccessibilityAction( 1515 R.id.accessibility_action_decrease_window_width, 1516 mContext.getString( 1517 R.string.accessibility_control_decrease_window_width))); 1518 } 1519 if ((mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin) > mMinWindowSize) { 1520 info.addAction(new AccessibilityAction( 1521 R.id.accessibility_action_decrease_window_height, 1522 mContext.getString( 1523 R.string.accessibility_control_decrease_window_height))); 1524 } 1525 } 1526 1527 info.setContentDescription(mContext.getString(R.string.magnification_window_title)); 1528 info.setStateDescription(formatStateDescription(getScale())); 1529 } 1530 1531 @Override performAccessibilityAction(View host, int action, Bundle args)1532 public boolean performAccessibilityAction(View host, int action, Bundle args) { 1533 if (performA11yAction(action)) { 1534 return true; 1535 } 1536 return super.performAccessibilityAction(host, action, args); 1537 } 1538 performA11yAction(int action)1539 private boolean performA11yAction(int action) { 1540 final float changeWindowSizeAmount = mContext.getResources().getFraction( 1541 R.fraction.magnification_resize_window_size_amount, 1542 /* base= */ 1, 1543 /* pbase= */ 1); 1544 1545 if (action == AccessibilityAction.ACTION_CLICK.getId()) { 1546 if (mEditSizeEnable) { 1547 // When edit mode is enabled, click the magnifier to exit edit mode. 1548 setEditMagnifierSizeMode(false); 1549 } else { 1550 // Simulate tapping the drag view so it opens the Settings. 1551 handleSingleTap(mDragView); 1552 } 1553 1554 } else if (action == R.id.accessibility_action_zoom_in) { 1555 performScale(mScale + A11Y_CHANGE_SCALE_DIFFERENCE); 1556 } else if (action == R.id.accessibility_action_zoom_out) { 1557 performScale(mScale - A11Y_CHANGE_SCALE_DIFFERENCE); 1558 } else if (action == R.id.accessibility_action_move_up) { 1559 move(0, -mSourceBounds.height()); 1560 } else if (action == R.id.accessibility_action_move_down) { 1561 move(0, mSourceBounds.height()); 1562 } else if (action == R.id.accessibility_action_move_left) { 1563 move(-mSourceBounds.width(), 0); 1564 } else if (action == R.id.accessibility_action_move_right) { 1565 move(mSourceBounds.width(), 0); 1566 } else if (action == R.id.accessibility_action_increase_window_width) { 1567 int newFrameWidth = 1568 (int) (mMagnificationFrame.width() * (1 + changeWindowSizeAmount)); 1569 setMagnificationFrameSize(newFrameWidth, mMagnificationFrame.height()); 1570 } else if (action == R.id.accessibility_action_increase_window_height) { 1571 int newFrameHeight = 1572 (int) (mMagnificationFrame.height() * (1 + changeWindowSizeAmount)); 1573 setMagnificationFrameSize(mMagnificationFrame.width(), newFrameHeight); 1574 } else if (action == R.id.accessibility_action_decrease_window_width) { 1575 int newFrameWidth = 1576 (int) (mMagnificationFrame.width() * (1 - changeWindowSizeAmount)); 1577 setMagnificationFrameSize(newFrameWidth, mMagnificationFrame.height()); 1578 } else if (action == R.id.accessibility_action_decrease_window_height) { 1579 int newFrameHeight = 1580 (int) (mMagnificationFrame.height() * (1 - changeWindowSizeAmount)); 1581 setMagnificationFrameSize(mMagnificationFrame.width(), newFrameHeight); 1582 } else { 1583 return false; 1584 } 1585 1586 mWindowMagnifierCallback.onAccessibilityActionPerformed(mDisplayId); 1587 return true; 1588 } 1589 performScale(float scale)1590 private void performScale(float scale) { 1591 scale = A11Y_ACTION_SCALE_RANGE.clamp(scale); 1592 mWindowMagnifierCallback.onPerformScaleAction( 1593 mDisplayId, scale, /* updatePersistence= */ true); 1594 } 1595 } 1596 1597 }