diff options
Diffstat (limited to 'androidx/media/MediaSessionService2.java')
-rw-r--r-- | androidx/media/MediaSessionService2.java | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/androidx/media/MediaSessionService2.java b/androidx/media/MediaSessionService2.java new file mode 100644 index 00000000..7bad65c5 --- /dev/null +++ b/androidx/media/MediaSessionService2.java @@ -0,0 +1,329 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.support.v4.media.MediaBrowserCompat.MediaItem; + +import androidx.annotation.CallSuper; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.media.MediaBrowserServiceCompat.BrowserRoot; +import androidx.media.MediaSession2.ControllerInfo; +import androidx.media.SessionToken2.TokenType; + +import java.util.List; + +/** + * @hide + * Base class for media session services, which is the service version of the {@link MediaSession2}. + * <p> + * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants + * to keep media playback in the background. + * <p> + * Here's the benefits of using {@link MediaSessionService2} instead of + * {@link MediaSession2}. + * <ul> + * <li>Another app can know that your app supports {@link MediaSession2} even when your app + * isn't running. + * <li>Another app can start playback of your app even when your app isn't running. + * </ul> + * For example, user's voice command can start playback of your app even when it's not running. + * <p> + * To extend this class, adding followings directly to your {@code AndroidManifest.xml}. + * <pre> + * <service android:name="component_name_of_your_implementation" > + * <intent-filter> + * <action android:name="android.media.MediaSessionService2" /> + * </intent-filter> + * </service></pre> + * <p> + * A {@link MediaSessionService2} is another form of {@link MediaSession2}. IDs shouldn't + * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By + * default, an empty string will be used for ID of the service. If you want to specify an ID, + * declare metadata in the manifest as follows. + * <pre> + * <service android:name="component_name_of_your_implementation" > + * <intent-filter> + * <action android:name="android.media.MediaSessionService2" /> + * </intent-filter> + * <meta-data android:name="android.media.session" + * android:value="session_id"/> + * </service></pre> + * <p> + * It's recommended for an app to have a single {@link MediaSessionService2} declared in the + * manifest. Otherwise, your app might be shown twice in the list of the Auto/Wearable, or another + * app fails to pick the right session service when it wants to start the playback this app. + * <p> + * If there's conflicts with the session ID among the services, services wouldn't be available for + * any controllers. + * <p> + * Topic covered here: + * <ol> + * <li><a href="#ServiceLifecycle">Service Lifecycle</a> + * <li><a href="#Permissions">Permissions</a> + * </ol> + * <div class="special reference"> + * <a name="ServiceLifecycle"></a> + * <h3>Service Lifecycle</h3> + * <p> + * Session service is bounded service. When a {@link MediaController2} is created for the + * session service, the controller binds to the session service. {@link #onCreateSession(String)} + * may be called after the {@link #onCreate} if the service hasn't created yet. + * <p> + * After the binding, session's + * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)} + * + * will be called to accept or reject connection request from a controller. If the connection is + * rejected, the controller will unbind. If it's accepted, the controller will be available to use + * and keep binding. + * <p> + * When playback is started for this session service, {@link #onUpdateNotification()} + * is called and service would become a foreground service. It's needed to keep playback after the + * controller is destroyed. The session service becomes background service when the playback is + * stopped. + * <a name="Permissions"></a> + * <h3>Permissions</h3> + * <p> + * Any app can bind to the session service with controller, but the controller can be used only if + * the session service accepted the connection request through + * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}. + */ +@RestrictTo(LIBRARY_GROUP) +public abstract class MediaSessionService2 extends Service { + //private final MediaSessionService2Provider mProvider; + + /** + * This is the interface name that a service implementing a session service should say that it + * support -- that is, this is the action it uses for its intent filter. + */ + public static final String SERVICE_INTERFACE = "android.media.MediaSessionService2"; + + /** + * Name under which a MediaSessionService2 component publishes information about itself. + * This meta-data must provide a string value for the ID. + */ + public static final String SERVICE_META_DATA = "android.media.session"; + + // Stub BrowserRoot for accepting any connction here. + // See MyBrowserService#onGetRoot() for detail. + static final BrowserRoot sDefaultBrowserRoot = new BrowserRoot(SERVICE_INTERFACE, null); + + private final MediaBrowserServiceCompat mBrowserServiceCompat; + + private final Object mLock = new Object(); + @GuardedBy("mLock") + private NotificationManager mNotificationManager; + @GuardedBy("mLock") + private Intent mStartSelfIntent; + @GuardedBy("mLock") + private boolean mIsRunningForeground; + @GuardedBy("mLock") + private MediaSession2 mSession; + + public MediaSessionService2() { + super(); + mBrowserServiceCompat = createBrowserServiceCompat(); + } + + MediaBrowserServiceCompat createBrowserServiceCompat() { + return new MyBrowserService(); + } + + /** + * Default implementation for {@link MediaSessionService2} to initialize session service. + * <p> + * Override this method if you need your own initialization. Derived classes MUST call through + * to the super class's implementation of this method. + */ + @CallSuper + @Override + public void onCreate() { + super.onCreate(); + mBrowserServiceCompat.attachToBaseContext(this); + mBrowserServiceCompat.onCreate(); + SessionToken2 token = new SessionToken2(this, + new ComponentName(getPackageName(), getClass().getName())); + if (token.getType() != getSessionType()) { + throw new RuntimeException("Expected session type " + getSessionType() + + " but was " + token.getType()); + } + MediaSession2 session = onCreateSession(token.getId()); + synchronized (mLock) { + mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + mStartSelfIntent = new Intent(this, getClass()); + mSession = session; + if (mSession == null || !token.getId().equals(mSession.getToken().getId())) { + throw new RuntimeException("Expected session with id " + token.getId() + + ", but got " + mSession); + } + mBrowserServiceCompat.setSessionToken(mSession.getToken().getSessionCompatToken()); + } + } + + @TokenType int getSessionType() { + return SessionToken2.TYPE_SESSION_SERVICE; + } + + /** + * Called when another app requested to start this service to get {@link MediaSession2}. + * <p> + * Session service will accept or reject the connection with the + * {@link MediaSession2.SessionCallback} in the created session. + * <p> + * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the + * expected ID that you've specified through the AndroidManifest.xml. + * <p> + * This method will be called on the main thread. + * + * @param sessionId session id written in the AndroidManifest.xml. + * @return a new session + * @see MediaSession2.Builder + * @see #getSession() + */ + public @NonNull abstract MediaSession2 onCreateSession(String sessionId); + + /** + * Called when the playback state of this session is changed so notification needs update. + * Override this method to show or cancel your own notification UI. + * <p> + * With the notification returned here, the service become foreground service when the playback + * is started. It becomes background service after the playback is stopped. + * + * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown. + */ + public @Nullable MediaNotification onUpdateNotification() { + return null; + } + + /** + * Get instance of the {@link MediaSession2} that you've previously created with the + * {@link #onCreateSession} for this service. + * <p> + * This may be {@code null} before the {@link #onCreate()} is finished. + * + * @return created session + */ + public final @Nullable MediaSession2 getSession() { + synchronized (mLock) { + return mSession; + } + } + + /** + * Default implementation for {@link MediaSessionService2} to handle incoming binding + * request. If the request is for getting the session, the intent will have action + * {@link #SERVICE_INTERFACE}. + * <p> + * Override this method if this service also needs to handle binder requests other than + * {@link #SERVICE_INTERFACE}. Derived classes MUST call through to the super class's + * implementation of this method. + * + * @param intent + * @return Binder + */ + @CallSuper + @Nullable + @Override + public IBinder onBind(Intent intent) { + if (MediaSessionService2.SERVICE_INTERFACE.equals(intent.getAction()) + || MediaBrowserServiceCompat.SERVICE_INTERFACE.equals(intent.getAction())) { + // Change the intent action for browser service. + Intent browserServiceIntent = new Intent(intent); + browserServiceIntent.setAction(MediaSessionService2.SERVICE_INTERFACE); + return mBrowserServiceCompat.onBind(intent); + } + return null; + } + + MediaBrowserServiceCompat getServiceCompat() { + return mBrowserServiceCompat; + } + + /** + * Returned by {@link #onUpdateNotification()} 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 foreground service. 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 id. + * + * @return the notification id + */ + public int getNotificationId() { + return mNotificationId; + } + + /** + * Gets the notification. + * + * @return the notification + */ + public @NonNull Notification getNotification() { + return mNotification; + } + } + + private static class MyBrowserService extends MediaBrowserServiceCompat { + @Override + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + // Returns *stub* root here. Here's the reason. + // 1. A non-null BrowserRoot should be returned here to keep the binding + // 2. MediaSessionService2 is defined as the simplified version of the library + // service with no browsing feature, so shouldn't allow MediaBrowserServiceCompat + // specific operations. + // TODO: Revisit here API not to return stub root here. The fake media ID here may be + // used by the browser service for real. + return sDefaultBrowserRoot; + } + + @Override + public void onLoadChildren(String parentId, Result<List<MediaItem>> result) { + // Disallow loading children. + } + } +} |