Skip to content

Commit 4ce171a

Browse files
tianyifmicrokatz
authored andcommitted
Load bitmaps for MediaBrowserCompat.
* Transforms the `ListenableFuture<LibraryResult<MediaItem>>` and `ListenableFuture<LibraryResult<List<MediaItem>>>` to `ListenableFuture<MediaBrowserCompat.MediaItem>` and `ListenableFuture<List<MediaBrowserCompat.MediaItem>>`, and the result will be sent out when `ListenableFuture` the `MediaBrowserCompat.MediaItem` (or the list of it) is fulfilled. * Add `artworkData` to the tests in `MediaBrowserCompatWithMediaLibraryServiceTest`. PiperOrigin-RevId: 489205547
1 parent 6e73fc5 commit 4ce171a

File tree

5 files changed

+225
-76
lines changed

5 files changed

+225
-76
lines changed

libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java

+154-23
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES;
2727

2828
import android.annotation.SuppressLint;
29+
import android.graphics.Bitmap;
2930
import android.os.BadParcelableException;
3031
import android.os.Bundle;
3132
import android.os.RemoteException;
@@ -37,21 +38,27 @@
3738
import androidx.media.MediaBrowserServiceCompat;
3839
import androidx.media.MediaSessionManager.RemoteUserInfo;
3940
import androidx.media3.common.MediaItem;
41+
import androidx.media3.common.MediaMetadata;
4042
import androidx.media3.common.util.ConditionVariable;
4143
import androidx.media3.common.util.Log;
4244
import androidx.media3.common.util.Util;
4345
import androidx.media3.session.MediaLibraryService.LibraryParams;
4446
import androidx.media3.session.MediaSession.ControllerCb;
4547
import androidx.media3.session.MediaSession.ControllerInfo;
4648
import com.google.common.collect.ImmutableList;
49+
import com.google.common.util.concurrent.AsyncFunction;
50+
import com.google.common.util.concurrent.Futures;
4751
import com.google.common.util.concurrent.ListenableFuture;
4852
import com.google.common.util.concurrent.MoreExecutors;
53+
import com.google.common.util.concurrent.SettableFuture;
4954
import java.util.ArrayList;
5055
import java.util.List;
5156
import java.util.concurrent.CancellationException;
5257
import java.util.concurrent.ExecutionException;
5358
import java.util.concurrent.Future;
59+
import java.util.concurrent.atomic.AtomicInteger;
5460
import java.util.concurrent.atomic.AtomicReference;
61+
import org.checkerframework.checker.nullness.compatqual.NullableType;
5562

5663
/**
5764
* Implementation of {@link MediaBrowserServiceCompat} for interoperability between {@link
@@ -218,7 +225,11 @@ public void onLoadChildren(
218225
ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> future =
219226
librarySessionImpl.onGetChildrenOnHandler(
220227
controller, parentId, page, pageSize, params);
221-
sendLibraryResultWithMediaItemsWhenReady(result, future);
228+
ListenableFuture<@NullableType List<MediaBrowserCompat.MediaItem>>
229+
browserItemsFuture =
230+
Util.transformFutureAsync(
231+
future, createMediaItemsToBrowserItemsAsyncFunction());
232+
sendLibraryResultWithMediaItemsWhenReady(result, browserItemsFuture);
222233
return;
223234
}
224235
// Cannot distinguish onLoadChildren() why it's called either by
@@ -236,7 +247,9 @@ public void onLoadChildren(
236247
/* page= */ 0,
237248
/* pageSize= */ Integer.MAX_VALUE,
238249
/* params= */ null);
239-
sendLibraryResultWithMediaItemsWhenReady(result, future);
250+
ListenableFuture<@NullableType List<MediaBrowserCompat.MediaItem>> browserItemsFuture =
251+
Util.transformFutureAsync(future, createMediaItemsToBrowserItemsAsyncFunction());
252+
sendLibraryResultWithMediaItemsWhenReady(result, browserItemsFuture);
240253
});
241254
}
242255

