Add BitmapLoader injection in MediaController

Also clean up the strict mode violations of using `BitmapFactory.convertToByteArray` on the main thread.

PiperOrigin-RevId: 496422355
This commit is contained in:
tianyifeng 2022-12-19 17:43:50 +00:00 committed by Tianyi Feng
parent 0744a52b8d
commit d848d3358a
6 changed files with 200 additions and 40 deletions

View File

@ -32,6 +32,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.util.Consumer;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaLibraryService.LibraryParams;
import com.google.common.collect.ImmutableList;
@ -57,6 +58,7 @@ public final class MediaBrowser extends MediaController {
private Bundle connectionHints;
private Listener listener;
private Looper applicationLooper;
private @MonotonicNonNull BitmapLoader bitmapLoader;
/**
* Creates a builder for {@link MediaBrowser}.
@ -121,6 +123,21 @@ public final class MediaBrowser extends MediaController {
return this;
}
/**
* Sets a {@link BitmapLoader} for the {@link MediaBrowser} to decode bitmaps from compressed
* binary data. If not set, a {@link CacheBitmapLoader} that wraps a {@link SimpleBitmapLoader}
* will be used.
*
* @param bitmapLoader The bitmap loader.
* @return The builder to allow chaining.
*/
@UnstableApi
@CanIgnoreReturnValue
public Builder setBitmapLoader(BitmapLoader bitmapLoader) {
this.bitmapLoader = checkNotNull(bitmapLoader);
return this;
}
/**
* Builds a {@link MediaBrowser} asynchronously.
*
@ -149,8 +166,12 @@ public final class MediaBrowser extends MediaController {
*/
public ListenableFuture<MediaBrowser> buildAsync() {
MediaControllerHolder<MediaBrowser> holder = new MediaControllerHolder<>(applicationLooper);
if (token.isLegacySession() && bitmapLoader == null) {
bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader());
}
MediaBrowser browser =
new MediaBrowser(context, token, connectionHints, listener, applicationLooper, holder);
new MediaBrowser(
context, token, connectionHints, listener, applicationLooper, holder, bitmapLoader);
postOrRun(new Handler(applicationLooper), () -> holder.setController(browser));
return holder;
}
@ -215,8 +236,16 @@ public final class MediaBrowser extends MediaController {
Bundle connectionHints,
Listener listener,
Looper applicationLooper,
ConnectionCallback connectionCallback) {
super(context, token, connectionHints, listener, applicationLooper, connectionCallback);
ConnectionCallback connectionCallback,
@Nullable BitmapLoader bitmapLoader) {
super(
context,
token,
connectionHints,
listener,
applicationLooper,
connectionCallback,
bitmapLoader);
}
@Override
@ -226,10 +255,13 @@ public final class MediaBrowser extends MediaController {
Context context,
SessionToken token,
Bundle connectionHints,
Looper applicationLooper) {
Looper applicationLooper,
@Nullable BitmapLoader bitmapLoader) {
MediaBrowserImpl impl;
if (token.isLegacySession()) {
impl = new MediaBrowserImplLegacy(context, this, token, applicationLooper);
impl =
new MediaBrowserImplLegacy(
context, this, token, applicationLooper, checkNotNull(bitmapLoader));
} else {
impl = new MediaBrowserImplBase(context, this, token, connectionHints, applicationLooper);
}

View File

@ -57,8 +57,9 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization;
Context context,
@UnderInitialization MediaBrowser instance,
SessionToken token,
Looper applicationLooper) {
super(context, instance, token, applicationLooper);
Looper applicationLooper,
BitmapLoader bitmapLoader) {
super(context, instance, token, applicationLooper, bitmapLoader);
this.instance = instance;
}

View File

