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.MediaItem;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.util.Consumer; import androidx.media3.common.util.Consumer;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaLibraryService.LibraryParams;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -57,6 +58,7 @@ public final class MediaBrowser extends MediaController {
private Bundle connectionHints; private Bundle connectionHints;
private Listener listener; private Listener listener;
private Looper applicationLooper; private Looper applicationLooper;
private @MonotonicNonNull BitmapLoader bitmapLoader;
/** /**
* Creates a builder for {@link MediaBrowser}. * Creates a builder for {@link MediaBrowser}.
@ -121,6 +123,21 @@ public final class MediaBrowser extends MediaController {
return this; 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. * Builds a {@link MediaBrowser} asynchronously.
* *
@ -149,8 +166,12 @@ public final class MediaBrowser extends MediaController {
*/ */
public ListenableFuture<MediaBrowser> buildAsync() { public ListenableFuture<MediaBrowser> buildAsync() {
MediaControllerHolder<MediaBrowser> holder = new MediaControllerHolder<>(applicationLooper); MediaControllerHolder<MediaBrowser> holder = new MediaControllerHolder<>(applicationLooper);
if (token.isLegacySession() && bitmapLoader == null) {
bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader());
}
MediaBrowser browser = 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)); postOrRun(new Handler(applicationLooper), () -> holder.setController(browser));
return holder; return holder;
} }
@ -215,8 +236,16 @@ public final class MediaBrowser extends MediaController {
Bundle connectionHints, Bundle connectionHints,
Listener listener, Listener listener,
Looper applicationLooper, Looper applicationLooper,
ConnectionCallback connectionCallback) { ConnectionCallback connectionCallback,
super(context, token, connectionHints, listener, applicationLooper, connectionCallback); @Nullable BitmapLoader bitmapLoader) {
super(
context,
token,
connectionHints,
listener,
applicationLooper,
connectionCallback,
bitmapLoader);
} }
@Override @Override
@ -226,10 +255,13 @@ public final class MediaBrowser extends MediaController {
Context context, Context context,
SessionToken token, SessionToken token,
Bundle connectionHints, Bundle connectionHints,
Looper applicationLooper) { Looper applicationLooper,
@Nullable BitmapLoader bitmapLoader) {
MediaBrowserImpl impl; MediaBrowserImpl impl;
if (token.isLegacySession()) { if (token.isLegacySession()) {
impl = new MediaBrowserImplLegacy(context, this, token, applicationLooper); impl =
new MediaBrowserImplLegacy(
context, this, token, applicationLooper, checkNotNull(bitmapLoader));
} else { } else {
impl = new MediaBrowserImplBase(context, this, token, connectionHints, applicationLooper); impl = new MediaBrowserImplBase(context, this, token, connectionHints, applicationLooper);
} }

View File

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

View File

@ -67,6 +67,7 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; import org.checkerframework.checker.initialization.qual.NotOnlyInitialized;
import org.checkerframework.checker.initialization.qual.UnderInitialization; 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 * 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 Bundle connectionHints;
private Listener listener; private Listener listener;
private Looper applicationLooper; private Looper applicationLooper;
private @MonotonicNonNull BitmapLoader bitmapLoader;
/** /**
* Creates a builder for {@link MediaController}. * Creates a builder for {@link MediaController}.
@ -261,6 +263,21 @@ public class MediaController implements Player {
return this; 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. * Builds a {@link MediaController} asynchronously.
* *
@ -290,8 +307,12 @@ public class MediaController implements Player {
public ListenableFuture<MediaController> buildAsync() { public ListenableFuture<MediaController> buildAsync() {
MediaControllerHolder<MediaController> holder = MediaControllerHolder<MediaController> holder =
new MediaControllerHolder<>(applicationLooper); new MediaControllerHolder<>(applicationLooper);
if (token.isLegacySession() && bitmapLoader == null) {
bitmapLoader = new CacheBitmapLoader(new SimpleBitmapLoader());
}
MediaController controller = 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)); postOrRun(new Handler(applicationLooper), () -> holder.setController(controller));
return holder; return holder;
} }
@ -404,7 +425,8 @@ public class MediaController implements Player {
Bundle connectionHints, Bundle connectionHints,
Listener listener, Listener listener,
Looper applicationLooper, Looper applicationLooper,
ConnectionCallback connectionCallback) { ConnectionCallback connectionCallback,
@Nullable BitmapLoader bitmapLoader) {
checkNotNull(context, "context must not be null"); checkNotNull(context, "context must not be null");
checkNotNull(token, "token must not be null"); checkNotNull(token, "token must not be null");
@ -417,7 +439,7 @@ public class MediaController implements Player {
applicationHandler = new Handler(applicationLooper); applicationHandler = new Handler(applicationLooper);
this.connectionCallback = connectionCallback; this.connectionCallback = connectionCallback;
impl = createImpl(context, token, connectionHints, applicationLooper); impl = createImpl(context, token, connectionHints, applicationLooper, bitmapLoader);
impl.connect(); impl.connect();
} }
@ -427,9 +449,11 @@ public class MediaController implements Player {
Context context, Context context,
SessionToken token, SessionToken token,
Bundle connectionHints, Bundle connectionHints,
Looper applicationLooper) { Looper applicationLooper,
@Nullable BitmapLoader bitmapLoader) {
if (token.isLegacySession()) { if (token.isLegacySession()) {
return new MediaControllerImplLegacy(context, this, token, applicationLooper); return new MediaControllerImplLegacy(
context, this, token, applicationLooper, checkNotNull(bitmapLoader));
} else { } else {
return new MediaControllerImplBase(context, this, token, connectionHints, applicationLooper); 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.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@ -76,7 +77,10 @@ import com.google.common.util.concurrent.SettableFuture;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.initialization.qual.UnderInitialization;
import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.compatqual.NullableType;
@ -93,6 +97,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private final SessionToken token; private final SessionToken token;
private final ListenerSet<Listener> listeners; private final ListenerSet<Listener> listeners;
private final ControllerCompatCallback controllerCompatCallback; private final ControllerCompatCallback controllerCompatCallback;
private final BitmapLoader bitmapLoader;
@Nullable private MediaControllerCompat controllerCompat; @Nullable private MediaControllerCompat controllerCompat;
@Nullable private MediaBrowserCompat browserCompat; @Nullable private MediaBrowserCompat browserCompat;
@ -106,7 +111,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
Context context, Context context,
@UnderInitialization MediaController instance, @UnderInitialization MediaController instance,
SessionToken token, SessionToken token,
Looper applicationLooper) { Looper applicationLooper,
BitmapLoader bitmapLoader) {
// Initialize default values. // Initialize default values.
legacyPlayerInfo = new LegacyPlayerInfo(); legacyPlayerInfo = new LegacyPlayerInfo();
pendingLegacyPlayerInfo = new LegacyPlayerInfo(); pendingLegacyPlayerInfo = new LegacyPlayerInfo();
@ -122,6 +128,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
this.instance = instance; this.instance = instance;
controllerCompatCallback = new ControllerCompatCallback(applicationLooper); controllerCompatCallback = new ControllerCompatCallback(applicationLooper);
this.token = token; this.token = token;
this.bitmapLoader = bitmapLoader;
} }
/* package */ MediaController getInstance() { /* package */ MediaController getInstance() {
@ -716,11 +723,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* mediaItemTransitionReason= */ null); /* mediaItemTransitionReason= */ null);
if (isPrepared()) { if (isPrepared()) {
for (int i = 0; i < mediaItems.size(); i++) { addQueueItems(mediaItems, index);
MediaItem mediaItem = mediaItems.get(i);
controllerCompat.addQueueItem(
MediaUtils.convertToMediaDescriptionCompat(mediaItem), index + i);
}
} }
} }
@ -1340,15 +1343,61 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
} }
// Add all other items to the playlist if supported. // Add all other items to the playlist if supported.
if (getAvailableCommands().contains(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { if (getAvailableCommands().contains(Player.COMMAND_CHANGE_MEDIA_ITEMS)) {
List<MediaItem> adjustedMediaItems = new ArrayList<>();
for (int i = 0; i < queueTimeline.getWindowCount(); i++) { for (int i = 0; i < queueTimeline.getWindowCount(); i++) {
if (i == currentIndex || queueTimeline.getQueueId(i) != QueueItem.UNKNOWN_ID) { if (i == currentIndex || queueTimeline.getQueueId(i) != QueueItem.UNKNOWN_ID) {
// Skip the current item (added above) and all items already known to the session. // Skip the current item (added above) and all items already known to the session.
continue; continue;
} }
MediaItem mediaItem = queueTimeline.getWindow(/* windowIndex= */ i, window).mediaItem; adjustedMediaItems.add(queueTimeline.getWindow(/* windowIndex= */ i, window).mediaItem);
controllerCompat.addQueueItem(
MediaUtils.convertToMediaDescriptionCompat(mediaItem), /* index= */ i);
} }
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.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
@ -311,23 +310,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return result; 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} */ /** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat} */
public static MediaDescriptionCompat convertToMediaDescriptionCompat( public static MediaDescriptionCompat convertToMediaDescriptionCompat(
MediaItem item, @Nullable Bitmap artworkBitmap) { 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 android.support.v4.media.session.PlaybackStateCompat.ShuffleMode;
import androidx.media.AudioManagerCompat; import androidx.media.AudioManagerCompat;
import androidx.media.VolumeProviderCompat; import androidx.media.VolumeProviderCompat;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackParameters; import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player; 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.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress; import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -327,7 +329,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
@Test @Test
public void addMediaItems() throws Exception { public void addMediaItems() throws Exception {
int size = 2; int size = 2;
List<MediaItem> testList = MediaTestUtils.createMediaItems(size); List<MediaItem> testList = MediaTestUtils.createMediaItemsWithArtworkData(size);
List<QueueItem> testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList); List<QueueItem> testQueue = MediaTestUtils.convertToQueueItemsWithoutBitmap(testList);
session.setQueue(testQueue); session.setQueue(testQueue);
@ -345,6 +347,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
assertThat(sessionCallback.queueIndices.get(i)).isEqualTo(testIndex + i); assertThat(sessionCallback.queueIndices.get(i)).isEqualTo(testIndex + i);
assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getMediaId()) assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getMediaId())
.isEqualTo(testList.get(i).mediaId); .isEqualTo(testList.get(i).mediaId);
assertThat(sessionCallback.queueDescriptionListForAdd.get(i).getIconBitmap()).isNotNull();
} }
} }
@ -391,6 +394,75 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
assertThat(sessionCallback.onSkipToNextCalled).isTrue(); 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 @Test
public void setShuffleMode() throws Exception { public void setShuffleMode() throws Exception {
session.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE); session.setShuffleMode(PlaybackStateCompat.SHUFFLE_MODE_NONE);