@@ -264,7 +277,9 @@ public void onLoadItem(String itemId, Result<MediaBrowserCompat.MediaItem> resul
264277
}
265278
ListenableFuture<LibraryResult<MediaItem>> future =
266279
librarySessionImpl.onGetItemOnHandler(controller, itemId);
267-
sendLibraryResultWithMediaItemWhenReady(result, future);
280+
ListenableFuture<MediaBrowserCompat.@NullableType MediaItem> browserItemFuture =
281+
Util.transformFutureAsync(future, createMediaItemToBrowserItemAsyncFunction());
282+
sendLibraryResultWithMediaItemWhenReady(result, browserItemFuture);
268283
});
269284
}
270285

@@ -362,17 +377,12 @@ private static void sendCustomActionResultWhenReady(
362377

363378
private static void sendLibraryResultWithMediaItemWhenReady(
364379
Result<MediaBrowserCompat.MediaItem> result,
365-
ListenableFuture<LibraryResult<MediaItem>> future) {
380+
ListenableFuture<MediaBrowserCompat.@NullableType MediaItem> future) {
366381
future.addListener(
367382
() -> {
368383
try {
369-
LibraryResult<MediaItem> libraryResult =
370-
checkNotNull(future.get(), "LibraryResult must not be null");
371-
if (libraryResult.resultCode != RESULT_SUCCESS || libraryResult.value == null) {
372-
result.sendResult(/* result= */ null);
373-
} else {
374-
result.sendResult(MediaUtils.convertToBrowserItem(libraryResult.value));
375-
}
384+
MediaBrowserCompat.MediaItem mediaItem = future.get();
385+
result.sendResult(mediaItem);
376386
} catch (CancellationException | ExecutionException | InterruptedException unused) {
377387
result.sendError(/* extras= */ null);
378388
}
@@ -382,27 +392,146 @@ private static void sendLibraryResultWithMediaItemWhenReady(
382392

383393
private static void sendLibraryResultWithMediaItemsWhenReady(
384394
Result<List<MediaBrowserCompat.MediaItem>> result,
385-
ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> future) {
395+
ListenableFuture<@NullableType List<MediaBrowserCompat.MediaItem>> future) {
386396
future.addListener(
387397
() -> {
388398
try {
389-
LibraryResult<ImmutableList<MediaItem>> libraryResult =
390-
checkNotNull(future.get(), "LibraryResult must not be null");
391-
if (libraryResult.resultCode != RESULT_SUCCESS || libraryResult.value == null) {
392-
result.sendResult(/* result= */ null);
393-
} else {
394-
result.sendResult(
395-
MediaUtils.truncateListBySize(
396-
MediaUtils.convertToBrowserItemList(libraryResult.value),
397-
TRANSACTION_SIZE_LIMIT_IN_BYTES));
398-
}
399+
List<MediaBrowserCompat.MediaItem> mediaItems = future.get();
400+
result.sendResult(
401+
(mediaItems == null)
402+
? null
403+
: MediaUtils.truncateListBySize(mediaItems, TRANSACTION_SIZE_LIMIT_IN_BYTES));
399404
} catch (CancellationException | ExecutionException | InterruptedException unused) {
400405
result.sendError(/* extras= */ null);
401406
}
402407
},
403408
MoreExecutors.directExecutor());
404409
}
405410

411+
private AsyncFunction<
412+
LibraryResult<ImmutableList<MediaItem>>, @NullableType List<MediaBrowserCompat.MediaItem>>
413+
createMediaItemsToBrowserItemsAsyncFunction() {
414+
return result -> {
415+
checkNotNull(result, "LibraryResult must not be null");
416+
SettableFuture<@NullableType List<MediaBrowserCompat.MediaItem>> outputFuture =
417+
SettableFuture.create();
418+
if (result.resultCode != RESULT_SUCCESS || result.value == null) {
419+
outputFuture.set(null);
420+
return outputFuture;
421+
}
422+
423+
ImmutableList<MediaItem> mediaItems = result.value;
424+
if (mediaItems.isEmpty()) {
425+
outputFuture.set(new ArrayList<>());
426+
return outputFuture;
427+
}
428+
429+
List<@NullableType ListenableFuture<Bitmap>> bitmapFutures = new ArrayList<>();
430+
outputFuture.addListener(
431+
() -> {
432+
if (outputFuture.isCancelled()) {
433+
cancelAllFutures(bitmapFutures);
434+
}
435+
},
436+
MoreExecutors.directExecutor());
437+
438+
final AtomicInteger resultCount = new AtomicInteger(0);
439+
Runnable handleBitmapFuturesTask =
440+
() -> {
441+
int completedBitmapFutureCount = resultCount.incrementAndGet();
442+
if (completedBitmapFutureCount == mediaItems.size()) {
443+
handleBitmapFuturesAllCompletedAndSetOutputFuture(
444+
bitmapFutures, mediaItems, outputFuture);
445+
}
446+
};
447+
448+
for (int i = 0; i < mediaItems.size(); i++) {
449+
MediaItem mediaItem = mediaItems.get(i);
450+
MediaMetadata metadata = mediaItem.mediaMetadata;
451+
if (metadata.artworkData == null) {
452+
bitmapFutures.add(null);
453+
handleBitmapFuturesTask.run();
454+
} else {
455+
ListenableFuture<Bitmap> bitmapFuture =
456+
librarySessionImpl.getBitmapLoader().decodeBitmap(metadata.artworkData);
457+
bitmapFutures.add(bitmapFuture);
458+
bitmapFuture.addListener(handleBitmapFuturesTask, MoreExecutors.directExecutor());
459+
}
460+
}
461+
return outputFuture;
462+
};
463+
}
464+
465+
private void handleBitmapFuturesAllCompletedAndSetOutputFuture(
466+
List<@NullableType ListenableFuture<Bitmap>> bitmapFutures,
467+
List<MediaItem> mediaItems,
468+
SettableFuture<@NullableType List<MediaBrowserCompat.MediaItem>> outputFuture) {
469+
List<MediaBrowserCompat.MediaItem> outputMediaItems = new ArrayList<>();
470+
for (int i = 0; i < bitmapFutures.size(); i++) {
471+
@Nullable ListenableFuture<Bitmap> future = bitmapFutures.get(i);
472+
@Nullable Bitmap bitmap = null;
473+
if (future != null) {
474+
try {
475+
bitmap = Futures.getDone(future);
476+
} catch (CancellationException | ExecutionException e) {
477+
Log.d(TAG, "Failed to get bitmap");
478+
}
479+
}
480+
outputMediaItems.add(MediaUtils.convertToBrowserItem(mediaItems.get(i), bitmap));
481+
}
482+
outputFuture.set(outputMediaItems);
483+
}
484+
485+
private static <T> void cancelAllFutures(List<@NullableType ListenableFuture<T>> futures) {
486+
for (int i = 0; i < futures.size(); i++) {
487+
if (futures.get(i) != null) {
488+
futures.get(i).cancel(/* mayInterruptIfRunning= */ false);
489+
}
490+
}
491+
}
492+
493+
private AsyncFunction<LibraryResult<MediaItem>, MediaBrowserCompat.@NullableType MediaItem>
494+
createMediaItemToBrowserItemAsyncFunction() {
495+
return result -> {
496+
checkNotNull(result, "LibraryResult must not be null");
497+
SettableFuture<MediaBrowserCompat.@NullableType MediaItem> outputFuture =
498+
SettableFuture.create();
499+
if (result.resultCode != RESULT_SUCCESS || result.value == null) {
500+
outputFuture.set(null);
501+
return outputFuture;
502+
}
503+
504+
MediaItem mediaItem = result.value;
505+
MediaMetadata metadata = mediaItem.mediaMetadata;
506+
if (metadata.artworkData == null) {
507+
outputFuture.set(MediaUtils.convertToBrowserItem(mediaItem, /* artworkBitmap= */ null));
508+
return outputFuture;
509+
}
510+
511+
ListenableFuture<Bitmap> bitmapFuture =
512+
librarySessionImpl.getBitmapLoader().decodeBitmap(metadata.artworkData);
513+
outputFuture.addListener(
514+
() -> {
515+
if (outputFuture.isCancelled()) {
516+
bitmapFuture.cancel(/* mayInterruptIfRunning= */ false);
517+
}
518+
},
519+
MoreExecutors.directExecutor());
520+
bitmapFuture.addListener(
521+
() -> {
522+
@Nullable Bitmap bitmap = null;
523+
try {
524+
bitmap = Futures.getDone(bitmapFuture);
525+
} catch (CancellationException | ExecutionException e) {
526+
Log.d(TAG, "failed to get bitmap");
527+
}
528+
outputFuture.set(MediaUtils.convertToBrowserItem(mediaItem, bitmap));
529+
},
530+
MoreExecutors.directExecutor());
531+
return outputFuture;
532+
};
533+
}
534+
406535
private static <T> void ignoreFuture(Future<T> unused) {
407536
// no-op
408537
}
@@ -504,7 +633,9 @@ public void onSearchResultChanged(
504633
ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> future =
505634
librarySessionImpl.onGetSearchResultOnHandler(
506635
request.controller, request.query, page, pageSize, libraryParams);
507-
sendLibraryResultWithMediaItemsWhenReady(request.result, future);
636+
ListenableFuture<@NullableType List<MediaBrowserCompat.MediaItem>> mediaItemsFuture =
637+
Util.transformFutureAsync(future, createMediaItemsToBrowserItemsAsyncFunction());
638+
sendLibraryResultWithMediaItemsWhenReady(request.result, mediaItemsFuture);
508639
}
509640
});
510641
}

libraries/session/src/main/java/androidx/media3/session/MediaUtils.java

+24-17
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ public static PlaybackException convertToPlaybackException(
136136
errorMessage, /* cause= */ null, PlaybackException.ERROR_CODE_REMOTE_ERROR);
137137
}
138138

139-
/** Converts a {@link MediaItem} to a {@link MediaBrowserCompat.MediaItem}. */
140-
public static MediaBrowserCompat.MediaItem convertToBrowserItem(MediaItem item) {
141-
MediaDescriptionCompat description = convertToMediaDescriptionCompat(item);
139+
public static MediaBrowserCompat.MediaItem convertToBrowserItem(
140+
MediaItem item, @Nullable Bitmap artworkBitmap) {
141+
MediaDescriptionCompat description = convertToMediaDescriptionCompat(item, artworkBitmap);
142142
MediaMetadata metadata = item.mediaMetadata;
143143
int flags = 0;
144144
if (metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE) {
@@ -150,15 +150,6 @@ public static MediaBrowserCompat.MediaItem convertToBrowserItem(MediaItem item)
150150
return new MediaBrowserCompat.MediaItem(description, flags);
151151
}
152152

153-
/** Converts a list of {@link MediaItem} to a list of {@link MediaBrowserCompat.MediaItem}. */
154-
public static List<MediaBrowserCompat.MediaItem> convertToBrowserItemList(List<MediaItem> items) {
155-
List<MediaBrowserCompat.MediaItem> result = new ArrayList<>();
156-
for (int i = 0; i < items.size(); i++) {
157-
result.add(convertToBrowserItem(items.get(i)));
158-
}
159-
return result;
160-
}
161-
162153
/** Converts a {@link MediaBrowserCompat.MediaItem} to a {@link MediaItem}. */
163154
public static MediaItem convertToMediaItem(MediaBrowserCompat.MediaItem item) {
164155
return convertToMediaItem(item.getDescription(), item.isBrowsable(), item.isPlayable());
@@ -320,16 +311,32 @@ public static <T extends Parcelable> List<T> truncateListBySize(
320311
return result;
321312
}
322313

323-
/** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat}. */
314+
/**
315+
* Converts a {@link MediaItem} to a {@link MediaDescriptionCompat}.
316+
*
317+
* @deprecated Use {@link #convertToMediaDescriptionCompat(MediaItem, Bitmap)} instead.
318+
*/
319+
@Deprecated
324320
public static MediaDescriptionCompat convertToMediaDescriptionCompat(MediaItem item) {
321+
MediaMetadata metadata = item.mediaMetadata;
322+
@Nullable Bitmap artworkBitmap = null;
323+
if (metadata.artworkData != null) {
324+
artworkBitmap =
325+
BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length);
326+
}
327+
328+
return convertToMediaDescriptionCompat(item, artworkBitmap);
329+
}
330+
331+
/** Converts a {@link MediaItem} to a {@link MediaDescriptionCompat} */
332+
public static MediaDescriptionCompat convertToMediaDescriptionCompat(
333+
MediaItem item, @Nullable Bitmap artworkBitmap) {
325334
MediaDescriptionCompat.Builder builder =
326335
new MediaDescriptionCompat.Builder()
327336
.setMediaId(item.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) ? null : item.mediaId);
328337
MediaMetadata metadata = item.mediaMetadata;
329-
if (metadata.artworkData != null) {
330-
Bitmap artwork =
331-
BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length);
332-
builder.setIconBitmap(artwork);
338+
if (artworkBitmap != null) {
339+
builder.setIconBitmap(artworkBitmap);
333340
}
334341
@Nullable Bundle extras = metadata.extras;
335342
if (metadata.folderType != null && metadata.folderType != MediaMetadata.FOLDER_TYPE_NONE) {

libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java

+5
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public void onItemLoaded(MediaItem item) {
129129
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
130130
assertThat(itemRef.get().getMediaId()).isEqualTo(mediaId);
131131
assertThat(itemRef.get().isBrowsable()).isTrue();
132+
assertThat(itemRef.get().getDescription().getIconBitmap()).isNotNull();
132133
}
133134

134135
@Test
@@ -151,6 +152,7 @@ public void onItemLoaded(MediaItem item) {
151152
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
152153
assertThat(itemRef.get().getMediaId()).isEqualTo(mediaId);
153154
assertThat(itemRef.get().isPlayable()).isTrue();
155+
assertThat(itemRef.get().getDescription().getIconBitmap()).isNotNull();
154156
}
155157

156158
@Test
@@ -181,6 +183,7 @@ public void onItemLoaded(MediaItem item) {
181183
BundleSubject.assertThat(description.getExtras())
182184
.string(METADATA_EXTRA_KEY)
183185
.isEqualTo(METADATA_EXTRA_VALUE);
186+
assertThat(description.getIconBitmap()).isNotNull();
184187
}
185188

186189
@Test
@@ -245,6 +248,7 @@ public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle o
245248
EXTRAS_KEY_COMPLETION_STATUS,
246249
/* defaultValue= */ EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + 1))
247250
.isEqualTo(EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED);
251+
assertThat(mediaItem.getDescription().getIconBitmap()).isNotNull();
248252
}
249253
}
250254

@@ -311,6 +315,7 @@ public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle o
311315
int relativeIndex = originalIndex - fromIndex;
312316
assertThat(children.get(relativeIndex).getMediaId())
313317
.isEqualTo(GET_CHILDREN_RESULT.get(originalIndex));
318+
assertThat(children.get(relativeIndex).getDescription().getIconBitmap()).isNotNull();
314319
}
315320
latch.countDown();
316321
}

0 commit comments

Comments
 (0)