Add injection of BitmapLoader from MediaSession.

* Add `BitmapLoader` in `MediaSession.Builder` and `MediaLibrarySession.Builder`.
* Pass `BitmapLoader` into the constructor of `MediaSession`, `MediaSessionImpl`, `MediaLibrarySession` and `MediaLibrarySessionImpl`.
* Add an interface method `loadBitmapFromMetadata(MediaMetadata)` in `BitmapLoader`.
* Remove the reference of `BitmapLoader` in `DefaultMediaNotificationProvider`.

PiperOrigin-RevId: 483654596
(cherry picked from commit 3f69df72db)
This commit is contained in:
tianyifeng 2022-10-25 13:21:08 +00:00 committed by microkatz
parent 373c23c11b
commit 88a413b2cb
8 changed files with 207 additions and 50 deletions

View file

@ -21,6 +21,7 @@ import static org.junit.Assert.assertThrows;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import androidx.media3.common.MediaMetadata;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -166,6 +167,64 @@ public class SimpleBitmapLoaderTest {
/* messagePart= */ "unknown protocol");
}
@Test
public void loadBitmapFromMetadata_decodeFromArtworkData() throws Exception {
byte[] imageData =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH);
MockWebServer mockWebServer = new MockWebServer();
Uri uri = Uri.parse(mockWebServer.url("test_path").toString());
// Set both artworkData and artworkUri
MediaMetadata metadata =
new MediaMetadata.Builder()
.setArtworkData(imageData, MediaMetadata.PICTURE_TYPE_FRONT_COVER)
.setArtworkUri(uri)
.build();
SimpleBitmapLoader bitmapLoader =
new SimpleBitmapLoader(MoreExecutors.newDirectExecutorService());
Bitmap bitmap = bitmapLoader.loadBitmapFromMetadata(metadata).get();
assertThat(
bitmap.sameAs(
BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length)))
.isTrue();
assertThat(mockWebServer.getRequestCount()).isEqualTo(0);
}
@Test
public void loadBitmapFromMetadata_loadFromArtworkUri() throws Exception {
byte[] imageData =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH);
MockWebServer mockWebServer = new MockWebServer();
Buffer responseBody = new Buffer().write(imageData);
mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody));
Uri uri = Uri.parse(mockWebServer.url("test_path").toString());
// Just set artworkUri
MediaMetadata metadata = new MediaMetadata.Builder().setArtworkUri(uri).build();
SimpleBitmapLoader bitmapLoader =
new SimpleBitmapLoader(MoreExecutors.newDirectExecutorService());
Bitmap bitmap = bitmapLoader.loadBitmapFromMetadata(metadata).get();
assertThat(
bitmap.sameAs(
BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length)))
.isTrue();
assertThat(mockWebServer.getRequestCount()).isEqualTo(1);
}
@Test
public void loadBitmapFromMetadata_returnNull() throws Exception {
// Neither artworkData nor artworkUri is set
MediaMetadata metadata = new MediaMetadata.Builder().build();
SimpleBitmapLoader bitmapLoader =
new SimpleBitmapLoader(MoreExecutors.newDirectExecutorService());
ListenableFuture<Bitmap> bitmapFuture = bitmapLoader.loadBitmapFromMetadata(metadata);
assertThat(bitmapFuture).isNull();
}
private static void assertException(
ThrowingRunnable runnable, Class<? extends Exception> clazz, String messagePart) {
ExecutionException executionException = assertThrows(ExecutionException.class, runnable);

View file

@ -17,6 +17,8 @@ package androidx.media3.session;
import android.graphics.Bitmap;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.util.UnstableApi;
import com.google.common.util.concurrent.ListenableFuture;
@ -25,6 +27,29 @@ import com.google.common.util.concurrent.ListenableFuture;
public interface BitmapLoader {
/** Decodes an image from compressed binary data. */
ListenableFuture<Bitmap> decodeBitmap(byte[] data);
/** Loads an image from {@code uri}. */
ListenableFuture<Bitmap> loadBitmap(Uri uri);
/**
* Loads an image from {@link MediaMetadata}. Returns null if {@code metadata} doesn't contain
* bitmap information.
*
* <p>By default, the method will try to decode an image from {@link MediaMetadata#artworkData} if
* it is present. Otherwise, the method will try to load an image from {@link
* MediaMetadata#artworkUri} if it is present. The method will return null if neither {@link
* MediaMetadata#artworkData} nor {@link MediaMetadata#artworkUri} is present.
*/
@Nullable
default ListenableFuture<Bitmap> loadBitmapFromMetadata(MediaMetadata metadata) {
@Nullable ListenableFuture<Bitmap> future;
if (metadata.artworkData != null) {
future = decodeBitmap(metadata.artworkData);
} else if (metadata.artworkUri != null) {
future = loadBitmap(metadata.artworkUri);
} else {
future = null;
}
return future;
}
}

View file

@ -123,7 +123,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
private NotificationIdProvider notificationIdProvider;
private String channelId;
@StringRes private int channelNameResourceId;
private BitmapLoader bitmapLoader;
private boolean built;
/**
@ -136,7 +135,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
notificationIdProvider = session -> DEFAULT_NOTIFICATION_ID;
channelId = DEFAULT_CHANNEL_ID;
channelNameResourceId = DEFAULT_CHANNEL_NAME_RESOURCE_ID;
bitmapLoader = new SimpleBitmapLoader();
}
/**
@ -196,19 +194,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
return this;
}
/**
* Sets the {@link BitmapLoader} used load artwork. By default, a {@link CacheBitmapLoader} with
* a {@link SimpleBitmapLoader} inside will be used.
*
* @param bitmapLoader The bitmap loader.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setBitmapLoader(BitmapLoader bitmapLoader) {
this.bitmapLoader = new CacheBitmapLoader(bitmapLoader);
return this;
}
/**
* Builds the {@link DefaultMediaNotificationProvider}. The method can be called at most once.
*/
@ -259,7 +244,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
private final String channelId;
@StringRes private final int channelNameResourceId;
private final NotificationManager notificationManager;
private final BitmapLoader bitmapLoader;
// Cache the last bitmap load request to avoid reloading the bitmap again, particularly useful
// when showing a notification for the same item (e.g. when switching from playing to paused).
private final Handler mainHandler;
@ -272,7 +256,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
this.notificationIdProvider = builder.notificationIdProvider;
this.channelId = builder.channelId;
this.channelNameResourceId = builder.channelNameResourceId;
this.bitmapLoader = new CacheBitmapLoader(builder.bitmapLoader);
notificationManager =
checkStateNotNull(
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
@ -312,7 +295,9 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
builder
.setContentTitle(getNotificationContentTitle(metadata))
.setContentText(getNotificationContentText(metadata));
@Nullable ListenableFuture<Bitmap> bitmapFuture = loadArtworkBitmap(metadata);
@Nullable
ListenableFuture<Bitmap> bitmapFuture =
mediaSession.getBitmapLoader().loadBitmapFromMetadata(metadata);
if (bitmapFuture != null) {
if (pendingOnBitmapLoadedFutureCallback != null) {
pendingOnBitmapLoadedFutureCallback.discardIfPending();
@ -578,23 +563,6 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
notificationManager, channelId, context.getString(channelNameResourceId));
}
/**
* Requests from the bitmapLoader to load artwork or returns null if the metadata don't include
* artwork.
*/
@Nullable
private ListenableFuture<Bitmap> loadArtworkBitmap(MediaMetadata metadata) {
@Nullable ListenableFuture<Bitmap> future;
if (metadata.artworkData != null) {
future = bitmapLoader.decodeBitmap(metadata.artworkData);
} else if (metadata.artworkUri != null) {
future = bitmapLoader.loadBitmap(metadata.artworkUri);
} else {
future = null;
}
return future;
}
private static long getPlaybackStartTimeEpochMs(Player player) {
// Changing "showWhen" causes notification flicker if SDK_INT < 21.
if (Util.SDK_INT >= 21

View file

@ -24,6 +24,7 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import androidx.annotation.IntDef;
@ -420,6 +421,28 @@ public abstract class MediaLibraryService extends MediaSessionService {
return super.setExtras(extras);
}
/**
* Sets a {@link BitmapLoader} for the {@link MediaLibrarySession} to decode bitmaps from
* compressed binary data or load bitmaps from {@link Uri}. If not set, a {@link
* CacheBitmapLoader} with a {@link SimpleBitmapLoader} inside will be used.
*
* <p>The provided instance will likely be called repeatedly with the same request, so it
* would be best if any provided instance does some caching. Simple caching can be added to
* any {@link BitmapLoader} implementation by wrapping it in {@link CacheBitmapLoader} before
* passing it to this method.
*
* <p>If no instance is set, a {@link CacheBitmapLoader} with a {@link SimpleBitmapLoader}
* inside will be used.
*
* @param bitmapLoader The bitmap loader {@link BitmapLoader}.
* @return The builder to allow chaining.
*/
@UnstableApi
@Override
public Builder setBitmapLoader(BitmapLoader bitmapLoader) {
return super.setBitmapLoader(bitmapLoader);
}
/**
* Builds a {@link MediaLibrarySession}.
*
@ -429,7 +452,11 @@ public abstract class MediaLibraryService extends MediaSessionService {
*/
@Override
public MediaLibrarySession build() {
return new MediaLibrarySession(context, id, player, sessionActivity, callback, extras);
if (bitmapLoader == null) {
bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader());
}
return new MediaLibrarySession(
context, id, player, sessionActivity, callback, extras, checkNotNull(bitmapLoader));
}
}
@ -439,8 +466,9 @@ public abstract class MediaLibraryService extends MediaSessionService {
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
Bundle tokenExtras) {
super(context, id, player, sessionActivity, callback, tokenExtras);
Bundle tokenExtras,
BitmapLoader bitmapLoader) {
super(context, id, player, sessionActivity, callback, tokenExtras, bitmapLoader);
}
@Override
@ -450,9 +478,17 @@ public abstract class MediaLibraryService extends MediaSessionService {
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
Bundle tokenExtras) {
Bundle tokenExtras,
BitmapLoader bitmapLoader) {
return new MediaLibrarySessionImpl(
this, context, id, player, sessionActivity, (Callback) callback, tokenExtras);
this,
context,
id,
player,
sessionActivity,
(Callback) callback,
tokenExtras,
bitmapLoader);
}
@Override

View file

@ -64,8 +64,9 @@ import java.util.concurrent.Future;
Player player,
@Nullable PendingIntent sessionActivity,
MediaLibrarySession.Callback callback,
Bundle tokenExtras) {
super(instance, context, id, player, sessionActivity, callback, tokenExtras);
Bundle tokenExtras,
BitmapLoader bitmapLoader) {
super(instance, context, id, player, sessionActivity, callback, tokenExtras, bitmapLoader);
this.instance = instance;
this.callback = callback;
subscriptions = new ArrayMap<>();

View file

@ -61,6 +61,7 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.HashMap;
import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A session that allows a media app to expose its transport controls and playback information in a
@ -307,6 +308,28 @@ public class MediaSession {
return super.setExtras(extras);
}
/**
* Sets a {@link BitmapLoader} for the {@link MediaSession} to decode bitmaps from compressed
* binary data or load bitmaps from {@link Uri}. If not set, a {@link CacheBitmapLoader} with a
* {@link SimpleBitmapLoader} inside will be used.
*
* <p>The provided instance will likely be called repeatedly with the same request, so it would
* be best if any provided instance does some caching. Simple caching can be added to any {@link
* BitmapLoader} implementation by wrapping it in {@link CacheBitmapLoader} before passing it to
* this method.
*
* <p>If no instance is set, a {@link CacheBitmapLoader} with a {@link SimpleBitmapLoader}
* inside will be used.
*
* @param bitmapLoader The bitmap loader {@link BitmapLoader}.
* @return The builder to allow chaining.
*/
@UnstableApi
@Override
public Builder setBitmapLoader(BitmapLoader bitmapLoader) {
return super.setBitmapLoader(bitmapLoader);
}
/**
* Builds a {@link MediaSession}.
*
@ -316,7 +339,11 @@ public class MediaSession {
*/
@Override
public MediaSession build() {
return new MediaSession(context, id, player, sessionActivity, callback, extras);
if (bitmapLoader == null) {
bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader());
}
return new MediaSession(
context, id, player, sessionActivity, callback, extras, checkNotNull(bitmapLoader));
}
}
@ -487,14 +514,15 @@ public class MediaSession {
Player player,
@Nullable PendingIntent sessionActivity,
Callback callback,
Bundle tokenExtras) {
Bundle tokenExtras,
BitmapLoader bitmapLoader) {
synchronized (STATIC_LOCK) {
if (SESSION_ID_TO_SESSION_MAP.containsKey(id)) {
throw new IllegalStateException("Session ID must be unique. ID=" + id);
}
SESSION_ID_TO_SESSION_MAP.put(id, this);
}
impl = createImpl(context, id, player, sessionActivity, callback, tokenExtras);
impl = createImpl(context, id, player, sessionActivity, callback, tokenExtras, bitmapLoader);
}
/* package */ MediaSessionImpl createImpl(
@ -503,8 +531,10 @@ public class MediaSession {
Player player,
@Nullable PendingIntent sessionActivity,
Callback callback,
Bundle tokenExtras) {
return new MediaSessionImpl(this, context, id, player, sessionActivity, callback, tokenExtras);
Bundle tokenExtras,
BitmapLoader bitmapLoader) {
return new MediaSessionImpl(
this, context, id, player, sessionActivity, callback, tokenExtras, bitmapLoader);
}
/* package */ MediaSessionImpl getImpl() {
@ -741,6 +771,12 @@ public class MediaSession {
impl.setSessionExtras(controller, sessionExtras);
}
/** Returns the {@link BitmapLoader}. */
@UnstableApi
public BitmapLoader getBitmapLoader() {
return impl.getBitmapLoader();
}
/**
* Sends a custom command to a specific controller.
*
@ -1218,6 +1254,7 @@ public class MediaSession {
/* package */ C callback;
/* package */ @Nullable PendingIntent sessionActivity;
/* package */ Bundle extras;
/* package */ @MonotonicNonNull BitmapLoader bitmapLoader;
public BuilderBase(Context context, Player player, C callback) {
this.context = checkNotNull(context);
@ -1252,6 +1289,12 @@ public class MediaSession {
return (U) this;
}
@SuppressWarnings("unchecked")
public U setBitmapLoader(BitmapLoader bitmapLoader) {
this.bitmapLoader = bitmapLoader;
return (U) this;
}
public abstract T build();
}
}

View file

@ -116,6 +116,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
private final PendingIntent mediaButtonIntent;
@Nullable private final BroadcastReceiver broadcastReceiver;
private final Handler applicationHandler;
private final BitmapLoader bitmapLoader;
@Nullable private PlayerListener playerListener;
@ -139,7 +140,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
Bundle tokenExtras) {
Bundle tokenExtras,
BitmapLoader bitmapLoader) {
this.context = context;
this.instance = instance;
@ -152,6 +154,7 @@ import org.checkerframework.checker.initialization.qual.Initialized;
applicationHandler = new Handler(player.getApplicationLooper());
this.callback = callback;
this.bitmapLoader = bitmapLoader;
playerInfo = PlayerInfo.DEFAULT;
onPlayerInfoChangedHandler = new PlayerInfoChangedHandler(player.getApplicationLooper());
@ -357,6 +360,10 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
}
public BitmapLoader getBitmapLoader() {
return bitmapLoader;
}
public void setAvailableCommands(
ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) {
if (sessionStub.getConnectedControllersManager().isConnected(controller)) {

View file

@ -418,10 +418,10 @@ public class DefaultMediaNotificationProviderTest {
new DefaultActionFactory(Robolectric.setupService(TestService.class));
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
SettableFuture<Bitmap> bitmapFuture = SettableFuture.create();
when(mockBitmapLoader.loadBitmap(any())).thenReturn(bitmapFuture);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(bitmapFuture);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader);
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.setBitmapLoader(mockBitmapLoader)
.build();
// Ask the notification provider to create a notification twice. Use separate callback instances
@ -456,6 +456,9 @@ public class DefaultMediaNotificationProviderTest {
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(context).build();
MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
@ -487,6 +490,9 @@ public class DefaultMediaNotificationProviderTest {
.setChannelName(/* channelNameResourceId= */ R.string.media3_controls_play_description)
.build();
MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
@ -521,6 +527,9 @@ public class DefaultMediaNotificationProviderTest {
.setChannelName(/* channelNameResourceId= */ R.string.media3_controls_play_description)
.build();
MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
@ -542,6 +551,9 @@ public class DefaultMediaNotificationProviderTest {
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mockMediaSession = createMockMediaSessionForNotification(MediaMetadata.EMPTY);
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader);
MediaNotification notification =
defaultMediaNotificationProvider.createNotification(
@ -574,6 +586,9 @@ public class DefaultMediaNotificationProviderTest {
MediaSession mockMediaSession =
createMockMediaSessionForNotification(
new MediaMetadata.Builder().setTitle("title").build());
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader);
MediaNotification notification =
defaultMediaNotificationProvider.createNotification(
@ -597,6 +612,9 @@ public class DefaultMediaNotificationProviderTest {
MediaSession mockMediaSession =
createMockMediaSessionForNotification(
new MediaMetadata.Builder().setArtist("artist").build());
BitmapLoader mockBitmapLoader = mock(BitmapLoader.class);
when(mockBitmapLoader.loadBitmapFromMetadata(any())).thenReturn(null);
when(mockMediaSession.getBitmapLoader()).thenReturn(mockBitmapLoader);
MediaNotification notification =
defaultMediaNotificationProvider.createNotification(