1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.accessibility; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.os.Handler; 24 import android.os.Message; 25 import android.util.MathUtils; 26 import android.util.Slog; 27 import android.util.TypedValue; 28 import android.view.GestureDetector; 29 import android.view.GestureDetector.SimpleOnGestureListener; 30 import android.view.InputDevice; 31 import android.view.KeyEvent; 32 import android.view.MotionEvent; 33 import android.view.MotionEvent.PointerCoords; 34 import android.view.MotionEvent.PointerProperties; 35 import android.view.ScaleGestureDetector; 36 import android.view.ScaleGestureDetector.OnScaleGestureListener; 37 import android.view.ViewConfiguration; 38 import android.view.accessibility.AccessibilityEvent; 39 40 /** 41 * This class handles magnification in response to touch events. 42 * 43 * The behavior is as follows: 44 * 45 * 1. Triple tap toggles permanent screen magnification which is magnifying 46 * the area around the location of the triple tap. One can think of the 47 * location of the triple tap as the center of the magnified viewport. 48 * For example, a triple tap when not magnified would magnify the screen 49 * and leave it in a magnified state. A triple tapping when magnified would 50 * clear magnification and leave the screen in a not magnified state. 51 * 52 * 2. Triple tap and hold would magnify the screen if not magnified and enable 53 * viewport dragging mode until the finger goes up. One can think of this 54 * mode as a way to move the magnified viewport since the area around the 55 * moving finger will be magnified to fit the screen. For example, if the 56 * screen was not magnified and the user triple taps and holds the screen 57 * would magnify and the viewport will follow the user's finger. When the 58 * finger goes up the screen will zoom out. If the same user interaction 59 * is performed when the screen is magnified, the viewport movement will 60 * be the same but when the finger goes up the screen will stay magnified. 61 * In other words, the initial magnified state is sticky. 62 * 63 * 3. Magnification can optionally be "triggered" by some external shortcut 64 * affordance. When this occurs via {@link #notifyShortcutTriggered()} a 65 * subsequent tap in a magnifiable region will engage permanent screen 66 * magnification as described in #1. Alternatively, a subsequent long-press 67 * or drag will engage magnification with viewport dragging as described in 68 * #2. Once magnified, all following behaviors apply whether magnification 69 * was engaged via a triple-tap or by a triggered shortcut. 70 * 71 * 4. Pinching with any number of additional fingers when viewport dragging 72 * is enabled, i.e. the user triple tapped and holds, would adjust the 73 * magnification scale which will become the current default magnification 74 * scale. The next time the user magnifies the same magnification scale 75 * would be used. 76 * 77 * 5. When in a permanent magnified state the user can use two or more fingers 78 * to pan the viewport. Note that in this mode the content is panned as 79 * opposed to the viewport dragging mode in which the viewport is moved. 80 * 81 * 6. When in a permanent magnified state the user can use two or more 82 * fingers to change the magnification scale which will become the current 83 * default magnification scale. The next time the user magnifies the same 84 * magnification scale would be used. 85 * 86 * 7. The magnification scale will be persisted in settings and in the cloud. 87 */ 88 class MagnificationGestureHandler implements EventStreamTransformation { 89 private static final String LOG_TAG = "MagnificationEventHandler"; 90 91 private static final boolean DEBUG_STATE_TRANSITIONS = false; 92 private static final boolean DEBUG_DETECTING = false; 93 private static final boolean DEBUG_PANNING = false; 94 95 private static final int STATE_DELEGATING = 1; 96 private static final int STATE_DETECTING = 2; 97 private static final int STATE_VIEWPORT_DRAGGING = 3; 98 private static final int STATE_MAGNIFIED_INTERACTION = 4; 99 100 private static final float MIN_SCALE = 2.0f; 101 private static final float MAX_SCALE = 5.0f; 102 103 private final MagnificationController mMagnificationController; 104 private final DetectingStateHandler mDetectingStateHandler; 105 private final MagnifiedContentInteractionStateHandler mMagnifiedContentInteractionStateHandler; 106 private final StateViewportDraggingHandler mStateViewportDraggingHandler; 107 108 private final ScreenStateReceiver mScreenStateReceiver; 109 110 private final boolean mDetectTripleTap; 111 private final boolean mTriggerable; 112 113 private EventStreamTransformation mNext; 114 115 private int mCurrentState; 116 private int mPreviousState; 117 118 private boolean mTranslationEnabledBeforePan; 119 120 private boolean mShortcutTriggered; 121 122 private PointerCoords[] mTempPointerCoords; 123 private PointerProperties[] mTempPointerProperties; 124 125 private long mDelegatingStateDownTime; 126 127 /** 128 * @param context Context for resolving various magnification-related resources 129 * @param ams AccessibilityManagerService used to obtain a {@link MagnificationController} 130 * @param detectTripleTap {@code true} if this detector should detect and respond to triple-tap 131 * gestures for engaging and disengaging magnification, 132 * {@code false} if it should ignore such gestures 133 * @param triggerable {@code true} if this detector should be "triggerable" by some external 134 * shortcut invoking {@link #notifyShortcutTriggered}, {@code 135 * false} if it should ignore such triggers. 136 */ 137 public MagnificationGestureHandler(Context context, AccessibilityManagerService ams, 138 boolean detectTripleTap, boolean triggerable) { 139 mMagnificationController = ams.getMagnificationController(); 140 mDetectingStateHandler = new DetectingStateHandler(context); 141 mStateViewportDraggingHandler = new StateViewportDraggingHandler(); 142 mMagnifiedContentInteractionStateHandler = 143 new MagnifiedContentInteractionStateHandler(context); 144 mDetectTripleTap = detectTripleTap; 145 mTriggerable = triggerable; 146 147 if (triggerable) { 148 mScreenStateReceiver = new ScreenStateReceiver(context, this); 149 mScreenStateReceiver.register(); 150 } else { 151 mScreenStateReceiver = null; 152 } 153 154 transitionToState(STATE_DETECTING); 155 } 156 157 @Override 158 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 159 if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) { 160 if (mNext != null) { 161 mNext.onMotionEvent(event, rawEvent, policyFlags); 162 } 163 return; 164 } 165 if (!mDetectTripleTap && !mTriggerable) { 166 if (mNext != null) { 167 dispatchTransformedEvent(event, rawEvent, policyFlags); 168 } 169 return; 170 } 171 mMagnifiedContentInteractionStateHandler.onMotionEvent(event, rawEvent, policyFlags); 172 switch (mCurrentState) { 173 case STATE_DELEGATING: { 174 handleMotionEventStateDelegating(event, rawEvent, policyFlags); 175 } 176 break; 177 case STATE_DETECTING: { 178 mDetectingStateHandler.onMotionEvent(event, rawEvent, policyFlags); 179 } 180 break; 181 case STATE_VIEWPORT_DRAGGING: { 182 mStateViewportDraggingHandler.onMotionEvent(event, rawEvent, policyFlags); 183 } 184 break; 185 case STATE_MAGNIFIED_INTERACTION: { 186 // mMagnifiedContentInteractionStateHandler handles events only 187 // if this is the current state since it uses ScaleGestureDetector 188 // and a GestureDetector which need well formed event stream. 189 } 190 break; 191 default: { 192 throw new IllegalStateException("Unknown state: " + mCurrentState); 193 } 194 } 195 } 196 197 @Override 198 public void onKeyEvent(KeyEvent event, int policyFlags) { 199 if (mNext != null) { 200 mNext.onKeyEvent(event, policyFlags); 201 } 202 } 203 204 @Override 205 public void onAccessibilityEvent(AccessibilityEvent event) { 206 if (mNext != null) { 207 mNext.onAccessibilityEvent(event); 208 } 209 } 210 211 @Override 212 public void setNext(EventStreamTransformation next) { 213 mNext = next; 214 } 215 216 @Override 217 public void clearEvents(int inputSource) { 218 if (inputSource == InputDevice.SOURCE_TOUCHSCREEN) { 219 clear(); 220 } 221 222 if (mNext != null) { 223 mNext.clearEvents(inputSource); 224 } 225 } 226 227 @Override 228 public void onDestroy() { 229 if (mScreenStateReceiver != null) { 230 mScreenStateReceiver.unregister(); 231 } 232 clear(); 233 } 234 235 void notifyShortcutTriggered() { 236 if (mTriggerable) { 237 if (mMagnificationController.resetIfNeeded(true)) { 238 clear(); 239 } else { 240 setMagnificationShortcutTriggered(!mShortcutTriggered); 241 } 242 } 243 } 244 245 private void setMagnificationShortcutTriggered(boolean state) { 246 if (mShortcutTriggered == state) { 247 return; 248 } 249 250 mShortcutTriggered = state; 251 mMagnificationController.setForceShowMagnifiableBounds(state); 252 } 253 254 private void clear() { 255 mCurrentState = STATE_DETECTING; 256 setMagnificationShortcutTriggered(false); 257 mDetectingStateHandler.clear(); 258 mStateViewportDraggingHandler.clear(); 259 mMagnifiedContentInteractionStateHandler.clear(); 260 } 261 262 private void handleMotionEventStateDelegating(MotionEvent event, 263 MotionEvent rawEvent, int policyFlags) { 264 switch (event.getActionMasked()) { 265 case MotionEvent.ACTION_DOWN: { 266 mDelegatingStateDownTime = event.getDownTime(); 267 } 268 break; 269 case MotionEvent.ACTION_UP: { 270 if (mDetectingStateHandler.mDelayedEventQueue == null) { 271 transitionToState(STATE_DETECTING); 272 } 273 } 274 break; 275 } 276 if (mNext != null) { 277 // We cache some events to see if the user wants to trigger magnification. 278 // If no magnification is triggered we inject these events with adjusted 279 // time and down time to prevent subsequent transformations being confused 280 // by stale events. After the cached events, which always have a down, are 281 // injected we need to also update the down time of all subsequent non cached 282 // events. All delegated events cached and non-cached are delivered here. 283 event.setDownTime(mDelegatingStateDownTime); 284 dispatchTransformedEvent(event, rawEvent, policyFlags); 285 } 286 } 287 288 private void dispatchTransformedEvent(MotionEvent event, MotionEvent rawEvent, 289 int policyFlags) { 290 // If the event is within the magnified portion of the screen we have 291 // to change its location to be where the user thinks he is poking the 292 // UI which may have been magnified and panned. 293 final float eventX = event.getX(); 294 final float eventY = event.getY(); 295 if (mMagnificationController.isMagnifying() 296 && mMagnificationController.magnificationRegionContains(eventX, eventY)) { 297 final float scale = mMagnificationController.getScale(); 298 final float scaledOffsetX = mMagnificationController.getOffsetX(); 299 final float scaledOffsetY = mMagnificationController.getOffsetY(); 300 final int pointerCount = event.getPointerCount(); 301 PointerCoords[] coords = getTempPointerCoordsWithMinSize(pointerCount); 302 PointerProperties[] properties = getTempPointerPropertiesWithMinSize( 303 pointerCount); 304 for (int i = 0; i < pointerCount; i++) { 305 event.getPointerCoords(i, coords[i]); 306 coords[i].x = (coords[i].x - scaledOffsetX) / scale; 307 coords[i].y = (coords[i].y - scaledOffsetY) / scale; 308 event.getPointerProperties(i, properties[i]); 309 } 310 event = MotionEvent.obtain(event.getDownTime(), 311 event.getEventTime(), event.getAction(), pointerCount, properties, 312 coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(), 313 event.getFlags()); 314 } 315 mNext.onMotionEvent(event, rawEvent, policyFlags); 316 } 317 318 private PointerCoords[] getTempPointerCoordsWithMinSize(int size) { 319 final int oldSize = (mTempPointerCoords != null) ? mTempPointerCoords.length : 0; 320 if (oldSize < size) { 321 PointerCoords[] oldTempPointerCoords = mTempPointerCoords; 322 mTempPointerCoords = new PointerCoords[size]; 323 if (oldTempPointerCoords != null) { 324 System.arraycopy(oldTempPointerCoords, 0, mTempPointerCoords, 0, oldSize); 325 } 326 } 327 for (int i = oldSize; i < size; i++) { 328 mTempPointerCoords[i] = new PointerCoords(); 329 } 330 return mTempPointerCoords; 331 } 332 333 private PointerProperties[] getTempPointerPropertiesWithMinSize(int size) { 334 final int oldSize = (mTempPointerProperties != null) ? mTempPointerProperties.length 335 : 0; 336 if (oldSize < size) { 337 PointerProperties[] oldTempPointerProperties = mTempPointerProperties; 338 mTempPointerProperties = new PointerProperties[size]; 339 if (oldTempPointerProperties != null) { 340 System.arraycopy(oldTempPointerProperties, 0, mTempPointerProperties, 0, 341 oldSize); 342 } 343 } 344 for (int i = oldSize; i < size; i++) { 345 mTempPointerProperties[i] = new PointerProperties(); 346 } 347 return mTempPointerProperties; 348 } 349 350 private void transitionToState(int state) { 351 if (DEBUG_STATE_TRANSITIONS) { 352 switch (state) { 353 case STATE_DELEGATING: { 354 Slog.i(LOG_TAG, "mCurrentState: STATE_DELEGATING"); 355 } 356 break; 357 case STATE_DETECTING: { 358 Slog.i(LOG_TAG, "mCurrentState: STATE_DETECTING"); 359 } 360 break; 361 case STATE_VIEWPORT_DRAGGING: { 362 Slog.i(LOG_TAG, "mCurrentState: STATE_VIEWPORT_DRAGGING"); 363 } 364 break; 365 case STATE_MAGNIFIED_INTERACTION: { 366 Slog.i(LOG_TAG, "mCurrentState: STATE_MAGNIFIED_INTERACTION"); 367 } 368 break; 369 default: { 370 throw new IllegalArgumentException("Unknown state: " + state); 371 } 372 } 373 } 374 mPreviousState = mCurrentState; 375 mCurrentState = state; 376 } 377 378 private interface MotionEventHandler { 379 380 void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags); 381 382 void clear(); 383 } 384 385 /** 386 * This class determines if the user is performing a scale or pan gesture. 387 */ 388 private final class MagnifiedContentInteractionStateHandler extends SimpleOnGestureListener 389 implements OnScaleGestureListener, MotionEventHandler { 390 391 private final ScaleGestureDetector mScaleGestureDetector; 392 393 private final GestureDetector mGestureDetector; 394 395 private final float mScalingThreshold; 396 397 private float mInitialScaleFactor = -1; 398 399 private boolean mScaling; 400 401 public MagnifiedContentInteractionStateHandler(Context context) { 402 final TypedValue scaleValue = new TypedValue(); 403 context.getResources().getValue( 404 com.android.internal.R.dimen.config_screen_magnification_scaling_threshold, 405 scaleValue, false); 406 mScalingThreshold = scaleValue.getFloat(); 407 mScaleGestureDetector = new ScaleGestureDetector(context, this); 408 mScaleGestureDetector.setQuickScaleEnabled(false); 409 mGestureDetector = new GestureDetector(context, this); 410 } 411 412 @Override 413 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 414 mScaleGestureDetector.onTouchEvent(event); 415 mGestureDetector.onTouchEvent(event); 416 if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { 417 return; 418 } 419 if (event.getActionMasked() == MotionEvent.ACTION_UP) { 420 clear(); 421 mMagnificationController.persistScale(); 422 if (mPreviousState == STATE_VIEWPORT_DRAGGING) { 423 transitionToState(STATE_VIEWPORT_DRAGGING); 424 } else { 425 transitionToState(STATE_DETECTING); 426 } 427 } 428 } 429 430 @Override 431 public boolean onScroll(MotionEvent first, MotionEvent second, float distanceX, 432 float distanceY) { 433 if (mCurrentState != STATE_MAGNIFIED_INTERACTION) { 434 return true; 435 } 436 if (DEBUG_PANNING) { 437 Slog.i(LOG_TAG, "Panned content by scrollX: " + distanceX 438 + " scrollY: " + distanceY); 439 } 440 mMagnificationController.offsetMagnifiedRegion(distanceX, distanceY, 441 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 442 return true; 443 } 444 445 @Override 446 public boolean onScale(ScaleGestureDetector detector) { 447 if (!mScaling) { 448 if (mInitialScaleFactor < 0) { 449 mInitialScaleFactor = detector.getScaleFactor(); 450 } else { 451 final float deltaScale = detector.getScaleFactor() - mInitialScaleFactor; 452 if (Math.abs(deltaScale) > mScalingThreshold) { 453 mScaling = true; 454 return true; 455 } 456 } 457 return false; 458 } 459 460 final float initialScale = mMagnificationController.getScale(); 461 final float targetScale = initialScale * detector.getScaleFactor(); 462 463 // Don't allow a gesture to move the user further outside the 464 // desired bounds for gesture-controlled scaling. 465 final float scale; 466 if (targetScale > MAX_SCALE && targetScale > initialScale) { 467 // The target scale is too big and getting bigger. 468 scale = MAX_SCALE; 469 } else if (targetScale < MIN_SCALE && targetScale < initialScale) { 470 // The target scale is too small and getting smaller. 471 scale = MIN_SCALE; 472 } else { 473 // The target scale may be outside our bounds, but at least 474 // it's moving in the right direction. This avoids a "jump" if 475 // we're at odds with some other service's desired bounds. 476 scale = targetScale; 477 } 478 479 final float pivotX = detector.getFocusX(); 480 final float pivotY = detector.getFocusY(); 481 mMagnificationController.setScale(scale, pivotX, pivotY, false, 482 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 483 return true; 484 } 485 486 @Override 487 public boolean onScaleBegin(ScaleGestureDetector detector) { 488 return (mCurrentState == STATE_MAGNIFIED_INTERACTION); 489 } 490 491 @Override 492 public void onScaleEnd(ScaleGestureDetector detector) { 493 clear(); 494 } 495 496 @Override 497 public void clear() { 498 mInitialScaleFactor = -1; 499 mScaling = false; 500 } 501 } 502 503 /** 504 * This class handles motion events when the event dispatcher has 505 * determined that the user is performing a single-finger drag of the 506 * magnification viewport. 507 */ 508 private final class StateViewportDraggingHandler implements MotionEventHandler { 509 510 private boolean mLastMoveOutsideMagnifiedRegion; 511 512 @Override 513 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 514 final int action = event.getActionMasked(); 515 switch (action) { 516 case MotionEvent.ACTION_DOWN: { 517 throw new IllegalArgumentException("Unexpected event type: ACTION_DOWN"); 518 } 519 case MotionEvent.ACTION_POINTER_DOWN: { 520 clear(); 521 transitionToState(STATE_MAGNIFIED_INTERACTION); 522 } 523 break; 524 case MotionEvent.ACTION_MOVE: { 525 if (event.getPointerCount() != 1) { 526 throw new IllegalStateException("Should have one pointer down."); 527 } 528 final float eventX = event.getX(); 529 final float eventY = event.getY(); 530 if (mMagnificationController.magnificationRegionContains(eventX, eventY)) { 531 if (mLastMoveOutsideMagnifiedRegion) { 532 mLastMoveOutsideMagnifiedRegion = false; 533 mMagnificationController.setCenter(eventX, eventY, true, 534 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 535 } else { 536 mMagnificationController.setCenter(eventX, eventY, false, 537 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 538 } 539 } else { 540 mLastMoveOutsideMagnifiedRegion = true; 541 } 542 } 543 break; 544 case MotionEvent.ACTION_UP: { 545 if (!mTranslationEnabledBeforePan) { 546 mMagnificationController.reset(true); 547 } 548 clear(); 549 transitionToState(STATE_DETECTING); 550 } 551 break; 552 case MotionEvent.ACTION_POINTER_UP: { 553 throw new IllegalArgumentException( 554 "Unexpected event type: ACTION_POINTER_UP"); 555 } 556 } 557 } 558 559 @Override 560 public void clear() { 561 mLastMoveOutsideMagnifiedRegion = false; 562 } 563 } 564 565 /** 566 * This class handles motion events when the event dispatch has not yet 567 * determined what the user is doing. It watches for various tap events. 568 */ 569 private final class DetectingStateHandler implements MotionEventHandler { 570 571 private static final int MESSAGE_ON_ACTION_TAP_AND_HOLD = 1; 572 573 private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2; 574 575 private static final int ACTION_TAP_COUNT = 3; 576 577 private final int mTapTimeSlop = ViewConfiguration.getJumpTapTimeout(); 578 579 private final int mMultiTapTimeSlop; 580 581 private final int mTapDistanceSlop; 582 583 private final int mMultiTapDistanceSlop; 584 585 private MotionEventInfo mDelayedEventQueue; 586 587 private MotionEvent mLastDownEvent; 588 589 private MotionEvent mLastTapUpEvent; 590 591 private int mTapCount; 592 593 public DetectingStateHandler(Context context) { 594 mMultiTapTimeSlop = ViewConfiguration.getDoubleTapTimeout() 595 + context.getResources().getInteger( 596 com.android.internal.R.integer.config_screen_magnification_multi_tap_adjustment); 597 mTapDistanceSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 598 mMultiTapDistanceSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); 599 } 600 601 private final Handler mHandler = new Handler() { 602 @Override 603 public void handleMessage(Message message) { 604 final int type = message.what; 605 switch (type) { 606 case MESSAGE_ON_ACTION_TAP_AND_HOLD: { 607 MotionEvent event = (MotionEvent) message.obj; 608 final int policyFlags = message.arg1; 609 onActionTapAndHold(event, policyFlags); 610 } 611 break; 612 case MESSAGE_TRANSITION_TO_DELEGATING_STATE: { 613 transitionToState(STATE_DELEGATING); 614 sendDelayedMotionEvents(); 615 clear(); 616 } 617 break; 618 default: { 619 throw new IllegalArgumentException("Unknown message type: " + type); 620 } 621 } 622 } 623 }; 624 625 @Override 626 public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { 627 cacheDelayedMotionEvent(event, rawEvent, policyFlags); 628 final int action = event.getActionMasked(); 629 switch (action) { 630 case MotionEvent.ACTION_DOWN: { 631 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 632 if (!mMagnificationController.magnificationRegionContains( 633 event.getX(), event.getY())) { 634 transitionToDelegatingState(!mShortcutTriggered); 635 return; 636 } 637 if (mShortcutTriggered) { 638 Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD, 639 policyFlags, 0, event); 640 mHandler.sendMessageDelayed(message, 641 ViewConfiguration.getLongPressTimeout()); 642 return; 643 } 644 if (mDetectTripleTap) { 645 if ((mTapCount == ACTION_TAP_COUNT - 1) && (mLastDownEvent != null) 646 && GestureUtils.isMultiTap(mLastDownEvent, event, mMultiTapTimeSlop, 647 mMultiTapDistanceSlop, 0)) { 648 Message message = mHandler.obtainMessage(MESSAGE_ON_ACTION_TAP_AND_HOLD, 649 policyFlags, 0, event); 650 mHandler.sendMessageDelayed(message, 651 ViewConfiguration.getLongPressTimeout()); 652 } else if (mTapCount < ACTION_TAP_COUNT) { 653 Message message = mHandler.obtainMessage( 654 MESSAGE_TRANSITION_TO_DELEGATING_STATE); 655 mHandler.sendMessageDelayed(message, mMultiTapTimeSlop); 656 } 657 clearLastDownEvent(); 658 mLastDownEvent = MotionEvent.obtain(event); 659 } else if (mMagnificationController.isMagnifying()) { 660 // If magnified, consume an ACTION_DOWN until mMultiTapTimeSlop or 661 // mTapDistanceSlop is reached to ensure MAGNIFIED_INTERACTION is reachable. 662 Message message = mHandler.obtainMessage( 663 MESSAGE_TRANSITION_TO_DELEGATING_STATE); 664 mHandler.sendMessageDelayed(message, mMultiTapTimeSlop); 665 return; 666 } else { 667 transitionToDelegatingState(true); 668 return; 669 } 670 } 671 break; 672 case MotionEvent.ACTION_POINTER_DOWN: { 673 if (mMagnificationController.isMagnifying()) { 674 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 675 transitionToState(STATE_MAGNIFIED_INTERACTION); 676 clear(); 677 } else { 678 transitionToDelegatingState(true); 679 } 680 } 681 break; 682 case MotionEvent.ACTION_MOVE: { 683 if (mLastDownEvent != null && mTapCount < ACTION_TAP_COUNT - 1) { 684 final double distance = GestureUtils.computeDistance(mLastDownEvent, 685 event, 0); 686 if (Math.abs(distance) > mTapDistanceSlop) { 687 transitionToDelegatingState(true); 688 } 689 } 690 } 691 break; 692 case MotionEvent.ACTION_UP: { 693 if (!mMagnificationController.magnificationRegionContains( 694 event.getX(), event.getY())) { 695 transitionToDelegatingState(!mShortcutTriggered); 696 return; 697 } 698 if (mShortcutTriggered) { 699 clear(); 700 onActionTap(event, policyFlags); 701 return; 702 } 703 if (mLastDownEvent == null) { 704 return; 705 } 706 mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); 707 if (!GestureUtils.isTap(mLastDownEvent, event, mTapTimeSlop, 708 mTapDistanceSlop, 0)) { 709 transitionToDelegatingState(true); 710 return; 711 } 712 if (mLastTapUpEvent != null && !GestureUtils.isMultiTap( 713 mLastTapUpEvent, event, mMultiTapTimeSlop, mMultiTapDistanceSlop, 0)) { 714 transitionToDelegatingState(true); 715 return; 716 } 717 mTapCount++; 718 if (DEBUG_DETECTING) { 719 Slog.i(LOG_TAG, "Tap count:" + mTapCount); 720 } 721 if (mTapCount == ACTION_TAP_COUNT) { 722 clear(); 723 onActionTap(event, policyFlags); 724 return; 725 } 726 clearLastTapUpEvent(); 727 mLastTapUpEvent = MotionEvent.obtain(event); 728 } 729 break; 730 case MotionEvent.ACTION_POINTER_UP: { 731 /* do nothing */ 732 } 733 break; 734 } 735 } 736 737 @Override 738 public void clear() { 739 setMagnificationShortcutTriggered(false); 740 mHandler.removeMessages(MESSAGE_ON_ACTION_TAP_AND_HOLD); 741 mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE); 742 clearTapDetectionState(); 743 clearDelayedMotionEvents(); 744 } 745 746 private void clearTapDetectionState() { 747 mTapCount = 0; 748 clearLastTapUpEvent(); 749 clearLastDownEvent(); 750 } 751 752 private void clearLastTapUpEvent() { 753 if (mLastTapUpEvent != null) { 754 mLastTapUpEvent.recycle(); 755 mLastTapUpEvent = null; 756 } 757 } 758 759 private void clearLastDownEvent() { 760 if (mLastDownEvent != null) { 761 mLastDownEvent.recycle(); 762 mLastDownEvent = null; 763 } 764 } 765 766 private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, 767 int policyFlags) { 768 MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent, 769 policyFlags); 770 if (mDelayedEventQueue == null) { 771 mDelayedEventQueue = info; 772 } else { 773 MotionEventInfo tail = mDelayedEventQueue; 774 while (tail.mNext != null) { 775 tail = tail.mNext; 776 } 777 tail.mNext = info; 778 } 779 } 780 781 private void sendDelayedMotionEvents() { 782 while (mDelayedEventQueue != null) { 783 MotionEventInfo info = mDelayedEventQueue; 784 mDelayedEventQueue = info.mNext; 785 MagnificationGestureHandler.this.onMotionEvent(info.mEvent, info.mRawEvent, 786 info.mPolicyFlags); 787 info.recycle(); 788 } 789 } 790 791 private void clearDelayedMotionEvents() { 792 while (mDelayedEventQueue != null) { 793 MotionEventInfo info = mDelayedEventQueue; 794 mDelayedEventQueue = info.mNext; 795 info.recycle(); 796 } 797 } 798 799 private void transitionToDelegatingState(boolean andClear) { 800 transitionToState(STATE_DELEGATING); 801 sendDelayedMotionEvents(); 802 if (andClear) { 803 clear(); 804 } 805 } 806 807 private void onActionTap(MotionEvent up, int policyFlags) { 808 if (DEBUG_DETECTING) { 809 Slog.i(LOG_TAG, "onActionTap()"); 810 } 811 812 if (!mMagnificationController.isMagnifying()) { 813 final float targetScale = mMagnificationController.getPersistedScale(); 814 final float scale = MathUtils.constrain(targetScale, MIN_SCALE, MAX_SCALE); 815 mMagnificationController.setScaleAndCenter(scale, up.getX(), up.getY(), true, 816 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 817 } else { 818 mMagnificationController.reset(true); 819 } 820 } 821 822 private void onActionTapAndHold(MotionEvent down, int policyFlags) { 823 if (DEBUG_DETECTING) { 824 Slog.i(LOG_TAG, "onActionTapAndHold()"); 825 } 826 827 clear(); 828 mTranslationEnabledBeforePan = mMagnificationController.isMagnifying(); 829 830 final float targetScale = mMagnificationController.getPersistedScale(); 831 final float scale = MathUtils.constrain(targetScale, MIN_SCALE, MAX_SCALE); 832 mMagnificationController.setScaleAndCenter(scale, down.getX(), down.getY(), true, 833 AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); 834 835 transitionToState(STATE_VIEWPORT_DRAGGING); 836 } 837 } 838 839 private static final class MotionEventInfo { 840 841 private static final int MAX_POOL_SIZE = 10; 842 843 private static final Object sLock = new Object(); 844 845 private static MotionEventInfo sPool; 846 847 private static int sPoolSize; 848 849 private MotionEventInfo mNext; 850 851 private boolean mInPool; 852 853 public MotionEvent mEvent; 854 855 public MotionEvent mRawEvent; 856 857 public int mPolicyFlags; 858 859 public static MotionEventInfo obtain(MotionEvent event, MotionEvent rawEvent, 860 int policyFlags) { 861 synchronized (sLock) { 862 MotionEventInfo info; 863 if (sPoolSize > 0) { 864 sPoolSize--; 865 info = sPool; 866 sPool = info.mNext; 867 info.mNext = null; 868 info.mInPool = false; 869 } else { 870 info = new MotionEventInfo(); 871 } 872 info.initialize(event, rawEvent, policyFlags); 873 return info; 874 } 875 } 876 877 private void initialize(MotionEvent event, MotionEvent rawEvent, 878 int policyFlags) { 879 mEvent = MotionEvent.obtain(event); 880 mRawEvent = MotionEvent.obtain(rawEvent); 881 mPolicyFlags = policyFlags; 882 } 883 884 public void recycle() { 885 synchronized (sLock) { 886 if (mInPool) { 887 throw new IllegalStateException("Already recycled."); 888 } 889 clear(); 890 if (sPoolSize < MAX_POOL_SIZE) { 891 sPoolSize++; 892 mNext = sPool; 893 sPool = this; 894 mInPool = true; 895 } 896 } 897 } 898 899 private void clear() { 900 mEvent.recycle(); 901 mEvent = null; 902 mRawEvent.recycle(); 903 mRawEvent = null; 904 mPolicyFlags = 0; 905 } 906 } 907 908 /** 909 * BroadcastReceiver used to cancel the magnification shortcut when the screen turns off 910 */ 911 private static class ScreenStateReceiver extends BroadcastReceiver { 912 private final Context mContext; 913 private final MagnificationGestureHandler mGestureHandler; 914 915 public ScreenStateReceiver(Context context, MagnificationGestureHandler gestureHandler) { 916 mContext = context; 917 mGestureHandler = gestureHandler; 918 } 919 920 public void register() { 921 mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_SCREEN_OFF)); 922 } 923 924 public void unregister() { 925 mContext.unregisterReceiver(this); 926 } 927 928 @Override 929 public void onReceive(Context context, Intent intent) { 930 mGestureHandler.setMagnificationShortcutTriggered(false); 931 } 932 } 933 } 934