// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.media;
import android.graphics.Bitmap;
import android.graphics.Rect;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.FileUtils;
import org.chromium.content_public.browser.ImageDownloadCallback;
import org.chromium.content_public.browser.WebContents;
import org.chromium.services.media_session.MediaImage;
import org.chromium.url.GURL;
import java.util.Iterator;
import java.util.List;
/**
* A class for managing the MediaImage download process.
*
* The manager takes a list of {@link MediaMetadata.MediaImage} as input, and
* selects one of them based on scoring and start download through
* {@link WebContents} asynchronously. When the download successfully finishes,
* the manager runs the callback function to notify the completion and pass the
* downloaded Bitmap.
*
* The scoring works as follows:
* - A image score is computed by multiplying the type score with the size score.
* - The type score lies in [0, 1] and is based on the image MIME type/file extension.
* - PNG and JPEG are prefered than others.
* - If unspecified, use the default type score (0.6).
* - The size score lies in [0, 1] and is computed by multiplying the dominant size score and aspect
* ratio score:
* - The dominant size score lies in [0, 1] and is computed using |mMinimumSize| and |mIdealSize|:
* - If size < |mMinimumSize| (too small), the size score is 0.
* - If |mMinimumSize| <= size <= |mIdealSize|, the score increases linearly from 0.2 to 1.
* - If size > |mIdealSize|, the score is |mIdealSize| / size, which drops from 1 to 0.
* - When the size is "any", the size score is 0.8.
* - If unspecified, use the default size score (0.4).
* - The aspect ratio score lies in [0, 1] and is computed by dividing the short edge length by
* the long edge.
*/
public class MediaImageManager implements ImageDownloadCallback {
// The default score of unknown image size.
private static final double DEFAULT_IMAGE_SIZE_SCORE = 0.4;
// The scores for different image types. Keep them sorted by value.
private static final double TYPE_SCORE_DEFAULT = 0.6;
private static final double TYPE_SCORE_PNG = 1.0;
private static final double TYPE_SCORE_JPEG = 0.7;
private static final double TYPE_SCORE_BMP = 0.5;
private static final double TYPE_SCORE_XICON = 0.4;
private static final double TYPE_SCORE_GIF = 0.3;
@VisibleForTesting static final int MAX_BITMAP_SIZE_FOR_DOWNLOAD = 2048;
private WebContents mWebContents;
// The minimum image size. Images that are smaller than |mMinimumSize| will be ignored.
final int mMinimumSize;
// The ideal image size. Images that are too large than |mIdealSize| will be ignored.
final int mIdealSize;
// The pending download image request id, which is set when calling
// {@link WebContents#downloadImage()}, and reset when image download completes or
// {@link #clearRequests()} is called.
private int mRequestId;
// The callback to be called when the pending download image request completes.
private MediaImageCallback mCallback;
// The last image src for download, used for avoiding fetching the same src when artwork is set
// multiple times but the same src is chosen.
//
// Will be reset when initiating a new download request.
private GURL mLastImageSrc;
/**
* MediaImageManager constructor.
* @param minimumSize The minimum size of images to download.
* @param idealSize The ideal size of images to download.
*/
public MediaImageManager(int minimumSize, int idealSize) {
mMinimumSize = minimumSize;
mIdealSize = idealSize;
clearRequests();
}
/**
* Called when the WebContent changes.
* @param contents The new WebContents.
*/
public void setWebContents(WebContents contents) {
mWebContents = contents;
clearRequests();
}
/**
* Select the best image from |images| and start download.
* @param images The list of images to choose from. Null is equivalent to empty list.
* @param callback The callback when image download completes.
*/
public void downloadImage(List<MediaImage> images, MediaImageCallback callback) {
if (mWebContents == null) return;
mCallback = callback;
MediaImage image = selectImage(images);
if (image == null) {
mLastImageSrc = null;
mCallback.onImageDownloaded(null);
clearRequests();
return;
}
// Avoid fetching the same image twice.
if (image.getSrc().equals(mLastImageSrc)) return;
mLastImageSrc = image.getSrc();
// Limit |maxBitmapSize| to |MAX_BITMAP_SIZE_FOR_DOWNLOAD| to avoid passing huge bitmaps
// through JNI. |maxBitmapSize| does not prevent huge images to be downloaded. It is used to
// filter/rescale the download images. See documentation of
// {@link WebContents#downloadImage()} for details.
mRequestId =
mWebContents.downloadImage(
image.getSrc(), // url
false, // isFavicon
MAX_BITMAP_SIZE_FOR_DOWNLOAD, // maxBitmapSize
false, // bypassCache
this); // callback
}
/**
* ImageDownloadCallback implementation. This method is called when an download image request is
* completed. The class will only keep the latest request. If some call to this method is
* corresponding to a previous request, it will be ignored.
*/
@Override
public void onFinishDownloadImage(
int id,
int httpStatusCode,
GURL imageUrl,
List<Bitmap> bitmaps,
List<Rect> originalImageSizes) {
if (id != mRequestId) return;
Iterator<Bitmap> iterBitmap = bitmaps.iterator();
Iterator<Rect> iterSize = originalImageSizes.iterator();
Bitmap bestBitmap = null;
double bestScore = 0;
while (iterBitmap.hasNext() && iterSize.hasNext()) {
Bitmap bitmap = iterBitmap.next();
Rect size = iterSize.next();
double newScore = getImageSizeScore(size);
if (bestScore < newScore) {
bestBitmap = bitmap;
bestScore = newScore;
}
}
mCallback.onImageDownloaded(bestBitmap);
clearRequests();
}
/**
* Select the best image from the |images|.
* @param images The list of images to select from. Null is equivalent to empty list.
*/
private MediaImage selectImage(List<MediaImage> images) {
if (images == null) return null;
MediaImage selectedImage = null;
double bestScore = 0;
for (MediaImage image : images) {
double newScore = getImageScore(image);
if (newScore > bestScore) {
bestScore = newScore;
selectedImage = image;
}
}
return selectedImage;
}
private void clearRequests() {
mRequestId = -1;
mCallback = null;
}
private double getImageScore(MediaImage image) {
if (image == null) return 0;
if (image.getSizes().isEmpty()) return DEFAULT_IMAGE_SIZE_SCORE;
double bestSizeScore = 0;
for (Rect size : image.getSizes()) {
bestSizeScore = Math.max(bestSizeScore, getImageSizeScore(size));
}
double typeScore = getImageTypeScore(image.getSrc(), image.getType());
return bestSizeScore * typeScore;
}
private double getImageSizeScore(Rect size) {
return getImageDominantSizeScore(size.width(), size.height())
* getImageAspectRatioScore(size.width(), size.height());
}
private double getImageDominantSizeScore(int width, int height) {
int dominantSize = Math.max(width, height);
// When the size is "any".
if (dominantSize == 0) return 0.8;
// Ignore images that are too small.
if (dominantSize < mMinimumSize) return 0;
if (dominantSize <= mIdealSize) {
return 0.8 * (dominantSize - mMinimumSize) / (mIdealSize - mMinimumSize) + 0.2;
}
return 1.0 * mIdealSize / dominantSize;
}
private double getImageAspectRatioScore(int width, int height) {
double longEdge = Math.max(width, height);
double shortEdge = Math.min(width, height);
return shortEdge / longEdge;
}
private double getImageTypeScore(GURL url, String type) {
String extension = FileUtils.getExtension(url.getSpec());
if ("bmp".equals(extension) || "image/bmp".equals(type)) {
return TYPE_SCORE_BMP;
} else if ("gif".equals(extension) || "image/gif".equals(type)) {
return TYPE_SCORE_GIF;
} else if ("icon".equals(extension) || "image/x-icon".equals(type)) {
return TYPE_SCORE_XICON;
} else if ("png".equals(extension) || "image/png".equals(type)) {
return TYPE_SCORE_PNG;
} else if ("jpeg".equals(extension)
|| "jpg".equals(extension)
|| "image/jpeg".equals(type)) {
return TYPE_SCORE_JPEG;
}
return TYPE_SCORE_DEFAULT;
}
}