Add APIs for creating a system priority session and getting controllers

This adds a hidden call to set flags and a flag for making a session an
exclusive high priority session. This will cause all media button events
to be sent to that session as long as it is stillr egistered. This
requires the MODIFY_PHONE_STATE permission like the old forCalls API.

This also adds a way to get controllers for all the ongoing sessions.
This is protected by the MEDIA_CONTENT_CONTROL permission like the
old RemoteController APIs.

Change-Id: I51540e8dcf3a7dbe02a0f8ee003821e40af653a3
diff --git a/api/current.txt b/api/current.txt
index a7118960..1fe625b 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -15492,7 +15492,7 @@
 
   public final class SessionManager {
     method public android.media.session.Session createSession(java.lang.String);
-    method public java.util.List<android.media.session.SessionController> getActiveSessions();
+    method public java.util.List<android.media.session.SessionController> getActiveSessions(android.content.ComponentName);
   }
 
   public class SessionToken implements android.os.Parcelable {
diff --git a/media/java/android/media/session/ISession.aidl b/media/java/android/media/session/ISession.aidl
index ca77f04..7cab6b6 100644
--- a/media/java/android/media/session/ISession.aidl
+++ b/media/java/android/media/session/ISession.aidl
@@ -32,6 +32,7 @@
     void sendEvent(String event, in Bundle data);
     ISessionController getController();
     void setTransportPerformerEnabled();
+    void setFlags(long flags);
     void publish();
     void destroy();
 
diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index 84b9a0f..7a8c22e 100644
--- a/media/java/android/media/session/ISessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -15,6 +15,7 @@
 
 package android.media.session;
 
+import android.content.ComponentName;
 import android.media.session.ISession;
 import android.media.session.ISessionCallback;
 import android.os.Bundle;
@@ -25,4 +26,5 @@
  */
 interface ISessionManager {
     ISession createSession(String packageName, in ISessionCallback cb, String tag);
+    List<IBinder> getSessions(in ComponentName compName);
 }
\ No newline at end of file
diff --git a/media/java/android/media/session/MediaSessionLegacyHelper.java b/media/java/android/media/session/MediaSessionLegacyHelper.java
index 4ee67d1..da0100a 100644
--- a/media/java/android/media/session/MediaSessionLegacyHelper.java
+++ b/media/java/android/media/session/MediaSessionLegacyHelper.java
@@ -43,7 +43,8 @@
     private Handler mHandler = new Handler(Looper.getMainLooper());
     // The legacy APIs use PendingIntents to register/unregister media button
     // receivers and these are associated with RCC.
-    private ArrayMap<PendingIntent, SessionHolder> mSessions = new ArrayMap<PendingIntent, SessionHolder>();
+    private ArrayMap<PendingIntent, SessionHolder> mSessions
+            = new ArrayMap<PendingIntent, SessionHolder>();
 
     private MediaSessionLegacyHelper(Context context) {
         mSessionManager = (SessionManager) context
diff --git a/media/java/android/media/session/Session.java b/media/java/android/media/session/Session.java
index 8ccd788..227175d 100644
--- a/media/java/android/media/session/Session.java
+++ b/media/java/android/media/session/Session.java
@@ -45,12 +45,13 @@
  * media to multiple routes or to provide finer grain controls of media.
  * <p>
  * A MediaSession is created by calling
- * {@link SessionManager#createSession(String)}. Once a session is created
- * apps that have the MEDIA_CONTENT_CONTROL permission can interact with the
- * session through {@link SessionManager#getActiveSessions()}. The owner of
- * the session may also use {@link #getSessionToken()} to allow apps without
- * this permission to create a {@link SessionController} to interact with this
- * session.
+ * {@link SessionManager#createSession(String)}. Once a session is created apps
+ * that have the MEDIA_CONTENT_CONTROL permission can interact with the session
+ * through
+ * {@link SessionManager#getActiveSessions(android.content.ComponentName)}. The
+ * owner of the session may also use {@link #getSessionToken()} to allow apps
+ * without this permission to create a {@link SessionController} to interact
+ * with this session.
  * <p>
  * To receive commands, media keys, and other events a Callback must be set with
  * {@link #addCallback(Callback)}.
@@ -63,6 +64,15 @@
 public final class Session {
     private static final String TAG = "Session";
 
+    /**
+     * System only flag for a session that needs to have priority over all other
+     * sessions. This flag ensures this session will receive media button events
+     * regardless of the current ordering in the system.
+     *
+     * @hide
+     */
+    public static final long FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 32;
+
     private static final int MSG_MEDIA_BUTTON = 1;
     private static final int MSG_COMMAND = 2;
     private static final int MSG_ROUTE_CHANGE = 3;
@@ -184,6 +194,24 @@
     }
 
     /**
+     * Set any flags for the session. This cannot be called after calling
+     * {@link #publish()}.
+     *
+     * @param flags The flags to set for this session.
+     * @hide remove hide once we have non-system flags
+     */
+    public void setFlags(long flags) {
+        if (mPublished) {
+            throw new IllegalStateException("setFlags may not be called after publish");
+        }
+        try {
+            mBinder.setFlags(flags);
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Failure in setFlags.", e);
+        }
+    }
+
+    /**
      * Call after you have finished setting up the session. This will make it
      * available to listeners and begin pushing updates to MediaControllers.
      * This can only be called once.
diff --git a/media/java/android/media/session/SessionManager.java b/media/java/android/media/session/SessionManager.java
index 15bf0e3..fd022fc 100644
--- a/media/java/android/media/session/SessionManager.java
+++ b/media/java/android/media/session/SessionManager.java
@@ -16,11 +16,13 @@
 
 package android.media.session;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.media.session.ISessionManager;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.service.notification.NotificationListenerService;
 import android.util.Log;
 
 import java.util.ArrayList;
@@ -79,12 +81,27 @@
     /**
      * Get a list of controllers for all ongoing sessions. This requires the
      * android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by
-     * the calling app.
+     * 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.
      *
-     * @return a list of controllers for ongoing sessions
+     * @param notificationListener The enabled notification listener component.
+     *            May be null.
+     * @return A list of controllers for ongoing sessions
      */
-    public List<SessionController> getActiveSessions() {
-        // TODO
-        return new ArrayList<SessionController>();
+    public List<SessionController> getActiveSessions(ComponentName notificationListener) {
+        ArrayList<SessionController> controllers = new ArrayList<SessionController>();
+        try {
+            List<IBinder> binders = mService.getSessions(notificationListener);
+            for (int i = binders.size() - 1; i >= 0; i--) {
+                SessionController controller = SessionController.fromBinder(ISessionController.Stub
+                        .asInterface(binders.get(i)));
+                controllers.add(controller);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to get active sessions: ", e);
+        }
+        return controllers;
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 3dc17fc..e4e5979 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -17,6 +17,7 @@
 package com.android.server.media;
 
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.media.routeprovider.RouteRequest;
 import android.media.session.ISessionController;
 import android.media.session.ISessionControllerCallback;
@@ -80,6 +81,7 @@
     private RouteConnectionRecord mConnection;
     // TODO define a RouteState class with relevant info
     private int mRouteState;
+    private long mFlags;
 
     // TransportPerformer fields
 
@@ -148,6 +150,25 @@
     }
 
     /**
+     * Get this session's flags.
+     *
+     * @return The flags for this session.
+     */
+    public long getFlags() {
+        return mFlags;
+    }
+
+    /**
+     * Check if this session has system priorty and should receive media buttons
+     * before any other sessions.
+     *
+     * @return True if this is a system priority session, false otherwise
+     */
+    public boolean isSystemPriority() {
+        return (mFlags & Session.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0;
+    }
+
+    /**
      * Set the selected route. This does not connect to the route, just notifies
      * the app that a new route has been selected.
      *
@@ -394,7 +415,8 @@
 
         @Override
         public void publish() {
-            mIsPublished = true; // TODO push update to service
+            mIsPublished = true;
+            mService.publishSession(MediaSessionRecord.this);
         }
         @Override
         public void setTransportPerformerEnabled() {
@@ -402,6 +424,19 @@
         }
 
         @Override
+        public void setFlags(long flags) {
+            if ((flags & Session.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
+                int pid = getCallingPid();
+                int uid = getCallingUid();
+                mService.enforcePhoneStatePermission(pid, uid);
+            }
+            if (mIsPublished) {
+                throw new IllegalStateException("Cannot set flags after publishing session.");
+            }
+            mFlags = flags;
+        }
+
+        @Override
         public void setMetadata(MediaMetadata metadata) {
             mMetadata = metadata;
             mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 107f6ad..3035521 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -17,17 +17,24 @@
 package com.android.server.media;
 
 import android.Manifest;
+import android.app.ActivityManager;
+import android.content.ComponentName;
 import android.content.Context;
 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.RouteInfo;
 import android.media.session.RouteOptions;
 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;
 
@@ -38,6 +45,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * System implementation of MediaSessionManager
@@ -57,6 +65,8 @@
     // TODO do we want a separate thread for handling mediasession messages?
     private final Handler mHandler = new Handler();
 
+    private MediaSessionRecord mPrioritySession;
+
     // Used to keep track of the current request to show routes for a specific
     // session so we drop late callbacks properly.
     private int mShowRoutesRequestId = 0;
@@ -121,6 +131,17 @@
         }
     }
 
+    public void publishSession(MediaSessionRecord record) {
+        synchronized (mLock) {
+            if (record.isSystemPriority()) {
+                if (mPrioritySession != null) {
+                    Log.w(TAG, "Replacing existing priority session with a new session");
+                }
+                mPrioritySession = record;
+            }
+        }
+    }
+
     @Override
     public void monitor() {
         synchronized (mLock) {
@@ -142,6 +163,9 @@
 
     private void destroySessionLocked(MediaSessionRecord session) {
         mSessions.remove(session);
+        if (session == mPrioritySession) {
+            mPrioritySession = null;
+        }
     }
 
     private void enforcePackageName(String packageName, int uid) {
@@ -158,8 +182,64 @@
         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:
+     * <ul>
+     * <li>the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL
+     * permission</li>
+     * <li>the caller's listener is one of the enabled notification listeners</li>
+     * </ul>
+     */
+    private void enforceMediaPermissions(ComponentName compName, int pid, int uid) {
+        if (getContext()
+                .checkPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid)
+                    != PackageManager.PERMISSION_GRANTED
+                && !isEnabledNotificationListener(compName)) {
+            throw new SecurityException("Missing permission to control media.");
+        }
+    }
+
+    private boolean isEnabledNotificationListener(ComponentName compName) {
+        if (compName != null) {
+            final int currentUser = ActivityManager.getCurrentUser();
+            final String enabledNotifListeners = Settings.Secure.getStringForUser(
+                    getContext().getContentResolver(),
+                    Settings.Secure.ENABLED_NOTIFICATION_LISTENERS,
+                    currentUser);
+            if (enabledNotifListeners != null) {
+                final String[] components = enabledNotifListeners.split(":");
+                for (int i = 0; i < components.length; i++) {
+                    final ComponentName component =
+                            ComponentName.unflattenFromString(components[i]);
+                    if (component != null) {
+                        if (compName.equals(component)) {
+                            if (DEBUG) {
+                                Log.d(TAG, "ok to get sessions: " + component +
+                                        " is authorized notification listener");
+                            }
+                            return true;
+                        }
+                    }
+                }
+            }
+            if (DEBUG) {
+                Log.d(TAG, "not ok to get sessions, " + compName +
+                        " is not in list of ENABLED_NOTIFICATION_LISTENERS");
+            }
+        }
+        return false;
+    }
+
     private MediaSessionRecord createSessionInternal(int pid, String packageName,
-            ISessionCallback cb, String tag) {
+            ISessionCallback cb, String tag, boolean forCalls) {
         synchronized (mLock) {
             return createSessionLocked(pid, packageName, cb, tag);
         }
@@ -201,6 +281,11 @@
         return -1;
     }
 
+    private boolean isSessionDiscoverable(MediaSessionRecord record) {
+        // TODO probably want to check more than if it's published.
+        return record.isPublished();
+    }
+
     private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
             = new MediaRouteProviderWatcher.Callback() {
         @Override
@@ -266,7 +351,38 @@
                 if (cb == null) {
                     throw new IllegalArgumentException("Controller callback cannot be null");
                 }
-                return createSessionInternal(pid, packageName, cb, tag).getSessionBinder();
+                return createSessionInternal(pid, packageName, cb, tag, false).getSessionBinder();
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public List<IBinder> getSessions(ComponentName componentName) {
+
+            final int pid = Binder.getCallingPid();
+            final int uid = Binder.getCallingUid();
+            final long token = Binder.clearCallingIdentity();
+
+            try {
+                if (componentName != null) {
+                    // If they gave us a component name verify they own the
+                    // package
+                    enforcePackageName(componentName.getPackageName(), uid);
+                }
+                // Then check if they have the permissions or their component is
+                // allowed
+                enforceMediaPermissions(componentName, pid, uid);
+                ArrayList<IBinder> binders = new ArrayList<IBinder>();
+                synchronized (mLock) {
+                    for (int i = mSessions.size() - 1; i >= 0; i--) {
+                        MediaSessionRecord record = mSessions.get(i);
+                        if (isSessionDiscoverable(record)) {
+                            binders.add(record.getControllerBinder().asBinder());
+                        }
+                    }
+                }
+                return binders;
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
@@ -286,6 +402,10 @@
             pw.println();
 
             synchronized (mLock) {
+                pw.println("Session for calls:" + mPrioritySession);
+                if (mPrioritySession != null) {
+                    mPrioritySession.dump(pw, "");
+                }
                 int count = mSessions.size();
                 pw.println("Sessions - have " + count + " states:");
                 for (int i = 0; i < count; i++) {