| // Copyright 2017 The Cobalt Authors. All Rights Reserved. |
| // |
| // 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 dev.cobalt.media; |
| |
| import static dev.cobalt.media.Log.TAG; |
| |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.util.Pair; |
| import android.util.Size; |
| import androidx.annotation.NonNull; |
| import dev.cobalt.util.DisplayUtil; |
| import dev.cobalt.util.Log; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.HttpURLConnection; |
| import java.net.URL; |
| |
| /** Loads MediaImage artwork, and caches one image. */ |
| public class ArtworkLoader { |
| |
| /** Callback to receive the image loaded in the background by getOrLoadArtwork() */ |
| public interface Callback { |
| void onArtworkLoaded(Bitmap bitmap); |
| } |
| |
| @NonNull private volatile String requestedArtworkUrl = ""; |
| @NonNull private volatile String currentArtworkUrl = ""; |
| private volatile Bitmap currentArtwork = null; |
| |
| private final Handler handler = new Handler(Looper.getMainLooper()); |
| private final Callback callback; |
| |
| public ArtworkLoader(Callback callback) { |
| this.callback = callback; |
| } |
| |
| /** |
| * Returns a cached image if available. If not cached, returns null and starts downloading it in |
| * the background, and then when ready the callback will be called with the image. |
| */ |
| public synchronized Bitmap getOrLoadArtwork(MediaImage[] artwork) { |
| MediaImage image = getBestFitImage(artwork, DisplayUtil.getDisplaySize()); |
| String url = (image == null) ? "" : image.src; |
| |
| // Check if this artwork is already loaded or requested. |
| if (url.equals(currentArtworkUrl)) { |
| return currentArtwork; |
| } else if (url.equals(requestedArtworkUrl)) { |
| return null; |
| } |
| |
| requestedArtworkUrl = url; |
| new DownloadArtworkThread(url, handler).start(); |
| return null; |
| } |
| |
| /** |
| * Returns the image that most closely matches the display size, or null if there are no images. |
| * We don't really know what size view the image may appear in (on the Now Playing card on Android |
| * TV launcher, or any other observer of the MediaSession), so we use display size as the largest |
| * useful size on any particular device. |
| */ |
| private MediaImage getBestFitImage(MediaImage[] artwork, Size displaySize) { |
| if (artwork == null || artwork.length == 0) { |
| return null; |
| } |
| MediaImage bestImage = artwork[0]; |
| int minDiagonalSquared = Integer.MAX_VALUE; |
| for (MediaImage image : artwork) { |
| Size imageSize = parseImageSize(image); |
| int widthDelta = displaySize.getWidth() - imageSize.getWidth(); |
| int heightDelta = displaySize.getHeight() - imageSize.getHeight(); |
| int diagonalSquared = widthDelta * widthDelta + heightDelta * heightDelta; |
| if (diagonalSquared < minDiagonalSquared) { |
| bestImage = image; |
| minDiagonalSquared = diagonalSquared; |
| } |
| } |
| return bestImage; |
| } |
| |
| private Size parseImageSize(MediaImage image) { |
| try { |
| String sizeStr = image.sizes.split("\\s+", -1)[0]; |
| return Size.parseSize(sizeStr.toLowerCase()); |
| } catch (NumberFormatException | NullPointerException e) { |
| return new Size(0, 0); |
| } |
| } |
| |
| private synchronized void onDownloadFinished(Pair<String, Bitmap> urlBitmapPair) { |
| String url = urlBitmapPair.first; |
| Bitmap bitmap = urlBitmapPair.second; |
| if (url.equals(requestedArtworkUrl)) { |
| requestedArtworkUrl = ""; |
| if (bitmap != null) { |
| currentArtworkUrl = url; |
| currentArtwork = bitmap; |
| callback.onArtworkLoaded(bitmap); |
| } |
| } |
| } |
| |
| private class DownloadArtworkThread extends Thread { |
| |
| private final String url; |
| private final Handler handler; |
| |
| DownloadArtworkThread(String url, Handler handler) { |
| super("ArtworkLoader"); |
| this.url = url; |
| this.handler = handler; |
| } |
| |
| @Override |
| public void run() { |
| Bitmap bitmap = null; |
| HttpURLConnection conn = null; |
| InputStream is = null; |
| try { |
| conn = (HttpURLConnection) new URL(url).openConnection(); |
| is = conn.getInputStream(); |
| bitmap = BitmapFactory.decodeStream(is); |
| } catch (IOException e) { |
| Log.e(TAG, "Could not download artwork", e); |
| } finally { |
| try { |
| if (conn != null) { |
| conn.disconnect(); |
| } |
| if (is != null) { |
| is.close(); |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Error closing connection for artwork", e); |
| } |
| } |
| |
| // Crop to 16:9 as needed |
| if (bitmap != null) { |
| int height = bitmap.getWidth() * 9 / 16; |
| if (bitmap.getHeight() > height) { |
| int top = (bitmap.getHeight() - height) / 2; |
| bitmap = Bitmap.createBitmap(bitmap, 0, top, bitmap.getWidth(), height); |
| } |
| } |
| |
| final Pair<String, Bitmap> urlBitmapPair = Pair.create(url, bitmap); |
| handler.post( |
| new Runnable() { |
| @Override |
| public void run() { |
| onDownloadFinished(urlBitmapPair); |
| } |
| }); |
| } |
| } |
| } |