1 /* 2 * Copyright (C) 2015 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; 18 19 import static android.view.InputDevice.SOURCE_TOUCHSCREEN; 20 import static android.view.MotionEvent.ACTION_CANCEL; 21 import static android.view.MotionEvent.ACTION_DOWN; 22 import static android.view.MotionEvent.ACTION_MOVE; 23 import static android.view.MotionEvent.ACTION_POINTER_DOWN; 24 import static android.view.MotionEvent.ACTION_POINTER_UP; 25 import static android.view.MotionEvent.ACTION_UP; 26 27 import static com.android.server.accessibility.GestureUtils.distance; 28 29 import static java.lang.Math.abs; 30 import static java.util.Arrays.asList; 31 import static java.util.Arrays.copyOfRange; 32 33 import android.annotation.NonNull; 34 import android.annotation.Nullable; 35 import android.content.BroadcastReceiver; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.IntentFilter; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.os.Message; 42 import android.util.Log; 43 import android.util.MathUtils; 44 import android.util.Slog; 45 import android.util.TypedValue; 46 import android.view.GestureDetector; 47 import android.view.GestureDetector.SimpleOnGestureListener; 48 import android.view.MotionEvent; 49 import android.view.MotionEvent.PointerCoords; 50 import android.view.MotionEvent.PointerProperties; 51 import android.view.ScaleGestureDetector; 52 import android.view.ScaleGestureDetector.OnScaleGestureListener; 53 import android.view.ViewConfiguration; 54 55 import com.android.internal.annotations.VisibleForTesting; 56 57 import java.util.ArrayDeque; 58 import java.util.Queue; 59 60 /** 61 * This class handles magnification in response to touch events. 62 * 63 * The behavior is as follows: 64 * 65 * 1. Triple tap toggles permanent screen magnification which is magnifying 66 * the area around the location of the triple tap. One can think of the 67 * location of the triple tap as the center of the magnified viewport. 68 * For example, a triple tap when not magnified would magnify the screen 69 * and leave it in a magnified state. A triple tapping when magnified would 70 * clear magnification and leave the screen in a not magnified state. 71 * 72 * 2. Triple tap and hold would magnify the screen if not magnified and enable 73 * viewport dragging mode until the finger goes up. One can think of this 74 * mode as a way to move the magnified viewport since the area around the 75 * moving finger will be magnified to fit the screen. For example, if the 76 * screen was not magnified and the user triple taps and holds the screen 77 * would magnify and the viewport will follow the user's finger. When the 78 * finger goes up the screen will zoom out. If the same user interaction 79 * is performed when the screen is magnified, the viewport movement will 80 * be the same but when the finger goes up the screen will stay magnified. 81 * In other words, the initial magnified state is sticky. 82 * 83 * 3. Magnification can optionally be "triggered" by some external shortcut 84 * affordance. When this occurs via {@link #notifyShortcutTriggered()} a 85 * subsequent tap in a magnifiable region will engage permanent screen 86 * magnification as described in #1. Alternatively, a subsequent long-press 87 * or drag will engage magnification with viewport dragging as described in 88 * #2. Once magnified, all following behaviors apply whether magnification 89 * was engaged via a triple-tap or by a triggered shortcut. 90 * 91 * 4. Pinching with any number of additional fingers when viewport dragging 92 * is enabled, i.e. the user triple tapped and holds, would adjust the 93 * magnification scale which will become the current default magnification 94 * scale. The next time the user magnifies the same magnification scale 95 * would be used. 96 * 97 * 5. When in a permanent magnified state the user can use two or more fingers 98 * to pan the viewport. Note that in this mode the content is panned as 99 * opposed to the viewport dragging mode in which the viewport is moved. 100 * 101 * 6. When in a permanent magnified state the user can use two or more 102 * fingers to change the magnification scale which will become the current 103 * default magnification scale. The next time the user magnifies the same 104 * magnification scale would be used. 105 * 106 * 7. The magnification scale will be persisted in settings and in the cloud. 107 */ 108 @SuppressWarnings("WeakerAccess") 109 class MagnificationGestureHandler extends BaseEventStreamTransformation { 110 private static final String LOG_TAG = "MagnificationGestureHandler"; 111 112 private static final boolean DEBUG_ALL = false; 113 private static final boolean DEBUG_STATE_TRANSITIONS = false || DEBUG_ALL; 114 private static final boolean DEBUG_DETECTING = false || DEBUG_ALL; 115 private static final boolean DEBUG_PANNING_SCALING = false || DEBUG_ALL; 116 private static final boolean DEBUG_EVENT_STREAM = false || DEBUG_ALL; 117 118 // The MIN_SCALE is different from MagnificationController.MIN_SCALE due 119 // to AccessibilityService.MagnificationController#setScale() has 120 // different scale range 121 private static final float MIN_SCALE = 2.0f; 122 private static final float MAX_SCALE = MagnificationController.MAX_SCALE; 123 124 @VisibleForTesting final MagnificationController mMagnificationController; 125 126 @VisibleForTesting final DelegatingState mDelegatingState; 127 @VisibleForTesting final DetectingState mDetectingState; 128 @VisibleForTesting final PanningScalingState mPanningScalingState; 129 @VisibleForTesting final ViewportDraggingState mViewportDraggingState; 130 131 private final ScreenStateReceiver mScreenStateReceiver; 132 133 /** 134 * {@code true} if this detector should detect and respond to triple-tap 135 * gestures for engaging and disengaging magnification, 136 * {@code false} if it should ignore such gestures 137 */ 138 final boolean mDetectTripleTap; 139 140 /** 141 * Whether {@link DetectingState#mShortcutTriggered shortcut} is enabled 142 */ 143 final boolean mDetectShortcutTrigger; 144 145 @VisibleForTesting State mCurrentState; 146 @VisibleForTesting State mPreviousState; 147 148 private PointerCoords[] mTempPointerCoords; 149 private PointerProperties[] mTempPointerProperties; 150 151 private final int mDisplayId; 152 153 private final Queue<MotionEvent> mDebugInputEventHistory; 154 private final Queue<MotionEvent> mDebugOutputEventHistory; 155 156 /** 157 * @param context Context for resolving various magnification-related resources 158 * @param magnificationController the {@link MagnificationController} 159 * 160 * @param detectTripleTap {@code true} if this detector should detect and respond to triple-tap 161 * gestures for engaging and disengaging magnification, 162 * {@code false} if it should ignore such gestures 163 * @param detectShortcutTrigger {@code true} if this detector should be "triggerable" by some 164 * external shortcut invoking {@link #notifyShortcutTriggered}, 165 * {@code false} if it should ignore such triggers. 166 * @param displayId The logical display id. 167 */ MagnificationGestureHandler(Context context, MagnificationController magnificationController, boolean detectTripleTap, boolean detectShortcutTrigger, int displayId)168 public MagnificationGestureHandler(Context context, 169 MagnificationController magnificationController, 170 boolean detectTripleTap, 171 boolean detectShortcutTrigger, 172 int displayId) { 173 if (DEBUG_ALL) { 174 Log.i(LOG_TAG, 175 "MagnificationGestureHandler(detectTripleTap = " + detectTripleTap 176 + ", detectShortcutTrigger = " + detectShortcutTrigger + ")"); 177 } 178 179 mMagnificationController = magnificationController; 180 mDisplayId = displayId; 181 182 mDelegatingState = new DelegatingState(); 183 mDetectingState = new DetectingState(context); 184 mViewportDraggingState = new ViewportDraggingState(); 185 mPanningScalingState = new PanningScalingState(context); 186 187 mDetectTripleTap = detectTripleTap; 188 mDetectShortcutTrigger = detectShortcutTrigger; 189 190 if (mDetectShortcutTrigger) { 191 mScreenStateReceiver = new ScreenStateReceiver(context, this); 192 mScreenStateReceiver.register(); 193 } else { 194 mScreenStateReceiver = null; 195 } 196 197 mDebugInputEventHistory = DEBUG_EVENT_STREAM ? new ArrayDeque<>() : null; 198 mDebugOutputEventHistory = DEBUG_EVENT_STREAM ? new ArrayDeque<>() : null; 199 200 transitionTo(mDetectingState); 201 } 202 203 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)204 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 205 if (DEBUG_EVENT_STREAM) { 206 storeEventInto(mDebugInputEventHistory, event); 207 try { 208 onMotionEventInternal(event, rawEvent, policyFlags); 209 } catch (Exception e) { 210 throw new RuntimeException( 211 "Exception following input events: " + mDebugInputEventHistory, e); 212 } 213 } else { 214 onMotionEventInternal(event, rawEvent, policyFlags); 215 } 216 } 217 onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags)218 private void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 219 if (DEBUG_ALL) Slog.i(LOG_TAG, "onMotionEvent(" + event + ")"); 220 221 if ((!mDetectTripleTap && !mDetectShortcutTrigger) 222 || !event.isFromSource(SOURCE_TOUCHSCREEN)) { 223 dispatchTransformedEvent(event, rawEvent, policyFlags); 224 return; 225 } 226 227 handleEventWith(mCurrentState, event, rawEvent, policyFlags); 228 } 229 handleEventWith(State stateHandler, MotionEvent event, MotionEvent rawEvent, int policyFlags)230 private void handleEventWith(State stateHandler, 231 MotionEvent event, MotionEvent rawEvent, int policyFlags) { 232 // To keep InputEventConsistencyVerifiers within GestureDetectors happy 233 mPanningScalingState.mScrollGestureDetector.onTouchEvent(event); 234 mPanningScalingState.mScaleGestureDetector.onTouchEvent(event); 235 236 stateHandler.onMotionEvent(event, rawEvent, policyFlags); 237 } 238 239 @Override clearEvents(int inputSource)240 public void clearEvents(int inputSource) { 241 if (inputSource == SOURCE_TOUCHSCREEN) { 242 clearAndTransitionToStateDetecting(); 243 } 244 245 super.clearEvents(inputSource); 246 } 247 248 @Override onDestroy()249 public void onDestroy() { 250 if (DEBUG_STATE_TRANSITIONS) { 251 Slog.i(LOG_TAG, "onDestroy(); delayed = " 252 + MotionEventInfo.toString(mDetectingState.mDelayedEventQueue)); 253 } 254 255 if (mScreenStateReceiver != null) { 256 mScreenStateReceiver.unregister(); 257 } 258 // Check if need to reset when MagnificationGestureHandler is the last magnifying service. 259 mMagnificationController.resetAllIfNeeded( 260 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 261 clearAndTransitionToStateDetecting(); 262 } 263 notifyShortcutTriggered()264 void notifyShortcutTriggered() { 265 if (mDetectShortcutTrigger) { 266 boolean wasMagnifying = mMagnificationController.resetIfNeeded(mDisplayId, 267 /* animate */ true); 268 if (wasMagnifying) { 269 clearAndTransitionToStateDetecting(); 270 } else { 271 mDetectingState.toggleShortcutTriggered(); 272 } 273 } 274 } 275 clearAndTransitionToStateDetecting()276 void clearAndTransitionToStateDetecting() { 277 mCurrentState = mDetectingState; 278 mDetectingState.clear(); 279 mViewportDraggingState.clear(); 280 mPanningScalingState.clear(); 281 } 282 dispatchTransformedEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)283 private void dispatchTransformedEvent(MotionEvent event, MotionEvent rawEvent, 284 int policyFlags) { 285 if (DEBUG_EVENT_STREAM) { 286 storeEventInto(mDebugOutputEventHistory, event); 287 try { 288 super.onMotionEvent(event, rawEvent, policyFlags); 289 } catch (Exception e) { 290 throw new RuntimeException( 291 "Exception downstream following input events: " + mDebugInputEventHistory 292 + "\nTransformed into output events: " + mDebugOutputEventHistory, 293 e); 294 } 295 } else { 296 super.onMotionEvent(event, rawEvent, policyFlags); 297 } 298 } 299 storeEventInto(Queue<MotionEvent> queue, MotionEvent event)300 private static void storeEventInto(Queue<MotionEvent> queue, MotionEvent event) { 301 queue.add(MotionEvent.obtain(event)); 302 // Prune old events 303 while (!queue.isEmpty() && (event.getEventTime() - queue.peek().getEventTime() > 5000)) { 304 queue.remove().recycle(); 305 } 306 } 307 getTempPointerCoordsWithMinSize(int size)308 private PointerCoords[] getTempPointerCoordsWithMinSize(int size) { 309 final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0; 310 if (oldSize < size) { 311 PointerCoords[] oldTempPointerCoords = mTempPointerCoords; 312 mTempPointerCoords = new PointerCoords[size]; 313 if (oldTempPointerCoords != null) { 314 System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize); 315 } 316 } 317 for (int i = oldSize; i < size; i++) { 318 mTempPointerCoords[i] = new PointerCoords(); 319 } 320 return mTempPointerCoords; 321 } 322 getTempPointerPropertiesWithMinSize(int size)323 private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) { 324 final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length 325 : 0; 326 if (oldSize < size) { 327 PointerProperties[] oldTempPointerProperties = mTempPointerProperties; 328 mTempPointerProperties = new PointerProperties[size]; 329 if (oldTempPointerProperties != null) { 330 System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, 331 oldSize); 332 } 333 } 334 for (int i = oldSize; i < size; i++) { 335 mTempPointerProperties[i] = new PointerProperties(); 336 } 337 return mTempPointerProperties; 338 } 339 transitionTo(State state)340 private void transitionTo(State state) { 341 if (DEBUG_STATE_TRANSITIONS) { 342 Slog.i(LOG_TAG, 343 (State.nameOf(mCurrentState) + " -> " + State.nameOf(state) 344 + " at " + asList(copyOfRange(new RuntimeException().getStackTrace(), 1, 5))) 345 .replace(getClass().getName(), "")); 346 } 347 mPreviousState = mCurrentState; 348 mCurrentState = state; 349 } 350 351 interface State { onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)352 void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags); 353 clear()354 default void clear() {} 355 name()356 default String name() { 357 return getClass().getSimpleName(); 358 } 359 nameOf(@ullable State s)360 static String nameOf(@Nullable State s) { 361 return s != null ? s.name() : "null"; 362 } 363 } 364 365 /** 366 * This class determines if the user is performing a scale or pan gesture. 367 * 368 * Unlike when {@link ViewportDraggingState dragging the viewport}, in panning mode the viewport 369 * moves in the same direction as the fingers, and allows to easily and precisely scale the 370 * magnification level. 371 * This makes it the preferred mode for one-off adjustments, due to its precision and ease of 372 * triggering. 373 */ 374 final class PanningScalingState extends SimpleOnGestureListener 375 implements OnScaleGestureListener, State { 376 377 private final ScaleGestureDetector mScaleGestureDetector; 378 private final GestureDetector mScrollGestureDetector; 379 final float mScalingThreshold; 380 381 float mInitialScaleFactor = -1; 382 boolean mScaling; 383 PanningScalingState(Context context)384 public PanningScalingState(Context context) { 385 final TypedValue scaleValue = new TypedValue(); 386 context.getResources().getValue( 387 com.android.internal.R.dimen.config_screen_magnification_scaling_threshold, 388 scaleValue, false); 389 mScalingThreshold = scaleValue.getFloat(); 390 mScaleGestureDetector = new ScaleGestureDetector(context, this, Handler.getMain()); 391 mScaleGestureDetector.setQuickScaleEnabled(false); 392 mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain()); 393 } 394 395 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)396 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 397 int action = event.getActionMasked(); 398 399 if (action == ACTION_POINTER_UP 400 && event.getPointerCount() == 2 // includes the pointer currently being released 401 && mPreviousState == mViewportDraggingState) { 402 403 persistScaleAndTransitionTo(mViewportDraggingState); 404 405 } else if (action == ACTION_UP || action == ACTION_CANCEL) { 406 407 persistScaleAndTransitionTo(mDetectingState); 408 409 } 410 } 411 persistScaleAndTransitionTo(State state)412 public void persistScaleAndTransitionTo(State state) { 413 mMagnificationController.persistScale(); 414 clear(); 415 transitionTo(state); 416 } 417 418 @Override onScroll(MotionEvent first, MotionEvent second, float distanceX, float distanceY)419 public boolean onScroll(MotionEvent first, MotionEvent second, 420 float distanceX, float distanceY) { 421 if (mCurrentState != mPanningScalingState) { 422 return true; 423 } 424 if (DEBUG_PANNING_SCALING) { 425 Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX 426 + " scrollY: " + distanceY); 427 } 428 mMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX, 429 distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 430 return /* event consumed: */ true; 431 } 432 433 @Override onScale(ScaleGestureDetector detector)434 public boolean onScale(ScaleGestureDetector detector) { 435 if (!mScaling) { 436 if (mInitialScaleFactor < 0) { 437 mInitialScaleFactor = detector.getScaleFactor(); 438 return false; 439 } 440 final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor; 441 mScaling = abs(deltaScale) > mScalingThreshold; 442 return mScaling; 443 } 444 445 final float initialScale = mMagnificationController.getScale(mDisplayId); 446 final float targetScale = initialScale * detector.getScaleFactor(); 447 448 // Don't allow a gesture to move the user further outside the 449 // desired bounds for gesture-controlled scaling. 450 final float scale; 451 if (targetScale > MAX_SCALE && targetScale > initialScale) { 452 // The target scale is too big and getting bigger. 453 scale = MAX_SCALE; 454 } else if (targetScale < MIN_SCALE && targetScale < initialScale) { 455 // The target scale is too small and getting smaller. 456 scale = MIN_SCALE; 457 } else { 458 // The target scale may be outside our bounds, but at least 459 // it's moving in the right direction. This avoids a "jump" if 460 // we're at odds with some other service's desired bounds. 461 scale = targetScale; 462 } 463 464 final float pivotX = detector.getFocusX(); 465 final float pivotY = detector.getFocusY(); 466 if (DEBUG_PANNING_SCALING) Slog.i(LOG_TAG, "Scaled content to: " + scale + "x"); 467 mMagnificationController.setScale(mDisplayId, scale, pivotX, pivotY, false, 468 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 469 return /* handled: */ true; 470 } 471 472 @Override onScaleBegin(ScaleGestureDetector detector)473 public boolean onScaleBegin(ScaleGestureDetector detector) { 474 return /* continue recognizing: */ (mCurrentState == mPanningScalingState); 475 } 476 477 @Override onScaleEnd(ScaleGestureDetector detector)478 public void onScaleEnd(ScaleGestureDetector detector) { 479 clear(); 480 } 481 482 @Override clear()483 public void clear() { 484 mInitialScaleFactor = -1; 485 mScaling = false; 486 } 487 488 @Override toString()489 public String toString() { 490 return "PanningScalingState{" + 491 "mInitialScaleFactor=" + mInitialScaleFactor + 492 ", mScaling=" + mScaling + 493 '}'; 494 } 495 } 496 497 /** 498 * This class handles motion events when the event dispatcher has 499 * determined that the user is performing a single-finger drag of the 500 * magnification viewport. 501 * 502 * Unlike when {@link PanningScalingState panning}, the viewport moves in the opposite direction 503 * of the finger, and any part of the screen is reachable without lifting the finger. 504 * This makes it the preferable mode for tasks like reading text spanning full screen width. 505 */ 506 final class ViewportDraggingState implements State { 507 508 /** Whether to disable zoom after dragging ends */ 509 boolean mZoomedInBeforeDrag; 510 private boolean mLastMoveOutsideMagnifiedRegion; 511 512 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)513 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 514 final int action = event.getActionMasked(); 515 switch (action) { 516 case ACTION_POINTER_DOWN: { 517 clear(); 518 transitionTo(mPanningScalingState); 519 } 520 break; 521 case ACTION_MOVE: { 522 if (event.getPointerCount() != 1) { 523 throw new IllegalStateException("Should have one pointer down."); 524 } 525 final float eventX = event.getX(); 526 final float eventY = event.getY(); 527 if (mMagnificationController.magnificationRegionContains( 528 mDisplayId, eventX, eventY)) { 529 mMagnificationController.setCenter(mDisplayId, eventX, eventY, 530 /* animate */ mLastMoveOutsideMagnifiedRegion, 531 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 532 mLastMoveOutsideMagnifiedRegion = false; 533 } else { 534 mLastMoveOutsideMagnifiedRegion = true; 535 } 536 } 537 break; 538 539 case ACTION_UP: 540 case ACTION_CANCEL: { 541 if (!mZoomedInBeforeDrag) zoomOff(); 542 clear(); 543 transitionTo(mDetectingState); 544 } 545 break; 546 547 case ACTION_DOWN: 548 case ACTION_POINTER_UP: { 549 throw new IllegalArgumentException( 550 "Unexpected event type: " + MotionEvent.actionToString(action)); 551 } 552 } 553 } 554 555 @Override clear()556 public void clear() { 557 mLastMoveOutsideMagnifiedRegion = false; 558 } 559 560 @Override toString()561 public String toString() { 562 return "ViewportDraggingState{" + 563 "mZoomedInBeforeDrag=" + mZoomedInBeforeDrag + 564 ", mLastMoveOutsideMagnifiedRegion=" + mLastMoveOutsideMagnifiedRegion + 565 '}'; 566 } 567 } 568 569 final class DelegatingState implements State { 570 /** 571 * Time of last {@link MotionEvent#ACTION_DOWN} while in {@link DelegatingState} 572 */ 573 public long mLastDelegatedDownEventTime; 574 575 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)576 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 577 578 // Ensure that the state at the end of delegation is consistent with the last delegated 579 // UP/DOWN event in queue: still delegating if pointer is down, detecting otherwise 580 switch (event.getActionMasked()) { 581 case ACTION_UP: 582 case ACTION_CANCEL: { 583 transitionTo(mDetectingState); 584 } break; 585 586 case ACTION_DOWN: { 587 transitionTo(mDelegatingState); 588 mLastDelegatedDownEventTime = event.getDownTime(); 589 } break; 590 } 591 592 if (getNext() != null) { 593 // We cache some events to see if the user wants to trigger magnification. 594 // If no magnification is triggered we inject these events with adjusted 595 // time and down time to prevent subsequent transformations being confused 596 // by stale events. After the cached events, which always have a down, are 597 // injected we need to also update the down time of all subsequent non cached 598 // events. All delegated events cached and non-cached are delivered here. 599 event.setDownTime(mLastDelegatedDownEventTime); 600 dispatchTransformedEvent(event, rawEvent, policyFlags); 601 } 602 } 603 } 604 605 /** 606 * This class handles motion events when the event dispatch has not yet 607 * determined what the user is doing. It watches for various tap events. 608 */ 609 final class DetectingState implements State, Handler.Callback { 610 611 private static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1; 612 private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; 613 614 final int mLongTapMinDelay; 615 final int mSwipeMinDistance; 616 final int mMultiTapMaxDelay; 617 final int mMultiTapMaxDistance; 618 619 private MotionEventInfo mDelayedEventQueue; 620 MotionEvent mLastDown; 621 private MotionEvent mPreLastDown; 622 private MotionEvent mLastUp; 623 private MotionEvent mPreLastUp; 624 625 @VisibleForTesting boolean mShortcutTriggered; 626 627 @VisibleForTesting Handler mHandler = new Handler(Looper.getMainLooper(), this); 628 DetectingState(Context context)629 public DetectingState(Context context) { 630 mLongTapMinDelay = ViewConfiguration.getLongPressTimeout(); 631 mMultiTapMaxDelay = ViewConfiguration.getDoubleTapTimeout() 632 + context.getResources().getInteger( 633 com.android.internal.R.integer.config_screen_magnification_multi_tap_adjustment); 634 mSwipeMinDistance = ViewConfiguration.get(context).getScaledTouchSlop(); 635 mMultiTapMaxDistance = ViewConfiguration.get(context).getScaledDoubleTapSlop(); 636 } 637 638 @Override handleMessage(Message message)639 public boolean handleMessage(Message message) { 640 final int type = message.what; 641 switch (type) { 642 case MESSAGE_ON_TRIPLE_TAP_AND_HOLD: { 643 MotionEvent down = (MotionEvent) message.obj; 644 transitionToViewportDraggingStateAndClear(down); 645 down.recycle(); 646 } 647 break; 648 case MESSAGE_TRANSITION_TO_DELEGATING_STATE: { 649 transitionToDelegatingStateAndClear(); 650 } 651 break; 652 default: { 653 throw new IllegalArgumentException("Unknown message type: " + type); 654 } 655 } 656 return true; 657 } 658 659 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)660 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 661 cacheDelayedMotionEvent(event, rawEvent, policyFlags); 662 switch (event.getActionMasked()) { 663 case MotionEvent.ACTION_DOWN: { 664 665 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 666 667 if (!mMagnificationController.magnificationRegionContains( 668 mDisplayId, event.getX(), event.getY())) { 669 670 transitionToDelegatingStateAndClear(); 671 672 } else if (isMultiTapTriggered(2 /* taps */)) { 673 674 // 3tap and hold 675 afterLongTapTimeoutTransitionToDraggingState(event); 676 677 } else if (isTapOutOfDistanceSlop()) { 678 679 transitionToDelegatingStateAndClear(); 680 681 } else if (mDetectTripleTap 682 // If magnified, delay an ACTION_DOWN for mMultiTapMaxDelay 683 // to ensure reachability of 684 // STATE_PANNING_SCALING(triggerable with ACTION_POINTER_DOWN) 685 || mMagnificationController.isMagnifying(mDisplayId)) { 686 687 afterMultiTapTimeoutTransitionToDelegatingState(); 688 689 } else { 690 691 // Delegate pending events without delay 692 transitionToDelegatingStateAndClear(); 693 } 694 } 695 break; 696 case ACTION_POINTER_DOWN: { 697 if (mMagnificationController.isMagnifying(mDisplayId)) { 698 transitionTo(mPanningScalingState); 699 clear(); 700 } else { 701 transitionToDelegatingStateAndClear(); 702 } 703 } 704 break; 705 case ACTION_MOVE: { 706 if (isFingerDown() 707 && distance(mLastDown, /* move */ event) > mSwipeMinDistance) { 708 709 // Swipe detected - transition immediately 710 711 // For convenience, viewport dragging takes precedence 712 // over insta-delegating on 3tap&swipe 713 // (which is a rare combo to be used aside from magnification) 714 if (isMultiTapTriggered(2 /* taps */)) { 715 transitionToViewportDraggingStateAndClear(event); 716 } else { 717 transitionToDelegatingStateAndClear(); 718 } 719 } 720 } 721 break; 722 case ACTION_UP: { 723 724 mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD); 725 726 if (!mMagnificationController.magnificationRegionContains( 727 mDisplayId, event.getX(), event.getY())) { 728 729 transitionToDelegatingStateAndClear(); 730 731 } else if (isMultiTapTriggered(3 /* taps */)) { 732 733 onTripleTap(/* up */ event); 734 735 } else if ( 736 // Possible to be false on: 3tap&drag -> scale -> PTR_UP -> UP 737 isFingerDown() 738 //TODO long tap should never happen here 739 && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay) 740 || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))) { 741 742 transitionToDelegatingStateAndClear(); 743 744 } 745 } 746 break; 747 } 748 } 749 isMultiTapTriggered(int numTaps)750 public boolean isMultiTapTriggered(int numTaps) { 751 752 // Shortcut acts as the 2 initial taps 753 if (mShortcutTriggered) return tapCount() + 2 >= numTaps; 754 755 return mDetectTripleTap 756 && tapCount() >= numTaps 757 && isMultiTap(mPreLastDown, mLastDown) 758 && isMultiTap(mPreLastUp, mLastUp); 759 } 760 isMultiTap(MotionEvent first, MotionEvent second)761 private boolean isMultiTap(MotionEvent first, MotionEvent second) { 762 return GestureUtils.isMultiTap(first, second, mMultiTapMaxDelay, mMultiTapMaxDistance); 763 } 764 isFingerDown()765 public boolean isFingerDown() { 766 return mLastDown != null; 767 } 768 timeBetween(@ullable MotionEvent a, @Nullable MotionEvent b)769 private long timeBetween(@Nullable MotionEvent a, @Nullable MotionEvent b) { 770 if (a == null && b == null) return 0; 771 return abs(timeOf(a) - timeOf(b)); 772 } 773 774 /** 775 * Nullsafe {@link MotionEvent#getEventTime} that interprets null event as something that 776 * has happened long enough ago to be gone from the event queue. 777 * Thus the time for a null event is a small number, that is below any other non-null 778 * event's time. 779 * 780 * @return {@link MotionEvent#getEventTime}, or {@link Long#MIN_VALUE} if the event is null 781 */ timeOf(@ullable MotionEvent event)782 private long timeOf(@Nullable MotionEvent event) { 783 return event != null ? event.getEventTime() : Long.MIN_VALUE; 784 } 785 tapCount()786 public int tapCount() { 787 return MotionEventInfo.countOf(mDelayedEventQueue, ACTION_UP); 788 } 789 790 /** -> {@link DelegatingState} */ afterMultiTapTimeoutTransitionToDelegatingState()791 public void afterMultiTapTimeoutTransitionToDelegatingState() { 792 mHandler.sendEmptyMessageDelayed( 793 MESSAGE_TRANSITION_TO_DELEGATING_STATE, 794 mMultiTapMaxDelay); 795 } 796 797 /** -> {@link ViewportDraggingState} */ afterLongTapTimeoutTransitionToDraggingState(MotionEvent event)798 public void afterLongTapTimeoutTransitionToDraggingState(MotionEvent event) { 799 mHandler.sendMessageDelayed( 800 mHandler.obtainMessage(MESSAGE_ON_TRIPLE_TAP_AND_HOLD, 801 MotionEvent.obtain(event)), 802 ViewConfiguration.getLongPressTimeout()); 803 } 804 805 @Override clear()806 public void clear() { 807 setShortcutTriggered(false); 808 removePendingDelayedMessages(); 809 clearDelayedMotionEvents(); 810 } 811 removePendingDelayedMessages()812 private void removePendingDelayedMessages() { 813 mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD); 814 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 815 } 816 cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)817 private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, 818 int policyFlags) { 819 if (event.getActionMasked() == ACTION_DOWN) { 820 mPreLastDown = mLastDown; 821 mLastDown = MotionEvent.obtain(event); 822 } else if (event.getActionMasked() == ACTION_UP) { 823 mPreLastUp = mLastUp; 824 mLastUp = MotionEvent.obtain(event); 825 } 826 827 MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent, 828 policyFlags); 829 if (mDelayedEventQueue == null) { 830 mDelayedEventQueue = info; 831 } else { 832 MotionEventInfo tail = mDelayedEventQueue; 833 while (tail.mNext != null) { 834 tail = tail.mNext; 835 } 836 tail.mNext = info; 837 } 838 } 839 sendDelayedMotionEvents()840 private void sendDelayedMotionEvents() { 841 while (mDelayedEventQueue != null) { 842 MotionEventInfo info = mDelayedEventQueue; 843 mDelayedEventQueue = info.mNext; 844 845 handleEventWith(mDelegatingState, info.event, info.rawEvent, info.policyFlags); 846 847 info.recycle(); 848 } 849 } 850 clearDelayedMotionEvents()851 private void clearDelayedMotionEvents() { 852 while (mDelayedEventQueue != null) { 853 MotionEventInfo info = mDelayedEventQueue; 854 mDelayedEventQueue = info.mNext; 855 info.recycle(); 856 } 857 mPreLastDown = null; 858 mPreLastUp = null; 859 mLastDown = null; 860 mLastUp = null; 861 } 862 transitionToDelegatingStateAndClear()863 void transitionToDelegatingStateAndClear() { 864 transitionTo(mDelegatingState); 865 sendDelayedMotionEvents(); 866 removePendingDelayedMessages(); 867 } 868 onTripleTap(MotionEvent up)869 private void onTripleTap(MotionEvent up) { 870 871 if (DEBUG_DETECTING) { 872 Slog.i(LOG_TAG, "onTripleTap(); delayed: " 873 + MotionEventInfo.toString(mDelayedEventQueue)); 874 } 875 clear(); 876 877 // Toggle zoom 878 if (mMagnificationController.isMagnifying(mDisplayId)) { 879 zoomOff(); 880 } else { 881 zoomOn(up.getX(), up.getY()); 882 } 883 } 884 transitionToViewportDraggingStateAndClear(MotionEvent down)885 void transitionToViewportDraggingStateAndClear(MotionEvent down) { 886 887 if (DEBUG_DETECTING) Slog.i(LOG_TAG, "onTripleTapAndHold()"); 888 clear(); 889 890 mViewportDraggingState.mZoomedInBeforeDrag = 891 mMagnificationController.isMagnifying(mDisplayId); 892 893 zoomOn(down.getX(), down.getY()); 894 895 transitionTo(mViewportDraggingState); 896 } 897 898 @Override toString()899 public String toString() { 900 return "DetectingState{" + 901 "tapCount()=" + tapCount() + 902 ", mShortcutTriggered=" + mShortcutTriggered + 903 ", mDelayedEventQueue=" + MotionEventInfo.toString(mDelayedEventQueue) + 904 '}'; 905 } 906 toggleShortcutTriggered()907 void toggleShortcutTriggered() { 908 setShortcutTriggered(!mShortcutTriggered); 909 } 910 setShortcutTriggered(boolean state)911 void setShortcutTriggered(boolean state) { 912 if (mShortcutTriggered == state) { 913 return; 914 } 915 if (DEBUG_DETECTING) Slog.i(LOG_TAG, "setShortcutTriggered(" + state + ")"); 916 917 mShortcutTriggered = state; 918 mMagnificationController.setForceShowMagnifiableBounds(mDisplayId, state); 919 } 920 921 /** 922 * Detects if last action down is out of distance slop between with previous 923 * one, when triple tap is enabled. 924 * 925 * @return true if tap is out of distance slop 926 */ isTapOutOfDistanceSlop()927 boolean isTapOutOfDistanceSlop() { 928 if (!mDetectTripleTap) return false; 929 if (mPreLastDown == null || mLastDown == null) { 930 return false; 931 } 932 final boolean outOfDistanceSlop = 933 GestureUtils.distance(mPreLastDown, mLastDown) > mMultiTapMaxDistance; 934 if (tapCount() > 0) { 935 return outOfDistanceSlop; 936 } 937 // There's no tap in the queue here. We still need to check if this is the case that 938 // user tap screen quickly and out of distance slop. 939 if (outOfDistanceSlop 940 && !GestureUtils.isTimedOut(mPreLastDown, mLastDown, mMultiTapMaxDelay)) { 941 return true; 942 } 943 return false; 944 } 945 } 946 zoomOn(float centerX, float centerY)947 private void zoomOn(float centerX, float centerY) { 948 if (DEBUG_DETECTING) Slog.i(LOG_TAG, "zoomOn(" + centerX + ", " + centerY + ")"); 949 950 final float scale = MathUtils.constrain( 951 mMagnificationController.getPersistedScale(), 952 MIN_SCALE, MAX_SCALE); 953 mMagnificationController.setScaleAndCenter(mDisplayId, 954 scale, centerX, centerY, 955 /* animate */ true, 956 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 957 } 958 zoomOff()959 private void zoomOff() { 960 if (DEBUG_DETECTING) Slog.i(LOG_TAG, "zoomOff()"); 961 mMagnificationController.reset(mDisplayId, /* animate */ true); 962 } 963 recycleAndNullify(@ullable MotionEvent event)964 private static MotionEvent recycleAndNullify(@Nullable MotionEvent event) { 965 if (event != null) { 966 event.recycle(); 967 } 968 return null; 969 } 970 971 @Override toString()972 public String toString() { 973 return "MagnificationGesture{" + 974 "mDetectingState=" + mDetectingState + 975 ", mDelegatingState=" + mDelegatingState + 976 ", mMagnifiedInteractionState=" + mPanningScalingState + 977 ", mViewportDraggingState=" + mViewportDraggingState + 978 ", mDetectTripleTap=" + mDetectTripleTap + 979 ", mDetectShortcutTrigger=" + mDetectShortcutTrigger + 980 ", mCurrentState=" + State.nameOf(mCurrentState) + 981 ", mPreviousState=" + State.nameOf(mPreviousState) + 982 ", mMagnificationController=" + mMagnificationController + 983 ", mDisplayId=" + mDisplayId + 984 '}'; 985 } 986 987 private static final class MotionEventInfo { 988 989 private static final int MAX_POOL_SIZE = 10; 990 private static final Object sLock = new Object(); 991 private static MotionEventInfo sPool; 992 private static int sPoolSize; 993 994 private MotionEventInfo mNext; 995 private boolean mInPool; 996 997 public MotionEvent event; 998 public MotionEvent rawEvent; 999 public int policyFlags; 1000 obtain(MotionEvent event, MotionEvent rawEvent, int policyFlags)1001 public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent, 1002 int policyFlags) { 1003 synchronized (sLock) { 1004 MotionEventInfo info = obtainInternal(); 1005 info.initialize(event, rawEvent, policyFlags); 1006 return info; 1007 } 1008 } 1009 1010 @NonNull obtainInternal()1011 private static MotionEventInfo obtainInternal() { 1012 MotionEventInfo info; 1013 if (sPoolSize > 0) { 1014 sPoolSize--; 1015 info = sPool; 1016 sPool = info.mNext; 1017 info.mNext = null; 1018 info.mInPool = false; 1019 } else { 1020 info = new MotionEventInfo(); 1021 } 1022 return info; 1023 } 1024 initialize(MotionEvent event, MotionEvent rawEvent, int policyFlags)1025 private void initialize(MotionEvent event, MotionEvent rawEvent, 1026 int policyFlags) { 1027 this.event = MotionEvent.obtain(event); 1028 this.rawEvent = MotionEvent.obtain(rawEvent); 1029 this.policyFlags = policyFlags; 1030 } 1031 recycle()1032 public void recycle() { 1033 synchronized (sLock) { 1034 if (mInPool) { 1035 throw new IllegalStateException("Already recycled."); 1036 } 1037 clear(); 1038 if (sPoolSize < MAX_POOL_SIZE) { 1039 sPoolSize++; 1040 mNext = sPool; 1041 sPool = this; 1042 mInPool = true; 1043 } 1044 } 1045 } 1046 clear()1047 private void clear() { 1048 event = recycleAndNullify(event); 1049 rawEvent = recycleAndNullify(rawEvent); 1050 policyFlags = 0; 1051 } 1052 countOf(MotionEventInfo info, int eventType)1053 static int countOf(MotionEventInfo info, int eventType) { 1054 if (info == null) return 0; 1055 return (info.event.getAction() == eventType ? 1 : 0) 1056 + countOf(info.mNext, eventType); 1057 } 1058 toString(MotionEventInfo info)1059 public static String toString(MotionEventInfo info) { 1060 return info == null 1061 ? "" 1062 : MotionEvent.actionToString(info.event.getAction()).replace("ACTION_", "") 1063 + " " + MotionEventInfo.toString(info.mNext); 1064 } 1065 } 1066 1067 /** 1068 * BroadcastReceiver used to cancel the magnification shortcut when the screen turns off 1069 */ 1070 private static class ScreenStateReceiver extends BroadcastReceiver { 1071 private final Context mContext; 1072 private final MagnificationGestureHandler mGestureHandler; 1073 ScreenStateReceiver(Context context, MagnificationGestureHandler gestureHandler)1074 public ScreenStateReceiver(Context context, MagnificationGestureHandler gestureHandler) { 1075 mContext = context; 1076 mGestureHandler = gestureHandler; 1077 } 1078 register()1079 public void register() { 1080 mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF)); 1081 } 1082 unregister()1083 public void unregister() { 1084 mContext.unregisterReceiver(this); 1085 } 1086 1087 @Override onReceive(Context context, Intent intent)1088 public void onReceive(Context context, Intent intent) { 1089 mGestureHandler.mDetectingState.setShortcutTriggered(false); 1090 } 1091 } 1092 } 1093