Avoid usage of MediaController in MediaNotification.Provider

PiperOrigin-RevId: 451155897
This commit is contained in:
bachinger 2022-05-26 14:17:17 +00:00 committed by Marc Baechinger
parent 3d2b335825
commit 821615cea0
4 changed files with 79 additions and 70 deletions

View File

@ -51,7 +51,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -126,16 +125,17 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
@Override
public final MediaNotification createNotification(
MediaController mediaController,
MediaSession mediaSession,
ImmutableList<CommandButton> customLayout,
MediaNotification.ActionFactory actionFactory,
Callback onNotificationChangedCallback) {
ensureNotificationChannel();
Player player = mediaSession.getPlayer();
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
// Set metadata info in the notification.
MediaMetadata metadata = mediaController.getMediaMetadata();
MediaMetadata metadata = player.getMediaMetadata();
builder.setContentTitle(metadata.title).setContentText(metadata.artist);
@Nullable ListenableFuture<Bitmap> bitmapFuture = loadArtworkBitmap(metadata);
@ -164,19 +164,16 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
MediaStyle mediaStyle = new MediaStyle();
int[] compactViewIndices =
addNotificationActions(
getMediaButtons(
mediaController.getAvailableCommands(),
customLayout,
mediaController.getPlayWhenReady()),
getMediaButtons(player.getAvailableCommands(), customLayout, player.getPlayWhenReady()),
builder,
actionFactory);
mediaStyle.setShowActionsInCompactView(compactViewIndices);
if (mediaController.isCommandAvailable(COMMAND_STOP) || Util.SDK_INT < 21) {
if (player.isCommandAvailable(COMMAND_STOP) || Util.SDK_INT < 21) {
// We must include a cancel intent for pre-L devices.
mediaStyle.setCancelButtonIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP));
}
long playbackStartTimeMs = getPlaybackStartTimeEpochMs(mediaController);
long playbackStartTimeMs = getPlaybackStartTimeEpochMs(player);
boolean displayElapsedTimeWithChronometer = playbackStartTimeMs != C.TIME_UNSET;
builder
.setWhen(playbackStartTimeMs)
@ -185,7 +182,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
Notification notification =
builder
.setContentIntent(mediaController.getSessionActivity())
.setContentIntent(mediaSession.getSessionActivity())
.setDeleteIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP))
.setOnlyAlertOnce(true)
.setSmallIcon(getSmallIconResId(context))
@ -197,34 +194,9 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
}
@Override
public final void handleCustomCommand(
MediaController mediaController, String action, Bundle extras) {
@Nullable SessionCommand customCommand = null;
for (SessionCommand command : mediaController.getAvailableSessionCommands().commands) {
if (command.commandCode == SessionCommand.COMMAND_CODE_CUSTOM
&& command.customAction.equals(action)) {
customCommand = command;
break;
}
}
if (customCommand != null) {
ListenableFuture<SessionResult> future =
mediaController.sendCustomCommand(customCommand, extras);
Futures.addCallback(
future,
new FutureCallback<SessionResult>() {
@Override
public void onSuccess(SessionResult result) {
// Do nothing.
}
@Override
public void onFailure(Throwable t) {
Log.w(TAG, "custom command " + action + " produced an error: " + t.getMessage(), t);
}
},
MoreExecutors.directExecutor());
}
public final boolean handleCustomCommand(MediaSession session, String action, Bundle extras) {
// Make the custom action being delegated to the session as a custom session command.
return false;
}
/**
@ -422,14 +394,14 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
}
}
private static long getPlaybackStartTimeEpochMs(MediaController controller) {
private static long getPlaybackStartTimeEpochMs(Player player) {
// Changing "showWhen" causes notification flicker if SDK_INT < 21.
if (Util.SDK_INT >= 21
&& controller.isPlaying()
&& !controller.isPlayingAd()
&& !controller.isCurrentMediaItemDynamic()
&& controller.getPlaybackParameters().speed == 1f) {
return System.currentTimeMillis() - controller.getContentPosition();
&& player.isPlaying()
&& !player.isPlayingAd()
&& !player.isCurrentMediaItemDynamic()
&& player.getPlaybackParameters().speed == 1f) {
return System.currentTimeMillis() - player.getContentPosition();
} else {
return C.TIME_UNSET;
}

View File

@ -74,7 +74,7 @@ public final class MediaNotification {
* <p>The returned {@link NotificationCompat.Action} will have a {@link PendingIntent} with the
* extras from {@link SessionCommand#customExtras}. Accordingly the {@linkplain
* SessionCommand#customExtras command's extras} will be passed to {@link
* Provider#handleCustomCommand(MediaController, String, Bundle)} when the action is executed.
* Provider#handleCustomCommand(MediaSession, String, Bundle)} when the action is executed.
*
* @param customCommandButton A {@linkplain CommandButton custom command button}.
* @see MediaNotification.Provider#handleCustomCommand
@ -116,8 +116,8 @@ public final class MediaNotification {
/**
* Creates a new {@link MediaNotification}.
*
* @param mediaController The controller of the session.
* @param actionFactory The {@link ActionFactory} for creating notification {@linkplain
* @param session The media session.
* @param actionFactory The {@link ActionFactory} for creating notification {@link
* NotificationCompat.Action actions}.
* @param customLayout The custom layout {@linkplain MediaSession#setCustomLayout(List) set by
* the session}.
@ -126,7 +126,7 @@ public final class MediaNotification {
* been loaded asynchronously.
*/
MediaNotification createNotification(
MediaController mediaController,
MediaSession session,
ImmutableList<CommandButton> customLayout,
ActionFactory actionFactory,
Callback onNotificationChangedCallback);
@ -134,13 +134,15 @@ public final class MediaNotification {
/**
* Handles a notification's custom command.
*
* @param mediaController The controller of the session.
* @param session The media session.
* @param action The custom command action.
* @param extras A bundle {@linkplain SessionCommand#customExtras set in the custom command},
* otherwise {@link Bundle#EMPTY}.
* @return {@code false} if the action should be delivered to the session as a custom command or
* {@code true} if the action has been handled completely by the provider.
* @see ActionFactory#createCustomAction
*/
void handleCustomCommand(MediaController mediaController, String action, Bundle extras);
boolean handleCustomCommand(MediaSession session, String action, Bundle extras);
}
/** The notification id. */

View File

@ -32,8 +32,10 @@ import androidx.media3.common.Player;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -122,28 +124,44 @@ import java.util.concurrent.TimeoutException;
return;
}
try {
MediaController mediaController = controllerFuture.get(0, TimeUnit.MILLISECONDS);
mediaNotificationProvider.handleCustomCommand(mediaController, action, extras);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
MediaController mediaController = checkStateNotNull(Futures.getDone(controllerFuture));
if (!mediaNotificationProvider.handleCustomCommand(session, action, extras)) {
@Nullable SessionCommand customCommand = null;
for (SessionCommand command : mediaController.getAvailableSessionCommands().commands) {
if (command.commandCode == SessionCommand.COMMAND_CODE_CUSTOM
&& command.customAction.equals(action)) {
customCommand = command;
break;
}
}
if (customCommand != null
&& mediaController.getAvailableSessionCommands().contains(customCommand)) {
ListenableFuture<SessionResult> future =
mediaController.sendCustomCommand(customCommand, Bundle.EMPTY);
Futures.addCallback(
future,
new FutureCallback<SessionResult>() {
@Override
public void onSuccess(SessionResult result) {
// Do nothing.
}
@Override
public void onFailure(Throwable t) {
Log.w(
TAG, "custom command " + action + " produced an error: " + t.getMessage(), t);
}
},
MoreExecutors.directExecutor());
}
}
} catch (ExecutionException e) {
// We should never reach this.
throw new IllegalStateException(e);
}
}
public void updateNotification(MediaSession session) {
@Nullable ListenableFuture<MediaController> controllerFuture = controllerMap.get(session);
if (controllerFuture == null) {
return;
}
MediaController mediaController;
try {
mediaController = checkStateNotNull(Futures.getDone(controllerFuture));
} catch (ExecutionException e) {
// We should never reach this point.
throw new IllegalStateException(e);
}
if (!mediaSessionService.isSessionAdded(session) || !canStartPlayback(session.getPlayer())) {
maybeStopForegroundService(/* removeNotifications= */ true);
return;
@ -157,10 +175,7 @@ import java.util.concurrent.TimeoutException;
MediaNotification mediaNotification =
this.mediaNotificationProvider.createNotification(
mediaController,
checkStateNotNull(customLayoutMap.get(session)),
actionFactory,
callback);
session, checkStateNotNull(customLayoutMap.get(session)), actionFactory, callback);
updateNotificationInternal(session, mediaNotification);
}

