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/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++) {