Fix bug of SimulatedTrackball

Simulated trackball should not generate KeyEvent if dispatchGenericMotionEvent
returns true

b/8022205

Change-Id: I1857e25407c508c98ef4db85fe146b1e25a0803e
diff --git a/core/java/android/view/SimulatedDpad.java b/core/java/android/view/SimulatedDpad.java
new file mode 100644
index 0000000..0a37fdd
--- /dev/null
+++ b/core/java/android/view/SimulatedDpad.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2012 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 android.view;
+
+import android.app.SearchManager;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.util.Log;
+
+/**
+ * This class creates DPAD events from touchpad events.
+ * 
+ * @see ViewRootImpl
+ */
+
+//TODO: Make this class an internal class of ViewRootImpl.java
+class SimulatedDpad {
+
+    private static final String TAG = "SimulatedDpad";
+
+    // Maximum difference in milliseconds between the down and up of a touch
+    // event for it to be considered a tap
+    // TODO:Read this value from a configuration file
+    private static final int MAX_TAP_TIME = 250;
+    // Where the cutoff is for determining an edge swipe
+    private static final float EDGE_SWIPE_THRESHOLD = 0.9f;
+    private static final int MSG_FLICK = 313;
+    // TODO: Pass touch slop from the input device
+    private static final int TOUCH_SLOP = 30;
+    // The position of the previous touchpad event
+    private float mLastTouchpadXPosition;
+    private float mLastTouchpadYPosition;
+    // Where the touchpad was initially pressed
+    private float mTouchpadEnterXPosition;
+    private float mTouchpadEnterYPosition;
+    // When the most recent ACTION_HOVER_ENTER occurred
+    private long mLastTouchPadStartTimeMs = 0;
+    // When the most recent direction key was sent
+    private long mLastTouchPadKeySendTimeMs = 0;
+    // When the most recent touch event of any type occurred
+    private long mLastTouchPadEventTimeMs = 0;
+    // Did the swipe begin in a valid region
+    private boolean mEdgeSwipePossible;
+
+    private final Context mContext;
+
+    // How quickly keys were sent;
+    private int mKeySendRateMs = 0;
+    private int mLastKeySent;
+    // Last movement in device screen pixels
+    private float mLastMoveX = 0;
+    private float mLastMoveY = 0;
+    // Offset from the initial touch. Gets reset as direction keys are sent.
+    private float mAccumulatedX;
+    private float mAccumulatedY;
+
+    // Change in position allowed during tap events
+    private float mTouchSlop;
+    private float mTouchSlopSquared;
+    // Has the TouchSlop constraint been invalidated
+    private boolean mAlwaysInTapRegion = true;
+
+    // Information from the most recent event.
+    // Used to determine what device sent the event during a fling.
+    private int mLastSource;
+    private int mLastMetaState;
+    private int mLastDeviceId;
+
+    // TODO: Currently using screen dimensions tuned to a Galaxy Nexus, need to
+    // read this from a config file instead
+    private int mDistancePerTick;
+    private int mDistancePerTickSquared;
+    // Highest rate that the flinged events can occur at before dying out
+    private int mMaxRepeatDelay;
+    // The square of the minimum distance needed for a flick to register
+    private int mMinFlickDistanceSquared;
+    // How quickly the repeated events die off
+    private float mFlickDecay;
+
+    public SimulatedDpad(Context context) {
+        mDistancePerTick = SystemProperties.getInt("persist.vr_dist_tick", 64);
+        mDistancePerTickSquared = mDistancePerTick * mDistancePerTick;
+        mMaxRepeatDelay = SystemProperties.getInt("persist.vr_repeat_delay", 300);
+        mMinFlickDistanceSquared = SystemProperties.getInt("persist.vr_min_flick", 20);
+        mMinFlickDistanceSquared *= mMinFlickDistanceSquared;
+        mFlickDecay = Float.parseFloat(SystemProperties.get(
+                "persist.sys.vr_flick_decay", "1.3"));
+        mTouchSlop = TOUCH_SLOP;
+        mTouchSlopSquared = mTouchSlop * mTouchSlop;
+
+        mContext = context;
+    }
+
+    private final Handler mHandler = new Handler(true /*async*/) {
+            @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_FLICK: {
+                    final long time = SystemClock.uptimeMillis();
+                    ViewRootImpl viewroot = (ViewRootImpl) msg.obj;
+                    // Send the key
+                    viewroot.enqueueInputEvent(new KeyEvent(time, time,
+                            KeyEvent.ACTION_DOWN, msg.arg2, 0, mLastMetaState,
+                            mLastDeviceId, 0, KeyEvent.FLAG_FALLBACK, mLastSource));
+                    viewroot.enqueueInputEvent(new KeyEvent(time, time,
+                            KeyEvent.ACTION_UP, msg.arg2, 0, mLastMetaState,
+                            mLastDeviceId, 0, KeyEvent.FLAG_FALLBACK, mLastSource));
+
+                    // Increase the delay by the decay factor and resend
+                    final int delay = (int) Math.ceil(mFlickDecay * msg.arg1);
+                    if (delay <= mMaxRepeatDelay) {
+                        Message msgCopy = Message.obtain(msg);
+                        msgCopy.arg1 = delay;
+                        msgCopy.setAsynchronous(true);
+                        mHandler.sendMessageDelayed(msgCopy, delay);
+                    }
+                    break;
+                }
+            }
+        }
+    };
+
+    public void updateTouchPad(ViewRootImpl viewroot, MotionEvent event,
+            boolean synthesizeNewKeys) {
+        if (!synthesizeNewKeys) {
+            mHandler.removeMessages(MSG_FLICK);
+        }
+        // Store what time the touchpad event occurred
+        final long time = SystemClock.uptimeMillis();
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mLastTouchPadStartTimeMs = time;
+                mAlwaysInTapRegion = true;
+                mTouchpadEnterXPosition = event.getX();
+                mTouchpadEnterYPosition = event.getY();
+                mAccumulatedX = 0;
+                mAccumulatedY = 0;
+                mLastMoveX = 0;
+                mLastMoveY = 0;
+                if (event.getDevice().getMotionRange(MotionEvent.AXIS_Y).getMax()
+                        * EDGE_SWIPE_THRESHOLD < event.getY()) {
+                    // Did the swipe begin in a valid region
+                    mEdgeSwipePossible = true;
+                }
+                // Clear any flings
+                if (synthesizeNewKeys) {
+                    mHandler.removeMessages(MSG_FLICK);
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // Determine whether the move is slop or an intentional move
+                float deltaX = event.getX() - mTouchpadEnterXPosition;
+                float deltaY = event.getY() - mTouchpadEnterYPosition;
+                if (mTouchSlopSquared < deltaX * deltaX + deltaY * deltaY) {
+                    mAlwaysInTapRegion = false;
+                }
+                // Checks if the swipe has crossed the midpoint
+                // and if our swipe gesture is complete
+                if (event.getY() < (event.getDevice().getMotionRange(MotionEvent.AXIS_Y).getMax()
+                        * .5) && mEdgeSwipePossible) {
+                    mEdgeSwipePossible = false;
+
+                    Intent intent =
+                            ((SearchManager)mContext.getSystemService(Context.SEARCH_SERVICE))
+                            .getAssistIntent(mContext, UserHandle.USER_CURRENT_OR_SELF);
+                    if (intent != null) {
+                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                        try {
+                            mContext.startActivity(intent);
+                        } catch (ActivityNotFoundException e){
+                            Log.e(TAG, "Could not start search activity");
+                        }
+                    } else {
+                        Log.e(TAG, "Could not find a search activity");
+                    }
+                }
+                // Find the difference in position between the two most recent
+                // touchpad events
+                mLastMoveX = event.getX() - mLastTouchpadXPosition;
+                mLastMoveY = event.getY() - mLastTouchpadYPosition;
+                mAccumulatedX += mLastMoveX;
+                mAccumulatedY += mLastMoveY;
+                float mAccumulatedXSquared = mAccumulatedX * mAccumulatedX;
+                float mAccumulatedYSquared = mAccumulatedY * mAccumulatedY;
+                // Determine if we've moved far enough to send a key press
+                if (mAccumulatedXSquared > mDistancePerTickSquared ||
+                        mAccumulatedYSquared > mDistancePerTickSquared) {
+                    float dominantAxis;
+                    float sign;
+                    boolean isXAxis;
+                    int key;
+                    int repeatCount = 0;
+                    // Determine dominant axis
+                    if (mAccumulatedXSquared > mAccumulatedYSquared) {
+                        dominantAxis = mAccumulatedX;
+                        isXAxis = true;
+                    } else {
+                        dominantAxis = mAccumulatedY;
+                        isXAxis = false;
+                    }
+                    // Determine sign of axis
+                    sign = (dominantAxis > 0) ? 1 : -1;
+                    // Determine key to send
+                    if (isXAxis) {
+                        key = (sign == 1) ? KeyEvent.KEYCODE_DPAD_RIGHT :
+                                KeyEvent.KEYCODE_DPAD_LEFT;
+                    } else {
+                        key = (sign == 1) ? KeyEvent.KEYCODE_DPAD_DOWN : KeyEvent.KEYCODE_DPAD_UP;
+                    }
+                    // Send key until maximum distance constraint is satisfied
+                    while (dominantAxis * dominantAxis > mDistancePerTickSquared) {
+                        repeatCount++;
+                        dominantAxis -= sign * mDistancePerTick;
+                        if (synthesizeNewKeys) {
+                            viewroot.enqueueInputEvent(new KeyEvent(time, time,
+                                    KeyEvent.ACTION_DOWN, key, 0, event.getMetaState(),
+                                    event.getDeviceId(), 0, KeyEvent.FLAG_FALLBACK,
+                                    event.getSource()));
+                            viewroot.enqueueInputEvent(new KeyEvent(time, time,
+                                    KeyEvent.ACTION_UP, key, 0, event.getMetaState(),
+                                    event.getDeviceId(), 0, KeyEvent.FLAG_FALLBACK,
+                                    event.getSource()));
+                        }
+                    }
+                    // Save new axis values
+                    mAccumulatedX = isXAxis ? dominantAxis : 0;
+                    mAccumulatedY = isXAxis ? 0 : dominantAxis;
+
+                    mLastKeySent = key;
+                    mKeySendRateMs = (int) ((time - mLastTouchPadKeySendTimeMs) / repeatCount);
+                    mLastTouchPadKeySendTimeMs = time;
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (time - mLastTouchPadStartTimeMs < MAX_TAP_TIME && mAlwaysInTapRegion) {
+                    if (synthesizeNewKeys) {
+                        viewroot.enqueueInputEvent(new KeyEvent(mLastTouchPadStartTimeMs, time,
+                                KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_CENTER, 0,
+                                event.getMetaState(), event.getDeviceId(), 0,
+                                KeyEvent.FLAG_FALLBACK, event.getSource()));
+                        viewroot.enqueueInputEvent(new KeyEvent(mLastTouchPadStartTimeMs, time,
+                                KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_CENTER, 0,
+                                event.getMetaState(), event.getDeviceId(), 0,
+                                KeyEvent.FLAG_FALLBACK, event.getSource()));
+                    }
+                } else {
+                    float xMoveSquared = mLastMoveX * mLastMoveX;
+                    float yMoveSquared = mLastMoveY * mLastMoveY;
+                    // Determine whether the last gesture was a fling.
+                    if (mMinFlickDistanceSquared <= xMoveSquared + yMoveSquared &&
+                            time - mLastTouchPadEventTimeMs <= MAX_TAP_TIME &&
+                            mKeySendRateMs <= mMaxRepeatDelay && mKeySendRateMs > 0) {
+                        mLastDeviceId = event.getDeviceId();
+                        mLastSource = event.getSource();
+                        mLastMetaState = event.getMetaState();
+
+                        if (synthesizeNewKeys) {
+                            Message message = Message.obtain(mHandler, MSG_FLICK,
+                                    mKeySendRateMs, mLastKeySent, viewroot);
+                            message.setAsynchronous(true);
+                            mHandler.sendMessageDelayed(message, mKeySendRateMs);
+                        }
+                    }
+                }
+                mEdgeSwipePossible = false;
+                break;
+        }
+
+        // Store touch event position and time
+        mLastTouchPadEventTimeMs = time;
+        mLastTouchpadXPosition = event.getX();
+        mLastTouchpadYPosition = event.getY();
+    }
+}