blob: 1265b2562fad5182703f87455097af89d673b584 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.feed;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.v7.content.res.AppCompatResources;
import android.text.TextUtils;
import com.google.android.libraries.feed.common.functional.Consumer;
import com.google.android.libraries.feed.host.imageloader.BundledAssets;
import com.google.android.libraries.feed.host.imageloader.ImageLoaderApi;
import org.chromium.base.Callback;
import org.chromium.base.DiscardableReferencePool;
import org.chromium.base.SysUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.cached_image_fetcher.CachedImageFetcher;
import org.chromium.chrome.browser.cached_image_fetcher.InMemoryCachedImageFetcher;
import org.chromium.chrome.browser.suggestions.ThumbnailGradient;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import java.util.Iterator;
import java.util.List;
/**
* Provides image loading and other host-specific asset fetches for Feed.
*/
public class FeedImageLoader implements ImageLoaderApi {
private static final String ASSET_PREFIX = "asset://";
private static final String OVERLAY_IMAGE_PREFIX = "overlay-image://";
private static final String OVERLAY_IMAGE_URL_PARAM = "url";
private static final String OVERLAY_IMAGE_DIRECTION_PARAM = "direction";
private static final String OVERLAY_IMAGE_DIRECTION_START = "start";
private static final String OVERLAY_IMAGE_DIRECTION_END = "end";
private Context mActivityContext;
private CachedImageFetcher mCachedImageFetcher;
/**
* Creates a FeedImageLoader for fetching image for the current user.
*
* @param activityContext Context of the user we are rendering the Feed for.
*/
public FeedImageLoader(Context activityContext, DiscardableReferencePool referencePool) {
mActivityContext = activityContext;
if (SysUtils.isLowEndDevice()) {
mCachedImageFetcher = CachedImageFetcher.getInstance();
} else {
mCachedImageFetcher = new InMemoryCachedImageFetcher(referencePool);
}
}
public void destroy() {
mCachedImageFetcher.destroy();
mCachedImageFetcher = null;
}
@Override
public void loadDrawable(
List<String> urls, int widthPx, int heightPx, Consumer<Drawable> consumer) {
loadDrawableWithIter(urls.iterator(), widthPx, heightPx, consumer);
}
/**
* Tries to load the next value in urlsIter, and recursively calls itself on failure to
* continue processing. Being recursive allows resuming after an async call across the bridge.
*
* @param urlsIter The stateful iterator of all urls to load. Each call removes one value.
* @param widthPx The width of the image in pixels. Will be {@link #DIMENSION_UNKNOWN} if
* unknown.
* @param heightPx The height of the image in pixels. Will be {@link #DIMENSION_UNKNOWN} if
* unknown.
* @param consumer The callback to be given the first successful image.
*/
private void loadDrawableWithIter(
Iterator<String> urlsIter, int widthPx, int heightPx, Consumer<Drawable> consumer) {
if (!urlsIter.hasNext() || mCachedImageFetcher == null) {
// Post to ensure callback is not run synchronously.
PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> consumer.accept(null));
return;
}
String url = urlsIter.next();
if (url.startsWith(ASSET_PREFIX)) {
Drawable drawable = getAssetDrawable(url);
if (drawable == null) {
loadDrawableWithIter(urlsIter, widthPx, heightPx, consumer);
} else {
// Post to ensure callback is not run synchronously.
PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> consumer.accept(drawable));
}
} else if (url.startsWith(OVERLAY_IMAGE_PREFIX)) {
Uri uri = Uri.parse(url);
int direction = overlayDirection(uri);
String sourceUrl = uri.getQueryParameter(OVERLAY_IMAGE_URL_PARAM);
assert !TextUtils.isEmpty(sourceUrl) : "Overlay image source URL empty";
fetchImage(sourceUrl, widthPx, heightPx, (Bitmap bitmap) -> {
if (bitmap == null) {
loadDrawableWithIter(urlsIter, widthPx, heightPx, consumer);
} else {
consumer.accept(ThumbnailGradient.createDrawableWithGradientIfNeeded(
bitmap, direction, mActivityContext.getResources()));
}
});
} else {
fetchImage(url, widthPx, heightPx, (Bitmap bitmap) -> {
if (bitmap == null) {
loadDrawableWithIter(urlsIter, widthPx, heightPx, consumer);
} else {
consumer.accept(new BitmapDrawable(mActivityContext.getResources(), bitmap));
}
});
}
}
/**
* @param url The fully qualified name of the resource.
* @return The resource as a Drawable on success, null otherwise.
*/
private Drawable getAssetDrawable(String url) {
String resourceName = url.substring(ASSET_PREFIX.length());
@DrawableRes
int id = lookupDrawableIdentifier(resourceName);
return id == 0 ? null : AppCompatResources.getDrawable(mActivityContext, id);
}
/**
* Translate {@Link BundledAssets} to android drawable resource. This method only translate
* resource name defined in {@Link BundledAssets}.
*
* @param resourceName The name of the drawable asset.
* @return The id of the drawable asset. May be 0 if it could not be found.
*/
private @DrawableRes int lookupDrawableIdentifier(String resourceName) {
switch (resourceName) {
case BundledAssets.OFFLINE_INDICATOR_BADGE:
return R.drawable.offline_pin_round;
case BundledAssets.VIDEO_INDICATOR_BADGE:
return R.drawable.ic_play_circle_filled_grey;
}
return 0;
}
/**
* Returns where the thumbnail is located in the card using the "direction" query param.
* @param overlayImageUri The URI for the overlay image.
* @return The direction in which the thumbnail is located relative to the card.
*/
private int overlayDirection(Uri overlayImageUri) {
String direction = overlayImageUri.getQueryParameter(OVERLAY_IMAGE_DIRECTION_PARAM);
assert TextUtils.equals(direction, OVERLAY_IMAGE_DIRECTION_START)
|| TextUtils.equals(direction, OVERLAY_IMAGE_DIRECTION_END)
: "Overlay image direction must be either start or end";
return TextUtils.equals(direction, OVERLAY_IMAGE_DIRECTION_START)
? ThumbnailGradient.ThumbnailLocation.START
: ThumbnailGradient.ThumbnailLocation.END;
}
@VisibleForTesting
protected void fetchImage(String url, int width, int height, Callback<Bitmap> callback) {
mCachedImageFetcher.fetchImage(url, width, height, callback);
}
@VisibleForTesting
FeedImageLoader(Context activityContext, CachedImageFetcher cachedImageFetcher) {
mActivityContext = activityContext;
mCachedImageFetcher = cachedImageFetcher;
}
}