[Media ML] Support customization for single/double/triple clicks

Track clicks to determine whether they are single/double/triple
clicks. Run the customized implementations if they exist.
Added test cases in "Testing scenarios" section of
go/apex-media-server-testing

Bug: 149260441
Test: manually
Change-Id: Ifaf759a33443a742995df390eb81e332f9879932
diff --git a/services/core/java/com/android/server/media/MediaKeyDispatcher.java b/services/core/java/com/android/server/media/MediaKeyDispatcher.java
index 0b96978..3a29622 100644
--- a/services/core/java/com/android/server/media/MediaKeyDispatcher.java
+++ b/services/core/java/com/android/server/media/MediaKeyDispatcher.java
@@ -33,29 +33,28 @@
 /**
  * Provides a way to customize behavior for media key events.
  * <p>
- * In order to override the implementation of the single/double/triple click or long press,
+ * In order to override the implementation of the single/double/triple tap or long press,
  * {@link #setOverriddenKeyEvents(int, int)} should be called for each key code with the
  * overridden {@link KeyEventType} bit value set, and the corresponding method,
- * {@link #onSingleClick(KeyEvent)}, {@link #onDoubleClick(KeyEvent)},
- * {@link #onTripleClick(KeyEvent)}, {@link #onLongPress(KeyEvent)} should be implemented.
+ * {@link #onSingleTap(KeyEvent)}, {@link #onDoubleTap(KeyEvent)},
+ * {@link #onTripleTap(KeyEvent)}, {@link #onLongPress(KeyEvent)} should be implemented.
  * <p>
  * Note: When instantiating this class, {@link MediaSessionService} will only use the constructor
  * without any parameters.
  */
