Allow privileged app to set media key event listener

If the media key listener is set, the listener will receive the media key
events before any other sessions, but after the global priority session.
If the event is handled by the listener, other sessions cannot get the event.

Privileged app needs permission android.permission.SET_MEDIA_KEY_LISTENER
to set the listener.

Bug: 30125811
Change-Id: I2b2cf4ac7873b70899194701c6921990dcb9de02
diff --git a/Android.mk b/Android.mk
index fac4d55..663f429 100644
--- a/Android.mk
+++ b/Android.mk
@@ -423,6 +423,7 @@
 	media/java/android/media/projection/IMediaProjectionManager.aidl \
 	media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl \
 	media/java/android/media/session/IActiveSessionsListener.aidl \
+	media/java/android/media/session/IOnMediaKeyListener.aidl \
 	media/java/android/media/session/IOnVolumeKeyLongPressListener.aidl \
 	media/java/android/media/session/ISession.aidl \
 	media/java/android/media/session/ISessionCallback.aidl \
diff --git a/api/system-current.txt b/api/system-current.txt
index 07f2ce5..b7a537e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -215,6 +215,7 @@
     field public static final java.lang.String SET_ALWAYS_FINISH = "android.permission.SET_ALWAYS_FINISH";
     field public static final java.lang.String SET_ANIMATION_SCALE = "android.permission.SET_ANIMATION_SCALE";
     field public static final java.lang.String SET_DEBUG_APP = "android.permission.SET_DEBUG_APP";
+    field public static final java.lang.String SET_MEDIA_KEY_LISTENER = "android.permission.SET_MEDIA_KEY_LISTENER";
     field public static final java.lang.String SET_ORIENTATION = "android.permission.SET_ORIENTATION";
     field public static final java.lang.String SET_POINTER_SPEED = "android.permission.SET_POINTER_SPEED";
     field public static final deprecated java.lang.String SET_PREFERRED_APPLICATIONS = "android.permission.SET_PREFERRED_APPLICATIONS";
@@ -24820,6 +24821,7 @@
     method public void addOnActiveSessionsChangedListener(android.media.session.MediaSessionManager.OnActiveSessionsChangedListener, android.content.ComponentName, android.os.Handler);
     method public java.util.List<android.media.session.MediaController> getActiveSessions(android.content.ComponentName);
     method public void removeOnActiveSessionsChangedListener(android.media.session.MediaSessionManager.OnActiveSessionsChangedListener);
+    method public void setOnMediaKeyListener(android.media.session.MediaSessionManager.OnMediaKeyListener, android.os.Handler);
     method public void setOnVolumeKeyLongPressListener(android.media.session.MediaSessionManager.OnVolumeKeyLongPressListener, android.os.Handler);
   }
 
@@ -24827,6 +24829,10 @@
     method public abstract void onActiveSessionsChanged(java.util.List<android.media.session.MediaController>);
   }
 
+  public static abstract interface MediaSessionManager.OnMediaKeyListener {
+    method public abstract boolean onMediaKey(android.view.KeyEvent);
+  }
+
   public static abstract interface MediaSessionManager.OnVolumeKeyLongPressListener {
     method public abstract void onVolumeKeyLongPress(android.view.KeyEvent);
   }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f7cec81..b4e3dd2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2632,6 +2632,14 @@
     <permission android:name="android.permission.SET_VOLUME_KEY_LONG_PRESS_LISTENER"
         android:protectionLevel="signature|privileged|development" />
 
+    <!-- @SystemApi @hide Allows an application to set media key event listener.
+         <p>When it's set, the application will receive the media key event before
+         any other media sessions. If the event is handled by the listener, other sessions
+         cannot get the event.</p>
+         <p>Not for use by third-party applications</p> -->
+    <permission android:name="android.permission.SET_MEDIA_KEY_LISTENER"
+        android:protectionLevel="signature|privileged|development" />
+
     <!-- @SystemApi Required to be able to disable the device (very dangerous!).
          <p>Not for use by third-party applications.
          @hide
diff --git a/media/java/android/media/session/IOnMediaKeyListener.aidl b/media/java/android/media/session/IOnMediaKeyListener.aidl
new file mode 100644
index 0000000..7752357
--- /dev/null
+++ b/media/java/android/media/session/IOnMediaKeyListener.aidl
@@ -0,0 +1,28 @@
+/* Copyright (C) 2016 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.media.session;
+
+import android.os.ResultReceiver;
+import android.view.KeyEvent;
+
+/**
+ * Listener to handle media key.
+ * @hide
+ */
+interface IOnMediaKeyListener {
+    void onMediaKey(in KeyEvent event, in ResultReceiver result);
+}
+
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index 9bd9b45..8a98773 100644
--- a/media/java/android/media/session/ISessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -19,6 +19,7 @@
 import android.media.IRemoteVolumeController;
 import android.media.session.IActiveSessionsListener;
 import android.media.session.IOnVolumeKeyLongPressListener;
