| /* |
| * 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. |
| } |
| } |
| } |