-// TODO: Change API names from using "click" to "tap"
 // TODO: Move this class to apex/media/
 public abstract class MediaKeyDispatcher {
     @IntDef(flag = true, value = {
-            KEY_EVENT_SINGLE_CLICK,
-            KEY_EVENT_DOUBLE_CLICK,
-            KEY_EVENT_TRIPLE_CLICK,
+            KEY_EVENT_SINGLE_TAP,
+            KEY_EVENT_DOUBLE_TAP,
+            KEY_EVENT_TRIPLE_TAP,
             KEY_EVENT_LONG_PRESS
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface KeyEventType {}
-    static final int KEY_EVENT_SINGLE_CLICK = 1 << 0;
-    static final int KEY_EVENT_DOUBLE_CLICK = 1 << 1;
-    static final int KEY_EVENT_TRIPLE_CLICK = 1 << 2;
+    static final int KEY_EVENT_SINGLE_TAP = 1 << 0;
+    static final int KEY_EVENT_DOUBLE_TAP = 1 << 1;
+    static final int KEY_EVENT_TRIPLE_TAP = 1 << 2;
     static final int KEY_EVENT_LONG_PRESS = 1 << 3;
 
     private Map<Integer, Integer> mOverriddenKeyEvents;
@@ -110,16 +109,16 @@
         return mOverriddenKeyEvents;
     }
 
-    static boolean isSingleClickOverridden(@KeyEventType int overriddenKeyEvents) {
-        return (overriddenKeyEvents & MediaKeyDispatcher.KEY_EVENT_SINGLE_CLICK) != 0;
+    static boolean isSingleTapOverridden(@KeyEventType int overriddenKeyEvents) {
+        return (overriddenKeyEvents & MediaKeyDispatcher.KEY_EVENT_SINGLE_TAP) != 0;
     }
 
-    static boolean isDoubleClickOverridden(@KeyEventType int overriddenKeyEvents) {
-        return (overriddenKeyEvents & MediaKeyDispatcher.KEY_EVENT_DOUBLE_CLICK) != 0;
+    static boolean isDoubleTapOverridden(@KeyEventType int overriddenKeyEvents) {
+        return (overriddenKeyEvents & MediaKeyDispatcher.KEY_EVENT_DOUBLE_TAP) != 0;
     }
 
-    static boolean isTripleClickOverridden(@KeyEventType int overriddenKeyEvents) {
-        return (overriddenKeyEvents & MediaKeyDispatcher.KEY_EVENT_TRIPLE_CLICK) != 0;
+    static boolean isTripleTapOverridden(@KeyEventType int overriddenKeyEvents) {
+        return (overriddenKeyEvents & MediaKeyDispatcher.KEY_EVENT_TRIPLE_TAP) != 0;
     }
 
     static boolean isLongPressOverridden(@KeyEventType int overriddenKeyEvents) {
@@ -150,11 +149,11 @@
     }
 
     /**
-     * Customized implementation for single click event. Will be run if
-     * {@link #KEY_EVENT_SINGLE_CLICK} flag is on for the corresponding key code from
+     * Customized implementation for single tap event. Will be run if
+     * {@link #KEY_EVENT_SINGLE_TAP} flag is on for the corresponding key code from
      * {@link #getOverriddenKeyEvents()}.
      *
-     * It is considered a single click if only one {@link KeyEvent} with the same
+     * It is considered a single tap if only one {@link KeyEvent} with the same
      * {@link KeyEvent#getKeyCode()} is dispatched within
      * {@link ViewConfiguration#getMultiPressTimeout()} milliseconds. Change the
      * {@link android.provider.Settings.Secure#MULTI_PRESS_TIMEOUT} value to adjust the interval.
@@ -163,15 +162,15 @@
      *
      * @param keyEvent
      */
-    void onSingleClick(KeyEvent keyEvent) {
+    void onSingleTap(KeyEvent keyEvent) {
     }
 
     /**
-     * Customized implementation for double click event. Will be run if
-     * {@link #KEY_EVENT_DOUBLE_CLICK} flag is on for the corresponding key code from
+     * Customized implementation for double tap event. Will be run if
+     * {@link #KEY_EVENT_DOUBLE_TAP} flag is on for the corresponding key code from
      * {@link #getOverriddenKeyEvents()}.
      *
-     * It is considered a double click if two {@link KeyEvent}s with the same
+     * It is considered a double tap if two {@link KeyEvent}s with the same
      * {@link KeyEvent#getKeyCode()} are dispatched within
      * {@link ViewConfiguration#getMultiPressTimeout()} milliseconds of each other. Change the
      * {@link android.provider.Settings.Secure#MULTI_PRESS_TIMEOUT} value to adjust the interval.
@@ -180,15 +179,15 @@
      *
      * @param keyEvent
      */
-    void onDoubleClick(KeyEvent keyEvent) {
+    void onDoubleTap(KeyEvent keyEvent) {
     }
 
     /**
-     * Customized implementation for triple click event. Will be run if
-     * {@link #KEY_EVENT_TRIPLE_CLICK} flag is on for the corresponding key code from
+     * Customized implementation for triple tap event. Will be run if
+     * {@link #KEY_EVENT_TRIPLE_TAP} flag is on for the corresponding key code from
      * {@link #getOverriddenKeyEvents()}.
      *
-     * It is considered a triple click if three {@link KeyEvent}s with the same
+     * It is considered a triple tap if three {@link KeyEvent}s with the same
      * {@link KeyEvent#getKeyCode()} are dispatched within
      * {@link ViewConfiguration#getMultiPressTimeout()} milliseconds of each other. Change the
      * {@link android.provider.Settings.Secure#MULTI_PRESS_TIMEOUT} value to adjust the interval.
@@ -197,7 +196,7 @@
      *
      * @param keyEvent
      */
-    void onTripleClick(KeyEvent keyEvent) {
+    void onTripleTap(KeyEvent keyEvent) {
     }
 
     /**
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index e5867e7..09b3ea8 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -18,6 +18,12 @@
 
 import static android.os.UserHandle.USER_ALL;
 
+import static com.android.server.media.MediaKeyDispatcher.KEY_EVENT_LONG_PRESS;
+import static com.android.server.media.MediaKeyDispatcher.isDoubleTapOverridden;
+import static com.android.server.media.MediaKeyDispatcher.isLongPressOverridden;
+import static com.android.server.media.MediaKeyDispatcher.isSingleTapOverridden;
+import static com.android.server.media.MediaKeyDispatcher.isTripleTapOverridden;
+
 import android.app.ActivityManager;
 import android.app.INotificationManager;
 import android.app.KeyguardManager;
@@ -105,7 +111,7 @@
     private static final int SESSION_CREATION_LIMIT_PER_UID = 100;
     private static final int LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout()
             + /* Buffer for delayed delivery of key event */ 50;
-    private static final int MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout();
+    private static final int MULTI_TAP_TIMEOUT = ViewConfiguration.getMultiPressTimeout();
 
     private final Context mContext;
     private final SessionManagerImpl mSessionManagerImpl;
@@ -1097,9 +1103,12 @@
                 "android.media.AudioService.WAKELOCK_ACQUIRED";
         private static final int WAKELOCK_RELEASE_ON_FINISHED = 1980; // magic number
 
-        private KeyEvent mPendingFirstDownKeyEvent = null;
+        private KeyEvent mTrackingFirstDownKeyEvent = null;
         private boolean mIsLongPressing = false;
         private Runnable mLongPressTimeoutRunnable = null;
+        private int mMultiTapCount = 0;
+        private int mMultiTapKeyCode = 0;
+        private Runnable mMultiTapTimeoutRunnable = null;
 
         @Override
         public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
@@ -2113,10 +2122,12 @@
         }
 
         // A long press is determined by:
-        // 1) A KeyEvent with KeyEvent.ACTION_DOWN and repeat count of 0, followed by
-        // 2) A KeyEvent with KeyEvent.ACTION_DOWN and repeat count of 1 and FLAG_LONG_PRESS within
-        //    ViewConfiguration.getLongPressTimeout().
-        // TODO: Add description about what a click is determined by.
+        // 1) A KeyEvent.ACTION_DOWN KeyEvent and repeat count of 0, followed by
+        // 2) A KeyEvent.ACTION_DOWN KeyEvent with the same key code, a repeat count of 1, and
+        //    FLAG_LONG_PRESS received within ViewConfiguration.getLongPressTimeout().
+        // A tap is determined by:
+        // 1) A KeyEvent.ACTION_DOWN KeyEvent followed by
+        // 2) A KeyEvent.ACTION_UP KeyEvent with the same key code.
         private void handleKeyEventLocked(String packageName, int pid, int uid,
                 boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
             if (keyEvent.isCanceled()) {
@@ -2125,61 +2136,121 @@
 
             int overriddenKeyEvents = (mCustomMediaKeyDispatcher == null) ? 0
                     : mCustomMediaKeyDispatcher.getOverriddenKeyEvents().get(keyEvent.getKeyCode());
-            cancelPendingIfNeeded(keyEvent);
-            if (!needPending(keyEvent, overriddenKeyEvents)) {
+            cancelTrackingIfNeeded(packageName, pid, uid, asSystemService, keyEvent, needWakeLock,
+                    overriddenKeyEvents);
+            if (!needTracking(keyEvent, overriddenKeyEvents)) {
                 dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService, keyEvent,
                         needWakeLock);
                 return;
             }
 
             if (isFirstDownKeyEvent(keyEvent)) {
-                mPendingFirstDownKeyEvent = keyEvent;
+                mTrackingFirstDownKeyEvent = keyEvent;
                 mIsLongPressing = false;
                 return;
             }
 
+            // Long press is always overridden here, otherwise the key event would have been already
+            // handled
             if (isFirstLongPressKeyEvent(keyEvent)) {
                 mIsLongPressing = true;
             }
             if (mIsLongPressing) {
                 handleLongPressLocked(keyEvent, needWakeLock, overriddenKeyEvents);
-            } else if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
-                mPendingFirstDownKeyEvent = null;
-                // TODO: Replace this with code to determine whether
-                // single/double/triple click and run custom implementations,
-                // if they exist.
-                dispatchDownAndUpKeyEventsLocked(packageName, pid, uid, asSystemService,
-                        keyEvent, needWakeLock);
+                return;
+            }
+
+            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+                mTrackingFirstDownKeyEvent = null;
+                if (shouldTrackForMultipleTapsLocked(overriddenKeyEvents)) {
+                    if (mMultiTapCount == 0) {
+                        mMultiTapTimeoutRunnable = createSingleTapRunnable(packageName, pid, uid,
+                                asSystemService, keyEvent, needWakeLock,
+                                isSingleTapOverridden(overriddenKeyEvents));
+                        if (isSingleTapOverridden(overriddenKeyEvents)
+                                && !isDoubleTapOverridden(overriddenKeyEvents)
+                                && !isTripleTapOverridden(overriddenKeyEvents)) {
+                            mMultiTapTimeoutRunnable.run();
+                        } else {
+                            mHandler.postDelayed(mMultiTapTimeoutRunnable,
+                                    MULTI_TAP_TIMEOUT);
+                            mMultiTapCount = 1;
+                            mMultiTapKeyCode = keyEvent.getKeyCode();
+                        }
+                    } else if (mMultiTapCount == 1) {
+                        mHandler.removeCallbacks(mMultiTapTimeoutRunnable);
+                        mMultiTapTimeoutRunnable = createDoubleTapRunnable(packageName, pid, uid,
+                                asSystemService, keyEvent, needWakeLock,
+                                isSingleTapOverridden(overriddenKeyEvents),
+                                isDoubleTapOverridden(overriddenKeyEvents));
+                        if (isTripleTapOverridden(overriddenKeyEvents)) {
+                            mHandler.postDelayed(mMultiTapTimeoutRunnable, MULTI_TAP_TIMEOUT);
+                            mMultiTapCount = 2;
+                        } else {
+                            mMultiTapTimeoutRunnable.run();
+                        }
+                    } else if (mMultiTapCount == 2) {
+                        mHandler.removeCallbacks(mMultiTapTimeoutRunnable);
+                        onTripleTap(keyEvent);
+                    }
+                } else {
+                    dispatchDownAndUpKeyEventsLocked(packageName, pid, uid, asSystemService,
+                            keyEvent, needWakeLock);
+                }
             }
         }
 
-        private void cancelPendingIfNeeded(KeyEvent keyEvent) {
-            if (mPendingFirstDownKeyEvent == null) {
+        private boolean shouldTrackForMultipleTapsLocked(int overriddenKeyEvents) {
+            return isSingleTapOverridden(overriddenKeyEvents)
+                    || isDoubleTapOverridden(overriddenKeyEvents)
+                    || isTripleTapOverridden(overriddenKeyEvents);
+        }
+
+        private void cancelTrackingIfNeeded(String packageName, int pid, int uid,
+                boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock,
+                int overriddenKeyEvents) {
+            if (mTrackingFirstDownKeyEvent == null && mMultiTapTimeoutRunnable == null) {
                 return;
             }
+
             if (isFirstDownKeyEvent(keyEvent)) {
                 if (mLongPressTimeoutRunnable != null) {
                     mHandler.removeCallbacks(mLongPressTimeoutRunnable);
                     mLongPressTimeoutRunnable.run();
-                } else {
-                    resetLongPressTracking();
                 }
+                if (mMultiTapTimeoutRunnable != null && keyEvent.getKeyCode() != mMultiTapKeyCode) {
+                    runExistingMultiTapRunnableLocked();
+                }
+                resetLongPressTracking();
                 return;
             }
-            if (mPendingFirstDownKeyEvent.getDownTime() == keyEvent.getDownTime()
-                    && mPendingFirstDownKeyEvent.getKeyCode() == keyEvent.getKeyCode()
-                    && keyEvent.getAction() == KeyEvent.ACTION_DOWN
-                    && keyEvent.getRepeatCount() > 1 && !mIsLongPressing) {
-                resetLongPressTracking();
+
+            if (mTrackingFirstDownKeyEvent != null
+                    && mTrackingFirstDownKeyEvent.getDownTime() == keyEvent.getDownTime()
+                    && mTrackingFirstDownKeyEvent.getKeyCode() == keyEvent.getKeyCode()
+                    && keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+                if (isFirstLongPressKeyEvent(keyEvent)) {
+                    if (mMultiTapTimeoutRunnable != null) {
+                        runExistingMultiTapRunnableLocked();
+                    }
+                    if ((overriddenKeyEvents & KEY_EVENT_LONG_PRESS) == 0
+                            && !isVoiceKey(keyEvent.getKeyCode())) {
+                        dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
+                                mTrackingFirstDownKeyEvent, needWakeLock);
+                        mTrackingFirstDownKeyEvent = null;
+                    }
+                } else if (keyEvent.getRepeatCount() > 1 && !mIsLongPressing) {
+                    resetLongPressTracking();
+                }
             }
         }
 
-        private boolean needPending(KeyEvent keyEvent, int overriddenKeyEvents) {
+        private boolean needTracking(KeyEvent keyEvent, int overriddenKeyEvents) {
             if (!isFirstDownKeyEvent(keyEvent)) {
-                if (mPendingFirstDownKeyEvent == null) {
+                if (mTrackingFirstDownKeyEvent == null) {
                     return false;
-                } else if (mPendingFirstDownKeyEvent.getDownTime() != keyEvent.getDownTime()
-                        || mPendingFirstDownKeyEvent.getKeyCode() != keyEvent.getKeyCode()) {
+                } else if (mTrackingFirstDownKeyEvent.getDownTime() != keyEvent.getDownTime()
+                        || mTrackingFirstDownKeyEvent.getKeyCode() != keyEvent.getKeyCode()) {
                     return false;
                 }
             }
@@ -2189,10 +2260,21 @@
             return true;
         }
 
+        private void runExistingMultiTapRunnableLocked() {
+            mHandler.removeCallbacks(mMultiTapTimeoutRunnable);
+            mMultiTapTimeoutRunnable.run();
+        }
+
+        private void resetMultiTapTrackingLocked() {
+            mMultiTapCount = 0;
+            mMultiTapTimeoutRunnable = null;
+            mMultiTapKeyCode = 0;
+        }
+
         private void handleLongPressLocked(KeyEvent keyEvent, boolean needWakeLock,
                 int overriddenKeyEvents) {
             if (mCustomMediaKeyDispatcher != null
-                    && mCustomMediaKeyDispatcher.isLongPressOverridden(overriddenKeyEvents)) {
+                    && isLongPressOverridden(overriddenKeyEvents)) {
                 mCustomMediaKeyDispatcher.onLongPress(keyEvent);
 
                 if (mLongPressTimeoutRunnable != null) {
@@ -2226,7 +2308,7 @@
         }
 
         private void resetLongPressTracking() {
-            mPendingFirstDownKeyEvent = null;
+            mTrackingFirstDownKeyEvent = null;
             mIsLongPressing = false;
             mLongPressTimeoutRunnable = null;
         }
@@ -2255,6 +2337,50 @@
                     keyEvent, needWakeLock);
         }
 
+        Runnable createSingleTapRunnable(String packageName, int pid, int uid,
+                boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock,
+                boolean overridden) {
+            return new Runnable() {
+                @Override
+                public void run() {
+                    resetMultiTapTrackingLocked();
+                    if (overridden) {
+                        mCustomMediaKeyDispatcher.onSingleTap(keyEvent);
+                    } else {
+                        dispatchDownAndUpKeyEventsLocked(packageName, pid, uid, asSystemService,
+                                keyEvent, needWakeLock);
+                    }
+                }
+            };
+        };
+
+        Runnable createDoubleTapRunnable(String packageName, int pid, int uid,
+                boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock,
+                boolean singleTapOverridden, boolean doubleTapOverridden) {
+            return new Runnable() {
+                @Override
+                public void run() {
+                    resetMultiTapTrackingLocked();
+                    if (doubleTapOverridden) {
+                        mCustomMediaKeyDispatcher.onDoubleTap(keyEvent);
+                    } else if (singleTapOverridden) {
+                        mCustomMediaKeyDispatcher.onSingleTap(keyEvent);
+                        mCustomMediaKeyDispatcher.onSingleTap(keyEvent);
+                    } else {
+                        dispatchDownAndUpKeyEventsLocked(packageName, pid, uid, asSystemService,
+                                keyEvent, needWakeLock);
+                        dispatchDownAndUpKeyEventsLocked(packageName, pid, uid, asSystemService,
+                                keyEvent, needWakeLock);
+                    }
+                }
+            };
+        };
+
+        private void onTripleTap(KeyEvent keyEvent) {
+            resetMultiTapTrackingLocked();
+            mCustomMediaKeyDispatcher.onTripleTap(keyEvent);
+        }
+
         private void dispatchMediaKeyEventLocked(String packageName, int pid, int uid,
                 boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
             if (mCurrentFullUserRecord.getMediaButtonSessionLocked()