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(), 415 this); 416 } 417 return mWindowMagnificationMgr; 418 } 419 } 420 421 private @Nullable getCurrentMagnificationBoundsCenterLocked(int displayId, int targetMode)422 PointF getCurrentMagnificationBoundsCenterLocked(int displayId, int targetMode) { 423 if (targetMode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) { 424 if (mWindowMagnificationMgr == null 425 || !mWindowMagnificationMgr.isWindowMagnifierEnabled(displayId)) { 426 return null; 427 } 428 mTempPoint.set(mWindowMagnificationMgr.getCenterX(displayId), 429 mWindowMagnificationMgr.getCenterY(displayId)); 430 } else { 431 if (mFullScreenMagnificationController == null 432 || !mFullScreenMagnificationController.isMagnifying(displayId)) { 433 return null; 434 } 435 mTempPoint.set(mFullScreenMagnificationController.getCenterX(displayId), 436 mFullScreenMagnificationController.getCenterY(displayId)); 437 } 438 return mTempPoint; 439 } 440 isActivated(int displayId, int mode)441 private boolean isActivated(int displayId, int mode) { 442 boolean isActivated = false; 443 if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) { 444 synchronized (mLock) { 445 if (mFullScreenMagnificationController == null) { 446 return false; 447 } 448 isActivated = mFullScreenMagnificationController.isMagnifying(displayId) 449 || mFullScreenMagnificationController.isForceShowMagnifiableBounds( 450 displayId); 451 } 452 } else if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) { 453 synchronized (mLock) { 454 if (mWindowMagnificationMgr == null) { 455 return false; 456 } 457 isActivated = mWindowMagnificationMgr.isWindowMagnifierEnabled(displayId); 458 } 459 } 460 return isActivated; 461 } 462 463 private final class DisableMagnificationCallback implements 464 MagnificationAnimationCallback { 465 private final TransitionCallBack mTransitionCallBack; 466 private boolean mExpired = false; 467 private final int mDisplayId; 468 private final int mTargetMode; 469 private final int mCurrentMode; 470 private final float mCurrentScale; 471 private final PointF mCurrentCenter = new PointF(); 472 DisableMagnificationCallback(TransitionCallBack transitionCallBack, int displayId, int targetMode, float scale, PointF currentCenter)473 DisableMagnificationCallback(TransitionCallBack transitionCallBack, 474 int displayId, int targetMode, float scale, PointF currentCenter) { 475 mTransitionCallBack = transitionCallBack; 476 mDisplayId = displayId; 477 mTargetMode = targetMode; 478 mCurrentMode = mTargetMode ^ ACCESSIBILITY_MAGNIFICATION_MODE_ALL; 479 mCurrentScale = scale; 480 mCurrentCenter.set(currentCenter); 481 } 482 483 @Override onResult(boolean success)484 public void onResult(boolean success) { 485 synchronized (mLock) { 486 if (DEBUG) { 487 Slog.d(TAG, "onResult success = " + success); 488 } 489 if (mExpired) { 490 return; 491 } 492 setExpiredAndRemoveFromListLocked(); 493 if (success) { 494 adjustCurrentCenterIfNeededLocked(); 495 applyMagnificationModeLocked(mTargetMode); 496 } 497 updateMagnificationButton(mDisplayId, mTargetMode); 498 mTransitionCallBack.onResult(success); 499 } 500 } 501 adjustCurrentCenterIfNeededLocked()502 private void adjustCurrentCenterIfNeededLocked() { 503 if (mTargetMode == ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW) { 504 return; 505 } 506 final Region outRegion = new Region(); 507 getFullScreenMagnificationController().getMagnificationRegion(mDisplayId, outRegion); 508 if (outRegion.contains((int) mCurrentCenter.x, (int) mCurrentCenter.y)) { 509 return; 510 } 511 final Rect bounds = outRegion.getBounds(); 512 mCurrentCenter.set(bounds.exactCenterX(), bounds.exactCenterY()); 513 } 514 restoreToCurrentMagnificationMode()515 void restoreToCurrentMagnificationMode() { 516 synchronized (mLock) { 517 if (mExpired) { 518 return; 519 } 520 setExpiredAndRemoveFromListLocked(); 521 applyMagnificationModeLocked(mCurrentMode); 522 updateMagnificationButton(mDisplayId, mCurrentMode); 523 mTransitionCallBack.onResult(true); 524 } 525 } 526 setExpiredAndRemoveFromListLocked()527 void setExpiredAndRemoveFromListLocked() { 528 mExpired = true; 529 setDisableMagnificationCallbackLocked(mDisplayId, null); 530 } 531 applyMagnificationModeLocked(int mode)532 private void applyMagnificationModeLocked(int mode) { 533 if (mode == ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN) { 534 getFullScreenMagnificationController().setScaleAndCenter(mDisplayId, 535 mCurrentScale, mCurrentCenter.x, 536 mCurrentCenter.y, true, 537 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 538 } else { 539 getWindowMagnificationMgr().enableWindowMagnification(mDisplayId, 540 mCurrentScale, mCurrentCenter.x, 541 mCurrentCenter.y); 542 } 543 } 544 } 545 } 546