diff options
| -rw-r--r-- | media/java/android/media/MediaController2.java | 9 | ||||
| -rw-r--r-- | media/java/android/media/MediaSession2.java | 45 | ||||
| -rw-r--r-- | media/java/android/media/MediaSession2Service.java | 162 |
3 files changed, 181 insertions, 35 deletions
diff --git a/media/java/android/media/MediaController2.java b/media/java/android/media/MediaController2.java index dd971959dd25..039f3601cc3d 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) { 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..a29b83dc2a09 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; @@ -45,8 +46,6 @@ import java.util.Map; * @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 +55,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 +90,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,7 +103,9 @@ 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; } @@ -104,10 +129,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 +171,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 +206,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 +228,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 +259,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; |