/*
 ** 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.gesture.Gesture;
import android.gesture.GestureLibraries;
import android.gesture.GestureLibrary;
import android.gesture.GesturePoint;
import android.gesture.GestureStore;
import android.gesture.GestureStroke;
import android.gesture.Prediction;
import android.graphics.Rect;
import android.os.Handler;
import android.os.SystemClock;
import android.util.Slog;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
import android.view.WindowManagerPolicy;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;

import com.android.internal.R;

import java.util.ArrayList;
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 slow around performs touch exploration.</li>
 *   <li>2. One finger moving fast around performs gestures.</li>
 *   <li>3. Two close fingers moving in the same direction perform a drag.</li>
 *   <li>4. Multi-finger gestures are delivered to view hierarchy.</li>
 *   <li>5. Pointers that have not moved more than a specified distance after they
 *          went down are considered inactive.</li>
 *   <li>6. Two fingers moving in different directions are considered a multi-finger gesture.</li>
 *   <li>7. Double tapping clicks on the on the last touch explored location of it was in
 *          a window that does not take focus, otherwise the click is within the accessibility
 *          focused rectangle.</li>
 *   <li>7. Tapping and holding for a while performs a long press in a similar fashion
 *          as the click above.</li>
 * <ol>
 *
 * @hide
 */
class TouchExplorer implements EventStreamTransformation {

    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;

    // 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)

    // 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 velocity above which we detect gestures.
    private static final int GESTURE_DETECTION_VELOCITY_DIP = 1000;

    // 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;

    // Temporary array for storing pointer IDs.
    private final int[] mTempPointerIds = new int[MAX_POINTER_COUNT];

    // Timeout before trying to decide what the user is trying to do.
    private final int mDetermineUserIntentTimeout;

    // Timeout within which we try to detect a tap.
    private final int mTapTimeout;

    // Timeout within which we try to detect a double tap.
    private final int mDoubleTapTimeout;

    // Slop between the down and up tap to be a tap.
    private final int mTouchSlop;

    // 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 event.
    private final SendHoverDelayed mSendHoverEnterDelayed;

    // Command for delayed sending of a hover exit event.
    private final SendHoverDelayed 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 delayed sending of a long press.
    private final PerformLongPressDelayed mPerformLongPressDelayed;

    // Command for exiting gesture detection mode after a timeout.
    private final ExitGestureDetectionModeDelayed mExitGestureDetectionModeDelayed;

    // Helper to detect and react to double tap in touch explore mode.
    private final DoubleTapDetector mDoubleTapDetector;

    // 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 scaled velocity above which we detect gestures.
    private final int mScaledGestureDetectionVelocity;

    // The handler to which to delegate events.
    private EventStreamTransformation mNext;

    // Helper to track gesture velocity.
    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();

    // 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 rectangle to avoid instantiation.
    private final Rect mTempRect = new Rect();

    // Context in which this explorer operates.
    private final Context mContext;

    // The X of the previous event.
    private float mPreviousX;

    // The Y of the previous event.
    private float mPreviousY;

