| /* |
| ** Copyright 2011, The Android Open Source Project |
| ** |
| ** Licensed under the Apache License, Version 2.0 (the "License"); |
| ** you may not use this file except in compliance with the License. |
| ** You may obtain a copy of the License at |
| ** |
| ** http://www.apache.org/licenses/LICENSE-2.0 |
| ** |
| ** Unless required by applicable law or agreed to in writing, software |
| ** distributed under the License is distributed on an "AS IS" BASIS, |
| ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| ** See the License for the specific language governing permissions and |
| ** limitations under the License. |
| */ |
| |
| package com.android.server.accessibility.gestures; |
| |
| import static android.view.MotionEvent.INVALID_POINTER_ID; |
| |
| import static com.android.server.accessibility.gestures.TouchState.ALL_POINTER_ID_BITS; |
| |
| import android.accessibilityservice.AccessibilityGestureEvent; |
| import android.content.Context; |
| import android.graphics.Region; |
| import android.os.Handler; |
| import android.util.Slog; |
| import android.view.InputDevice; |
| import android.view.MotionEvent; |
| import android.view.ViewConfiguration; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| |
| import com.android.server.accessibility.AccessibilityManagerService; |
| import com.android.server.accessibility.BaseEventStreamTransformation; |
| import com.android.server.accessibility.EventStreamTransformation; |
| import com.android.server.policy.WindowManagerPolicy; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * This class is a strategy for performing touch exploration. It |
| * transforms the motion event stream by modifying, adding, replacing, |
| * and consuming certain events. The interaction model is: |
| * |
| * <ol> |
| * <li>1. One finger moving slow around performs touch exploration.</li> |
| * <li>2. One finger moving fast around performs gestures.</li> |
| * <li>3. Two close fingers moving in the same direction perform a drag.</li> |
| * <li>4. Multi-finger gestures are delivered to view hierarchy.</li> |
| * <li>5. Two fingers moving in different directions are considered a multi-finger gesture.</li> |
| * <li>6. Double tapping performs a click action on the accessibility |
| * focused rectangle.</li> |
| * <li>7. Tapping and holding for a while performs a long press in a similar fashion |
| * as the click above.</li> |
| * <ol> |
| * |
| * @hide |
| */ |
| public class TouchExplorer extends BaseEventStreamTransformation |
| implements GestureManifold.Listener { |
| |
| static final boolean DEBUG = false; |
| |
| // Tag for logging received events. |
| private static final String LOG_TAG = "TouchExplorer"; |
| |
| // The maximum of the cosine between the vectors of two moving |
| // pointers so they can be considered moving in the same direction. |
| private static final float MAX_DRAGGING_ANGLE_COS = 0.525321989f; // cos(pi/4) |
| |
| // The timeout after which we are no longer trying to detect a gesture. |
| private static final int EXIT_GESTURE_DETECTION_TIMEOUT = 2000; |
| |
| // Timeout before trying to decide what the user is trying to do. |
| private final int mDetermineUserIntentTimeout; |
| |
| // Slop between the first and second tap to be a double tap. |
| private final int mDoubleTapSlop; |
| |
| // The current state of the touch explorer. |
| private TouchState mState; |
| |
| // The ID of the pointer used for dragging. |
| private int mDraggingPointerId; |
| |
| |
| // Handler for performing asynchronous operations. |
| private final Handler mHandler; |
| |
| // Command for delayed sending of a hover enter and move event. |
| private final SendHoverEnterAndMoveDelayed mSendHoverEnterAndMoveDelayed; |
| |
| // Command for delayed sending of a hover exit event. |
| private final SendHoverExitDelayed mSendHoverExitDelayed; |
| |
| // Command for delayed sending of touch exploration end events. |
| private final SendAccessibilityEventDelayed mSendTouchExplorationEndDelayed; |
| |
| // Command for delayed sending of touch interaction end events. |
| private final SendAccessibilityEventDelayed mSendTouchInteractionEndDelayed; |
| |
| // Command for exiting gesture detection mode after a timeout. |
| private final ExitGestureDetectionModeDelayed mExitGestureDetectionModeDelayed; |
| |
| // Helper to detect gestures. |
| private final GestureManifold mGestureDetector; |
| |
| // Helper class to track received pointers. |
| private final TouchState.ReceivedPointerTracker mReceivedPointerTracker; |
| |
| private final EventDispatcher mDispatcher; |
| |
| // Handle to the accessibility manager service. |
| private final AccessibilityManagerService mAms; |
| |
| |
| // Context in which this explorer operates. |
| private final Context mContext; |
| |
| private Region mGestureDetectionPassthroughRegion; |
| private Region mTouchExplorationPassthroughRegion; |
| |
| /** |
| * Creates a new instance. |
| * |
| * @param context A context handle for accessing resources. |
| * @param service The service to notify touch interaction and gesture completed and to perform |
| * action. |
| */ |
| public TouchExplorer(Context context, AccessibilityManagerService service) { |
| this(context, service, null); |
| } |
| |
| /** |
| * Creates a new instance. |
| * |
| * @param context A context handle for accessing resources. |
| * @param service The service to notify touch interaction and gesture completed and to perform |
| * action. |
| * @param detector The gesture detector to handle accessibility touch event. If null the default |
| * one created in place, or for testing purpose. |
| */ |
| public TouchExplorer(Context context, AccessibilityManagerService service, |
| GestureManifold detector) { |
| mContext = context; |
| mAms = service; |
| mState = new TouchState(); |
| mReceivedPointerTracker = mState.getReceivedPointerTracker(); |
| mDispatcher = new EventDispatcher(context, mAms, super.getNext(), mState); |
| mDetermineUserIntentTimeout = ViewConfiguration.getDoubleTapTimeout(); |
| mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); |
| mHandler = new Handler(context.getMainLooper()); |
| mExitGestureDetectionModeDelayed = new ExitGestureDetectionModeDelayed(); |
| mSendHoverEnterAndMoveDelayed = new SendHoverEnterAndMoveDelayed(); |
| mSendHoverExitDelayed = new SendHoverExitDelayed(); |
| mSendTouchExplorationEndDelayed = new SendAccessibilityEventDelayed( |
| AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END, |
| mDetermineUserIntentTimeout); |
| mSendTouchInteractionEndDelayed = new SendAccessibilityEventDelayed( |
| AccessibilityEvent.TYPE_TOUCH_INTERACTION_END, |
| mDetermineUserIntentTimeout); |
| if (detector == null) { |
| mGestureDetector = new GestureManifold(context, this, mState); |
| } else { |
| mGestureDetector = detector; |
| } |
| mGestureDetectionPassthroughRegion = new Region(); |
| mTouchExplorationPassthroughRegion = new Region(); |
| } |
| |
| @Override |
| public void clearEvents(int inputSource) { |
| if (inputSource == InputDevice.SOURCE_TOUCHSCREEN) { |
| clear(); |
| } |
| super.clearEvents(inputSource); |
| } |
| |
| @Override |
| public void onDestroy() { |
| clear(); |
| } |
| |
| private void clear() { |
| // If we have not received an event then we are in initial |
| // state. Therefore, there is not need to clean anything. |
| MotionEvent event = mState.getLastReceivedEvent(); |
| if (event != null) { |
| clear(event, WindowManagerPolicy.FLAG_TRUSTED); |
| } |
| } |
| |
| private void clear(MotionEvent event, int policyFlags) { |
| if (mState.isTouchExploring()) { |
| // If a touch exploration gesture is in progress send events for its end. |
| sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); |
| } else if (mState.isDragging()) { |
| mDraggingPointerId = INVALID_POINTER_ID; |
| // Send exit to all pointers that we have delivered. |
| mDispatcher.sendUpForInjectedDownPointers(event, policyFlags); |
| } else if (mState.isDelegating()) { |
| // Send exit to all pointers that we have delivered. |
| mDispatcher.sendUpForInjectedDownPointers(event, policyFlags); |
| } else if (mState.isGestureDetecting()) { |
| // No state specific cleanup required. |
| } |
| // Remove all pending callbacks. |
| mSendHoverEnterAndMoveDelayed.cancel(); |
| mSendHoverExitDelayed.cancel(); |
| mExitGestureDetectionModeDelayed.cancel(); |
| mSendTouchExplorationEndDelayed.cancel(); |
| mSendTouchInteractionEndDelayed.cancel(); |
| // Clear the gesture detector |
| mGestureDetector.clear(); |
| // Go to initial state. |
| mState.clear(); |
| mAms.onTouchInteractionEnd(); |
| } |
| |
| @Override |
| public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) { |
| super.onMotionEvent(event, rawEvent, policyFlags); |
| return; |
| } |
| |
| if (DEBUG) { |
| Slog.d(LOG_TAG, "Received event: " + event + ", policyFlags=0x" |
| + Integer.toHexString(policyFlags)); |
| Slog.d(LOG_TAG, mState.toString()); |
| } |
| |
| mState.onReceivedMotionEvent(rawEvent); |
| if (shouldPerformGestureDetection(event)) { |
| if (mGestureDetector.onMotionEvent(event, rawEvent, policyFlags)) { |
| // Event was handled by the gesture detector. |
| return; |
| } |
| } |
| |
| if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { |
| clear(event, policyFlags); |
| return; |
| } |
| |
| // TODO: extract the below functions into separate handlers for each state. |
| // Right now the number of functions and number of states make the code messy. |
| if (mState.isClear()) { |
| handleMotionEventStateClear(event, rawEvent, policyFlags); |
| } else if (mState.isTouchInteracting()) { |
| handleMotionEventStateTouchInteracting(event, rawEvent, policyFlags); |
| } else if (mState.isTouchExploring()) { |
| handleMotionEventStateTouchExploring(event, rawEvent, policyFlags); |
| } else if (mState.isDragging()) { |
| handleMotionEventStateDragging(event, rawEvent, policyFlags); |
| } else if (mState.isDelegating()) { |
| handleMotionEventStateDelegating(event, rawEvent, policyFlags); |
| } else if (mState.isGestureDetecting()) { |
| // Make sure we don't prematurely get TOUCH_INTERACTION_END |
| // It will be delivered on gesture completion or cancelation. |
| // Note that the delay for sending GESTURE_DETECTION_END remains in place. |
| mSendTouchInteractionEndDelayed.cancel(); |
| } else { |
| Slog.e(LOG_TAG, "Illegal state: " + mState); |
| clear(event, policyFlags); |
| } |
| } |
| |
| @Override |
| public void onAccessibilityEvent(AccessibilityEvent event) { |
| final int eventType = event.getEventType(); |
| |
| if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) { |
| sendsPendingA11yEventsIfNeed(); |
| } |
| mState.onReceivedAccessibilityEvent(event); |
| super.onAccessibilityEvent(event); |
| } |
| |
| /* |
| * Sends pending {@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END} or {@{@link |
| * AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END}} after receiving last hover exit |
| * event. |
| */ |
| private void sendsPendingA11yEventsIfNeed() { |
| // The last hover exit A11y event should be sent by view after receiving hover exit motion |
| // event. In some view hierarchy, the ViewGroup transforms hover move motion event to hover |
| // exit motion event and than dispatch to itself. It causes unexpected A11y exit events. |
| if (mSendHoverExitDelayed.isPending()) { |
| return; |
| } |
| // The event for gesture end should be strictly after the |
| // last hover exit event. |
| if (mSendTouchExplorationEndDelayed.isPending()) { |
| mSendTouchExplorationEndDelayed.cancel(); |
| mDispatcher.sendAccessibilityEvent( |
| AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END); |
| } |
| |
| // The event for touch interaction end should be strictly after the |
| // last hover exit and the touch exploration gesture end events. |
| if (mSendTouchInteractionEndDelayed.isPending()) { |
| mSendTouchInteractionEndDelayed.cancel(); |
| mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); |
| } |
| } |
| |
| @Override |
| public void onDoubleTapAndHold(MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| if (mDispatcher.longPressWithTouchEvents(event, policyFlags)) { |
| sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); |
| mState.startDelegating(); |
| } |
| } |
| |
| @Override |
| public boolean onDoubleTap(MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| mAms.onTouchInteractionEnd(); |
| // Remove pending event deliveries. |
| mSendHoverEnterAndMoveDelayed.cancel(); |
| mSendHoverExitDelayed.cancel(); |
| |
| if (mSendTouchExplorationEndDelayed.isPending()) { |
| mSendTouchExplorationEndDelayed.forceSendAndRemove(); |
| } |
| |
| // Announce the end of a new touch interaction. |
| mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); |
| mSendTouchInteractionEndDelayed.cancel(); |
| // Try to use the standard accessibility API to click |
| if (!mAms.performActionOnAccessibilityFocusedItem( |
| AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)) { |
| Slog.e(LOG_TAG, "ACTION_CLICK failed. Dispatching motion events to simulate click."); |
| |
| mDispatcher.clickWithTouchEvents(event, rawEvent, policyFlags); |
| return true; |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onGestureStarted() { |
| // We have to perform gesture detection, so |
| // clear the current state and try to detect. |
| mState.startGestureDetecting(); |
| mSendHoverEnterAndMoveDelayed.cancel(); |
| mSendHoverExitDelayed.cancel(); |
| mExitGestureDetectionModeDelayed.post(); |
| // Send accessibility event to announce the start |
| // of gesture recognition. |
| mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_START); |
| return false; |
| } |
| |
| @Override |
| public boolean onGestureCompleted(AccessibilityGestureEvent gestureEvent) { |
| endGestureDetection(true); |
| mSendTouchInteractionEndDelayed.cancel(); |
| mAms.onGesture(gestureEvent); |
| |
| return true; |
| } |
| |
| @Override |
| public boolean onGestureCancelled(MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| if (mState.isGestureDetecting()) { |
| endGestureDetection(event.getActionMasked() == MotionEvent.ACTION_UP); |
| return true; |
| } else if (mState.isTouchExploring()) { |
| // If the finger is still moving, pass the event on. |
| if (event.getActionMasked() == MotionEvent.ACTION_MOVE) { |
| final int pointerId = mReceivedPointerTracker.getPrimaryPointerId(); |
| final int pointerIdBits = (1 << pointerId); |
| |
| // We have just decided that the user is touch, |
| // exploring so start sending events. |
| mSendHoverEnterAndMoveDelayed.addEvent(event, mState.getLastReceivedEvent()); |
| mSendHoverEnterAndMoveDelayed.forceSendAndRemove(); |
| mSendHoverExitDelayed.cancel(); |
| mDispatcher.sendMotionEvent( |
| event, |
| MotionEvent.ACTION_HOVER_MOVE, |
| mState.getLastReceivedEvent(), |
| pointerIdBits, |
| policyFlags); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Handles a motion event in the clear state i.e. no fingers are touching the screen. |
| */ |
| private void handleMotionEventStateClear( |
| MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| switch (event.getActionMasked()) { |
| // The only way to leave the clear state is for a pointer to go down. |
| case MotionEvent.ACTION_DOWN: |
| handleActionDown(event, rawEvent, policyFlags); |
| break; |
| default: |
| // Some other nonsensical event. |
| break; |
| } |
| } |
| |
| /** |
| * Handles ACTION_DOWN while in the clear or touch interacting states. This event represents the |
| * first finger touching the screen. |
| */ |
| private void handleActionDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| mAms.onTouchInteractionStart(); |
| |
| // If we still have not notified the user for the last |
| // touch, we figure out what to do. If were waiting |
| // we resent the delayed callback and wait again. |
| mSendHoverEnterAndMoveDelayed.cancel(); |
| mSendHoverExitDelayed.cancel(); |
| // If a touch exploration gesture is in progress send events for its end. |
| if (mState.isTouchExploring()) { |
| sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); |
| } |
| |
| if (mState.isClear()) { |
| if (!mSendHoverEnterAndMoveDelayed.isPending()) { |
| // Queue a delayed transition to STATE_TOUCH_EXPLORING. |
| // If we do not detect that this is a gesture, delegation or drag the transition |
| // will fire by default. |
| // The idea is to avoid getting stuck in STATE_TOUCH_INTERACTING |
| final int pointerId = mReceivedPointerTracker.getPrimaryPointerId(); |
| final int pointerIdBits = (1 << pointerId); |
| mSendHoverEnterAndMoveDelayed.post(event, rawEvent, pointerIdBits, policyFlags); |
| } else { |
| // Cache the event until we discern exploration from gesturing. |
| mSendHoverEnterAndMoveDelayed.addEvent(event, rawEvent); |
| } |
| mSendTouchExplorationEndDelayed.forceSendAndRemove(); |
| mSendTouchInteractionEndDelayed.forceSendAndRemove(); |
| mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_START); |
| if (mTouchExplorationPassthroughRegion.contains( |
| (int) event.getX(), (int) event.getY())) { |
| // The touch exploration passthrough overrides the gesture detection passthrough in |
| // the event they overlap. |
| // Pass this entire gesture through to the system as-is. |
| mState.startDelegating(); |
| event = MotionEvent.obtainNoHistory(event); |
| mDispatcher.sendMotionEvent( |
| event, event.getAction(), rawEvent, ALL_POINTER_ID_BITS, policyFlags); |
| mSendHoverEnterAndMoveDelayed.cancel(); |
| } else if (mGestureDetectionPassthroughRegion.contains( |
| (int) event.getX(), (int) event.getY())) { |
| // Jump straight to touch exploration. |
| mSendHoverEnterAndMoveDelayed.forceSendAndRemove(); |
| } |
| } else { |
| // Avoid duplicated TYPE_TOUCH_INTERACTION_START event when 2nd tap of double tap. |
| mSendTouchInteractionEndDelayed.cancel(); |
| } |
| } |
| |
| /** |
| * Handles a motion event in touch interacting state. |
| * |
| * @param event The event to be handled. |
| * @param rawEvent The raw (unmodified) motion event. |
| * @param policyFlags The policy flags associated with the event. |
| */ |
| private void handleMotionEventStateTouchInteracting( |
| MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: |
| // Continue the previous interaction. |
| mSendTouchInteractionEndDelayed.cancel(); |
| handleActionDown(event, rawEvent, policyFlags); |
| break; |
| case MotionEvent.ACTION_POINTER_DOWN: |
| handleActionPointerDown(event, rawEvent, policyFlags); |
| break; |
| case MotionEvent.ACTION_MOVE: |
| handleActionMoveStateTouchInteracting(event, rawEvent, policyFlags); |
| break; |
| case MotionEvent.ACTION_UP: |
| handleActionUp(event, rawEvent, policyFlags); |
| break; |
| } |
| } |
| |
| /** |
| * Handles a motion event in touch exploring state. |
| * |
| * @param event The event to be handled. |
| * @param rawEvent The raw (unmodified) motion event. |
| * @param policyFlags The policy flags associated with the event. |
| */ |
| private void handleMotionEventStateTouchExploring( |
| MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: |
| // We should have already received ACTION_DOWN. Ignore. |
| break; |
| case MotionEvent.ACTION_POINTER_DOWN: |
| handleActionPointerDown(event, rawEvent, policyFlags); |
| break; |
| case MotionEvent.ACTION_MOVE: |
| handleActionMoveStateTouchExploring(event, rawEvent, policyFlags); |
| break; |
| case MotionEvent.ACTION_UP: |
| handleActionUp(event, rawEvent, policyFlags); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * Handles ACTION_POINTER_DOWN when in the touch exploring state. This event represents an |
| * additional finger touching the screen. |
| */ |
| private void handleActionPointerDown(MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| // Another finger down means that if we have not started to deliver |
| // hover events, we will not have to. The code for ACTION_MOVE will |
| // decide what we will actually do next. |
| |
| if (mSendHoverEnterAndMoveDelayed.isPending()) { |
| mSendHoverEnterAndMoveDelayed.cancel(); |
| mSendHoverExitDelayed.cancel(); |
| } else { |
| // We have already delivered at least one hover event, so send hover exit to keep the |
| // stream consistent. |
| sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); |
| } |
| } |
| /** |
| * Handles ACTION_MOVE while in the touch interacting state. This is where transitions to |
| * delegating and dragging states are handled. |
| */ |
| private void handleActionMoveStateTouchInteracting( |
| MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| final int pointerId = mReceivedPointerTracker.getPrimaryPointerId(); |
| final int pointerIndex = event.findPointerIndex(pointerId); |
| final int pointerIdBits = (1 << pointerId); |
| switch (event.getPointerCount()) { |
| case 1: |
| // We have not started sending events since we try to |
| // figure out what the user is doing. |
| if (mSendHoverEnterAndMoveDelayed.isPending()) { |
| // Cache the event until we discern exploration from gesturing. |
| mSendHoverEnterAndMoveDelayed.addEvent(event, rawEvent); |
| } |
| break; |
| case 2: |
| if (mGestureDetector.isMultiFingerGesturesEnabled()) { |
| return; |
| } |
| // Make sure we don't have any pending transitions to touch exploration |
| mSendHoverEnterAndMoveDelayed.cancel(); |
| mSendHoverExitDelayed.cancel(); |
| // More than one pointer so the user is not touch exploring |
| // and now we have to decide whether to delegate or drag. |
| // Remove move history before send injected non-move events |
| event = MotionEvent.obtainNoHistory(event); |
| if (isDraggingGesture(event)) { |
| // Two pointers moving in the same direction within |
| // a given distance perform a drag. |
| mState.startDragging(); |
| mDraggingPointerId = pointerId; |
| adjustEventLocationForDrag(event); |
| event.setEdgeFlags(mReceivedPointerTracker.getLastReceivedDownEdgeFlags()); |
| mDispatcher.sendMotionEvent( |
| event, MotionEvent.ACTION_DOWN, rawEvent, pointerIdBits, policyFlags); |
| } else { |
| // Two pointers moving arbitrary are delegated to the view hierarchy. |
| mState.startDelegating(); |
| mDispatcher.sendDownForAllNotInjectedPointers(event, policyFlags); |
| } |
| break; |
| default: |
| if (mGestureDetector.isMultiFingerGesturesEnabled()) { |
| return; |
| } |
| // More than two pointers are delegated to the view hierarchy. |
| mState.startDelegating(); |
| event = MotionEvent.obtainNoHistory(event); |
| mDispatcher.sendDownForAllNotInjectedPointers(event, policyFlags); |
| break; |
| } |
| } |
| |
| /** |
| * Handles ACTION_UP while in the touch interacting state. This event represents all fingers |
| * being lifted from the screen. |
| */ |
| private void handleActionUp(MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| mAms.onTouchInteractionEnd(); |
| final int pointerId = event.getPointerId(event.getActionIndex()); |
| final int pointerIdBits = (1 << pointerId); |
| if (mSendHoverEnterAndMoveDelayed.isPending()) { |
| // If we have not delivered the enter schedule an exit. |
| mSendHoverExitDelayed.post(event, rawEvent, pointerIdBits, policyFlags); |
| } else { |
| // The user is touch exploring so we send events for end. |
| sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); |
| } |
| if (!mSendTouchInteractionEndDelayed.isPending()) { |
| mSendTouchInteractionEndDelayed.post(); |
| } |
| } |
| |
| /** |
| * Handles move events while touch exploring. this is also where we drag or delegate based on |
| * the number of fingers moving on the screen. |
| */ |
| private void handleActionMoveStateTouchExploring( |
| MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| final int pointerId = mReceivedPointerTracker.getPrimaryPointerId(); |
| final int pointerIdBits = (1 << pointerId); |
| final int pointerIndex = event.findPointerIndex(pointerId); |
| switch (event.getPointerCount()) { |
| case 1: |
| // Touch exploration. |
| sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags); |
| mDispatcher.sendMotionEvent( |
| event, MotionEvent.ACTION_HOVER_MOVE, rawEvent, pointerIdBits, policyFlags); |
| break; |
| case 2: |
| if (mGestureDetector.isMultiFingerGesturesEnabled()) { |
| return; |
| } |
| if (mSendHoverEnterAndMoveDelayed.isPending()) { |
| // We have not started sending events so cancel |
| // scheduled sending events. |
| mSendHoverEnterAndMoveDelayed.cancel(); |
| mSendHoverExitDelayed.cancel(); |
| } |
| // If the user is touch exploring the second pointer may be |
| // performing a double tap to activate an item without need |
| // for the user to lift his exploring finger. |
| // It is *important* to use the distance traveled by the pointers |
| // on the screen which may or may not be magnified. |
| final float deltaX = |
| mReceivedPointerTracker.getReceivedPointerDownX(pointerId) |
| - rawEvent.getX(pointerIndex); |
| final float deltaY = |
| mReceivedPointerTracker.getReceivedPointerDownY(pointerId) |
| - rawEvent.getY(pointerIndex); |
| final double moveDelta = Math.hypot(deltaX, deltaY); |
| if (moveDelta > mDoubleTapSlop) { |
| // The user is trying to either delegate or drag. |
| handleActionMoveStateTouchInteracting(event, rawEvent, policyFlags); |
| } else { |
| // Otherwise the double tap will be handled by the gesture detector. |
| sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); |
| } |
| break; |
| default: |
| if (mGestureDetector.isMultiFingerGesturesEnabled()) { |
| return; |
| } |
| // Three or more fingers is something other than touch exploration. |
| if (mSendHoverEnterAndMoveDelayed.isPending()) { |
| // We have not started sending events so cancel |
| // scheduled sending events. |
| mSendHoverEnterAndMoveDelayed.cancel(); |
| mSendHoverExitDelayed.cancel(); |
| } else { |
| sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); |
| } |
| handleActionMoveStateTouchInteracting(event, rawEvent, policyFlags); |
| break; |
| } |
| } |
| |
| /** |
| * Handles a motion event in dragging state. |
| * |
| * @param event The event to be handled. |
| * @param policyFlags The policy flags associated with the event. |
| */ |
| private void handleMotionEventStateDragging( |
| MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| if (mGestureDetector.isMultiFingerGesturesEnabled()) { |
| // Multi-finger gestures conflict with this functionality. |
| return; |
| } |
| int pointerIdBits = 0; |
| // Clear the dragging pointer id if it's no longer valid. |
| if (event.findPointerIndex(mDraggingPointerId) == -1) { |
| Slog.e(LOG_TAG, "mDraggingPointerId doesn't match any pointers on current event. " + |
| "mDraggingPointerId: " + Integer.toString(mDraggingPointerId) + |
| ", Event: " + event); |
| mDraggingPointerId = INVALID_POINTER_ID; |
| } else { |
| pointerIdBits = (1 << mDraggingPointerId); |
| } |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: { |
| Slog.e(LOG_TAG, "Dragging state can be reached only if two " |
| + "pointers are already down"); |
| clear(event, policyFlags); |
| return; |
| } |
| case MotionEvent.ACTION_POINTER_DOWN: { |
| // We are in dragging state so we have two pointers and another one |
| // goes down => delegate the three pointers to the view hierarchy |
| mState.startDelegating(); |
| if (mDraggingPointerId != INVALID_POINTER_ID) { |
| mDispatcher.sendMotionEvent( |
| event, MotionEvent.ACTION_UP, rawEvent, pointerIdBits, policyFlags); |
| } |
| mDispatcher.sendDownForAllNotInjectedPointers(event, policyFlags); |
| } break; |
| case MotionEvent.ACTION_MOVE: { |
| if (mDraggingPointerId == INVALID_POINTER_ID) { |
| break; |
| } |
| switch (event.getPointerCount()) { |
| case 1: { |
| // do nothing |
| } break; |
| case 2: { |
| if (isDraggingGesture(event)) { |
| // If still dragging send a drag event. |
| adjustEventLocationForDrag(event); |
| mDispatcher.sendMotionEvent( |
| event, |
| MotionEvent.ACTION_MOVE, |
| rawEvent, |
| pointerIdBits, |
| policyFlags); |
| } else { |
| // The two pointers are moving either in different directions or |
| // no close enough => delegate the gesture to the view hierarchy. |
| mState.startDelegating(); |
| // Remove move history before send injected non-move events |
| event = MotionEvent.obtainNoHistory(event); |
| // Send an event to the end of the drag gesture. |
| mDispatcher.sendMotionEvent( |
| event, |
| MotionEvent.ACTION_UP, |
| rawEvent, |
| pointerIdBits, |
| policyFlags); |
| // Deliver all pointers to the view hierarchy. |
| mDispatcher.sendDownForAllNotInjectedPointers(event, policyFlags); |
| } |
| } break; |
| default: { |
| mState.startDelegating(); |
| event = MotionEvent.obtainNoHistory(event); |
| // Send an event to the end of the drag gesture. |
| mDispatcher.sendMotionEvent( |
| event, |
| MotionEvent.ACTION_UP, |
| rawEvent, |
| pointerIdBits, |
| policyFlags); |
| // Deliver all pointers to the view hierarchy. |
| mDispatcher.sendDownForAllNotInjectedPointers(event, policyFlags); |
| } |
| } |
| } break; |
| case MotionEvent.ACTION_POINTER_UP: { |
| final int pointerId = event.getPointerId(event.getActionIndex()); |
| if (pointerId == mDraggingPointerId) { |
| mDraggingPointerId = INVALID_POINTER_ID; |
| // Send an event to the end of the drag gesture. |
| mDispatcher.sendMotionEvent( |
| event, MotionEvent.ACTION_UP, rawEvent, pointerIdBits, policyFlags); |
| } |
| } break; |
| case MotionEvent.ACTION_UP: { |
| mAms.onTouchInteractionEnd(); |
| // Announce the end of a new touch interaction. |
| mDispatcher.sendAccessibilityEvent( |
| AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); |
| final int pointerId = event.getPointerId(event.getActionIndex()); |
| if (pointerId == mDraggingPointerId) { |
| mDraggingPointerId = INVALID_POINTER_ID; |
| // Send an event to the end of the drag gesture. |
| mDispatcher.sendMotionEvent( |
| event, MotionEvent.ACTION_UP, rawEvent, pointerIdBits, policyFlags); |
| } |
| } break; |
| } |
| } |
| |
| /** |
| * Handles a motion event in delegating state. |
| * |
| * @param event The event to be handled. |
| * @param policyFlags The policy flags associated with the event. |
| */ |
| private void handleMotionEventStateDelegating( |
| MotionEvent event, MotionEvent rawEvent, int policyFlags) { |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: { |
| Slog.e(LOG_TAG, "Delegating state can only be reached if " |
| + "there is at least one pointer down!"); |
| clear(event, policyFlags); |
| return; |
| } |
| case MotionEvent.ACTION_UP: { |
| // Deliver the event. |
| mDispatcher.sendMotionEvent( |
| event, event.getAction(), rawEvent, ALL_POINTER_ID_BITS, policyFlags); |
| |
| // Announce the end of a the touch interaction. |
| mAms.onTouchInteractionEnd(); |
| mDispatcher.clear(); |
| mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); |
| |
| } break; |
| default: { |
| // Deliver the event. |
| mDispatcher.sendMotionEvent( |
| event, event.getAction(), rawEvent, ALL_POINTER_ID_BITS, policyFlags); |
| } |
| } |
| } |
| |
| private void endGestureDetection(boolean interactionEnd) { |
| mAms.onTouchInteractionEnd(); |
| |
| // Announce the end of the gesture recognition. |
| mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END); |
| // Don't announce the end of a the touch interaction if users didn't lift their fingers. |
| if (interactionEnd) { |
| mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); |
| } |
| |
| mExitGestureDetectionModeDelayed.cancel(); |
| } |
| |
| |
| /** |
| * Sends the exit events if needed. Such events are hover exit and touch explore |
| * gesture end. |
| * |
| * @param policyFlags The policy flags associated with the event. |
| */ |
| private void sendHoverExitAndTouchExplorationGestureEndIfNeeded(int policyFlags) { |
| MotionEvent event = mState.getLastInjectedHoverEvent(); |
| if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) { |
| final int pointerIdBits = event.getPointerIdBits(); |
| if (!mSendTouchExplorationEndDelayed.isPending()) { |
| mSendTouchExplorationEndDelayed.post(); |
| } |
| mDispatcher.sendMotionEvent( |
| event, |
| MotionEvent.ACTION_HOVER_EXIT, |
| mState.getLastReceivedEvent(), |
| pointerIdBits, |
| policyFlags); |
| } |
| } |
| |
| /** |
| * Sends the enter events if needed. Such events are hover enter and touch explore |
| * gesture start. |
| * |
| * @param policyFlags The policy flags associated with the event. |
| */ |
| private void sendTouchExplorationGestureStartAndHoverEnterIfNeeded(int policyFlags) { |
| MotionEvent event = mState.getLastInjectedHoverEvent(); |
| if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) { |
| final int pointerIdBits = event.getPointerIdBits(); |
| mDispatcher.sendMotionEvent( |
| event, |
| MotionEvent.ACTION_HOVER_ENTER, |
| mState.getLastReceivedEvent(), |
| pointerIdBits, |
| policyFlags); |
| } |
| } |
| |
| |
| /** |
| * Determines whether a two pointer gesture is a dragging one. |
| * |
| * @param event The event with the pointer data. |
| * @return True if the gesture is a dragging one. |
| */ |
| private boolean isDraggingGesture(MotionEvent event) { |
| |
| final float firstPtrX = event.getX(0); |
| final float firstPtrY = event.getY(0); |
| final float secondPtrX = event.getX(1); |
| final float secondPtrY = event.getY(1); |
| |
| final float firstPtrDownX = mReceivedPointerTracker.getReceivedPointerDownX(0); |
| final float firstPtrDownY = mReceivedPointerTracker.getReceivedPointerDownY(0); |
| final float secondPtrDownX = mReceivedPointerTracker.getReceivedPointerDownX(1); |
| final float secondPtrDownY = mReceivedPointerTracker.getReceivedPointerDownY(1); |
| |
| return GestureUtils.isDraggingGesture(firstPtrDownX, firstPtrDownY, secondPtrDownX, |
| secondPtrDownY, firstPtrX, firstPtrY, secondPtrX, secondPtrY, |
| MAX_DRAGGING_ANGLE_COS); |
| } |
| |
| /** |
| * Adjust the location of an injected event when performing a drag The new location will be in |
| * between the two fingers touching the screen. |
| */ |
| private void adjustEventLocationForDrag(MotionEvent event) { |
| |
| final float firstPtrX = event.getX(0); |
| final float firstPtrY = event.getY(0); |
| final float secondPtrX = event.getX(1); |
| final float secondPtrY = event.getY(1); |
| final int pointerIndex = event.findPointerIndex(mDraggingPointerId); |
| final float deltaX = |
| (pointerIndex == 0) ? (secondPtrX - firstPtrX) : (firstPtrX - secondPtrX); |
| final float deltaY = |
| (pointerIndex == 0) ? (secondPtrY - firstPtrY) : (firstPtrY - secondPtrY); |
| event.offsetLocation(deltaX / 2, deltaY / 2); |
| } |
| |
| public TouchState getState() { |
| return mState; |
| } |
| |
| @Override |
| public void setNext(EventStreamTransformation next) { |
| mDispatcher.setReceiver(next); |
| super.setNext(next); |
| } |
| |
| /** |
| * Whether to dispatch double tap and double tap and hold to the service rather than handle them |
| * in the framework. |
| */ |
| public void setServiceHandlesDoubleTap(boolean mode) { |
| mGestureDetector.setServiceHandlesDoubleTap(mode); |
| } |
| |
| /** |
| * This function turns on and off multi-finger gestures. When enabled, multi-finger gestures |
| * will disable delegating and dragging functionality. |
| */ |
| public void setMultiFingerGesturesEnabled(boolean enabled) { |
| mGestureDetector.setMultiFingerGesturesEnabled(enabled); |
| } |
| |
| public void setGestureDetectionPassthroughRegion(Region region) { |
| mGestureDetectionPassthroughRegion = region; |
| } |
| |
| public void setTouchExplorationPassthroughRegion(Region region) { |
| mTouchExplorationPassthroughRegion = region; |
| } |
| |
| private boolean shouldPerformGestureDetection(MotionEvent event) { |
| if (mState.isDelegating()) { |
| return false; |
| } |
| if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { |
| final int x = (int) event.getX(); |
| final int y = (int) event.getY(); |
| if (mTouchExplorationPassthroughRegion.contains(x, y) |
| || mGestureDetectionPassthroughRegion.contains(x, y)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Class for delayed exiting from gesture detecting mode. |
| */ |
| private final class ExitGestureDetectionModeDelayed implements Runnable { |
| |
| public void post() { |
| mHandler.postDelayed(this, EXIT_GESTURE_DETECTION_TIMEOUT); |
| } |
| |
| public void cancel() { |
| mHandler.removeCallbacks(this); |
| } |
| |
| @Override |
| public void run() { |
| // Announce the end of gesture recognition. |
| mDispatcher.sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END); |
| clear(); |
| } |
| } |
| |
| /** |
| * Class for delayed sending of hover enter and move events. |
| */ |
| class SendHoverEnterAndMoveDelayed implements Runnable { |
| private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverEnterAndMoveDelayed"; |
| |
| private final List<MotionEvent> mEvents = new ArrayList<MotionEvent>(); |
| private final List<MotionEvent> mRawEvents = new ArrayList<MotionEvent>(); |
| |
| private int mPointerIdBits; |
| private int mPolicyFlags; |
| |
| public void post( |
| MotionEvent event, MotionEvent rawEvent, int pointerIdBits, int policyFlags) { |
| cancel(); |
| addEvent(event, rawEvent); |
| mPointerIdBits = pointerIdBits; |
| mPolicyFlags = policyFlags; |
| mHandler.postDelayed(this, mDetermineUserIntentTimeout); |
| } |
| |
| public void addEvent(MotionEvent event, MotionEvent rawEvent) { |
| mEvents.add(MotionEvent.obtain(event)); |
| mRawEvents.add(MotionEvent.obtain(rawEvent)); |
| } |
| |
| public void cancel() { |
| if (isPending()) { |
| mHandler.removeCallbacks(this); |
| clear(); |
| } |
| } |
| |
| private boolean isPending() { |
| return mHandler.hasCallbacks(this); |
| } |
| |
| private void clear() { |
| mPointerIdBits = -1; |
| mPolicyFlags = 0; |
| final int eventCount = mEvents.size(); |
| for (int i = eventCount - 1; i >= 0; i--) { |
| mEvents.remove(i).recycle(); |
| } |
| final int rawEventcount = mRawEvents.size(); |
| for (int i = rawEventcount - 1; i >= 0; i--) { |
| mRawEvents.remove(i).recycle(); |
| } |
| } |
| |
| public void forceSendAndRemove() { |
| if (isPending()) { |
| run(); |
| cancel(); |
| } |
| } |
| |
| public void run() { |
| // Send an accessibility event to announce the touch exploration start. |
| mDispatcher.sendAccessibilityEvent( |
| AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); |
| |
| if (!mEvents.isEmpty() && !mRawEvents.isEmpty()) { |
| // Deliver a down event. |
| mDispatcher.sendMotionEvent(mEvents.get(0), MotionEvent.ACTION_HOVER_ENTER, |
| mRawEvents.get(0), mPointerIdBits, mPolicyFlags); |
| if (DEBUG) { |
| Slog.d(LOG_TAG_SEND_HOVER_DELAYED, |
| "Injecting motion event: ACTION_HOVER_ENTER"); |
| } |
| |
| // Deliver move events. |
| final int eventCount = mEvents.size(); |
| for (int i = 1; i < eventCount; i++) { |
| mDispatcher.sendMotionEvent(mEvents.get(i), MotionEvent.ACTION_HOVER_MOVE, |
| mRawEvents.get(i), mPointerIdBits, mPolicyFlags); |
| if (DEBUG) { |
| Slog.d(LOG_TAG_SEND_HOVER_DELAYED, |
| "Injecting motion event: ACTION_HOVER_MOVE"); |
| } |
| } |
| } |
| clear(); |
| } |
| } |
| |
| /** |
| * Class for delayed sending of hover exit events. |
| */ |
| class SendHoverExitDelayed implements Runnable { |
| private final String LOG_TAG_SEND_HOVER_DELAYED = "SendHoverExitDelayed"; |
| |
| private MotionEvent mPrototype; |
| private MotionEvent mRawEvent; |
| private int mPointerIdBits; |
| private int mPolicyFlags; |
| |
| public void post( |
| MotionEvent prototype, MotionEvent rawEvent, int pointerIdBits, int policyFlags) { |
| cancel(); |
| mPrototype = MotionEvent.obtain(prototype); |
| mRawEvent = MotionEvent.obtain(rawEvent); |
| mPointerIdBits = pointerIdBits; |
| mPolicyFlags = policyFlags; |
| mHandler.postDelayed(this, mDetermineUserIntentTimeout); |
| } |
| |
| public void cancel() { |
| if (isPending()) { |
| mHandler.removeCallbacks(this); |
| clear(); |
| } |
| } |
| |
| private boolean isPending() { |
| return mHandler.hasCallbacks(this); |
| } |
| |
| private void clear() { |
| if (mPrototype != null) { |
| mPrototype.recycle(); |
| } |
| if (mRawEvent != null) { |
| mRawEvent.recycle(); |
| } |
| mPrototype = null; |
| mRawEvent = null; |
| mPointerIdBits = -1; |
| mPolicyFlags = 0; |
| } |
| |
| public void forceSendAndRemove() { |
| if (isPending()) { |
| run(); |
| cancel(); |
| } |
| } |
| |
| public void run() { |
| if (DEBUG) { |
| Slog.d(LOG_TAG_SEND_HOVER_DELAYED, "Injecting motion event:" |
| + " ACTION_HOVER_EXIT"); |
| } |
| mDispatcher.sendMotionEvent( |
| mPrototype, |
| MotionEvent.ACTION_HOVER_EXIT, |
| mRawEvent, |
| mPointerIdBits, |
| mPolicyFlags); |
| if (!mSendTouchExplorationEndDelayed.isPending()) { |
| mSendTouchExplorationEndDelayed.cancel(); |
| mSendTouchExplorationEndDelayed.post(); |
| } |
| if (mSendTouchInteractionEndDelayed.isPending()) { |
| mSendTouchInteractionEndDelayed.cancel(); |
| mSendTouchInteractionEndDelayed.post(); |
| } |
| clear(); |
| } |
| } |
| |
| private class SendAccessibilityEventDelayed implements Runnable { |
| private final int mEventType; |
| private final int mDelay; |
| |
| public SendAccessibilityEventDelayed(int eventType, int delay) { |
| mEventType = eventType; |
| mDelay = delay; |
| } |
| |
| public void cancel() { |
| mHandler.removeCallbacks(this); |
| } |
| |
| public void post() { |
| mHandler.postDelayed(this, mDelay); |
| } |
| |
| public boolean isPending() { |
| return mHandler.hasCallbacks(this); |
| } |
| |
| public void forceSendAndRemove() { |
| if (isPending()) { |
| run(); |
| cancel(); |
| } |
| } |
| |
| @Override |
| public void run() { |
| mDispatcher.sendAccessibilityEvent(mEventType); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "TouchExplorer { " |
| + "mTouchState: " + mState |
| + ", mDetermineUserIntentTimeout: " + mDetermineUserIntentTimeout |
| + ", mDoubleTapSlop: " + mDoubleTapSlop |
| + ", mDraggingPointerId: " + mDraggingPointerId |
| + " }"; |
| } |
| } |