| /* |
| ** 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(); |
| } |
| } |
| } |