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.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -126,16 +125,17 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
@Override @Override
public final MediaNotification createNotification( public final MediaNotification createNotification(
MediaController mediaController, MediaSession mediaSession,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
MediaNotification.ActionFactory actionFactory, MediaNotification.ActionFactory actionFactory,
Callback onNotificationChangedCallback) { Callback onNotificationChangedCallback) {
ensureNotificationChannel(); ensureNotificationChannel();
Player player = mediaSession.getPlayer();
NotificationCompat.Builder builder = NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID); new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
// Set metadata info in the notification. // Set metadata info in the notification.
MediaMetadata metadata = mediaController.getMediaMetadata(); MediaMetadata metadata = player.getMediaMetadata();
builder.setContentTitle(metadata.title).setContentText(metadata.artist); builder.setContentTitle(metadata.title).setContentText(metadata.artist);
@Nullable ListenableFuture<Bitmap> bitmapFuture = loadArtworkBitmap(metadata); @Nullable ListenableFuture<Bitmap> bitmapFuture = loadArtworkBitmap(metadata);
@ -164,19 +164,16 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
MediaStyle mediaStyle = new MediaStyle(); MediaStyle mediaStyle = new MediaStyle();
int[] compactViewIndices = int[] compactViewIndices =
addNotificationActions( addNotificationActions(
getMediaButtons( getMediaButtons(player.getAvailableCommands(), customLayout, player.getPlayWhenReady()),
mediaController.getAvailableCommands(),
customLayout,
mediaController.getPlayWhenReady()),
builder, builder,
actionFactory); actionFactory);
mediaStyle.setShowActionsInCompactView(compactViewIndices); 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. // We must include a cancel intent for pre-L devices.
mediaStyle.setCancelButtonIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP)); mediaStyle.setCancelButtonIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP));
} }
long playbackStartTimeMs = getPlaybackStartTimeEpochMs(mediaController); long playbackStartTimeMs = getPlaybackStartTimeEpochMs(player);
boolean displayElapsedTimeWithChronometer = playbackStartTimeMs != C.TIME_UNSET; boolean displayElapsedTimeWithChronometer = playbackStartTimeMs != C.TIME_UNSET;
builder builder
.setWhen(playbackStartTimeMs) .setWhen(playbackStartTimeMs)
@ -185,7 +182,7 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
Notification notification = Notification notification =
builder builder
.setContentIntent(mediaController.getSessionActivity()) .setContentIntent(mediaSession.getSessionActivity())
.setDeleteIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP)) .setDeleteIntent(actionFactory.createMediaActionPendingIntent(COMMAND_STOP))
.setOnlyAlertOnce(true) .setOnlyAlertOnce(true)
.setSmallIcon(getSmallIconResId(context)) .setSmallIcon(getSmallIconResId(context))
@ -197,34 +194,9 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
} }
@Override @Override
public final void handleCustomCommand( public final boolean handleCustomCommand(MediaSession session, String action, Bundle extras) {
MediaController mediaController, String action, Bundle extras) { // Make the custom action being delegated to the session as a custom session command.
@Nullable SessionCommand customCommand = null; return false;
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());
}
} }
/** /**
@ -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. // Changing "showWhen" causes notification flicker if SDK_INT < 21.
if (Util.SDK_INT >= 21 if (Util.SDK_INT >= 21
&& controller.isPlaying() && player.isPlaying()
&& !controller.isPlayingAd() && !player.isPlayingAd()
&& !controller.isCurrentMediaItemDynamic() && !player.isCurrentMediaItemDynamic()
&& controller.getPlaybackParameters().speed == 1f) { && player.getPlaybackParameters().speed == 1f) {
return System.currentTimeMillis() - controller.getContentPosition(); return System.currentTimeMillis() - player.getContentPosition();
} else { } else {
return C.TIME_UNSET; 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 * <p>The returned {@link NotificationCompat.Action} will have a {@link PendingIntent} with the
* extras from {@link SessionCommand#customExtras}. Accordingly the {@linkplain * extras from {@link SessionCommand#customExtras}. Accordingly the {@linkplain
* SessionCommand#customExtras command's extras} will be passed to {@link * 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}. * @param customCommandButton A {@linkplain CommandButton custom command button}.
* @see MediaNotification.Provider#handleCustomCommand * @see MediaNotification.Provider#handleCustomCommand
@ -116,8 +116,8 @@ public final class MediaNotification {
/** /**
* Creates a new {@link MediaNotification}. * Creates a new {@link MediaNotification}.
* *
* @param mediaController The controller of the session. * @param session The media session.
* @param actionFactory The {@link ActionFactory} for creating notification {@linkplain * @param actionFactory The {@link ActionFactory} for creating notification {@link
* NotificationCompat.Action actions}. * NotificationCompat.Action actions}.
* @param customLayout The custom layout {@linkplain MediaSession#setCustomLayout(List) set by * @param customLayout The custom layout {@linkplain MediaSession#setCustomLayout(List) set by
* the session}. * the session}.
@ -126,7 +126,7 @@ public final class MediaNotification {
* been loaded asynchronously. * been loaded asynchronously.
*/ */
MediaNotification createNotification( MediaNotification createNotification(
MediaController mediaController, MediaSession session,
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
ActionFactory actionFactory, ActionFactory actionFactory,
Callback onNotificationChangedCallback); Callback onNotificationChangedCallback);
@ -134,13 +134,15 @@ public final class MediaNotification {
/** /**
* Handles a notification's custom command. * 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 action The custom command action.
* @param extras A bundle {@linkplain SessionCommand#customExtras set in the custom command}, * @param extras A bundle {@linkplain SessionCommand#customExtras set in the custom command},
* otherwise {@link Bundle#EMPTY}. * 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 * @see ActionFactory#createCustomAction
*/ */
void handleCustomCommand(MediaController mediaController, String action, Bundle extras); boolean handleCustomCommand(MediaSession session, String action, Bundle extras);
} }
/** The notification id. */ /** 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.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -122,28 +124,44 @@ import java.util.concurrent.TimeoutException;
return; return;
} }
try { try {
MediaController mediaController = controllerFuture.get(0, TimeUnit.MILLISECONDS); MediaController mediaController = checkStateNotNull(Futures.getDone(controllerFuture));
mediaNotificationProvider.handleCustomCommand(mediaController, action, extras); if (!mediaNotificationProvider.handleCustomCommand(session, action, extras)) {
} catch (InterruptedException | ExecutionException | TimeoutException e) { @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. // We should never reach this.
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
} }
public void updateNotification(MediaSession session) { 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())) { if (!mediaSessionService.isSessionAdded(session) || !canStartPlayback(session.getPlayer())) {
maybeStopForegroundService(/* removeNotifications= */ true); maybeStopForegroundService(/* removeNotifications= */ true);
return; return;
@ -157,10 +175,7 @@ import java.util.concurrent.TimeoutException;
MediaNotification mediaNotification = MediaNotification mediaNotification =
this.mediaNotificationProvider.createNotification( this.mediaNotificationProvider.createNotification(
mediaController, session, checkStateNotNull(customLayoutMap.get(session)), actionFactory, callback);
checkStateNotNull(customLayoutMap.get(session)),
actionFactory,
callback);
updateNotificationInternal(session, mediaNotification); updateNotificationInternal(session, mediaNotification);
} }

View File

@ -35,6 +35,7 @@ import android.view.KeyEvent;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media.MediaSessionManager.RemoteUserInfo;
import androidx.media3.common.AudioAttributes; import androidx.media3.common.AudioAttributes;
import androidx.media3.common.DeviceInfo; 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 * 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. * 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. * @param pendingIntent The pending intent.
* @return The builder to allow chaining. * @return The builder to allow chaining.
*/ */
@ -514,6 +523,17 @@ public class MediaSession {
return null; 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. * Sets the underlying {@link Player} for this session to dispatch incoming events to.
* *