/* ** 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 static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END; import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START; import android.content.Context; import android.os.Handler; import android.util.Slog; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.view.WindowManagerPolicy; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import com.android.server.accessibility.AccessibilityInputFilter.Explorer; import com.android.server.wm.InputFilter; import java.util.Arrays; /** * 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 around performs touch exploration.</li> * <li>2. Two close fingers moving in the same direction perform a drag.</li> * <li>3. Multi-finger gestures are delivered to view hierarchy.</li> * <li>4. Pointers that have not moved more than a specified distance after they * went down are considered inactive.</li> * <li>5. Two fingers moving too far from each other or in different directions * are considered a multi-finger gesture.</li> * <li>6. Tapping on the last touch explored location within given time and * distance slop performs a click.</li> * <li>7. Tapping and holding for a while on the last touch explored location within * given time and distance slop performs a long press.</li> * <ol> * * @hide */ public class TouchExplorer implements Explorer { private static final boolean DEBUG = false; // Tag for logging received events. private static final String LOG_TAG_RECEIVED = "TouchExplorer-RECEIVED"; // Tag for logging injected events. private static final String LOG_TAG_INJECTED = "TouchExplorer-INJECTED"; // Tag for logging the current state. private static final String LOG_TAG_STATE = "TouchExplorer-STATE"; // 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; // Invalid pointer ID. private static final int INVALID_POINTER_ID = -1; // The time slop in milliseconds for activating an item after it has // been touch explored. Tapping on an item within this slop will perform // a click and tapping and holding down a long press. private static final long ACTIVATION_TIME_SLOP = 2000; // 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; // The minimum 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 delay for sending a hover enter event. private static final long DELAY_SEND_HOVER_ENTER = 200; // Constant referring to the ids bits of all pointers. private static final int ALL_POINTER_ID_BITS = 0xFFFFFFFF; // Temporary array for storing pointer IDs. private final int[] mTempPointerIds = new int[MAX_POINTER_COUNT]; // The distance from the last touch explored location tapping within // which would perform a click and tapping and holding a long press. private final int mTouchExplorationTapSlop; // The InputFilter this tracker is associated with i.e. the filter // which delegates event processing to this touch explorer. private final InputFilter mInputFilter; // Helper class for tracking pointers on the screen, for example which // pointers are down, which are active, etc. private final PointerTracker mPointerTracker; // Handle to the accessibility manager for firing accessibility events // announcing touch exploration gesture start and end. private final AccessibilityManager mAccessibilityManager; // The last event that was received while performing touch exploration. private MotionEvent mLastTouchExploreEvent; // The current state of the touch explorer. private int mCurrentState = STATE_TOUCH_EXPLORING; // Flag whether a touch exploration gesture is in progress. private boolean mTouchExploreGestureInProgress; // 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 event. private final SendHoverDelayed mSendHoverDelayed; // Command for delayed sending of a long press. private final PerformLongPressDelayed mPerformLongPressDelayed; /** * Creates a new instance. * * @param inputFilter The input filter associated with this explorer. * @param context A context handle for accessing resources. */ public TouchExplorer(InputFilter inputFilter, Context context) { mInputFilter = inputFilter; mTouchExplorationTapSlop = ViewConfiguration.get(context).getScaledTouchExplorationTapSlop(); mPointerTracker = new PointerTracker(context); mHandler = new Handler(context.getMainLooper()); mSendHoverDelayed = new SendHoverDelayed(); mPerformLongPressDelayed = new PerformLongPressDelayed(); mAccessibilityManager = AccessibilityManager.getInstance(context); } public void clear(MotionEvent event, int policyFlags) { sendUpForInjectedDownPointers(event, policyFlags); clear(); } /** * {@inheritDoc} */ public void onMotionEvent(MotionEvent event, int policyFlags) { if (DEBUG) { Slog.d(LOG_TAG_RECEIVED, "Received event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); Slog.d(LOG_TAG_STATE, getStateSymbolicName(mCurrentState)); } // Keep track of the pointers's state. mPointerTracker.onReceivedMotionEvent(event); switch(mCurrentState) { case STATE_TOUCH_EXPLORING: { handleMotionEventStateTouchExploring(event, policyFlags); } break; case STATE_DRAGGING: { handleMotionEventStateDragging(event, policyFlags); } break; case STATE_DELEGATING: { handleMotionEventStateDelegating(event, policyFlags); } break; default: { throw new IllegalStateException("Illegal state: " + mCurrentState); } } } /** * Handles a motion event in touch exploring state. * * @param event The event to be handled. * @param policyFlags The policy flags associated with the event. */ private void handleMotionEventStateTouchExploring(MotionEvent event, int policyFlags) { PointerTracker pointerTracker = mPointerTracker; final int activePointerCount = pointerTracker.getActivePointerCount(); switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: { switch (activePointerCount) { case 0: { throw new IllegalStateException("The must always be one active pointer in" + "touch exploring state!"); } case 1: { mSendHoverDelayed.remove(); mPerformLongPressDelayed.remove(); // Send a hover for every finger down so the user gets feedback. final int pointerId = pointerTracker.getPrimaryActivePointerId(); final int pointerIdBits = (1 << pointerId); final int lastAction = pointerTracker.getLastInjectedHoverAction(); // Deliver hover enter with a delay to have a change to detect // whether the user actually starts a scrolling gesture. if (lastAction == MotionEvent.ACTION_HOVER_EXIT) { mSendHoverDelayed.post(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags, DELAY_SEND_HOVER_ENTER); } else { sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags); } if (mLastTouchExploreEvent == null) { break; } // If more pointers down on the screen since the last touch // exploration we discard the last cached touch explore event. if (event.getPointerCount() != mLastTouchExploreEvent.getPointerCount()) { mLastTouchExploreEvent = null; break; } // If the down is in the time slop => schedule a long press. final long pointerDownTime = pointerTracker.getReceivedPointerDownTime(pointerId); final long lastExploreTime = mLastTouchExploreEvent.getEventTime(); final long deltaTimeExplore = pointerDownTime - lastExploreTime; if (deltaTimeExplore <= ACTIVATION_TIME_SLOP) { mPerformLongPressDelayed.post(event, policyFlags, ViewConfiguration.getLongPressTimeout()); break; } } break; default: { /* do nothing - let the code for ACTION_MOVE decide what to do */ } break; } } break; case MotionEvent.ACTION_MOVE: { final int pointerId = pointerTracker.getPrimaryActivePointerId(); final int pointerIndex = event.findPointerIndex(pointerId); final int pointerIdBits = (1 << pointerId); switch (activePointerCount) { case 0: { /* do nothing - no active pointers so we swallow the event */ } break; case 1: { // Detect touch exploration gesture start by having one active pointer // that moved more than a given distance. if (!mTouchExploreGestureInProgress) { final float deltaX = pointerTracker.getReceivedPointerDownX(pointerId) - event.getX(pointerIndex); final float deltaY = pointerTracker.getReceivedPointerDownY(pointerId) - event.getY(pointerIndex); final double moveDelta = Math.hypot(deltaX, deltaY); if (moveDelta > mTouchExplorationTapSlop) { mTouchExploreGestureInProgress = true; sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_START); // Make sure the scheduled down/move event is sent. mSendHoverDelayed.forceSendAndRemove(); mPerformLongPressDelayed.remove(); // If we have transitioned to exploring state from another one // we need to send a hover enter event here. final int lastAction = mPointerTracker.getLastInjectedHoverAction(); if (lastAction == MotionEvent.ACTION_HOVER_EXIT) { sendMotionEvent(event, MotionEvent.ACTION_HOVER_ENTER, pointerIdBits, policyFlags); } sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags); } } else { // Touch exploration gesture in progress so send a hover event. sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits, policyFlags); } // If the exploring pointer moved enough => cancel the long press. if (!mTouchExploreGestureInProgress && mLastTouchExploreEvent != null && mPerformLongPressDelayed.isPenidng()) { // If the pointer moved more than the tap slop => cancel long press. final float deltaX = mLastTouchExploreEvent.getX(pointerIndex) - event.getX(pointerIndex); final float deltaY = mLastTouchExploreEvent.getY(pointerIndex) - event.getY(pointerIndex); final float moveDelta = (float) Math.hypot(deltaX, deltaY); if (moveDelta > mTouchExplorationTapSlop) { mLastTouchExploreEvent = null; mPerformLongPressDelayed.remove(); break; } } } break; case 2: { mSendHoverDelayed.remove(); mPerformLongPressDelayed.remove(); // We want to no longer hover over the location so subsequent // touch at the same spot will generate a hover enter. ensureHoverExitSent(event, pointerIdBits, policyFlags); if (isDraggingGesture(event)) { // Two pointers moving in the same direction within // a given distance perform a drag. mCurrentState = STATE_DRAGGING; if (mTouchExploreGestureInProgress) { sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); mTouchExploreGestureInProgress = false; } mLastTouchExploreEvent = null; mDraggingPointerId = pointerId; sendMotionEvent(event, MotionEvent.ACTION_DOWN, pointerIdBits, policyFlags); } else { // Two pointers moving arbitrary are delegated to the view hierarchy. mCurrentState = STATE_DELEGATING; mSendHoverDelayed.remove(); if (mTouchExploreGestureInProgress) { sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); mTouchExploreGestureInProgress = false; } mLastTouchExploreEvent = null; sendDownForAllActiveNotInjectedPointers(event, policyFlags); } } break; default: { mSendHoverDelayed.remove(); mPerformLongPressDelayed.remove(); // We want to no longer hover over the location so subsequent // touch at the same spot will generate a hover enter. ensureHoverExitSent(event, pointerIdBits, policyFlags); // More than two pointers are delegated to the view hierarchy. mCurrentState = STATE_DELEGATING; mSendHoverDelayed.remove(); if (mTouchExploreGestureInProgress) { sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); mTouchExploreGestureInProgress = false; } mLastTouchExploreEvent = null; sendDownForAllActiveNotInjectedPointers(event, policyFlags); } } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: { final int pointerId = pointerTracker.getLastReceivedUpPointerId(); final int pointerIdBits = (1 << pointerId); switch (activePointerCount) { case 0: { // If the pointer that went up was not active we have nothing to do. if (!pointerTracker.wasLastReceivedUpPointerActive()) { break; } mPerformLongPressDelayed.remove(); // If touch exploring announce the end of the gesture. // Also do not click on the last explored location. if (mTouchExploreGestureInProgress) { mTouchExploreGestureInProgress = false; mSendHoverDelayed.forceSendAndRemove(); ensureHoverExitSent(event, pointerIdBits, policyFlags); mLastTouchExploreEvent = MotionEvent.obtain(event); sendAccessibilityEvent(TYPE_TOUCH_EXPLORATION_GESTURE_END); break; } // Detect whether to activate i.e. click on the last explored location. if (mLastTouchExploreEvent != null) { // If the down was not in the time slop => nothing else to do. final long eventTime = pointerTracker.getLastReceivedUpPointerDownTime(); final long exploreTime = mLastTouchExploreEvent.getEventTime(); final long deltaTime = eventTime - exploreTime; if (deltaTime > ACTIVATION_TIME_SLOP) { mSendHoverDelayed.forceSendAndRemove(); ensureHoverExitSent(event, pointerIdBits, policyFlags); mLastTouchExploreEvent = MotionEvent.obtain(event); break; } // If a tap is farther than the tap slop => nothing to do. final int pointerIndex = event.findPointerIndex(pointerId); final float deltaX = mLastTouchExploreEvent.getX(pointerIndex) - event.getX(pointerIndex); final float deltaY = mLastTouchExploreEvent.getY(pointerIndex) - event.getY(pointerIndex); final float deltaMove = (float) Math.hypot(deltaX, deltaY); if (deltaMove > mTouchExplorationTapSlop) { mSendHoverDelayed.forceSendAndRemove(); ensureHoverExitSent(event, pointerIdBits, policyFlags); mLastTouchExploreEvent = MotionEvent.obtain(event); break; } // This is a tap so do not send hover events since // this events will result in firing the corresponding // accessibility events confusing the user about what // is actually clicked. mSendHoverDelayed.remove(); ensureHoverExitSent(event, pointerIdBits, policyFlags); // All preconditions are met, so click the last explored location. sendActionDownAndUp(mLastTouchExploreEvent, policyFlags); mLastTouchExploreEvent = null; } else { mSendHoverDelayed.forceSendAndRemove(); ensureHoverExitSent(event, pointerIdBits, policyFlags); mLastTouchExploreEvent = MotionEvent.obtain(event); } } break; } } break; case MotionEvent.ACTION_CANCEL: { mSendHoverDelayed.remove(); mPerformLongPressDelayed.remove(); final int pointerId = pointerTracker.getPrimaryActivePointerId(); final int pointerIdBits = (1 << pointerId); ensureHoverExitSent(event, pointerIdBits, policyFlags); clear(); } 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) { final int 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; sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); sendDownForAllActiveNotInjectedPointers(event, policyFlags); } break; case MotionEvent.ACTION_MOVE: { final int activePointerCount = mPointerTracker.getActivePointerCount(); switch (activePointerCount) { case 1: { // do nothing } break; case 2: { if (isDraggingGesture(event)) { // 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 active pointers to the view hierarchy. sendDownForAllActiveNotInjectedPointers(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 active pointers to the view hierarchy. sendDownForAllActiveNotInjectedPointers(event, policyFlags); } } } break; case MotionEvent.ACTION_POINTER_UP: { final int activePointerCount = mPointerTracker.getActivePointerCount(); switch (activePointerCount) { case 1: { // Send an event to the end of the drag gesture. sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags); } break; default: { mCurrentState = STATE_TOUCH_EXPLORING; } } } break; case MotionEvent.ACTION_UP: { mCurrentState = STATE_TOUCH_EXPLORING; } break; case MotionEvent.ACTION_CANCEL: { clear(); } 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: { mCurrentState = STATE_TOUCH_EXPLORING; } break; case MotionEvent.ACTION_MOVE: { // Check whether some other pointer became active because they have moved // a given distance and if such exist send them to the view hierarchy final int notInjectedCount = mPointerTracker.getNotInjectedActivePointerCount(); if (notInjectedCount > 0) { MotionEvent prototype = MotionEvent.obtain(event); sendDownForAllActiveNotInjectedPointers(prototype, policyFlags); } } break; case MotionEvent.ACTION_POINTER_UP: { // No active pointers => go to initial state. if (mPointerTracker.getActivePointerCount() == 0) { mCurrentState = STATE_TOUCH_EXPLORING; } } break; case MotionEvent.ACTION_CANCEL: { clear(); } break; } // Deliver the event striping out inactive pointers. sendMotionEventStripInactivePointers(event, policyFlags); } /** * Sends down events to the view hierarchy for all active 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 sendDownForAllActiveNotInjectedPointers(MotionEvent prototype, int policyFlags) { final PointerTracker pointerTracker = mPointerTracker; int pointerIdBits = 0; final int pointerCount = prototype.getPointerCount(); // Find which pointers are already injected. for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); if (pointerTracker.isInjectedPointerDown(pointerId)) { pointerIdBits |= (1 << pointerId); } } // Inject the active and not injected pointers. for (int i = 0; i < pointerCount; i++) { final int pointerId = prototype.getPointerId(i); // Skip inactive pointers. if (!pointerTracker.isActivePointer(pointerId)) { continue; } // Do not send event for already delivered pointers. if (pointerTracker.isInjectedPointerDown(pointerId)) { continue; } pointerIdBits |= (1 << pointerId); final int action = computeInjectionAction(MotionEvent.ACTION_DOWN, i); sendMotionEvent(prototype, action, pointerIdBits, policyFlags); } } /** * Ensures that hover exit has been sent. * * @param prototype The prototype from which to create the injected events. * @param pointerIdBits The bits of the pointers to send. * @param policyFlags The policy flags associated with the event. */ private void ensureHoverExitSent(MotionEvent prototype, int pointerIdBits, int policyFlags) { final int lastAction = mPointerTracker.getLastInjectedHoverAction(); if (lastAction != MotionEvent.ACTION_HOVER_EXIT) { sendMotionEvent(prototype, MotionEvent.ACTION_HOVER_EXIT, pointerIdBits, policyFlags); } } /** * Sends up events to the view hierarchy for all active 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 PointerTracker pointerTracker = mPointerTracker; 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 (!pointerTracker.isInjectedPointerDown(pointerId)) { continue; } pointerIdBits |= (1 << pointerId); final int action = computeInjectionAction(MotionEvent.ACTION_UP, i); sendMotionEvent(prototype, action, pointerIdBits, policyFlags); } } /** * Sends a motion event by first stripping the inactive pointers. * * @param prototype The prototype from which to create the injected event. * @param policyFlags The policy flags associated with the event. */ private void sendMotionEventStripInactivePointers(MotionEvent prototype, int policyFlags) { PointerTracker pointerTracker = mPointerTracker; // All pointers active therefore we just inject the event as is. if (prototype.getPointerCount() == pointerTracker.getActivePointerCount()) { sendMotionEvent(prototype, prototype.getAction(), ALL_POINTER_ID_BITS, policyFlags); return; } // No active pointers and the one that just went up was not // active, therefore we have nothing to do. if (pointerTracker.getActivePointerCount() == 0 && !pointerTracker.wasLastReceivedUpPointerActive()) { return; } // If the action pointer going up/down is not active we have nothing to do. // However, for moves we keep going to report moves of active pointers. final int actionMasked = prototype.getActionMasked(); final int actionPointerId = prototype.getPointerId(prototype.getActionIndex()); if (actionMasked != MotionEvent.ACTION_MOVE) { if (!pointerTracker.isActiveOrWasLastActiveUpPointer(actionPointerId)) { return; } } // If the pointer is active or the pointer that just went up // was active we keep the pointer data in the event. int pointerIdBits = 0; final int pointerCount = prototype.getPointerCount(); for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) { final int pointerId = prototype.getPointerId(pointerIndex); if (pointerTracker.isActiveOrWasLastActiveUpPointer(pointerId)) { pointerIdBits |= (1 << pointerId); } } sendMotionEvent(prototype, prototype.getAction(), 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. */ private void sendActionDownAndUp(MotionEvent prototype, int policyFlags) { // Tap with the pointer that last explored - we may have inactive pointers. final int pointerId = prototype.getPointerId(prototype.getActionIndex()); final int pointerIdBits = (1 << pointerId); sendMotionEvent(prototype, MotionEvent.ACTION_DOWN, pointerIdBits, policyFlags); 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 { event = prototype.split(pointerIdBits); } if (action == MotionEvent.ACTION_DOWN) { event.setDownTime(event.getEventTime()); } else { event.setDownTime(mPointerTracker.getLastInjectedDownEventTime()); } if (DEBUG) { Slog.d(LOG_TAG_INJECTED, "Injecting event: " + event + ", policyFlags=0x" + Integer.toHexString(policyFlags)); } // Make sure that the user will see the event. policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER; mPointerTracker.onInjectedMotionEvent(event); mInputFilter.sendInputEvent(event, policyFlags); if (event != prototype) { event.recycle(); } } /** * 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: { PointerTracker pointerTracker = mPointerTracker; // Compute the action based on how many down pointers are injected. if (pointerTracker.getInjectedPointerDownCount() == 0) { return MotionEvent.ACTION_DOWN; } else { return (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT) | MotionEvent.ACTION_POINTER_DOWN; } } case MotionEvent.ACTION_POINTER_UP: { PointerTracker pointerTracker = mPointerTracker; // Compute the action based on how many down pointers are injected. if (pointerTracker.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) { PointerTracker pointerTracker = mPointerTracker; int[] pointerIds = mTempPointerIds; pointerTracker.populateActivePointerIds(pointerIds); final int firstPtrIndex = event.findPointerIndex(pointerIds[0]); final int secondPtrIndex = event.findPointerIndex(pointerIds[1]); final float firstPtrX = event.getX(firstPtrIndex); final float firstPtrY = event.getY(firstPtrIndex); final float secondPtrX = event.getX(secondPtrIndex); final float secondPtrY = event.getY(secondPtrIndex); // Check if the pointers are moving in the same direction. final float firstDeltaX = firstPtrX - pointerTracker.getReceivedPointerDownX(firstPtrIndex); final float firstDeltaY = firstPtrY - pointerTracker.getReceivedPointerDownY(firstPtrIndex); if (firstDeltaX == 0 && firstDeltaY == 0) { return true; } final float firstMagnitude = (float) Math.sqrt(firstDeltaX * firstDeltaX + firstDeltaY * firstDeltaY); final float firstXNormalized = (firstMagnitude > 0) ? firstDeltaX / firstMagnitude : firstDeltaX; final float firstYNormalized = (firstMagnitude > 0) ? firstDeltaY / firstMagnitude : firstDeltaY; final float secondDeltaX = secondPtrX - pointerTracker.getReceivedPointerDownX(secondPtrIndex); final float secondDeltaY = secondPtrY - pointerTracker.getReceivedPointerDownY(secondPtrIndex); if (secondDeltaX == 0 && secondDeltaY == 0) { return true; } final float secondMagnitude = (float) Math.sqrt(secondDeltaX * secondDeltaX + secondDeltaY * secondDeltaY); final float secondXNormalized = (secondMagnitude > 0) ? secondDeltaX / secondMagnitude : secondDeltaX; final float secondYNormalized = (secondMagnitude > 0) ? secondDeltaY / secondMagnitude : secondDeltaY; final float angleCos = firstXNormalized * secondXNormalized + firstYNormalized * secondYNormalized; if (angleCos < MAX_DRAGGING_ANGLE_COS) { return false; } return true; } /** * Sends an event announcing the start/end of a touch exploration gesture. * * @param eventType The type of the event to send. */ private void sendAccessibilityEvent(int eventType) { AccessibilityEvent event = AccessibilityEvent.obtain(eventType); mAccessibilityManager.sendAccessibilityEvent(event); } /** * Clears the internal state of this explorer. */ public void clear() { mSendHoverDelayed.remove(); mPerformLongPressDelayed.remove(); mPointerTracker.clear(); mLastTouchExploreEvent = null; mCurrentState = STATE_TOUCH_EXPLORING; mTouchExploreGestureInProgress = false; mDraggingPointerId = INVALID_POINTER_ID; } /** * 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"; default: throw new IllegalArgumentException("Unknown state: " + state); } } /** * Helper class for tracking pointers and more specifically which of * them are currently down, which are active, and which are delivered * to the view hierarchy. The enclosing {@link TouchExplorer} uses the * pointer state reported by this class to perform touch exploration. * <p> * The main purpose of this class is to allow the touch explorer to * disregard pointers put down by accident by the user and not being * involved in the interaction. For example, a blind user grabs the * device with her left hand such that she touches the screen and she * uses her right hand's index finger to explore the screen content. * In this scenario the touches generated by the left hand are to be * ignored. */ class PointerTracker { private static final String LOG_TAG = "PointerTracker"; // The coefficient by which to multiply // ViewConfiguration.#getScaledTouchSlop() // to compute #mThresholdActivePointer. private static final int COEFFICIENT_ACTIVE_POINTER = 2; // Pointers that moved less than mThresholdActivePointer // are considered active i.e. are ignored. private final double mThresholdActivePointer; // 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; // Which down pointers are active. private int mActivePointers; // Primary active pointer which is either the first that went down // or if it goes up the next active that most recently went down. private int mPrimaryActivePointerId; // Flag indicating that there is at least one active pointer moving. private boolean mHasMovingActivePointer; // Keep track of which pointers sent to the system are down. private int mInjectedPointersDown; // Keep track of the last up pointer data. private long mLastReceivedUpPointerDownTime; private int mLastReceivedUpPointerId; private boolean mLastReceivedUpPointerActive; // The time of the last injected down. private long mLastInjectedDownEventTime; // The action of the last injected hover event. private int mLastInjectedHoverEventAction = MotionEvent.ACTION_HOVER_EXIT; /** * Creates a new instance. * * @param context Context for looking up resources. */ public PointerTracker(Context context) { mThresholdActivePointer = ViewConfiguration.get(context).getScaledTouchSlop() * COEFFICIENT_ACTIVE_POINTER; } /** * Clears the internals state. */ public void clear() { Arrays.fill(mReceivedPointerDownX, 0); Arrays.fill(mReceivedPointerDownY, 0); Arrays.fill(mReceivedPointerDownTime, 0); mReceivedPointersDown = 0; mActivePointers = 0; mPrimaryActivePointerId = 0; mHasMovingActivePointer = false; mInjectedPointersDown = 0; mLastReceivedUpPointerDownTime = 0; mLastReceivedUpPointerId = 0; mLastReceivedUpPointerActive = false; } /** * Processes a received {@link MotionEvent} event. * * @param event The event to process. */ public void onReceivedMotionEvent(MotionEvent event) { final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { // New gesture so restart tracking injected down pointers. mInjectedPointersDown = 0; handleReceivedPointerDown(event.getActionIndex(), event); } break; case MotionEvent.ACTION_POINTER_DOWN: { handleReceivedPointerDown(event.getActionIndex(), event); } break; case MotionEvent.ACTION_MOVE: { handleReceivedPointerMove(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: " + toString()); } } /** * Processes an injected {@link MotionEvent} event. * * @param event The event to process. */ public void onInjectedMotionEvent(MotionEvent event) { final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { handleInjectedPointerDown(event.getActionIndex(), event); mLastInjectedDownEventTime = event.getDownTime(); } break; case MotionEvent.ACTION_POINTER_DOWN: { handleInjectedPointerDown(event.getActionIndex(), event); } break; case MotionEvent.ACTION_UP: { handleInjectedPointerUp(event.getActionIndex(), event); } break; case MotionEvent.ACTION_POINTER_UP: { handleInjectedPointerUp(event.getActionIndex(), event); } break; case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_HOVER_EXIT: { mLastInjectedHoverEventAction = event.getActionMasked(); } break; } if (DEBUG) { Slog.i(LOG_TAG, "Injected pointer: " + toString()); } } /** * @return The number of received pointers that are down. */ public int getReceivedPointerDownCount() { return Integer.bitCount(mReceivedPointersDown); } /** * @return The number of down input pointers that are active. */ public int getActivePointerCount() { return Integer.bitCount(mActivePointers); } /** * 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; } /** * 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 number of down pointers injected to the view hierarchy. */ public int getInjectedPointerDownCount() { return Integer.bitCount(mInjectedPointersDown); } /** * Whether an input pointer is active. * * @param pointerId The unique pointer id. * @return True if the pointer is active. */ public boolean isActivePointer(int pointerId) { final int pointerFlag = (1 << pointerId); return (mActivePointers & 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 getPrimaryActivePointerId() { if (mPrimaryActivePointerId == INVALID_POINTER_ID) { mPrimaryActivePointerId = findPrimaryActivePointer(); } return mPrimaryActivePointerId; } /** * @return The time when the last up received pointer went down. */ public long getLastReceivedUpPointerDownTime() { return mLastReceivedUpPointerDownTime; } /** * @return The id of the last received pointer that went up. */ public int getLastReceivedUpPointerId() { return mLastReceivedUpPointerId; } /** * @return Whether the last received pointer that went up was active. */ public boolean wasLastReceivedUpPointerActive() { return mLastReceivedUpPointerActive; } /** * @return The time of the last injected down event. */ public long getLastInjectedDownEventTime() { return mLastInjectedDownEventTime; } /** * @return The action of the last injected hover event. */ public int getLastInjectedHoverAction() { return mLastInjectedHoverEventAction; } /** * Populates the active pointer IDs to the given array. * <p> * Note: The client is responsible for providing large enough array. * * @param outPointerIds The array to which to write the active pointers. */ public void populateActivePointerIds(int[] outPointerIds) { int index = 0; for (int idBits = mActivePointers; idBits != 0; ) { final int id = Integer.numberOfTrailingZeros(idBits); idBits &= ~(1 << id); outPointerIds[index] = id; index++; } } /** * @return The number of non injected active pointers. */ public int getNotInjectedActivePointerCount() { final int pointerState = mActivePointers & ~mInjectedPointersDown; return Integer.bitCount(pointerState); } /** * @param pointerId The unique pointer id. * @return Whether the pointer is active or was the last active than went up. */ private boolean isActiveOrWasLastActiveUpPointer(int pointerId) { return (isActivePointer(pointerId) || (mLastReceivedUpPointerId == pointerId && mLastReceivedUpPointerActive)); } /** * 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); mLastReceivedUpPointerId = 0; mLastReceivedUpPointerDownTime = 0; mLastReceivedUpPointerActive = false; mReceivedPointersDown |= pointerFlag; mReceivedPointerDownX[pointerId] = event.getX(pointerIndex); mReceivedPointerDownY[pointerId] = event.getY(pointerIndex); mReceivedPointerDownTime[pointerId] = event.getEventTime(); if (!mHasMovingActivePointer) { // If still no moving active pointers every // down pointer is the only active one. mActivePointers = pointerFlag; mPrimaryActivePointerId = pointerId; } else { // If at least one moving active pointer every // subsequent down pointer is active. mActivePointers |= pointerFlag; } } /** * Handles a received pointer move event. * * @param event The event to be handled. */ private void handleReceivedPointerMove(MotionEvent event) { detectActivePointers(event); } /** * 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); mLastReceivedUpPointerId = pointerId; mLastReceivedUpPointerDownTime = getReceivedPointerDownTime(pointerId); mLastReceivedUpPointerActive = isActivePointer(pointerId); mReceivedPointersDown &= ~pointerFlag; mActivePointers &= ~pointerFlag; mReceivedPointerDownX[pointerId] = 0; mReceivedPointerDownY[pointerId] = 0; mReceivedPointerDownTime[pointerId] = 0; if (mActivePointers == 0) { mHasMovingActivePointer = false; } if (mPrimaryActivePointerId == pointerId) { mPrimaryActivePointerId = INVALID_POINTER_ID; } } /** * Handles a injected pointer down event. * * @param pointerIndex The index of the pointer that has changed. * @param event The event to be handled. */ private void handleInjectedPointerDown(int pointerIndex, MotionEvent event) { final int pointerId = event.getPointerId(pointerIndex); final int pointerFlag = (1 << pointerId); mInjectedPointersDown |= pointerFlag; } /** * Handles a injected pointer up event. * * @param pointerIndex The index of the pointer that has changed. * @param event The event to be handled. */ private void handleInjectedPointerUp(int pointerIndex, MotionEvent event) { final int pointerId = event.getPointerId(pointerIndex); final int pointerFlag = (1 << pointerId); mInjectedPointersDown &= ~pointerFlag; if (mInjectedPointersDown == 0) { mLastInjectedDownEventTime = 0; } } /** * Detects the active pointers in an event. * * @param event The event to examine. */ private void detectActivePointers(MotionEvent event) { for (int i = 0, count = event.getPointerCount(); i < count; i++) { final int pointerId = event.getPointerId(i); if (mHasMovingActivePointer) { // If already active => nothing to do. if (isActivePointer(pointerId)) { continue; } } // Active pointers are ones that moved more than a given threshold. final float pointerDeltaMove = computePointerDeltaMove(i, event); if (pointerDeltaMove > mThresholdActivePointer) { final int pointerFlag = (1 << pointerId); mActivePointers |= pointerFlag; mHasMovingActivePointer = true; } } } /** * @return The primary active pointer. */ private int findPrimaryActivePointer() { int primaryActivePointerId = INVALID_POINTER_ID; long minDownTime = Long.MAX_VALUE; // Find the active pointer that went down first. for (int i = 0, count = mReceivedPointerDownTime.length; i < count; i++) { if (isActivePointer(i)) { final long downPointerTime = mReceivedPointerDownTime[i]; if (downPointerTime < minDownTime) { minDownTime = downPointerTime; primaryActivePointerId = i; } } } return primaryActivePointerId; } /** * Computes the move for a given action pointer index since the * corresponding pointer went down. * * @param pointerIndex The action pointer index. * @param event The event to examine. * @return The distance the pointer has moved. */ private float computePointerDeltaMove(int pointerIndex, MotionEvent event) { final int pointerId = event.getPointerId(pointerIndex); final float deltaX = event.getX(pointerIndex) - mReceivedPointerDownX[pointerId]; final float deltaY = event.getY(pointerIndex) - mReceivedPointerDownY[pointerId]; return (float) Math.hypot(deltaX, deltaY); } @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("\nActive pointers #"); builder.append(getActivePointerCount()); builder.append(" [ "); for (int i = 0; i < MAX_POINTER_COUNT; i++) { if (isActivePointer(i)) { builder.append(i); builder.append(" "); } } builder.append("]"); builder.append("\nPrimary active pointer id [ "); builder.append(getPrimaryActivePointerId()); builder.append(" ]"); builder.append("\n========================="); return builder.toString(); } } /** * Class for delayed sending of long press. */ private final class PerformLongPressDelayed implements Runnable { private MotionEvent mEvent; private int mPolicyFlags; public void post(MotionEvent prototype, int policyFlags, long delay) { mEvent = MotionEvent.obtain(prototype); mPolicyFlags = policyFlags; mHandler.postDelayed(this, delay); } public void remove() { if (isPenidng()) { mHandler.removeCallbacks(this); clear(); } } private boolean isPenidng() { return (mEvent != null); } @Override public void run() { mCurrentState = STATE_DELEGATING; // Make sure the scheduled hover exit is delivered. mSendHoverDelayed.remove(); final int pointerId = mPointerTracker.getPrimaryActivePointerId(); final int pointerIdBits = (1 << pointerId); ensureHoverExitSent(mEvent, pointerIdBits, mPolicyFlags); sendDownForAllActiveNotInjectedPointers(mEvent, mPolicyFlags); mTouchExploreGestureInProgress = false; mLastTouchExploreEvent = null; clear(); } private void clear() { if (!isPenidng()) { return; } mEvent.recycle(); mEvent = null; mPolicyFlags = 0; } } /** * Class for delayed sending of hover events. */ private final class SendHoverDelayed implements Runnable { private static final String LOG_TAG = "SendHoverEnterOrExitDelayed"; private MotionEvent mEvent; private int mAction; private int mPointerIdBits; private int mPolicyFlags; public void post(MotionEvent prototype, int action, int pointerIdBits, int policyFlags, long delay) { remove(); mEvent = MotionEvent.obtain(prototype); mAction = action; mPointerIdBits = pointerIdBits; mPolicyFlags = policyFlags; mHandler.postDelayed(this, delay); } public void remove() { mHandler.removeCallbacks(this); clear(); } private boolean isPenidng() { return (mEvent != null); } private void clear() { if (!isPenidng()) { return; } mEvent.recycle(); mEvent = null; mAction = 0; mPointerIdBits = -1; mPolicyFlags = 0; } public void forceSendAndRemove() { if (isPenidng()) { run(); remove(); } } public void run() { if (DEBUG) { if (mAction == MotionEvent.ACTION_HOVER_ENTER) { Slog.d(LOG_TAG, "Injecting: " + MotionEvent.ACTION_HOVER_ENTER); } else if (mAction == MotionEvent.ACTION_HOVER_MOVE) { Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_MOVE"); } else if (mAction == MotionEvent.ACTION_HOVER_EXIT) { Slog.d(LOG_TAG, "Injecting: MotionEvent.ACTION_HOVER_EXIT"); } } sendMotionEvent(mEvent, mAction, mPointerIdBits, mPolicyFlags); clear(); } } }