    // Buffer for storing points for gesture detection.
    private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100);

    // The minimal delta between moves to add a gesture point.
    private static final int TOUCH_TOLERANCE = 3;

    // The minimal score for accepting a predicted gesture.
    private static final float MIN_PREDICTION_SCORE = 2.0f;

    // The library for gesture detection.
    private GestureLibrary mGestureLibrary;

    // 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(context);
        mInjectedPointerTracker = new InjectedPointerTracker();
        mTapTimeout = ViewConfiguration.getTapTimeout();
        mDetermineUserIntentTimeout = ViewConfiguration.getDoubleTapTimeout();
        mDoubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mDoubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
        mHandler = new Handler(context.getMainLooper());
        mPerformLongPressDelayed = new PerformLongPressDelayed();
        mExitGestureDetectionModeDelayed = new ExitGestureDetectionModeDelayed();
        mGestureLibrary = GestureLibraries.fromRawResource(context, R.raw.accessibility_gestures);
        mGestureLibrary.setOrientationStyle(8);
        mGestureLibrary.setSequenceType(GestureStore.SEQUENCE_SENSITIVE);
        mGestureLibrary.load();
        mSendHoverEnterDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_ENTER, true);
        mSendHoverExitDelayed = new SendHoverDelayed(MotionEvent.ACTION_HOVER_EXIT, false);
        mSendTouchExplorationEndDelayed = new SendAccessibilityEventDelayed(
                AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END,
                mDetermineUserIntentTimeout);
        mSendTouchInteractionEndDelayed = new SendAccessibilityEventDelayed(
                AccessibilityEvent.TYPE_TOUCH_INTERACTION_END,
                mDetermineUserIntentTimeout);
        mDoubleTapDetector = new DoubleTapDetector();
        final float density = context.getResources().getDisplayMetrics().density;
        mScaledMinPointerDistanceToUseMiddleLocation =
            (int) (MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP * density);
        mScaledGestureDetectionVelocity = (int) (GESTURE_DETECTION_VELOCITY_DIP * density);
    }

    public 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);
        }
    }

    public void onDestroy() {
        // TODO: Implement
    }

    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: {
                // Clear the current stroke.
                mStrokeBuffer.clear();
            } break;
        }
        // Remove all pending callbacks.
        mSendHoverEnterDelayed.remove();
        mSendHoverExitDelayed.remove();
        mPerformLongPressDelayed.remove();
        mExitGestureDetectionModeDelayed.remove();
        mSendTouchExplorationEndDelayed.remove();
        mSendTouchInteractionEndDelayed.remove();
        // Reset the pointer trackers.
        mReceivedPointerTracker.clear();
        mInjectedPointerTracker.clear();
        // Clear the double tap detector
        mDoubleTapDetector.clear();
        // Go to initial state.
        // Clear the long pressing pointer remap data.
        mLongPressingPointerId = -1;
        mLongPressingPointerDeltaX = 0;
        mLongPressingPointerDeltaY = 0;
        mCurrentState = STATE_TOUCH_EXPLORING;
        if (mNext != null) {
            mNext.clear();
        }
        mTouchExplorationInProgress = false;
        mAms.onTouchInteractionEnd();
    }

    @Override
    public void setNext(EventStreamTransformation next) {
        mNext = next;
    }

    @Override
    public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
        if (DEBUG) {
            Slog.d(LOG_TAG, "Received event: " + event + ", policyFlags=0x"
                    + Integer.toHexString(policyFlags));
            Slog.d(LOG_TAG, getStateSymbolicName(mCurrentState));
        }

        mReceivedPointerTracker.onMotionEvent(rawEvent);

        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: {
                handleMotionEventGestureDetecting(rawEvent, policyFlags);
            } break;
            default:
                throw new IllegalStateException("Illegal state: " + mCurrentState);
        }
    }

    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.remove();
            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.remove();
            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);
        }
    }

    /**
     * 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;
        final int activePointerCount = receivedTracker.getActivePointerCount();

        mVelocityTracker.addMovement(rawEvent);

        mDoubleTapDetector.onMotionEvent(event, policyFlags);

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mAms.onTouchInteractionStart();
                // Pre-feed the motion events to the gesture detector since we
                // have a distance slop before getting into gesture detection
                // mode and not using the points within this slop significantly
                // decreases the quality of gesture recognition.
                handleMotionEventGestureDetecting(rawEvent, policyFlags);
                //$FALL-THROUGH$
            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: {
                        // 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.
                        if (mSendHoverEnterDelayed.isPending()) {
                            mSendHoverEnterDelayed.remove();
                            mSendHoverExitDelayed.remove();
                        }

                        if (mSendTouchExplorationEndDelayed.isPending()) {
                            mSendTouchExplorationEndDelayed.forceSendAndRemove();
                        }

                        if (mSendTouchInteractionEndDelayed.isPending()) {
                            mSendTouchInteractionEndDelayed.forceSendAndRemove();
                        }

                        // Every pointer that goes down is active until it moves or
                        // another one goes down. Hence, having more than one pointer
                        // down we have already send the interaction start event.
                        if (event.getPointerCount() == 1) {
                            sendAccessibilityEvent(
                                    AccessibilityEvent.TYPE_TOUCH_INTERACTION_START);
                        }

                        mPerformLongPressDelayed.remove();

                        // If we have the first tap schedule a long press and break
                        // since we do not want to schedule hover enter because
                        // the delayed callback will kick in before the long click.
                        // This would lead to a state transition resulting in long
                        // pressing the item below the double taped area which is
                        // not necessary where accessibility focus is.
                        if (mDoubleTapDetector.firstTapDetected()) {
                            // We got a tap now post a long press action.
                            mPerformLongPressDelayed.post(event, policyFlags);
                            break;
                        }
                        if (!mTouchExplorationInProgress) {
                            // Deliver hover enter with a delay to have a chance
                            // to detect what the user is trying to do.
                            final int pointerId = receivedTracker.getPrimaryActivePointerId();
                            final int pointerIdBits = (1 << pointerId);
                            mSendHoverEnterDelayed.post(event, true, pointerIdBits, policyFlags);
                        }
                    } break;
                    default: {
                        /* do nothing - let the code for ACTION_MOVE decide what to do */
                    } break;
                }
            } break;
            case MotionEvent.ACTION_MOVE: {
                final int pointerId = receivedTracker.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: {
                        // We have not started sending events since we try to
                        // figure out what the user is doing.
                        if (mSendHoverEnterDelayed.isPending()) {
                            // Pre-feed the motion events to the gesture detector since we
                            // have a distance slop before getting into gesture detection
                            // mode and not using the points within this slop significantly
                            // decreases the quality of gesture recognition.
                            handleMotionEventGestureDetecting(rawEvent, policyFlags);
                            // 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);
                            // The user has moved enough for us to decide.
                            if (moveDelta > mDoubleTapSlop) {
                                // Check whether the user is performing a gesture. We
                                // detect gestures if the pointer is moving above a
                                // given velocity.
                                mVelocityTracker.computeCurrentVelocity(1000);
                                final float maxAbsVelocity = Math.max(
                                        Math.abs(mVelocityTracker.getXVelocity(pointerId)),
                                        Math.abs(mVelocityTracker.getYVelocity(pointerId)));
                                if (maxAbsVelocity > mScaledGestureDetectionVelocity) {
                                    // We have to perform gesture detection, so
                                    // clear the current state and try to detect.
                                    mCurrentState = STATE_GESTURE_DETECTING;
                                    mVelocityTracker.clear();
                                    mSendHoverEnterDelayed.remove();
                                    mSendHoverExitDelayed.remove();
                                    mPerformLongPressDelayed.remove();
                                    mExitGestureDetectionModeDelayed.post();
                                    // Send accessibility event to announce the start
                                    // of gesture recognition.
                                    sendAccessibilityEvent(
                                            AccessibilityEvent.TYPE_GESTURE_DETECTION_START);
                                } else {
                                    // We have just decided that the user is touch,
                                    // exploring so start sending events.
                                    mSendHoverEnterDelayed.forceSendAndRemove();
                                    mSendHoverExitDelayed.remove();
                                    mPerformLongPressDelayed.remove();
                                    sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE,
                                            pointerIdBits, policyFlags);
                                }
                                break;
                            }
                        } else {
                            // Cancel the long press if pending and the user
                            // moved more than the slop.
                            if (mPerformLongPressDelayed.isPending()) {
                                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);
                                // The user has moved enough for us to decide.
                                if (moveDelta > mTouchSlop) {
                                    mPerformLongPressDelayed.remove();
                                }
                            }
                            // The user is wither double tapping or performing long
                            // press so do not send move events yet.
                            if (mDoubleTapDetector.firstTapDetected()) {
                                break;
                            }
                            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 (mSendHoverEnterDelayed.isPending()) {
                            // We have not started sending events so cancel
                            // scheduled sending events.
                            mSendHoverEnterDelayed.remove();
                            mSendHoverExitDelayed.remove();
                            mPerformLongPressDelayed.remove();
                        } else {
                            mPerformLongPressDelayed.remove();
                            // 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);
                        }

                        // We know that a new state transition is to happen and the
                        // new state will not be gesture recognition, so clear the
                        // stashed gesture strokes.
                        mStrokeBuffer.clear();

                        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;
                            sendDownForAllActiveNotInjectedPointers(event, policyFlags);
                        }
                        mVelocityTracker.clear();
                    } 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 (mSendHoverEnterDelayed.isPending()) {
                            // We have not started sending events so cancel
                            // scheduled sending events.
                            mSendHoverEnterDelayed.remove();
                            mSendHoverExitDelayed.remove();
                            mPerformLongPressDelayed.remove();
                        } else {
                            mPerformLongPressDelayed.remove();
                            // 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;
                        sendDownForAllActiveNotInjectedPointers(event, policyFlags);
                        mVelocityTracker.clear();
                    }
                }
            } break;
            case MotionEvent.ACTION_UP:
                mAms.onTouchInteractionEnd();
                // We know that we do not need the pre-fed gesture points are not
                // needed anymore since the last pointer just went up.
                mStrokeBuffer.clear();
                //$FALL-THROUGH$
            case MotionEvent.ACTION_POINTER_UP: {
                final int pointerId = receivedTracker.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 (!receivedTracker.wasLastReceivedUpPointerActive()) {
                            break;
                        }

                        mPerformLongPressDelayed.remove();

                        // If we have not delivered the enter schedule exit.
                        if (mSendHoverEnterDelayed.isPending()) {
                            mSendHoverExitDelayed.post(event, false, pointerIdBits, policyFlags);
                        } else {
                            // The user is touch exploring so we send events for end.
                            sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
                        }

                        if (!mSendTouchInteractionEndDelayed.isPending()) {
                            mSendTouchInteractionEndDelayed.post();
                        }
                    } break;
                }
                mVelocityTracker.clear();
            } break;
            case MotionEvent.ACTION_CANCEL: {
                clear(event, policyFlags);
            } break;
        }
    }

    /**
     * Handles a motion event in dragging state.
     *
     * @param event The event to be handled.
     * @param policyFlags The policy flags associated with the event.
     */
    private void handleMotionEventStateDragging(MotionEvent event, 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;
                if (mDraggingPointerId != INVALID_POINTER_ID) {
                    sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags);
                }
                sendDownForAllActiveNotInjectedPointers(event, policyFlags);
            } break;
            case MotionEvent.ACTION_MOVE: {
                final int activePointerCount = mReceivedPointerTracker.getActivePointerCount();
                switch (activePointerCount) {
                    case 1: {
                        // do nothing
                    } break;
                    case 2: {
                        if (isDraggingGesture(event)) {
                            // If the dragging pointer are closer that a given distance we
                            // use the location of the primary one. Otherwise, we take the
                            // middle between the pointers.
                            int[] pointerIds = mTempPointerIds;
                            mReceivedPointerTracker.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);

                            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 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 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;
            case MotionEvent.ACTION_CANCEL: {
                clear(event, policyFlags);
            } break;
        }
    }

    /**
     * Handles a motion event in delegating state.
     *
     * @param event The event to be handled.
     * @param policyFlags The policy flags associated with the event.
     */
    private void handleMotionEventStateDelegating(MotionEvent event, 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_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 = getNotInjectedActivePointerCount(
                        mReceivedPointerTracker, mInjectedPointerTracker);
                if (notInjectedCount > 0) {
                    MotionEvent prototype = MotionEvent.obtain(event);
                    sendDownForAllActiveNotInjectedPointers(prototype, policyFlags);
                }
            } break;
            case MotionEvent.ACTION_UP:
                // Announce the end of a new touch interaction.
                sendAccessibilityEvent(
                        AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
                //$FALL-THROUGH$
            case MotionEvent.ACTION_POINTER_UP: {
                mAms.onTouchInteractionEnd();
                mLongPressingPointerId = -1;
                mLongPressingPointerDeltaX = 0;
                mLongPressingPointerDeltaY = 0;
                // No active pointers => go to initial state.
                if (mReceivedPointerTracker.getActivePointerCount() == 0) {
                    mCurrentState = STATE_TOUCH_EXPLORING;
                }
            } break;
            case MotionEvent.ACTION_CANCEL: {
                clear(event, policyFlags);
            } break;
        }
        // Deliver the event striping out inactive pointers.
        sendMotionEventStripInactivePointers(event, policyFlags);
    }

    private void handleMotionEventGestureDetecting(MotionEvent event, int policyFlags) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                final float x = event.getX();
                final float y = event.getY();
                mPreviousX = x;
                mPreviousY = y;
                mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
            } break;
            case MotionEvent.ACTION_MOVE: {
                final float x = event.getX();
                final float y = event.getY();
                final float dX = Math.abs(x - mPreviousX);
                final float dY = Math.abs(y - mPreviousY);
                if (dX >= TOUCH_TOLERANCE || dY >= TOUCH_TOLERANCE) {
                    mPreviousX = x;
                    mPreviousY = y;
                    mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
                }
            } break;
            case MotionEvent.ACTION_UP: {
                mAms.onTouchInteractionEnd();
                // Announce the end of gesture recognition.
                sendAccessibilityEvent(
                        AccessibilityEvent.TYPE_GESTURE_DETECTION_END);
                // Announce the end of a new touch interaction.
                sendAccessibilityEvent(
                        AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);

                float x = event.getX();
                float y = event.getY();
                mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));

                Gesture gesture = new Gesture();
                gesture.addStroke(new GestureStroke(mStrokeBuffer));

                ArrayList<Prediction> predictions = mGestureLibrary.recognize(gesture);
                if (!predictions.isEmpty()) {
                    Prediction bestPrediction = predictions.get(0);
                    if (bestPrediction.score >= MIN_PREDICTION_SCORE) {
                        if (DEBUG) {
                            Slog.i(LOG_TAG, "gesture: " + bestPrediction.name + " score: "
                                    + bestPrediction.score);
                        }
                        try {
                            final int gestureId = Integer.parseInt(bestPrediction.name);
                            mAms.onGesture(gestureId);
                        } catch (NumberFormatException nfe) {
                            Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name);
                        }
                    }
                }

                mStrokeBuffer.clear();
                mExitGestureDetectionModeDelayed.remove();
                mCurrentState = STATE_TOUCH_EXPLORING;
            } break;
            case MotionEvent.ACTION_CANCEL: {
                clear(event, policyFlags);
            } break;
        }
    }

    /**
     * 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);
            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 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) {
        ReceivedPointerTracker receivedPointers = mReceivedPointerTracker;
        InjectedPointerTracker injectedPointers = mInjectedPointerTracker;
        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 (injectedPointers.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 (!receivedPointers.isActivePointer(pointerId)) {
                continue;
            }
            // Do not send event for already delivered pointers.
            if (injectedPointers.isInjectedPointerDown(pointerId)) {
                continue;
            }
            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 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 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 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) {
        ReceivedPointerTracker receivedTracker = mReceivedPointerTracker;

        // All pointers active therefore we just inject the event as is.
        if (prototype.getPointerCount() == receivedTracker.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 (receivedTracker.getActivePointerCount() == 0
                && !receivedTracker.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 (!receivedTracker.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 (receivedTracker.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(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) {
            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 -= mLongPressingPointerDeltaX;
                    coords[i].y -= mLongPressingPointerDeltaY;
                }
            }
            MotionEvent remapped = 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());
            if (event != prototype) {
                event.recycle();
            }
            event = remapped;
        }

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

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

    private class DoubleTapDetector {
        private MotionEvent mDownEvent;
        private MotionEvent mFirstTapEvent;

        public void onMotionEvent(MotionEvent event, int policyFlags) {
            final int actionIndex = event.getActionIndex();
            final int action = event.getActionMasked();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                case MotionEvent.ACTION_POINTER_DOWN: {
                    if (mFirstTapEvent != null
                            && !GestureUtils.isSamePointerContext(mFirstTapEvent, event)) {
                        clear();
                    }
                    mDownEvent = MotionEvent.obtain(event);
                } break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_POINTER_UP: {
                    if (mDownEvent == null) {
                        return;
                    }
                    if (!GestureUtils.isSamePointerContext(mDownEvent, event)) {
                        clear();
                        return;
                    }
                    if (GestureUtils.isTap(mDownEvent, event, mTapTimeout, mTouchSlop,
                            actionIndex)) {
                        if (mFirstTapEvent == null || GestureUtils.isTimedOut(mFirstTapEvent,
                                event, mDoubleTapTimeout)) {
                            mFirstTapEvent = MotionEvent.obtain(event);
                            mDownEvent.recycle();
                            mDownEvent = null;
                            return;
                        }
                        if (GestureUtils.isMultiTap(mFirstTapEvent, event, mDoubleTapTimeout,
                                mDoubleTapSlop, actionIndex)) {
                            onDoubleTap(event, policyFlags);
                            mFirstTapEvent.recycle();
                            mFirstTapEvent = null;
                            mDownEvent.recycle();
                            mDownEvent = null;
                            return;
                        }
                        mFirstTapEvent.recycle();
                        mFirstTapEvent = null;
                    } else {
                        if (mFirstTapEvent != null) {
                            mFirstTapEvent.recycle();
                            mFirstTapEvent = null;
                        }
                    }
                    mDownEvent.recycle();
                    mDownEvent = null;
                } break;
            }
        }

        public void onDoubleTap(MotionEvent secondTapUp, int policyFlags) {
            // This should never be called when more than two pointers are down.
            if (secondTapUp.getPointerCount() > 2) {
                return;
            }

            // Remove pending event deliveries.
            mSendHoverEnterDelayed.remove();
            mSendHoverExitDelayed.remove();
            mPerformLongPressDelayed.remove();

            if (mSendTouchExplorationEndDelayed.isPending()) {
                mSendTouchExplorationEndDelayed.forceSendAndRemove();
            }
            if (mSendTouchInteractionEndDelayed.isPending()) {
                mSendTouchInteractionEndDelayed.forceSendAndRemove();
            }

            int clickLocationX;
            int clickLocationY;

            final int pointerId = secondTapUp.getPointerId(secondTapUp.getActionIndex());
            final int pointerIndex = secondTapUp.findPointerIndex(pointerId);

            MotionEvent lastExploreEvent =
                mInjectedPointerTracker.getLastInjectedHoverEventForClick();
            if (lastExploreEvent == null) {
                // No last touch explored event but there is accessibility focus in
                // the active window. We click in the middle of the focus bounds.
                Rect focusBounds = mTempRect;
                if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) {
                    clickLocationX = focusBounds.centerX();
                    clickLocationY = focusBounds.centerY();
                } else {
                    // Out of luck - do nothing.
                    return;
                }
            } else {
                // If the click is within the active window but not within the
                // accessibility focus bounds we click in the focus center.
                final int lastExplorePointerIndex = lastExploreEvent.getActionIndex();
                clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex);
                clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex);
                Rect activeWindowBounds = mTempRect;
                if (mLastTouchedWindowId == mAms.getActiveWindowId()) {
                    mAms.getActiveWindowBounds(activeWindowBounds);
                    if (activeWindowBounds.contains(clickLocationX, clickLocationY)) {
                        Rect focusBounds = mTempRect;
                        if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) {
                            if (!focusBounds.contains(clickLocationX, clickLocationY)) {
                                clickLocationX = focusBounds.centerX();
                                clickLocationY = focusBounds.centerY();
                            }
                        }
                    }
                }
            }

            // Do the click.
            PointerProperties[] properties = new PointerProperties[1];
            properties[0] = new PointerProperties();
            secondTapUp.getPointerProperties(pointerIndex, properties[0]);
            PointerCoords[] coords = new PointerCoords[1];
            coords[0] = new PointerCoords();
            coords[0].x = clickLocationX;
            coords[0].y = clickLocationY;
            MotionEvent event = MotionEvent.obtain(secondTapUp.getDownTime(),
                    secondTapUp.getEventTime(), MotionEvent.ACTION_DOWN, 1, properties,
                    coords, 0, 0, 1.0f, 1.0f, secondTapUp.getDeviceId(), 0,
                    secondTapUp.getSource(), secondTapUp.getFlags());
            sendActionDownAndUp(event, policyFlags);
            event.recycle();
        }

        public void clear() {
            if (mDownEvent != null) {
                mDownEvent.recycle();
                mDownEvent = null;
            }
            if (mFirstTapEvent != null) {
                mFirstTapEvent.recycle();
                mFirstTapEvent = null;
            }
        }

        public boolean firstTapDetected() {
            return mFirstTapEvent != null
                && SystemClock.uptimeMillis() - mFirstTapEvent.getEventTime() < mDoubleTapTimeout;
        }
    }

    /**
     * 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;
        int[] pointerIds = mTempPointerIds;
        receivedTracker.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);

        final float firstPtrDownX = receivedTracker.getReceivedPointerDownX(firstPtrIndex);
        final float firstPtrDownY = receivedTracker.getReceivedPointerDownY(firstPtrIndex);
        final float secondPtrDownX = receivedTracker.getReceivedPointerDownX(secondPtrIndex);
        final float secondPtrDownY = receivedTracker.getReceivedPointerDownY(secondPtrIndex);

        return GestureUtils.isDraggingGesture(firstPtrDownX, firstPtrDownY, secondPtrDownX,
                secondPtrDownY, firstPtrX, firstPtrY, secondPtrX, secondPtrY,
                MAX_DRAGGING_ANGLE_COS);
    }

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

    /**
     * @return The number of non injected active pointers.
     */
    private int getNotInjectedActivePointerCount(ReceivedPointerTracker receivedTracker,
            InjectedPointerTracker injectedTracker) {
        final int pointerState = receivedTracker.getActivePointers()
                & ~injectedTracker.getInjectedPointersDown();
        return Integer.bitCount(pointerState);
    }

    /**
     * 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 remove() {
            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 long press.
     */
    private final class PerformLongPressDelayed implements Runnable {
        private MotionEvent mEvent;
        private int mPolicyFlags;

        public void post(MotionEvent prototype, int policyFlags) {
            mEvent = MotionEvent.obtain(prototype);
            mPolicyFlags = policyFlags;
            mHandler.postDelayed(this, ViewConfiguration.getLongPressTimeout());
        }

        public void remove() {
            if (isPending()) {
                mHandler.removeCallbacks(this);
                clear();
            }
        }

        public boolean isPending() {
            return (mEvent != null);
        }

        @Override
        public void run() {
            // Active pointers should not be zero when running this command.
            if (mReceivedPointerTracker.getActivePointerCount() == 0) {
                return;
            }

            int clickLocationX;
            int clickLocationY;

            final int pointerId = mEvent.getPointerId(mEvent.getActionIndex());
            final int pointerIndex = mEvent.findPointerIndex(pointerId);

            MotionEvent lastExploreEvent =
                mInjectedPointerTracker.getLastInjectedHoverEventForClick();
            if (lastExploreEvent == null) {
                // No last touch explored event but there is accessibility focus in
                // the active window. We click in the middle of the focus bounds.
                Rect focusBounds = mTempRect;
                if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) {
                    clickLocationX = focusBounds.centerX();
                    clickLocationY = focusBounds.centerY();
                } else {
                    // Out of luck - do nothing.
                    return;
                }
            } else {
                // If the click is within the active window but not within the
                // accessibility focus bounds we click in the focus center.
                final int lastExplorePointerIndex = lastExploreEvent.getActionIndex();
                clickLocationX = (int) lastExploreEvent.getX(lastExplorePointerIndex);
                clickLocationY = (int) lastExploreEvent.getY(lastExplorePointerIndex);
                Rect activeWindowBounds = mTempRect;
                if (mLastTouchedWindowId == mAms.getActiveWindowId()) {
                    mAms.getActiveWindowBounds(activeWindowBounds);
                    if (activeWindowBounds.contains(clickLocationX, clickLocationY)) {
                        Rect focusBounds = mTempRect;
                        if (mAms.getAccessibilityFocusBoundsInActiveWindow(focusBounds)) {
                            if (!focusBounds.contains(clickLocationX, clickLocationY)) {
                                clickLocationX = focusBounds.centerX();
                                clickLocationY = focusBounds.centerY();
                            }
                        }
                    }
                }
            }

            mLongPressingPointerId = pointerId;
            mLongPressingPointerDeltaX = (int) mEvent.getX(pointerIndex) - clickLocationX;
            mLongPressingPointerDeltaY = (int) mEvent.getY(pointerIndex) - clickLocationY;

            sendHoverExitAndTouchExplorationGestureEndIfNeeded(mPolicyFlags);

            mCurrentState = STATE_DELEGATING;
            sendDownForAllActiveNotInjectedPointers(mEvent, mPolicyFlags);
            clear();
        }

        private void clear() {
            if (!isPending()) {
                return;
            }
            mEvent.recycle();
            mEvent = null;
            mPolicyFlags = 0;
        }
    }

    /**
     * Class for delayed sending of hover events.
     */
    class SendHoverDelayed implements Runnable {
        private final String LOG_TAG_SEND_HOVER_DELAYED = SendHoverDelayed.class.getName();

        private final int mHoverAction;
        private final boolean mGestureStarted;

        private MotionEvent mPrototype;
        private int mPointerIdBits;
        private int mPolicyFlags;

        public SendHoverDelayed(int hoverAction, boolean gestureStarted) {
            mHoverAction = hoverAction;
            mGestureStarted = gestureStarted;
        }

        public void post(MotionEvent prototype, boolean touchExplorationInProgress,
                int pointerIdBits, int policyFlags) {
            remove();
            mPrototype = MotionEvent.obtain(prototype);
            mPointerIdBits = pointerIdBits;
            mPolicyFlags = policyFlags;
            mHandler.postDelayed(this, mDetermineUserIntentTimeout);
        }

        public float getX() {
            if (isPending()) {
                return mPrototype.getX();
            }
            return 0;
        }

        public float getY() {
            if (isPending()) {
                return mPrototype.getY();
            }
            return 0;
        }

        public void remove() {
            mHandler.removeCallbacks(this);
            clear();
        }

        private boolean isPending() {
            return (mPrototype != null);
        }

        private void clear() {
            if (!isPending()) {
                return;
            }
            mPrototype.recycle();
            mPrototype = null;
            mPointerIdBits = -1;
            mPolicyFlags = 0;
        }

        public void forceSendAndRemove() {
            if (isPending()) {
                run();
                remove();
            }
        }

        public void run() {
            if (DEBUG) {
                Slog.d(LOG_TAG_SEND_HOVER_DELAYED, "Injecting motion event: "
                        + MotionEvent.actionToString(mHoverAction));
                Slog.d(LOG_TAG_SEND_HOVER_DELAYED, mGestureStarted ?
                        "touchExplorationGestureStarted" : "touchExplorationGestureEnded");
            }
            if (mGestureStarted) {
                sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START);
            } else {
                if (!mSendTouchExplorationEndDelayed.isPending()) {
                    mSendTouchExplorationEndDelayed.post();
                }
                if (mSendTouchInteractionEndDelayed.isPending()) {
                    mSendTouchInteractionEndDelayed.remove();
                    mSendTouchInteractionEndDelayed.post();
                }
            }
            sendMotionEvent(mPrototype, mHoverAction, mPointerIdBits, mPolicyFlags);
            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 remove() {
            mHandler.removeCallbacks(this);
        }

        public void post() {
            mHandler.postDelayed(this, mDelay);
        }

        public boolean isPending() {
            return mHandler.hasCallbacks(this);
        }

        public void forceSendAndRemove() {
            if (isPending()) {
                run();
                remove();
            }
        }

        @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";

        // 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;

        // The edge flags of the last received down event.
        private int mLastReceivedDownEdgeFlags;

        // 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 the last up pointer data.
        private long mLastReceivedUpPointerDownTime;
        private int mLastReceivedUpPointerId;
        private boolean mLastReceivedUpPointerActive;
        private float mLastReceivedUpPointerDownX;
        private float mLastReceivedUpPointerDownY;

        private MotionEvent mLastReceivedEvent;

        /**
         * Creates a new instance.
         *
         * @param context Context for looking up resources.
         */
        public ReceivedPointerTracker(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;
            mLastReceivedUpPointerDownTime = 0;
            mLastReceivedUpPointerId = 0;
            mLastReceivedUpPointerActive = false;
            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_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_TRACKER, "Received pointer: " + 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);
        }

        /**
         * @return The bits of the pointers that are active.
         */
        public int getActivePointers() {
            return mActivePointers;
        }

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

        /**
         * @return Whether the last received pointer that went up was active.
         */
        public boolean wasLastReceivedUpPointerActive() {
            return mLastReceivedUpPointerActive;
        }
        /**
         * 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++;
            }
        }

        /**
         * @param pointerId The unique pointer id.
         * @return Whether the pointer is active or was the last active than went up.
         */
        public 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;
            mLastReceivedUpPointerDownX = 0;
            mLastReceivedUpPointerDownX = 0;

            mLastReceivedDownEdgeFlags = event.getEdgeFlags();

            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);
            mLastReceivedUpPointerDownX = mReceivedPointerDownX[pointerId];
            mLastReceivedUpPointerDownY = mReceivedPointerDownY[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;
            }
        }

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