diff --git a/services/core/java/com/android/server/media/MediaKeyDispatcher.java b/services/core/java/com/android/server/media/MediaKeyDispatcher.java index e0efd8a759722..0b9697840a1fb 100644 --- a/services/core/java/com/android/server/media/MediaKeyDispatcher.java +++ b/services/core/java/com/android/server/media/MediaKeyDispatcher.java @@ -16,24 +16,64 @@ package com.android.server.media; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.media.session.ISessionManager; import android.media.session.MediaSession; import android.os.Binder; import android.view.KeyEvent; +import android.view.ViewConfiguration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.Map; /** * Provides a way to customize behavior for media key events. - * + *
+ * In order to override the implementation of the single/double/triple click 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. + *
* 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_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_LONG_PRESS = 1 << 3;
+
+ private Map
+ * The list of valid key codes are the following:
+ *
+ * The list of valid key codes are the following:
+ *
* Handles the dispatching of the volume button events to one of the
@@ -1662,7 +1662,7 @@ public class MediaSessionService extends SystemService implements Monitor {
* {@link KeyEvent#KEYCODE_VOLUME_DOWN},
* or {@link KeyEvent#KEYCODE_VOLUME_MUTE}.
* @param stream stream type to adjust volume.
- * @param musicOnly true if both UI nor haptic feedback aren't needed when adjust volume.
+ * @param musicOnly true if both UI and haptic feedback aren't needed when adjusting volume.
* @see #dispatchVolumeKeyEventToSessionAsSystemService
*/
@Override
@@ -1707,7 +1707,7 @@ public class MediaSessionService extends SystemService implements Monitor {
mHandler.obtainMessage(
MessageHandler.MSG_VOLUME_INITIAL_DOWN,
mCurrentFullUserRecord.mFullUserId, 0),
- mLongPressTimeout);
+ LONG_PRESS_TIMEOUT);
}
if (keyEvent.getRepeatCount() > 0 || keyEvent.isLongPress()) {
mHandler.removeMessages(MessageHandler.MSG_VOLUME_INITIAL_DOWN);
@@ -2112,31 +2112,147 @@ public class MediaSessionService extends SystemService implements Monitor {
}
}
- private void handleVoiceKeyEventLocked(String packageName, int pid, int uid,
+ // 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.
+ private void handleKeyEventLocked(String packageName, int pid, int uid,
boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
- int action = keyEvent.getAction();
- boolean isLongPress = (keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0;
- if (action == KeyEvent.ACTION_DOWN) {
- if (keyEvent.getRepeatCount() == 0) {
- mVoiceButtonDown = true;
- mVoiceButtonHandled = false;
- } else if (mVoiceButtonDown && !mVoiceButtonHandled && isLongPress) {
- mVoiceButtonHandled = true;
- startVoiceInput(needWakeLock);
+ if (keyEvent.isCanceled()) {
+ return;
+ }
+
+ int overriddenKeyEvents = (mCustomMediaKeyDispatcher == null) ? 0
+ : mCustomMediaKeyDispatcher.getOverriddenKeyEvents().get(keyEvent.getKeyCode());
+ cancelPendingIfNeeded(keyEvent);
+ if (!needPending(keyEvent, overriddenKeyEvents)) {
+ dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService, keyEvent,
+ needWakeLock);
+ return;
+ }
+
+ if (isFirstDownKeyEvent(keyEvent)) {
+ mPendingFirstDownKeyEvent = keyEvent;
+ mIsLongPressing = false;
+ return;
+ }
+
+ 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);
+ }
+ }
+
+ private void cancelPendingIfNeeded(KeyEvent keyEvent) {
+ if (mPendingFirstDownKeyEvent == null) {
+ return;
+ }
+ if (isFirstDownKeyEvent(keyEvent)) {
+ if (mLongPressTimeoutRunnable != null) {
+ mHandler.removeCallbacks(mLongPressTimeoutRunnable);
+ mLongPressTimeoutRunnable.run();
+ } else {
+ resetLongPressTracking();
}
- } else if (action == KeyEvent.ACTION_UP) {
- if (mVoiceButtonDown) {
- mVoiceButtonDown = false;
- if (!mVoiceButtonHandled && !keyEvent.isCanceled()) {
- // Resend the down then send this event through
- KeyEvent downEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_DOWN);
- dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
- downEvent, needWakeLock);
- dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
- keyEvent, needWakeLock);
- }
+ return;
+ }
+ if (mPendingFirstDownKeyEvent.getDownTime() == keyEvent.getDownTime()
+ && mPendingFirstDownKeyEvent.getKeyCode() == keyEvent.getKeyCode()
+ && keyEvent.getAction() == KeyEvent.ACTION_DOWN
+ && keyEvent.getRepeatCount() > 1 && !mIsLongPressing) {
+ resetLongPressTracking();
+ }
+ }
+
+ private boolean needPending(KeyEvent keyEvent, int overriddenKeyEvents) {
+ if (!isFirstDownKeyEvent(keyEvent)) {
+ if (mPendingFirstDownKeyEvent == null) {
+ return false;
+ } else if (mPendingFirstDownKeyEvent.getDownTime() != keyEvent.getDownTime()
+ || mPendingFirstDownKeyEvent.getKeyCode() != keyEvent.getKeyCode()) {
+ return false;
}
}
+ if (overriddenKeyEvents == 0 && !isVoiceKey(keyEvent.getKeyCode())) {
+ return false;
+ }
+ return true;
+ }
+
+ private void handleLongPressLocked(KeyEvent keyEvent, boolean needWakeLock,
+ int overriddenKeyEvents) {
+ if (mCustomMediaKeyDispatcher != null
+ && mCustomMediaKeyDispatcher.isLongPressOverridden(overriddenKeyEvents)) {
+ mCustomMediaKeyDispatcher.onLongPress(keyEvent);
+
+ if (mLongPressTimeoutRunnable != null) {
+ mHandler.removeCallbacks(mLongPressTimeoutRunnable);
+ }
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+ if (mLongPressTimeoutRunnable == null) {
+ mLongPressTimeoutRunnable = createLongPressTimeoutRunnable(keyEvent);
+ }
+ mHandler.postDelayed(mLongPressTimeoutRunnable, LONG_PRESS_TIMEOUT);
+ } else {
+ resetLongPressTracking();
+ }
+ } else if (isFirstLongPressKeyEvent(keyEvent) && isVoiceKey(keyEvent.getKeyCode())) {
+ // Default implementation
+ startVoiceInput(needWakeLock);
+ resetLongPressTracking();
+ }
+ }
+
+ private Runnable createLongPressTimeoutRunnable(KeyEvent keyEvent) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ if (mCustomMediaKeyDispatcher != null) {
+ mCustomMediaKeyDispatcher.onLongPress(createCanceledKeyEvent(keyEvent));
+ }
+ resetLongPressTracking();
+ }
+ };
+ }
+
+ private void resetLongPressTracking() {
+ mPendingFirstDownKeyEvent = null;
+ mIsLongPressing = false;
+ mLongPressTimeoutRunnable = null;
+ }
+
+ private KeyEvent createCanceledKeyEvent(KeyEvent keyEvent) {
+ KeyEvent upEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_UP);
+ return KeyEvent.changeTimeRepeat(upEvent, System.currentTimeMillis(), 0,
+ KeyEvent.FLAG_CANCELED);
+ }
+
+ private boolean isFirstLongPressKeyEvent(KeyEvent keyEvent) {
+ return ((keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0)
+ && keyEvent.getRepeatCount() == 1;
+ }
+
+ private boolean isFirstDownKeyEvent(KeyEvent keyEvent) {
+ return keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.getRepeatCount() == 0;
+ }
+
+ private void dispatchDownAndUpKeyEventsLocked(String packageName, int pid, int uid,
+ boolean asSystemService, KeyEvent keyEvent, boolean needWakeLock) {
+ KeyEvent downEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_DOWN);
+ dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
+ downEvent, needWakeLock);
+ dispatchMediaKeyEventLocked(packageName, pid, uid, asSystemService,
+ keyEvent, needWakeLock);
}
private void dispatchMediaKeyEventLocked(String packageName, int pid, int uid,
@@ -2149,15 +2265,11 @@ public class MediaSessionService extends SystemService implements Monitor {
MediaSessionRecord session = null;
// Retrieve custom session for key event if it exists.
- if (mCustomMediaKeyDispatcher != null && mGetSessionForKeyEventMethod != null) {
- try {
- Object tokenObject = mGetSessionForKeyEventMethod.invoke(
- mCustomMediaKeyDispatcher, keyEvent, uid, asSystemService);
- if (tokenObject != null) {
- session = getMediaSessionRecordLocked((MediaSession.Token) tokenObject);
- }
- } catch (InvocationTargetException | IllegalAccessException e) {
- Log.w(TAG, "Encountered problem while using reflection", e);
+ if (mCustomMediaKeyDispatcher != null) {
+ MediaSession.Token token = mCustomMediaKeyDispatcher.getSessionForKeyEvent(
+ keyEvent, uid, asSystemService);
+ if (token != null) {
+ session = getMediaSessionRecordLocked(token);
}
}
@@ -2312,12 +2424,11 @@ public class MediaSessionService extends SystemService implements Monitor {
mHandled = true;
mHandler.removeCallbacks(this);
synchronized (mLock) {
- if (!isGlobalPriorityActiveLocked()
- && isVoiceKey(mKeyEvent.getKeyCode())) {
- handleVoiceKeyEventLocked(mPackageName, mPid, mUid, mAsSystemService,
+ if (isGlobalPriorityActiveLocked()) {
+ dispatchMediaKeyEventLocked(mPackageName, mPid, mUid, mAsSystemService,
mKeyEvent, mNeedWakeLock);
} else {
- dispatchMediaKeyEventLocked(mPackageName, mPid, mUid, mAsSystemService,
+ handleKeyEventLocked(mPackageName, mPid, mUid, mAsSystemService,
mKeyEvent, mNeedWakeLock);
}
}
diff --git a/services/core/java/com/android/server/media/SessionPolicyProvider.java b/services/core/java/com/android/server/media/SessionPolicyProvider.java
index 40a3d2d66b1bf..5f02a075344e9 100644
--- a/services/core/java/com/android/server/media/SessionPolicyProvider.java
+++ b/services/core/java/com/android/server/media/SessionPolicyProvider.java
@@ -29,6 +29,7 @@ import java.lang.annotation.RetentionPolicy;
* Note: When instantiating this class, {@link MediaSessionService} will only use the constructor
* without any parameters.
*/
+// TODO: Move this class to apex/media/
public abstract class SessionPolicyProvider {
@IntDef(value = {
SESSION_POLICY_IGNORE_BUTTON_RECEIVER,
+ *
+ * @see {@link KeyEvent#isMediaSessionKey(int)}
+ */
+ @KeyEventType Map
+ *
+ * @see {@link KeyEvent#isMediaSessionKey(int)}
+ * @param keyCode
+ */
+ void setOverriddenKeyEvents(int keyCode, @KeyEventType int keyEventType) {
+ mOverriddenKeyEvents.put(keyCode, keyEventType);
+ }
+
+ /**
+ * Customized implementation for single click event. Will be run if
+ * {@link #KEY_EVENT_SINGLE_CLICK} 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
+ * {@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.
+ *
+ * Note: This will only be called once with the {@link KeyEvent#ACTION_UP} KeyEvent.
+ *
+ * @param keyEvent
+ */
+ void onSingleClick(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
+ * {@link #getOverriddenKeyEvents()}.
+ *
+ * It is considered a double click 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.
+ *
+ * Note: This will only be called once with the {@link KeyEvent#ACTION_UP} KeyEvent.
+ *
+ * @param keyEvent
+ */
+ void onDoubleClick(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
+ * {@link #getOverriddenKeyEvents()}.
+ *
+ * It is considered a triple click 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.
+ *
+ * Note: This will only be called once with the {@link KeyEvent#ACTION_UP} KeyEvent.
+ *
+ * @param keyEvent
+ */
+ void onTripleClick(KeyEvent keyEvent) {
+ }
+
+ /**
+ * Customized implementation for long press event. Will be run if
+ * {@link #KEY_EVENT_LONG_PRESS} flag is on for the corresponding key code from
+ * {@link #getOverriddenKeyEvents()}.
+ *
+ * It is considered a long press if an {@link KeyEvent#ACTION_DOWN} key event is followed by
+ * another {@link KeyEvent#ACTION_DOWN} key event with {@link KeyEvent#FLAG_LONG_PRESS}
+ * enabled, and an {@link KeyEvent#getRepeatCount()} that is equal to 1.
+ *
+ * Note: This will be called for the following key events:
+ *
+ *
+ *
+ * @param keyEvent
+ */
+ void onLongPress(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 5757b1a9697b6..e5867e7939484 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -86,10 +86,10 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* System implementation of MediaSessionManager
@@ -103,12 +103,14 @@ public class MediaSessionService extends SystemService implements Monitor {
private static final int WAKELOCK_TIMEOUT = 5000;
private static final int MEDIA_KEY_LISTENER_TIMEOUT = 1000;
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 final Context mContext;
private final SessionManagerImpl mSessionManagerImpl;
private final MessageHandler mHandler = new MessageHandler();
private final PowerManager.WakeLock mMediaEventWakeLock;
- private final int mLongPressTimeout;
private final INotificationManager mNotificationManager;
private final Object mLock = new Object();
// Keeps the full user id for each user.
@@ -142,8 +144,7 @@ public class MediaSessionService extends SystemService implements Monitor {
private SessionPolicyProvider mCustomSessionPolicyProvider;
private MediaKeyDispatcher mCustomMediaKeyDispatcher;
- private Method mGetSessionForKeyEventMethod;
- private Method mGetSessionPoliciesMethod;
+ private Map