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.server.accessibility.gestures.GestureUtils.distance; 28 import static com.android.server.accessibility.gestures.GestureUtils.distanceClosestPointerToPoint; 29 30 import static java.lang.Math.abs; 31 import static java.util.Arrays.asList; 32 import static java.util.Arrays.copyOfRange; 33 34 import android.accessibilityservice.MagnificationConfig; 35 import android.annotation.IntDef; 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.content.pm.PackageManager; 44 import android.graphics.PointF; 45 import android.graphics.Rect; 46 import android.graphics.Region; 47 import android.os.Handler; 48 import android.os.Looper; 49 import android.os.Message; 50 import android.os.SystemClock; 51 import android.os.VibrationEffect; 52 import android.os.Vibrator; 53 import android.provider.Settings; 54 import android.util.Log; 55 import android.util.MathUtils; 56 import android.util.Slog; 57 import android.util.TypedValue; 58 import android.view.GestureDetector; 59 import android.view.GestureDetector.SimpleOnGestureListener; 60 import android.view.MotionEvent; 61 import android.view.MotionEvent.PointerCoords; 62 import android.view.MotionEvent.PointerProperties; 63 import android.view.ScaleGestureDetector; 64 import android.view.ScaleGestureDetector.OnScaleGestureListener; 65 import android.view.VelocityTracker; 66 import android.view.ViewConfiguration; 67 68 import com.android.internal.R; 69 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils; 70 import com.android.internal.annotations.VisibleForTesting; 71 import com.android.server.accessibility.AccessibilityManagerService; 72 import com.android.server.accessibility.AccessibilityTraceManager; 73 import com.android.server.accessibility.Flags; 74 import com.android.server.accessibility.gestures.GestureUtils; 75 76 /** 77 * This class handles full screen magnification in response to touch events. 78 * 79 * The behavior is as follows: 80 * 81 * 1. Triple tap toggles permanent screen magnification which is magnifying 82 * the area around the location of the triple tap. One can think of the 83 * location of the triple tap as the center of the magnified viewport. 84 * For example, a triple tap when not magnified would magnify the screen 85 * and leave it in a magnified state. A triple tapping when magnified would 86 * clear magnification and leave the screen in a not magnified state. 87 * 88 * 2. Triple tap and hold would magnify the screen if not magnified and enable 89 * viewport dragging mode until the finger goes up. One can think of this 90 * mode as a way to move the magnified viewport since the area around the 91 * moving finger will be magnified to fit the screen. For example, if the 92 * screen was not magnified and the user triple taps and holds the screen 93 * would magnify and the viewport will follow the user's finger. When the 94 * finger goes up the screen will zoom out. If the same user interaction 95 * is performed when the screen is magnified, the viewport movement will 96 * be the same but when the finger goes up the screen will stay magnified. 97 * In other words, the initial magnified state is sticky. 98 * 99 * 3. Magnification can optionally be "triggered" by some external shortcut 100 * affordance. When this occurs via {@link #notifyShortcutTriggered()} a 101 * subsequent tap in a magnifiable region will engage permanent screen 102 * magnification as described in #1. Alternatively, a subsequent long-press 103 * or drag will engage magnification with viewport dragging as described in 104 * #2. Once magnified, all following behaviors apply whether magnification 105 * was engaged via a triple-tap or by a triggered shortcut. 106 * 107 * 4. Pinching with any number of additional fingers when viewport dragging 108 * is enabled, i.e. the user triple tapped and holds, would adjust the 109 * magnification scale which will become the current default magnification 110 * scale. The next time the user magnifies the same magnification scale 111 * would be used. 112 * 113 * 5. When in a permanent magnified state the user can use two or more fingers 114 * to pan the viewport. Note that in this mode the content is panned as 115 * opposed to the viewport dragging mode in which the viewport is moved. 116 * 117 * 6. When in a permanent magnified state the user can use two or more 118 * fingers to change the magnification scale which will become the current 119 * default magnification scale. The next time the user magnifies the same 120 * magnification scale would be used. 121 * 122 * 7. The magnification scale will be persisted in settings and in the cloud. 123 */ 124 @SuppressWarnings("WeakerAccess") 125 public class FullScreenMagnificationGestureHandler extends MagnificationGestureHandler { 126 127 private static final boolean DEBUG_STATE_TRANSITIONS = false | DEBUG_ALL; 128 private static final boolean DEBUG_DETECTING = false | DEBUG_ALL; 129 private static final boolean DEBUG_PANNING_SCALING = false | DEBUG_ALL; 130 131 // The MIN_SCALE is different from MagnificationScaleProvider.MIN_SCALE due 132 // to AccessibilityService.MagnificationController#setScale() has 133 // different scale range 134 private static final float MIN_SCALE = 1.0f; 135 private static final float MAX_SCALE = MagnificationScaleProvider.MAX_SCALE; 136 137 @VisibleForTesting final FullScreenMagnificationController mFullScreenMagnificationController; 138 139 private final FullScreenMagnificationController.MagnificationInfoChangedCallback 140 mMagnificationInfoChangedCallback; 141 @VisibleForTesting final DelegatingState mDelegatingState; 142 @VisibleForTesting final DetectingState mDetectingState; 143 @VisibleForTesting final PanningScalingState mPanningScalingState; 144 @VisibleForTesting final ViewportDraggingState mViewportDraggingState; 145 @VisibleForTesting final SinglePanningState mSinglePanningState; 146 147 private final ScreenStateReceiver mScreenStateReceiver; 148 private final WindowMagnificationPromptController mPromptController; 149 @NonNull private final MagnificationLogger mMagnificationLogger; 150 151 @VisibleForTesting State mCurrentState; 152 @VisibleForTesting State mPreviousState; 153 154 private PointerCoords[] mTempPointerCoords; 155 private PointerProperties[] mTempPointerProperties; 156 157 @VisibleForTesting static final int OVERSCROLL_NONE = 0; 158 @VisibleForTesting static final int OVERSCROLL_LEFT_EDGE = 1; 159 @VisibleForTesting static final int OVERSCROLL_RIGHT_EDGE = 2; 160 @VisibleForTesting static final int OVERSCROLL_VERTICAL_EDGE = 3; 161 162 @IntDef({ 163 OVERSCROLL_NONE, 164 OVERSCROLL_LEFT_EDGE, 165 OVERSCROLL_RIGHT_EDGE, 166 OVERSCROLL_VERTICAL_EDGE 167 }) 168 public @interface OverscrollState {} 169 170 @VisibleForTesting final OneFingerPanningSettingsProvider mOneFingerPanningSettingsProvider; 171 172 private final FullScreenMagnificationVibrationHelper mFullScreenMagnificationVibrationHelper; 173 174 @VisibleForTesting 175 @Nullable 176 final OverscrollHandler mOverscrollHandler; 177 178 private final float mOverscrollEdgeSlop; 179 180 private final boolean mIsWatch; 181 182 @Nullable private VelocityTracker mVelocityTracker; 183 private final int mMinimumVelocity; 184 private final int mMaximumVelocity; 185 FullScreenMagnificationGestureHandler(@iContext Context context, FullScreenMagnificationController fullScreenMagnificationController, AccessibilityTraceManager trace, Callback callback, boolean detectSingleFingerTripleTap, boolean detectTwoFingerTripleTap, boolean detectShortcutTrigger, @NonNull WindowMagnificationPromptController promptController, int displayId, FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper)186 public FullScreenMagnificationGestureHandler(@UiContext Context context, 187 FullScreenMagnificationController fullScreenMagnificationController, 188 AccessibilityTraceManager trace, 189 Callback callback, 190 boolean detectSingleFingerTripleTap, 191 boolean detectTwoFingerTripleTap, 192 boolean detectShortcutTrigger, 193 @NonNull WindowMagnificationPromptController promptController, 194 int displayId, 195 FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) { 196 this( 197 context, 198 fullScreenMagnificationController, 199 trace, 200 callback, 201 detectSingleFingerTripleTap, 202 detectTwoFingerTripleTap, 203 detectShortcutTrigger, 204 promptController, 205 displayId, 206 fullScreenMagnificationVibrationHelper, 207 /* magnificationLogger= */ null, 208 ViewConfiguration.get(context), 209 new OneFingerPanningSettingsProvider( 210 context, 211 Flags.enableMagnificationOneFingerPanningGesture() 212 )); 213 } 214 215 /** Constructor for tests. */ 216 @VisibleForTesting FullScreenMagnificationGestureHandler( @iContext Context context, FullScreenMagnificationController fullScreenMagnificationController, AccessibilityTraceManager trace, Callback callback, boolean detectSingleFingerTripleTap, boolean detectTwoFingerTripleTap, boolean detectShortcutTrigger, @NonNull WindowMagnificationPromptController promptController, int displayId, FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper, MagnificationLogger magnificationLogger, ViewConfiguration viewConfiguration, OneFingerPanningSettingsProvider oneFingerPanningSettingsProvider )217 FullScreenMagnificationGestureHandler( 218 @UiContext Context context, 219 FullScreenMagnificationController fullScreenMagnificationController, 220 AccessibilityTraceManager trace, 221 Callback callback, 222 boolean detectSingleFingerTripleTap, 223 boolean detectTwoFingerTripleTap, 224 boolean detectShortcutTrigger, 225 @NonNull WindowMagnificationPromptController promptController, 226 int displayId, 227 FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper, 228 MagnificationLogger magnificationLogger, 229 ViewConfiguration viewConfiguration, 230 OneFingerPanningSettingsProvider oneFingerPanningSettingsProvider 231 ) { 232 super(displayId, detectSingleFingerTripleTap, detectTwoFingerTripleTap, 233 detectShortcutTrigger, trace, callback); 234 if (DEBUG_ALL) { 235 Log.i(mLogTag, 236 "FullScreenMagnificationGestureHandler(detectSingleFingerTripleTap = " 237 + detectSingleFingerTripleTap 238 + ", detectTwoFingerTripleTap = " + detectTwoFingerTripleTap 239 + ", detectShortcutTrigger = " + detectShortcutTrigger + ")"); 240 } 241 242 if (Flags.fullscreenFlingGesture()) { 243 mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); 244 mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity(); 245 } else { 246 mMinimumVelocity = 0; 247 mMaximumVelocity = 0; 248 } 249 250 mFullScreenMagnificationController = fullScreenMagnificationController; 251 mMagnificationInfoChangedCallback = 252 new FullScreenMagnificationController.MagnificationInfoChangedCallback() { 253 @Override 254 public void onRequestMagnificationSpec(int displayId, int serviceId) { 255 return; 256 } 257 258 @Override 259 public void onFullScreenMagnificationActivationState(int displayId, 260 boolean activated) { 261 if (displayId != mDisplayId) { 262 return; 263 } 264 265 if (!activated) { 266 // cancel the magnification shortcut 267 mDetectingState.setShortcutTriggered(false); 268 } 269 } 270 271 @Override 272 public void onImeWindowVisibilityChanged(int displayId, boolean shown) { 273 return; 274 } 275 276 @Override 277 public void onFullScreenMagnificationChanged(int displayId, 278 @NonNull Region region, 279 @NonNull MagnificationConfig config) { 280 return; 281 } 282 }; 283 mFullScreenMagnificationController.addInfoChangedCallback( 284 mMagnificationInfoChangedCallback); 285 286 mPromptController = promptController; 287 288 if (magnificationLogger != null) { 289 mMagnificationLogger = magnificationLogger; 290 } else { 291 mMagnificationLogger = new MagnificationLogger() { 292 @Override 293 public void logMagnificationTripleTap(boolean enabled) { 294 AccessibilityStatsLogUtils.logMagnificationTripleTap(enabled); 295 } 296 297 @Override 298 public void logMagnificationTwoFingerTripleTap(boolean enabled) { 299 AccessibilityStatsLogUtils.logMagnificationTwoFingerTripleTap(enabled); 300 } 301 }; 302 } 303 304 mDelegatingState = new DelegatingState(); 305 mDetectingState = Flags.enableMagnificationMultipleFingerMultipleTapGesture() 306 ? new DetectingStateWithMultiFinger(context) 307 : new DetectingState(context); 308 mViewportDraggingState = Flags.enableMagnificationMultipleFingerMultipleTapGesture() 309 ? new ViewportDraggingStateWithMultiFinger() 310 : new ViewportDraggingState(); 311 mPanningScalingState = new PanningScalingState(context); 312 mSinglePanningState = new SinglePanningState(context); 313 mFullScreenMagnificationVibrationHelper = fullScreenMagnificationVibrationHelper; 314 mOneFingerPanningSettingsProvider = oneFingerPanningSettingsProvider; 315 boolean overscrollHandlerSupported = context.getResources().getBoolean( 316 R.bool.config_enable_a11y_fullscreen_magnification_overscroll_handler); 317 mOverscrollHandler = overscrollHandlerSupported ? new OverscrollHandler() : null; 318 mOverscrollEdgeSlop = context.getResources().getDimensionPixelSize( 319 R.dimen.accessibility_fullscreen_magnification_gesture_edge_slop); 320 mIsWatch = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); 321 322 if (mDetectShortcutTrigger) { 323 mScreenStateReceiver = new ScreenStateReceiver(context, this); 324 mScreenStateReceiver.register(); 325 } else { 326 mScreenStateReceiver = null; 327 } 328 329 transitionTo(mDetectingState); 330 } 331 332 @Override onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags)333 void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 334 if (event.getActionMasked() == ACTION_DOWN) { 335 cancelFling(); 336 } 337 338 handleEventWith(mCurrentState, event, rawEvent, policyFlags); 339 } 340 handleEventWith(State stateHandler, MotionEvent event, MotionEvent rawEvent, int policyFlags)341 private void handleEventWith(State stateHandler, 342 MotionEvent event, MotionEvent rawEvent, int policyFlags) { 343 // To keep InputEventConsistencyVerifiers within GestureDetectors happy 344 mPanningScalingState.mScrollGestureDetector.onTouchEvent(event); 345 mPanningScalingState.mScaleGestureDetector.onTouchEvent(event); 346 mSinglePanningState.mScrollGestureDetector.onTouchEvent(event); 347 348 try { 349 stateHandler.onMotionEvent(event, rawEvent, policyFlags); 350 } catch (GestureException e) { 351 Slog.e(mLogTag, "Error processing motion event", e); 352 clearAndTransitionToStateDetecting(); 353 } 354 } 355 356 @Override clearEvents(int inputSource)357 public void clearEvents(int inputSource) { 358 if (inputSource == SOURCE_TOUCHSCREEN) { 359 clearAndTransitionToStateDetecting(); 360 } 361 362 super.clearEvents(inputSource); 363 } 364 365 @Override onDestroy()366 public void onDestroy() { 367 if (DEBUG_STATE_TRANSITIONS) { 368 Slog.i(mLogTag, "onDestroy(); delayed = " 369 + MotionEventInfo.toString(mDetectingState.mDelayedEventQueue)); 370 } 371 mOneFingerPanningSettingsProvider.unregister(); 372 373 if (mScreenStateReceiver != null) { 374 mScreenStateReceiver.unregister(); 375 } 376 mPromptController.onDestroy(); 377 // Check if need to reset when MagnificationGestureHandler is the last magnifying service. 378 mFullScreenMagnificationController.resetIfNeeded( 379 mDisplayId, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 380 mFullScreenMagnificationController.removeInfoChangedCallback( 381 mMagnificationInfoChangedCallback); 382 clearAndTransitionToStateDetecting(); 383 } 384 385 @Override handleShortcutTriggered()386 public void handleShortcutTriggered() { 387 final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId); 388 389 if (isActivated) { 390 zoomOff(); 391 clearAndTransitionToStateDetecting(); 392 } else { 393 mDetectingState.toggleShortcutTriggered(); 394 } 395 396 if (mDetectingState.isShortcutTriggered()) { 397 mPromptController.showNotificationIfNeeded(); 398 zoomToScale(1.0f, Float.NaN, Float.NaN); 399 } 400 } 401 402 @Override getMode()403 public int getMode() { 404 return Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; 405 } 406 clearAndTransitionToStateDetecting()407 void clearAndTransitionToStateDetecting() { 408 mCurrentState = mDetectingState; 409 mDetectingState.clear(); 410 mViewportDraggingState.clear(); 411 mPanningScalingState.clear(); 412 } 413 getTempPointerCoordsWithMinSize(int size)414 private PointerCoords[] getTempPointerCoordsWithMinSize(int size) { 415 final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0; 416 if (oldSize < size) { 417 PointerCoords[] oldTempPointerCoords = mTempPointerCoords; 418 mTempPointerCoords = new PointerCoords[size]; 419 if (oldTempPointerCoords != null) { 420 System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize); 421 } 422 } 423 for (int i = oldSize; i < size; i++) { 424 mTempPointerCoords[i] = new PointerCoords(); 425 } 426 return mTempPointerCoords; 427 } 428 getTempPointerPropertiesWithMinSize(int size)429 private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) { 430 final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length 431 : 0; 432 if (oldSize < size) { 433 PointerProperties[] oldTempPointerProperties = mTempPointerProperties; 434 mTempPointerProperties = new PointerProperties[size]; 435 if (oldTempPointerProperties != null) { 436 System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, 437 oldSize); 438 } 439 } 440 for (int i = oldSize; i < size; i++) { 441 mTempPointerProperties[i] = new PointerProperties(); 442 } 443 return mTempPointerProperties; 444 } 445 446 @VisibleForTesting transitionTo(State state)447 void transitionTo(State state) { 448 if (DEBUG_STATE_TRANSITIONS) { 449 Slog.i(mLogTag, 450 (State.nameOf(mCurrentState) + " -> " + State.nameOf(state) 451 + " at " + asList(copyOfRange(new RuntimeException().getStackTrace(), 1, 5))) 452 .replace(getClass().getName(), "")); 453 } 454 mPreviousState = mCurrentState; 455 if (state == mPanningScalingState) { 456 mPanningScalingState.prepareForState(); 457 } 458 mCurrentState = state; 459 } 460 461 /** An interface that allows testing magnification log events. */ 462 interface MagnificationLogger { logMagnificationTripleTap(boolean enabled)463 void logMagnificationTripleTap(boolean enabled); logMagnificationTwoFingerTripleTap(boolean enabled)464 void logMagnificationTwoFingerTripleTap(boolean enabled); 465 } 466 467 interface State { onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)468 void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) 469 throws GestureException; 470 clear()471 default void clear() {} 472 name()473 default String name() { 474 return getClass().getSimpleName(); 475 } 476 nameOf(@ullable State s)477 static String nameOf(@Nullable State s) { 478 return s != null ? s.name() : "null"; 479 } 480 } 481 482 /** 483 * This class determines if the user is performing a scale or pan gesture. 484 * 485 * Unlike when {@link ViewportDraggingState dragging the viewport}, in panning mode the viewport 486 * moves in the same direction as the fingers, and allows to easily and precisely scale the 487 * magnification level. 488 * This makes it the preferred mode for one-off adjustments, due to its precision and ease of 489 * triggering. 490 */ 491 final class PanningScalingState extends SimpleOnGestureListener 492 implements OnScaleGestureListener, State { 493 494 private final Context mContext; 495 private final ScaleGestureDetector mScaleGestureDetector; 496 private final GestureDetector mScrollGestureDetector; 497 final float mScalingThreshold; 498 499 float mInitialScaleFactor = -1; 500 @VisibleForTesting boolean mScaling; 501 502 /** 503 * Whether it needs to detect the target scale passes 504 * {@link FullScreenMagnificationController#getPersistedScale} during panning scale. 505 */ 506 @VisibleForTesting boolean mDetectingPassPersistedScale; 507 508 // The threshold for relative difference from given scale to persisted scale. If the 509 // difference >= threshold, we can start detecting if the scale passes the persisted 510 // scale during panning. 511 @VisibleForTesting static final float CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD = 0.2f; 512 // The threshold for relative difference from given scale to persisted scale. If the 513 // difference < threshold, we can decide that the scale passes the persisted scale. 514 @VisibleForTesting static final float PASSING_PERSISTED_SCALE_THRESHOLD = 0.01f; 515 PanningScalingState(Context context)516 PanningScalingState(Context context) { 517 final TypedValue scaleValue = new TypedValue(); 518 context.getResources().getValue( 519 R.dimen.config_screen_magnification_scaling_threshold, 520 scaleValue, false); 521 mContext = context; 522 mScalingThreshold = scaleValue.getFloat(); 523 mScaleGestureDetector = new ScaleGestureDetector(context, this, Handler.getMain()); 524 mScaleGestureDetector.setQuickScaleEnabled(false); 525 mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain()); 526 } 527 528 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)529 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 530 int action = event.getActionMasked(); 531 if (action == ACTION_POINTER_UP 532 && event.getPointerCount() == 2 // includes the pointer currently being released 533 && mPreviousState == mViewportDraggingState) { 534 if (mOverscrollHandler != null) { 535 mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded(); 536 mOverscrollHandler.clearEdgeState(); 537 } 538 persistScaleAndTransitionTo(mViewportDraggingState); 539 } else if (action == ACTION_UP || action == ACTION_CANCEL) { 540 onPanningFinished(event); 541 if (mOverscrollHandler != null) { 542 mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded(); 543 mOverscrollHandler.clearEdgeState(); 544 } 545 persistScaleAndTransitionTo(mDetectingState); 546 } 547 } 548 prepareForState()549 void prepareForState() { 550 checkShouldDetectPassPersistedScale(); 551 } 552 checkShouldDetectPassPersistedScale()553 private void checkShouldDetectPassPersistedScale() { 554 if (mDetectingPassPersistedScale) { 555 return; 556 } 557 558 final float currentScale = 559 mFullScreenMagnificationController.getScale(mDisplayId); 560 final float persistedScale = 561 mFullScreenMagnificationController.getPersistedScale(mDisplayId); 562 563 mDetectingPassPersistedScale = 564 (abs(currentScale - persistedScale) / persistedScale) 565 >= CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD; 566 } 567 persistScaleAndTransitionTo(State state)568 public void persistScaleAndTransitionTo(State state) { 569 // If device is a watch don't change user settings scale. On watches, warp effect 570 // is enabled and the current display scale could be differ from the default user 571 // settings scale (should not change the scale due to the warp effect) 572 if (!mIsWatch) { 573 mFullScreenMagnificationController.persistScale(mDisplayId); 574 } 575 clear(); 576 transitionTo(state); 577 } 578 579 @VisibleForTesting setScaleAndClearIfNeeded(float scale, float pivotX, float pivotY)580 void setScaleAndClearIfNeeded(float scale, float pivotX, float pivotY) { 581 if (mDetectingPassPersistedScale) { 582 final float persistedScale = 583 mFullScreenMagnificationController.getPersistedScale(mDisplayId); 584 // If the scale passes the persisted scale during panning, perform a vibration 585 // feedback to user. Also, call {@link clear} to create a buffer zone so that 586 // user needs to panning more than {@link mScalingThreshold} to change scale again. 587 if (abs(scale - persistedScale) / persistedScale 588 < PASSING_PERSISTED_SCALE_THRESHOLD) { 589 scale = persistedScale; 590 final Vibrator vibrator = mContext.getSystemService(Vibrator.class); 591 if (vibrator != null) { 592 vibrator.vibrate( 593 VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)); 594 } 595 clear(); 596 } 597 } 598 599 if (DEBUG_PANNING_SCALING) Slog.i(mLogTag, "Scaled content to: " + scale + "x"); 600 mFullScreenMagnificationController.setScale(mDisplayId, scale, pivotX, pivotY, false, 601 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 602 603 checkShouldDetectPassPersistedScale(); 604 } 605 606 @Override onScroll(MotionEvent first, MotionEvent second, float distanceX, float distanceY)607 public boolean onScroll(MotionEvent first, MotionEvent second, 608 float distanceX, float distanceY) { 609 if (mCurrentState != mPanningScalingState) { 610 return true; 611 } 612 if (DEBUG_PANNING_SCALING) { 613 Slog.i(mLogTag, "Panned content by scrollX: " + distanceX 614 + " scrollY: " + distanceY); 615 } 616 onPan(second); 617 mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX, 618 distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 619 if (mOverscrollHandler != null) { 620 mOverscrollHandler.onScrollStateChanged(first, second); 621 } 622 return /* event consumed: */ true; 623 } 624 625 @Override onScale(ScaleGestureDetector detector)626 public boolean onScale(ScaleGestureDetector detector) { 627 if (!mScaling) { 628 if (mInitialScaleFactor < 0) { 629 mInitialScaleFactor = detector.getScaleFactor(); 630 return false; 631 } 632 final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor; 633 mScaling = abs(deltaScale) > mScalingThreshold; 634 return mScaling; 635 } 636 final float initialScale = mFullScreenMagnificationController.getScale(mDisplayId); 637 final float targetScale = initialScale * detector.getScaleFactor(); 638 639 // Don't allow a gesture to move the user further outside the 640 // desired bounds for gesture-controlled scaling. 641 final float scale; 642 if (targetScale > MAX_SCALE && targetScale > initialScale) { 643 // The target scale is too big and getting bigger. 644 scale = MAX_SCALE; 645 } else if (targetScale < MIN_SCALE && targetScale < initialScale) { 646 // The target scale is too small and getting smaller. 647 scale = MIN_SCALE; 648 } else { 649 // The target scale may be outside our bounds, but at least 650 // it's moving in the right direction. This avoids a "jump" if 651 // we're at odds with some other service's desired bounds. 652 scale = targetScale; 653 } 654 655 setScaleAndClearIfNeeded(scale, detector.getFocusX(), detector.getFocusY()); 656 return /* handled: */ true; 657 } 658 659 @Override onScaleBegin(ScaleGestureDetector detector)660 public boolean onScaleBegin(ScaleGestureDetector detector) { 661 return /* continue recognizing: */ (mCurrentState == mPanningScalingState); 662 } 663 664 @Override onScaleEnd(ScaleGestureDetector detector)665 public void onScaleEnd(ScaleGestureDetector detector) { 666 clear(); 667 } 668 669 @Override clear()670 public void clear() { 671 mInitialScaleFactor = -1; 672 mScaling = false; 673 mDetectingPassPersistedScale = false; 674 } 675 676 @Override toString()677 public String toString() { 678 return "PanningScalingState{" + "mInitialScaleFactor=" + mInitialScaleFactor 679 + ", mScaling=" + mScaling 680 + '}'; 681 } 682 } 683 684 final class ViewportDraggingStateWithMultiFinger extends ViewportDraggingState { 685 // LINT.IfChange(viewport_dragging_state_with_multi_finger) 686 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)687 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) 688 throws GestureException { 689 final int action = event.getActionMasked(); 690 switch (action) { 691 case ACTION_POINTER_DOWN: { 692 clearAndTransitToPanningScalingState(); 693 } 694 break; 695 case ACTION_MOVE: { 696 if (event.getPointerCount() > 2) { 697 throw new GestureException("Should have one pointer down."); 698 } 699 final float eventX = event.getX(); 700 final float eventY = event.getY(); 701 if (mFullScreenMagnificationController.magnificationRegionContains( 702 mDisplayId, eventX, eventY)) { 703 mFullScreenMagnificationController.setCenter(mDisplayId, eventX, eventY, 704 /* animate */ mLastMoveOutsideMagnifiedRegion, 705 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 706 mLastMoveOutsideMagnifiedRegion = false; 707 } else { 708 mLastMoveOutsideMagnifiedRegion = true; 709 } 710 } 711 break; 712 713 case ACTION_UP: 714 case ACTION_CANCEL: { 715 // If mScaleToRecoverAfterDraggingEnd >= 1.0, the dragging state is triggered 716 // by zoom in temporary, and the magnifier needs to recover to original scale 717 // after exiting dragging state. 718 // Otherwise, the magnifier should be disabled. 719 if (mScaleToRecoverAfterDraggingEnd >= 1.0f) { 720 zoomToScale(mScaleToRecoverAfterDraggingEnd, event.getX(), 721 event.getY()); 722 } else { 723 zoomOff(); 724 } 725 clear(); 726 mScaleToRecoverAfterDraggingEnd = Float.NaN; 727 transitionTo(mDetectingState); 728 } 729 break; 730 731 case ACTION_DOWN: { 732 throw new GestureException( 733 "Unexpected event type: " + MotionEvent.actionToString(action)); 734 } 735 } 736 } 737 // LINT.ThenChange(:viewport_dragging_state) 738 } 739 740 /** 741 * This class handles motion events when the event dispatcher has 742 * determined that the user is performing a single-finger drag of the 743 * magnification viewport. 744 * 745 * Unlike when {@link PanningScalingState panning}, the viewport moves in the opposite direction 746 * of the finger, and any part of the screen is reachable without lifting the finger. 747 * This makes it the preferable mode for tasks like reading text spanning full screen width. 748 */ 749 class ViewportDraggingState implements State { 750 751 /** 752 * The cached scale for recovering after dragging ends. 753 * If the scale >= 1.0, the magnifier needs to recover to scale. 754 * Otherwise, the magnifier should be disabled. 755 */ 756 @VisibleForTesting protected float mScaleToRecoverAfterDraggingEnd = Float.NaN; 757 758 protected boolean mLastMoveOutsideMagnifiedRegion; 759 760 // LINT.IfChange(viewport_dragging_state) 761 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)762 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) 763 throws GestureException { 764 final int action = event.getActionMasked(); 765 switch (action) { 766 case ACTION_POINTER_DOWN: { 767 clearAndTransitToPanningScalingState(); 768 } 769 break; 770 case ACTION_MOVE: { 771 if (event.getPointerCount() != 1) { 772 throw new GestureException("Should have one pointer down."); 773 } 774 final float eventX = event.getX(); 775 final float eventY = event.getY(); 776 if (mFullScreenMagnificationController.magnificationRegionContains( 777 mDisplayId, eventX, eventY)) { 778 mFullScreenMagnificationController.setCenter(mDisplayId, eventX, eventY, 779 /* animate */ mLastMoveOutsideMagnifiedRegion, 780 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 781 mLastMoveOutsideMagnifiedRegion = false; 782 } else { 783 mLastMoveOutsideMagnifiedRegion = true; 784 } 785 } 786 break; 787 788 case ACTION_UP: 789 case ACTION_CANCEL: { 790 // If mScaleToRecoverAfterDraggingEnd >= 1.0, the dragging state is triggered 791 // by zoom in temporary, and the magnifier needs to recover to original scale 792 // after exiting dragging state. 793 // Otherwise, the magnifier should be disabled. 794 if (mScaleToRecoverAfterDraggingEnd >= 1.0f) { 795 zoomToScale(mScaleToRecoverAfterDraggingEnd, event.getX(), 796 event.getY()); 797 } else { 798 zoomOff(); 799 } 800 clear(); 801 mScaleToRecoverAfterDraggingEnd = Float.NaN; 802 transitionTo(mDetectingState); 803 } 804 break; 805 806 case ACTION_DOWN: 807 case ACTION_POINTER_UP: { 808 throw new GestureException( 809 "Unexpected event type: " + MotionEvent.actionToString(action)); 810 } 811 } 812 } 813 // LINT.ThenChange(:viewport_dragging_state_with_multi_finger) 814 isAlwaysOnMagnificationEnabled()815 private boolean isAlwaysOnMagnificationEnabled() { 816 return mFullScreenMagnificationController.isAlwaysOnMagnificationEnabled(); 817 } 818 prepareForZoomInTemporary(boolean shortcutTriggered)819 public void prepareForZoomInTemporary(boolean shortcutTriggered) { 820 boolean shouldRecoverAfterDraggingEnd; 821 if (mFullScreenMagnificationController.isActivated(mDisplayId)) { 822 // For b/267210808, if always-on feature is not enabled, we keep the expected 823 // behavior. If users tap shortcut and then tap-and-hold to zoom in temporary, 824 // the magnifier should be disabled after release. 825 // If always-on feature is enabled, in the same scenario the magnifier would 826 // zoom to 1.0 and keep activated. 827 if (shortcutTriggered) { 828 shouldRecoverAfterDraggingEnd = isAlwaysOnMagnificationEnabled(); 829 } else { 830 shouldRecoverAfterDraggingEnd = true; 831 } 832 } else { 833 shouldRecoverAfterDraggingEnd = false; 834 } 835 836 mScaleToRecoverAfterDraggingEnd = shouldRecoverAfterDraggingEnd 837 ? mFullScreenMagnificationController.getScale(mDisplayId) : Float.NaN; 838 } 839 clearAndTransitToPanningScalingState()840 protected void clearAndTransitToPanningScalingState() { 841 final float scaleToRecovery = mScaleToRecoverAfterDraggingEnd; 842 clear(); 843 mScaleToRecoverAfterDraggingEnd = scaleToRecovery; 844 transitionTo(mPanningScalingState); 845 } 846 847 @Override clear()848 public void clear() { 849 mLastMoveOutsideMagnifiedRegion = false; 850 851 mScaleToRecoverAfterDraggingEnd = Float.NaN; 852 } 853 854 @Override toString()855 public String toString() { 856 return "ViewportDraggingState{" 857 + "mScaleToRecoverAfterDraggingEnd=" + mScaleToRecoverAfterDraggingEnd 858 + ", mLastMoveOutsideMagnifiedRegion=" + mLastMoveOutsideMagnifiedRegion 859 + '}'; 860 } 861 } 862 863 final class DelegatingState implements State { 864 /** 865 * Time of last {@link MotionEvent#ACTION_DOWN} while in {@link DelegatingState} 866 */ 867 public long mLastDelegatedDownEventTime; 868 869 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)870 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 871 // Ensures that the state at the end of delegation is consistent with the last delegated 872 // UP/DOWN event in queue: still delegating if pointer is down, detecting otherwise 873 switch (event.getActionMasked()) { 874 case ACTION_UP: 875 case ACTION_CANCEL: { 876 transitionTo(mDetectingState); 877 } 878 break; 879 880 case ACTION_DOWN: { 881 transitionTo(mDelegatingState); 882 mLastDelegatedDownEventTime = event.getDownTime(); 883 } break; 884 } 885 886 if (getNext() != null) { 887 // We cache some events to see if the user wants to trigger magnification. 888 // If no magnification is triggered we inject these events with adjusted 889 // time and down time to prevent subsequent transformations being confused 890 // by stale events. After the cached events, which always have a down, are 891 // injected we need to also update the down time of all subsequent non cached 892 // events. All delegated events cached and non-cached are delivered here. 893 event.setDownTime(mLastDelegatedDownEventTime); 894 dispatchTransformedEvent(event, rawEvent, policyFlags); 895 } 896 } 897 } 898 899 final class DetectingStateWithMultiFinger extends DetectingState { 900 private static final int TWO_FINGER_GESTURE_MAX_TAPS = 2; 901 // A flag set to true when two fingers have touched down. 902 // Used to indicate what next finger action should be. 903 private boolean mIsTwoFingerCountReached = false; 904 // A tap counts when two fingers are down and up once. 905 private int mCompletedTapCount = 0; DetectingStateWithMultiFinger(Context context)906 DetectingStateWithMultiFinger(Context context) { 907 super(context); 908 } 909 910 // LINT.IfChange(detecting_state_with_multi_finger) 911 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)912 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 913 cacheDelayedMotionEvent(event, rawEvent, policyFlags); 914 switch (event.getActionMasked()) { 915 case MotionEvent.ACTION_DOWN: { 916 mLastDetectingDownEventTime = event.getDownTime(); 917 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 918 919 mFirstPointerDownLocation.set(event.getX(), event.getY()); 920 921 if (!mFullScreenMagnificationController.magnificationRegionContains( 922 mDisplayId, event.getX(), event.getY())) { 923 924 transitionToDelegatingStateAndClear(); 925 926 } else if (isMultiTapTriggered(2 /* taps */)) { 927 928 // 3tap and hold 929 afterLongTapTimeoutTransitionToDraggingState(event); 930 931 } else if (isTapOutOfDistanceSlop()) { 932 933 transitionToDelegatingStateAndClear(); 934 935 } else if (mDetectSingleFingerTripleTap 936 || mDetectTwoFingerTripleTap 937 // If activated, delay an ACTION_DOWN for mMultiTapMaxDelay 938 // to ensure reachability of 939 // STATE_PANNING_SCALING(triggerable with ACTION_POINTER_DOWN) 940 || isActivated()) { 941 942 afterMultiTapTimeoutTransitionToDelegatingState(); 943 944 } else { 945 946 // Delegate pending events without delay 947 transitionToDelegatingStateAndClear(); 948 } 949 } 950 break; 951 case ACTION_POINTER_DOWN: { 952 mIsTwoFingerCountReached = mDetectTwoFingerTripleTap 953 && event.getPointerCount() == 2; 954 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 955 956 if (event.getPointerCount() == 2) { 957 if (isMultiFingerMultiTapTriggered( 958 TWO_FINGER_GESTURE_MAX_TAPS - 1, event)) { 959 // 3tap and hold 960 afterLongTapTimeoutTransitionToDraggingState(event); 961 } else { 962 if (mDetectTwoFingerTripleTap) { 963 // If mDetectTwoFingerTripleTap, delay transition to the delegating 964 // state for mMultiTapMaxDelay to ensure reachability of 965 // multi finger multi tap 966 afterMultiTapTimeoutTransitionToDelegatingState(); 967 } 968 969 if (isActivated()) { 970 // If activated, delay transition to the panning scaling 971 // state for tap timeout to ensure reachability of 972 // multi finger multi tap 973 storePointerDownLocation(mSecondPointerDownLocation, event); 974 mHandler.sendEmptyMessageDelayed( 975 MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE, 976 ViewConfiguration.getTapTimeout()); 977 } 978 } 979 } else { 980 transitionToDelegatingStateAndClear(); 981 } 982 } 983 break; 984 case ACTION_POINTER_UP: { 985 // If it is a two-finger gesture, do not transition to the delegating state 986 // to ensure the reachability of 987 // the two-finger triple tap (triggerable with ACTION_MOVE and ACTION_UP) 988 if (!mIsTwoFingerCountReached) { 989 transitionToDelegatingStateAndClear(); 990 } 991 } 992 break; 993 case ACTION_MOVE: { 994 if (isFingerDown() 995 && distance(mLastDown, /* move */ event) > mSwipeMinDistance) { 996 // Swipe detected - transition immediately 997 998 // For convenience, viewport dragging takes precedence 999 // over insta-delegating on 3tap&swipe 1000 // (which is a rare combo to be used aside from magnification) 1001 if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) { 1002 transitionToViewportDraggingStateAndClear(event); 1003 } else if (isMultiFingerMultiTapTriggered( 1004 TWO_FINGER_GESTURE_MAX_TAPS - 1, event) 1005 && event.getPointerCount() == 2) { 1006 transitionToViewportDraggingStateAndClear(event); 1007 } else if (isActivated() && event.getPointerCount() == 2) { 1008 if (mOverscrollHandler != null 1009 && overscrollState(event, mFirstPointerDownLocation) 1010 == OVERSCROLL_VERTICAL_EDGE) { 1011 transitionToDelegatingStateAndClear(); 1012 } else { 1013 //Primary pointer is swiping, so transit to PanningScalingState 1014 transitToPanningScalingStateAndClear(); 1015 } 1016 } else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled() 1017 && isActivated() 1018 && event.getPointerCount() == 1) { 1019 if (mOverscrollHandler != null 1020 && overscrollState(event, mFirstPointerDownLocation) 1021 == OVERSCROLL_VERTICAL_EDGE) { 1022 transitionToDelegatingStateAndClear(); 1023 } else if (overscrollState(event, mFirstPointerDownLocation) 1024 != OVERSCROLL_NONE) { 1025 transitionToDelegatingStateAndClear(); 1026 } else { 1027 transitToSinglePanningStateAndClear(); 1028 } 1029 } else if (!mIsTwoFingerCountReached) { 1030 // If it is a two-finger gesture, do not transition to the 1031 // delegating state to ensure the reachability of 1032 // the two-finger triple tap (triggerable with ACTION_UP) 1033 transitionToDelegatingStateAndClear(); 1034 } 1035 } else if (isActivated() && pointerDownValid(mSecondPointerDownLocation) 1036 && distanceClosestPointerToPoint( 1037 mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) { 1038 // Second pointer is swiping, so transit to PanningScalingState 1039 // Delay an ACTION_MOVE for tap timeout to ensure it is not trigger from 1040 // multi finger multi tap 1041 storePointerDownLocation(mSecondPointerDownLocation, event); 1042 mHandler.sendEmptyMessageDelayed( 1043 MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE, 1044 ViewConfiguration.getTapTimeout()); 1045 } 1046 } 1047 break; 1048 case ACTION_UP: { 1049 1050 mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD); 1051 mHandler.removeMessages(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE); 1052 1053 if (!mFullScreenMagnificationController.magnificationRegionContains( 1054 mDisplayId, event.getX(), event.getY())) { 1055 transitionToDelegatingStateAndClear(); 1056 1057 } else if (isMultiFingerMultiTapTriggered(TWO_FINGER_GESTURE_MAX_TAPS, event)) { 1058 // Placing multiple fingers before a single finger, because achieving a 1059 // multi finger multi tap also means achieving a single finger triple tap 1060 onTripleTap(event); 1061 1062 } else if (isMultiTapTriggered(3 /* taps */)) { 1063 onTripleTap(/* up */ event); 1064 1065 } else if ( 1066 // Possible to be false on: 3tap&drag -> scale -> PTR_UP -> UP 1067 isFingerDown() 1068 //TODO long tap should never happen here 1069 && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay) 1070 || (distance(mLastDown, mLastUp) >= mSwipeMinDistance)) 1071 // If it is a two-finger but not reach 3 tap, do not transition to the 1072 // delegating state to ensure the reachability of the triple tap 1073 && mCompletedTapCount == 0) { 1074 transitionToDelegatingStateAndClear(); 1075 1076 } 1077 } 1078 break; 1079 } 1080 } 1081 // LINT.ThenChange(:detecting_state) 1082 1083 @Override clear()1084 public void clear() { 1085 mCompletedTapCount = 0; 1086 setShortcutTriggered(false); 1087 removePendingDelayedMessages(); 1088 clearDelayedMotionEvents(); 1089 mFirstPointerDownLocation.set(Float.NaN, Float.NaN); 1090 mSecondPointerDownLocation.set(Float.NaN, Float.NaN); 1091 } 1092 isMultiFingerMultiTapTriggered(int targetTapCount, MotionEvent event)1093 private boolean isMultiFingerMultiTapTriggered(int targetTapCount, MotionEvent event) { 1094 if (event.getActionMasked() == ACTION_UP && mIsTwoFingerCountReached) { 1095 mCompletedTapCount++; 1096 mIsTwoFingerCountReached = false; 1097 } 1098 1099 if (mDetectTwoFingerTripleTap && mCompletedTapCount > TWO_FINGER_GESTURE_MAX_TAPS - 1) { 1100 final boolean enabled = !isActivated(); 1101 mMagnificationLogger.logMagnificationTwoFingerTripleTap(enabled); 1102 } 1103 return mDetectTwoFingerTripleTap && mCompletedTapCount == targetTapCount; 1104 } 1105 transitionToDelegatingStateAndClear()1106 void transitionToDelegatingStateAndClear() { 1107 mCompletedTapCount = 0; 1108 transitionTo(mDelegatingState); 1109 sendDelayedMotionEvents(); 1110 removePendingDelayedMessages(); 1111 mFirstPointerDownLocation.set(Float.NaN, Float.NaN); 1112 mSecondPointerDownLocation.set(Float.NaN, Float.NaN); 1113 } 1114 transitionToViewportDraggingStateAndClear(MotionEvent down)1115 void transitionToViewportDraggingStateAndClear(MotionEvent down) { 1116 1117 if (DEBUG_DETECTING) Slog.i(mLogTag, "onTripleTapAndHold()"); 1118 final boolean shortcutTriggered = mShortcutTriggered; 1119 1120 // Only log the 3tap and hold event 1121 if (!shortcutTriggered) { 1122 final boolean enabled = !isActivated(); 1123 if (mCompletedTapCount == TWO_FINGER_GESTURE_MAX_TAPS - 1) { 1124 // Two finger triple tap and hold 1125 mMagnificationLogger.logMagnificationTwoFingerTripleTap(enabled); 1126 } else { 1127 // Triple tap and hold also belongs to triple tap event 1128 mMagnificationLogger.logMagnificationTripleTap(enabled); 1129 } 1130 } 1131 clear(); 1132 1133 mViewportDraggingState.prepareForZoomInTemporary(shortcutTriggered); 1134 zoomInTemporary(down.getX(), down.getY(), shortcutTriggered); 1135 transitionTo(mViewportDraggingState); 1136 } 1137 } 1138 1139 /** 1140 * This class handles motion events when the event dispatch has not yet 1141 * determined what the user is doing. It watches for various tap events. 1142 */ 1143 class DetectingState implements State, Handler.Callback { 1144 1145 protected static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1; 1146 protected static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; 1147 protected static final int MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE = 3; 1148 1149 final int mLongTapMinDelay; 1150 final int mSwipeMinDistance; 1151 final int mMultiTapMaxDelay; 1152 final int mMultiTapMaxDistance; 1153 1154 protected MotionEventInfo mDelayedEventQueue; 1155 protected MotionEvent mLastDown; 1156 protected MotionEvent mPreLastDown; 1157 protected MotionEvent mLastUp; 1158 protected MotionEvent mPreLastUp; 1159 1160 protected PointF mFirstPointerDownLocation = new PointF(Float.NaN, Float.NaN); 1161 protected PointF mSecondPointerDownLocation = new PointF(Float.NaN, Float.NaN); 1162 protected long mLastDetectingDownEventTime; 1163 1164 @VisibleForTesting boolean mShortcutTriggered; 1165 1166 @VisibleForTesting Handler mHandler = new Handler(Looper.getMainLooper(), this); 1167 DetectingState(Context context)1168 DetectingState(Context context) { 1169 mLongTapMinDelay = ViewConfiguration.getLongPressTimeout(); 1170 mMultiTapMaxDelay = ViewConfiguration.getDoubleTapTimeout() 1171 + context.getResources().getInteger( 1172 R.integer.config_screen_magnification_multi_tap_adjustment); 1173 mSwipeMinDistance = ViewConfiguration.get(context).getScaledTouchSlop(); 1174 mMultiTapMaxDistance = ViewConfiguration.get(context).getScaledDoubleTapSlop(); 1175 } 1176 1177 @Override handleMessage(Message message)1178 public boolean handleMessage(Message message) { 1179 final int type = message.what; 1180 switch (type) { 1181 case MESSAGE_ON_TRIPLE_TAP_AND_HOLD: { 1182 MotionEvent down = (MotionEvent) message.obj; 1183 transitionToViewportDraggingStateAndClear(down); 1184 down.recycle(); 1185 } 1186 break; 1187 case MESSAGE_TRANSITION_TO_DELEGATING_STATE: { 1188 transitionToDelegatingStateAndClear(); 1189 } 1190 break; 1191 case MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE: { 1192 transitToPanningScalingStateAndClear(); 1193 } 1194 break; 1195 default: { 1196 throw new IllegalArgumentException("Unknown message type: " + type); 1197 } 1198 } 1199 return true; 1200 } 1201 1202 // LINT.IfChange(detecting_state) 1203 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)1204 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 1205 cacheDelayedMotionEvent(event, rawEvent, policyFlags); 1206 switch (event.getActionMasked()) { 1207 case MotionEvent.ACTION_DOWN: { 1208 mLastDetectingDownEventTime = event.getDownTime(); 1209 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 1210 1211 mFirstPointerDownLocation.set(event.getX(), event.getY()); 1212 1213 if (!mFullScreenMagnificationController.magnificationRegionContains( 1214 mDisplayId, event.getX(), event.getY())) { 1215 1216 transitionToDelegatingStateAndClear(); 1217 1218 } else if (isMultiTapTriggered(2 /* taps */)) { 1219 1220 // 3tap and hold 1221 afterLongTapTimeoutTransitionToDraggingState(event); 1222 1223 } else if (isTapOutOfDistanceSlop()) { 1224 1225 transitionToDelegatingStateAndClear(); 1226 1227 } else if (mDetectSingleFingerTripleTap 1228 // If activated, delay an ACTION_DOWN for mMultiTapMaxDelay 1229 // to ensure reachability of 1230 // STATE_PANNING_SCALING(triggerable with ACTION_POINTER_DOWN) 1231 || isActivated()) { 1232 1233 afterMultiTapTimeoutTransitionToDelegatingState(); 1234 1235 } else { 1236 1237 // Delegate pending events without delay 1238 transitionToDelegatingStateAndClear(); 1239 } 1240 } 1241 break; 1242 case ACTION_POINTER_DOWN: { 1243 if (isActivated() && event.getPointerCount() == 2) { 1244 storePointerDownLocation(mSecondPointerDownLocation, event); 1245 mHandler.sendEmptyMessageDelayed(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE, 1246 ViewConfiguration.getTapTimeout()); 1247 } else { 1248 transitionToDelegatingStateAndClear(); 1249 } 1250 } 1251 break; 1252 case ACTION_POINTER_UP: { 1253 transitionToDelegatingStateAndClear(); 1254 } 1255 break; 1256 case ACTION_MOVE: { 1257 if (isFingerDown() 1258 && distance(mLastDown, /* move */ event) > mSwipeMinDistance) { 1259 // Swipe detected - transition immediately 1260 1261 // For convenience, viewport dragging takes precedence 1262 // over insta-delegating on 3tap&swipe 1263 // (which is a rare combo to be used aside from magnification) 1264 if (isMultiTapTriggered(2 /* taps */) && event.getPointerCount() == 1) { 1265 transitionToViewportDraggingStateAndClear(event); 1266 } else if (isActivated() && event.getPointerCount() == 2) { 1267 if (mOverscrollHandler != null 1268 && overscrollState(event, mFirstPointerDownLocation) 1269 == OVERSCROLL_VERTICAL_EDGE) { 1270 transitionToDelegatingStateAndClear(); 1271 } else { 1272 //Primary pointer is swiping, so transit to PanningScalingState 1273 transitToPanningScalingStateAndClear(); 1274 } 1275 } else if (mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled() 1276 && isActivated() 1277 && event.getPointerCount() == 1) { 1278 if (mOverscrollHandler != null 1279 && overscrollState(event, mFirstPointerDownLocation) 1280 == OVERSCROLL_VERTICAL_EDGE) { 1281 transitionToDelegatingStateAndClear(); 1282 } else if (overscrollState(event, mFirstPointerDownLocation) 1283 != OVERSCROLL_NONE) { 1284 transitionToDelegatingStateAndClear(); 1285 } else { 1286 transitToSinglePanningStateAndClear(); 1287 } 1288 } else { 1289 transitionToDelegatingStateAndClear(); 1290 } 1291 } else if (isActivated() && pointerDownValid(mSecondPointerDownLocation) 1292 && distanceClosestPointerToPoint( 1293 mSecondPointerDownLocation, /* move */ event) > mSwipeMinDistance) { 1294 //Second pointer is swiping, so transit to PanningScalingState 1295 transitToPanningScalingStateAndClear(); 1296 } 1297 } 1298 break; 1299 case ACTION_UP: { 1300 1301 mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD); 1302 1303 if (!mFullScreenMagnificationController.magnificationRegionContains( 1304 mDisplayId, event.getX(), event.getY())) { 1305 transitionToDelegatingStateAndClear(); 1306 1307 } else if (isMultiTapTriggered(3 /* taps */)) { 1308 onTripleTap(/* up */ event); 1309 1310 } else if ( 1311 // Possible to be false on: 3tap&drag -> scale -> PTR_UP -> UP 1312 isFingerDown() 1313 //TODO long tap should never happen here 1314 && ((timeBetween(mLastDown, mLastUp) >= mLongTapMinDelay) 1315 || (distance(mLastDown, mLastUp) >= mSwipeMinDistance))) { 1316 transitionToDelegatingStateAndClear(); 1317 1318 } 1319 } 1320 break; 1321 } 1322 } 1323 // LINT.ThenChange(:detecting_state_with_multi_finger) 1324 storePointerDownLocation(PointF pointerDownLocation, MotionEvent event)1325 protected void storePointerDownLocation(PointF pointerDownLocation, MotionEvent event) { 1326 final int index = event.getActionIndex(); 1327 pointerDownLocation.set(event.getX(index), event.getY(index)); 1328 } 1329 pointerDownValid(PointF pointerDownLocation)1330 protected boolean pointerDownValid(PointF pointerDownLocation) { 1331 return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN( 1332 pointerDownLocation.y)); 1333 } 1334 transitToPanningScalingStateAndClear()1335 protected void transitToPanningScalingStateAndClear() { 1336 transitionTo(mPanningScalingState); 1337 clear(); 1338 } 1339 transitToSinglePanningStateAndClear()1340 protected void transitToSinglePanningStateAndClear() { 1341 transitionTo(mSinglePanningState); 1342 clear(); 1343 } 1344 isMultiTapTriggered(int numTaps)1345 public boolean isMultiTapTriggered(int numTaps) { 1346 1347 // Shortcut acts as the 2 initial taps 1348 if (mShortcutTriggered) return tapCount() + 2 >= numTaps; 1349 1350 final boolean multitapTriggered = mDetectSingleFingerTripleTap 1351 && tapCount() >= numTaps 1352 && isMultiTap(mPreLastDown, mLastDown) 1353 && isMultiTap(mPreLastUp, mLastUp); 1354 1355 // Only log the triple tap event, use numTaps to filter 1356 if (multitapTriggered && numTaps > 2) { 1357 final boolean enabled = !isActivated(); 1358 mMagnificationLogger.logMagnificationTripleTap(enabled); 1359 } 1360 return multitapTriggered; 1361 } 1362 isMultiTap(MotionEvent first, MotionEvent second)1363 private boolean isMultiTap(MotionEvent first, MotionEvent second) { 1364 return GestureUtils.isMultiTap(first, second, mMultiTapMaxDelay, mMultiTapMaxDistance); 1365 } 1366 isFingerDown()1367 public boolean isFingerDown() { 1368 return mLastDown != null; 1369 } 1370 timeBetween(@ullable MotionEvent a, @Nullable MotionEvent b)1371 protected long timeBetween(@Nullable MotionEvent a, @Nullable MotionEvent b) { 1372 if (a == null && b == null) return 0; 1373 return abs(timeOf(a) - timeOf(b)); 1374 } 1375 1376 /** 1377 * Nullsafe {@link MotionEvent#getEventTime} that interprets null event as something that 1378 * has happened long enough ago to be gone from the event queue. 1379 * Thus the time for a null event is a small number, that is below any other non-null 1380 * event's time. 1381 * 1382 * @return {@link MotionEvent#getEventTime}, or {@link Long#MIN_VALUE} if the event is null 1383 */ timeOf(@ullable MotionEvent event)1384 private long timeOf(@Nullable MotionEvent event) { 1385 return event != null ? event.getEventTime() : Long.MIN_VALUE; 1386 } 1387 tapCount()1388 public int tapCount() { 1389 return MotionEventInfo.countOf(mDelayedEventQueue, ACTION_UP); 1390 } 1391 1392 /** -> {@link DelegatingState} */ afterMultiTapTimeoutTransitionToDelegatingState()1393 public void afterMultiTapTimeoutTransitionToDelegatingState() { 1394 mHandler.sendEmptyMessageDelayed( 1395 MESSAGE_TRANSITION_TO_DELEGATING_STATE, 1396 mMultiTapMaxDelay); 1397 } 1398 1399 /** -> {@link ViewportDraggingState} */ afterLongTapTimeoutTransitionToDraggingState(MotionEvent event)1400 public void afterLongTapTimeoutTransitionToDraggingState(MotionEvent event) { 1401 mHandler.sendMessageDelayed( 1402 mHandler.obtainMessage(MESSAGE_ON_TRIPLE_TAP_AND_HOLD, 1403 MotionEvent.obtain(event)), 1404 ViewConfiguration.getLongPressTimeout()); 1405 } 1406 1407 @Override clear()1408 public void clear() { 1409 setShortcutTriggered(false); 1410 removePendingDelayedMessages(); 1411 clearDelayedMotionEvents(); 1412 mFirstPointerDownLocation.set(Float.NaN, Float.NaN); 1413 mSecondPointerDownLocation.set(Float.NaN, Float.NaN); 1414 } 1415 removePendingDelayedMessages()1416 protected void removePendingDelayedMessages() { 1417 mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD); 1418 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 1419 mHandler.removeMessages(MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE); 1420 } 1421 cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)1422 protected void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, 1423 int policyFlags) { 1424 if (event.getActionMasked() == ACTION_DOWN) { 1425 mPreLastDown = mLastDown; 1426 mLastDown = MotionEvent.obtain(event); 1427 } else if (event.getActionMasked() == ACTION_UP) { 1428 mPreLastUp = mLastUp; 1429 mLastUp = MotionEvent.obtain(event); 1430 } 1431 1432 MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent, 1433 policyFlags); 1434 if (mDelayedEventQueue == null) { 1435 mDelayedEventQueue = info; 1436 } else { 1437 MotionEventInfo tail = mDelayedEventQueue; 1438 while (tail.mNext != null) { 1439 tail = tail.mNext; 1440 } 1441 tail.mNext = info; 1442 } 1443 } 1444 sendDelayedMotionEvents()1445 protected void sendDelayedMotionEvents() { 1446 if (mDelayedEventQueue == null) { 1447 return; 1448 } 1449 1450 // Adjust down time to prevent subsequent modules being misleading, and also limit 1451 // the maximum offset to mMultiTapMaxDelay to prevent the down time of 2nd tap is 1452 // in the future when multi-tap happens. 1453 final long offset = Math.min( 1454 SystemClock.uptimeMillis() - mLastDetectingDownEventTime, mMultiTapMaxDelay); 1455 1456 do { 1457 MotionEventInfo info = mDelayedEventQueue; 1458 mDelayedEventQueue = info.mNext; 1459 1460 info.event.setDownTime(info.event.getDownTime() + offset); 1461 handleEventWith(mDelegatingState, info.event, info.rawEvent, info.policyFlags); 1462 1463 info.recycle(); 1464 } while (mDelayedEventQueue != null); 1465 } 1466 clearDelayedMotionEvents()1467 protected void clearDelayedMotionEvents() { 1468 while (mDelayedEventQueue != null) { 1469 MotionEventInfo info = mDelayedEventQueue; 1470 mDelayedEventQueue = info.mNext; 1471 info.recycle(); 1472 } 1473 mPreLastDown = null; 1474 mPreLastUp = null; 1475 mLastDown = null; 1476 mLastUp = null; 1477 } 1478 transitionToDelegatingStateAndClear()1479 void transitionToDelegatingStateAndClear() { 1480 transitionTo(mDelegatingState); 1481 sendDelayedMotionEvents(); 1482 removePendingDelayedMessages(); 1483 mSecondPointerDownLocation.set(Float.NaN, Float.NaN); 1484 } 1485 1486 /** 1487 * This method could be triggered by both 2 cases. 1488 * 1. direct three tap gesture 1489 * 2. one tap while shortcut triggered (it counts as two taps). 1490 */ onTripleTap(MotionEvent up)1491 protected void onTripleTap(MotionEvent up) { 1492 if (DEBUG_DETECTING) { 1493 Slog.i(mLogTag, "onTripleTap(); delayed: " 1494 + MotionEventInfo.toString(mDelayedEventQueue)); 1495 } 1496 1497 // We put mShortcutTriggered into conditions. 1498 // The reason is when the shortcut is triggered, 1499 // the magnifier is activated and keeps in scale 1.0, 1500 // and in this case, we still want to zoom on the magnifier. 1501 if (!isActivated() || mShortcutTriggered) { 1502 mPromptController.showNotificationIfNeeded(); 1503 zoomOn(up.getX(), up.getY()); 1504 } else { 1505 zoomOff(); 1506 } 1507 1508 clear(); 1509 } 1510 isActivated()1511 protected boolean isActivated() { 1512 return mFullScreenMagnificationController.isActivated(mDisplayId); 1513 } 1514 transitionToViewportDraggingStateAndClear(MotionEvent down)1515 void transitionToViewportDraggingStateAndClear(MotionEvent down) { 1516 1517 if (DEBUG_DETECTING) Slog.i(mLogTag, "onTripleTapAndHold()"); 1518 final boolean shortcutTriggered = mShortcutTriggered; 1519 1520 // Only log the 3tap and hold event 1521 if (!shortcutTriggered) { 1522 // Triple tap and hold also belongs to triple tap event 1523 final boolean enabled = !isActivated(); 1524 mMagnificationLogger.logMagnificationTripleTap(enabled); 1525 } 1526 clear(); 1527 1528 mViewportDraggingState.prepareForZoomInTemporary(shortcutTriggered); 1529 zoomInTemporary(down.getX(), down.getY(), shortcutTriggered); 1530 transitionTo(mViewportDraggingState); 1531 } 1532 1533 @Override toString()1534 public String toString() { 1535 return "DetectingState{" 1536 + "tapCount()=" + tapCount() 1537 + ", mShortcutTriggered=" + mShortcutTriggered 1538 + ", mDelayedEventQueue=" + MotionEventInfo.toString(mDelayedEventQueue) 1539 + '}'; 1540 } 1541 toggleShortcutTriggered()1542 void toggleShortcutTriggered() { 1543 setShortcutTriggered(!mShortcutTriggered); 1544 } 1545 setShortcutTriggered(boolean state)1546 void setShortcutTriggered(boolean state) { 1547 if (mShortcutTriggered == state) { 1548 return; 1549 } 1550 if (DEBUG_DETECTING) Slog.i(mLogTag, "setShortcutTriggered(" + state + ")"); 1551 1552 mShortcutTriggered = state; 1553 } 1554 isShortcutTriggered()1555 private boolean isShortcutTriggered() { 1556 return mShortcutTriggered; 1557 } 1558 1559 /** 1560 * Detects if last action down is out of distance slop between with previous 1561 * one, when triple tap is enabled. 1562 * 1563 * @return true if tap is out of distance slop 1564 */ isTapOutOfDistanceSlop()1565 boolean isTapOutOfDistanceSlop() { 1566 if (!mDetectSingleFingerTripleTap) return false; 1567 if (mPreLastDown == null || mLastDown == null) { 1568 return false; 1569 } 1570 final boolean outOfDistanceSlop = 1571 GestureUtils.distance(mPreLastDown, mLastDown) > mMultiTapMaxDistance; 1572 if (tapCount() > 0) { 1573 return outOfDistanceSlop; 1574 } 1575 // There's no tap in the queue here. We still need to check if this is the case that 1576 // user tap screen quickly and out of distance slop. 1577 if (outOfDistanceSlop 1578 && !GestureUtils.isTimedOut(mPreLastDown, mLastDown, mMultiTapMaxDelay)) { 1579 return true; 1580 } 1581 return false; 1582 } 1583 } 1584 zoomInTemporary(float centerX, float centerY, boolean shortcutTriggered)1585 private void zoomInTemporary(float centerX, float centerY, boolean shortcutTriggered) { 1586 final float currentScale = mFullScreenMagnificationController.getScale(mDisplayId); 1587 final float persistedScale = MathUtils.constrain( 1588 mFullScreenMagnificationController.getPersistedScale(mDisplayId), 1589 MIN_SCALE, MAX_SCALE); 1590 1591 final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId); 1592 final boolean isShortcutTriggered = shortcutTriggered; 1593 final boolean isZoomedOutFromService = 1594 mFullScreenMagnificationController.isZoomedOutFromService(mDisplayId); 1595 1596 boolean zoomInWithPersistedScale = 1597 !isActivated || isShortcutTriggered || isZoomedOutFromService; 1598 final float scale = zoomInWithPersistedScale ? persistedScale : (currentScale + 1.0f); 1599 zoomToScale(scale, centerX, centerY); 1600 } 1601 zoomOn(float centerX, float centerY)1602 private void zoomOn(float centerX, float centerY) { 1603 if (DEBUG_DETECTING) { 1604 Slog.i(mLogTag, "zoomOn(" + centerX + ", " + centerY + ")"); 1605 } 1606 1607 final float scale = MathUtils.constrain( 1608 mFullScreenMagnificationController.getPersistedScale(mDisplayId), 1609 MIN_SCALE, MAX_SCALE); 1610 zoomToScale(scale, centerX, centerY); 1611 } 1612 zoomToScale(float scale, float centerX, float centerY)1613 private void zoomToScale(float scale, float centerX, float centerY) { 1614 scale = MathUtils.constrain(scale, MIN_SCALE, MAX_SCALE); 1615 mFullScreenMagnificationController.setScaleAndCenter(mDisplayId, 1616 scale, centerX, centerY, 1617 /* animate */ true, 1618 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 1619 } 1620 zoomOff()1621 private void zoomOff() { 1622 if (DEBUG_DETECTING) { 1623 Slog.i(mLogTag, "zoomOff()"); 1624 } 1625 mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true); 1626 } 1627 recycleAndNullify(@ullable MotionEvent event)1628 private static MotionEvent recycleAndNullify(@Nullable MotionEvent event) { 1629 if (event != null) { 1630 event.recycle(); 1631 } 1632 return null; 1633 } 1634 1635 @Override toString()1636 public String toString() { 1637 return "MagnificationGesture{" 1638 + "mDetectingState=" + mDetectingState 1639 + ", mDelegatingState=" + mDelegatingState 1640 + ", mMagnifiedInteractionState=" + mPanningScalingState 1641 + ", mViewportDraggingState=" + mViewportDraggingState 1642 + ", mSinglePanningState=" + mSinglePanningState 1643 + ", mDetectSingleFingerTripleTap=" + mDetectSingleFingerTripleTap 1644 + ", mDetectShortcutTrigger=" + mDetectShortcutTrigger 1645 + ", mCurrentState=" + State.nameOf(mCurrentState) 1646 + ", mPreviousState=" + State.nameOf(mPreviousState) 1647 + ", mMagnificationController=" + mFullScreenMagnificationController 1648 + ", mDisplayId=" + mDisplayId 1649 + ", mIsSinglePanningEnabled=" 1650 + mOneFingerPanningSettingsProvider.isOneFingerPanningEnabled() 1651 + ", mOverscrollHandler=" + mOverscrollHandler 1652 + '}'; 1653 } 1654 overscrollState(MotionEvent event, PointF firstPointerDownLocation)1655 private int overscrollState(MotionEvent event, PointF firstPointerDownLocation) { 1656 if (!pointerValid(firstPointerDownLocation)) { 1657 return OVERSCROLL_NONE; 1658 } 1659 float dX = event.getX() - firstPointerDownLocation.x; 1660 float dY = event.getY() - firstPointerDownLocation.y; 1661 if (isAtLeftEdge() && dX > 0) { 1662 return OVERSCROLL_LEFT_EDGE; 1663 } else if (isAtRightEdge() && dX < 0) { 1664 return OVERSCROLL_RIGHT_EDGE; 1665 } else if ((isAtTopEdge() && dY > 0) || (isAtBottomEdge() && dY < 0)) { 1666 return OVERSCROLL_VERTICAL_EDGE; 1667 } 1668 return OVERSCROLL_NONE; 1669 } 1670 isAtLeftEdge()1671 private boolean isAtLeftEdge() { 1672 return mFullScreenMagnificationController.isAtLeftEdge(mDisplayId, mOverscrollEdgeSlop); 1673 } 1674 isAtRightEdge()1675 private boolean isAtRightEdge() { 1676 return mFullScreenMagnificationController.isAtRightEdge(mDisplayId, mOverscrollEdgeSlop); 1677 } 1678 isAtTopEdge()1679 private boolean isAtTopEdge() { 1680 return mFullScreenMagnificationController.isAtTopEdge(mDisplayId, mOverscrollEdgeSlop); 1681 } 1682 isAtBottomEdge()1683 private boolean isAtBottomEdge() { 1684 return mFullScreenMagnificationController.isAtBottomEdge(mDisplayId, mOverscrollEdgeSlop); 1685 } 1686 pointerValid(PointF pointerDownLocation)1687 private boolean pointerValid(PointF pointerDownLocation) { 1688 return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(pointerDownLocation.y)); 1689 } 1690 1691 private static final class MotionEventInfo { 1692 1693 private static final int MAX_POOL_SIZE = 10; 1694 private static final Object sLock = new Object(); 1695 private static MotionEventInfo sPool; 1696 private static int sPoolSize; 1697 1698 private MotionEventInfo mNext; 1699 private boolean mInPool; 1700 1701 public MotionEvent event; 1702 public MotionEvent rawEvent; 1703 public int policyFlags; 1704 obtain(MotionEvent event, MotionEvent rawEvent, int policyFlags)1705 public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent, 1706 int policyFlags) { 1707 synchronized (sLock) { 1708 MotionEventInfo info = obtainInternal(); 1709 info.initialize(event, rawEvent, policyFlags); 1710 return info; 1711 } 1712 } 1713 1714 @NonNull obtainInternal()1715 private static MotionEventInfo obtainInternal() { 1716 MotionEventInfo info; 1717 if (sPoolSize > 0) { 1718 sPoolSize--; 1719 info = sPool; 1720 sPool = info.mNext; 1721 info.mNext = null; 1722 info.mInPool = false; 1723 } else { 1724 info = new MotionEventInfo(); 1725 } 1726 return info; 1727 } 1728 initialize(MotionEvent event, MotionEvent rawEvent, int policyFlags)1729 private void initialize(MotionEvent event, MotionEvent rawEvent, 1730 int policyFlags) { 1731 this.event = MotionEvent.obtain(event); 1732 this.rawEvent = MotionEvent.obtain(rawEvent); 1733 this.policyFlags = policyFlags; 1734 } 1735 recycle()1736 public void recycle() { 1737 synchronized (sLock) { 1738 if (mInPool) { 1739 throw new IllegalStateException("Already recycled."); 1740 } 1741 clear(); 1742 if (sPoolSize < MAX_POOL_SIZE) { 1743 sPoolSize++; 1744 mNext = sPool; 1745 sPool = this; 1746 mInPool = true; 1747 } 1748 } 1749 } 1750 clear()1751 private void clear() { 1752 event = recycleAndNullify(event); 1753 rawEvent = recycleAndNullify(rawEvent); 1754 policyFlags = 0; 1755 } 1756 countOf(MotionEventInfo info, int eventType)1757 static int countOf(MotionEventInfo info, int eventType) { 1758 if (info == null) return 0; 1759 return (info.event.getAction() == eventType ? 1 : 0) 1760 + countOf(info.mNext, eventType); 1761 } 1762 toString(MotionEventInfo info)1763 public static String toString(MotionEventInfo info) { 1764 return info == null 1765 ? "" 1766 : MotionEvent.actionToString(info.event.getAction()).replace("ACTION_", "") 1767 + " " + MotionEventInfo.toString(info.mNext); 1768 } 1769 } 1770 1771 /** 1772 * BroadcastReceiver used to cancel the magnification shortcut when the screen turns off 1773 */ 1774 private static class ScreenStateReceiver extends BroadcastReceiver { 1775 private final Context mContext; 1776 private final FullScreenMagnificationGestureHandler mGestureHandler; 1777 ScreenStateReceiver(Context context, FullScreenMagnificationGestureHandler gestureHandler)1778 ScreenStateReceiver(Context context, 1779 FullScreenMagnificationGestureHandler gestureHandler) { 1780 mContext = context; 1781 mGestureHandler = gestureHandler; 1782 } 1783 register()1784 public void register() { 1785 mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF)); 1786 } 1787 unregister()1788 public void unregister() { 1789 mContext.unregisterReceiver(this); 1790 } 1791 1792 @Override onReceive(Context context, Intent intent)1793 public void onReceive(Context context, Intent intent) { 1794 mGestureHandler.mDetectingState.setShortcutTriggered(false); 1795 } 1796 } 1797 1798 /** 1799 * Indicates an error with a gesture handler or state. 1800 */ 1801 private static class GestureException extends Exception { 1802 GestureException(String message)1803 GestureException(String message) { 1804 super(message); 1805 } 1806 } 1807 1808 /** Call during MOVE events for a panning gesture. */ onPan(MotionEvent event)1809 private void onPan(MotionEvent event) { 1810 if (!Flags.fullscreenFlingGesture()) { 1811 return; 1812 } 1813 1814 if (mVelocityTracker == null) { 1815 mVelocityTracker = VelocityTracker.obtain(); 1816 } 1817 mVelocityTracker.addMovement(event); 1818 } 1819 1820 /** 1821 * Call during UP events for a panning gesture, so we can detect a fling and play a physics- 1822 * based fling animation. 1823 */ onPanningFinished(MotionEvent event)1824 private void onPanningFinished(MotionEvent event) { 1825 if (!Flags.fullscreenFlingGesture()) { 1826 return; 1827 } 1828 1829 if (mVelocityTracker == null) { 1830 Log.e(mLogTag, "onPanningFinished: mVelocityTracker is null"); 1831 return; 1832 } 1833 mVelocityTracker.addMovement(event); 1834 mVelocityTracker.computeCurrentVelocity(/* units= */ 1000, mMaximumVelocity); 1835 1836 float xPixelsPerSecond = mVelocityTracker.getXVelocity(); 1837 float yPixelsPerSecond = mVelocityTracker.getYVelocity(); 1838 1839 mVelocityTracker.recycle(); 1840 mVelocityTracker = null; 1841 1842 if (DEBUG_PANNING_SCALING) { 1843 Slog.v( 1844 mLogTag, 1845 "onPanningFinished: pixelsPerSecond: " 1846 + xPixelsPerSecond 1847 + ", " 1848 + yPixelsPerSecond 1849 + " mMinimumVelocity: " 1850 + mMinimumVelocity); 1851 } 1852 1853 if ((Math.abs(yPixelsPerSecond) > mMinimumVelocity) 1854 || (Math.abs(xPixelsPerSecond) > mMinimumVelocity)) { 1855 mFullScreenMagnificationController.startFling( 1856 mDisplayId, 1857 xPixelsPerSecond, 1858 yPixelsPerSecond, 1859 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 1860 } 1861 } 1862 cancelFling()1863 private void cancelFling() { 1864 if (!Flags.fullscreenFlingGesture()) { 1865 return; 1866 } 1867 1868 mFullScreenMagnificationController.cancelFling( 1869 mDisplayId, 1870 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 1871 } 1872 1873 final class SinglePanningState extends SimpleOnGestureListener implements State { 1874 1875 private final GestureDetector mScrollGestureDetector; 1876 private MotionEventInfo mEvent; SinglePanningState(Context context)1877 SinglePanningState(Context context) { 1878 mScrollGestureDetector = new GestureDetector(context, this, Handler.getMain()); 1879 } 1880 1881 @Override onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags)1882 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 1883 int action = event.getActionMasked(); 1884 switch (action) { 1885 case ACTION_UP: 1886 onPanningFinished(event); 1887 // fall-through! 1888 case ACTION_CANCEL: 1889 if (mOverscrollHandler != null) { 1890 mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded(); 1891 mOverscrollHandler.clearEdgeState(); 1892 } 1893 transitionTo(mDetectingState); 1894 break; 1895 } 1896 } 1897 1898 @Override onScroll( MotionEvent first, MotionEvent second, float distanceX, float distanceY)1899 public boolean onScroll( 1900 MotionEvent first, MotionEvent second, float distanceX, float distanceY) { 1901 if (mCurrentState != mSinglePanningState) { 1902 return true; 1903 } 1904 onPan(second); 1905 mFullScreenMagnificationController.offsetMagnifiedRegion( 1906 mDisplayId, 1907 distanceX, 1908 distanceY, 1909 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 1910 if (DEBUG_PANNING_SCALING) { 1911 Slog.i( 1912 mLogTag, 1913 "SinglePanningState Panned content by scrollX: " 1914 + distanceX 1915 + " scrollY: " 1916 + distanceY 1917 + " isAtEdge: " 1918 + mFullScreenMagnificationController.isAtEdge(mDisplayId)); 1919 } 1920 if (mOverscrollHandler != null) { 1921 mOverscrollHandler.onScrollStateChanged(first, second); 1922 } 1923 return /* event consumed: */ true; 1924 } 1925 1926 @Override toString()1927 public String toString() { 1928 return "SinglePanningState{" 1929 + "isEdgeOfView=" 1930 + mFullScreenMagnificationController.isAtEdge(mDisplayId); 1931 } 1932 } 1933 1934 /** Overscroll Handler handles the logic when user is at the edge and scrolls past an edge */ 1935 final class OverscrollHandler { 1936 1937 @VisibleForTesting int mOverscrollState; 1938 1939 // mPivotEdge is the point on the edge of the screen when the magnified view hits the edge 1940 // This point sets the center of magnified view when warp/scale effect is triggered 1941 private final PointF mPivotEdge; 1942 1943 // mReachedEdgeCoord is the user's pointer location on the screen when the magnified view 1944 // has hit the edge 1945 private final PointF mReachedEdgeCoord; 1946 1947 // mEdgeCooldown value will be set to true when user hits the edge and will be set to false 1948 // once the user moves x distance away from the edge. This is so that vibrating haptic 1949 // doesn't get triggered by slight movements 1950 private boolean mEdgeCooldown; 1951 OverscrollHandler()1952 OverscrollHandler() { 1953 mOverscrollState = OVERSCROLL_NONE; 1954 mPivotEdge = new PointF(Float.NaN, Float.NaN); 1955 mReachedEdgeCoord = new PointF(Float.NaN, Float.NaN); 1956 mEdgeCooldown = false; 1957 } 1958 warpEffectReset(MotionEvent second)1959 protected boolean warpEffectReset(MotionEvent second) { 1960 float scale = calculateOverscrollScale(second); 1961 if (scale < 0) return false; 1962 mFullScreenMagnificationController.setScaleAndCenter( 1963 /* displayId= */ mDisplayId, 1964 /* scale= */ scale, 1965 /* centerX= */ mPivotEdge.x, 1966 /* centerY= */ mPivotEdge.y, 1967 /* animate= */ true, 1968 /* id= */ AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 1969 if (scale == 1.0f) { 1970 return true; 1971 } 1972 return false; 1973 } 1974 calculateOverscrollScale(MotionEvent second)1975 private float calculateOverscrollScale(MotionEvent second) { 1976 // if at left and overshootDistX is negative or if at right and overshootDistX is 1977 // positive then user is not in overscroll state anymore overscroll state. Reset 1978 // overscroll values by clearing 1979 float overshootDistX = second.getX() - mReachedEdgeCoord.x; 1980 if ((mOverscrollState == OVERSCROLL_LEFT_EDGE && overshootDistX < 0) 1981 || (mOverscrollState == OVERSCROLL_RIGHT_EDGE && overshootDistX > 0)) { 1982 clearEdgeState(); 1983 return -1.0f; 1984 } 1985 float overshootDistY = second.getY() - mReachedEdgeCoord.y; 1986 float overshootDist = (float) (Math.hypot(abs(overshootDistX), abs(overshootDistY))); 1987 Rect bounds = new Rect(); 1988 mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds); 1989 float overShootFraction = overshootDist / (float) bounds.width(); 1990 float minDist = 0.05f * bounds.width(); 1991 if (mEdgeCooldown && (overshootDist > minDist)) { 1992 mEdgeCooldown = false; 1993 } 1994 float scale = (1 - overShootFraction) * getSensitivityScale(); 1995 scale = 1996 MathUtils.constrain( 1997 /* amount= */ scale, 1998 /* low= */ 1.0f, 1999 /* high= */ mFullScreenMagnificationController.getPersistedScale( 2000 mDisplayId)); 2001 return scale; 2002 } 2003 getSensitivityScale()2004 private float getSensitivityScale() { 2005 float magnificationScale = 2006 mFullScreenMagnificationController.getPersistedScale(mDisplayId); 2007 float sensitivityFactor = 0.0f; 2008 if (magnificationScale < 1.7f) { 2009 sensitivityFactor = 1.0f; 2010 } else if (magnificationScale < 2.0f) { 2011 sensitivityFactor = 1.0f; 2012 } else if (magnificationScale < 2.2f) { 2013 sensitivityFactor = 0.95f; 2014 } else if (magnificationScale < 2.5f) { 2015 sensitivityFactor = 1.1f; 2016 } else if (magnificationScale < 2.7f) { 2017 sensitivityFactor = 1.3f; 2018 } else if (magnificationScale < 3.0f) { 2019 sensitivityFactor = 1.0f; 2020 } else { 2021 sensitivityFactor = 1.0f; 2022 } 2023 return magnificationScale * sensitivityFactor; 2024 } 2025 vibrateIfNeeded(MotionEvent event)2026 private void vibrateIfNeeded(MotionEvent event) { 2027 if (mOverscrollState != OVERSCROLL_NONE) { 2028 return; 2029 } 2030 if ((isAtLeftEdge() || isAtRightEdge()) && !mEdgeCooldown) { 2031 mFullScreenMagnificationVibrationHelper.vibrateIfSettingEnabled(); 2032 } 2033 } 2034 setPivotEdge(MotionEvent event)2035 private void setPivotEdge(MotionEvent event) { 2036 if (!pointerValid(mPivotEdge)) { 2037 Rect bounds = new Rect(); 2038 mFullScreenMagnificationController.getMagnificationBounds(mDisplayId, bounds); 2039 if (mOverscrollState == OVERSCROLL_LEFT_EDGE) { 2040 mPivotEdge.set( 2041 bounds.left, mFullScreenMagnificationController.getCenterY(mDisplayId)); 2042 } else if (mOverscrollState == OVERSCROLL_RIGHT_EDGE) { 2043 mPivotEdge.set( 2044 bounds.right, 2045 mFullScreenMagnificationController.getCenterY(mDisplayId)); 2046 } 2047 mReachedEdgeCoord.set(event.getX(), event.getY()); 2048 mEdgeCooldown = true; 2049 } 2050 } 2051 onScrollStateChanged(MotionEvent first, MotionEvent second)2052 private void onScrollStateChanged(MotionEvent first, MotionEvent second) { 2053 if (mFullScreenMagnificationController.isAtEdge(mDisplayId)) { 2054 vibrateIfNeeded(second); 2055 setPivotEdge(second); 2056 } 2057 switch (mOverscrollState) { 2058 case OVERSCROLL_NONE: 2059 onNoOverscroll(first, second); 2060 break; 2061 case OVERSCROLL_VERTICAL_EDGE: 2062 onVerticalOverscroll(); 2063 break; 2064 case OVERSCROLL_LEFT_EDGE: 2065 case OVERSCROLL_RIGHT_EDGE: 2066 onHorizontalOverscroll(second); 2067 break; 2068 default: 2069 Slog.d(mLogTag, "Invalid overscroll state"); 2070 break; 2071 } 2072 } 2073 onNoOverscroll(MotionEvent first, MotionEvent second)2074 public void onNoOverscroll(MotionEvent first, MotionEvent second) { 2075 mOverscrollState = overscrollState(second, new PointF(first.getX(), first.getY())); 2076 } 2077 onVerticalOverscroll()2078 public void onVerticalOverscroll() { 2079 clearEdgeState(); 2080 transitionTo(mDelegatingState); 2081 } 2082 onHorizontalOverscroll(MotionEvent second)2083 public void onHorizontalOverscroll(MotionEvent second) { 2084 boolean reset = warpEffectReset(second); 2085 if (reset) { 2086 mFullScreenMagnificationController.reset(mDisplayId, /* animate */ true); 2087 clearEdgeState(); 2088 transitionTo(mDelegatingState); 2089 } 2090 } 2091 setScaleAndCenterToEdgeIfNeeded()2092 private void setScaleAndCenterToEdgeIfNeeded() { 2093 if (mOverscrollState == OVERSCROLL_LEFT_EDGE 2094 || mOverscrollState == OVERSCROLL_RIGHT_EDGE) { 2095 mFullScreenMagnificationController.setScaleAndCenter( 2096 mDisplayId, 2097 mFullScreenMagnificationController.getPersistedScale(mDisplayId), 2098 mPivotEdge.x, 2099 mPivotEdge.y, 2100 true, 2101 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 2102 } 2103 } 2104 clearEdgeState()2105 private void clearEdgeState() { 2106 mOverscrollState = OVERSCROLL_NONE; 2107 mPivotEdge.set(Float.NaN, Float.NaN); 2108 mReachedEdgeCoord.set(Float.NaN, Float.NaN); 2109 mEdgeCooldown = false; 2110 } 2111 2112 @Override toString()2113 public String toString() { 2114 return "OverscrollHandler {" 2115 + "mOverscrollState=" 2116 + mOverscrollState 2117 + "mPivotEdge.x=" 2118 + mPivotEdge.x 2119 + "mPivotEdge.y=" 2120 + mPivotEdge.y 2121 + "mReachedEdgeCoord.x=" 2122 + mReachedEdgeCoord.x 2123 + "mReachedEdgeCoord.y=" 2124 + mReachedEdgeCoord.y 2125 + "mEdgeCooldown=" 2126 + mEdgeCooldown 2127 + "}"; 2128 } 2129 } 2130 } 2131