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