@ -67,6 +67,7 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import org.checkerframework.checker.initialization.qual.NotOnlyInitialized;
import org.checkerframework.checker.initialization.qual.UnderInitialization;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A controller that interacts with a {@link MediaSession}, a {@link MediaSessionService} hosting a
@ -183,6 +184,7 @@ public class MediaController implements Player {
private Bundle connectionHints;
private Listener listener;
private Looper applicationLooper;
private @MonotonicNonNull BitmapLoader bitmapLoader;
/**
* Creates a builder for {@link MediaController}.
@ -261,6 +263,21 @@ public class MediaController implements Player {
return this;
}
/**
* Sets a {@link BitmapLoader} for the {@link MediaController} to decode bitmaps from compressed
* binary data. If not set, a {@link CacheBitmapLoader} that wraps a {@link SimpleBitmapLoader}
* will be used.
*
* @param bitmapLoader The bitmap loader.
* @return The builder to allow chaining.
*/
@UnstableApi
@CanIgnoreReturnValue
public Builder setBitmapLoader(BitmapLoader bitmapLoader) {
this.bitmapLoader = checkNotNull(bitmapLoader);
return this;
}
/**
* Builds a {@link MediaController} asynchronously.
*
@ -290,8 +307,12 @@ public class MediaController implements Player {
public ListenableFuture<MediaController> buildAsync() {
MediaControllerHolder<MediaController> holder =
new MediaControllerHolder<>(applicationLooper);
if (token.isLegacySession() && bitmapLoader == null) {
bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader());
}
MediaController controller =
new MediaController(context, token, connectionHints, listener, applicationLooper, holder);
new MediaController(
context, token, connectionHints, listener, applicationLooper, holder, bitmapLoader);
postOrRun(new Handler(applicationLooper), () -> holder.setController(controller));
return holder;
}
@ -404,7 +425,8 @@ public class MediaController implements Player {
Bundle connectionHints,
Listener listener,
Looper applicationLooper,
ConnectionCallback connectionCallback) {
ConnectionCallback connectionCallback,
@Nullable BitmapLoader bitmapLoader) {
checkNotNull(context, "context must not be null");
checkNotNull(token, "token must not be null");
@ -417,7 +439,7 @@ public class MediaController implements Player {
applicationHandler = new Handler(applicationLooper);
this.connectionCallback = connectionCallback;
impl = createImpl(context, token, connectionHints, applicationLooper);
impl = createImpl(context, token, connectionHints, applicationLooper, bitmapLoader);
impl.connect();
}
@ -427,9 +449,11 @@ public class MediaController implements Player {
Context context,
SessionToken token,
Bundle connectionHints,
Looper applicationLooper) {
Looper applicationLooper,
@Nullable BitmapLoader bitmapLoader) {
if (token.isLegacySession()) {
return new MediaControllerImplLegacy(context, this, token, applicationLooper);
return new MediaControllerImplLegacy(
context, this, token, applicationLooper, checkNotNull(bitmapLoader));
} else {
return new MediaControllerImplBase(context, this, token, connectionHints, applicationLooper);
}

View File

@ -25,6 +25,7 @@ import static java.lang.Math.min;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
@ -76,7 +77,10 @@ import com.google.common.util.concurrent.SettableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import org.checkerframework.checker.initialization.qual.UnderInitialization;
import org.checkerframework.checker.nullness.compatqual.NullableType;
@ -93,6 +97,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private final SessionToken token;
private final ListenerSet<Listener> listeners;
private final ControllerCompatCallback controllerCompatCallback;
private final BitmapLoader bitmapLoader;
@Nullable private MediaControllerCompat controllerCompat;
@Nullable private MediaBrowserCompat browserCompat;
@ -106,7 +111,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
Context context,
@UnderInitialization MediaController instance,
SessionToken token,
Looper applicationLooper) {
Looper applicationLooper,
BitmapLoader bitmapLoader) {
// Initialize default values.
legacyPlayerInfo = new LegacyPlayerInfo();
pendingLegacyPlayerInfo = new LegacyPlayerInfo();
@ -122,6 +128,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
this.instance = instance;
controllerCompatCallback = new ControllerCompatCallback(applicationLooper);
this.token = token;
this.bitmapLoader = bitmapLoader;
}
/* package */ MediaController getInstance() {
@ -716,11 +723,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* mediaItemTransitionReason= */ null);
if (isPrepared()) {
for (int i = 0; i < mediaItems.size(); i++) {
MediaItem mediaItem = mediaItems.get(i);
controllerCompat.addQueueItem(
MediaUtils.convertToMediaDescriptionCompat(mediaItem), index + i);
}
addQueueItems(mediaItems, index);
}
}
@ -1340,15 +1343,61 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
// Add all other items to the playlist if supported.
if (getAvailableCommands().contains(Player.COMMAND_CHANGE_MEDIA_ITEMS)) {
List<MediaItem> adjustedMediaItems = new ArrayList<>();
for (int i = 0; i < queueTimeline.getWindowCount(); i++) {
if (i == currentIndex || queueTimeline.getQueueId(i) != QueueItem.UNKNOWN_ID) {
// Skip the current item (added above) and all items already known to the session.
continue;
}
MediaItem mediaItem = queueTimeline.getWindow(/* windowIndex= */ i, window).mediaItem;
controllerCompat.addQueueItem(
MediaUtils.convertToMediaDescriptionCompat(mediaItem), /* index= */ i);
adjustedMediaItems.add(queueTimeline.getWindow(/* windowIndex= */ i, window).mediaItem);
}
addQueueItems(adjustedMediaItems, /* startIndex= */ 0);
}
}
private void addQueueItems(List<MediaItem> mediaItems, int startIndex) {
List<@NullableType ListenableFuture<Bitmap>> bitmapFutures = new ArrayList<>();
final AtomicInteger resultCount = new AtomicInteger(0);
Runnable handleBitmapFuturesTask =
() -> {
int completedBitmapFutureCount = resultCount.incrementAndGet();
if (completedBitmapFutureCount == mediaItems.size()) {
handleBitmapFuturesAllCompletedAndAddQueueItems(
bitmapFutures, mediaItems, /* startIndex= */ startIndex);
}
};
for (int i = 0; i < mediaItems.size(); i++) {
MediaItem mediaItem = mediaItems.get(i);
MediaMetadata metadata = mediaItem.mediaMetadata;
if (metadata.artworkData == null) {
bitmapFutures.add(null);
handleBitmapFuturesTask.run();
} else {
ListenableFuture<Bitmap> bitmapFuture = bitmapLoader.decodeBitmap(metadata.artworkData);
bitmapFutures.add(bitmapFuture);
bitmapFuture.addListener(handleBitmapFuturesTask, getInstance().applicationHandler::post);
}
}
}
private void handleBitmapFuturesAllCompletedAndAddQueueItems(
List<@NullableType ListenableFuture<Bitmap>> bitmapFutures,
List<MediaItem> mediaItems,
int startIndex) {
for (int i = 0; i < bitmapFutures.size(); i++) {
@Nullable ListenableFuture<Bitmap> future = bitmapFutures.get(i);
@Nullable Bitmap bitmap = null;
if (future != null) {
try {
bitmap = Futures.getDone(future);
} catch (CancellationException | ExecutionException e) {
Log.d(TAG, "Failed to get bitmap");
}
}
controllerCompat.addQueueItem(
MediaUtils.convertToMediaDescriptionCompat(mediaItems.get(i), bitmap),
/* index= */ startIndex + i);
}
}

