1 /* 2 * Copyright (C) 2020 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.server.accessibility.magnification; 18 19 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL; 20 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; 21 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; 22 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.content.Context; 27 import android.graphics.PointF; 28 import android.graphics.Rect; 29 import android.graphics.Region; 30 import android.os.SystemClock; 31 import android.provider.Settings; 32 import android.util.Slog; 33 import android.util.SparseArray; 34 import android.view.accessibility.MagnificationAnimationCallback; 35 36 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; 37 import com.android.internal.annotations.GuardedBy; 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.server.accessibility.AccessibilityManagerService; 40 41 /** 42 * Handles all magnification controllers initialization, generic interactions, 43 * magnification mode transition and magnification switch UI show/hide logic 44 * in the following callbacks: 45 * 46 * <ol> 47 * <li> 1. {@link #onTouchInteractionStart} shows magnification switch UI when 48 * the user touch interaction starts if magnification capabilities is all. </li> 49 * <li> 2. {@link #onTouchInteractionEnd} shows magnification switch UI when 50 * the user touch interaction ends if magnification capabilities is all. </li> 51 * <li> 3. {@link #onShortcutTriggered} updates magnification switch UI depending on 52 * magnification capabilities and magnification active state when magnification shortcut 53 * is triggered.</li> 54 * <li> 4. {@link #onTripleTapped} updates magnification switch UI depending on magnification 55 * capabilities and magnification active state when triple-tap gesture is detected. </li> 56 * <li> 4. {@link #onRequestMagnificationSpec} updates magnification switch UI depending on 57 * magnification capabilities and magnification active state when new magnification spec is 58 * changed by external request from calling public APIs. </li> 59 * </ol> 60 * 61 * <b>Note</b> Updates magnification switch UI when magnification mode transition 62 * is done and before invoking {@link TransitionCallBack#onResult}. 63 */ 64 public class MagnificationController implements WindowMagnificationManager.Callback, 65 MagnificationGestureHandler.Callback, 66 FullScreenMagnificationController.MagnificationInfoChangedCallback { 67 68 private static final boolean DEBUG = false; 69 private static final String TAG = "MagnificationController"; 70 private final AccessibilityManagerService mAms; 71 private final PointF mTempPoint = new PointF(); 72 private final Object mLock; 73 private final Context mContext; 74 private final SparseArray<DisableMagnificationCallback> 75 mMagnificationEndRunnableSparseArray = new SparseArray(); 76 77 private FullScreenMagnificationController mFullScreenMagnificationController; 78 private WindowMagnificationManager mWindowMagnificationMgr; 79 private int mMagnificationCapabilities = ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; 80 81 @GuardedBy("mLock") 82 private int mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_NONE; 83 @GuardedBy("mLock") 84 private boolean mImeWindowVisible = false; 85 private long mWindowModeEnabledTime = 0; 86 private long mFullScreenModeEnabledTime = 0; 87 88 /** 89 * A callback to inform the magnification transition result. 90 */ 91 public interface TransitionCallBack { 92 /** 93 * Invoked when the transition ends. 94 * @param success {@code true} if the transition success. 95 */ onResult(boolean success)96 void onResult(boolean success); 97 } 98 MagnificationController(AccessibilityManagerService ams, Object lock, Context context)99 public MagnificationController(AccessibilityManagerService ams, Object lock, 100 Context context) { 101 mAms = ams; 102 mLock = lock; 103 mContext = context; 104 } 105 106 @VisibleForTesting MagnificationController(AccessibilityManagerService ams, Object lock, Context context, FullScreenMagnificationController fullScreenMagnificationController, WindowMagnificationManager windowMagnificationManager)107 public MagnificationController(AccessibilityManagerService ams, Object lock, 108 Context context, FullScreenMagnificationController fullScreenMagnificationController, 109 WindowMagnificationManager windowMagnificationManager) { 110 this(ams, lock, context); 111 mFullScreenMagnificationController = fullScreenMagnificationController; 112 mWindowMagnificationMgr = windowMagnificationManager; 113 } 114 115 @Override onPerformScaleAction(int displayId, float scale)116 public void onPerformScaleAction(int displayId, float scale) { 117 getWindowMagnificationMgr().setScale(displayId, scale); 118 getWindowMagnificationMgr().persistScale(displayId); 119 } 120 121 @Override onAccessibilityActionPerformed(int displayId)122 public void onAccessibilityActionPerformed(int displayId) { 123 updateMagnificationButton(displayId, ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); 124 } 125 126 @Override onTouchInteractionStart(int displayId, int mode)127 public void onTouchInteractionStart(int displayId, int mode) { 128 handleUserInteractionChanged(displayId, mode); 129 } 130 131 @Override onTouchInteractionEnd(int displayId, int mode)132 public void onTouchInteractionEnd(int displayId, int mode) { 133 handleUserInteractionChanged(displayId, mode); 134 } 135 handleUserInteractionChanged(int displayId, int mode)136 private void handleUserInteractionChanged(int displayId, int mode) { 137 if (mMagnificationCapabilities != Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL) { 138 return; 139 } 140 if (isActivated(displayId, mode)) { 141 getWindowMagnificationMgr().showMagnificationButton(displayId, mode); 142 } 143 } 144 145 @Override onShortcutTriggered(int displayId, int mode)146 public void onShortcutTriggered(int displayId, int mode) { 147 updateMagnificationButton(displayId, mode); 148 } 149 150 @Override onTripleTapped(int displayId, int mode)151 public void onTripleTapped(int displayId, int mode) { 152 updateMagnificationButton(displayId, mode); 153 } 154 updateMagnificationButton(int displayId, int mode)155 private void updateMagnificationButton(int displayId, int mode) { 156 final boolean isActivated = isActivated(displayId, mode); 157 final boolean showButton; 158 synchronized (mLock) { 159 showButton = isActivated && mMagnificationCapabilities 160 == Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL; 161 } 162 if (showButton) { 163 getWindowMagnificationMgr().showMagnificationButton(displayId, mode); 164 } else { 165 getWindowMagnificationMgr().removeMagnificationButton(displayId); 166 } 167 } 168 169 /** 170 * Transitions to the target Magnification mode with current center of the magnification mode 171 * if it is available. 172 * 173 * @param displayId The logical display 174 * @param targetMode The target magnification mode 175 * @param transitionCallBack The callback invoked when the transition is finished. 176 */ transitionMagnificationModeLocked(int displayId, int targetMode, @NonNull TransitionCallBack transitionCallBack)177 public void transitionMagnificationModeLocked(int displayId, int targetMode, 178 @NonNull TransitionCallBack transitionCallBack) { 179 final PointF magnificationCenter = getCurrentMagnificationBoundsCenterLocked(displayId, 180 targetMode); 181 final DisableMagnificationCallback animationCallback = 182 getDisableMagnificationEndRunnableLocked(displayId); 183 if (magnificationCenter == null && animationCallback == null) { 184 transitionCallBack.onResult(true); 185 return; 186 } 187 188 if (animationCallback != null) { 189 if (animationCallback.mCurrentMode == targetMode) { 190 animationCallback.restoreToCurrentMagnificationMode(); 191 return; 192 } 193 Slog.w(TAG, "request during transition, abandon current:" 194 + animationCallback.mTargetMode); 195 animationCallback.setExpiredAndRemoveFromListLocked(); 196 } 197 198 if (magnificationCenter == null) { 199 Slog.w(TAG, "Invalid center, ignore it"); 200 transitionCallBack.onResult(true); 201 return; 202 } 203 final FullScreenMagnificationController screenMagnificationController = 204 getFullScreenMagnificationController(); 205 final WindowMagnificationManager windowMagnificationMgr = getWindowMagnificationMgr(); 206 final float scale = windowMagnificationMgr.getPersistedScale(); 207 final DisableMagnificationCallback animationEndCallback = 208 new DisableMagnificationCallback(transitionCallBack, displayId, targetMode, 209 scale, magnificationCenter); 210 if (targetMode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) { 211 screenMagnificationController.reset(displayId, animationEndCallback); 212 } else { 213 windowMagnificationMgr.disableWindowMagnification(displayId, false, 214 animationEndCallback); 215 } 216 setDisableMagnificationCallbackLocked(displayId, animationEndCallback); 217 } 218 219 @Override onRequestMagnificationSpec(int displayId, int serviceId)220 public void onRequestMagnificationSpec(int displayId, int serviceId) { 221 final WindowMagnificationManager windowMagnificationManager; 222 synchronized (mLock) { 223 if (serviceId == AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID) { 224 return; 225 } 226 updateMagnificationButton(displayId, ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); 227 windowMagnificationManager = mWindowMagnificationMgr; 228 } 229 if (windowMagnificationManager != null) { 230 mWindowMagnificationMgr.disableWindowMagnification(displayId, false); 231 } 232 } 233 234 // TODO : supporting multi-display (b/182227245). 235 @Override onWindowMagnificationActivationState(int displayId, boolean activated)236 public void onWindowMagnificationActivationState(int displayId, boolean activated) { 237 if (activated) { 238 mWindowModeEnabledTime = SystemClock.uptimeMillis(); 239 240 synchronized (mLock) { 241 mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; 242 } 243 logMagnificationModeWithImeOnIfNeeded(); 244 disableFullScreenMagnificationIfNeeded(displayId); 245 } else { 246 logMagnificationUsageState(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, 247 SystemClock.uptimeMillis() - mWindowModeEnabledTime); 248 249 synchronized (mLock) { 250 mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_NONE; 251 } 252 } 253 } 254 disableFullScreenMagnificationIfNeeded(int displayId)255 private void disableFullScreenMagnificationIfNeeded(int displayId) { 256 final FullScreenMagnificationController fullScreenMagnificationController = 257 getFullScreenMagnificationController(); 258 // Internal request may be for transition, so we just need to check external request. 259 final boolean isMagnifyByExternalRequest = 260 fullScreenMagnificationController.getIdOfLastServiceToMagnify(displayId) > 0; 261 if (isMagnifyByExternalRequest) { 262 fullScreenMagnificationController.reset(displayId, false); 263 } 264 } 265 266 @Override onFullScreenMagnificationActivationState(boolean activated)267 public void onFullScreenMagnificationActivationState(boolean activated) { 268 if (activated) { 269 mFullScreenModeEnabledTime = SystemClock.uptimeMillis(); 270 271 synchronized (mLock) { 272 mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; 273 } 274 logMagnificationModeWithImeOnIfNeeded(); 275 } else { 276 logMagnificationUsageState(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN, 277 SystemClock.uptimeMillis() - mFullScreenModeEnabledTime); 278 279 synchronized (mLock) { 280 mActivatedMode = ACCESSIBILITY_MAGNIFICATION_MODE_NONE; 281 } 282 } 283 } 284 285 @Override onImeWindowVisibilityChanged(boolean shown)286 public void onImeWindowVisibilityChanged(boolean shown) { 287 synchronized (mLock) { 288 mImeWindowVisible = shown; 289 } 290 logMagnificationModeWithImeOnIfNeeded(); 291 } 292 293 /** 294 * Wrapper method of logging the magnification activated mode and its duration of the usage 295 * when the magnification is disabled. 296 * 297 * @param mode The activated magnification mode. 298 * @param duration The duration in milliseconds during the magnification is activated. 299 */ 300 @VisibleForTesting logMagnificationUsageState(int mode, long duration)301 public void logMagnificationUsageState(int mode, long duration) { 302 AccessibilityStatsLogUtils.logMagnificationUsageState(mode, duration); 303 } 304 305 /** 306 * Wrapper method of logging the activated mode of the magnification when the IME window 307 * is shown on the screen. 308 * 309 * @param mode The activated magnification mode. 310 */ 311 @VisibleForTesting logMagnificationModeWithIme(int mode)312 public void logMagnificationModeWithIme(int mode) { 313 AccessibilityStatsLogUtils.logMagnificationModeWithImeOn(mode); 314 } 315 316 /** 317 * Updates the active user ID of {@link FullScreenMagnificationController} and {@link 318 * WindowMagnificationManager}. 319 * 320 * @param userId the currently active user ID 321 */ updateUserIdIfNeeded(int userId)322 public void updateUserIdIfNeeded(int userId) { 323 synchronized (mLock) { 324 if (mFullScreenMagnificationController != null) { 325 mFullScreenMagnificationController.setUserId(userId); 326 } 327 if (mWindowMagnificationMgr != null) { 328 mWindowMagnificationMgr.setUserId(userId); 329 } 330 } 331 } 332 333 /** 334 * Removes the magnification instance with given id. 335 * 336 * @param displayId The logical display id. 337 */ onDisplayRemoved(int displayId)338 public void onDisplayRemoved(int displayId) { 339 synchronized (mLock) { 340 if (mFullScreenMagnificationController != null) { 341 mFullScreenMagnificationController.onDisplayRemoved(displayId); 342 } 343 if (mWindowMagnificationMgr != null) { 344 mWindowMagnificationMgr.onDisplayRemoved(displayId); 345 } 346 } 347 } 348 setMagnificationCapabilities(int capabilities)349 public void setMagnificationCapabilities(int capabilities) { 350 mMagnificationCapabilities = capabilities; 351 } 352 getDisableMagnificationEndRunnableLocked( int displayId)353 private DisableMagnificationCallback getDisableMagnificationEndRunnableLocked( 354 int displayId) { 355 return mMagnificationEndRunnableSparseArray.get(displayId); 356 } 357 setDisableMagnificationCallbackLocked(int displayId, @Nullable DisableMagnificationCallback callback)358 private void setDisableMagnificationCallbackLocked(int displayId, 359 @Nullable DisableMagnificationCallback callback) { 360 mMagnificationEndRunnableSparseArray.put(displayId, callback); 361 if (DEBUG) { 362 Slog.d(TAG, "setDisableMagnificationCallbackLocked displayId = " + displayId 363 + ", callback = " + callback); 364 } 365 } 366 logMagnificationModeWithImeOnIfNeeded()367 private void logMagnificationModeWithImeOnIfNeeded() { 368 final int mode; 369 370 synchronized (mLock) { 371 if (!mImeWindowVisible || mActivatedMode == ACCESSIBILITY_MAGNIFICATION_MODE_NONE) { 372 return; 373 } 374 mode = mActivatedMode; 375 } 376 logMagnificationModeWithIme(mode); 377 } 378 379 /** 380 * Getter of {@link FullScreenMagnificationController}. 381 * 382 * @return {@link FullScreenMagnificationController}. 383 */ getFullScreenMagnificationController()384 public FullScreenMagnificationController getFullScreenMagnificationController() { 385 synchronized (mLock) { 386 if (mFullScreenMagnificationController == null) { 387 mFullScreenMagnificationController = new FullScreenMagnificationController(mContext, 388 mAms, mLock, this); 389 mFullScreenMagnificationController.setUserId(mAms.getCurrentUserIdLocked()); 390 } 391 } 392 return mFullScreenMagnificationController; 393 } 394 395 /** 396 * Is {@link #mFullScreenMagnificationController} is initialized. 397 * @return {code true} if {@link #mFullScreenMagnificationController} is initialized. 398 */ isFullScreenMagnificationControllerInitialized()399 public boolean isFullScreenMagnificationControllerInitialized() { 400 synchronized (mLock) { 401 return mFullScreenMagnificationController != null; 402 } 403 } 404 405 /** 406 * Getter of {@link WindowMagnificationManager}. 407 * 408 * @return {@link WindowMagnificationManager}. 409 */ getWindowMagnificationMgr()410 public WindowMagnificationManager getWindowMagnificationMgr() { 411 synchronized (mLock) { 412 if (mWindowMagnificationMgr == null) { 413 mWindowMagnificationMgr = new WindowMagnificationManager(mContext, 414 mAms.getCurrentUserIdLocked(), this, mAms.getTraceManager()); 415 } 416 return mWindowMagnificationMgr; 417 } 418 } 419 420 private @Nullable getCurrentMagnificationBoundsCenterLocked(int displayId, int targetMode)421 PointF getCurrentMagnificationBoundsCenterLocked(int displayId, int targetMode) { 422 if (targetMode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) { 423 if (mWindowMagnificationMgr == null 424 || !mWindowMagnificationMgr.isWindowMagnifierEnabled(displayId)) { 425 return null; 426 } 427 mTempPoint.set(mWindowMagnificationMgr.getCenterX(displayId), 428 mWindowMagnificationMgr.getCenterY(displayId)); 429 } else { 430 if (mFullScreenMagnificationController == null 431 || !mFullScreenMagnificationController.isMagnifying(displayId)) { 432 return null; 433 } 434 mTempPoint.set(mFullScreenMagnificationController.getCenterX(displayId), 435 mFullScreenMagnificationController.getCenterY(displayId)); 436 } 437 return mTempPoint; 438 } 439 isActivated(int displayId, int mode)440 private boolean isActivated(int displayId, int mode) { 441 boolean isActivated = false; 442 if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) { 443 synchronized (mLock) { 444 if (mFullScreenMagnificationController == null) { 445 return false; 446 } 447 isActivated = mFullScreenMagnificationController.isMagnifying(displayId) 448 || mFullScreenMagnificationController.isForceShowMagnifiableBounds( 449 displayId); 450 } 451 } else if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) { 452 synchronized (mLock) { 453 if (mWindowMagnificationMgr == null) { 454 return false; 455 } 456 isActivated = mWindowMagnificationMgr.isWindowMagnifierEnabled(displayId); 457 } 458 } 459 return isActivated; 460 } 461 462 private final class DisableMagnificationCallback implements 463 MagnificationAnimationCallback { 464 private final TransitionCallBack mTransitionCallBack; 465 private boolean mExpired = false; 466 private final int mDisplayId; 467 private final int mTargetMode; 468 private final int mCurrentMode; 469 private final float mCurrentScale; 470 private final PointF mCurrentCenter = new PointF(); 471 DisableMagnificationCallback(TransitionCallBack transitionCallBack, int displayId, int targetMode, float scale, PointF currentCenter)472 DisableMagnificationCallback(TransitionCallBack transitionCallBack, 473 int displayId, int targetMode, float scale, PointF currentCenter) { 474 mTransitionCallBack = transitionCallBack; 475 mDisplayId = displayId; 476 mTargetMode = targetMode; 477 mCurrentMode = mTargetMode ^ ACCESSIBILITY_MAGNIFICATION_MODE_ALL; 478 mCurrentScale = scale; 479 mCurrentCenter.set(currentCenter); 480 } 481 482 @Override onResult(boolean success)483 public void onResult(boolean success) { 484 synchronized (mLock) { 485 if (DEBUG) { 486 Slog.d(TAG, "onResult success = " + success); 487 } 488 if (mExpired) { 489 return; 490 } 491 setExpiredAndRemoveFromListLocked(); 492 if (success) { 493 adjustCurrentCenterIfNeededLocked(); 494 applyMagnificationModeLocked(mTargetMode); 495 } 496 updateMagnificationButton(mDisplayId, mTargetMode); 497 mTransitionCallBack.onResult(success); 498 } 499 } 500 adjustCurrentCenterIfNeededLocked()501 private void adjustCurrentCenterIfNeededLocked() { 502 if (mTargetMode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) { 503 return; 504 } 505 final Region outRegion = new Region(); 506 getFullScreenMagnificationController().getMagnificationRegion(mDisplayId, outRegion); 507 if (outRegion.contains((int) mCurrentCenter.x, (int) mCurrentCenter.y)) { 508 return; 509 } 510 final Rect bounds = outRegion.getBounds(); 511 mCurrentCenter.set(bounds.exactCenterX(), bounds.exactCenterY()); 512 } 513 restoreToCurrentMagnificationMode()514 void restoreToCurrentMagnificationMode() { 515 synchronized (mLock) { 516 if (mExpired) { 517 return; 518 } 519 setExpiredAndRemoveFromListLocked(); 520 applyMagnificationModeLocked(mCurrentMode); 521 updateMagnificationButton(mDisplayId, mCurrentMode); 522 mTransitionCallBack.onResult(true); 523 } 524 } 525 setExpiredAndRemoveFromListLocked()526 void setExpiredAndRemoveFromListLocked() { 527 mExpired = true; 528 setDisableMagnificationCallbackLocked(mDisplayId, null); 529 } 530 applyMagnificationModeLocked(int mode)531 private void applyMagnificationModeLocked(int mode) { 532 if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) { 533 getFullScreenMagnificationController().setScaleAndCenter(mDisplayId, 534 mCurrentScale, mCurrentCenter.x, 535 mCurrentCenter.y, true, 536 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 537 } else { 538 getWindowMagnificationMgr().enableWindowMagnification(mDisplayId, 539 mCurrentScale, mCurrentCenter.x, 540 mCurrentCenter.y); 541 } 542 } 543 } 544 } 545