/* ** 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; import android.content.Context; import android.graphics.Point; import android.os.Handler; import android.util.Slog; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; import android.view.ViewConfiguration; import android.view.WindowManagerPolicy; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import java.util.ArrayList; import java.util.Arrays; 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: * *
    *
  1. 1. One finger moving slow around performs touch exploration.
  2. *
  3. 2. One finger moving fast around performs gestures.
  4. *
  5. 3. Two close fingers moving in the same direction perform a drag.
  6. *
  7. 4. Multi-finger gestures are delivered to view hierarchy.
  8. *
  9. 5. Two fingers moving in different directions are considered a multi-finger gesture.
  10. *
  11. 7. Double tapping clicks on the on the last touch explored location if it was in * a window that does not take focus, otherwise the click is within the accessibility * focused rectangle.
  12. *
  13. 7. Tapping and holding for a while performs a long press in a similar fashion * as the click above.
  14. *
      * * @hide */ class TouchExplorer implements EventStreamTransformation, AccessibilityGestureDetector.Listener { private static final boolean DEBUG = false; // Tag for logging received events. private static final String LOG_TAG = "TouchExplorer"; // States this explorer can be in. private static final int STATE_TOUCH_EXPLORING = 0x00000001; private static final int STATE_DRAGGING = 0x00000002; private static final int STATE_DELEGATING = 0x00000004; private static final int STATE_GESTURE_DETECTING = 0x00000005; private static final int CLICK_LOCATION_NONE = 0; private static final int CLICK_LOCATION_ACCESSIBILITY_FOCUS = 1; private static final int CLICK_LOCATION_LAST_TOUCH_EXPLORED = 2; // 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) // Constant referring to the ids bits of all pointers. private static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF; // This constant captures the current implementation detail that // pointer IDs are between 0 and 31 inclusive (subject to change). // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) private static final int MAX_POINTER_COUNT = 32; // Invalid pointer ID. private static final int INVALID_POINTER_ID = -1; // The minimal distance before we take the middle of the distance between // the two dragging pointers as opposed to use the location of the primary one. private static final int MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP = 200; // 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 int mCurrentState = STATE_TOUCH_EXPLORING; // 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 AccessibilityGestureDetector mGestureDetector; // The scaled minimal distance before we take the middle of the distance between // the two dragging pointers as opposed to use the location of the primary one. private final int mScaledMinPointerDistanceToUseMiddleLocation; // The handler to which to delegate events. private EventStreamTransformation mNext; // Helper class to track received pointers. private final ReceivedPointerTracker mReceivedPointerTracker; // Helper class to track injected pointers. private final InjectedPointerTracker mInjectedPointerTracker; // Handle to the accessibility manager service. private final AccessibilityManagerService mAms; // Temporary point to avoid instantiation. private final Point mTempPoint = new Point(); // Context in which this explorer operates. private final Context mContext; // The long pressing pointer id if coordinate remapping is needed. private int mLongPressingPointerId = -1; // The long pressing pointer X if coordinate remapping is needed. private int mLongPressingPointerDeltaX; // The long pressing pointer Y if coordinate remapping is needed. private int mLongPressingPointerDeltaY; // The id of the last touch explored window. private int mLastTouchedWindowId; // Whether touch exploration is in progress. private boolean mTouchExplorationInProgress; /** * Creates a new instance. * * @param inputFilter The input filter associated with this explorer. * @param context A context handle for accessing resources. */ public TouchExplorer(Context context, AccessibilityManagerService service) { mContext = context; mAms = service; mReceivedPointerTracker = new ReceivedPointerTracker(); mInjectedPointerTracker = new InjectedPointerTracker(); 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); mGestureDetector = new AccessibilityGestureDetector(context, this); final float density = context.getResources().getDisplayMetrics().density; mScaledMinPointerDistanceToUseMiddleLocation = (int) (MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP * density); } @Override public void clearEvents(int inputSource) { if (inputSource == InputDevice.SOURCE_TOUCHSCREEN) { clear(); } if (mNext != null) { mNext.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 = mReceivedPointerTracker.getLastReceivedEvent(); if (event != null) { clear(mReceivedPointerTracker.getLastReceivedEvent(), WindowManagerPolicy.FLAG_TRUSTED); } } private void clear(MotionEvent event, int policyFlags) { switch (mCurrentState) { case STATE_TOUCH_EXPLORING: { // If a touch exploration gesture is in progress send events for its end. sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); } break; case STATE_DRAGGING: { mDraggingPointerId = INVALID_POINTER_ID; // Send exit to all pointers that we have delivered. sendUpForInjectedDownPointers(event, policyFlags); } break; case STATE_DELEGATING: { // Send exit to all pointers that we have delivered. sendUpForInjectedDownPointers(event, policyFlags); } break; case STATE_GESTURE_DETECTING: { // No state specific cleanup required. } break; } // Remove all pending callbacks. mSendHoverEnterAndMoveDelayed.cancel(); mSendHoverExitDelayed.cancel(); mExitGestureDetectionModeDelayed.cancel(); mSendTouchExplorationEndDelayed.cancel(); mSendTouchInteractionEndDelayed.cancel(); // Reset the pointer trackers. mReceivedPointerTracker.clear(); mInjectedPointerTracker.clear(); // Clear the gesture detector mGestureDetector.clear(); // Go to initial state. // Clear the long pressing pointer remap data. mLongPressingPointerId = -1; mLongPressingPointerDeltaX = 0; mLongPressingPointerDeltaY = 0; mCurrentState = STATE_TOUCH_EXPLORING; mTouchExplorationInProgress = false; mAms.onTouchInteractionEnd(); } @Override public void setNext(EventStreamTransformation next) { mNext = next; } @Override public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) { if (mNext != null) { mNext.onMotionEvent(event, rawEvent, policyFlags); } return; } if (DEBUG) { Slog.d(LOG_TAG, "Received event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); Slog.d(LOG_TAG, getStateSymbolicName(mCurrentState)); } mReceivedPointerTracker.onMotionEvent(rawEvent); // The motion detector is interested in the movements in physical space, // so it uses the rawEvent to ignore magnification and other // transformations. if (mGestureDetector.onMotionEvent(rawEvent, policyFlags)) { // Event was handled by the gesture detector. return; } if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { clear(event, policyFlags); return; } switch(mCurrentState) { case STATE_TOUCH_EXPLORING: { handleMotionEventStateTouchExploring(event, rawEvent, policyFlags); } break; case STATE_DRAGGING: { handleMotionEventStateDragging(event, policyFlags); } break; case STATE_DELEGATING: { handleMotionEventStateDelegating(event, policyFlags); } break; case STATE_GESTURE_DETECTING: { // Already handled. } break; default: throw new IllegalStateException("Illegal state: " + mCurrentState); } } @Override public void onKeyEvent(KeyEvent event, int policyFlags) { if (mNext != null) { mNext.onKeyEvent(event, policyFlags); } } @Override public void onAccessibilityEvent(AccessibilityEvent event) { final int eventType = event.getEventType(); // The event for gesture end should be strictly after the // last hover exit event. if (mSendTouchExplorationEndDelayed.isPending() && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) { mSendTouchExplorationEndDelayed.cancel(); 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() && eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) { mSendTouchInteractionEndDelayed.cancel(); sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); } // If a new window opens or the accessibility focus moves we no longer // want to click/long press on the last touch explored location. switch (eventType) { case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: { if (mInjectedPointerTracker.mLastInjectedHoverEventForClick != null) { mInjectedPointerTracker.mLastInjectedHoverEventForClick.recycle(); mInjectedPointerTracker.mLastInjectedHoverEventForClick = null; } mLastTouchedWindowId = -1; } break; case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: { mLastTouchedWindowId = event.getWindowId(); } break; } if (mNext != null) { mNext.onAccessibilityEvent(event); } } @Override public void onDoubleTapAndHold(MotionEvent event, int policyFlags) { // Ignore the event if we aren't touch exploring. if (mCurrentState != STATE_TOUCH_EXPLORING) { return; } // Pointers should not be zero when running this command. if (mReceivedPointerTracker.getLastReceivedEvent().getPointerCount() == 0) { return; } final int pointerIndex = event.getActionIndex(); final int pointerId = event.getPointerId(pointerIndex); Point clickLocation = mTempPoint; final int result = computeClickLocation(clickLocation); if (result == CLICK_LOCATION_NONE) { return; } mLongPressingPointerId = pointerId; mLongPressingPointerDeltaX = (int) event.getX(pointerIndex) - clickLocation.x; mLongPressingPointerDeltaY = (int) event.getY(pointerIndex) - clickLocation.y; sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); mCurrentState = STATE_DELEGATING; sendDownForAllNotInjectedPointers(event, policyFlags); } @Override public boolean onDoubleTap(MotionEvent event, int policyFlags) { // Ignore the event if we aren't touch exploring. if (mCurrentState != STATE_TOUCH_EXPLORING) { return false; } // Remove pending event deliveries. mSendHoverEnterAndMoveDelayed.cancel(); mSendHoverExitDelayed.cancel(); if (mSendTouchExplorationEndDelayed.isPending()) { mSendTouchExplorationEndDelayed.forceSendAndRemove(); } if (mSendTouchInteractionEndDelayed.isPending()) { mSendTouchInteractionEndDelayed.forceSendAndRemove(); } // Try to use the standard accessibility API to click if (mAms.performActionOnAccessibilityFocusedItem( AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK)) { return true; } Slog.e(LOG_TAG, "ACTION_CLICK failed. Dispatching motion events to simulate click."); final int pointerIndex = event.getActionIndex(); final int pointerId = event.getPointerId(pointerIndex); Point clickLocation = mTempPoint; final int result = computeClickLocation(clickLocation); if (result == CLICK_LOCATION_NONE) { // We can't send a click to no location, but the gesture was still // consumed. return true; } // Do the click. PointerProperties[] properties = new PointerProperties[1]; properties[0] = new PointerProperties(); event.getPointerProperties(pointerIndex, properties[0]); PointerCoords[] coords = new PointerCoords[1]; coords[0] = new PointerCoords(); coords[0].x = clickLocation.x; coords[0].y = clickLocation.y; MotionEvent click_event = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), MotionEvent.ACTION_DOWN, 1, properties, coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(), event.getFlags()); final boolean targetAccessibilityFocus = (result == CLICK_LOCATION_ACCESSIBILITY_FOCUS); sendActionDownAndUp(click_event, policyFlags, targetAccessibilityFocus); click_event.recycle(); return true; } @Override public boolean onGestureStarted() { // We have to perform gesture detection, so // clear the current state and try to detect. mCurrentState = STATE_GESTURE_DETECTING; mSendHoverEnterAndMoveDelayed.cancel(); mSendHoverExitDelayed.cancel(); mExitGestureDetectionModeDelayed.post(); // Send accessibility event to announce the start // of gesture recognition. sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_START); return false; } @Override public boolean onGestureCompleted(int gestureId) { if (mCurrentState != STATE_GESTURE_DETECTING) { return false; } endGestureDetection(); mAms.onGesture(gestureId); return true; } @Override public boolean onGestureCancelled(MotionEvent event, int policyFlags) { if (mCurrentState == STATE_GESTURE_DETECTING) { endGestureDetection(); return true; } else if (mCurrentState == STATE_TOUCH_EXPLORING) { // 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); mSendHoverEnterAndMoveDelayed.forceSendAndRemove(); mSendHoverExitDelayed.cancel(); sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags); return true; } } return false; } /** * 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) { ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: { mAms.onTouchInteractionStart(); sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_START); // 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 (mSendTouchExplorationEndDelayed.isPending()) { mSendTouchExplorationEndDelayed.forceSendAndRemove(); } if (mSendTouchInteractionEndDelayed.isPending()) { mSendTouchInteractionEndDelayed.forceSendAndRemove(); } if (!mGestureDetector.firstTapDetected() && !mTouchExplorationInProgress) { if (!mSendHoverEnterAndMoveDelayed.isPending()) { // Deliver hover enter with a delay to have a chance // to detect what the user is trying to do. final int pointerId = receivedTracker.getPrimaryPointerId(); final int pointerIdBits = (1 << pointerId); mSendHoverEnterAndMoveDelayed.post(event, true, pointerIdBits, policyFlags); } else { // Cache the event until we discern exploration from gesturing. mSendHoverEnterAndMoveDelayed.addEvent(event); } } } break; case MotionEvent.ACTION_POINTER_DOWN: { // 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. mSendHoverEnterAndMoveDelayed.cancel(); mSendHoverExitDelayed.cancel(); } break; case MotionEvent.ACTION_MOVE: { final int pointerId = receivedTracker.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); } else { if (mTouchExplorationInProgress) { sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags); sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags); } } } break; case 2: { // More than one pointer so the user is not touch exploring // and now we have to decide whether to delegate or drag. if (mSendHoverEnterAndMoveDelayed.isPending()) { // We have not started sending events so cancel // scheduled sending events. mSendHoverEnterAndMoveDelayed.cancel(); mSendHoverExitDelayed.cancel(); } else { if (mTouchExplorationInProgress) { // 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 = receivedTracker.getReceivedPointerDownX( pointerId) - rawEvent.getX(pointerIndex); final float deltaY = receivedTracker.getReceivedPointerDownY( pointerId) - rawEvent.getY(pointerIndex); final double moveDelta = Math.hypot(deltaX, deltaY); if (moveDelta < mDoubleTapSlop) { break; } // We are sending events so send exit and gesture // end since we transition to another state. sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); } } if (isDraggingGesture(event)) { // Two pointers moving in the same direction within // a given distance perform a drag. mCurrentState = STATE_DRAGGING; mDraggingPointerId = pointerId; event.setEdgeFlags(receivedTracker.getLastReceivedDownEdgeFlags()); sendMotionEvent(event, MotionEvent.ACTION_DOWN, pointerIdBits, policyFlags); } else { // Two pointers moving arbitrary are delegated to the view hierarchy. mCurrentState = STATE_DELEGATING; sendDownForAllNotInjectedPointers(event, policyFlags); } } break; default: { // More than one pointer so the user is not touch exploring // and now we have to decide whether to delegate or drag. if (mSendHoverEnterAndMoveDelayed.isPending()) { // We have not started sending events so cancel // scheduled sending events. mSendHoverEnterAndMoveDelayed.cancel(); mSendHoverExitDelayed.cancel(); } else { // We are sending events so send exit and gesture // end since we transition to another state. sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); } // More than two pointers are delegated to the view hierarchy. mCurrentState = STATE_DELEGATING; sendDownForAllNotInjectedPointers(event, policyFlags); } } } break; case MotionEvent.ACTION_UP: { 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, pointerIdBits, policyFlags); } else { // The user is touch exploring so we send events for end. sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags); } if (!mSendTouchInteractionEndDelayed.isPending()) { mSendTouchInteractionEndDelayed.post(); } } 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, int policyFlags) { 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: { throw new IllegalStateException("Dragging state can be reached only if two " + "pointers are already down"); } 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 mCurrentState = STATE_DELEGATING; if (mDraggingPointerId != INVALID_POINTER_ID) { sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); } 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)) { 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 deltaX = firstPtrX - secondPtrX; final float deltaY = firstPtrY - secondPtrY; final double distance = Math.hypot(deltaX, deltaY); if (distance > mScaledMinPointerDistanceToUseMiddleLocation) { event.setLocation(deltaX / 2, deltaY / 2); } // If still dragging send a drag event. sendMotionEvent(event, MotionEvent.ACTION_MOVE, pointerIdBits, policyFlags); } else { // The two pointers are moving either in different directions or // no close enough => delegate the gesture to the view hierarchy. mCurrentState = STATE_DELEGATING; // Send an event to the end of the drag gesture. sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); // Deliver all pointers to the view hierarchy. sendDownForAllNotInjectedPointers(event, policyFlags); } } break; default: { mCurrentState = STATE_DELEGATING; // Send an event to the end of the drag gesture. sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); // Deliver all pointers to the view hierarchy. 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. sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); } } break; case MotionEvent.ACTION_UP: { mAms.onTouchInteractionEnd(); // Announce the end of a new touch interaction. 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. sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); } mCurrentState = STATE_TOUCH_EXPLORING; } 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, int policyFlags) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: { throw new IllegalStateException("Delegating state can only be reached if " + "there is at least one pointer down!"); } case MotionEvent.ACTION_UP: { // Offset the event if we are doing a long press as the // target is not necessarily under the user's finger. if (mLongPressingPointerId >= 0) { event = offsetEvent(event, - mLongPressingPointerDeltaX, - mLongPressingPointerDeltaY); // Clear the long press state. mLongPressingPointerId = -1; mLongPressingPointerDeltaX = 0; mLongPressingPointerDeltaY = 0; } // Deliver the event. sendMotionEvent(event, event.getAction(), ALL_POINTER_ID_BITS, policyFlags); // Announce the end of a the touch interaction. mAms.onTouchInteractionEnd(); sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); mCurrentState = STATE_TOUCH_EXPLORING; } break; default: { // Deliver the event. sendMotionEvent(event, event.getAction(), ALL_POINTER_ID_BITS, policyFlags); } } } private void endGestureDetection() { mAms.onTouchInteractionEnd(); // Announce the end of the gesture recognition. sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END); // Announce the end of a the touch interaction. sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END); mExitGestureDetectionModeDelayed.cancel(); mCurrentState = STATE_TOUCH_EXPLORING; } /** * Sends an accessibility event of the given type. * * @param type The event type. */ private void sendAccessibilityEvent(int type) { AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mContext); if (accessibilityManager.isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain(type); event.setWindowId(mAms.getActiveWindowId()); accessibilityManager.sendAccessibilityEvent(event); switch (type) { case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START: { mTouchExplorationInProgress = true; } break; case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END: { mTouchExplorationInProgress = false; } break; } } } /** * Sends down events to the view hierarchy for all pointers which are * not already being delivered i.e. pointers that are not yet injected. * * @param prototype The prototype from which to create the injected events. * @param policyFlags The policy flags associated with the event. */ private void sendDownForAllNotInjectedPointers(MotionEvent prototype, int policyFlags) { InjectedPointerTracker injectedPointers = mInjectedPointerTracker; // Inject the injected pointers. int pointerIdBits = 0; final int pointerCount = prototype.getPointerCount(); for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); // Do not send event for already delivered pointers. if (!injectedPointers.isInjectedPointerDown(pointerId)) { pointerIdBits |= (1 << pointerId); final int action = computeInjectionAction(MotionEvent.ACTION_DOWN, i); sendMotionEvent(prototype, action, pointerIdBits, policyFlags); } } } /** * 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 = mInjectedPointerTracker.getLastInjectedHoverEvent(); if (event != null && event.getActionMasked() != MotionEvent.ACTION_HOVER_EXIT) { final int pointerIdBits = event.getPointerIdBits(); if (!mSendTouchExplorationEndDelayed.isPending()) { mSendTouchExplorationEndDelayed.post(); } sendMotionEvent(event, MotionEvent.ACTION_HOVER_EXIT, 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 = mInjectedPointerTracker.getLastInjectedHoverEvent(); if (event != null && event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) { final int pointerIdBits = event.getPointerIdBits(); sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags); } } /** * Sends up events to the view hierarchy for all pointers which are * already being delivered i.e. pointers that are injected. * * @param prototype The prototype from which to create the injected events. * @param policyFlags The policy flags associated with the event. */ private void sendUpForInjectedDownPointers(MotionEvent prototype, int policyFlags) { final InjectedPointerTracker injectedTracked = mInjectedPointerTracker; int pointerIdBits = 0; final int pointerCount = prototype.getPointerCount(); for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); // Skip non injected down pointers. if (!injectedTracked.isInjectedPointerDown(pointerId)) { continue; } pointerIdBits |= (1 << pointerId); final int action = computeInjectionAction(MotionEvent.ACTION_UP, i); sendMotionEvent(prototype, action, pointerIdBits, policyFlags); } } /** * Sends an up and down events. * * @param prototype The prototype from which to create the injected events. * @param policyFlags The policy flags associated with the event. * @param targetAccessibilityFocus Whether the event targets the accessibility focus. */ private void sendActionDownAndUp(MotionEvent prototype, int policyFlags, boolean targetAccessibilityFocus) { // Tap with the pointer that last explored. final int pointerId = prototype.getPointerId(prototype.getActionIndex()); final int pointerIdBits = (1 << pointerId); prototype.setTargetAccessibilityFocus(targetAccessibilityFocus); sendMotionEvent(prototype, MotionEvent.ACTION_DOWN, pointerIdBits, policyFlags); prototype.setTargetAccessibilityFocus(targetAccessibilityFocus); sendMotionEvent(prototype, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); } /** * Sends an event. * * @param prototype The prototype from which to create the injected events. * @param action The action of the event. * @param pointerIdBits The bits of the pointers to send. * @param policyFlags The policy flags associated with the event. */ private void sendMotionEvent(MotionEvent prototype, int action, int pointerIdBits, int policyFlags) { prototype.setAction(action); MotionEvent event = null; if (pointerIdBits == ALL_POINTER_ID_BITS) { event = prototype; } else { try { event = prototype.split(pointerIdBits); } catch (IllegalArgumentException e) { Slog.e(LOG_TAG, "sendMotionEvent: Failed to split motion event: " + e); return; } } if (action == MotionEvent.ACTION_DOWN) { event.setDownTime(event.getEventTime()); } else { event.setDownTime(mInjectedPointerTracker.getLastInjectedDownEventTime()); } // If the user is long pressing but the long pressing pointer // was not exactly over the accessibility focused item we need // to remap the location of that pointer so the user does not // have to explicitly touch explore something to be able to // long press it, or even worse to avoid the user long pressing // on the wrong item since click and long press behave differently. if (mLongPressingPointerId >= 0) { event = offsetEvent(event, - mLongPressingPointerDeltaX, - mLongPressingPointerDeltaY); } if (DEBUG) { Slog.d(LOG_TAG, "Injecting event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); } // Make sure that the user will see the event. policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER; if (mNext != null) { // TODO: For now pass null for the raw event since the touch // explorer is the last event transformation and it does // not care about the raw event. mNext.onMotionEvent(event, null, policyFlags); } mInjectedPointerTracker.onMotionEvent(event); if (event != prototype) { event.recycle(); } } /** * Offsets all pointers in the given event by adding the specified X and Y * offsets. * * @param event The event to offset. * @param offsetX The X offset. * @param offsetY The Y offset. * @return An event with the offset pointers or the original event if both * offsets are zero. */ private MotionEvent offsetEvent(MotionEvent event, int offsetX, int offsetY) { if (offsetX == 0 && offsetY == 0) { return event; } final int remappedIndex = event.findPointerIndex(mLongPressingPointerId); final int pointerCount = event.getPointerCount(); PointerProperties[] props = PointerProperties.createArray(pointerCount); PointerCoords[] coords = PointerCoords.createArray(pointerCount); for (int i = 0; i < pointerCount; i++) { event.getPointerProperties(i, props[i]); event.getPointerCoords(i, coords[i]); if (i == remappedIndex) { coords[i].x += offsetX; coords[i].y += offsetY; } } return MotionEvent.obtain(event.getDownTime(), event.getEventTime(), event.getAction(), event.getPointerCount(), props, coords, event.getMetaState(), event.getButtonState(), 1.0f, 1.0f, event.getDeviceId(), event.getEdgeFlags(), event.getSource(), event.getFlags()); } /** * Computes the action for an injected event based on a masked action * and a pointer index. * * @param actionMasked The masked action. * @param pointerIndex The index of the pointer which has changed. * @return The action to be used for injection. */ private int computeInjectionAction(int actionMasked, int pointerIndex) { switch (actionMasked) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: { InjectedPointerTracker injectedTracker = mInjectedPointerTracker; // Compute the action based on how many down pointers are injected. if (injectedTracker.getInjectedPointerDownCount() == 0) { return MotionEvent.ACTION_DOWN; } else { return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) | MotionEvent.ACTION_POINTER_DOWN; } } case MotionEvent.ACTION_POINTER_UP: { InjectedPointerTracker injectedTracker = mInjectedPointerTracker; // Compute the action based on how many down pointers are injected. if (injectedTracker.getInjectedPointerDownCount() == 1) { return MotionEvent.ACTION_UP; } else { return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) | MotionEvent.ACTION_POINTER_UP; } } default: return actionMasked; } } /** * 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) { ReceivedPointerTracker receivedTracker = mReceivedPointerTracker; 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 = receivedTracker.getReceivedPointerDownX(0); final float firstPtrDownY = receivedTracker.getReceivedPointerDownY(0); final float secondPtrDownX = receivedTracker.getReceivedPointerDownX(1); final float secondPtrDownY = receivedTracker.getReceivedPointerDownY(1); return GestureUtils.isDraggingGesture(firstPtrDownX, firstPtrDownY, secondPtrDownX, secondPtrDownY, firstPtrX, firstPtrY, secondPtrX, secondPtrY, MAX_DRAGGING_ANGLE_COS); } private int computeClickLocation(Point outLocation) { MotionEvent lastExploreEvent = mInjectedPointerTracker.getLastInjectedHoverEventForClick(); if (lastExploreEvent != null) { final int lastExplorePointerIndex = lastExploreEvent.getActionIndex(); outLocation.x = (int) lastExploreEvent.getX(lastExplorePointerIndex); outLocation.y = (int) lastExploreEvent.getY(lastExplorePointerIndex); if (!mAms.accessibilityFocusOnlyInActiveWindow() || mLastTouchedWindowId == mAms.getActiveWindowId()) { if (mAms.getAccessibilityFocusClickPointInScreen(outLocation)) { return CLICK_LOCATION_ACCESSIBILITY_FOCUS; } else { return CLICK_LOCATION_LAST_TOUCH_EXPLORED; } } } if (mAms.getAccessibilityFocusClickPointInScreen(outLocation)) { return CLICK_LOCATION_ACCESSIBILITY_FOCUS; } return CLICK_LOCATION_NONE; } /** * Gets the symbolic name of a state. * * @param state A state. * @return The state symbolic name. */ private static String getStateSymbolicName(int state) { switch (state) { case STATE_TOUCH_EXPLORING: return "STATE_TOUCH_EXPLORING"; case STATE_DRAGGING: return "STATE_DRAGGING"; case STATE_DELEGATING: return "STATE_DELEGATING"; case STATE_GESTURE_DETECTING: return "STATE_GESTURE_DETECTING"; default: throw new IllegalArgumentException("Unknown state: " + state); } } /** * 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. sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END); // Clearing puts is in touch exploration state with a finger already // down, so announce the transition to exploration state. sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); 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 mEvents = new ArrayList(); private int mPointerIdBits; private int mPolicyFlags; public void post(MotionEvent event, boolean touchExplorationInProgress, int pointerIdBits, int policyFlags) { cancel(); addEvent(event); mPointerIdBits = pointerIdBits; mPolicyFlags = policyFlags; mHandler.postDelayed(this, mDetermineUserIntentTimeout); } public void addEvent(MotionEvent event) { mEvents.add(MotionEvent.obtain(event)); } 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(); } } public void forceSendAndRemove() { if (isPending()) { run(); cancel(); } } public void run() { // Send an accessibility event to announce the touch exploration start. sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START); if (!mEvents.isEmpty()) { // Deliver a down event. sendMotionEvent(mEvents.get(0), MotionEvent.ACTION_HOVER_ENTER, 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++) { sendMotionEvent(mEvents.get(i), MotionEvent.ACTION_HOVER_MOVE, 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 int mPointerIdBits; private int mPolicyFlags; public void post(MotionEvent prototype, int pointerIdBits, int policyFlags) { cancel(); mPrototype = MotionEvent.obtain(prototype); 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() { mPrototype.recycle(); mPrototype = 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"); } sendMotionEvent(mPrototype, MotionEvent.ACTION_HOVER_EXIT, 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() { sendAccessibilityEvent(mEventType); } } @Override public String toString() { return LOG_TAG; } class InjectedPointerTracker { private static final String LOG_TAG_INJECTED_POINTER_TRACKER = "InjectedPointerTracker"; // Keep track of which pointers sent to the system are down. private int mInjectedPointersDown; // The time of the last injected down. private long mLastInjectedDownEventTime; // The last injected hover event. private MotionEvent mLastInjectedHoverEvent; // The last injected hover event used for performing clicks. private MotionEvent mLastInjectedHoverEventForClick; /** * Processes an injected {@link MotionEvent} event. * * @param event The event to process. */ public void onMotionEvent(MotionEvent event) { final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: { final int pointerId = event.getPointerId(event.getActionIndex()); final int pointerFlag = (1 << pointerId); mInjectedPointersDown |= pointerFlag; mLastInjectedDownEventTime = event.getDownTime(); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: { final int pointerId = event.getPointerId(event.getActionIndex()); final int pointerFlag = (1 << pointerId); mInjectedPointersDown &= ~pointerFlag; if (mInjectedPointersDown == 0) { mLastInjectedDownEventTime = 0; } } break; case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_HOVER_EXIT: { if (mLastInjectedHoverEvent != null) { mLastInjectedHoverEvent.recycle(); } mLastInjectedHoverEvent = MotionEvent.obtain(event); if (mLastInjectedHoverEventForClick != null) { mLastInjectedHoverEventForClick.recycle(); } mLastInjectedHoverEventForClick = MotionEvent.obtain(event); } break; } if (DEBUG) { Slog.i(LOG_TAG_INJECTED_POINTER_TRACKER, "Injected pointer:\n" + toString()); } } /** * Clears the internals state. */ public void clear() { mInjectedPointersDown = 0; } /** * @return The time of the last injected down event. */ public long getLastInjectedDownEventTime() { return mLastInjectedDownEventTime; } /** * @return The number of down pointers injected to the view hierarchy. */ public int getInjectedPointerDownCount() { return Integer.bitCount(mInjectedPointersDown); } /** * @return The bits of the injected pointers that are down. */ public int getInjectedPointersDown() { return mInjectedPointersDown; } /** * Whether an injected pointer is down. * * @param pointerId The unique pointer id. * @return True if the pointer is down. */ public boolean isInjectedPointerDown(int pointerId) { final int pointerFlag = (1 << pointerId); return (mInjectedPointersDown & pointerFlag) != 0; } /** * @return The the last injected hover event. */ public MotionEvent getLastInjectedHoverEvent() { return mLastInjectedHoverEvent; } /** * @return The the last injected hover event. */ public MotionEvent getLastInjectedHoverEventForClick() { return mLastInjectedHoverEventForClick; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("========================="); builder.append("\nDown pointers #"); builder.append(Integer.bitCount(mInjectedPointersDown)); builder.append(" [ "); for (int i = 0; i < MAX_POINTER_COUNT; i++) { if ((mInjectedPointersDown & i) != 0) { builder.append(i); builder.append(" "); } } builder.append("]"); builder.append("\n========================="); return builder.toString(); } } class ReceivedPointerTracker { private static final String LOG_TAG_RECEIVED_POINTER_TRACKER = "ReceivedPointerTracker"; // Keep track of where and when a pointer went down. private final float[] mReceivedPointerDownX = new float[MAX_POINTER_COUNT]; private final float[] mReceivedPointerDownY = new float[MAX_POINTER_COUNT]; private final long[] mReceivedPointerDownTime = new long[MAX_POINTER_COUNT]; // Which pointers are down. private int mReceivedPointersDown; // The edge flags of the last received down event. private int mLastReceivedDownEdgeFlags; // Primary pointer which is either the first that went down // or if it goes up the next one that most recently went down. private int mPrimaryPointerId; // Keep track of the last up pointer data. private long mLastReceivedUpPointerDownTime; private float mLastReceivedUpPointerDownX; private float mLastReceivedUpPointerDownY; private MotionEvent mLastReceivedEvent; /** * Clears the internals state. */ public void clear() { Arrays.fill(mReceivedPointerDownX, 0); Arrays.fill(mReceivedPointerDownY, 0); Arrays.fill(mReceivedPointerDownTime, 0); mReceivedPointersDown = 0; mPrimaryPointerId = 0; mLastReceivedUpPointerDownTime = 0; mLastReceivedUpPointerDownX = 0; mLastReceivedUpPointerDownY = 0; } /** * Processes a received {@link MotionEvent} event. * * @param event The event to process. */ public void onMotionEvent(MotionEvent event) { if (mLastReceivedEvent != null) { mLastReceivedEvent.recycle(); } mLastReceivedEvent = MotionEvent.obtain(event); final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { handleReceivedPointerDown(event.getActionIndex(), event); } break; case MotionEvent.ACTION_POINTER_DOWN: { handleReceivedPointerDown(event.getActionIndex(), event); } break; case MotionEvent.ACTION_UP: { handleReceivedPointerUp(event.getActionIndex(), event); } break; case MotionEvent.ACTION_POINTER_UP: { handleReceivedPointerUp(event.getActionIndex(), event); } break; } if (DEBUG) { Slog.i(LOG_TAG_RECEIVED_POINTER_TRACKER, "Received pointer:\n" + toString()); } } /** * @return The last received event. */ public MotionEvent getLastReceivedEvent() { return mLastReceivedEvent; } /** * @return The number of received pointers that are down. */ public int getReceivedPointerDownCount() { return Integer.bitCount(mReceivedPointersDown); } /** * Whether an received pointer is down. * * @param pointerId The unique pointer id. * @return True if the pointer is down. */ public boolean isReceivedPointerDown(int pointerId) { final int pointerFlag = (1 << pointerId); return (mReceivedPointersDown & pointerFlag) != 0; } /** * @param pointerId The unique pointer id. * @return The X coordinate where the pointer went down. */ public float getReceivedPointerDownX(int pointerId) { return mReceivedPointerDownX[pointerId]; } /** * @param pointerId The unique pointer id. * @return The Y coordinate where the pointer went down. */ public float getReceivedPointerDownY(int pointerId) { return mReceivedPointerDownY[pointerId]; } /** * @param pointerId The unique pointer id. * @return The time when the pointer went down. */ public long getReceivedPointerDownTime(int pointerId) { return mReceivedPointerDownTime[pointerId]; } /** * @return The id of the primary pointer. */ public int getPrimaryPointerId() { if (mPrimaryPointerId == INVALID_POINTER_ID) { mPrimaryPointerId = findPrimaryPointerId(); } return mPrimaryPointerId; } /** * @return The time when the last up received pointer went down. */ public long getLastReceivedUpPointerDownTime() { return mLastReceivedUpPointerDownTime; } /** * @return The down X of the last received pointer that went up. */ public float getLastReceivedUpPointerDownX() { return mLastReceivedUpPointerDownX; } /** * @return The down Y of the last received pointer that went up. */ public float getLastReceivedUpPointerDownY() { return mLastReceivedUpPointerDownY; } /** * @return The edge flags of the last received down event. */ public int getLastReceivedDownEdgeFlags() { return mLastReceivedDownEdgeFlags; } /** * Handles a received pointer down event. * * @param pointerIndex The index of the pointer that has changed. * @param event The event to be handled. */ private void handleReceivedPointerDown(int pointerIndex, MotionEvent event) { final int pointerId = event.getPointerId(pointerIndex); final int pointerFlag = (1 << pointerId); mLastReceivedUpPointerDownTime = 0; mLastReceivedUpPointerDownX = 0; mLastReceivedUpPointerDownX = 0; mLastReceivedDownEdgeFlags = event.getEdgeFlags(); mReceivedPointersDown |= pointerFlag; mReceivedPointerDownX[pointerId] = event.getX(pointerIndex); mReceivedPointerDownY[pointerId] = event.getY(pointerIndex); mReceivedPointerDownTime[pointerId] = event.getEventTime(); mPrimaryPointerId = pointerId; } /** * Handles a received pointer up event. * * @param pointerIndex The index of the pointer that has changed. * @param event The event to be handled. */ private void handleReceivedPointerUp(int pointerIndex, MotionEvent event) { final int pointerId = event.getPointerId(pointerIndex); final int pointerFlag = (1 << pointerId); mLastReceivedUpPointerDownTime = getReceivedPointerDownTime(pointerId); mLastReceivedUpPointerDownX = mReceivedPointerDownX[pointerId]; mLastReceivedUpPointerDownY = mReceivedPointerDownY[pointerId]; mReceivedPointersDown &= ~pointerFlag; mReceivedPointerDownX[pointerId] = 0; mReceivedPointerDownY[pointerId] = 0; mReceivedPointerDownTime[pointerId] = 0; if (mPrimaryPointerId == pointerId) { mPrimaryPointerId = INVALID_POINTER_ID; } } /** * @return The primary pointer id. */ private int findPrimaryPointerId() { int primaryPointerId = INVALID_POINTER_ID; long minDownTime = Long.MAX_VALUE; // Find the pointer that went down first. int pointerIdBits = mReceivedPointersDown; while (pointerIdBits > 0) { final int pointerId = Integer.numberOfTrailingZeros(pointerIdBits); pointerIdBits &= ~(1 << pointerId); final long downPointerTime = mReceivedPointerDownTime[pointerId]; if (downPointerTime < minDownTime) { minDownTime = downPointerTime; primaryPointerId = pointerId; } } return primaryPointerId; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("========================="); builder.append("\nDown pointers #"); builder.append(getReceivedPointerDownCount()); builder.append(" [ "); for (int i = 0; i < MAX_POINTER_COUNT; i++) { if (isReceivedPointerDown(i)) { builder.append(i); builder.append(" "); } } builder.append("]"); builder.append("\nPrimary pointer id [ "); builder.append(getPrimaryPointerId()); builder.append(" ]"); builder.append("\n========================="); return builder.toString(); } } }