Adds listeners for changes to the list of active sessions

The listeners get notified when sessions are added, removed, or
reprioritized.

Change-Id: I7f3bfc84049719c3b9c19016c6bac92e1a5c3179
diff --git a/Android.mk b/Android.mk
index a1d91c36..8b0e556 100644
--- a/Android.mk
+++ b/Android.mk
@@ -306,14 +306,15 @@
 	media/java/android/media/IRemoteVolumeObserver.aidl \
 	media/java/android/media/IRingtonePlayer.aidl \
 	media/java/android/media/IVolumeController.aidl \
-        media/java/android/media/routeprovider/IRouteConnection.aidl \
-        media/java/android/media/routeprovider/IRouteProvider.aidl \
-        media/java/android/media/routeprovider/IRouteProviderCallback.aidl \
-        media/java/android/media/session/ISessionController.aidl \
-        media/java/android/media/session/ISessionControllerCallback.aidl \
-        media/java/android/media/session/ISession.aidl \
-        media/java/android/media/session/ISessionCallback.aidl \
-        media/java/android/media/session/ISessionManager.aidl \
+	media/java/android/media/routeprovider/IRouteConnection.aidl \
+	media/java/android/media/routeprovider/IRouteProvider.aidl \
+	media/java/android/media/routeprovider/IRouteProviderCallback.aidl \
+	media/java/android/media/session/IActiveSessionsListener.aidl \
+	media/java/android/media/session/ISessionController.aidl \
+	media/java/android/media/session/ISessionControllerCallback.aidl \
+	media/java/android/media/session/ISession.aidl \
+	media/java/android/media/session/ISessionCallback.aidl \
+	media/java/android/media/session/ISessionManager.aidl \
 	media/java/android/media/tv/ITvInputClient.aidl \
 	media/java/android/media/tv/ITvInputHardware.aidl \
 	media/java/android/media/tv/ITvInputHardwareCallback.aidl \
diff --git a/media/java/android/media/session/IActiveSessionsListener.aidl b/media/java/android/media/session/IActiveSessionsListener.aidl
new file mode 100644
index 0000000..e5e24bc
--- /dev/null
+++ b/media/java/android/media/session/IActiveSessionsListener.aidl
@@ -0,0 +1,26 @@
+/* Copyright (C) 2014 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.media.session.MediaSessionToken;
+
+/**
+ * Listens for changes to the list of active sessions.
+ * @hide
+ */
+oneway interface IActiveSessionsListener {
+    void onActiveSessionsChanged(in List<MediaSessionToken> sessions);
+}
\ No newline at end of file
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index 6d9888f..bd1fa85 100644
--- a/media/java/android/media/session/ISessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -16,6 +16,7 @@
 package android.media.session;
 
 import android.content.ComponentName;
+import android.media.session.IActiveSessionsListener;
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
 import android.os.Bundle;
@@ -30,4 +31,7 @@
     List<IBinder> getSessions(in ComponentName compName, int userId);
     void dispatchMediaKeyEvent(in KeyEvent keyEvent, boolean needWakeLock);
     void dispatchAdjustVolumeBy(int suggestedStream, int delta, int flags);