+import android.media.session.IOnMediaKeyListener;
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
 import android.os.Bundle;
@@ -45,5 +46,6 @@
     boolean isGlobalPriorityActive();
 
     void setOnVolumeKeyLongPressListener(in IOnVolumeKeyLongPressListener listener);
+    void setOnMediaKeyListener(in IOnMediaKeyListener listener);
 }
 
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index 2005573..80d2a0c 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -27,6 +27,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
@@ -51,6 +52,18 @@
 public final class MediaSessionManager {
     private static final String TAG = "SessionManager";
 
+    /**
+     * Used by IOnMediaKeyListener to indicate that the media key event isn't handled.
+     * @hide
+     */
+    public static final int RESULT_MEDIA_KEY_NOT_HANDLED = 0;
+
+    /**
+     * Used by IOnMediaKeyListener to indicate that the media key event is handled.
+     * @hide
+     */
+    public static final int RESULT_MEDIA_KEY_HANDLED = 1;
+
     private final ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper> mListeners
             = new ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper>();
     private final Object mLock = new Object();
@@ -59,6 +72,7 @@
     private Context mContext;
 
     private OnVolumeKeyLongPressListenerImpl mOnVolumeKeyLongPressListener;
+    private OnMediaKeyListenerImpl mOnMediaKeyListener;
 
     /**
      * @hide
@@ -364,6 +378,39 @@
     }
 
     /**
+     * Set the media key listener. While the listener is set, the listener
+     * gets the media key before any other media sessions but after the global priority session.
+     * If the listener handles the key (i.e. returns {@code true}),
+     * other sessions will not get the event.
+     *
+     * <p>System can only have a single media key listener.
+     *
+     * @param listener The media key listener. {@code null} to reset.
+     * @param handler The handler on which the listener should be invoked, or {@code null}
+     *            if the listener should be invoked on the calling thread's looper.
+     * @hide
+     */
+    @SystemApi
+    public void setOnMediaKeyListener(OnMediaKeyListener listener, @Nullable Handler handler) {
+        synchronized (mLock) {
+            try {
+                if (listener == null) {
+                    mOnMediaKeyListener = null;
+                    mService.setOnMediaKeyListener(null);
+                } else {
+                    if (handler == null) {
+                        handler = new Handler();
+                    }
+                    mOnMediaKeyListener = new OnMediaKeyListenerImpl(listener, handler);
+                    mService.setOnMediaKeyListener(mOnMediaKeyListener);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to set media key listener", e);
+            }
+        }
+    }
+
+    /**
      * Listens for changes to the list of active sessions. This can be added
      * using {@link #addOnActiveSessionsChangedListener}.
      */
@@ -384,6 +431,20 @@
         void onVolumeKeyLongPress(KeyEvent event);
     }
 
+    /**
+     * Listens the media key.
+     * @hide
+     */
+    @SystemApi
+    public interface OnMediaKeyListener {
+        /**
+         * Called when the media key is pressed.
+         * <p>If it takes more than 1s to return, the key event will be sent to
+         * other media sessions.
+         */
+        boolean onMediaKey(KeyEvent event);
+    }
+
     private static final class SessionsChangedWrapper {
         private Context mContext;
         private OnActiveSessionsChangedListener mListener;
@@ -451,4 +512,33 @@
             });
         }
     }
+
+    private static final class OnMediaKeyListenerImpl extends IOnMediaKeyListener.Stub {
+        private OnMediaKeyListener mListener;
+        private Handler mHandler;
+
+        public OnMediaKeyListenerImpl(OnMediaKeyListener listener, Handler handler) {
+            mListener = listener;
+            mHandler = handler;
+        }
+
+        @Override
+        public void onMediaKey(KeyEvent event, ResultReceiver result) {
+            if (mListener == null || mHandler == null) {
+                Log.w(TAG, "Failed to call media key listener." +
+                        " Either mListener or mHandler is null");
+                return;
+            }
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    boolean handled = mListener.onMediaKey(event);
+                    Log.d(TAG, "The media key listener is returned " + handled);
+                    result.send(
+                            handled ? RESULT_MEDIA_KEY_HANDLED : RESULT_MEDIA_KEY_NOT_HANDLED,
+                            null);
+                }
+            });
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 04d5d9f..3bf95ef 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -37,11 +37,13 @@
 import android.media.IAudioService;
 import android.media.IRemoteVolumeController;
 import android.media.session.IActiveSessionsListener;
+import android.media.session.IOnMediaKeyListener;
 import android.media.session.IOnVolumeKeyLongPressListener;
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
 import android.media.session.ISessionManager;
 import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
