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