MediaSession2: Add listeners for change in session token

Test: Run all MediaComponents tests once
Change-Id: Ic46ad9e4e4c9e1ce43b3dbad904eae7fc30d52a0
diff --git a/Android.bp b/Android.bp
index 763d242..28e51e7 100644
--- a/Android.bp
+++ b/Android.bp
@@ -419,9 +419,9 @@
         "location/java/android/location/IGpsGeofenceHardware.aidl",
         "location/java/android/location/INetInitiatedListener.aidl",
         "location/java/com/android/internal/location/ILocationProvider.aidl",
-        "media/java/android/media/IAudioService.aidl",
         "media/java/android/media/IAudioFocusDispatcher.aidl",
         "media/java/android/media/IAudioRoutesObserver.aidl",
+        "media/java/android/media/IAudioService.aidl",
         "media/java/android/media/IMediaHTTPConnection.aidl",
         "media/java/android/media/IMediaHTTPService.aidl",
         "media/java/android/media/IMediaResourceMonitor.aidl",
@@ -432,6 +432,7 @@
         "media/java/android/media/IMediaSession2.aidl",
         "media/java/android/media/IMediaSession2Callback.aidl",
         "media/java/android/media/IPlaybackConfigDispatcher.aidl",
+        "media/java/android/media/ISessionTokensListener.aidl",
         ":libaudioclient_aidl",
         "media/java/android/media/IRecordingConfigDispatcher.aidl",
         "media/java/android/media/IRemoteDisplayCallback.aidl",
diff --git a/media/java/android/media/ISessionTokensListener.aidl b/media/java/android/media/ISessionTokensListener.aidl
new file mode 100644
index 0000000..c83a19e
--- /dev/null
+++ b/media/java/android/media/ISessionTokensListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018 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;
+
+import android.os.Bundle;
+
+/**
+ * Listens for changes to the list of session tokens.
+ * @hide
+ */
+oneway interface ISessionTokensListener {
+    void onSessionTokensChanged(in List<Bundle> tokens);
+}
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index 8135106..37c46cb 100644
--- a/media/java/android/media/session/ISessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -18,6 +18,7 @@
 import android.content.ComponentName;
 import android.media.IRemoteVolumeController;
 import android.media.IMediaSession2;
+import android.media.ISessionTokensListener;
 import android.media.session.IActiveSessionsListener;
 import android.media.session.ICallback;
 import android.media.session.IOnMediaKeyListener;
@@ -55,4 +56,8 @@
     boolean onSessionCreated(in Bundle sessionToken);
     void onSessionDestroyed(in Bundle sessionToken);
     List<Bundle> getSessionTokens(boolean activeSessionOnly, boolean sessionServiceOnly);
+
+    void addSessionTokensListener(in ISessionTokensListener listener, int userId,
+            String packageName);
+    void removeSessionTokensListener(in ISessionTokensListener listener);
 }
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index 1023a10..454113c 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -16,6 +16,7 @@
 
 package android.media.session;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -24,8 +25,8 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.media.AudioManager;
-import android.media.IMediaSession2;
 import android.media.IRemoteVolumeController;
+import android.media.ISessionTokensListener;
 import android.media.MediaSession2;
 import android.media.MediaSessionService2;
 import android.media.SessionToken2;
@@ -44,6 +45,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * Provides support for interacting with {@link MediaSession media sessions}
@@ -71,6 +73,8 @@
 
     private final ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper> mListeners
             = new ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper>();
+    private final ArrayMap<OnSessionTokensChangedListener, SessionTokensChangedWrapper>
+            mSessionTokensListener = new ArrayMap<>();
     private final Object mLock = new Object();
     private final ISessionManager mService;
 
@@ -371,13 +375,15 @@
      * Get {@link List} of {@link SessionToken2} whose sessions are active now. This list represents
      * active sessions regardless of whether they're {@link MediaSession2} or
      * {@link MediaSessionService2}.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
      *
-     * @return list of Tokens
+     * @return list of tokens
      * @hide
      */
     // TODO(jaewan): Unhide
