blob: 7bad65c5a141d584779229590177ac723dac7fe4 [file] [log] [blame]
/*
* 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>
* &lt;service android:name="component_name_of_your_implementation" &gt;
* &lt;intent-filter&gt;
* &lt;action android:name="android.media.MediaSessionService2" /&gt;
* &lt;/intent-filter&gt;
* &lt;/service&gt;</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>
* &lt;service android:name="component_name_of_your_implementation" &gt;
* &lt;intent-filter&gt;
* &lt;action android:name="android.media.MediaSessionService2" /&gt;
* &lt;/intent-filter&gt;
* &lt;meta-data android:name="android.media.session"
* android:value="session_id"/&gt;
* &lt;/service&gt;</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.
}
}
}