diff --git a/libraries/common/src/main/java/androidx/media3/common/util/BitmapLoader.java b/libraries/common/src/main/java/androidx/media3/common/util/BitmapLoader.java index 44b7cadb28..118d2cd575 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/BitmapLoader.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/BitmapLoader.java @@ -16,6 +16,7 @@ package androidx.media3.common.util; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import androidx.annotation.Nullable; import androidx.media3.common.MediaMetadata; @@ -28,7 +29,12 @@ public interface BitmapLoader { ListenableFuture decodeBitmap(byte[] data); /** Loads an image from {@code uri}. */ - ListenableFuture loadBitmap(Uri uri); + default ListenableFuture loadBitmap(Uri uri) { + return loadBitmap(uri, /* options= */ null); + } + + /** Loads an image from {@code uri} with the given {@link BitmapFactory.Options}. */ + ListenableFuture loadBitmap(Uri uri, @Nullable BitmapFactory.Options options); /** * Loads an image from {@link MediaMetadata}. Returns null if {@code metadata} doesn't contain diff --git a/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java b/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java index 32d366bb8b..5c81878f1e 100644 --- a/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java +++ b/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java @@ -198,6 +198,24 @@ public class DataSourceBitmapLoaderTest { .isTrue(); } + @Test + public void loadBitmap_withFileUriAndOptions_loadsDataWithRespectToOptions() throws Exception { + byte[] imageData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH); + File file = tempFolder.newFile(); + Files.write(Paths.get(file.getAbsolutePath()), imageData); + Uri uri = Uri.fromFile(file); + DataSourceBitmapLoader bitmapLoader = + new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + + Bitmap bitmap = bitmapLoader.loadBitmap(uri, options).get(); + + assertThat(bitmap.isMutable()).isTrue(); + } + @Test public void loadBitmap_fileUriWithFileNotExisting_throws() { DataSourceBitmapLoader bitmapLoader = diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java index 0f60321b5d..6c0b547874 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java @@ -78,17 +78,21 @@ public final class DataSourceBitmapLoader implements BitmapLoader { @Override public ListenableFuture decodeBitmap(byte[] data) { - return listeningExecutorService.submit(() -> decode(data)); + return listeningExecutorService.submit(() -> decode(data, /* options= */ null)); } - /** Loads an image from a {@link Uri}. */ @Override - public ListenableFuture loadBitmap(Uri uri) { - return listeningExecutorService.submit(() -> load(dataSourceFactory.createDataSource(), uri)); + public ListenableFuture loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) { + return listeningExecutorService.submit( + () -> load(dataSourceFactory.createDataSource(), uri, options)); } - private static Bitmap decode(byte[] data) throws IOException { - @Nullable Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length); + // BitmapFactory's options parameter is null-ok. + @SuppressWarnings("nullness:argument.type.incompatible") + private static Bitmap decode(byte[] data, @Nullable BitmapFactory.Options options) + throws IOException { + @Nullable + Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length, options); checkArgument(bitmap != null, "Could not decode image data"); ExifInterface exifInterface; try (InputStream inputStream = new ByteArrayInputStream(data)) { @@ -111,12 +115,13 @@ public final class DataSourceBitmapLoader implements BitmapLoader { return bitmap; } - private static Bitmap load(DataSource dataSource, Uri uri) throws IOException { + private static Bitmap load( + DataSource dataSource, Uri uri, @Nullable BitmapFactory.Options options) throws IOException { try { DataSpec dataSpec = new DataSpec(uri); dataSource.open(dataSpec); byte[] readData = DataSourceUtil.readToEnd(dataSource); - return decode(readData); + return decode(readData, options); } finally { dataSource.close(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/CacheBitmapLoader.java b/libraries/session/src/main/java/androidx/media3/session/CacheBitmapLoader.java index 4fc3f5717f..5426d670a2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/CacheBitmapLoader.java +++ b/libraries/session/src/main/java/androidx/media3/session/CacheBitmapLoader.java @@ -18,6 +18,7 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkStateNotNull; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import androidx.annotation.Nullable; import androidx.media3.common.util.BitmapLoader; @@ -59,11 +60,11 @@ public final class CacheBitmapLoader implements BitmapLoader { } @Override - public ListenableFuture loadBitmap(Uri uri) { + public ListenableFuture loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) { if (lastBitmapLoadRequest != null && lastBitmapLoadRequest.matches(uri)) { return lastBitmapLoadRequest.getFuture(); } - ListenableFuture future = bitmapLoader.loadBitmap(uri); + ListenableFuture future = bitmapLoader.loadBitmap(uri, options); lastBitmapLoadRequest = new BitmapLoadRequest(uri, future); return future; } diff --git a/libraries/session/src/main/java/androidx/media3/session/SimpleBitmapLoader.java b/libraries/session/src/main/java/androidx/media3/session/SimpleBitmapLoader.java index 5a74e4ae38..76d42b608e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SimpleBitmapLoader.java +++ b/libraries/session/src/main/java/androidx/media3/session/SimpleBitmapLoader.java @@ -76,21 +76,24 @@ public final class SimpleBitmapLoader implements BitmapLoader { @Override public ListenableFuture decodeBitmap(byte[] data) { - return executorService.submit(() -> decode(data)); + return executorService.submit(() -> decode(data, /* options= */ null)); } @Override - public ListenableFuture loadBitmap(Uri uri) { - return executorService.submit(() -> load(uri)); + public ListenableFuture loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) { + return executorService.submit(() -> load(uri, options)); } - private static Bitmap decode(byte[] data) { - @Nullable Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length); + // BitmapFactory's options parameter is null-ok. + @SuppressWarnings("nullness:argument.type.incompatible") + private static Bitmap decode(byte[] data, @Nullable BitmapFactory.Options options) { + @Nullable + Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length, options); checkArgument(bitmap != null, "Could not decode image data"); return bitmap; } - private static Bitmap load(Uri uri) throws IOException { + private static Bitmap load(Uri uri, @Nullable BitmapFactory.Options options) throws IOException { if ("file".equals(uri.getScheme())) { @Nullable String path = uri.getPath(); if (path == null) { @@ -113,7 +116,7 @@ public final class SimpleBitmapLoader implements BitmapLoader { throw new IOException("Invalid response status code: " + responseCode); } try (InputStream inputStream = httpConnection.getInputStream()) { - return decode(ByteStreams.toByteArray(inputStream)); + return decode(ByteStreams.toByteArray(inputStream), options); } } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java index 805eb42cb2..4ad4a57bcf 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java @@ -26,6 +26,8 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.ColorSpace; import android.os.Looper; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -35,6 +37,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSourceBitmapLoader; import androidx.media3.datasource.DefaultDataSource; @@ -109,7 +112,12 @@ public final class ImageAssetLoader implements AssetLoader { MoreExecutors.listeningDecorator(scheduledExecutorService), dataSourceFactory); MediaItem.LocalConfiguration localConfiguration = checkNotNull(editedMediaItem.mediaItem.localConfiguration); - ListenableFuture future = bitmapLoader.loadBitmap(localConfiguration.uri); + @Nullable BitmapFactory.Options options = null; + if (Util.SDK_INT >= 26) { + options = new BitmapFactory.Options(); + options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); + } + ListenableFuture future = bitmapLoader.loadBitmap(localConfiguration.uri, options); Futures.addCallback( future, new FutureCallback() {