Commit a5ef437e authored by David Trainor's avatar David Trainor Committed by Commit Bot

Download Home - Add thumbnail support to new code path

Adds support for items querying thumbnails.  This is the same capability
that exists in the current downloads home.  This change has the
following details;

- Exposes a callback to get and cancel visual requests to the View
layer through the model.
- Adds the concept of a ViewHolder that knows how to request for
thumbnail assets.
- Connects the thumbnail request to the ThumbnailProviderImpl and
propertly interacts with the download backend and the
OfflineContentProvider backends.

Bug: 842345
Change-Id: I884a9d46bcece9dba08707f97f40a5b15936ddf0
Reviewed-on: https://chromium-review.googlesource.com/1084077
Commit-Queue: David Trainor <dtrainor@chromium.org>
Reviewed-by: default avatarMin Qin <qinmin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#564750}
parent 097ca56d
......@@ -5,6 +5,7 @@
package org.chromium.chrome.browser.download.home.glue;
import android.os.Handler;
import android.text.TextUtils;
import org.chromium.base.Callback;
import org.chromium.base.CollectionUtil;
......@@ -52,12 +53,14 @@ public class DownloadGlue implements DownloadObserver {
/** @see OfflineContentProvider.Observer#onItemsAdded(ArrayList) */
@Override
public void onDownloadItemCreated(DownloadItem item) {
if (!canShowDownloadItem(item)) return;
mDelegate.onItemsAdded(CollectionUtil.newArrayList(DownloadItem.createOfflineItem(item)));
}
/** @see OfflineContentProvider.Observer#onItemUpdated(OfflineItem) */
@Override
public void onDownloadItemUpdated(DownloadItem item) {
if (!canShowDownloadItem(item)) return;
mDelegate.onItemUpdated(DownloadItem.createOfflineItem(item));
}
......@@ -75,6 +78,7 @@ public class DownloadGlue implements DownloadObserver {
ArrayList<OfflineItem> offlineItems = new ArrayList<>();
for (DownloadItem item : items) {
if (!canShowDownloadItem(item)) continue;
offlineItems.add(DownloadItem.createOfflineItem(item));
}
......@@ -139,4 +143,15 @@ public class DownloadGlue implements DownloadObserver {
public void getVisualsForItem(ContentId id, VisualsCallback callback) {
new Handler().post(() -> callback.onVisualsAvailable(id, null));
}
/**
* There could be some situations where we can't visually represent this download in the UI.
* This should be handled in native/be more generic, but it's here in the glue for now.
* @return Whether or not {@code item} should be shown in the UI.
*/
private static boolean canShowDownloadItem(DownloadItem item) {
if (TextUtils.isEmpty(item.getDownloadInfo().getFilePath())) return false;
if (TextUtils.isEmpty(item.getDownloadInfo().getFileName())) return false;
return true;
}
}
\ No newline at end of file
......@@ -5,7 +5,11 @@
package org.chromium.chrome.browser.download.home.glue;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.widget.ThumbnailProvider;
import org.chromium.chrome.browser.widget.ThumbnailProviderImpl;
import org.chromium.components.offline_items_collection.ContentId;
import org.chromium.components.offline_items_collection.LegacyHelpers;
import org.chromium.components.offline_items_collection.OfflineContentProvider;
......@@ -27,6 +31,7 @@ public class OfflineContentProviderGlue implements OfflineContentProvider.Observ
private final boolean mIncludeOffTheRecord;
private final DownloadGlue mDownloadProvider;
private final ThumbnailProvider mThumbnailProvider;
private Query mOutstandingQuery;
......@@ -36,6 +41,8 @@ public class OfflineContentProviderGlue implements OfflineContentProvider.Observ
mProvider = provider;
mIncludeOffTheRecord = includeOffTheRecord;
mDownloadProvider = new DownloadGlue(this);
mThumbnailProvider = new ThumbnailProviderImpl(
((ChromeApplication) ContextUtils.getApplicationContext()).getReferencePool());
mProvider.addObserver(this);
}
......@@ -112,13 +119,14 @@ public class OfflineContentProviderGlue implements OfflineContentProvider.Observ
mOutstandingQuery.add(callback);
}
/** @see OfflineContentProvider#getVisualsForItem(ContentId, VisualsCallback) */
public void getVisualsForItem(ContentId id, VisualsCallback callback) {
if (LegacyHelpers.isLegacyDownload(id)) {
mDownloadProvider.getVisualsForItem(id, callback);
} else {
mProvider.getVisualsForItem(id, callback);
}
/**
* @return Whether or not querying for a thumbail for this {@code id} is supported.
* @see OfflineContentProvider#getVisualsForItem(ContentId, VisualsCallback)
*/
public boolean getVisualsForItem(ContentId id, VisualsCallback callback) {
if (LegacyHelpers.isLegacyDownload(id)) return false;
mProvider.getVisualsForItem(id, callback);
return true;
}
/** @see OfflineContentProvider#addObserver(OfflineContentProvider.Observer) */
......
// 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.download.home.glue;
import android.graphics.Bitmap;
import org.chromium.base.Callback;
import org.chromium.chrome.browser.widget.ThumbnailProvider.ThumbnailRequest;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OfflineItemVisuals;
import org.chromium.components.offline_items_collection.VisualsCallback;
/**
* Glue class responsible for connecting the current downloads and {@link OfflineContentProvider}
* thumbnail work to the {@link ThumbnailProvider} via a custon {@link ThumbnailProviderImpl}.
*/
public class ThumbnailRequestGlue implements ThumbnailRequest {
private final OfflineContentProviderGlue mProvider;
private final OfflineItem mItem;
private final int mIconWidthPx;
private final int mIconHeightPx;
private final VisualsCallback mCallback;
/** Creates a {@link ThumbnailRequestGlue} instance. */
public ThumbnailRequestGlue(OfflineContentProviderGlue provider, OfflineItem item,
int iconWidthPx, int iconHeightPx, VisualsCallback callback) {
mProvider = provider;
mItem = item;
mIconWidthPx = iconWidthPx;
mIconHeightPx = iconHeightPx;
mCallback = callback;
}
// ThumbnailRequest implementation.
@Override
public String getFilePath() {
return mItem.filePath;
}
@Override
public String getContentId() {
return mItem.id.id;
}
@Override
public void onThumbnailRetrieved(String contentId, Bitmap thumbnail) {
OfflineItemVisuals visuals = null;
if (thumbnail != null) {
visuals = new OfflineItemVisuals();
visuals.icon = thumbnail;
}
mCallback.onVisualsAvailable(mItem.id, visuals);
}
@Override
public int getIconSize() {
return mIconWidthPx;
}
@Override
public boolean getThumbnail(Callback<Bitmap> callback) {
return mProvider.getVisualsForItem(mItem.id, (id, visuals) -> {
if (visuals == null) {
callback.onResult(null);
} else {
callback.onResult(Bitmap.createScaledBitmap(
visuals.icon, mIconWidthPx, mIconHeightPx, false));
}
});
}
}
\ No newline at end of file
......@@ -6,6 +6,8 @@ package org.chromium.chrome.browser.download.home.list;
import android.os.Handler;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.download.home.OfflineItemSource;
import org.chromium.chrome.browser.download.home.filter.DeleteUndoOfflineItemFilter;
import org.chromium.chrome.browser.download.home.filter.Filters.FilterType;
......@@ -14,7 +16,13 @@ import org.chromium.chrome.browser.download.home.filter.OfflineItemFilterSource;
import org.chromium.chrome.browser.download.home.filter.SearchOfflineItemFilter;
import org.chromium.chrome.browser.download.home.filter.TypeOfflineItemFilter;
import org.chromium.chrome.browser.download.home.glue.OfflineContentProviderGlue;
import org.chromium.chrome.browser.download.home.glue.ThumbnailRequestGlue;
import org.chromium.chrome.browser.widget.ThumbnailProvider;
import org.chromium.chrome.browser.widget.ThumbnailProvider.ThumbnailRequest;
import org.chromium.chrome.browser.widget.ThumbnailProviderImpl;
import org.chromium.components.offline_items_collection.OfflineContentProvider;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.VisualsCallback;
import java.io.Closeable;
......@@ -23,11 +31,14 @@ import java.io.Closeable;
* home. This includes support for filtering, deleting, etc..
*/
class DateOrderedListMediator {
private final Handler mHandler = new Handler();
private final OfflineContentProviderGlue mProvider;
private final ListItemModel mModel;
private final OfflineItemSource mSource;
private final DateOrderedListMutator mListMutator;
private final ThumbnailProvider mThumbnailProvider;
private final OffTheRecordOfflineItemFilter mOffTheRecordFilter;
private final DeleteUndoOfflineItemFilter mDeleteUndoFilter;
......@@ -63,6 +74,9 @@ class DateOrderedListMediator {
mSearchFilter = new SearchOfflineItemFilter(mTypeFilter);
mListMutator = new DateOrderedListMutator(mSearchFilter, mModel);
mThumbnailProvider = new ThumbnailProviderImpl(
((ChromeApplication) ContextUtils.getApplicationContext()).getReferencePool());
mModel.getProperties().setEnableItemAnimations(true);
mModel.getProperties().setOpenCallback(item -> mProvider.openItem(item));
mModel.getProperties().setPauseCallback(item -> mProvider.pauseDownload(item));
......@@ -71,12 +85,15 @@ class DateOrderedListMediator {
mModel.getProperties().setShareCallback(item -> {});
// TODO(dtrainor): Pipe into the undo snackbar and the DeleteUndoOfflineItemFilter.
mModel.getProperties().setRemoveCallback(item -> mProvider.removeItem(item));
mModel.getProperties().setVisualsProvider(
(item, w, h, callback) -> { return getVisuals(item, w, h, callback); });
}
/** Tears down this mediator. */
public void destroy() {
mSource.destroy();
mProvider.destroy();
mThumbnailProvider.destroy();
}
/**
......@@ -107,6 +124,19 @@ class DateOrderedListMediator {
return mDeleteUndoFilter;
}
private Runnable getVisuals(
OfflineItem item, int iconWidthPx, int iconHeightPx, VisualsCallback callback) {
if (!UiUtils.canHaveThumbnails(item)) {
mHandler.post(() -> callback.onVisualsAvailable(item.id, null));
return () -> {};
}
ThumbnailRequest request =
new ThumbnailRequestGlue(mProvider, item, iconWidthPx, iconHeightPx, callback);
mThumbnailProvider.getThumbnail(request);
return () -> mThumbnailProvider.cancelRetrieval(request);
}
/** Helper class to disable animations for certain list changes. */
private class AnimationDisableClosable implements Closeable {
AnimationDisableClosable() {
......@@ -116,7 +146,7 @@ class DateOrderedListMediator {
// Closeable implementation.
@Override
public void close() {
new Handler().post(() -> mModel.getProperties().setEnableItemAnimations(true));
mHandler.post(() -> mModel.getProperties().setEnableItemAnimations(true));
}
}
}
\ No newline at end of file
......@@ -4,6 +4,9 @@
package org.chromium.chrome.browser.download.home.list;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.CallSuper;
import android.support.v7.widget.AppCompatTextView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.View;
......@@ -12,9 +15,13 @@ import android.view.ViewGroup.LayoutParams;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.browser.download.home.list.ListItem.DateListItem;
import org.chromium.chrome.browser.download.home.list.ListItem.OfflineItemListItem;
import org.chromium.chrome.browser.download.home.list.ListItem.ViewListItem;
import org.chromium.components.offline_items_collection.ContentId;
import org.chromium.components.offline_items_collection.OfflineItemVisuals;
import org.chromium.components.offline_items_collection.VisualsCallback;
/**
* A {@link ViewHolder} responsible for building and setting properties on the underlying Android
......@@ -40,7 +47,7 @@ abstract class ListItemViewHolder extends ViewHolder {
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
// DateOrderedViewListHolder implemenation.
// ListItemViewHolder implemenation.
@Override
public void bind(ListPropertyModel properties, ListItem item) {
ViewListItem viewItem = (ViewListItem) item;
......@@ -62,7 +69,7 @@ abstract class ListItemViewHolder extends ViewHolder {
super(new AppCompatTextView(parent.getContext()));
}
// DateOrderedListViewHolder implementation.
// ListItemViewHolder implementation.
@Override
public void bind(ListPropertyModel properties, ListItem item) {
DateListItem dateItem = (DateListItem) item;
......@@ -76,7 +83,7 @@ abstract class ListItemViewHolder extends ViewHolder {
super(new AppCompatTextView(parent.getContext()));
}
// DateOrderedListViewHolder implementation.
// ListItemViewHolder implementation.
@Override
public void bind(ListPropertyModel properties, ListItem item) {
OfflineItemListItem offlineItem = (OfflineItemListItem) item;
......@@ -85,17 +92,29 @@ abstract class ListItemViewHolder extends ViewHolder {
}
/** A {@link ViewHolder} specifically meant to display a generic {@code OfflineItem}. */
public static class GenericViewHolder extends ListItemViewHolder {
public static class GenericViewHolder extends ThumbnailAwareViewHolder {
public GenericViewHolder(ViewGroup parent) {
super(new AppCompatTextView(parent.getContext()));
}
// DateOrderedListViewHolder implementation.
// ListItemViewHolder implementation.
@Override
public void bind(ListPropertyModel properties, ListItem item) {
super.bind(properties, item);
OfflineItemListItem offlineItem = (OfflineItemListItem) item;
((TextView) itemView).setText(offlineItem.item.title);
}
@Override
void onVisualsChanged(OfflineItemVisuals visuals) {
Drawable drawable = null;
if (visuals != null && visuals.icon != null) {
drawable = new BitmapDrawable(itemView.getResources(), visuals.icon);
}
ApiCompatibilityUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(
(TextView) itemView, drawable, null, null, null);
}
}
/** A {@link ViewHolder} specifically meant to display a video {@code OfflineItem}. */
......@@ -104,7 +123,7 @@ abstract class ListItemViewHolder extends ViewHolder {
super(new AppCompatTextView(parent.getContext()));
}
// DateOrderedListViewHolder implementation.
// ListItemViewHolder implementation.
@Override
public void bind(ListPropertyModel properties, ListItem item) {
OfflineItemListItem offlineItem = (OfflineItemListItem) item;
......@@ -113,16 +132,72 @@ abstract class ListItemViewHolder extends ViewHolder {
}
/** A {@link ViewHolder} specifically meant to display an image {@code OfflineItem}. */
public static class ImageViewHolder extends ListItemViewHolder {
public static class ImageViewHolder extends ThumbnailAwareViewHolder {
public ImageViewHolder(ViewGroup parent) {
super(new AppCompatTextView(parent.getContext()));
}
// DateOrderedListViewHolder implementation.
// ThumbnailAwareViewHolder implementation.
@Override
public void bind(ListPropertyModel properties, ListItem item) {
super.bind(properties, item);
OfflineItemListItem offlineItem = (OfflineItemListItem) item;
((TextView) itemView).setText(offlineItem.item.title);
}
@Override
void onVisualsChanged(OfflineItemVisuals visuals) {
Drawable drawable = null;
if (visuals != null && visuals.icon != null) {
drawable = new BitmapDrawable(itemView.getResources(), visuals.icon);
}
ApiCompatibilityUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(
(TextView) itemView, drawable, null, null, null);
}
}
/** Helper {@link ViewHolder} that handles querying for thumbnails if necessary. */
private abstract static class ThumbnailAwareViewHolder
extends ListItemViewHolder implements VisualsCallback {
private static final int TEST_IMAGE_SIZE_PX = 100;
/** Track whether or not the result of a query came back while we were making it. */
private boolean mQueryFinished;
private ContentId mQueryId;
private Runnable mQueryCancelRunnable;
public ThumbnailAwareViewHolder(View view) {
super(view);
}
abstract void onVisualsChanged(OfflineItemVisuals visuals);
// ListItemViewHolder implementation.
@Override
@CallSuper
public void bind(ListPropertyModel properties, ListItem item) {
OfflineItemListItem offlineItem = (OfflineItemListItem) item;
if (offlineItem.item.id.equals(mQueryId)) return;
if (mQueryId != null) onVisualsChanged(null);
if (mQueryCancelRunnable != null) mQueryCancelRunnable.run();
mQueryId = offlineItem.item.id;
mQueryFinished = false;
mQueryCancelRunnable = properties.getVisualsProvider().getVisuals(
offlineItem.item, TEST_IMAGE_SIZE_PX, TEST_IMAGE_SIZE_PX, this);
// Handle reentrancy case.
if (mQueryFinished) mQueryCancelRunnable = null;
}
// VisualsCallback implementation.
@Override
public void onVisualsAvailable(ContentId id, OfflineItemVisuals visuals) {
if (!id.equals(mQueryId)) return;
mQueryCancelRunnable = null;
mQueryFinished = true;
onVisualsChanged(visuals);
}
}
}
\ No newline at end of file
......@@ -7,6 +7,8 @@ package org.chromium.chrome.browser.download.home.list;
import org.chromium.base.Callback;
import org.chromium.chrome.browser.modelutil.PropertyObservable;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OfflineItemVisuals;
import org.chromium.components.offline_items_collection.VisualsCallback;
/**
* A {@link PropertyObservable} that contains two types of properties for the download manager:
......@@ -14,6 +16,19 @@ import org.chromium.components.offline_items_collection.OfflineItem;
* (2) A set of properties that are effectively shared across all list items like callbacks.
*/
class ListPropertyModel extends PropertyObservable<ListPropertyModel.PropertyKey> {
@FunctionalInterface
public interface VisualsProvider {
/**
* @param item The {@link OfflineItem} to get the {@link OfflineItemVisuals} for.
* @param iconWidthPx The desired width of the icon in pixels (not guaranteed).
* @param iconHeightPx The desired height of the icon in pixels (not guaranteed).
* @param callback A {@link Callback} that will be notified on completion.
* @return A {@link Runnable} that can be used to cancel the request.
*/
Runnable getVisuals(
OfflineItem item, int iconWidthPx, int iconHeightPx, VisualsCallback callback);
}
static class PropertyKey {
// RecyclerView general properties.
static final PropertyKey ENABLE_ITEM_ANIMATIONS = new PropertyKey();
......@@ -26,6 +41,9 @@ class ListPropertyModel extends PropertyObservable<ListPropertyModel.PropertyKey
static final PropertyKey CALLBACK_SHARE = new PropertyKey();
static final PropertyKey CALLBACK_REMOVE = new PropertyKey();
// Utility properties.
static final PropertyKey PROVIDER_VISUALS = new PropertyKey();
private PropertyKey() {}
}
......@@ -36,6 +54,7 @@ class ListPropertyModel extends PropertyObservable<ListPropertyModel.PropertyKey
private Callback<OfflineItem> mCancelCallback;
private Callback<OfflineItem> mShareCallback;
private Callback<OfflineItem> mRemoveCallback;
private VisualsProvider mVisualsProvider;
/** Sets whether or not item animations should be enabled. */
public void setEnableItemAnimations(boolean enableItemAnimations) {
......@@ -120,4 +139,16 @@ class ListPropertyModel extends PropertyObservable<ListPropertyModel.PropertyKey
public Callback<OfflineItem> getRemoveCallback() {
return mRemoveCallback;
}
/** Sets the provider for when the UI needs expensive assets for a {@link OfflineItem}. */
public void setVisualsProvider(VisualsProvider callback) {
if (mVisualsProvider == callback) return;
mVisualsProvider = callback;
notifyPropertyChanged(PropertyKey.PROVIDER_VISUALS);
}
/** @return The provider to retrieve expensive assets for a {@link OfflineItem}. */
public VisualsProvider getVisualsProvider() {
return mVisualsProvider;
}
}
\ No newline at end of file
......@@ -32,7 +32,8 @@ class ListPropertyViewBinder
|| propertyKey == ListPropertyModel.PropertyKey.CALLBACK_RESUME
|| propertyKey == ListPropertyModel.PropertyKey.CALLBACK_CANCEL
|| propertyKey == ListPropertyModel.PropertyKey.CALLBACK_SHARE
|| propertyKey == ListPropertyModel.PropertyKey.CALLBACK_REMOVE) {
|| propertyKey == ListPropertyModel.PropertyKey.CALLBACK_REMOVE
|| propertyKey == ListPropertyModel.PropertyKey.PROVIDER_VISUALS) {
view.getAdapter().notifyItemChanged(0, view.getAdapter().getItemCount());
}
}
......
......@@ -9,6 +9,8 @@ import android.text.format.DateUtils;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.R;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OfflineItemFilter;
import java.util.Calendar;
import java.util.Date;
......@@ -47,4 +49,16 @@ public final class UiUtils {
return builder;
}
/** @return Whether or not {@code item} can show a thumbnail in the UI. */
public static boolean canHaveThumbnails(OfflineItem item) {
switch (item.filter) {
case OfflineItemFilter.FILTER_PAGE:
case OfflineItemFilter.FILTER_VIDEO:
case OfflineItemFilter.FILTER_IMAGE:
return true;
default:
return false;
}
}
}
\ No newline at end of file
......@@ -167,6 +167,8 @@ public class ThumbnailProviderImpl implements ThumbnailProvider, ThumbnailStorag
String key = getKey(contentId, mCurrentRequest.getIconSize());
mBitmapCache.putBitmap(key, bitmap);
mCurrentRequest.onThumbnailRetrieved(contentId, bitmap);
} else {
mCurrentRequest.onThumbnailRetrieved(contentId, null);
}
mCurrentRequest = null;
......
......@@ -449,6 +449,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/download/home/filter/chips/ChipView.java",
"java/src/org/chromium/chrome/browser/download/home/glue/DownloadGlue.java",
"java/src/org/chromium/chrome/browser/download/home/glue/OfflineContentProviderGlue.java",
"java/src/org/chromium/chrome/browser/download/home/glue/ThumbnailRequestGlue.java",
"java/src/org/chromium/chrome/browser/download/home/list/CalendarFactory.java",
"java/src/org/chromium/chrome/browser/download/home/list/CalendarUtils.java",
"java/src/org/chromium/chrome/browser/download/home/list/BatchListObservable.java",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment