media/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java
tonihei 4a7442ef3a Replace or suppress deprecated usages
Many usages are needed to support other deprecations and some
can be replaced by the recommended direct alternative.

Also replace links to deprecated/redirected dev site

PiperOrigin-RevId: 601795998
(cherry picked from commit ed5b7004b4)
2024-02-08 17:29:12 +00:00

763 lines
29 KiB
Java

/*
* 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.ComponentName;
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 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.MediaLibraryInfo;
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}.
*
* <p>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.
*
* <p>To extend this class, declare the intent filter in your {@code AndroidManifest.xml}:
*
* <pre>{@code
* <service
* android:name="NameOfYourService"
* android:foregroundServiceType="mediaPlayback"
* android:exported="true">
* <intent-filter>
* <action android:name="androidx.media3.session.MediaSessionService"/>
* </intent-filter>
* </service>
* }</pre>
*
* <p>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.
*
* <p>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 <a href="#MultipleSessions">Supporting Multiple Sessions</a>.
*
* <p>Topics covered here:
*
* <ol>
* <li><a href="#ServiceLifecycle">Service Lifecycle</a>
* <li><a href="#MultipleSessions">Supporting Multiple Sessions</a>
* </ol>
*
* <h2 id="ServiceLifecycle">Service Lifecycle</h2>
*
* <p>A media session service is a bound service and its <a
* href="https://developer.android.com/guide/topics/manifest/service-element#foregroundservicetype">
* foreground service type</a> must include <em>mediaPlayback</em>. 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)}.
*
* <p>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.
*
* <p>{@link #onUpdateNotification(MediaSession, boolean)} 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 <a
* href="https://developer.android.com/guide/components/foreground-services">foreground service</a>.
* 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, boolean)}. In this case, you must also start or stop the
* service from the foreground, when playback starts or stops respectively.
*
* <p>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.
*
* <h2 id="MultipleSessions">Supporting Multiple Sessions</h2>
*
* <p>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 <a
* href="https://developer.android.com/media/optimize/audio-focus">audio focus policy</a> 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.
*
* <p>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)}.
*
* <p>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}.
*
* <p>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 = "MSessionService";
private final Object lock;
private final Handler mainHandler;
@GuardedBy("lock")
private final Map<String, MediaSession> sessions;
@GuardedBy("lock")
@Nullable
private MediaSessionServiceStub stub;
@GuardedBy("lock")
private @MonotonicNonNull MediaNotificationManager mediaNotificationManager;
@GuardedBy("lock")
private MediaNotification.@MonotonicNonNull Provider mediaNotificationProvider;
@GuardedBy("lock")
private @MonotonicNonNull DefaultActionFactory actionFactory;
@GuardedBy("lock")
@Nullable
private Listener listener;
private boolean defaultMethodCalled;
/** Creates a service. */
public MediaSessionService() {
lock = new Object();
mainHandler = new Handler(Looper.getMainLooper());
sessions = new ArrayMap<>();
defaultMethodCalled = false;
}
/**
* Called when the service is created.
*
* <p>Override this method if you need your own initialization.
*
* <p>This method will be called on the main thread.
*/
@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.
*
* <p>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)}.
*
* <p>There are two special cases where the {@link ControllerInfo#getPackageName()} returns a
* non-existent package name:
*
* <ul>
* <li>When the service is started by a media button event, the package name will be {@link
* Intent#ACTION_MEDIA_BUTTON}. If you want to allow the service to be started by media
* button events, do not return {@code null}.
* <li>When a legacy {@link android.media.browse.MediaBrowser} or a {@link
* android.support.v4.media.MediaBrowserCompat} tries to connect, the package name will be
* {@link MediaBrowserServiceCompat#SERVICE_INTERFACE}. If you want to allow the service to
* be bound by the legacy media browsers, do not return {@code null}.
* </ul>
*
* <p>For those special cases, the values returned by {@link ControllerInfo#getUid()} and {@link
* ControllerInfo#getConnectionHints()} have no meaning.
*
* <p>This method will be 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 <a
* href="#MultipleSessions">Supporting Multiple Sessions</a> for details.
*
* <p>The added session will be removed automatically {@linkplain MediaSession#release() when the
* session is released}.
*
* <p>This method can be called from any thread.
*
* @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 <a href="#MultipleSessions">Supporting Multiple Sessions</a> for details.
*
* <p>This method can be called from any thread.
*
* @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)}.
*
* <p>This method can be called from any thread.
*/
public final List<MediaSession> getSessions() {
synchronized (lock) {
return new ArrayList<>(sessions.values());
}
}
/**
* Returns whether {@code session} has been added to this service via {@link #addSession} or
* {@link #onGetSession(ControllerInfo)}.
*
* <p>This method can be called from any thread.
*/
public final boolean isSessionAdded(MediaSession session) {
synchronized (lock) {
return sessions.containsKey(session.getId());
}
}
/**
* Sets the {@linkplain Listener listener}.
*
* <p>This method can be called from any thread.
*/
@UnstableApi
public final void setListener(Listener listener) {
synchronized (lock) {
this.listener = listener;
}
}
/**
* Clears the {@linkplain Listener listener}.
*
* <p>This method can be called from any thread.
*/
@UnstableApi
public final void clearListener() {
synchronized (lock) {
this.listener = null;
}
}
/**
* Called when a component is about to bind to the service.
*
* <p>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}.
*
* <p>This method will be called on the main thread.
*/
@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)}.
*
* <p>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}.
*
* <p>This method will be called on the main thread.
*/
@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);
}
MediaSessionImpl sessionImpl = session.getImpl();
sessionImpl
.getApplicationHandler()
.post(
() -> {
ControllerInfo callerInfo = sessionImpl.getMediaNotificationControllerInfo();
if (callerInfo == null) {
callerInfo = createFallbackMediaButtonCaller(intent);
}
if (!sessionImpl.onMediaButtonEvent(callerInfo, intent)) {
Log.d(TAG, "Ignored unrecognized media button intent.");
}
});
} 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;
}
private static ControllerInfo createFallbackMediaButtonCaller(Intent mediaButtonIntent) {
@Nullable ComponentName componentName = mediaButtonIntent.getComponent();
String packageName =
componentName != null
? componentName.getPackageName()
: "androidx.media3.session.MediaSessionService";
return new ControllerInfo(
new MediaSessionManager.RemoteUserInfo(
packageName,
MediaSessionManager.RemoteUserInfo.UNKNOWN_PID,
MediaSessionManager.RemoteUserInfo.UNKNOWN_UID),
MediaLibraryInfo.VERSION_INT,
MediaControllerStub.VERSION_INT,
/* trusted= */ false,
/* cb= */ null,
/* connectionHints= */ Bundle.EMPTY);
}
/**
* Called when the service is no longer used and is being removed.
*
* <p>Override this method if you need your own clean up.
*
* <p>This method will be called on the main thread.
*/
@CallSuper
@Override
public void onDestroy() {
super.onDestroy();
synchronized (lock) {
if (stub != null) {
stub.release();
stub = null;
}
}
}
/**
* @deprecated Use {@link #onUpdateNotification(MediaSession, boolean)} instead.
*/
@Deprecated
public void onUpdateNotification(MediaSession session) {
defaultMethodCalled = true;
}
/**
* Called when a notification needs to be updated. Override this method to show or cancel your own
* notifications.
*
* <p>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.
*
* <p>Override this method to create your own notification and customize the foreground handling
* of your service.
*
* <p>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 <a
* href="https://developer.android.com/guide/components/foreground-services">foreground</a> when
* playback is ongoing and put back into background otherwise.
*
* <p>Apps targeting {@code SDK_INT >= 28} must request the permission, {@link
* android.Manifest.permission#FOREGROUND_SERVICE}.
*
* <p>This method will be called on the main thread.
*
* @param session A session that needs notification update.
* @param startInForegroundRequired Whether the service is required to start in the foreground.
*/
@SuppressWarnings("deprecation") // Calling deprecated method.
public void onUpdateNotification(MediaSession session, boolean startInForegroundRequired) {
onUpdateNotification(session);
if (defaultMethodCalled) {
getMediaNotificationManager().updateNotification(session, startInForegroundRequired);
}
}
/**
* Sets the {@link MediaNotification.Provider} to customize notifications.
*
* <p>This should be called before {@link #onCreate()} returns.
*
* <p>This method can be called from any thread.
*/
@UnstableApi
protected final void setMediaNotificationProvider(
MediaNotification.Provider mediaNotificationProvider) {
checkNotNull(mediaNotificationProvider);
synchronized (lock) {
this.mediaNotificationProvider = mediaNotificationProvider;
}
}
/* package */ IBinder getServiceBinder() {
synchronized (lock) {
return checkStateNotNull(stub).asBinder();
}
}
/**
* Triggers notification update and handles {@code ForegroundServiceStartNotAllowedException}.
*
* <p>This method will be called on the main thread.
*/
/* package */ boolean onUpdateNotificationInternal(
MediaSession session, boolean startInForegroundWhenPaused) {
try {
boolean startInForegroundRequired =
getMediaNotificationManager().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;
}
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;
}
}
@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<MediaSessionService> serviceReference;
private final Handler handler;
private final MediaSessionManager mediaSessionManager;
private final Set<IMediaController> pendingControllers;
public MediaSessionServiceStub(MediaSessionService serviceReference) {
this.serviceReference = new WeakReference<>(serviceReference);
Context context = serviceReference.getApplicationContext();
handler = new Handler(context.getMainLooper());
mediaSessionManager = MediaSessionManager.getSessionManager(context);
// ConcurrentHashMap has a bug in APIs 21-22 that can result in lost updates.
pendingControllers = Collections.synchronizedSet(new HashSet<>());
}
@Override
public void connect(
@Nullable IMediaController caller, @Nullable Bundle connectionRequestBundle) {
if (caller == null || connectionRequestBundle == null) {
// Malformed call from potentially malicious controller.
// No need to notify that we're ignoring call.
return;
}
ConnectionRequest request;
try {
request = ConnectionRequest.fromBundle(connectionRequestBundle);
} catch (RuntimeException e) {
// Malformed call from potentially malicious controller.
// No need to notify that we're ignoring call.
Log.w(TAG, "Ignoring malformed Bundle for ConnectionRequest", e);
return;
}
if (serviceReference.get() == null) {
try {
caller.onDisconnected(/* seq= */ 0);
} catch (RemoteException e) {
// Controller may be died prematurely.
// Not an issue because we'll ignore it anyway.
}
return;
}
int callingPid = Binder.getCallingPid();
int uid = Binder.getCallingUid();
long token = Binder.clearCallingIdentity();
int pid = (callingPid != 0) ? callingPid : request.pid;
MediaSessionManager.RemoteUserInfo remoteUserInfo =
new MediaSessionManager.RemoteUserInfo(request.packageName, pid, uid);
boolean isTrusted = mediaSessionManager.isTrustedForMediaControl(remoteUserInfo);
pendingControllers.add(caller);
try {
handler.post(
() -> {
pendingControllers.remove(caller);
boolean shouldNotifyDisconnected = true;
try {
@Nullable MediaSessionService service = serviceReference.get();
if (service == null) {
return;
}
ControllerInfo controllerInfo =
new ControllerInfo(
remoteUserInfo,
request.libraryVersion,
request.controllerInterfaceVersion,
isTrusted,
new MediaSessionStub.Controller2Cb(caller),
request.connectionHints);
@Nullable MediaSession session;
try {
session = service.onGetSession(controllerInfo);
if (session == null) {
return;
}
service.addSession(session);
shouldNotifyDisconnected = false;
session.handleControllerConnectionFromService(caller, controllerInfo);
} catch (Exception e) {
// Don't propagate exception in service to the controller.
Log.w(TAG, "Failed to add a session to session service", e);
}
} finally {
// Trick to call onDisconnected() in one place.
if (shouldNotifyDisconnected) {
try {
caller.onDisconnected(/* seq= */ 0);
} catch (RemoteException e) {
// Controller may be died prematurely.
// Not an issue because we'll ignore it anyway.
}
}
}
});
} finally {
Binder.restoreCallingIdentity(token);
}
}
public void release() {
serviceReference.clear();
handler.removeCallbacksAndMessages(null);
for (IMediaController controller : pendingControllers) {
try {
controller.onDisconnected(/* seq= */ 0);
} catch (RemoteException e) {
// Ignore. We're releasing.
}
}
}
}
@RequiresApi(31)
private static final class Api31 {
@DoNotInline
public static boolean instanceOfForegroundServiceStartNotAllowedException(
IllegalStateException e) {
return e instanceof ForegroundServiceStartNotAllowedException;
}
}
}