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