View File

@ -35,6 +35,7 @@ import android.view.KeyEvent;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import androidx.media.MediaSessionManager.RemoteUserInfo;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.DeviceInfo;
@ -247,6 +248,14 @@ public class MediaSession {
* Sets a {@link PendingIntent} to launch an {@link android.app.Activity} for the {@link
* MediaSession}. This can be used as a quick link to an ongoing media screen.
*
* <p>A client can use this pending intent to start an activity belonging to this session. When
* this pending intent is for instance included in the notification {@linkplain
* NotificationCompat.Builder#setContentIntent(PendingIntent) as the content intent}, tapping
* the notification will open this activity.
*
* <p>See <a href="https://developer.android.com/training/notify-user/navigation">'Start an
* Activity from a Notification'</a> also.
*
* @param pendingIntent The pending intent.
* @return The builder to allow chaining.
*/
@ -514,6 +523,17 @@ public class MediaSession {
return null;
}
/**
* Returns the {@link PendingIntent} to launch {@linkplain
* Builder#setSessionActivity(PendingIntent) the session activity} or null if not set.
*
* @return The {@link PendingIntent} to launch an activity belonging to the session.
*/
@Nullable
public PendingIntent getSessionActivity() {
return impl.getSessionActivity();
}
/**
* Sets the underlying {@link Player} for this session to dispatch incoming events to.
*