@@ -84,6 +86,7 @@
     private static final boolean DEBUG_KEY_EVENT = true;
 
     private static final int WAKELOCK_TIMEOUT = 5000;
+    private static final int MEDIA_KEY_LISTENER_TIMEOUT = 1000;
 
     /* package */final IBinder mICallback = new Binder();
 
@@ -550,6 +553,9 @@
         private int mInitialDownVolumeStream;
         private boolean mInitialDownMusicOnly;
 
+        private IOnMediaKeyListener mOnMediaKeyListener;
+        private int mOnMediaKeyListenerUid;
+
         public UserRecord(Context context, int userId) {
             mContext = context;
             mUserId = userId;
@@ -583,6 +589,9 @@
             pw.println(indent + "Volume key long-press listener:" + mOnVolumeKeyLongPressListener);
             pw.println(indent + "Volume key long-press listener package:" +
                     getCallingPackageName(mOnVolumeKeyLongPressListenerUid));
+            pw.println(indent + "Media key listener: " + mOnMediaKeyListener);
+            pw.println(indent + "Media key listener package: " +
+                    getCallingPackageName(mOnMediaKeyListenerUid));
             int size = mSessions.size();
             pw.println(indent + size + " Sessions:");
             for (int i = 0; i < size; i++) {
@@ -788,28 +797,10 @@
                 }
 
                 synchronized (mLock) {
-                    // If we don't have a media button receiver to fall back on
-                    // include non-playing sessions for dispatching
-                    boolean useNotPlayingSessions = true;
-                    for (int userId : mCurrentUserIdList) {
-                        UserRecord ur = mUserRecords.get(userId);
-                        if (ur.mLastMediaButtonReceiver != null
-                                || ur.mRestoredMediaButtonReceiver != null) {
-                            useNotPlayingSessions = false;
-                            break;
-                        }
-                    }
-
-                    if (DEBUG) {
-                        Log.d(TAG, "dispatchMediaKeyEvent, useNotPlayingSessions="
-                                + useNotPlayingSessions);
-                    }
-                    MediaSessionRecord session = mPriorityStack.getDefaultMediaButtonSession(
-                            mCurrentUserIdList, useNotPlayingSessions);
-                    if (isVoiceKey(keyEvent.getKeyCode())) {
-                        handleVoiceKeyEventLocked(keyEvent, needWakeLock, session);
+                    if (!isGlobalPriorityActive() && isVoiceKey(keyEvent.getKeyCode())) {
+                        handleVoiceKeyEventLocked(keyEvent, needWakeLock);
                     } else {
-                        dispatchMediaKeyEventLocked(keyEvent, needWakeLock, session);
+                        dispatchMediaKeyEventLocked(keyEvent, needWakeLock, true);
                     }
                 }
             } finally {
@@ -868,6 +859,56 @@
             }
         }
 