-    // TODO(jaewan): Protect this with permission.
-    // TODO(jaewna): Add listener for change in lists.
     public List<SessionToken2> getActiveSessionTokens() {
         try {
             List<Bundle> bundles = mService.getSessionTokens(
@@ -392,12 +398,15 @@
     /**
      * Get {@link List} of {@link SessionToken2} for {@link MediaSessionService2} regardless of their
      * activeness. This list represents media apps that support background playback.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
      *
-     * @return list of Tokens
+     * @return list of tokens
      * @hide
      */
     // TODO(jaewan): Unhide
-    // TODO(jaewna): Add listener for change in lists.
     public List<SessionToken2> getSessionServiceTokens() {
         try {
             List<Bundle> bundles = mService.getSessionTokens(
@@ -412,15 +421,17 @@
     /**
      * Get all {@link SessionToken2}s. This is the combined list of {@link #getActiveSessionTokens()}
      * and {@link #getSessionServiceTokens}.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
      *
-     * @return list of Tokens
+     * @return list of tokens
      * @see #getActiveSessionTokens
      * @see #getSessionServiceTokens
      * @hide
      */
     // TODO(jaewan): Unhide
-    // TODO(jaewan): Protect this with permission.
-    // TODO(jaewna): Add listener for change in lists.
     public List<SessionToken2> getAllSessionTokens() {
         try {
             List<Bundle> bundles = mService.getSessionTokens(
@@ -432,6 +443,86 @@
         }
     }
 
+    /**
+     * Add a listener to be notified when the {@link #getAllSessionTokens()} changes.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
+     *
+     * @param executor executor to run this command
+     * @param listener The listener to add.
+     * @hide
+     */
+    // TODO(jaewan): Unhide
+    public void addOnSessionTokensChangedListener(@NonNull @CallbackExecutor Executor executor,
+            @NonNull OnSessionTokensChangedListener listener) {
+        addOnSessionTokensChangedListener(UserHandle.myUserId(), executor, listener);
+    }
+
+    /**
+     * Add a listener to be notified when the {@link #getAllSessionTokens()} changes.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
+     *
+     * @param userId The userId to listen for changes on.
+     * @param executor executor to run this command
+     * @param listener The listener to add.
+     * @hide
+     */
+    public void addOnSessionTokensChangedListener(int userId,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnSessionTokensChangedListener listener) {
+        if (executor == null) {
+            throw new IllegalArgumentException("executor may not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("listener may not be null");
+        }
+        synchronized (mLock) {
+            if (mSessionTokensListener.get(listener) != null) {
+                Log.w(TAG, "Attempted to add session listener twice, ignoring.");
+                return;
+            }
+            SessionTokensChangedWrapper wrapper = new SessionTokensChangedWrapper(
+                    mContext, executor, listener);
+            try {
+                mService.addSessionTokensListener(wrapper.mStub, userId, mContext.getPackageName());
+                mSessionTokensListener.put(listener, wrapper);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in addSessionTokensListener.", e);
+            }
+        }
+    }
+
+    /**
+     * Stop receiving session token updates on the specified listener.
+     *
+     * @param listener The listener to remove.
+     * @hide
+     */
+    // TODO(jaewan): Unhide
+    public void removeOnSessionTokensChangedListener(
+            @NonNull OnSessionTokensChangedListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener may not be null");
+        }
+        synchronized (mLock) {
+            SessionTokensChangedWrapper wrapper = mSessionTokensListener.remove(listener);
+            if (wrapper != null) {
+                try {
+                    mService.removeSessionTokensListener(wrapper.mStub);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error in removeSessionTokensListener.", e);
+                } finally {
+                    wrapper.release();
+                }
+            }
+        }
+    }
+
     private static List<SessionToken2> toTokenList(Context context, List<Bundle> bundles) {
         List<SessionToken2> tokens = new ArrayList<>();
         if (bundles != null) {
@@ -567,6 +658,16 @@
     }
 
     /**
+     * Listens for changes to the {@link #getAllSessionTokens()}. This can be added
+     * using {@link #addOnActiveSessionsChangedListener}.
+     * @hide
+     */
+    // TODO(jaewan): Unhide
+    public interface OnSessionTokensChangedListener {
+        void onSessionTokensChanged(@NonNull List<SessionToken2> tokens);
+    }
+
+    /**
      * Listens the volume key long-presses.
      * @hide
      */
@@ -692,6 +793,35 @@
         }
     }
 
+    private static final class SessionTokensChangedWrapper {
+        private Context mContext;
+        private Executor mExecutor;
+        private OnSessionTokensChangedListener mListener;
+
+        public SessionTokensChangedWrapper(Context context, Executor executor,
+                OnSessionTokensChangedListener listener) {
+            mContext = context;
+            mExecutor = executor;
+            mListener = listener;
+        }
+
+        private final ISessionTokensListener.Stub mStub = new ISessionTokensListener.Stub() {
+            @Override
+            public void onSessionTokensChanged(final List<Bundle> bundles) {
+                mExecutor.execute(() -> {
+                    List<SessionToken2> tokens = toTokenList(mContext, bundles);
+                    mListener.onSessionTokensChanged(tokens);
+                });
+            }
+        };
+
+        private void release() {
+            mListener = null;
+            mContext = null;
+            mExecutor = null;
+        }
+    }
+
     private static final class OnVolumeKeyLongPressListenerImpl
             extends IOnVolumeKeyLongPressListener.Stub {
         private OnVolumeKeyLongPressListener mListener;
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 1d6d1ed..02df84e 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -41,6 +41,7 @@
 import android.media.IAudioService;
 import android.media.IMediaSession2;
 import android.media.IRemoteVolumeController;
+import android.media.ISessionTokensListener;
 import android.media.MediaLibraryService2;
 import android.media.MediaSessionService2;
 import android.media.SessionToken2;
@@ -1480,7 +1481,6 @@
         }
 
         // TODO(jaewan): Protect this API with permission
-        // TODO(jaewan): Add listeners for change in operations..
         @Override
         public List<Bundle> getSessionTokens(boolean activeSessionOnly,
                 boolean sessionServiceOnly) throws RemoteException {
@@ -1504,6 +1504,17 @@
             return tokens;
         }
 
+        @Override
+        public void addSessionTokensListener(ISessionTokensListener listener, int userId,
+                String packageName) {
+            // TODO(jaewan): Implement.
+        }
+
+        @Override
+        public void removeSessionTokensListener(ISessionTokensListener listener) {
+            // TODO(jaewan): Implement
+        }
+
         private int verifySessionsRequest(ComponentName componentName, int userId, final int pid,
                 final int uid) {
             String packageName = null;