diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java new file mode 100644 index 0000000000..fba9b917a9 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultActionFactory.java @@ -0,0 +1,149 @@ +/* + * Copyright 2022 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 android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.media.session.PlaybackStateCompat; +import android.view.KeyEvent; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.core.app.NotificationCompat; +import androidx.core.graphics.drawable.IconCompat; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; + +/** The default {@link MediaNotification.ActionFactory}. */ +@UnstableApi +/* package */ final class DefaultActionFactory implements MediaNotification.ActionFactory { + + private static final String ACTION_CUSTOM = "androidx.media3.session.CUSTOM_NOTIFICATION_ACTION"; + private static final String EXTRAS_KEY_ACTION_CUSTOM = + "androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION"; + public static final String EXTRAS_KEY_ACTION_CUSTOM_EXTRAS = + "androidx.media3.session.EXTRAS_KEY_CUSTOM_NOTIFICATION_ACTION_EXTRAS"; + + private final Context context; + + public DefaultActionFactory(Context context) { + this.context = context.getApplicationContext(); + } + + @Override + public NotificationCompat.Action createMediaAction( + IconCompat icon, CharSequence title, @Command long command) { + return new NotificationCompat.Action(icon, title, createMediaActionPendingIntent(command)); + } + + @Override + public NotificationCompat.Action createCustomAction( + IconCompat icon, CharSequence title, String customAction, Bundle extras) { + return new NotificationCompat.Action( + icon, title, createCustomActionPendingIntent(customAction, extras)); + } + + @Override + public PendingIntent createMediaActionPendingIntent(@Command long command) { + int keyCode = PlaybackStateCompat.toKeyCode(command); + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.setComponent(new ComponentName(context, context.getClass())); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + if (Util.SDK_INT >= 26 && command != COMMAND_PAUSE && command != COMMAND_STOP) { + return Api26.createPendingIntent(context, /* requestCode= */ keyCode, intent); + } else { + return PendingIntent.getService( + context, + /* requestCode= */ keyCode, + intent, + Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0); + } + } + + private PendingIntent createCustomActionPendingIntent(String action, Bundle extras) { + Intent intent = new Intent(ACTION_CUSTOM); + intent.setComponent(new ComponentName(context, context.getClass())); + intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM, action); + intent.putExtra(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS, extras); + if (Util.SDK_INT >= 26) { + return Api26.createPendingIntent( + context, /* requestCode= */ KeyEvent.KEYCODE_UNKNOWN, intent); + } else { + return PendingIntent.getService( + context, + /* requestCode= */ KeyEvent.KEYCODE_UNKNOWN, + intent, + Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0); + } + } + + /** Returns whether {@code intent} was part of a {@link #createMediaAction media action}. */ + public boolean isMediaAction(Intent intent) { + return Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()); + } + + /** Returns whether {@code intent} was part of a {@link #createCustomAction custom action }. */ + public boolean isCustomAction(Intent intent) { + return ACTION_CUSTOM.equals(intent.getAction()); + } + + /** + * Returns the {@link KeyEvent} that was included in the media action, or {@code null} if no + * {@link KeyEvent} is found in the {@code intent}. + */ + @Nullable + public KeyEvent getKeyEvent(Intent intent) { + @Nullable Bundle extras = intent.getExtras(); + if (extras != null && extras.containsKey(Intent.EXTRA_KEY_EVENT)) { + return extras.getParcelable(Intent.EXTRA_KEY_EVENT); + } + return null; + } + + /** + * Returns the custom action that was included in the {@link #createCustomAction custom action}, + * or {@code null} if no custom action is found in the {@code intent}. + */ + @Nullable + public String getCustomAction(Intent intent) { + @Nullable Bundle extras = intent.getExtras(); + @Nullable Object customAction = extras != null ? extras.get(EXTRAS_KEY_ACTION_CUSTOM) : null; + return customAction instanceof String ? (String) customAction : null; + } + + /** + * Returns extras that were included in the {@link #createCustomAction custom action}, or {@link + * Bundle#EMPTY} is no extras are found. + */ + public Bundle getCustomActionExtras(Intent intent) { + @Nullable Bundle extras = intent.getExtras(); + @Nullable + Object customExtras = extras != null ? extras.get(EXTRAS_KEY_ACTION_CUSTOM_EXTRAS) : null; + return customExtras instanceof Bundle ? (Bundle) customExtras : Bundle.EMPTY; + } + + @RequiresApi(26) + private static final class Api26 { + private Api26() {} + + public static PendingIntent createPendingIntent(Context context, int keyCode, Intent intent) { + return PendingIntent.getForegroundService( + context, /* requestCode= */ keyCode, intent, PendingIntent.FLAG_IMMUTABLE); + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java new file mode 100644 index 0000000000..f253433905 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -0,0 +1,173 @@ +/* + * Copyright 2022 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.checkStateNotNull; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import androidx.core.app.NotificationCompat; +import androidx.core.graphics.drawable.IconCompat; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; + +/** + * The default {@link MediaNotification.Provider}. + * + *
The provider is required to create a {@link androidx.core.app.NotificationChannelCompat + * notification channel}, which is required to show notification for {@code SDK_INT >= 26}. + */ + @UnstableApi + public interface Provider { + /** Receives updates for a notification. */ + interface Callback { + /** + * Called when a {@link MediaNotification} is changed. + * + *
This callback is called when notifications are updated, for example after a bitmap is
+ * loaded asynchronously and needs to be displayed.
+ *
+ * @param notification The updated {@link MediaNotification}
+ */
+ void onNotificationChanged(MediaNotification notification);
+ }
+
+ /**
+ * Creates a new {@link MediaNotification}.
+ *
+ * @param mediaController The controller of the session.
+ * @param actionFactory The {@link ActionFactory} for creating notification {@link
+ * NotificationCompat.Action actions}.
+ * @param onNotificationChangedCallback A callback that the provider needs to notify when the
+ * notification has changed and needs to be posted again, for example after a bitmap has
+ * been loaded asynchronously.
+ */
+ MediaNotification createNotification(
+ MediaController mediaController,
+ ActionFactory actionFactory,
+ Callback onNotificationChangedCallback);
+
+ /**
+ * Handles a notification's custom action.
+ *
+ * @param mediaController The controller of the session.
+ * @param action The custom action.
+ * @param extras Extras set in the custom action, otherwise {@link Bundle#EMPTY}.
+ * @see ActionFactory#createCustomAction
+ */
+ void handleCustomAction(MediaController mediaController, String action, Bundle extras);
+ }
+
+ /** The notification id. */
+ @IntRange(from = 1)
+ public final int notificationId;
+
+ /** The {@link Notification}. */
+ public final Notification notification;
+
+ /**
+ * Creates an instance.
+ *
+ * @param notificationId The notification id to be used for {@link NotificationManager#notify(int,
+ * Notification)}.
+ * @param notification A {@link Notification} that reflects the sate of a {@link MediaController}
+ * and to send media commands to a {@link MediaSession}. The notification may be used to start
+ * a service in the foreground.
+ * It's highly recommended to use a {@link androidx.media.app.NotificationCompat.MediaStyle
+ * media style} {@link Notification notification}.
+ */
+ public MediaNotification(@IntRange(from = 1) int notificationId, Notification notification) {
+ this.notificationId = notificationId;
+ this.notification = checkNotNull(notification);
+ }
+}
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java
index 50555885d3..c89ebe092d 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaNotificationManager.java
@@ -15,30 +15,14 @@
*/
package androidx.media3.session;
-import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE;
-import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY;
-import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
-import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
-import static android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP;
-
import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
+import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
-import android.support.v4.media.session.PlaybackStateCompat;
-import android.view.KeyEvent;
import androidx.annotation.Nullable;
-import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
-import androidx.media.app.NotificationCompat.MediaStyle;
-import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.Util;
import com.google.common.util.concurrent.ListenableFuture;
@@ -52,63 +36,33 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
- * Provides default media notifications for {@link MediaSessionService} and sets the service as
+ * Manages media notifications for a {@link MediaSessionService} and sets the service as
* foreground/background according to the player state.
*/
/* package */ final class MediaNotificationManager {
- private static final int NOTIFICATION_ID = 1001;
- private static final String NOTIFICATION_CHANNEL_ID = "default_channel_id";
-
- private final MediaSessionService service;
+ private final MediaSessionService mediaSessionService;
+ private final MediaNotification.Provider mediaNotificationProvider;
+ private final MediaNotification.ActionFactory actionFactory;
+ private final NotificationManagerCompat notificationManagerCompat;
private final Executor mainExecutor;
- private final NotificationManager notificationManager;
- private final String notificationChannelName;
-
private final Intent startSelfIntent;
- private final NotificationCompat.Action playAction;
- private final NotificationCompat.Action pauseAction;
- private final NotificationCompat.Action skipToPrevAction;
- private final NotificationCompat.Action skipToNextAction;
-
private final Map It's highly recommended for an app to use this class if they want to keep media playback in
* the background. The service allows other apps to know that your app supports {@link MediaSession}
- * even when your app isn't running. For example, user's voice command may start your app to play
+ * even when your app isn't running. For example, a user voice command may start your app to play
* media.
*
- * To extend this class, declare the intent filter in your {@code AndroidManifest.xml}.
+ * To extend this class, declare the intent filter in your {@code AndroidManifest.xml}:
*
* A media session service is a bound service. When a {@link MediaController} is created for the
* service, the controller binds to the service. {@link #onGetSession(ControllerInfo)} will be
- * called inside of the {@link #onBind(Intent)}.
+ * called from {@link #onBind(Intent)}.
*
- * After the binding, the session's {@link MediaSession.SessionCallback#onConnect(MediaSession,
+ * After binding, the session's {@link MediaSession.SessionCallback#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.
*
- * When a playback is started on the service, {@link #onUpdateNotification(MediaSession)} is
- * called and 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.
+ * When a playback is started on the service, the service will obtain a {@link MediaNotification}
+ * from the {@link MediaNotification.Provider} that's set with {@link #setMediaNotificationProvider}
+ * (or {@link DefaultMediaNotificationProvider}, if no provider is set), and 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.
*
* The service will be destroyed when all sessions are closed, or no controller is binding to the
* service while the service is in the background.
@@ -110,18 +109,18 @@ import java.util.concurrent.ConcurrentHashMap;
* Generally, multiple sessions aren't necessary for most media apps. One exception is if your
- * app can play multiple media content at the same time, but only for the playback of video-only
- * media or remote playback, since the audio focus policy
- * recommends not playing multiple audio content at the same time. Also, keep in mind that multiple
- * media sessions would make Android Auto and Bluetooth device with display to show your apps
+ * 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 {@link MediaController} can be created with {@link SessionToken} to connect to a
+ * 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)}.
@@ -144,8 +143,13 @@ public abstract class MediaSessionService extends Service {
private MediaSessionServiceStub stub;
@GuardedBy("lock")
- @Nullable
- private MediaNotificationManager notificationHandler;
+ private @MonotonicNonNull MediaNotificationManager mediaNotificationManager;
+
+ @GuardedBy("lock")
+ private MediaNotification.@MonotonicNonNull Provider mediaNotificationProvider;
+
+ @GuardedBy("lock")
+ private @MonotonicNonNull DefaultActionFactory actionFactory;
/** Creates a service. */
public MediaSessionService() {
@@ -165,7 +169,6 @@ public abstract class MediaSessionService extends Service {
super.onCreate();
synchronized (lock) {
stub = new MediaSessionServiceStub(this);
- notificationHandler = new MediaNotificationManager(this);
}
}
@@ -179,7 +182,7 @@ public abstract class MediaSessionService extends Service {
* 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
+ * There are two special cases where the {@link ControllerInfo#getPackageName()} returns a
* non-existent package name:
*
* Added session will be removed automatically when it's closed.
+ * The added session will be removed automatically when it's closed.
*
* @param session A session to be added.
* @see #removeSession(MediaSession)
@@ -227,11 +230,8 @@ public abstract class MediaSessionService extends Service {
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 handler;
- synchronized (lock) {
- handler = checkStateNotNull(notificationHandler);
- }
- postOrRun(mainHandler, () -> handler.addSession(session));
+ MediaNotificationManager notificationManager = getMediaNotificationManager();
+ postOrRun(mainHandler, () -> notificationManager.addSession(session));
}
}
@@ -245,39 +245,12 @@ public abstract class MediaSessionService extends Service {
*/
public final void removeSession(MediaSession session) {
checkNotNull(session, "session must not be null");
- MediaNotificationManager handler;
synchronized (lock) {
+ checkArgument(sessions.containsKey(session.getId()), "session not found");
sessions.remove(session.getId());
- handler = checkStateNotNull(notificationHandler);
}
- postOrRun(mainHandler, () -> handler.removeSession(session));
- }
-
- /**
- * Called when {@link MediaNotification} needs to be updated. Override this method to show or
- * cancel your own notification.
- *
- * This will be called on the application thread of the underlying {@link Player} of {@link
- * MediaSession}.
- *
- * With the notification returned by this method, the service becomes a foreground
- * service when the playback is started. Apps targeting {@code SDK_INT >= 28} must request the
- * permission, {@link android.Manifest.permission#FOREGROUND_SERVICE}. It becomes a background
- * service after the playback is stopped.
- *
- * @param session A session that needs notification update.
- * @return A {@link MediaNotification}, or {@code null} if you don't want the automatic
- * foreground/background transitions.
- */
- @Nullable
- public MediaNotification onUpdateNotification(MediaSession session) {
- checkNotNull(session, "session must not be null");
- MediaNotificationManager handler;
- synchronized (lock) {
- handler = checkStateNotNull(notificationHandler, "Service hasn't created");
- }
- return handler.onUpdateNotification(session);
+ MediaNotificationManager notificationManager = getMediaNotificationManager();
+ postOrRun(mainHandler, () -> notificationManager.removeSession(session));
}
/**
@@ -347,9 +320,15 @@ public abstract class MediaSessionService extends Service {
if (intent == null) {
return START_STICKY;
}
- if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
- @Nullable Uri uri = intent.getData();
- @Nullable MediaSession session = uri != null ? MediaSession.getSession(uri) : null;
+
+ DefaultActionFactory actionFactory;
+ synchronized (lock) {
+ actionFactory = checkStateNotNull(this.actionFactory);
+ }
+
+ @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);
@@ -358,10 +337,16 @@ public abstract class MediaSessionService extends Service {
}
addSession(session);
}
- KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+ @Nullable KeyEvent keyEvent = actionFactory.getKeyEvent(intent);
if (keyEvent != null) {
session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent);
}
+ } else if (actionFactory.isCustomAction(intent)) {
+ @Nullable String customAction = actionFactory.getCustomAction(intent);
+ if (session != null && customAction != null) {
+ Bundle customExtras = actionFactory.getCustomActionExtras(intent);
+ getMediaNotificationManager().onCustomAction(session, customAction, customExtras);
+ }
}
return START_STICKY;
}
@@ -383,35 +368,40 @@ public abstract class MediaSessionService extends Service {
}
}
+ /**
+ * Sets the {@link MediaNotification.Provider} to customize notifications.
+ *
+ * This should be called before any session is attached to this service via {@link
+ * #onGetSession(ControllerInfo)} or {@link #addSession(MediaSession)}. Otherwise a default UX
+ * will be shown with {@link DefaultMediaNotificationProvider}.
+ */
+ @UnstableApi
+ protected final void setMediaNotificationProvider(
+ MediaNotification.Provider mediaNotificationProvider) {
+ checkNotNull(mediaNotificationProvider);
+ synchronized (lock) {
+ this.mediaNotificationProvider = mediaNotificationProvider;
+ }
+ }
+
/* package */ IBinder getServiceBinder() {
synchronized (lock) {
return checkStateNotNull(stub).asBinder();
}
}
- /** A notification for media playback returned by {@link #onUpdateNotification(MediaSession)}. */
- public static final class MediaNotification {
-
- /** The notification id. */
- @IntRange(from = 1)
- public final int notificationId;
-
- /** The {@link Notification}. */
- public final Notification notification;
-
- /**
- * Creates an instance.
- *
- * @param notificationId The notification id to be used for {@link
- * NotificationManager#notify(int, Notification)}.
- * @param notification A {@link Notification} to make the {@link MediaSessionService} foreground.
- * It's highly recommended to use a {@link androidx.media.app.NotificationCompat.MediaStyle
- * media style} {@link Notification notification}.
- */
- public MediaNotification(@IntRange(from = 1) int notificationId, Notification notification) {
- this.notificationId = notificationId;
- this.notification = checkNotNull(notification);
+ private MediaNotificationManager getMediaNotificationManager() {
+ synchronized (lock) {
+ if (mediaNotificationManager == null) {
+ if (mediaNotificationProvider == null) {
+ mediaNotificationProvider = new DefaultMediaNotificationProvider(getApplicationContext());
+ }
+ actionFactory = new DefaultActionFactory(getApplicationContext());
+ mediaNotificationManager =
+ new MediaNotificationManager(
+ /* mediaSessionService= */ this, mediaNotificationProvider, actionFactory);
+ }
+ return mediaNotificationManager;
}
}
diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java
new file mode 100644
index 0000000000..9301b618b0
--- /dev/null
+++ b/libraries/session/src/test/java/androidx/media3/session/DefaultActionFactoryTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022 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 com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.shadows.ShadowPendingIntent;
+
+/** Tests for {@link DefaultActionFactory}. */
+@RunWith(AndroidJUnit4.class)
+public class DefaultActionFactoryTest {
+
+ @Test
+ public void createMediaPendingIntent_intentIsMediaAction() {
+ DefaultActionFactory actionFactory =
+ new DefaultActionFactory(ApplicationProvider.getApplicationContext());
+
+ PendingIntent pendingIntent =
+ actionFactory.createMediaActionPendingIntent(MediaNotification.ActionFactory.COMMAND_PLAY);
+
+ ShadowPendingIntent shadowPendingIntent = shadowOf(pendingIntent);
+ assertThat(actionFactory.isMediaAction(shadowPendingIntent.getSavedIntent())).isTrue();
+ }
+
+ @Test
+ public void isMediaAction_withNonMediaIntent_returnsFalse() {
+ DefaultActionFactory actionFactory =
+ new DefaultActionFactory(ApplicationProvider.getApplicationContext());
+
+ Intent intent = new Intent("invalid_action");
+
+ assertThat(actionFactory.isMediaAction(intent)).isFalse();
+ }
+
+ @Test
+ public void isCustomAction_withNonCustomActionIntent_returnsFalse() {
+ DefaultActionFactory actionFactory =
+ new DefaultActionFactory(ApplicationProvider.getApplicationContext());
+
+ Intent intent = new Intent("invalid_action");
+
+ assertThat(actionFactory.isCustomAction(intent)).isFalse();
+ }
+}
{@code
*
Supporting Multiple Sessions
*
*
@@ -209,7 +212,7 @@ public abstract class MediaSessionService extends Service {
* Adds a {@link MediaSession} to this service. This is not necessary for most media apps. See Supporting Multiple Sessions for details.
*
- *