diff options
| author | 2019-01-23 04:40:21 +0000 | |
|---|---|---|
| committer | 2019-01-23 04:40:21 +0000 | |
| commit | ca626b63195bf5dc196069a9d5b307911b33517b (patch) | |
| tree | 3413f4eb81cbe6b854b0fe8d7c4664b329471d06 | |
| parent | f1cef973457809c9f8563c6119006dbe1e1d87ce (diff) | |
| parent | 70c8cb1dce2f9d331e99469d35b685d85b95dc25 (diff) | |
Merge changes from topic "unhide_mss"
* changes:
MediaSession2Service: Unhide
MediaSession2Service: Add onUpdateNotification()
| -rw-r--r-- | api/current.txt | 17 | ||||
| -rw-r--r-- | media/java/android/media/MediaController2.java | 11 | ||||
| -rw-r--r-- | media/java/android/media/MediaSession2.java | 45 | ||||
| -rw-r--r-- | media/java/android/media/MediaSession2Service.java | 171 | ||||
| -rw-r--r-- | media/java/android/media/Session2Token.java | 13 |
5 files changed, 202 insertions, 55 deletions
diff --git a/api/current.txt b/api/current.txt index 9f39f3af1fec..efd805cf21e9 100644 --- a/api/current.txt +++ b/api/current.txt @@ -25912,6 +25912,23 @@ package android.media { method @Nullable public android.media.Session2Command.Result onSessionCommand(@NonNull android.media.MediaSession2, @NonNull android.media.MediaSession2.ControllerInfo, @NonNull android.media.Session2Command, @Nullable android.os.Bundle); } + public abstract class MediaSession2Service extends android.app.Service { + ctor public MediaSession2Service(); + method public final void addSession(@NonNull android.media.MediaSession2); + method @NonNull public final java.util.List<android.media.MediaSession2> getSessions(); + method @CallSuper @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent); + method @NonNull public abstract android.media.MediaSession2 onGetPrimarySession(); + method @Nullable public abstract android.media.MediaSession2Service.MediaNotification onUpdateNotification(@NonNull android.media.MediaSession2); + method public final void removeSession(@NonNull android.media.MediaSession2); + field public static final String SERVICE_INTERFACE = "android.media.MediaSession2Service"; + } + + public static class MediaSession2Service.MediaNotification { + ctor public MediaSession2Service.MediaNotification(int, @NonNull android.app.Notification); + method @NonNull public android.app.Notification getNotification(); + method public int getNotificationId(); + } + public final class MediaSync { ctor public MediaSync(); method @NonNull public android.view.Surface createInputSurface(); diff --git a/media/java/android/media/MediaController2.java b/media/java/android/media/MediaController2.java index dd971959dd25..814bc72a2a78 100644 --- a/media/java/android/media/MediaController2.java +++ b/media/java/android/media/MediaController2.java @@ -71,6 +71,8 @@ public class MediaController2 implements AutoCloseable { private final Object mLock = new Object(); //@GuardedBy("mLock") + private boolean mClosed; + //@GuardedBy("mLock") private int mNextSeqNumber; //@GuardedBy("mLock") private Session2Link mSessionBinder; @@ -141,7 +143,14 @@ public class MediaController2 implements AutoCloseable { @Override public void close() { synchronized (mLock) { + if (mClosed) { + // Already closed. Ignore rest of clean up code. + // Note: unbindService() throws IllegalArgumentException when it's called twice. + return; + } + mClosed = true; if (mServiceConnection != null) { + // Note: This should be called even when the bindService() has returned false. mContext.unbindService(mServiceConnection); } if (mSessionBinder != null) { @@ -167,7 +176,7 @@ public class MediaController2 implements AutoCloseable { * If it is not connected yet, it returns {@code null}. * <p> * This may differ with the {@link Session2Token} from the constructor. For example, if the - * controller is created with the token for MediaSession2Service, this would return + * controller is created with the token for {@link MediaSession2Service}, this would return * token for the {@link MediaSession2} in the service. * * @return Session2Token of the connected session, or {@code null} if not connected diff --git a/media/java/android/media/MediaSession2.java b/media/java/android/media/MediaSession2.java index 3adac7295fff..76ef27a40aad 100644 --- a/media/java/android/media/MediaSession2.java +++ b/media/java/android/media/MediaSession2.java @@ -90,6 +90,8 @@ public class MediaSession2 implements AutoCloseable { private boolean mClosed; //@GuardedBy("mLock") private boolean mPlaybackActive; + //@GuardedBy("mLock") + private ForegroundServiceEventCallback mForegroundServiceEventCallback; MediaSession2(@NonNull Context context, @NonNull String id, PendingIntent sessionActivity, @NonNull Executor callbackExecutor, @NonNull SessionCallback callback) { @@ -119,6 +121,7 @@ public class MediaSession2 implements AutoCloseable { public void close() { try { List<ControllerInfo> controllerInfos; + ForegroundServiceEventCallback callback; synchronized (mLock) { if (mClosed) { return; @@ -126,11 +129,15 @@ public class MediaSession2 implements AutoCloseable { mClosed = true; controllerInfos = getConnectedControllers(); mConnectedControllers.clear(); - mCallback.onSessionClosed(this); + callback = mForegroundServiceEventCallback; + mForegroundServiceEventCallback = null; } synchronized (MediaSession2.class) { SESSION_ID_LIST.remove(mSessionId); } + if (callback != null) { + callback.onSessionClosed(this); + } for (ControllerInfo info : controllerInfos) { info.notifyDisconnected(); } @@ -224,11 +231,16 @@ public class MediaSession2 implements AutoCloseable { * @param playbackActive {@code true} if the playback active, {@code false} otherwise. **/ public void setPlaybackActive(boolean playbackActive) { + final ForegroundServiceEventCallback serviceCallback; synchronized (mLock) { if (mPlaybackActive == playbackActive) { return; } mPlaybackActive = playbackActive; + serviceCallback = mForegroundServiceEventCallback; + } + if (serviceCallback != null) { + serviceCallback.onPlaybackActiveChanged(this, playbackActive); } List<ControllerInfo> controllerInfos = getConnectedControllers(); for (ControllerInfo controller : controllerInfos) { @@ -257,6 +269,18 @@ public class MediaSession2 implements AutoCloseable { return mCallback; } + void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) { + synchronized (mLock) { + if (mForegroundServiceEventCallback == callback) { + return; + } + if (mForegroundServiceEventCallback != null && callback != null) { + throw new IllegalStateException("A session cannot be added to multiple services"); + } + mForegroundServiceEventCallback = callback; + } + } + // Called by Session2Link.onConnect and MediaSession2Service.MediaSession2ServiceStub.connect void onConnect(final Controller2Link controller, int callingPid, int callingUid, int seq, Bundle connectionRequest) { @@ -695,8 +719,6 @@ public class MediaSession2 implements AutoCloseable { * This API is not generally intended for third party application developers. */ public abstract static class SessionCallback { - ForegroundServiceEventCallback mForegroundServiceEventCallback; - /** * Called when a controller is created for this session. Return allowed commands for * controller. By default it returns {@code null}. @@ -753,19 +775,10 @@ public class MediaSession2 implements AutoCloseable { public void onCommandResult(@NonNull MediaSession2 session, @NonNull ControllerInfo controller, @NonNull Object token, @NonNull Session2Command command, @NonNull Session2Command.Result result) {} + } - final void onSessionClosed(MediaSession2 session) { - if (mForegroundServiceEventCallback != null) { - mForegroundServiceEventCallback.onSessionClosed(session); - } - } - - void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) { - mForegroundServiceEventCallback = callback; - } - - abstract static class ForegroundServiceEventCallback { - public void onSessionClosed(MediaSession2 session) {} - } + abstract static class ForegroundServiceEventCallback { + public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) {} + public void onSessionClosed(MediaSession2 session) {} } } diff --git a/media/java/android/media/MediaSession2Service.java b/media/java/android/media/MediaSession2Service.java index 8fb00fe487e8..5bb746a7f9e3 100644 --- a/media/java/android/media/MediaSession2Service.java +++ b/media/java/android/media/MediaSession2Service.java @@ -19,7 +19,10 @@ package android.media; import android.annotation.CallSuper; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.Notification; +import android.app.NotificationManager; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.Bundle; @@ -28,8 +31,6 @@ import android.os.IBinder; import android.util.ArrayMap; import android.util.Log; -import com.android.internal.annotations.GuardedBy; - import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; @@ -42,11 +43,7 @@ import java.util.Map; * Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a> * <a href="{@docRoot}reference/androidx/media2/package-summary.html">Media2 Library</a> * for consistent behavior across all devices. - * @hide */ -// TODO: Unhide -// TODO: Add onUpdateNotification(), and calls it to get Notification for startForegroundService() -// when a session's player state becomes playing. public abstract class MediaSession2Service extends Service { /** * The {@link Intent} that must be declared as handled by the service. @@ -56,10 +53,29 @@ public abstract class MediaSession2Service extends Service { private static final String TAG = "MediaSession2Service"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final MediaSession2.ForegroundServiceEventCallback mForegroundServiceEventCallback = + new MediaSession2.ForegroundServiceEventCallback() { + @Override + public void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) { + MediaSession2Service.this.onPlaybackActiveChanged(session, playbackActive); + } + + @Override + public void onSessionClosed(MediaSession2 session) { + removeSession(session); + } + }; + private final Object mLock = new Object(); - @GuardedBy("mLock") + //@GuardedBy("mLock") + private NotificationManager mNotificationManager; + //@GuardedBy("mLock") + private Intent mStartSelfIntent; + //@GuardedBy("mLock") private Map<String, MediaSession2> mSessions = new ArrayMap<>(); - + //@GuardedBy("mLock") + private Map<MediaSession2, MediaNotification> mNotifications = new ArrayMap<>(); + //@GuardedBy("mLock") private MediaSession2ServiceStub mStub; /** @@ -72,7 +88,12 @@ public abstract class MediaSession2Service extends Service { @Override public void onCreate() { super.onCreate(); - mStub = new MediaSession2ServiceStub(this); + synchronized (mLock) { + mStub = new MediaSession2ServiceStub(this); + mStartSelfIntent = new Intent(this, this.getClass()); + mNotificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + } } @CallSuper @@ -80,18 +101,13 @@ public abstract class MediaSession2Service extends Service { @Nullable public IBinder onBind(@NonNull Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction())) { - return mStub; + synchronized (mLock) { + return mStub; + } } return null; } - @CallSuper - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - // TODO: Dispatch media key events to the primary session. - return START_STICKY; - } - /** * Called by the system to notify that it is no longer used and is being removed. Do not call * this method directly. @@ -104,10 +120,12 @@ public abstract class MediaSession2Service extends Service { public void onDestroy() { super.onDestroy(); synchronized (mLock) { - for (MediaSession2 session : mSessions.values()) { - session.getCallback().setForegroundServiceEventCallback(null); + List<MediaSession2> sessions = getSessions(); + for (MediaSession2 session : sessions) { + removeSession(session); } mSessions.clear(); + mNotifications.clear(); } mStub.close(); } @@ -144,6 +162,24 @@ public abstract class MediaSession2Service extends Service { public abstract MediaSession2 onGetPrimarySession(); /** + * Called when notification UI needs update. Override this method to show or cancel your own + * notification UI. + * <p> + * This would be called on {@link MediaSession2}'s callback executor when playback state is + * changed. + * <p> + * With the notification returned here, the service becomes foreground service when the playback + * is started. Apps must request the permission + * {@link android.Manifest.permission#FOREGROUND_SERVICE} in order to use this API. It becomes + * background service after the playback is stopped. + * + * @param session a session that needs notification update. + * @return a {@link MediaNotification}. Can be {@code null}. + */ + @Nullable + public abstract MediaNotification onUpdateNotification(@NonNull MediaSession2 session); + + /** * Adds a session to this service. * <p> * Added session will be removed automatically when it's closed, or removed when @@ -161,21 +197,15 @@ public abstract class MediaSession2Service extends Service { } synchronized (mLock) { MediaSession2 previousSession = mSessions.get(session.getSessionId()); - if (previousSession != session) { - if (previousSession != null) { + if (previousSession != null) { + if (previousSession != session) { Log.w(TAG, "Session ID should be unique, ID=" + session.getSessionId() + ", previous=" + previousSession + ", session=" + session); } return; } mSessions.put(session.getSessionId(), session); - session.getCallback().setForegroundServiceEventCallback( - new MediaSession2.SessionCallback.ForegroundServiceEventCallback() { - @Override - public void onSessionClosed(MediaSession2 session) { - removeSession(session); - } - }); + session.setForegroundServiceEventCallback(mForegroundServiceEventCallback); } } @@ -189,8 +219,21 @@ public abstract class MediaSession2Service extends Service { if (session == null) { throw new IllegalArgumentException("session shouldn't be null"); } + MediaNotification notification; synchronized (mLock) { + if (mSessions.get(session.getSessionId()) != session) { + // Session isn't added or removed already. + return; + } mSessions.remove(session.getSessionId()); + notification = mNotifications.remove(session); + } + session.setForegroundServiceEventCallback(null); + if (notification != null) { + mNotificationManager.cancel(notification.getNotificationId()); + } + if (getSessions().isEmpty()) { + stopForeground(false); } } @@ -207,6 +250,78 @@ public abstract class MediaSession2Service extends Service { return list; } + /** + * Called by registered {@link MediaSession2.ForegroundServiceEventCallback} + * + * @param session session with change + * @param playbackActive {@code true} if playback is active. + */ + void onPlaybackActiveChanged(MediaSession2 session, boolean playbackActive) { + MediaNotification mediaNotification = onUpdateNotification(session); + if (mediaNotification == null) { + // The service implementation doesn't want to use the automatic start/stopForeground + // feature. + return; + } + synchronized (mLock) { + mNotifications.put(session, mediaNotification); + } + int id = mediaNotification.getNotificationId(); + Notification notification = mediaNotification.getNotification(); + if (!playbackActive) { + mNotificationManager.notify(id, notification); + return; + } + // playbackActive == true + startForegroundService(mStartSelfIntent); + startForeground(id, notification); + } + + /** + * Returned by {@link #onUpdateNotification(MediaSession2)} for making session service + * foreground service to keep playback running in the background. It's highly recommended to + * show media style notification here. + */ + public static class MediaNotification { + private final int mNotificationId; + private final Notification mNotification; + + /** + * Default constructor + * + * @param notificationId notification id to be used for + * {@link NotificationManager#notify(int, Notification)}. + * @param notification a notification to make session service run in the foreground. Media + * style notification is recommended here. + */ + public MediaNotification(int notificationId, @NonNull Notification notification) { + if (notification == null) { + throw new IllegalArgumentException("notification shouldn't be null"); + } + mNotificationId = notificationId; + mNotification = notification; + } + + /** + * Gets the id of the notification. + * + * @return the notification id + */ + public int getNotificationId() { + return mNotificationId; + } + + /** + * Gets the notification. + * + * @return the notification + */ + @NonNull + public Notification getNotification() { + return mNotification; + } + } + private static final class MediaSession2ServiceStub extends IMediaSession2Service.Stub implements AutoCloseable { final WeakReference<MediaSession2Service> mService; diff --git a/media/java/android/media/Session2Token.java b/media/java/android/media/Session2Token.java index d8f74c518bf4..023ee4659b6c 100644 --- a/media/java/android/media/Session2Token.java +++ b/media/java/android/media/Session2Token.java @@ -48,14 +48,6 @@ import java.util.Objects; * <p> * It can be also obtained by {@link android.media.session.MediaSessionManager}. */ -// New version of MediaSession2.Token for following reasons -// - Stop implementing Parcelable for updatable support -// - Represent session and library service (formerly browser service) in one class. -// Previously MediaSession2.Token was for session and ComponentName was for service. -// This helps controller apps to keep target of dispatching media key events in uniform way. -// For details about the reason, see following. (Android O+) -// android.media.session.MediaSessionManager.Callback#onAddressedPlayerChanged -// TODO: use @link for MediaSession2Service public final class Session2Token implements Parcelable { private static final String TAG = "Session2Token"; @@ -85,12 +77,13 @@ public final class Session2Token implements Parcelable { public static final int TYPE_SESSION = 0; /** - * Type for MediaSession2Service. + * Type for {@link MediaSession2Service}. */ public static final int TYPE_SESSION_SERVICE = 1; private final int mUid; - private final @TokenType int mType; + @TokenType + private final int mType; private final String mPackageName; private final String mServiceName; private final Session2Link mSessionLink; |