+    void addSessionsListener(in IActiveSessionsListener listener, in ComponentName compName,
+            int userId);
+    void removeSessionsListener(in IActiveSessionsListener listener);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java
index 9e8b0d3..291bfc8 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/MediaSessionManager.java
@@ -142,6 +142,50 @@
     }
 
     /**
+     * Add a listener to be notified when the list of active sessions
+     * changes.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, in which case you must pass the
+     * {@link ComponentName} of your enabled listener.
+     *
+     * @param sessionListener The listener to add.
+     * @param notificationListener The enabled notification listener component.
+     *            May be null.
+     * @param userId The userId to listen for changes on.
+     * @hide
+     */
+    public void addActiveSessionsListener(SessionListener sessionListener,
+            ComponentName notificationListener, int userId) {
+        if (sessionListener == null) {
+            throw new IllegalArgumentException("listener may not be null");
+        }
+        try {
+            mService.addSessionsListener(sessionListener.mStub, notificationListener, userId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in addActiveSessionsListener.", e);
+        }
+    }
+
+    /**
+     * Stop receiving active sessions updates on the specified listener.
+     *
+     * @param listener The listener to remove.
+     * @hide
+     */
+    public void removeActiveSessionsListener(SessionListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener may not be null");
+        }
+        try {
+            mService.removeSessionsListener(listener.mStub);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in removeActiveSessionsListener.", e);
+        }
+    }
+
+    /**
      * Send a media key event. The receiver will be selected automatically.
      *
      * @param keyEvent The KeyEvent to send.
@@ -184,4 +228,35 @@
             Log.e(TAG, "Failed to send adjust volume.", e);
         }
     }
+
+    /**
+     * Listens for changes to the list of active sessions. This can be added
+     * using {@link #addActiveSessionsListener}.
+     *
+     * @hide
+     */
+    public static abstract class SessionListener {
+        /**
+         * Called when the list of active sessions has changed. This can be due
+         * to a session being added or removed or the order of sessions
+         * changing.
+         *
+         * @param controllers The updated list of controllers for the user that
+         *            changed.
+         */
+        public abstract void onActiveSessionsChanged(List<MediaController> controllers);
+
+        private final IActiveSessionsListener.Stub mStub = new IActiveSessionsListener.Stub() {
+            @Override
+            public void onActiveSessionsChanged(List<MediaSessionToken> tokens)
+                    throws RemoteException {
+                ArrayList<MediaController> controllers = new ArrayList<MediaController>();
+                int size = tokens.size();
+                for (int i = 0; i < size; i++) {
+                    controllers.add(MediaController.fromToken(tokens.get(i)));
+                }
+                SessionListener.this.onActiveSessionsChanged(controllers);
+            }
+        };
+    }
 }
diff --git a/media/java/android/media/session/MediaSessionToken.java b/media/java/android/media/session/MediaSessionToken.java
index 86f5662..e599189 100644
--- a/media/java/android/media/session/MediaSessionToken.java
+++ b/media/java/android/media/session/MediaSessionToken.java
@@ -31,7 +31,7 @@
     /**
      * @hide
      */
-    MediaSessionToken(ISessionController binder) {
+    public MediaSessionToken(ISessionController binder) {
         mBinder = binder;
     }
 
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 87665e1..4d1829d 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -29,9 +29,11 @@
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.media.routeprovider.RouteRequest;
+import android.media.session.IActiveSessionsListener;
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
 import android.media.session.ISessionManager;
+import android.media.session.MediaSessionToken;
 import android.media.session.RouteInfo;
 import android.media.session.RouteOptions;
 import android.media.session.MediaSession;
@@ -39,6 +41,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -75,10 +78,12 @@
 
     private final ArrayList<MediaSessionRecord> mAllSessions = new ArrayList<MediaSessionRecord>();
     private final SparseArray<UserRecord> mUserRecords = new SparseArray<UserRecord>();
+    private final ArrayList<SessionsListenerRecord> mSessionsListeners
+            = new ArrayList<SessionsListenerRecord>();
     // private final ArrayList<MediaRouteProviderProxy> mProviders
     // = new ArrayList<MediaRouteProviderProxy>();
     private final Object mLock = new Object();
-    private final Handler mHandler = new Handler();
+    private final MessageHandler mHandler = new MessageHandler();
     private final PowerManager.WakeLock mMediaEventWakeLock;
 
     private KeyguardManager mKeyguardManager;
@@ -200,15 +205,20 @@
                 }
             }
         }
+        mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, record.getUserId(), 0);
     }
 
     public void onSessionPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
