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.view.InputDevice.SOURCE_TOUCHSCREEN; 20 import static android.view.MotionEvent.ACTION_CANCEL; 21 import static android.view.MotionEvent.ACTION_MOVE; 22 import static android.view.MotionEvent.ACTION_UP; 23 24 import static java.util.Arrays.asList; 25 import static java.util.Arrays.copyOfRange; 26 27 import android.annotation.Nullable; 28 import android.annotation.UiContext; 29 import android.content.Context; 30 import android.graphics.Point; 31 import android.os.SystemClock; 32 import android.provider.Settings; 33 import android.util.MathUtils; 34 import android.util.Slog; 35 import android.view.Display; 36 import android.view.MotionEvent; 37 38 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.server.accessibility.AccessibilityTraceManager; 41 import com.android.server.accessibility.EventStreamTransformation; 42 import com.android.server.accessibility.gestures.MultiTap; 43 import com.android.server.accessibility.gestures.MultiTapAndHold; 44 45 import java.util.List; 46 47 /** 48 * This class handles window magnification in response to touch events and shortcut. 49 * 50 * The behavior is as follows: 51 * 52 * <ol> 53 * <li> 1. Toggle Window magnification by triple-tap gesture shortcut. It is triggered via 54 * {@link #onTripleTap(MotionEvent)}. 55 * <li> 2. Toggle Window magnification by tapping shortcut. It is triggered via 56 * {@link #notifyShortcutTriggered()}. 57 * <li> When the window magnifier is visible, pinching with any number of additional fingers 58 * would adjust the magnification scale .<strong>Note</strong> that this operation is valid only 59 * when at least one finger is in the window. 60 * <li> When the window magnifier is visible, to do scrolling to move the window magnifier, 61 * the user can use two or more fingers and at least one of them is inside the window. 62 * <br><strong>Note</strong> that the offset of this callback is opposed to moving direction. 63 * The operation becomes invalid after performing scaling operation until all fingers are 64 * lifted. 65 * </ol> 66 */ 67 @SuppressWarnings("WeakerAccess") 68 public class WindowMagnificationGestureHandler extends MagnificationGestureHandler { 69 70 private static final boolean DEBUG_STATE_TRANSITIONS = false | DEBUG_ALL; 71 private static final boolean DEBUG_DETECTING = false | DEBUG_ALL; 72 73 //Ensure the range has consistency with FullScreenMagnificationGestureHandler. 74 private static final float MIN_SCALE = 1.0f; 75 private static final float MAX_SCALE = MagnificationScaleProvider.MAX_SCALE; 76 77 private final WindowMagnificationManager mWindowMagnificationMgr; 78 @VisibleForTesting 79 final DelegatingState mDelegatingState; 80 @VisibleForTesting 81 final DetectingState mDetectingState; 82 @VisibleForTesting 83 final PanningScalingGestureState mObservePanningScalingState; 84 @VisibleForTesting 85 final ViewportDraggingState mViewportDraggingState; 86 87 @VisibleForTesting 88 State mCurrentState; 89 @VisibleForTesting 90 State mPreviousState; 91 92 private MotionEventDispatcherDelegate mMotionEventDispatcherDelegate; 93 private final Context mContext; 94 private final Point mTempPoint = new Point(); 95 96 private long mTripleTapAndHoldStartedTime = 0; 97 WindowMagnificationGestureHandler(@iContext Context context, WindowMagnificationManager windowMagnificationMgr, AccessibilityTraceManager trace, Callback callback, boolean detectTripleTap, boolean detectShortcutTrigger, int displayId)98 public WindowMagnificationGestureHandler(@UiContext Context context, 99 WindowMagnificationManager windowMagnificationMgr, 100 AccessibilityTraceManager trace, 101 Callback callback, 102 boolean detectTripleTap, boolean detectShortcutTrigger, int displayId) { 103 super(displayId, detectTripleTap, detectShortcutTrigger, trace, callback); 104 if (DEBUG_ALL) { 105 Slog.i(mLogTag, 106 "WindowMagnificationGestureHandler() , displayId = " + displayId + ")"); 107 } 108 mContext = context; 109 mWindowMagnificationMgr = windowMagnificationMgr; 110 mMotionEventDispatcherDelegate = new MotionEventDispatcherDelegate(context, 111 (event, rawEvent, policyFlags) -> dispatchTransformedEvent(event, rawEvent, 112 policyFlags)); 113 mDelegatingState = new DelegatingState(mMotionEventDispatcherDelegate); 114 mDetectingState = new DetectingState(context, mDetectTripleTap); 115 mViewportDraggingState = new ViewportDraggingState(); 116 mObservePanningScalingState = new PanningScalingGestureState( 117 new PanningScalingHandler(context, MAX_SCALE, MIN_SCALE, true, 118 new PanningScalingHandler.MagnificationDelegate() { 119 @Override 120 public boolean processScroll(int displayId, float distanceX, 121 float distanceY) { 122 return mWindowMagnificationMgr.processScroll(displayId, distanceX, 123 distanceY); 124 } 125 126 @Override 127 public void setScale(int displayId, float scale) { 128 mWindowMagnificationMgr.setScale(displayId, scale); 129 } 130 131 @Override 132 public float getScale(int displayId) { 133 return mWindowMagnificationMgr.getScale(displayId); 134 } 135 })); 136 137 transitionTo(mDetectingState); 138 } 139 140 @Override onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags)141 void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 142 // To keep InputEventConsistencyVerifiers within GestureDetectors happy. 143 mObservePanningScalingState.mPanningScalingHandler.onTouchEvent(event); 144 mCurrentState.onMotionEvent(event, rawEvent, policyFlags); 145 } 146 147 @Override clearEvents(int inputSource)148 public void clearEvents(int inputSource) { 149 if (inputSource == SOURCE_TOUCHSCREEN) { 150 resetToDetectState(); 151 } 152 super.clearEvents(inputSource); 153 } 154 155 @Override onDestroy()156 public void onDestroy() { 157 if (DEBUG_ALL) { 158 Slog.i(mLogTag, "onDestroy(); delayed = " 159 + mDetectingState.toString()); 160 } 161 mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, true); 162 resetToDetectState(); 163 } 164 165 @Override handleShortcutTriggered()166 public void handleShortcutTriggered() { 167 final Point screenSize = mTempPoint; 168 getScreenSize(mTempPoint); 169 toggleMagnification(screenSize.x / 2.0f, screenSize.y / 2.0f, 170 WindowMagnificationManager.WINDOW_POSITION_AT_CENTER); 171 } 172 getScreenSize(Point outSize)173 private void getScreenSize(Point outSize) { 174 final Display display = mContext.getDisplay(); 175 display.getRealSize(outSize); 176 } 177 178 @Override getMode()179 public int getMode() { 180 return Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; 181 } 182 enableWindowMagnifier(float centerX, float centerY, @WindowMagnificationManager.WindowPosition int windowPosition)183 private void enableWindowMagnifier(float centerX, float centerY, 184 @WindowMagnificationManager.WindowPosition int windowPosition) { 185 if (DEBUG_ALL) { 186 Slog.i(mLogTag, "enableWindowMagnifier :" 187 + centerX + ", " + centerY + ", " + windowPosition); 188 } 189 190 final float scale = MathUtils.constrain( 191 mWindowMagnificationMgr.getPersistedScale(mDisplayId), MIN_SCALE, MAX_SCALE); 192 mWindowMagnificationMgr.enableWindowMagnification(mDisplayId, scale, centerX, centerY, 193 windowPosition); 194 } 195 disableWindowMagnifier()196 private void disableWindowMagnifier() { 197 if (DEBUG_ALL) { 198 Slog.i(mLogTag, "disableWindowMagnifier()"); 199 } 200 mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, false); 201 } 202 toggleMagnification(float centerX, float centerY, @WindowMagnificationManager.WindowPosition int windowPosition)203 private void toggleMagnification(float centerX, float centerY, 204 @WindowMagnificationManager.WindowPosition int windowPosition) { 205 if (mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId)) { 206 disableWindowMagnifier(); 207 } else { 208 enableWindowMagnifier(centerX, centerY, windowPosition); 209 } 210 } 211 onTripleTap(MotionEvent up)212 private void onTripleTap(MotionEvent up) { 213 if (DEBUG_DETECTING) { 214 Slog.i(mLogTag, "onTripleTap()"); 215 } 216 toggleMagnification(up.getX(), up.getY(), 217 WindowMagnificationManager.WINDOW_POSITION_AT_CENTER); 218 } 219 220 @VisibleForTesting onTripleTapAndHold(MotionEvent up)221 void onTripleTapAndHold(MotionEvent up) { 222 if (DEBUG_DETECTING) { 223 Slog.i(mLogTag, "onTripleTapAndHold()"); 224 } 225 mViewportDraggingState.mEnabledBeforeDrag = 226 mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId); 227 enableWindowMagnifier(up.getX(), up.getY(), 228 WindowMagnificationManager.WINDOW_POSITION_AT_TOP_LEFT); 229 mTripleTapAndHoldStartedTime = SystemClock.uptimeMillis(); 230 transitionTo(mViewportDraggingState); 231 } 232 233 @VisibleForTesting releaseTripleTapAndHold()234 void releaseTripleTapAndHold() { 235 if (!mViewportDraggingState.mEnabledBeforeDrag) { 236 mWindowMagnificationMgr.disableWindowMagnification(mDisplayId, true); 237 } 238 transitionTo(mDetectingState); 239 if (mTripleTapAndHoldStartedTime != 0) { 240 final long duration = SystemClock.uptimeMillis() - mTripleTapAndHoldStartedTime; 241 logMagnificationTripleTapAndHoldSession(duration); 242 mTripleTapAndHoldStartedTime = 0; 243 } 244 } 245 246 /** 247 * Logs the duration for the magnification session which is activated by the triple tap and 248 * hold gesture. 249 * 250 * @param duration The duration of a triple-tap-and-hold activation session. 251 */ 252 @VisibleForTesting logMagnificationTripleTapAndHoldSession(long duration)253 void logMagnificationTripleTapAndHoldSession(long duration) { 254 AccessibilityStatsLogUtils.logMagnificationTripleTapAndHoldSession(duration); 255 } 256 resetToDetectState()257 void resetToDetectState() { 258 transitionTo(mDetectingState); 259 } 260 261 /** 262 * An interface to intercept the {@link MotionEvent} for gesture detection. The intercepted 263 * events should be delivered to next {@link EventStreamTransformation} with { 264 * {@link EventStreamTransformation#onMotionEvent(MotionEvent, MotionEvent, int)}} if there is 265 * no valid gestures. 266 */ 267 interface State { onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)268 void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags); 269 clear()270 default void clear() { 271 } 272 onEnter()273 default void onEnter() { 274 } 275 onExit()276 default void onExit() { 277 } 278 name()279 default String name() { 280 return getClass().getSimpleName(); 281 } 282 nameOf(@ullable State s)283 static String nameOf(@Nullable State s) { 284 return s != null ? s.name() : "null"; 285 } 286 } 287 transitionTo(State state)288 private void transitionTo(State state) { 289 if (DEBUG_STATE_TRANSITIONS) { 290 Slog.i(mLogTag, "state transition: " + (State.nameOf(mCurrentState) + " -> " 291 + State.nameOf(state) + " at " 292 + asList(copyOfRange(new RuntimeException().getStackTrace(), 1, 5))) 293 .replace(getClass().getName(), "")); 294 } 295 mPreviousState = mCurrentState; 296 if (mPreviousState != null) { 297 mPreviousState.onExit(); 298 } 299 mCurrentState = state; 300 if (mCurrentState != null) { 301 mCurrentState.onEnter(); 302 } 303 } 304 305 /** 306 * When entering this state, {@link PanningScalingHandler} will be enabled to address the 307 * gestures until receiving {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL}. 308 * When leaving this state, current scale will be persisted. 309 */ 310 final class PanningScalingGestureState implements State { 311 private final PanningScalingHandler mPanningScalingHandler; 312 PanningScalingGestureState(PanningScalingHandler panningScalingHandler)313 PanningScalingGestureState(PanningScalingHandler panningScalingHandler) { 314 mPanningScalingHandler = panningScalingHandler; 315 } 316 317 @Override onEnter()318 public void onEnter() { 319 mPanningScalingHandler.setEnabled(true); 320 } 321 322 @Override onExit()323 public void onExit() { 324 mPanningScalingHandler.setEnabled(false); 325 mWindowMagnificationMgr.persistScale(mDisplayId); 326 clear(); 327 } 328 329 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)330 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 331 int action = event.getActionMasked(); 332 if (action == ACTION_UP || action == ACTION_CANCEL) { 333 transitionTo(mDetectingState); 334 } 335 } 336 337 @Override clear()338 public void clear() { 339 mPanningScalingHandler.clear(); 340 } 341 342 @Override toString()343 public String toString() { 344 return "PanningScalingState{" 345 + "mPanningScalingHandler=" + mPanningScalingHandler + '}'; 346 } 347 } 348 349 /** 350 * A state not to intercept {@link MotionEvent}. Leaving this state until receiving 351 * {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL}. 352 */ 353 final class DelegatingState implements State { 354 private final MotionEventDispatcherDelegate mMotionEventDispatcherDelegate; 355 DelegatingState(MotionEventDispatcherDelegate motionEventDispatcherDelegate)356 DelegatingState(MotionEventDispatcherDelegate motionEventDispatcherDelegate) { 357 mMotionEventDispatcherDelegate = motionEventDispatcherDelegate; 358 } 359 360 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)361 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 362 mMotionEventDispatcherDelegate.dispatchMotionEvent(event, rawEvent, policyFlags); 363 switch (event.getActionMasked()) { 364 case ACTION_UP: 365 case ACTION_CANCEL: { 366 transitionTo(mDetectingState); 367 } 368 break; 369 } 370 } 371 } 372 373 374 /** 375 * This class handles motion events when the event dispatcher has 376 * determined that the user is performing a single-finger drag of the 377 * magnification viewport. 378 * 379 * Leaving this state until receiving {@link MotionEvent#ACTION_UP} 380 * or {@link MotionEvent#ACTION_CANCEL}. 381 */ 382 final class ViewportDraggingState implements State { 383 384 /** Whether to disable zoom after dragging ends */ 385 boolean mEnabledBeforeDrag; 386 387 private float mLastX = Float.NaN; 388 private float mLastY = Float.NaN; 389 390 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)391 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 392 final int action = event.getActionMasked(); 393 switch (action) { 394 case ACTION_MOVE: { 395 if (!Float.isNaN(mLastX) && !Float.isNaN(mLastY)) { 396 float offsetX = event.getX() - mLastX; 397 float offsetY = event.getY() - mLastY; 398 mWindowMagnificationMgr.moveWindowMagnification(mDisplayId, offsetX, 399 offsetY); 400 } 401 mLastX = event.getX(); 402 mLastY = event.getY(); 403 } 404 break; 405 406 case ACTION_UP: 407 case ACTION_CANCEL: { 408 releaseTripleTapAndHold(); 409 } 410 break; 411 } 412 } 413 414 @Override clear()415 public void clear() { 416 mLastX = Float.NaN; 417 mLastY = Float.NaN; 418 } 419 420 @Override onExit()421 public void onExit() { 422 clear(); 423 } 424 425 @Override toString()426 public String toString() { 427 return "ViewportDraggingState{" 428 + "mLastX=" + mLastX 429 + ",mLastY=" + mLastY 430 + '}'; 431 } 432 } 433 434 /** 435 * This class handles motion events in a duration to determine if the user is going to 436 * manipulate the window magnifier or want to interact with current UI. The rule of leaving 437 * this state is as follows: 438 * <ol> 439 * <li> If {@link MagnificationGestureMatcher#GESTURE_TWO_FINGERS_DOWN_OR_SWIPE} is detected, 440 * {@link State} will be transited to {@link PanningScalingGestureState}.</li> 441 * <li> If other gesture is detected and the last motion event is neither ACTION_UP nor 442 * ACTION_CANCEL. 443 * </ol> 444 * <b>Note</b> The motion events will be cached and dispatched before leaving this state. 445 */ 446 final class DetectingState implements State, 447 MagnificationGesturesObserver.Callback { 448 449 private final MagnificationGesturesObserver mGesturesObserver; 450 451 /** 452 * {@code true} if this detector should detect and respond to triple-tap 453 * gestures for engaging and disengaging magnification, 454 * {@code false} if it should ignore such gestures 455 */ 456 private final boolean mDetectTripleTap; 457 DetectingState(@iContext Context context, boolean detectTripleTap)458 DetectingState(@UiContext Context context, boolean detectTripleTap) { 459 mDetectTripleTap = detectTripleTap; 460 final MultiTap multiTap = new MultiTap(context, mDetectTripleTap ? 3 : 1, 461 mDetectTripleTap 462 ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP 463 : MagnificationGestureMatcher.GESTURE_SINGLE_TAP, null); 464 final MultiTapAndHold multiTapAndHold = new MultiTapAndHold(context, 465 mDetectTripleTap ? 3 : 1, 466 mDetectTripleTap 467 ? MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD 468 : MagnificationGestureMatcher.GESTURE_SINGLE_TAP_AND_HOLD, null); 469 mGesturesObserver = new MagnificationGesturesObserver(this, 470 new SimpleSwipe(context), 471 multiTap, 472 multiTapAndHold, 473 new TwoFingersDownOrSwipe(context)); 474 } 475 476 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)477 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 478 mGesturesObserver.onMotionEvent(event, rawEvent, policyFlags); 479 } 480 481 @Override toString()482 public String toString() { 483 return "DetectingState{" 484 + "mGestureTimeoutObserver=" + mGesturesObserver 485 + '}'; 486 } 487 488 @Override shouldStopDetection(MotionEvent motionEvent)489 public boolean shouldStopDetection(MotionEvent motionEvent) { 490 return !mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId) 491 && !mDetectTripleTap; 492 } 493 494 @Override onGestureCompleted(int gestureId, long lastDownEventTime, List<MotionEventInfo> delayedEventQueue, MotionEvent motionEvent)495 public void onGestureCompleted(int gestureId, long lastDownEventTime, 496 List<MotionEventInfo> delayedEventQueue, 497 MotionEvent motionEvent) { 498 if (DEBUG_DETECTING) { 499 Slog.d(mLogTag, "onGestureDetected : gesture = " 500 + MagnificationGestureMatcher.gestureIdToString( 501 gestureId)); 502 Slog.d(mLogTag, 503 "onGestureDetected : delayedEventQueue = " + delayedEventQueue); 504 } 505 if (gestureId == MagnificationGestureMatcher.GESTURE_TWO_FINGERS_DOWN_OR_SWIPE 506 && mWindowMagnificationMgr.pointersInWindow(mDisplayId, motionEvent) > 0) { 507 transitionTo(mObservePanningScalingState); 508 } else if (gestureId == MagnificationGestureMatcher.GESTURE_TRIPLE_TAP) { 509 onTripleTap(motionEvent); 510 } else if (gestureId == MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD) { 511 onTripleTapAndHold(motionEvent); 512 } else { 513 mMotionEventDispatcherDelegate.sendDelayedMotionEvents(delayedEventQueue, 514 lastDownEventTime); 515 changeToDelegateStateIfNeed(motionEvent); 516 } 517 } 518 519 @Override onGestureCancelled(long lastDownEventTime, List<MotionEventInfo> delayedEventQueue, MotionEvent motionEvent)520 public void onGestureCancelled(long lastDownEventTime, 521 List<MotionEventInfo> delayedEventQueue, 522 MotionEvent motionEvent) { 523 if (DEBUG_DETECTING) { 524 Slog.d(mLogTag, 525 "onGestureCancelled : delayedEventQueue = " + delayedEventQueue); 526 } 527 mMotionEventDispatcherDelegate.sendDelayedMotionEvents(delayedEventQueue, 528 lastDownEventTime); 529 changeToDelegateStateIfNeed(motionEvent); 530 } 531 changeToDelegateStateIfNeed(MotionEvent motionEvent)532 private void changeToDelegateStateIfNeed(MotionEvent motionEvent) { 533 if (motionEvent != null && (motionEvent.getActionMasked() == ACTION_UP 534 || motionEvent.getActionMasked() == ACTION_CANCEL)) { 535 return; 536 } 537 transitionTo(mDelegatingState); 538 } 539 } 540 541 @Override toString()542 public String toString() { 543 return "WindowMagnificationGestureHandler{" 544 + "mDetectingState=" + mDetectingState 545 + ", mDelegatingState=" + mDelegatingState 546 + ", mViewportDraggingState=" + mViewportDraggingState 547 + ", mMagnifiedInteractionState=" + mObservePanningScalingState 548 + ", mCurrentState=" + State.nameOf(mCurrentState) 549 + ", mPreviousState=" + State.nameOf(mPreviousState) 550 + ", mWindowMagnificationMgr=" + mWindowMagnificationMgr 551 + ", mDisplayId=" + mDisplayId 552 + '}'; 553 } 554 } 555