OSDN Git Service

Add UserRecords to separate user interactions
authorRoboErik <epastern@google.com>
Tue, 13 May 2014 17:13:04 +0000 (10:13 -0700)
committerRoboErik <epastern@google.com>
Wed, 14 May 2014 20:19:48 +0000 (13:19 -0700)
Each user record maintains the list of sessions and providers that
are running under that user. Lifecycle for providers has been modified
to stop discovery when the user is no longer current but keep the
binder connection open so long as there's a session that has selected
a route from that provider. When a user is stopped all providers on
that user will be unbound even if they were still in use.

Change-Id: Iadf1efded3415f7ecf384d3a73513883de9c86b0

api/current.txt
media/java/android/media/session/ISession.aidl
media/java/android/media/session/ISessionCallback.aidl
media/java/android/media/session/Session.java
services/core/java/com/android/server/media/MediaRouteProviderProxy.java
services/core/java/com/android/server/media/MediaRouteProviderWatcher.java
services/core/java/com/android/server/media/MediaSessionRecord.java
services/core/java/com/android/server/media/MediaSessionService.java
services/core/java/com/android/server/media/MediaSessionStack.java

index 17f5e53..30bbc8b 100644 (file)
@@ -15548,7 +15548,7 @@ package android.media.session {
     method public void addCallback(android.media.session.Session.Callback);
     method public void addCallback(android.media.session.Session.Callback, android.os.Handler);
     method public void connect(android.media.session.RouteInfo, android.media.session.RouteOptions);
-    method public void disconnect(android.media.session.RouteInfo);
+    method public void disconnect();
     method public android.media.session.SessionToken getSessionToken();
     method public android.media.session.TransportPerformer getTransportPerformer();
     method public boolean isActive();
@@ -15558,6 +15558,11 @@ package android.media.session {
     method public void setActive(boolean);
     method public void setFlags(int);
     method public void setRouteOptions(java.util.List<android.media.session.RouteOptions>);
+    field public static final int DISCONNECT_REASON_PROVIDER_DISCONNECTED = 2; // 0x2
+    field public static final int DISCONNECT_REASON_ROUTE_CHANGED = 3; // 0x3
+    field public static final int DISCONNECT_REASON_SESSION_DESTROYED = 5; // 0x5
+    field public static final int DISCONNECT_REASON_SESSION_DISCONNECTED = 4; // 0x4
+    field public static final int DISCONNECT_REASON_USER_STOPPING = 1; // 0x1
     field public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1; // 0x1
     field public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 2; // 0x2
   }
index 3ff07d9..096550f 100644 (file)
@@ -40,6 +40,7 @@ interface ISession {
     boolean setRoute(in RouteInfo route);
     void setRouteOptions(in List<RouteOptions> options);
     void connectToRoute(in RouteInfo route, in RouteOptions options);
+    void disconnectFromRoute(in RouteInfo route);
     void sendRouteCommand(in RouteCommand event, in ResultReceiver cb);
 
     // These commands are for the TransportPerformer
index f04cbcc..1552513 100644 (file)
@@ -31,6 +31,7 @@ oneway interface ISessionCallback {
     void onMediaButton(in Intent mediaButtonIntent);
     void onRequestRouteChange(in RouteInfo route);
     void onRouteConnected(in RouteInfo route, in RouteOptions options);
+    void onRouteDisconnected(in RouteInfo route, int reason);
     void onRouteStateChange(int state);
     void onRouteEvent(in RouteEvent event);
 
index 194679e..2ffced6 100644 (file)
@@ -86,10 +86,39 @@ public final class Session {
      */
     public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16;
 
+    /**
+     * Indicates the session was disconnected because the user that the session
+     * belonged to is stopping.
+     */
+    public static final int DISCONNECT_REASON_USER_STOPPING = 1;
+
+    /**
+     * Indicates the session was disconnected because the provider disconnected
+     * the route.
+     */
+    public static final int DISCONNECT_REASON_PROVIDER_DISCONNECTED = 2;
+
+    /**
+     * Indicates the session was disconnected because the route has changed.
+     */
+    public static final int DISCONNECT_REASON_ROUTE_CHANGED = 3;
+
+    /**
+     * Indicates the session was disconnected because the session owner
+     * requested it disconnect.
+     */
+    public static final int DISCONNECT_REASON_SESSION_DISCONNECTED = 4;
+
+    /**
+     * Indicates the session was disconnected because it was destroyed.
+     */
+    public static final int DISCONNECT_REASON_SESSION_DESTROYED = 5;
+
     private static final int MSG_MEDIA_BUTTON = 1;
     private static final int MSG_COMMAND = 2;
     private static final int MSG_ROUTE_CHANGE = 3;
     private static final int MSG_ROUTE_CONNECTED = 4;
+    private static final int MSG_ROUTE_DISCONNECTED = 5;
 
     private static final String KEY_COMMAND = "command";
     private static final String KEY_EXTRAS = "extras";
@@ -302,11 +331,15 @@ public final class Session {
     /**
      * Disconnect from the current route. After calling you will be switched
      * back to the default route.
-     *
-     * @param route The route to disconnect from.
      */
-    public void disconnect(RouteInfo route) {
-        // TODO
+    public void disconnect() {
+        if (mRoute != null) {
+            try {
+                mBinder.disconnectFromRoute(mRoute.getRouteInfo());
+            } catch (RemoteException e) {
+                Log.wtf(TAG, "Error disconnecting from route");
+            }
+        }
     }
 
     /**
@@ -406,6 +439,16 @@ public final class Session {
         }
     }
 
+    private void postRouteDisconnected(RouteInfo route, int reason) {
+        synchronized (mLock) {
+            if (mRoute != null && TextUtils.equals(mRoute.getRouteInfo().getId(), route.getId())) {
+                for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                    mCallbacks.get(i).post(MSG_ROUTE_DISCONNECTED, mRoute, reason);
+                }
+            }
+        }
+    }
+
     /**
      * Receives commands or updates from controllers and routes. An app can
      * specify what commands and buttons it supports by setting them on the
@@ -467,6 +510,11 @@ public final class Session {
          * <p>
          * Valid reasons are:
          * <ul>
+         * <li>{@link #DISCONNECT_REASON_USER_STOPPING}</li>
+         * <li>{@link #DISCONNECT_REASON_PROVIDER_DISCONNECTED}</li>
+         * <li>{@link #DISCONNECT_REASON_ROUTE_CHANGED}</li>
+         * <li>{@link #DISCONNECT_REASON_SESSION_DISCONNECTED}</li>
+         * <li>{@link #DISCONNECT_REASON_SESSION_DESTROYED}</li>
          * </ul>
          *
          * @param route The route that disconnected
@@ -520,6 +568,14 @@ public final class Session {
         }
 
         @Override
+        public void onRouteDisconnected(RouteInfo route, int reason) {
+            Session session = mMediaSession.get();
+            if (session != null) {
+                session.postRouteDisconnected(route, reason);
+            }
+        }
+
+        @Override
         public void onPlay() throws RemoteException {
             Session session = mMediaSession.get();
             if (session != null) {
@@ -668,6 +724,9 @@ public final class Session {
                     case MSG_ROUTE_CONNECTED:
                         mCallback.onRouteConnected((Route) msg.obj);
                         break;
+                    case MSG_ROUTE_DISCONNECTED:
+                        mCallback.onRouteDisconnected((Route) msg.obj, msg.arg1);
+                        break;
                 }
             }
         }
@@ -675,6 +734,10 @@ public final class Session {
         public void post(int what, Object obj) {
             obtainMessage(what, obj).sendToTarget();
         }
+
+        public void post(int what, Object obj, int arg1) {
+            obtainMessage(what, arg1, 0, obj).sendToTarget();
+        }
     }
 
     private static final class Command {
index c4e2058..1c5cacd 100644 (file)
@@ -58,12 +58,16 @@ public class MediaRouteProviderProxy {
     private final int mUserId;
     // Interfaces declared in the manifest
     private final ArrayList<String> mInterfaces = new ArrayList<String>();
-    private final ArrayList<RouteConnectionRecord> mConnections = new ArrayList<RouteConnectionRecord>();
+    private final ArrayList<RouteConnectionRecord> mConnections
+            = new ArrayList<RouteConnectionRecord>();
+    // The sessions that have a route from this provider selected
+    private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
     private final Handler mHandler = new Handler();
 
     private Intent mBindIntent;
     private IRouteProvider mBinder;
     private boolean mRunning;
+    private boolean mPaused;
     private boolean mInterested;
     private boolean mBound;
     private int mRetryCount;
@@ -83,6 +87,12 @@ public class MediaRouteProviderProxy {
         mBindIntent.setComponent(mComponentName);
     }
 
+    public void destroy() {
+        stop();
+        mSessions.clear();
+        updateBinding();
+    }
+
     /**
      * Send any cleanup messages and unbind from the media route provider
      */
@@ -236,6 +246,19 @@ public class MediaRouteProviderProxy {
         return mId;
     }
 
+    public void addSession(MediaSessionRecord session) {
+        mSessions.add(session);
+    }
+
+    public void removeSession(MediaSessionRecord session) {
+        mSessions.remove(session);
+        updateBinding();
+    }
+
+    public int getSessionCount() {
+        return mSessions.size();
+    }
+
     public void dump(PrintWriter pw, String prefix) {
         pw.println(prefix + mId + " " + this);
         String indent = prefix + "  ";
@@ -257,8 +280,10 @@ public class MediaRouteProviderProxy {
         }
     }
 
+    // We want to bind as long as we're interested in this provider or there are
+    // sessions connected to it.
     private boolean shouldBind() {
-        return mRunning && mInterested;
+        return (mRunning && mInterested) || (!mSessions.isEmpty());
     }
 
     private void bind() {
index cf1d95a..734eab9 100644 (file)
@@ -90,6 +90,8 @@ public class MediaRouteProviderWatcher {
         }
     }
 
+    // Stop discovering providers and routes. Providers that still have an
+    // active session connected to them will not unbind.
     public void stop() {
         if (mRunning) {
             mRunning = false;
@@ -97,13 +99,21 @@ public class MediaRouteProviderWatcher {
             mContext.unregisterReceiver(mScanPackagesReceiver);
             mHandler.removeCallbacks(mScanPackagesRunnable);
 
-            // Stop all providers.
+            // Stop all inactive providers.
             for (int i = mProviders.size() - 1; i >= 0; i--) {
                 mProviders.get(i).stop();
             }
         }
     }
 
+    // Clean up the providers forcibly unbinding if necessary
+    public void destroy() {
+        for (int i = mProviders.size() - 1; i >= 0; i--) {
+            mProviders.get(i).destroy();
+            mProviders.remove(i);
+        }
+    }
+
     public ArrayList<MediaRouteProviderProxy> getProviders() {
         return mProviders;
     }
index 41ab626..9677577 100644 (file)
@@ -113,6 +113,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
     // End TransportPerformer fields
 
     private boolean mIsActive = false;
+    private boolean mDestroyed = false;
 
     public MediaSessionRecord(int ownerPid, int ownerUid, int userId, String ownerPackageName,
             ISessionCallback cb, String tag, MediaSessionService service, Handler handler) {
@@ -220,10 +221,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
     public void selectRoute(RouteInfo route) {
         synchronized (mLock) {
             if (route != mRoute) {
-                if (mConnection != null) {
-                    mConnection.disconnect();
-                    mConnection = null;
-                }
+                disconnect(Session.DISCONNECT_REASON_ROUTE_CHANGED);
             }
             mRoute = route;
         }
@@ -261,14 +259,19 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
     public boolean setRouteConnected(RouteInfo route, RouteOptions request,
             RouteConnectionRecord connection) {
         synchronized (mLock) {
+            if (mDestroyed) {
+                Log.i(TAG, "setRouteConnected: session has been destroyed");
+                connection.disconnect();
+                return false;
+            }
             if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
                 Log.w(TAG, "setRouteConnected: connected route is stale");
-                // TODO figure out disconnection path
+                connection.disconnect();
                 return false;
             }
             if (request != mRequest) {
                 Log.w(TAG, "setRouteConnected: connection request is stale");
-                // TODO figure out disconnection path
+                connection.disconnect();
                 return false;
             }
             mConnection = connection;
@@ -284,7 +287,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
      * @return True if the session is active, false otherwise.
      */
     public boolean isActive() {
-        return mIsActive;
+        return mIsActive && !mDestroyed;
     }
 
     /**
@@ -307,6 +310,30 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
         return false;
     }
 
+    /**
+     * @return True if this session is currently connected to a route.
+     */
+    public boolean isConnected() {
+        return mConnection != null;
+    }
+
+    public void disconnect(int reason) {
+        synchronized (mLock) {
+            if (!mDestroyed) {
+                disconnectLocked(reason);
+            }
+        }
+    }
+
+    private void disconnectLocked(int reason) {
+        if (mConnection != null) {
+            mConnection.setListener(null);
+            mConnection.disconnect();
+            mConnection = null;
+            pushDisconnected(reason);
+        }
+    }
+
     public boolean isTransportControlEnabled() {
         return hasFlag(Session.FLAG_HANDLES_TRANSPORT_CONTROLS);
     }
@@ -316,6 +343,28 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
         mService.sessionDied(this);
     }
 
+    /**
+     * Finish cleaning up this session, including disconnecting if connected and
+     * removing the death observer from the callback binder.
+     */
+    public void onDestroy() {
+        synchronized (mLock) {
+            if (mDestroyed) {
+                return;
+            }
+            if (isConnected()) {
+                disconnectLocked(Session.DISCONNECT_REASON_SESSION_DESTROYED);
+            }
+            mRoute = null;
+            mRequest = null;
+            mDestroyed = true;
+        }
+    }
+
+    public ISessionCallback getCallback() {
+        return mSessionCb.mCb;
+    }
+
     public void dump(PrintWriter pw, String prefix) {
         pw.println(prefix + mTag + " " + this);
 
@@ -323,7 +372,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
         pw.println(indent + "ownerPid=" + mOwnerPid + ", ownerUid=" + mOwnerUid
                 + ", userId=" + mUserId);
         pw.println(indent + "info=" + mSessionInfo.toString());
-        pw.println(indent + "published=" + mIsActive);
+        pw.println(indent + "active=" + mIsActive);
         pw.println(indent + "flags=" + mFlags);
         pw.println(indent + "rating type=" + mRatingType);
         pw.println(indent + "controllers: " + mControllerCallbacks.size());
@@ -356,12 +405,17 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
         return "size=" + fields + ", title=" + title;
     }
 
-    private void onDestroy() {
-        mService.destroySession(this);
+    private void pushDisconnected(int reason) {
+        synchronized (mLock) {
+            mSessionCb.sendRouteDisconnected(reason);
+        }
     }
 
     private void pushPlaybackStateUpdate() {
         synchronized (mLock) {
+            if (mDestroyed) {
+                return;
+            }
             for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
                 ISessionControllerCallback cb = mControllerCallbacks.get(i);
                 try {
@@ -376,6 +430,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
 
     private void pushMetadataUpdate() {
         synchronized (mLock) {
+            if (mDestroyed) {
+                return;
+            }
             for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
                 ISessionControllerCallback cb = mControllerCallbacks.get(i);
                 try {
@@ -390,6 +447,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
 
     private void pushRouteUpdate() {
         synchronized (mLock) {
+            if (mDestroyed) {
+                return;
+            }
             for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
                 ISessionControllerCallback cb = mControllerCallbacks.get(i);
                 try {
@@ -404,6 +464,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
 
     private void pushEvent(String event, Bundle data) {
         synchronized (mLock) {
+            if (mDestroyed) {
+                return;
+            }
             for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
                 ISessionControllerCallback cb = mControllerCallbacks.get(i);
                 try {
@@ -417,6 +480,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
 
     private void pushRouteCommand(RouteCommand command, ResultReceiver cb) {
         synchronized (mLock) {
+            if (mDestroyed) {
+                return;
+            }
             if (mRoute == null || !TextUtils.equals(command.getRouteInfo(), mRoute.getId())) {
                 if (cb != null) {
                     cb.send(RouteInterface.RESULT_ROUTE_IS_STALE, null);
@@ -470,14 +536,14 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
 
         @Override
         public void disconnect() {
-            // TODO
+            MediaSessionRecord.this.disconnect(Session.DISCONNECT_REASON_PROVIDER_DISCONNECTED);
         }
     };
 
     private final class SessionStub extends ISession.Stub {
         @Override
         public void destroy() {
-            onDestroy();
+            mService.destroySession(MediaSessionRecord.this);
         }
 
         @Override
@@ -554,6 +620,14 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
         }
 
         @Override
+        public void disconnectFromRoute(RouteInfo route) {
+            if (route != null && mRoute != null
+                    && TextUtils.equals(route.getId(), mRoute.getId())) {
+                disconnect(Session.DISCONNECT_REASON_SESSION_DISCONNECTED);
+            }
+        }
+
+        @Override
         public void setRouteOptions(List<RouteOptions> options) throws RemoteException {
             mRequests.clear();
             for (int i = options.size() - 1; i >= 0; i--) {
@@ -621,6 +695,14 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
             }
         }
 
+        public void sendRouteDisconnected(int reason) {
+            try {
+                mCb.onRouteDisconnected(mRoute, reason);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote failure in sendRouteDisconnected");
+            }
+        }
+
         public void play() {
             try {
                 mCb.onPlay();
index 008f9be..78f3b5f 100644 (file)
@@ -24,20 +24,19 @@ import android.content.pm.PackageManager;
 import android.media.routeprovider.RouteRequest;
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
-import android.media.session.ISessionController;
 import android.media.session.ISessionManager;
-import android.media.session.PlaybackState;
 import android.media.session.RouteInfo;
 import android.media.session.RouteOptions;
+import android.media.session.Session;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.SparseArray;
 
 import com.android.server.SystemService;
 import com.android.server.Watchdog;
@@ -56,16 +55,18 @@ public class MediaSessionService extends SystemService implements Monitor {
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final SessionManagerImpl mSessionManagerImpl;
-    private final MediaRouteProviderWatcher mRouteProviderWatcher;
+    // private final MediaRouteProviderWatcher mRouteProviderWatcher;
     private final MediaSessionStack mPriorityStack;
 
-    private final ArrayList<MediaSessionRecord> mRecords = new ArrayList<MediaSessionRecord>();
-    private final ArrayList<MediaRouteProviderProxy> mProviders
-            = new ArrayList<MediaRouteProviderProxy>();
+    private final ArrayList<MediaSessionRecord> mAllSessions = new ArrayList<MediaSessionRecord>();
+    private final SparseArray<UserRecord> mUserRecords = new SparseArray<UserRecord>();
+    // private final ArrayList<MediaRouteProviderProxy> mProviders
+    // = new ArrayList<MediaRouteProviderProxy>();
     private final Object mLock = new Object();
     private final Handler mHandler = new Handler();
 
     private MediaSessionRecord mPrioritySession;
+    private int mCurrentUserId = -1;
 
     // Used to keep track of the current request to show routes for a specific
     // session so we drop late callbacks properly.
@@ -77,16 +78,14 @@ public class MediaSessionService extends SystemService implements Monitor {
     public MediaSessionService(Context context) {
         super(context);
         mSessionManagerImpl = new SessionManagerImpl();
-        mRouteProviderWatcher = new MediaRouteProviderWatcher(context, mProviderWatcherCallback,
-                mHandler, context.getUserId());
         mPriorityStack = new MediaSessionStack();
     }
 
     @Override
     public void onStart() {
         publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
-        mRouteProviderWatcher.start();
         Watchdog.getInstance().addMonitor(this);
+        updateUser();
     }
 
     /**
@@ -98,16 +97,28 @@ public class MediaSessionService extends SystemService implements Monitor {
     public void showRoutePickerForSession(MediaSessionRecord record) {
         // TODO for now just toggle the route to test (we will only have one
         // match for now)
-        if (record.getRoute() != null) {
-            // For now send null to mean the local route
-            record.selectRoute(null);
-            return;
-        }
-        mShowRoutesRequestId++;
-        ArrayList<MediaRouteProviderProxy> providers = mRouteProviderWatcher.getProviders();
-        for (int i = providers.size() - 1; i >= 0; i--) {
-            MediaRouteProviderProxy provider = providers.get(i);
-            provider.getRoutes(record, mShowRoutesRequestId);
+        synchronized (mLock) {
+            if (!mAllSessions.contains(record)) {
+                Log.d(TAG, "Unknown session tried to show route picker. Ignoring.");
+                return;
+            }
+            RouteInfo current = record.getRoute();
+            UserRecord user = mUserRecords.get(record.getUserId());
+            if (current != null) {
+                // For now send null to mean the local route
+                MediaRouteProviderProxy proxy = user.getProviderLocked(current.getProvider());
+                if (proxy != null) {
+                    proxy.removeSession(record);
+                }
+                record.selectRoute(null);
+                return;
+            }
+            ArrayList<MediaRouteProviderProxy> providers = user.getProvidersLocked();
+            mShowRoutesRequestId++;
+            for (int i = providers.size() - 1; i >= 0; i--) {
+                MediaRouteProviderProxy provider = providers.get(i);
+                provider.getRoutes(record, mShowRoutesRequestId);
+            }
         }
     }
 
@@ -121,19 +132,31 @@ public class MediaSessionService extends SystemService implements Monitor {
     public void connectToRoute(MediaSessionRecord session, RouteInfo route,
             RouteOptions options) {
         synchronized (mLock) {
-            MediaRouteProviderProxy proxy = getProviderLocked(route.getProvider());
+            if (!mAllSessions.contains(session)) {
+                Log.d(TAG, "Unknown session attempting to connect to route. Ignoring");
+                return;
+            }
+            UserRecord user = mUserRecords.get(session.getUserId());
+            if (user == null) {
+                Log.wtf(TAG, "connectToRoute: User " + session.getUserId() + " does not exist.");
+                return;
+            }
+            MediaRouteProviderProxy proxy = user.getProviderLocked(route.getProvider());
             if (proxy == null) {
                 Log.w(TAG, "Provider for route " + route.getName() + " does not exist.");
                 return;
             }
             RouteRequest request = new RouteRequest(session.getSessionInfo(), options, true);
-            // TODO make connect an async call to a ThreadPoolExecutor
             proxy.connectToRoute(session, route, request);
         }
     }
 
     public void updateSession(MediaSessionRecord record) {
         synchronized (mLock) {
+            if (!mAllSessions.contains(record)) {
+                Log.d(TAG, "Unknown session updated. Ignoring.");
+                return;
+            }
             mPriorityStack.onSessionStateChange(record);
             if (record.isSystemPriority()) {
                 if (record.isActive()) {
@@ -152,17 +175,48 @@ public class MediaSessionService extends SystemService implements Monitor {
 
     public void onSessionPlaystateChange(MediaSessionRecord record, int oldState, int newState) {
         synchronized (mLock) {
+            if (!mAllSessions.contains(record)) {
+                Log.d(TAG, "Unknown session changed playback state. Ignoring.");
+                return;
+            }
             mPriorityStack.onPlaystateChange(record, oldState, newState);
         }
     }
 
     @Override
+    public void onStartUser(int userHandle) {
+        updateUser();
+    }
+
+    @Override
+    public void onSwitchUser(int userHandle) {
+        updateUser();
+    }
+
+    @Override
+    public void onStopUser(int userHandle) {
+        synchronized (mLock) {
+            UserRecord user = mUserRecords.get(userHandle);
+            if (user != null) {
+                destroyUserLocked(user);
+            }
+        }
+    }
+
+    @Override
     public void monitor() {
         synchronized (mLock) {
             // Check for deadlock
         }
     }
 
+    protected void enforcePhoneStatePermission(int pid, int uid) {
+        if (getContext().checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, pid, uid)
+                != PackageManager.PERMISSION_GRANTED) {
+            throw new SecurityException("Must hold the MODIFY_PHONE_STATE permission.");
+        }
+    }
+
     void sessionDied(MediaSessionRecord session) {
         synchronized (mLock) {
             destroySessionLocked(session);
@@ -175,12 +229,63 @@ public class MediaSessionService extends SystemService implements Monitor {
         }
     }
 
+    private void updateUser() {
+        synchronized (mLock) {
+            int userId = ActivityManager.getCurrentUser();
+            if (mCurrentUserId != userId) {
+                final int oldUserId = mCurrentUserId;
+                mCurrentUserId = userId; // do this first
+
+                UserRecord oldUser = mUserRecords.get(oldUserId);
+                if (oldUser != null) {
+                    oldUser.stopLocked();
+                }
+
+                UserRecord newUser = getOrCreateUser(userId);
+                newUser.startLocked();
+            }
+        }
+    }
+
+    /**
+     * Stop the user and unbind from everything.
+     *
+     * @param user The user to dispose of
+     */
+    private void destroyUserLocked(UserRecord user) {
+        user.stopLocked();
+        user.destroyLocked();
+        mUserRecords.remove(user.mUserId);
+    }
+
+    /*
+     * When a session is removed several things need to happen.
+     * 1. We need to remove it from the relevant user.
+     * 2. We need to remove it from the priority stack.
+     * 3. We need to remove it from all sessions.
+     * 4. If this is the system priority session we need to clear it.
+     * 5. We need to unlink to death from the cb binder
+     * 6. We need to tell the session to do any final cleanup (onDestroy)
+     */
     private void destroySessionLocked(MediaSessionRecord session) {
-        mRecords.remove(session);
+        int userId = session.getUserId();
+        UserRecord user = mUserRecords.get(userId);
+        if (user != null) {
+            user.removeSessionLocked(session);
+        }
+
         mPriorityStack.removeSession(session);
+        mAllSessions.remove(session);
         if (session == mPrioritySession) {
             mPrioritySession = null;
         }
+
+        try {
+            session.getCallback().asBinder().unlinkToDeath(session, 0);
+        } catch (Exception e) {
+            // ignore exceptions while destroying a session.
+        }
+        session.onDestroy();
     }
 
     private void enforcePackageName(String packageName, int uid) {
@@ -197,13 +302,6 @@ public class MediaSessionService extends SystemService implements Monitor {
         throw new IllegalArgumentException("packageName is not owned by the calling process");
     }
 
-    protected void enforcePhoneStatePermission(int pid, int uid) {
-        if (getContext().checkPermission(android.Manifest.permission.MODIFY_PHONE_STATE, pid, uid)
-                != PackageManager.PERMISSION_GRANTED) {
-            throw new SecurityException("Must hold the MODIFY_PHONE_STATE permission.");
-        }
-    }
-
     /**
      * Checks a caller's authorization to register an IRemoteControlDisplay.
      * Authorization is granted if one of the following is true:
@@ -271,14 +369,22 @@ public class MediaSessionService extends SystemService implements Monitor {
     }
 
     private MediaSessionRecord createSessionInternal(int callerPid, int callerUid, int userId,
-            String callerPackageName, ISessionCallback cb, String tag) {
+            String callerPackageName, ISessionCallback cb, String tag) throws RemoteException {
         synchronized (mLock) {
             return createSessionLocked(callerPid, callerUid, userId, callerPackageName, cb, tag);
         }
     }
 
+    /*
+     * When a session is created the following things need to happen.
+     * 1. It's callback binder needs a link to death
+     * 2. It needs to be added to all sessions.
+     * 3. It needs to be added to the priority stack.
+     * 4. It needs to be added to the relevant user record.
+     */
     private MediaSessionRecord createSessionLocked(int callerPid, int callerUid, int userId,
             String callerPackageName, ISessionCallback cb, String tag) {
+
         final MediaSessionRecord session = new MediaSessionRecord(callerPid, callerUid, userId,
                 callerPackageName, cb, tag, this, mHandler);
         try {
@@ -286,17 +392,31 @@ public class MediaSessionService extends SystemService implements Monitor {
         } catch (RemoteException e) {
             throw new RuntimeException("Media Session owner died prematurely.", e);
         }
-        mRecords.add(session);
+
+        mAllSessions.add(session);
         mPriorityStack.addSession(session);
+
+        UserRecord user = getOrCreateUser(userId);
+        user.addSessionLocked(session);
+
         if (DEBUG) {
             Log.d(TAG, "Created session for package " + callerPackageName + " with tag " + tag);
         }
         return session;
     }
 
+    private UserRecord getOrCreateUser(int userId) {
+        UserRecord user = mUserRecords.get(userId);
+        if (user == null) {
+            user = new UserRecord(getContext(), userId);
+            mUserRecords.put(userId, user);
+        }
+        return user;
+    }
+
     private int findIndexOfSessionForIdLocked(String sessionId) {
-        for (int i = mRecords.size() - 1; i >= 0; i--) {
-            MediaSessionRecord session = mRecords.get(i);
+        for (int i = mAllSessions.size() - 1; i >= 0; i--) {
+            MediaSessionRecord session = mAllSessions.get(i);
             if (TextUtils.equals(session.getSessionInfo().getId(), sessionId)) {
                 return i;
             }
@@ -304,42 +424,11 @@ public class MediaSessionService extends SystemService implements Monitor {
         return -1;
     }
 
-    private MediaRouteProviderProxy getProviderLocked(String providerId) {
-        for (int i = mProviders.size() - 1; i >= 0; i--) {
-            MediaRouteProviderProxy provider = mProviders.get(i);
-            if (TextUtils.equals(providerId, provider.getId())) {
-                return provider;
-            }
-        }
-        return null;
-    }
-
     private boolean isSessionDiscoverable(MediaSessionRecord record) {
-        // TODO probably want to check more than if it's published.
+        // TODO probably want to check more than if it's active.
         return record.isActive();
     }
 
-    private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
-            = new MediaRouteProviderWatcher.Callback() {
-        @Override
-        public void removeProvider(MediaRouteProviderProxy provider) {
-            synchronized (mLock) {
-                mProviders.remove(provider);
-                provider.setRoutesListener(null);
-                provider.setInterested(false);
-            }
-        }
-
-        @Override
-        public void addProvider(MediaRouteProviderProxy provider) {
-            synchronized (mLock) {
-                mProviders.add(provider);
-                provider.setRoutesListener(mRoutesCallback);
-                provider.setInterested(true);
-            }
-        }
-    };
-
     private MediaRouteProviderProxy.RoutesListener mRoutesCallback
             = new MediaRouteProviderProxy.RoutesListener() {
         @Override
@@ -350,8 +439,12 @@ public class MediaSessionService extends SystemService implements Monitor {
             synchronized (mLock) {
                 int index = findIndexOfSessionForIdLocked(sessionId);
                 if (index != -1 && routes != null && routes.size() > 0) {
-                    MediaSessionRecord record = mRecords.get(index);
-                    record.selectRoute(routes.get(0));
+                    MediaSessionRecord record = mAllSessions.get(index);
+                    RouteInfo route = routes.get(0);
+                    record.selectRoute(route);
+                    UserRecord user = mUserRecords.get(record.getUserId());
+                    MediaRouteProviderProxy provider = user.getProviderLocked(route.getProvider());
+                    provider.addSession(record);
                 }
             }
         }
@@ -362,13 +455,135 @@ public class MediaSessionService extends SystemService implements Monitor {
             synchronized (mLock) {
                 int index = findIndexOfSessionForIdLocked(sessionId);
                 if (index != -1) {
-                    MediaSessionRecord session = mRecords.get(index);
+                    MediaSessionRecord session = mAllSessions.get(index);
                     session.setRouteConnected(route, options.getConnectionOptions(), connection);
                 }
             }
         }
     };
 
+    /**
+     * Information about a particular user. The contents of this object is
+     * guarded by mLock.
+     */
+    final class UserRecord {
+        private final int mUserId;
+        private final MediaRouteProviderWatcher mRouteProviderWatcher;
+        private final ArrayList<MediaRouteProviderProxy> mProviders
+                = new ArrayList<MediaRouteProviderProxy>();
+        private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
+
+        public UserRecord(Context context, int userId) {
+            mUserId = userId;
+            mRouteProviderWatcher = new MediaRouteProviderWatcher(context,
+                    mProviderWatcherCallback, mHandler, userId);
+        }
+
+        public void startLocked() {
+            mRouteProviderWatcher.start();
+        }
+
+        public void stopLocked() {
+            mRouteProviderWatcher.stop();
+            updateInterestLocked();
+        }
+
+        public void destroyLocked() {
+            for (int i = mSessions.size() - 1; i >= 0; i--) {
+                MediaSessionRecord session = mSessions.get(i);
+                MediaSessionService.this.destroySessionLocked(session);
+                if (session.isConnected()) {
+                    session.disconnect(Session.DISCONNECT_REASON_USER_STOPPING);
+                }
+            }
+        }
+
+        public ArrayList<MediaRouteProviderProxy> getProvidersLocked() {
+            return mProviders;
+        }
+
+        public ArrayList<MediaSessionRecord> getSessionsLocked() {
+            return mSessions;
+        }
+
+        public void addSessionLocked(MediaSessionRecord session) {
+            mSessions.add(session);
+            updateInterestLocked();
+        }
+
+        public void removeSessionLocked(MediaSessionRecord session) {
+            mSessions.remove(session);
+            RouteInfo route = session.getRoute();
+            if (route != null) {
+                MediaRouteProviderProxy provider = getProviderLocked(route.getProvider());
+                if (provider != null) {
+                    provider.removeSession(session);
+                }
+            }
+            updateInterestLocked();
+        }
+
+        public void dumpLocked(PrintWriter pw, String prefix) {
+            pw.println(prefix + "Record for user " + mUserId);
+            String indent = prefix + "  ";
+            int size = mProviders.size();
+            pw.println(indent + size + " Providers:");
+            for (int i = 0; i < size; i++) {
+                mProviders.get(i).dump(pw, indent);
+            }
+            pw.println();
+            size = mSessions.size();
+            pw.println(indent + size + " Sessions:");
+            for (int i = 0; i < size; i++) {
+                // Just print the session info, the full session dump will
+                // already be in the list of all sessions.
+                pw.println(indent + mSessions.get(i).getSessionInfo());
+            }
+        }
+
+        public void updateInterestLocked() {
+            // TODO go through the sessions and build up the set of interfaces
+            // we're interested in. Update the provider watcher.
+            // For now, just express interest in all providers for the current
+            // user
+            boolean interested = mUserId == mCurrentUserId;
+            for (int i = mProviders.size() - 1; i >= 0; i--) {
+                mProviders.get(i).setInterested(interested);
+            }
+        }
+
+        private MediaRouteProviderProxy getProviderLocked(String providerId) {
+            for (int i = mProviders.size() - 1; i >= 0; i--) {
+                MediaRouteProviderProxy provider = mProviders.get(i);
+                if (TextUtils.equals(providerId, provider.getId())) {
+                    return provider;
+                }
+            }
+            return null;
+        }
+
+        private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
+                = new MediaRouteProviderWatcher.Callback() {
+            @Override
+            public void removeProvider(MediaRouteProviderProxy provider) {
+                synchronized (mLock) {
+                    mProviders.remove(provider);
+                    provider.setRoutesListener(null);
+                    provider.setInterested(false);
+                }
+            }
+
+            @Override
+            public void addProvider(MediaRouteProviderProxy provider) {
+                synchronized (mLock) {
+                    mProviders.add(provider);
+                    provider.setRoutesListener(mRoutesCallback);
+                    provider.setInterested(true);
+                }
+            }
+        };
+    }
+
     class SessionManagerImpl extends ISessionManager.Stub {
         // TODO add createSessionAsUser, pass user-id to
         // ActivityManagerNative.handleIncomingUser and stash result for use
@@ -447,19 +662,19 @@ public class MediaSessionService extends SystemService implements Monitor {
                 if (mPrioritySession != null) {
                     mPrioritySession.dump(pw, "");
                 }
-                int count = mRecords.size();
+                int count = mAllSessions.size();
                 pw.println(count + " Sessions:");
                 for (int i = 0; i < count; i++) {
-                    mRecords.get(i).dump(pw, "");
+                    mAllSessions.get(i).dump(pw, "");
                     pw.println();
                 }
                 mPriorityStack.dump(pw, "");
 
-                pw.println("Providers:");
-                count = mProviders.size();
+                pw.println("User Records:");
+                count = mUserRecords.size();
                 for (int i = 0; i < count; i++) {
-                    MediaRouteProviderProxy provider = mProviders.get(i);
-                    provider.dump(pw, "");
+                    UserRecord user = mUserRecords.get(i);
+                    user.dumpLocked(pw, "");
                 }
             }
         }
index 1e1818d..f89b14a 100644 (file)
@@ -19,7 +19,6 @@ package com.android.server.media;
 import android.media.session.PlaybackState;
 import android.media.session.Session;
 import android.os.UserHandle;
-import android.text.TextUtils;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -49,6 +48,8 @@ public class MediaSessionStack {
 
     private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>();
 
+    private MediaSessionRecord mGlobalPrioritySession;
+
     private MediaSessionRecord mCachedButtonReceiver;
     private MediaSessionRecord mCachedDefault;
     private ArrayList<MediaSessionRecord> mCachedActiveList;
@@ -61,6 +62,9 @@ public class MediaSessionStack {
      */
     public void addSession(MediaSessionRecord record) {
         mSessions.add(record);
+        if ((record.getFlags() & Session.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
+            mGlobalPrioritySession = record;
+        }
         clearCache();
     }
 
@@ -71,6 +75,9 @@ public class MediaSessionStack {
      */
     public void removeSession(MediaSessionRecord record) {
         mSessions.remove(record);
+        if (record == mGlobalPrioritySession) {
+            mGlobalPrioritySession = null;
+        }
         clearCache();
     }
 
@@ -156,6 +163,9 @@ public class MediaSessionStack {
      * @return The default media button session or null.
      */
     public MediaSessionRecord getDefaultMediaButtonSession(int userId) {
+        if (mGlobalPrioritySession != null && mGlobalPrioritySession.isActive()) {
+            return mGlobalPrioritySession;
+        }
         if (mCachedButtonReceiver != null) {
             return mCachedButtonReceiver;
         }