+        boolean updateSessions = false;
         synchronized (mLock) {
             if (!mAllSessions.contains(record)) {
                 Log.d(TAG, "Unknown session changed playback state. Ignoring.");
                 return;
             }
-            mPriorityStack.onPlaystateChange(record, oldState, newState);
+            updateSessions = mPriorityStack.onPlaystateChange(record, oldState, newState);
+        }
+        if (updateSessions) {
+            mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, record.getUserId(), 0);
         }
     }
 
@@ -315,6 +325,8 @@
             // ignore exceptions while destroying a session.
         }
         session.onDestroy();
+
+        mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, session.getUserId(), 0);
     }
 
     private void enforcePackageName(String packageName, int uid) {
@@ -428,6 +440,8 @@
         UserRecord user = getOrCreateUser(userId);
         user.addSessionLocked(session);
 
+        mHandler.post(MessageHandler.MSG_SESSIONS_CHANGED, userId, 0);
+
         if (DEBUG) {
             Log.d(TAG, "Created session for package " + callerPackageName + " with tag " + tag);
         }
@@ -453,11 +467,43 @@
         return -1;
     }
 
+    private int findIndexOfSessionsListenerLocked(IActiveSessionsListener listener) {
+        for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
+            if (mSessionsListeners.get(i).mListener == listener) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     private boolean isSessionDiscoverable(MediaSessionRecord record) {
         // TODO probably want to check more than if it's active.
         return record.isActive();
     }
 
+    private void pushSessionsChanged(int userId) {
+        synchronized (mLock) {
+            List<MediaSessionRecord> records = mPriorityStack.getActiveSessions(userId);
+            int size = records.size();
+            ArrayList<MediaSessionToken> tokens = new ArrayList<MediaSessionToken>();
+            for (int i = 0; i < size; i++) {
+                tokens.add(new MediaSessionToken(records.get(i).getControllerBinder()));
+            }
+            for (int i = mSessionsListeners.size() - 1; i >= 0; i--) {
+                SessionsListenerRecord record = mSessionsListeners.get(i);
+                if (record.mUserId == UserHandle.USER_ALL || record.mUserId == userId) {
+                    try {
+                        record.mListener.onActiveSessionsChanged(tokens);
+                    } catch (RemoteException e) {
+                        Log.w(TAG, "Dead ActiveSessionsListener in pushSessionsChanged, removing",
+                                e);
+                        mSessionsListeners.remove(i);
+                    }
+                }
+            }
+        }
+    }
+
     private MediaRouteProviderProxy.RoutesListener mRoutesCallback
             = new MediaRouteProviderProxy.RoutesListener() {
         @Override
@@ -613,6 +659,23 @@
         };
     }
 
+    final class SessionsListenerRecord implements IBinder.DeathRecipient {
+        private final IActiveSessionsListener mListener;
+        private final int mUserId;
+
+        public SessionsListenerRecord(IActiveSessionsListener listener, int userId) {
+            mListener = listener;
+            mUserId = userId;
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mLock) {
+                mSessionsListeners.remove(this);
+            }
+        }
+    }
+
     class SessionManagerImpl extends ISessionManager.Stub {
         private static final String EXTRA_WAKELOCK_ACQUIRED =
                 "android.media.AudioService.WAKELOCK_ACQUIRED";
@@ -648,20 +711,7 @@
             final long token = Binder.clearCallingIdentity();
 
             try {
-                String packageName = null;
-                if (componentName != null) {
-                    // If they gave us a component name verify they own the
-                    // package
-                    packageName = componentName.getPackageName();
-                    enforcePackageName(packageName, uid);
-                }
-                // Check that they can make calls on behalf of the user and
-                // get the final user id
-                int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
-                        true /* allowAll */, true /* requireFull */, "getSessions", packageName);
-                // Check if they have the permissions or their component is
-                // enabled for the user they're calling from.
-                enforceMediaPermissions(componentName, pid, uid, resolvedUserId);
+                int resolvedUserId = verifySessionsRequest(componentName, userId, pid, uid);
                 ArrayList<IBinder> binders = new ArrayList<IBinder>();
                 synchronized (mLock) {
                     ArrayList<MediaSessionRecord> records = mPriorityStack
@@ -677,6 +727,52 @@
             }
         }
 
