Merge changes from topic "unhide_mss"

* changes:
  MediaSession2Service: Unhide
  MediaSession2Service: Add onUpdateNotification()
diff --git a/api/current.txt b/api/current.txt
index 9f39f3a..efd805c 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;