Merge "Merge "Add get/set user selected outgoing phone account APIs." am: bf10036306 am: 6e7caec9ae am: 5797bc598b"
diff --git a/api/current.txt b/api/current.txt
index dbd3454..0ff804e6 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -25912,6 +25912,23 @@
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 dd97195..814bc72 100644
--- a/media/java/android/media/MediaController2.java
+++ b/media/java/android/media/MediaController2.java
@@ -71,6 +71,8 @@
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 @@
@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 @@
* 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 3adac72..76ef27a 100644
--- a/media/java/android/media/MediaSession2.java
+++ b/media/java/android/media/MediaSession2.java
@@ -90,6 +90,8 @@
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 void close() {
try {
List<ControllerInfo> controllerInfos;
+ ForegroundServiceEventCallback callback;
synchronized (mLock) {
if (mClosed) {
return;
@@ -126,11 +129,15 @@
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 @@
* @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 @@
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 @@
* 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 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 8fb00fe..5bb746a 100644
--- a/media/java/android/media/MediaSession2Service.java
+++ b/media/java/android/media/MediaSession2Service.java
@@ -19,7 +19,10 @@
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.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 @@
* 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 @@
private static final String TAG = "MediaSession2Service";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- private final Object mLock = new Object();
- @GuardedBy("mLock")
- private Map<String, MediaSession2> mSessions = new ArrayMap<>();
+ 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")
+ 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 @@
@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 @@
@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 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 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 @@
}
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 @@
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 @@
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 d8f74c5..023ee46 100644
--- a/media/java/android/media/Session2Token.java
+++ b/media/java/android/media/Session2Token.java
@@ -48,14 +48,6 @@
* <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 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;