+        @Override
+        public void addSessionsListener(IActiveSessionsListener listener,
+                ComponentName componentName, int userId) throws RemoteException {
+            final int pid = Binder.getCallingPid();
+            final int uid = Binder.getCallingUid();
+            final long token = Binder.clearCallingIdentity();
+
+            try {
+                int resolvedUserId = verifySessionsRequest(componentName, userId, pid, uid);
+                synchronized (mLock) {
+                    int index = findIndexOfSessionsListenerLocked(listener);
+                    if (index != -1) {
+                        Log.w(TAG, "ActiveSessionsListener is already added, ignoring");
+                        return;
+                    }
+                    SessionsListenerRecord record = new SessionsListenerRecord(listener,
+                            resolvedUserId);
+                    try {
+                        listener.asBinder().linkToDeath(record, 0);
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "ActiveSessionsListener is dead, ignoring it", e);
+                        return;
+                    }
+                    mSessionsListeners.add(record);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public void removeSessionsListener(IActiveSessionsListener listener)
+                throws RemoteException {
+            synchronized (mLock) {
+                int index = findIndexOfSessionsListenerLocked(listener);
+                if (index != -1) {
+                    SessionsListenerRecord record = mSessionsListeners.remove(index);
+                    try {
+                        record.mListener.asBinder().unlinkToDeath(record, 0);
+                    } catch (Exception e) {
+                        // ignore exceptions, the record is being removed
+                    }
+                }
+            }
+        }
+
         /**
          * Handles the dispatching of the media button events to one of the
          * registered listeners, or if there was none, broadcast an
@@ -764,6 +860,25 @@
             }
         }
 
+        private int verifySessionsRequest(ComponentName componentName, int userId, final int pid,
+                final int uid) {
+            String packageName = null;
+            if (componentName != null) {
+                // If they gave us a component name verify they own the
+                // package
+                packageName = componentName.getPackageName();
+                enforcePackageName(packageName, uid);
+            }
+            // Check that they can make calls on behalf of the user and
+            // get the final user id
+            int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
+                    true /* allowAll */, true /* requireFull */, "getSessions", packageName);
+            // Check if they have the permissions or their component is
+            // enabled for the user they're calling from.
+            enforceMediaPermissions(componentName, pid, uid, resolvedUserId);
+            return resolvedUserId;
+        }
+
         private void dispatchAdjustVolumeByLocked(int suggestedStream, int delta, int flags,
                 MediaSessionRecord session) {
             int direction = 0;
@@ -994,4 +1109,20 @@
         };
     }
 
+    final class MessageHandler extends Handler {
+        private static final int MSG_SESSIONS_CHANGED = 1;
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_SESSIONS_CHANGED:
+                    pushSessionsChanged(msg.arg1);
+                    break;
+            }
+        }
+
+        public void post(int what, int arg1, int arg2) {
+            obtainMessage(what, arg1, arg2).sendToTarget();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java
index 803dee2..144ccfa 100644
--- a/services/core/java/com/android/server/media/MediaSessionStack.java
+++ b/services/core/java/com/android/server/media/MediaSessionStack.java
@@ -88,16 +88,19 @@
      * @param record The record that changed.
      * @param oldState Its old playback state.
      * @param newState Its new playback state.
+     * @return true if the priority order was updated, false otherwise.
      */
-    public void onPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
+    public boolean onPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
         if (shouldUpdatePriority(oldState, newState)) {
             mSessions.remove(record);
             mSessions.add(0, record);
             clearCache();
+            return true;
         } else if (newState == PlaybackState.STATE_PAUSED) {
             // Just clear the volume cache in this case
             mCachedVolumeDefault = null;
         }
+        return false;
     }
 
     /**