[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()