Skip to content

Commit f4dbb97

Browse files
toniheicopybara-github
authored andcommitted
Fix forwarding logic in BitmapLoaders
loadBitmapFromMetadata is a default method in the interface, but not forwarded to the actual implementation for the two wrapping BitmapLoaders, preventing any customization of the method. PiperOrigin-RevId: 819766711
1 parent fe4f235 commit f4dbb97

File tree

4 files changed

+320
-14
lines changed

4 files changed

+320
-14
lines changed

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

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,21 @@
2020
import android.graphics.Bitmap;
2121
import android.net.Uri;
2222
import androidx.annotation.Nullable;
23+
import androidx.media3.common.MediaMetadata;
2324
import androidx.media3.common.util.BitmapLoader;
2425
import androidx.media3.common.util.UnstableApi;
2526
import com.google.common.util.concurrent.ListenableFuture;
2627
import java.util.Arrays;
2728
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
2829

2930
/**
30-
* A {@link BitmapLoader} that caches the result of the last {@link #decodeBitmap(byte[])} or {@link
31-
* #loadBitmap(Uri)} request. Requests are fulfilled from the last bitmap load request when the last
32-
* bitmap is requested from the same {@code data} or the last bitmap is requested from the same
33-
* {@code uri}. If it's not the above two cases, the request is forwarded to the provided {@link
34-
* BitmapLoader} and the result is cached.
31+
* A {@link BitmapLoader} that caches the result of the last {@link #decodeBitmap(byte[])}, {@link
32+
* #loadBitmap(Uri)} or {@link #loadBitmapFromMetadata(MediaMetadata)} request.
33+
*
34+
* <p>Requests are fulfilled from the last bitmap load request when the last bitmap is requested
35+
* from the same {@code data}, the same {@code uri}, or the same {@link MediaMetadata#artworkUri} or
36+
* {@link MediaMetadata#artworkData}. If the request doesn't match the previous request, the request
37+
* is forwarded to the provided {@link BitmapLoader} and the result is cached.
3538
*/
3639
@UnstableApi
3740
public final class CacheBitmapLoader implements BitmapLoader {
@@ -73,6 +76,20 @@ public ListenableFuture<Bitmap> loadBitmap(Uri uri) {
7376
return future;
7477
}
7578

79+
@Nullable
80+
@Override
81+
public ListenableFuture<Bitmap> loadBitmapFromMetadata(MediaMetadata metadata) {
82+
if (lastBitmapLoadRequest != null && lastBitmapLoadRequest.matches(metadata)) {
83+
return lastBitmapLoadRequest.getFuture();
84+
}
85+
ListenableFuture<Bitmap> future = bitmapLoader.loadBitmapFromMetadata(metadata);
86+
if (future == null) {
87+
return null;
88+
}
89+
lastBitmapLoadRequest = new BitmapLoadRequest(metadata, future);
90+
return future;
91+
}
92+
7693
/**
7794
* Stores the result of a bitmap load request. Requests are identified either by a byte array, if
7895
* the bitmap is loaded from compressed data, or a URI, if the bitmap was loaded from a URI.
@@ -82,30 +99,48 @@ private static class BitmapLoadRequest {
8299
@Nullable private final Uri uri;
83100
@Nullable private final ListenableFuture<Bitmap> future;
84101

85-
public BitmapLoadRequest(byte[] data, ListenableFuture<Bitmap> future) {
102+
/** Creates load request for byte array. */
103+
private BitmapLoadRequest(byte[] data, ListenableFuture<Bitmap> future) {
86104
this.data = data;
87105
this.uri = null;
88106
this.future = future;
89107
}
90108

91-
public BitmapLoadRequest(Uri uri, ListenableFuture<Bitmap> future) {
109+
/** Creates load request for URI. */
110+
private BitmapLoadRequest(Uri uri, ListenableFuture<Bitmap> future) {
92111
this.data = null;
93112
this.uri = uri;
94113
this.future = future;
95114
}
96115

116+
/** Creates load request for media metadata. */
117+
private BitmapLoadRequest(MediaMetadata metadata, ListenableFuture<Bitmap> future) {
118+
this.data = metadata.artworkData;
119+
this.uri = metadata.artworkUri;
120+
this.future = future;
121+
}
122+
97123
/** Whether the bitmap load request was performed for {@code data}. */
98-
public boolean matches(@Nullable byte[] data) {
124+
private boolean matches(byte[] data) {
99125
return this.data != null && Arrays.equals(this.data, data);
100126
}
101127

102128
/** Whether the bitmap load request was performed for {@code uri}. */
103-
public boolean matches(@Nullable Uri uri) {
129+
private boolean matches(Uri uri) {
104130
return this.uri != null && this.uri.equals(uri);
105131
}
106132

133+
/**
134+
* Whether the bitmap load request was performed for either {@code metadata.artworkUri} or
135+
* {@code metadata.artworkData}.
136+
*/
137+
private boolean matches(MediaMetadata metadata) {
138+
return (this.uri != null && this.uri.equals(metadata.artworkUri))
139+
|| (this.data != null && Arrays.equals(this.data, metadata.artworkData));
140+
}
141+
107142
/** Returns the future that set for the bitmap load request. */
108-
public ListenableFuture<Bitmap> getFuture() {
143+
private ListenableFuture<Bitmap> getFuture() {
109144
return checkNotNull(future);
110145
}
111146
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import android.graphics.Bitmap;
2222
import android.net.Uri;
23+
import androidx.annotation.Nullable;
24+
import androidx.media3.common.MediaMetadata;
2325
import androidx.media3.common.util.BitmapLoader;
2426
import androidx.media3.common.util.UnstableApi;
2527
import com.google.common.util.concurrent.Futures;
@@ -59,6 +61,15 @@ public ListenableFuture<Bitmap> loadBitmap(Uri uri) {
5961
return Futures.transform(future, this::scaleIfNecessary, directExecutor());
6062
}
6163

64+
@Nullable
65+
@Override
66+
public ListenableFuture<Bitmap> loadBitmapFromMetadata(MediaMetadata metadata) {
67+
ListenableFuture<Bitmap> future = bitmapLoader.loadBitmapFromMetadata(metadata);
68+
return future == null
69+
? null
70+
: Futures.transform(future, this::scaleIfNecessary, directExecutor());
71+
}
72+
6273
private Bitmap scaleIfNecessary(Bitmap bitmap) {
6374
if (bitmap.getWidth() > maxBitmapSize || bitmap.getHeight() > maxBitmapSize) {
6475
int width = bitmap.getWidth();

libraries/session/src/test/java/androidx/media3/session/CacheBitmapLoaderTest.java

Lines changed: 208 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package androidx.media3.session;
1717

18+
import static androidx.media3.common.MediaMetadata.PICTURE_TYPE_MEDIA;
19+
import static androidx.media3.test.utils.TestUtil.assertSubclassOverridesAllMethods;
1820
import static com.google.common.truth.Truth.assertThat;
1921
import static java.util.concurrent.TimeUnit.SECONDS;
2022
import static org.junit.Assert.assertThrows;
@@ -25,7 +27,9 @@
2527
import android.graphics.BitmapFactory;
2628
import android.graphics.Matrix;
2729
import android.net.Uri;
30+
import androidx.media3.common.MediaMetadata;
2831
import androidx.media3.common.ParserException;
32+
import androidx.media3.common.util.BitmapLoader;
2933
import androidx.media3.datasource.DataSourceBitmapLoader;
3034
import androidx.media3.datasource.HttpDataSource;
3135
import androidx.media3.test.utils.TestUtil;
@@ -67,7 +71,12 @@ public void setUp() {
6771
}
6872

6973
@Test
70-
public void decodeBitmap_requestWithSameDataTwice_success() throws Exception {
74+
public void overridesAllMethods() throws NoSuchMethodException {
75+
assertSubclassOverridesAllMethods(BitmapLoader.class, CacheBitmapLoader.class);
76+
}
77+
78+
@Test
79+
public void decodeBitmap_requestWithSameDataTwice_returnsCachedFuture() throws Exception {
7180
CacheBitmapLoader cacheBitmapLoader =
7281
new CacheBitmapLoader(new DataSourceBitmapLoader(context));
7382
byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH);
@@ -87,7 +96,7 @@ public void decodeBitmap_requestWithSameDataTwice_success() throws Exception {
8796
}
8897

8998
@Test
90-
public void decodeBitmap_requestWithDifferentData_success() throws Exception {
99+
public void decodeBitmap_requestWithDifferentData_returnsNewFuture() throws Exception {
91100
CacheBitmapLoader cacheBitmapLoader =
92101
new CacheBitmapLoader(new DataSourceBitmapLoader(context));
93102
byte[] imageData1 = TestUtil.getByteArray(context, TEST_IMAGE_PATH);
@@ -130,7 +139,7 @@ public void decodeBitmap_requestWithSameDataTwice_throwsException() {
130139
}
131140

132141
@Test
133-
public void loadBitmap_httpUri_requestWithSameUriTwice_success() throws Exception {
142+
public void loadBitmap_httpUri_requestWithSameUriTwice_returnsCachedFuture() throws Exception {
134143
CacheBitmapLoader cacheBitmapLoader =
135144
new CacheBitmapLoader(new DataSourceBitmapLoader(context));
136145
byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH);
@@ -155,7 +164,7 @@ public void loadBitmap_httpUri_requestWithSameUriTwice_success() throws Exceptio
155164
}
156165

157166
@Test
158-
public void loadBitmap_httpUri_requestWithDifferentUri_success() throws Exception {
167+
public void loadBitmap_httpUri_requestWithDifferentUri_returnsNewFuture() throws Exception {
159168
CacheBitmapLoader cacheBitmapLoader =
160169
new CacheBitmapLoader(new DataSourceBitmapLoader(context));
161170
byte[] imageData1 = TestUtil.getByteArray(context, TEST_IMAGE_PATH);
@@ -213,6 +222,173 @@ public void loadBitmap_httpUri_requestWithSameUriTwice_throwsException() {
213222
assertThat(mockWebServer.getRequestCount()).isEqualTo(1);
214223
}
215224

225+
@Test
226+
public void loadBitmapFromMetadata_requestWithSameDataTwice_returnsCachedFuture()
227+
throws Exception {
228+
CacheBitmapLoader cacheBitmapLoader =
229+
new CacheBitmapLoader(
230+
new LoadBitmapFromMetadataOnlyBitmapLoader(new DataSourceBitmapLoader(context)));
231+
byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH);
232+
Bitmap expectedBitmap =
233+
apply90DegreeExifRotation(
234+
BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length));
235+
MediaMetadata metadata =
236+
new MediaMetadata.Builder().setArtworkData(imageData, PICTURE_TYPE_MEDIA).build();
237+
238+
// First request, no cached bitmap load request.
239+
ListenableFuture<Bitmap> future1 = cacheBitmapLoader.loadBitmapFromMetadata(metadata);
240+
241+
assertThat(future1.get(10, SECONDS).sameAs(expectedBitmap)).isTrue();
242+
243+
// Second request, has cached bitmap load request.
244+
ListenableFuture<Bitmap> future2 = cacheBitmapLoader.loadBitmapFromMetadata(metadata);
245+
246+
assertThat(future1).isSameInstanceAs(future2);
247+
}
248+
249+
@Test
250+
public void loadBitmapFromMetadata_requestWithDifferentData_returnsNewFuture() throws Exception {
251+
CacheBitmapLoader cacheBitmapLoader =
252+
new CacheBitmapLoader(
253+
new LoadBitmapFromMetadataOnlyBitmapLoader(new DataSourceBitmapLoader(context)));
254+
byte[] imageData1 = TestUtil.getByteArray(context, TEST_IMAGE_PATH);
255+
byte[] imageData2 = TestUtil.getByteArray(context, SECOND_TEST_IMAGE_PATH);
256+
Bitmap expectedBitmap1 =
257+
apply90DegreeExifRotation(
258+
BitmapFactory.decodeByteArray(imageData1, /* offset= */ 0, imageData1.length));
259+
Bitmap expectedBitmap2 =
260+
apply90DegreeExifRotation(
261+
BitmapFactory.decodeByteArray(imageData2, /* offset= */ 0, imageData2.length));
262+
MediaMetadata metadata1 =
263+
new MediaMetadata.Builder().setArtworkData(imageData1, PICTURE_TYPE_MEDIA).build();
264+
MediaMetadata metadata2 =
265+
new MediaMetadata.Builder().setArtworkData(imageData2, PICTURE_TYPE_MEDIA).build();
266+
267+
// First request.
268+
ListenableFuture<Bitmap> future1 = cacheBitmapLoader.loadBitmapFromMetadata(metadata1);
269+
270+
assertThat(future1.get(10, SECONDS).sameAs(expectedBitmap1)).isTrue();
271+
272+
// Second request.
273+
ListenableFuture<Bitmap> future2 = cacheBitmapLoader.loadBitmapFromMetadata(metadata2);
274+
275+
assertThat(future2.get(10, SECONDS).sameAs(expectedBitmap2)).isTrue();
276+
assertThat(future1).isNotSameInstanceAs(future2);
277+
}
278+
279+
@Test
280+
public void loadBitmapFromMetadata_requestWithSameDataTwice_throwsException() {
281+
CacheBitmapLoader cacheBitmapLoader =
282+
new CacheBitmapLoader(
283+
new LoadBitmapFromMetadataOnlyBitmapLoader(new DataSourceBitmapLoader(context)));
284+
MediaMetadata metadata =
285+
new MediaMetadata.Builder().setArtworkData(new byte[0], PICTURE_TYPE_MEDIA).build();
286+
287+
// First request, no cached bitmap load request.
288+
ListenableFuture<Bitmap> future1 = cacheBitmapLoader.loadBitmapFromMetadata(metadata);
289+
290+
// Second request, has cached bitmap load request.
291+
ListenableFuture<Bitmap> future2 = cacheBitmapLoader.loadBitmapFromMetadata(metadata);
292+
293+
assertThat(future1).isSameInstanceAs(future2);
294+
ExecutionException executionException =
295+
assertThrows(ExecutionException.class, () -> future1.get(10, SECONDS));
296+
assertThat(executionException).hasCauseThat().isInstanceOf(ParserException.class);
297+
assertThat(executionException).hasMessageThat().contains("Could not decode image data");
298+
}
299+
300+
@Test
301+
public void loadBitmapFromMetadata_requestWithSameUriTwice_returnsCachedFuture()
302+
throws Exception {
303+
CacheBitmapLoader cacheBitmapLoader =
304+
new CacheBitmapLoader(
305+
new LoadBitmapFromMetadataOnlyBitmapLoader(new DataSourceBitmapLoader(context)));
306+
byte[] imageData = TestUtil.getByteArray(context, TEST_IMAGE_PATH);
307+
Buffer responseBody = new Buffer().write(imageData);
308+
MockWebServer mockWebServer = new MockWebServer();
309+
mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody));
310+
Uri uri = Uri.parse(mockWebServer.url("test_path").toString());
311+
Bitmap expectedBitmap =
312+
apply90DegreeExifRotation(
313+
BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length));
314+
MediaMetadata metadata = new MediaMetadata.Builder().setArtworkUri(uri).build();
315+
316+
// First request, no cached bitmap load request.
317+
Bitmap bitmap = cacheBitmapLoader.loadBitmapFromMetadata(metadata).get(10, SECONDS);
318+
319+
assertThat(bitmap.sameAs(expectedBitmap)).isTrue();
320+
321+
// Second request, has cached bitmap load request.
322+
bitmap = cacheBitmapLoader.loadBitmapFromMetadata(metadata).get(10, SECONDS);
323+
324+
assertThat(bitmap.sameAs(expectedBitmap)).isTrue();
325+
assertThat(mockWebServer.getRequestCount()).isEqualTo(1);
326+
}
327+
328+
@Test
329+
public void loadBitmapFromMetadata_requestWithDifferentUri_returnsNewFuture() throws Exception {
330+
CacheBitmapLoader cacheBitmapLoader =
331+
new CacheBitmapLoader(
332+
new LoadBitmapFromMetadataOnlyBitmapLoader(new DataSourceBitmapLoader(context)));
333+
byte[] imageData1 = TestUtil.getByteArray(context, TEST_IMAGE_PATH);
334+
byte[] imageData2 = TestUtil.getByteArray(context, SECOND_TEST_IMAGE_PATH);
335+
Buffer responseBody1 = new Buffer().write(imageData1);
336+
Buffer responseBody2 = new Buffer().write(imageData2);
337+
MockWebServer mockWebServer = new MockWebServer();
338+
mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody1));
339+
mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody2));
340+
Uri uri1 = Uri.parse(mockWebServer.url("test_path_1").toString());
341+
Uri uri2 = Uri.parse(mockWebServer.url("test_path_2").toString());
342+
Bitmap expectedBitmap1 =
343+
apply90DegreeExifRotation(
344+
BitmapFactory.decodeByteArray(imageData1, /* offset= */ 0, imageData1.length));
345+
Bitmap expectedBitmap2 =
346+
apply90DegreeExifRotation(
347+
BitmapFactory.decodeByteArray(imageData2, /* offset= */ 0, imageData2.length));
348+
MediaMetadata metadata1 = new MediaMetadata.Builder().setArtworkUri(uri1).build();
349+
MediaMetadata metadata2 = new MediaMetadata.Builder().setArtworkUri(uri2).build();
350+
351+
// First request.
352+
Bitmap bitmap = cacheBitmapLoader.loadBitmapFromMetadata(metadata1).get(10, SECONDS);
353+
354+
assertThat(bitmap.sameAs(expectedBitmap1)).isTrue();
355+
356+
// Second request.
357+
bitmap = cacheBitmapLoader.loadBitmapFromMetadata(metadata2).get(10, SECONDS);
358+
359+
assertThat(bitmap.sameAs(expectedBitmap2)).isTrue();
360+
assertThat(mockWebServer.getRequestCount()).isEqualTo(2);
361+
}
362+
363+
@Test
364+
public void loadBitmapFromMetadata_requestWithSameUriTwice_throwsException() {
365+
CacheBitmapLoader cacheBitmapLoader =
366+
new CacheBitmapLoader(
367+
new LoadBitmapFromMetadataOnlyBitmapLoader(new DataSourceBitmapLoader(context)));
368+
MockWebServer mockWebServer = new MockWebServer();
369+
mockWebServer.enqueue(new MockResponse().setResponseCode(404));
370+
Uri uri = Uri.parse(mockWebServer.url("test_path").toString());
371+
MediaMetadata metadata = new MediaMetadata.Builder().setArtworkUri(uri).build();
372+
373+
// First request, no cached bitmap load request.
374+
ListenableFuture<Bitmap> future1 = cacheBitmapLoader.loadBitmapFromMetadata(metadata);
375+
376+
// Second request, has cached bitmap load request.
377+
ListenableFuture<Bitmap> future2 = cacheBitmapLoader.loadBitmapFromMetadata(metadata);
378+
379+
ExecutionException executionException1 =
380+
assertThrows(ExecutionException.class, () -> future1.get(10, SECONDS));
381+
ExecutionException executionException2 =
382+
assertThrows(ExecutionException.class, () -> future2.get(10, SECONDS));
383+
assertThat(executionException1)
384+
.hasCauseThat()
385+
.isInstanceOf(HttpDataSource.InvalidResponseCodeException.class);
386+
assertThat(executionException2)
387+
.hasCauseThat()
388+
.isInstanceOf(HttpDataSource.InvalidResponseCodeException.class);
389+
assertThat(mockWebServer.getRequestCount()).isEqualTo(1);
390+
}
391+
216392
private static Bitmap apply90DegreeExifRotation(Bitmap bitmap) {
217393
Matrix rotationMatrix = new Matrix();
218394
rotationMatrix.postRotate(/* degrees= */ 90);
@@ -225,4 +401,32 @@ private static Bitmap apply90DegreeExifRotation(Bitmap bitmap) {
225401
rotationMatrix,
226402
/* filter= */ false);
227403
}
404+
405+
private static class LoadBitmapFromMetadataOnlyBitmapLoader implements BitmapLoader {
406+
private final BitmapLoader bitmapLoader;
407+
408+
private LoadBitmapFromMetadataOnlyBitmapLoader(BitmapLoader bitmapLoader) {
409+
this.bitmapLoader = bitmapLoader;
410+
}
411+
412+
@Override
413+
public boolean supportsMimeType(String mimeType) {
414+
return true;
415+
}
416+
417+
@Override
418+
public ListenableFuture<Bitmap> decodeBitmap(byte[] data) {
419+
throw new UnsupportedOperationException();
420+
}
421+
422+
@Override
423+
public ListenableFuture<Bitmap> loadBitmap(Uri uri) {
424+
throw new UnsupportedOperationException();
425+
}
426+
427+
@Override
428+
public ListenableFuture<Bitmap> loadBitmapFromMetadata(MediaMetadata metadata) {
429+
return bitmapLoader.loadBitmapFromMetadata(metadata);
430+
}
431+
}
228432
}

0 commit comments

Comments
 (0)