diff options
| author | RoboErik <epastern@google.com> | 2014-04-30 12:48:25 -0700 |
|---|---|---|
| committer | RoboErik <epastern@google.com> | 2014-05-06 14:06:29 -0700 |
| commit | e7880d8eb1903d42e4e2a90c99b58e2240e01e82 (patch) | |
| tree | 5741d2b360b17a5be6c26d804dde98f55705b01e | |
| parent | cedde01622a6798f5c4526ef1227bd906b6e59ef (diff) | |
| download | frameworks_base-e7880d8eb1903d42e4e2a90c99b58e2240e01e82.zip frameworks_base-e7880d8eb1903d42e4e2a90c99b58e2240e01e82.tar.gz frameworks_base-e7880d8eb1903d42e4e2a90c99b58e2240e01e82.tar.bz2 | |
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
8 files changed, 220 insertions, 16 deletions
diff --git a/api/current.txt b/api/current.txt index a711896..1fe625b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -15492,7 +15492,7 @@ package android.media.session { 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 @@ interface ISession { 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 @@ import android.os.Bundle; */ 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 @@ public class MediaSessionLegacyHelper { 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 @@ import java.util.List; * 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 @@ import java.util.List; 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 @@ public final class Session { } /** + * 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 @@ public final class SessionManager { /** * 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 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { private RouteConnectionRecord mConnection; // TODO define a RouteState class with relevant info private int mRouteState; + private long mFlags; // TransportPerformer fields @@ -148,6 +150,25 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } /** + * 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 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { @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 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } @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 com.android.server.Watchdog.Monitor; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.List; /** * System implementation of MediaSessionManager @@ -57,6 +65,8 @@ public class MediaSessionService extends SystemService implements Monitor { // 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 class MediaSessionService extends SystemService implements Monitor { } } + 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 @@ public class MediaSessionService extends SystemService implements Monitor { private void destroySessionLocked(MediaSessionRecord session) { mSessions.remove(session); + if (session == mPrioritySession) { + mPrioritySession = null; + } } private void enforcePackageName(String packageName, int uid) { @@ -158,8 +182,64 @@ 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: + * <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 @@ public class MediaSessionService extends SystemService implements Monitor { 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 @@ public class MediaSessionService extends SystemService implements Monitor { 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 @@ public class MediaSessionService extends SystemService implements Monitor { 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++) { |
