/* * Copyright 2019 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.media3.session; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.postOrRun; import android.app.ForegroundServiceStartNotAllowedException; import android.app.Service; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.view.KeyEvent; import androidx.annotation.CallSuper; import androidx.annotation.DoNotInline; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.collection.ArrayMap; import androidx.media.MediaBrowserServiceCompat; import androidx.media.MediaSessionManager; import androidx.media3.common.Player; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerInfo; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Superclass to be extended by services hosting {@link MediaSession media sessions}. * *
It's highly recommended for an app to use this class if media playback should continue while * in the background. The service allows other apps to know that your app supports {@link * MediaSession} even when your app isn't running. This way, a user voice command may be able start * your app to play media. * *
To extend this class, declare the intent filter in your {@code AndroidManifest.xml}: * *
{@code ** ** * }* **
You may also declare the action {@code android.media.browse.MediaBrowserService} for * compatibility with {@link android.support.v4.media.MediaBrowserCompat}. This service can handle * the case automatically. * *
It's recommended for an app to have a single service declared in the manifest. Otherwise, your * app might be shown twice in the list of the controller apps, or another app might fail to pick * the right service when it wants to start a playback on this app. If you want to provide multiple * sessions, take a look at Supporting Multiple Sessions. * *
Topics covered here: * *
* *A media session service is a bound service and its * foreground service type must include mediaPlayback. When a {@link MediaController} * is created for the service, the controller binds to the service. {@link * #onGetSession(ControllerInfo)} will be called from {@link #onBind(Intent)}. * *
After binding, the session's {@link MediaSession.Callback#onConnect(MediaSession, * MediaSession.ControllerInfo)} will be called to accept or reject the connection request from the * controller. If it's accepted, the controller will be available and keep the binding. If it's * rejected, the controller will unbind. * *
{@link #onUpdateNotification(MediaSession)} will be called whenever a notification needs to be * shown, updated or cancelled. The default implementation will display notifications using a * default UI or using a {@link MediaNotification.Provider} that's set with {@link * #setMediaNotificationProvider}. In addition, when playback starts, the service will become a foreground service. * It's required to keep the playback after the controller is destroyed. The service will become a * background service when all playbacks are stopped. Apps targeting {@code SDK_INT >= 28} must * request the permission, {@link android.Manifest.permission#FOREGROUND_SERVICE}, in order to make * the service foreground. You can control when to show or hide notifications by overriding {@link * #onUpdateNotification(MediaSession)}. In this case, you must also start or stop the service from * the foreground, when playback starts or stops respectively. * *
The service will be destroyed when all sessions are {@linkplain MediaController#release() * released}, or no controller is binding to the service while the service is in the background. * *
Generally, multiple sessions aren't necessary for most media apps. One exception is if your * app can play multiple media contents at the same time, but only for playback of video-only media * or remote playback, since the audio focus policy * recommends not playing multiple audio contents at the same time. Also, keep in mind that multiple * media sessions would make Android Auto and Bluetooth devices with a display to show your apps * multiple times, because they list up media sessions, not media apps. * *
However, if you're capable of handling multiple playbacks and want to keep their sessions * while the app is in the background, create multiple sessions and add them to this service with * {@link #addSession(MediaSession)}. * *
Note that a {@link MediaController} can be created with {@link SessionToken} to connect to a * session in this service. In that case, {@link #onGetSession(ControllerInfo)} will be called to * decide which session to handle the connection request. Pick the best session among the added * sessions, or create a new session and return it from {@link #onGetSession(ControllerInfo)}. */ public abstract class MediaSessionService extends Service { /** * Listener for {@link MediaSessionService}. * *
The methods will be called on the main thread.
*/
@UnstableApi
public interface Listener {
/**
* Called when the service fails to start in the foreground and a {@link
* ForegroundServiceStartNotAllowedException} is thrown on Android 12 or later.
*/
@RequiresApi(31)
default void onForegroundServiceStartNotAllowedException() {}
}
/** The action for {@link Intent} filter that must be declared by the service. */
public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaSessionService";
private static final String TAG = "MSSImpl";
private final Object lock;
private final Handler mainHandler;
@GuardedBy("lock")
private final Map Override this method if you need your own initialization.
*/
@CallSuper
@Override
public void onCreate() {
super.onCreate();
synchronized (lock) {
stub = new MediaSessionServiceStub(this);
}
}
/**
* Called when a {@link MediaController} is created with this service's {@link SessionToken}.
* Return a {@link MediaSession} that the controller will connect to, or {@code null} to reject
* the connection request.
*
* The service automatically maintains the returned sessions. In other words, a session
* returned by this method will be added to the service, and removed from the service when the
* session is closed. You don't need to manually call {@link #addSession(MediaSession)} nor {@link
* #removeSession(MediaSession)}.
*
* There are two special cases where the {@link ControllerInfo#getPackageName()} returns a
* non-existent package name:
*
* For those special cases, the values returned by {@link ControllerInfo#getUid()} and {@link
* ControllerInfo#getConnectionHints()} have no meaning.
*
* This method is always called on the main thread.
*
* @param controllerInfo The information of the controller that is trying to connect.
* @return A {@link MediaSession} for the controller, or {@code null} to reject the connection.
* @see MediaSession.Builder
* @see #getSessions()
*/
@Nullable
public abstract MediaSession onGetSession(ControllerInfo controllerInfo);
/**
* Adds a {@link MediaSession} to this service. This is not necessary for most media apps. See Supporting Multiple Sessions for details.
*
* The added session will be removed automatically {@linkplain MediaSession#release() when the
* session is released}.
*
* @param session A session to be added.
* @see #removeSession(MediaSession)
* @see #getSessions()
*/
public final void addSession(MediaSession session) {
checkNotNull(session, "session must not be null");
checkArgument(!session.isReleased(), "session is already released");
@Nullable MediaSession old;
synchronized (lock) {
old = sessions.get(session.getId());
checkArgument(old == null || old == session, "Session ID should be unique");
sessions.put(session.getId(), session);
}
if (old == null) {
// Session has returned for the first time. Register callbacks.
// TODO(b/191644474): Check whether the session is registered to multiple services.
MediaNotificationManager notificationManager = getMediaNotificationManager();
postOrRun(mainHandler, () -> notificationManager.addSession(session));
session.setListener(new MediaSessionListener());
}
}
/**
* Removes a {@link MediaSession} from this service. This is not necessary for most media apps.
* See Supporting Multiple Sessions for details.
*
* @param session A session to be removed.
* @see #addSession(MediaSession)
* @see #getSessions()
*/
public final void removeSession(MediaSession session) {
checkNotNull(session, "session must not be null");
synchronized (lock) {
checkArgument(sessions.containsKey(session.getId()), "session not found");
sessions.remove(session.getId());
}
MediaNotificationManager notificationManager = getMediaNotificationManager();
postOrRun(mainHandler, () -> notificationManager.removeSession(session));
session.clearListener();
}
/**
* Returns the list of {@linkplain MediaSession sessions} that you've added to this service via
* {@link #addSession} or {@link #onGetSession(ControllerInfo)}.
*/
public final List The default implementation handles the incoming requests from {@link MediaController
* controllers}. In this case, the intent will have the action {@link #SERVICE_INTERFACE}.
* Override this method if this service also needs to handle actions other than {@link
* #SERVICE_INTERFACE}.
*/
@CallSuper
@Override
@Nullable
public IBinder onBind(@Nullable Intent intent) {
if (intent == null) {
return null;
}
@Nullable String action = intent.getAction();
if (action == null) {
return null;
}
switch (action) {
case MediaSessionService.SERVICE_INTERFACE:
return getServiceBinder();
case MediaBrowserServiceCompat.SERVICE_INTERFACE:
{
ControllerInfo controllerInfo = ControllerInfo.createLegacyControllerInfo();
@Nullable MediaSession session = onGetSession(controllerInfo);
if (session == null) {
// Legacy MediaBrowser(Compat) cannot connect to this service.
return null;
}
addSession(session);
// Return a specific session's legacy binder although the Android framework caches
// the returned binder here and next binding request may reuse cached binder even
// after the session is closed.
// Disclaimer: Although MediaBrowserCompat can only get the session that initially
// set, it doesn't make things bad. Such limitation had been there between
// MediaBrowserCompat and MediaBrowserServiceCompat.
return session.getLegacyBrowserServiceBinder();
}
default:
return null;
}
}
/**
* Called when a component calls {@link android.content.Context#startService(Intent)}.
*
* The default implementation handles the incoming media button events. In this case, the
* intent will have the action {@link Intent#ACTION_MEDIA_BUTTON}. Override this method if this
* service also needs to handle actions other than {@link Intent#ACTION_MEDIA_BUTTON}.
*/
@CallSuper
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
if (intent == null) {
return START_STICKY;
}
DefaultActionFactory actionFactory = getActionFactory();
@Nullable Uri uri = intent.getData();
@Nullable MediaSession session = uri != null ? MediaSession.getSession(uri) : null;
if (actionFactory.isMediaAction(intent)) {
if (session == null) {
ControllerInfo controllerInfo = ControllerInfo.createLegacyControllerInfo();
session = onGetSession(controllerInfo);
if (session == null) {
return START_STICKY;
}
addSession(session);
}
@Nullable KeyEvent keyEvent = actionFactory.getKeyEvent(intent);
if (keyEvent != null) {
session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
}
} else if (session != null && actionFactory.isCustomAction(intent)) {
@Nullable String customAction = actionFactory.getCustomAction(intent);
if (customAction == null) {
return START_STICKY;
}
Bundle customExtras = actionFactory.getCustomActionExtras(intent);
getMediaNotificationManager().onCustomAction(session, customAction, customExtras);
}
return START_STICKY;
}
/**
* Called when the service is no longer used and is being removed.
*
* Override this method if you need your own clean up.
*/
@CallSuper
@Override
public void onDestroy() {
super.onDestroy();
synchronized (lock) {
if (stub != null) {
stub.release();
stub = null;
}
}
}
/**
* Called when a notification needs to be updated. Override this method to show or cancel your own
* notifications.
*
* This method is called whenever the service has detected a change that requires to show,
* update or cancel a notification. The method will be called on the application thread of the app
* that the service belongs to.
*
* Override this method to create your own notification and customize the foreground handling
* of your service.
*
* At most one of {@link #onUpdateNotification(MediaSession, boolean)} and this method should
* be overridden. If neither of the two methods is overridden, the default implementation will
* present a default notification or the notification provided by the {@link
* MediaNotification.Provider} that is {@link
* #setMediaNotificationProvider(MediaNotification.Provider) set} by the app. Further, the service
* is started in the foreground when
* playback is ongoing and put back into background otherwise.
*
* Apps targeting {@code SDK_INT >= 28} must request the permission, {@link
* android.Manifest.permission#FOREGROUND_SERVICE}.
*
* @param session A session that needs notification update.
*/
public void onUpdateNotification(MediaSession session) {
setDefaultMethodCalled(true);
}
/**
* Called when a notification needs to be updated. Override this method to show or cancel your own
* notifications.
*
* This method is called whenever the service has detected a change that requires to show,
* update or cancel a notification with a flag {@code startInForegroundRequired} suggested by the
* service whether starting in the foreground is required. The method will be called on the
* application thread of the app that the service belongs to.
*
* Override this method to create your own notification and customize the foreground handling
* of your service.
*
* At most one of {@link #onUpdateNotification(MediaSession)} and this method should be
* overridden. If neither of the two methods is overridden, the default implementation will
* present a default notification or the notification provided by the {@link
* MediaNotification.Provider} that is {@link
* #setMediaNotificationProvider(MediaNotification.Provider) set} by the app. Further, the service
* is started in the foreground when
* playback is ongoing and put back into background otherwise.
*
* Apps targeting {@code SDK_INT >= 28} must request the permission, {@link
* android.Manifest.permission#FOREGROUND_SERVICE}.
*
* @param session A session that needs notification update.
* @param startInForegroundRequired Whether the service is required to start in the foreground.
*/
@UnstableApi
public void onUpdateNotification(MediaSession session, boolean startInForegroundRequired) {
onUpdateNotification(session);
if (isDefaultMethodCalled()) {
getMediaNotificationManager().updateNotification(session, startInForegroundRequired);
}
}
/**
* Sets the {@link MediaNotification.Provider} to customize notifications.
*
* This should be called before {@link #onCreate()} returns.
*/
@UnstableApi
protected final void setMediaNotificationProvider(
MediaNotification.Provider mediaNotificationProvider) {
checkNotNull(mediaNotificationProvider);
synchronized (lock) {
this.mediaNotificationProvider = mediaNotificationProvider;
}
}
/* package */ IBinder getServiceBinder() {
synchronized (lock) {
return checkStateNotNull(stub).asBinder();
}
}
/* package */ boolean onUpdateNotificationInternal(
MediaSession session, boolean startInForegroundWhenPaused) {
try {
boolean startInForegroundRequired =
shouldRunInForeground(session, startInForegroundWhenPaused);
onUpdateNotification(session, startInForegroundRequired);
} catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) {
if ((Util.SDK_INT >= 31) && Api31.instanceOfForegroundServiceStartNotAllowedException(e)) {
Log.e(TAG, "Failed to start foreground", e);
onForegroundServiceStartNotAllowedException();
return false;
}
throw e;
}
return true;
}
/* package */ static boolean shouldRunInForeground(
MediaSession session, boolean startInForegroundWhenPaused) {
Player player = session.getPlayer();
return (player.getPlayWhenReady() || startInForegroundWhenPaused)
&& (player.getPlaybackState() == Player.STATE_READY
|| player.getPlaybackState() == Player.STATE_BUFFERING);
}
private MediaNotificationManager getMediaNotificationManager() {
synchronized (lock) {
if (mediaNotificationManager == null) {
if (mediaNotificationProvider == null) {
mediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(getApplicationContext()).build();
}
mediaNotificationManager =
new MediaNotificationManager(
/* mediaSessionService= */ this, mediaNotificationProvider, getActionFactory());
}
return mediaNotificationManager;
}
}
private DefaultActionFactory getActionFactory() {
synchronized (lock) {
if (actionFactory == null) {
actionFactory = new DefaultActionFactory(/* service= */ this);
}
return actionFactory;
}
}
@Nullable
private Listener getListener() {
synchronized (lock) {
return this.listener;
}
}
private boolean isDefaultMethodCalled() {
synchronized (lock) {
return this.defaultMethodCalled;
}
}
private void setDefaultMethodCalled(boolean defaultMethodCalled) {
synchronized (lock) {
this.defaultMethodCalled = defaultMethodCalled;
}
}
@RequiresApi(31)
private void onForegroundServiceStartNotAllowedException() {
mainHandler.post(
() -> {
@Nullable MediaSessionService.Listener serviceListener = getListener();
if (serviceListener != null) {
serviceListener.onForegroundServiceStartNotAllowedException();
}
});
}
private final class MediaSessionListener implements MediaSession.Listener {
@Override
public void onNotificationRefreshRequired(MediaSession session) {
MediaSessionService.this.onUpdateNotificationInternal(
session, /* startInForegroundWhenPaused= */ false);
}
@Override
public boolean onPlayRequested(MediaSession session) {
if (Util.SDK_INT < 31 || Util.SDK_INT >= 33) {
return true;
}
// Check if service can start foreground successfully on Android 12 and 12L.
if (!getMediaNotificationManager().isStartedInForeground()) {
return onUpdateNotificationInternal(session, /* startInForegroundWhenPaused= */ true);
}
return true;
}
}
private static final class MediaSessionServiceStub extends IMediaSessionService.Stub {
private final WeakReference
*
*
*