+        @Override
+        public void setOnMediaKeyListener(IOnMediaKeyListener listener) {
+            final int pid = Binder.getCallingPid();
+            final int uid = Binder.getCallingUid();
+            final long token = Binder.clearCallingIdentity();
+            try {
+                // Enforce SET_MEDIA_KEY_LISTENER permission.
+                if (getContext().checkPermission(
+                        android.Manifest.permission.SET_MEDIA_KEY_LISTENER, pid, uid)
+                            != PackageManager.PERMISSION_GRANTED) {
+                    throw new SecurityException("Must hold the SET_MEDIA_KEY_LISTENER" +
+                            " permission.");
+                }
+
+                synchronized (mLock) {
+                    int userId = UserHandle.getUserId(uid);
+                    UserRecord user = mUserRecords.get(userId);
+                    if (user.mOnMediaKeyListener != null && user.mOnMediaKeyListenerUid != uid) {
+                        Log.w(TAG, "Media key listener cannot be reset by another app");
+                        return;
+                    }
+
+                    user.mOnMediaKeyListener = listener;
+                    user.mOnMediaKeyListenerUid = uid;
+
+                    Log.d(TAG, "Media key listener " + user.mOnMediaKeyListener
+                            + " is set by " + getCallingPackageName(uid));
+
+                    if (user.mOnMediaKeyListener != null) {
+                        try {
+                            user.mOnMediaKeyListener.asBinder().linkToDeath(
+                                    new IBinder.DeathRecipient() {
+                                        @Override
+                                        public void binderDied() {
+                                            synchronized (mLock) {
+                                                user.mOnMediaKeyListener = null;
+                                            }
+                                        }
+                                    }, 0);
+                        } catch (RemoteException e) {
+                            Log.w(TAG, "Failed to set death recipient " + user.mOnMediaKeyListener);
+                            user.mOnMediaKeyListener = null;
+                        }
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
         /**
          * Handles the dispatching of the volume button events to one of the
          * registered listeners. If there's a volume key long-press listener and
@@ -1127,13 +1168,7 @@
             }
         }
 
-        private void handleVoiceKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock,
-                MediaSessionRecord session) {
-            if (session != null && session.hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY)) {
-                // If the phone app has priority just give it the event
-                dispatchMediaKeyEventLocked(keyEvent, needWakeLock, session);
-                return;
-            }
+        private void handleVoiceKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock) {
             int action = keyEvent.getAction();
             boolean isLongPress = (keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0;
             if (action == KeyEvent.ACTION_DOWN) {
@@ -1150,15 +1185,52 @@
                     if (!mVoiceButtonHandled && !keyEvent.isCanceled()) {
                         // Resend the down then send this event through
                         KeyEvent downEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_DOWN);
-                        dispatchMediaKeyEventLocked(downEvent, needWakeLock, session);
-                        dispatchMediaKeyEventLocked(keyEvent, needWakeLock, session);
+                        dispatchMediaKeyEventLocked(downEvent, needWakeLock, true);
+                        dispatchMediaKeyEventLocked(keyEvent, needWakeLock, true);
                     }
                 }
             }
         }
 
         private void dispatchMediaKeyEventLocked(KeyEvent keyEvent, boolean needWakeLock,
-                MediaSessionRecord session) {
+                boolean checkMediaKeyListener) {
+            // If we don't have a media button receiver to fall back on
+            // include non-playing sessions for dispatching.
+            boolean useNotPlayingSessions = true;
+            for (int userId : mCurrentUserIdList) {
+                UserRecord ur = mUserRecords.get(userId);
+                if (ur.mLastMediaButtonReceiver != null
+                        || ur.mRestoredMediaButtonReceiver != null) {
+                    useNotPlayingSessions = false;
+                    break;
+                }
+            }
+            if (DEBUG) {
+                Log.d(TAG, "dispatchMediaKeyEvent, useNotPlayingSessions="
+                        + useNotPlayingSessions);
+            }
+
+            MediaSessionRecord session = mPriorityStack.getDefaultMediaButtonSession(
+                    mCurrentUserIdList, useNotPlayingSessions);
+
+            if ((session == null
+                    || !session.hasFlag(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY))
+                    && checkMediaKeyListener) {
+                // Only consider full user.
+                UserRecord user = mUserRecords.get(mCurrentUserIdList.get(0));
+                if (user.mOnMediaKeyListener != null) {
+                    if (DEBUG_KEY_EVENT) {
+                        Log.d(TAG, "Send " + keyEvent + " to media key listener");
+                    }
+                    try {
+                        user.mOnMediaKeyListener.onMediaKey(keyEvent,
+                                new MediaKeyListenerResultReceiver(keyEvent, needWakeLock));
+                        return;
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "Failed to send " + keyEvent + " to media key listener");
+                    }
+                }
+            }
             if (session != null) {
                 if (DEBUG_KEY_EVENT) {
                     Log.d(TAG, "Sending " + keyEvent + " to " + session);
@@ -1282,6 +1354,46 @@
                     && streamType <= AudioManager.STREAM_NOTIFICATION;
         }
 
+        private class MediaKeyListenerResultReceiver extends ResultReceiver implements Runnable {
+            private KeyEvent mKeyEvent;
+            private boolean mNeedWakeLock;
+            private boolean mHandled;
+
+            private MediaKeyListenerResultReceiver(KeyEvent keyEvent, boolean needWakeLock) {
+                super(mHandler);
+                mHandler.postDelayed(this, MEDIA_KEY_LISTENER_TIMEOUT);
+                mKeyEvent = keyEvent;
+                mNeedWakeLock = needWakeLock;
+            }
+
+            @Override
+            public void run() {
+                Log.d(TAG, "The media key listener is timed-out for " + mKeyEvent);
+                dispatchMediaKeyEvent();
+            }
+
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                if (resultCode == MediaSessionManager.RESULT_MEDIA_KEY_HANDLED) {
+                    mHandled = true;
+                    mHandler.removeCallbacks(this);
+                    return;
+                }
+                dispatchMediaKeyEvent();
+            }
+
+            private void dispatchMediaKeyEvent() {
+                if (mHandled) {
+                    return;
+                }
+                mHandled = true;
+                mHandler.removeCallbacks(this);
+                synchronized (mLock) {
+                    dispatchMediaKeyEventLocked(mKeyEvent, mNeedWakeLock, false);
+                }
+            }
+        }
+
         private KeyEventWakeLockReceiver mKeyEventReceiver = new KeyEventWakeLockReceiver(mHandler);
 
         class KeyEventWakeLockReceiver extends ResultReceiver implements Runnable,