Create BitmapLoader
Create BitmapLoader component for loading artwork images. Add the SimpleBitmapLoader which fetches images from HTTP/HTTPS endpoints. Integrate BitmapLoader in DefaultMediaNotificationProvider. PiperOrigin-RevId: 429010249
This commit is contained in:
parent
b8687a3111
commit
b59869ede3
@ -16,14 +16,27 @@ apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle"
|
||||
// TODO(b/178560255): Remove the "group" override after the "group" in build.gradle changed
|
||||
group 'androidx.media3'
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
multiDexEnabled true
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
testCoverageEnabled = true
|
||||
}
|
||||
}
|
||||
sourceSets.androidTest.assets.srcDir '../test_data/src/test/assets/'
|
||||
}
|
||||
dependencies {
|
||||
api project(modulePrefix + 'lib-common')
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
implementation 'androidx.collection:collection:' + androidxCollectionVersion
|
||||
implementation 'androidx.media:media:' + androidxMediaVersion
|
||||
|
||||
testImplementation project(modulePrefix + 'test-utils')
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:' + okhttpVersion
|
||||
androidTestImplementation project(modulePrefix + 'test-utils')
|
||||
androidTestImplementation project(modulePrefix + 'test-data')
|
||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||
}
|
||||
|
||||
|
34
libraries/session/src/androidTest/AndroidManifest.xml
Normal file
34
libraries/session/src/androidTest/AndroidManifest.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2022 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="androidx.media3.session.test">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"
|
||||
android:usesCleartextTraffic="true"/>
|
||||
|
||||
<instrumentation
|
||||
android:targetPackage="androidx.media3.session.test"
|
||||
android:name="androidx.test.runner.AndroidJUnitRunner"/>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.session;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okio.Buffer;
|
||||
import org.junit.Test;
|
||||
import org.junit.function.ThrowingRunnable;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleBitmapLoader}.
|
||||
*
|
||||
* <p>This test needs to run as an androidTest because robolectric's BitmapFactory is not fully
|
||||
* functional.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SimpleBitmapLoaderTest {
|
||||
|
||||
private static final String TEST_IMAGE_PATH = "media/jpeg/non-motion-photo-shortened.jpg";
|
||||
|
||||
@Test
|
||||
public void loadData() throws Exception {
|
||||
SimpleBitmapLoader bitmapLoader =
|
||||
new SimpleBitmapLoader(MoreExecutors.newDirectExecutorService());
|
||||
byte[] imageData =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH);
|
||||
|
||||
Bitmap bitmap = bitmapLoader.decodeBitmap(imageData).get();
|
||||
|
||||
assertThat(
|
||||
bitmap.sameAs(
|
||||
BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length)))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadData_withInvalidData_throwsException() {
|
||||
SimpleBitmapLoader bitmapLoader =
|
||||
new SimpleBitmapLoader(MoreExecutors.newDirectExecutorService());
|
||||
|
||||
ListenableFuture<Bitmap> future = bitmapLoader.decodeBitmap(new byte[0]);
|
||||
|
||||
assertException(
|
||||
future::get, IllegalArgumentException.class, /* messagePart= */ "Could not decode bitmap");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUri_loadsImage() throws Exception {
|
||||
SimpleBitmapLoader bitmapLoader =
|
||||
new SimpleBitmapLoader(MoreExecutors.newDirectExecutorService());
|
||||
MockWebServer mockWebServer = new MockWebServer();
|
||||
byte[] imageData =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH);
|
||||
Buffer responseBody = new Buffer().write(imageData);
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody));
|
||||
|
||||
Bitmap bitmap =
|
||||
bitmapLoader.loadBitmap(Uri.parse(mockWebServer.url("test_path").toString())).get();
|
||||
|
||||
assertThat(
|
||||
bitmap.sameAs(
|
||||
BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length)))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUri_serverError_throwsException() {
|
||||
SimpleBitmapLoader bitmapLoader =
|
||||
new SimpleBitmapLoader(MoreExecutors.newDirectExecutorService());
|
||||
MockWebServer mockWebServer = new MockWebServer();
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(404));
|
||||
|
||||
ListenableFuture<Bitmap> future =
|
||||
bitmapLoader.loadBitmap(Uri.parse(mockWebServer.url("test_path").toString()));
|
||||
|
||||
assertException(future::get, IOException.class, /* messagePart= */ "Invalid response status");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadUri_nonHttpUri_throwsException() {
|
||||
SimpleBitmapLoader bitmapLoader =
|
||||
new SimpleBitmapLoader(MoreExecutors.newDirectExecutorService());
|
||||
|
||||
assertException(
|
||||
() -> bitmapLoader.loadBitmap(Uri.parse("/local/path")).get(),
|
||||
MalformedURLException.class,
|
||||
/* messagePart= */ "no protocol");
|
||||
assertException(
|
||||
() -> bitmapLoader.loadBitmap(Uri.parse("file://local/path")).get(),
|
||||
UnsupportedOperationException.class,
|
||||
/* messagePart= */ "Unsupported scheme");
|
||||
assertException(
|
||||
() -> bitmapLoader.loadBitmap(Uri.parse("asset://asset/path")).get(),
|
||||
MalformedURLException.class,
|
||||
/* messagePart= */ "unknown protocol");
|
||||
assertException(
|
||||
() -> bitmapLoader.loadBitmap(Uri.parse("raw://raw/path")).get(),
|
||||
MalformedURLException.class,
|
||||
/* messagePart= */ "unknown protocol");
|
||||
assertException(
|
||||
() -> bitmapLoader.loadBitmap(Uri.parse("data://data")).get(),
|
||||
MalformedURLException.class,
|
||||
/* messagePart= */ "unknown protocol");
|
||||
}
|
||||
|
||||
private static void assertException(
|
||||
ThrowingRunnable runnable, Class<? extends Exception> clazz, String messagePart) {
|
||||
ExecutionException executionException = assertThrows(ExecutionException.class, runnable);
|
||||
assertThat(executionException).hasCauseThat().isInstanceOf(clazz);
|
||||
assertThat(executionException).hasMessageThat().contains(messagePart);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
package androidx.media3.session;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
/** Loads images. */
|
||||
@UnstableApi
|
||||
public interface BitmapLoader {
|
||||
/** Decodes an image from compressed binary data. */
|
||||
ListenableFuture<Bitmap> decodeBitmap(byte[] data);
|
||||
/** Loads an image from {@code uri}. */
|
||||
ListenableFuture<Bitmap> loadBitmap(Uri uri);
|
||||
}
|
@ -16,19 +16,30 @@
|
||||
package androidx.media3.session;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
import static androidx.media3.common.util.Util.castNonNull;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.util.Consumer;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* The default {@link MediaNotification.Provider}.
|
||||
@ -60,21 +71,37 @@ import androidx.media3.common.util.Util;
|
||||
* </ul>
|
||||
*/
|
||||
@UnstableApi
|
||||
/* package */ final class DefaultMediaNotificationProvider implements MediaNotification.Provider {
|
||||
|
||||
public final class DefaultMediaNotificationProvider implements MediaNotification.Provider {
|
||||
private static final String TAG = "NotificationProvider";
|
||||
private static final int NOTIFICATION_ID = 1001;
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "default_channel_id";
|
||||
private static final String NOTIFICATION_CHANNEL_NAME = "Now playing";
|
||||
|
||||
private final Context context;
|
||||
private final NotificationManager notificationManager;
|
||||
private final BitmapLoader bitmapLoader;
|
||||
// Cache the last loaded bitmap to avoid reloading the bitmap again, particularly useful when
|
||||
// showing a notification for the same item (e.g. when switching from playing to paused).
|
||||
private final LoadedBitmapInfo lastLoadedBitmapInfo;
|
||||
private final Handler mainHandler;
|
||||
|
||||
/** Creates an instance. */
|
||||
private OnBitmapLoadedFutureCallback pendingOnBitmapLoadedFutureCallback;
|
||||
|
||||
/** Creates an instance that uses a {@link SimpleBitmapLoader} for loading artwork images. */
|
||||
public DefaultMediaNotificationProvider(Context context) {
|
||||
this(context, new SimpleBitmapLoader());
|
||||
}
|
||||
|
||||
/** Creates an instance that uses the {@code bitmapLoader} for loading artwork images. */
|
||||
public DefaultMediaNotificationProvider(Context context, BitmapLoader bitmapLoader) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.bitmapLoader = bitmapLoader;
|
||||
lastLoadedBitmapInfo = new LoadedBitmapInfo();
|
||||
mainHandler = new Handler(Looper.getMainLooper());
|
||||
notificationManager =
|
||||
checkStateNotNull(
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
|
||||
pendingOnBitmapLoadedFutureCallback = new OnBitmapLoadedFutureCallback(bitmap -> {});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -118,10 +145,28 @@ import androidx.media3.common.util.Util;
|
||||
// Set metadata info in the notification.
|
||||
MediaMetadata metadata = mediaController.getMediaMetadata();
|
||||
builder.setContentTitle(metadata.title).setContentText(metadata.artist);
|
||||
if (metadata.artworkData != null) {
|
||||
Bitmap artworkBitmap =
|
||||
BitmapFactory.decodeByteArray(metadata.artworkData, 0, metadata.artworkData.length);
|
||||
builder.setLargeIcon(artworkBitmap);
|
||||
|
||||
@Nullable ListenableFuture<Bitmap> bitmapFuture = loadArtworkBitmap(metadata);
|
||||
if (bitmapFuture != null) {
|
||||
if (bitmapFuture.isDone()) {
|
||||
try {
|
||||
builder.setLargeIcon(Futures.getDone(bitmapFuture));
|
||||
} catch (ExecutionException e) {
|
||||
Log.w(TAG, "Failed to load bitmap", e);
|
||||
}
|
||||
} else {
|
||||
Futures.addCallback(
|
||||
bitmapFuture,
|
||||
new OnBitmapLoadedFutureCallback(
|
||||
bitmap -> {
|
||||
builder.setLargeIcon(bitmap);
|
||||
onNotificationChangedCallback.onNotificationChanged(
|
||||
new MediaNotification(NOTIFICATION_ID, builder.build()));
|
||||
}),
|
||||
// This callback must be executed on the next looper iteration, after this method has
|
||||
// returned a media notification.
|
||||
mainHandler::post);
|
||||
}
|
||||
}
|
||||
|
||||
androidx.media.app.NotificationCompat.MediaStyle mediaStyle =
|
||||
@ -162,6 +207,41 @@ import androidx.media3.common.util.Util;
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests from the bitmapLoader to load artwork or returns null if the metadata don't include
|
||||
* artwork.
|
||||
*/
|
||||
@Nullable
|
||||
private ListenableFuture<Bitmap> loadArtworkBitmap(MediaMetadata metadata) {
|
||||
if (lastLoadedBitmapInfo.matches(metadata.artworkData)
|
||||
|| lastLoadedBitmapInfo.matches(metadata.artworkUri)) {
|
||||
return Futures.immediateFuture(lastLoadedBitmapInfo.getBitmap());
|
||||
}
|
||||
|
||||
ListenableFuture<Bitmap> future;
|
||||
Consumer<Bitmap> onBitmapLoaded;
|
||||
if (metadata.artworkData != null) {
|
||||
future = bitmapLoader.decodeBitmap(metadata.artworkData);
|
||||
onBitmapLoaded =
|
||||
bitmap -> lastLoadedBitmapInfo.setBitmapInfo(castNonNull(metadata.artworkData), bitmap);
|
||||
} else if (metadata.artworkUri != null) {
|
||||
future = bitmapLoader.loadBitmap(metadata.artworkUri);
|
||||
onBitmapLoaded =
|
||||
bitmap -> lastLoadedBitmapInfo.setBitmapInfo(castNonNull(metadata.artworkUri), bitmap);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
pendingOnBitmapLoadedFutureCallback.discardIfPending();
|
||||
pendingOnBitmapLoadedFutureCallback = new OnBitmapLoadedFutureCallback(onBitmapLoaded);
|
||||
Futures.addCallback(
|
||||
future,
|
||||
pendingOnBitmapLoadedFutureCallback,
|
||||
// It's ok to run this immediately to update the last loaded bitmap.
|
||||
runnable -> Util.postOrRun(mainHandler, runnable));
|
||||
return future;
|
||||
}
|
||||
|
||||
private static int getSmallIconResId(Context context) {
|
||||
int appIcon = context.getApplicationInfo().icon;
|
||||
if (appIcon != 0) {
|
||||
@ -170,4 +250,67 @@ import androidx.media3.common.util.Util;
|
||||
return Util.SDK_INT >= 21 ? R.drawable.media_session_service_notification_ic_music_note : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnBitmapLoadedFutureCallback implements FutureCallback<Bitmap> {
|
||||
|
||||
private final Consumer<Bitmap> consumer;
|
||||
|
||||
private boolean discarded;
|
||||
|
||||
private OnBitmapLoadedFutureCallback(Consumer<Bitmap> consumer) {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
public void discardIfPending() {
|
||||
discarded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(Bitmap result) {
|
||||
if (!discarded) {
|
||||
consumer.accept(result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
if (!discarded) {
|
||||
Log.d(TAG, "Failed to load bitmap", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the last loaded bitmap. The key to identify a bitmap is either a byte array, if the
|
||||
* bitmap is loaded from compressed data, or a URI, if the bitmap was loaded from a URI.
|
||||
*/
|
||||
private static class LoadedBitmapInfo {
|
||||
@Nullable private byte[] data;
|
||||
@Nullable private Uri uri;
|
||||
@Nullable private Bitmap bitmap;
|
||||
|
||||
public boolean matches(@Nullable byte[] data) {
|
||||
return this.data != null && data != null && Arrays.equals(this.data, data);
|
||||
}
|
||||
|
||||
public boolean matches(@Nullable Uri uri) {
|
||||
return this.uri != null && this.uri.equals(uri);
|
||||
}
|
||||
|
||||
public Bitmap getBitmap() {
|
||||
return checkStateNotNull(bitmap);
|
||||
}
|
||||
|
||||
public void setBitmapInfo(byte[] data, Bitmap bitmap) {
|
||||
this.data = data;
|
||||
this.bitmap = bitmap;
|
||||
this.uri = null;
|
||||
}
|
||||
|
||||
public void setBitmapInfo(Uri uri, Bitmap bitmap) {
|
||||
this.uri = uri;
|
||||
this.bitmap = bitmap;
|
||||
this.data = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
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.UnstableApi;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* A simple bitmap loader that delegates all tasks to an executor and supports fetching images from
|
||||
* HTTP/HTTPS endpoints.
|
||||
*
|
||||
* <p>Loading tasks are delegated to an {@link ExecutorService} (or {@link
|
||||
* ListeningExecutorService}) defined during construction. If no executor service is defined, all
|
||||
* tasks are delegated to a single-thread executor service that is shared between instances of this
|
||||
* class.
|
||||
*
|
||||
* <p>The supported URI scheme is only HTTP/HTTPS and this class reads a resource only when the
|
||||
* endpoint responds with an {@code HTTP 200} after sending the HTTP request.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class SimpleBitmapLoader implements BitmapLoader {
|
||||
|
||||
private static final Supplier<ListeningExecutorService> DEFAULT_EXECUTOR_SERVICE =
|
||||
Suppliers.memoize(
|
||||
() -> MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()));
|
||||
|
||||
private final ListeningExecutorService executorService;
|
||||
|
||||
/**
|
||||
* Creates an instance that delegates all load tasks to a single-thread executor service shared
|
||||
* between instances.
|
||||
*/
|
||||
public SimpleBitmapLoader() {
|
||||
this(checkStateNotNull(DEFAULT_EXECUTOR_SERVICE.get()));
|
||||
}
|
||||
|
||||
/** Creates an instance that delegates loading tasks to the {@code executorService}. */
|
||||
public SimpleBitmapLoader(ExecutorService executorService) {
|
||||
this.executorService = MoreExecutors.listeningDecorator(executorService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Bitmap> decodeBitmap(byte[] data) {
|
||||
return executorService.submit(() -> decode(data));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Bitmap> loadBitmap(Uri uri) {
|
||||
return executorService.submit(() -> load(uri));
|
||||
}
|
||||
|
||||
private static Bitmap decode(byte[] data) {
|
||||
@Nullable Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length);
|
||||
if (bitmap == null) {
|
||||
throw new IllegalArgumentException("Could not decode bitmap");
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private static Bitmap load(Uri uri) throws IOException {
|
||||
URLConnection connection = new URL(uri.toString()).openConnection();
|
||||
if (!(connection instanceof HttpURLConnection)) {
|
||||
throw new UnsupportedOperationException("Unsupported scheme: " + uri.getScheme());
|
||||
}
|
||||
HttpURLConnection httpConnection = (HttpURLConnection) connection;
|
||||
httpConnection.connect();
|
||||
int responseCode = httpConnection.getResponseCode();
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw new IOException("Invalid response status code: " + responseCode);
|
||||
}
|
||||
try (InputStream inputStream = httpConnection.getInputStream()) {
|
||||
return decode(ByteStreams.toByteArray(inputStream));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user