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