Debounce touch navigation taps and button presses

Bug: 8990644
Change-Id: Ib4ef2e2ab699a109c12614c1d64e4b7e63b514b0
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java
index 73fad08..5a5fc10 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -1837,6 +1837,19 @@
         }
     }
 
+    /** Whether key will, by default, trigger a click on the focused view.
+     * @hide
+     */
+    public static final boolean isConfirmKey(int keyCode) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+            case KeyEvent.KEYCODE_ENTER:
+                return true;
+            default:
+                return false;
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public final int getDeviceId() {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 95a2469..188ddf2 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -7945,21 +7945,17 @@
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         boolean result = false;
 
-        switch (keyCode) {
-            case KeyEvent.KEYCODE_DPAD_CENTER:
-            case KeyEvent.KEYCODE_ENTER: {
-                if ((mViewFlags & ENABLED_MASK) == DISABLED) {
-                    return true;
-                }
-                // Long clickable items don't necessarily have to be clickable
-                if (((mViewFlags & CLICKABLE) == CLICKABLE ||
-                        (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) &&
-                        (event.getRepeatCount() == 0)) {
-                    setPressed(true);
-                    checkForLongClick(0);
-                    return true;
-                }
-                break;
+        if (KeyEvent.isConfirmKey(event.getKeyCode())) {
+            if ((mViewFlags & ENABLED_MASK) == DISABLED) {
+                return true;
+            }
+            // Long clickable items don't necessarily have to be clickable
+            if (((mViewFlags & CLICKABLE) == CLICKABLE ||
+                    (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) &&
+                    (event.getRepeatCount() == 0)) {
+                setPressed(true);
+                checkForLongClick(0);
+                return true;
             }
         }
         return result;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index b0f67ac..075719c 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -228,6 +228,7 @@
 
     InputStage mFirstInputStage;
     InputStage mFirstPostImeInputStage;
+    SyntheticInputStage mSyntheticInputStage;
 
     boolean mWindowAttributesChanged = false;
     int mWindowAttributesChangesFlag = 0;
@@ -589,8 +590,8 @@
 
                 // Set up the input pipeline.
                 CharSequence counterSuffix = attrs.getTitle();
-                InputStage syntheticStage = new SyntheticInputStage();
-                InputStage viewPostImeStage = new ViewPostImeInputStage(syntheticStage);
+                mSyntheticInputStage = new SyntheticInputStage();
+                InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
                 InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
                         "aq:native-post-ime:" + counterSuffix);
                 InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
@@ -3773,6 +3774,9 @@
         private int processKeyEvent(QueuedInputEvent q) {
             final KeyEvent event = (KeyEvent)q.mEvent;
 
+            // The synthetic stage occasionally needs to know about keys in order to debounce taps
+            mSyntheticInputStage.notifyKeyEvent(event);
+
             // Deliver the key to the view hierarchy.
             if (mView.dispatchKeyEvent(event)) {
                 return FINISH_HANDLED;
@@ -3945,6 +3949,10 @@
             }
             super.onDeliverToNext(q);
         }
+
+        public void notifyKeyEvent(KeyEvent e) {
+            mTouchNavigation.notifyKeyEvent(e);
+        }
     }
 
     /**
@@ -4375,6 +4383,9 @@
         // Tap timeout in milliseconds.
         private static final int TAP_TIMEOUT = 250;
 
+        // Debounce timeout for touch nav devices with a button under their pad, in milliseconds
+        private static final int DEBOUNCE_TIME = 250;
+
         // The maximum distance traveled for a gesture to be considered a tap in millimeters.
         private static final int TAP_SLOP_MILLIMETERS = 5;
 
@@ -4409,6 +4420,9 @@
         private int mConfigTapTimeout;
         private float mConfigTapSlop;
 
+        // Amount of time to wait between button presses and tap generation for debouncing
+        private int mConfigDebounceTime;
+
         // The scaled tick distance.  A movement of this amount should generally translate
         // into a single dpad event in a given direction.
         private float mConfigTickDistance;
@@ -4454,6 +4468,11 @@
         private boolean mFlinging;
         private float mFlingVelocity;
 
+        // The last time a confirm key was pressed on the touch nav device
+        private long mLastConfirmKeyTime = Long.MAX_VALUE;
+
+        private boolean mHasButtonUnderPad;
+
         public SyntheticTouchNavigationHandler() {
             super(true);
         }
@@ -4497,6 +4516,8 @@
                                 MIN_FLING_VELOCITY_TICKS_PER_SECOND * mConfigTickDistance;
                         mConfigMaxFlingVelocity =
                                 MAX_FLING_VELOCITY_TICKS_PER_SECOND * mConfigTickDistance;
+                        mConfigDebounceTime = DEBOUNCE_TIME;
+                        mHasButtonUnderPad = device.hasButtonUnderPad();
 
                         if (LOCAL_DEBUG) {
                             Log.d(LOCAL_TAG, "Configured device " + mCurrentDeviceId
@@ -4567,10 +4588,13 @@
                         if (!mConsumedMovement
                                 && Math.hypot(mLastX - mStartX, mLastY - mStartY) < mConfigTapSlop
                                 && time <= mStartTime + mConfigTapTimeout) {
-                            // It's a tap!
-                            finishKeys(time);
-                            sendKeyDownOrRepeat(time, KeyEvent.KEYCODE_DPAD_CENTER, metaState);
-                            sendKeyUp(time);
+                            if (!mHasButtonUnderPad ||
+                                        time >= mLastConfirmKeyTime + mConfigDebounceTime) {
+                                // It's a tap!
+                                finishKeys(time);
+                                sendKeyDownOrRepeat(time, KeyEvent.KEYCODE_DPAD_CENTER, metaState);
+                                sendKeyUp(time);
+                            }
                         } else if (mConsumedMovement
                                 && mPendingKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
                             // It might be a fling.
@@ -4603,6 +4627,13 @@
             }
         }
 
+        public void notifyKeyEvent(KeyEvent e) {
+            final int keyCode = e.getKeyCode();
+            if (KeyEvent.isConfirmKey(e.getKeyCode())) {
+                mLastConfirmKeyTime  = e.getDownTime();
+            }
+        }
+
         private void finishKeys(long time) {
             cancelFling();
             sendKeyUp(time);