View File

@ -46,7 +46,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
@ -311,23 +310,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return result;
}
/**
* Converts a {@link MediaItem} to a {@link MediaDescriptionCompat}.
*
* @deprecated Use {@link #convertToMediaDescriptionCompat(MediaItem, Bitmap)} instead.
*/
@Deprecated
public static MediaDescriptionCompat convertToMediaDescriptionCompat(MediaItem item) {
MediaMetadata metadata = item.mediaMetadata;
@Nullable Bitmap artworkBitmap = null;
if (metadata.artworkData != null) {
artworkBitmap =
BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length);
}
return convertToMediaDescriptionCompat(item, artworkBitmap);
}
/** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat} */
public static MediaDescriptionCompat convertToMediaDescriptionCompat(
MediaItem item, @Nullable Bitmap artworkBitmap) {

View File

@ -37,6 +37,7 @@ import android.support.v4.media.session.PlaybackStateCompat.RepeatMode;
import android.support.v4.media.session.PlaybackStateCompat.ShuffleMode;
import androidx.media.AudioManagerCompat;
import androidx.media.VolumeProviderCompat;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
@ -52,6 +53,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@ -327,7 +329,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
@Test
public void addMediaItems() throws Exception {
int size = 2;
List<MediaItem> testList = MediaTestUtils.createMediaItems(size);
List<MediaItem> testList = MediaTestUtils.createMediaItemsWithArtworkData(size);
List<QueueItem> testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList);
session.setQueue(testQueue);
@ -345,6 +347,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
assertThat(sessionCallback.queueIndices.get(i)).isEqualTo(testIndex + i);
assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getMediaId())
.isEqualTo(testList.get(i).mediaId);
assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getIconBitmap()).isNotNull();
}
}
@ -391,6 +394,75 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
assertThat(sessionCallback.onSkipToNextCalled).isTrue();
}
@Test
public void setMediaItems_nonEmptyList_startFromFirstMediaItem() throws Exception {
int size = 3;
List<MediaItem> testList = MediaTestUtils.createMediaItemsWithArtworkData(size);
session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(size);
controller.setMediaItems(testList);
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue();
assertThat(sessionCallback.mediaId).isEqualTo(testList.get(0).mediaId);
for (int i = 0; i < size - 1; i++) {
assertThat(sessionCallback.queueIndices.get(i)).isEqualTo(i);
assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getMediaId())
.isEqualTo(testList.get(i + 1).mediaId);
assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getIconBitmap()).isNotNull();
}
}
@Test
public void setMediaItems_nonEmptyList_startFromNonFirstMediaItem() throws Exception {
int size = 5;
List<MediaItem> testList = MediaTestUtils.createMediaItemsWithArtworkData(size);
session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(size);
int testStartIndex = 2;
controller.setMediaItems(testList, testStartIndex, /* startPositionMs= */ C.TIME_UNSET);
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue();
assertThat(sessionCallback.mediaId).isEqualTo(testList.get(testStartIndex).mediaId);
for (int i = 0; i < size - 1; i++) {
assertThat(sessionCallback.queueIndices.get(i)).isEqualTo(i);
int adjustedIndex = (i < testStartIndex) ? i : i + 1;
assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getMediaId())
.isEqualTo(testList.get(adjustedIndex).mediaId);
assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getIconBitmap()).isNotNull();
}
}
@Test
public void setMediaItems_emptyList() throws Exception {
int size = 3;
List<MediaItem> testList = MediaTestUtils.createMediaItems(size);
List<QueueItem> testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList);
session.setQueue(testQueue);
session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(size);
controller.setMediaItems(ImmutableList.of());
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
for (int i = 0; i < size; i++) {
assertThat(sessionCallback.queueDescriptionListForRemove.get(i).getMediaId())
.isEqualTo(testList.get(i).mediaId);
}
}
@Test
public void setShuffleMode() throws Exception {
session.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE);