Use thumbnail of other sizes if it's missing in current size.
Bug: 26881628
Change-Id: Id7aa6f5c8c1a415f7dd97143a088ba89fae43eea
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java
index 5ea6cfa..cb9ce25 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -24,7 +24,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.graphics.Point;
import android.net.Uri;
import android.os.RemoteException;
import android.text.format.DateUtils;
@@ -33,21 +32,16 @@
private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
private RootsCache mRoots;
- private Point mThumbnailsSize;
- private ThumbnailCache mThumbnails;
+
+ private ThumbnailCache mThumbnailCache;
public static RootsCache getRootsCache(Context context) {
return ((DocumentsApplication) context.getApplicationContext()).mRoots;
}
- public static ThumbnailCache getThumbnailsCache(Context context, Point size) {
+ public static ThumbnailCache getThumbnailCache(Context context) {
final DocumentsApplication app = (DocumentsApplication) context.getApplicationContext();
- final ThumbnailCache thumbnails = app.mThumbnails;
- if (!size.equals(app.mThumbnailsSize)) {
- thumbnails.evictAll();
- app.mThumbnailsSize = size;
- }
- return thumbnails;
+ return app.mThumbnailCache;
}
public static ContentProviderClient acquireUnstableProviderOrThrow(
@@ -71,7 +65,7 @@
mRoots = new RootsCache(this);
mRoots.updateAsync(false);
- mThumbnails = new ThumbnailCache(memoryClassBytes / 4);
+ mThumbnailCache = new ThumbnailCache(memoryClassBytes / 4);
final IntentFilter packageFilter = new IntentFilter();
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
@@ -90,11 +84,7 @@
public void onTrimMemory(int level) {
super.onTrimMemory(level);
- if (level >= TRIM_MEMORY_MODERATE) {
- mThumbnails.evictAll();
- } else if (level >= TRIM_MEMORY_BACKGROUND) {
- mThumbnails.trimToSize(mThumbnails.size() / 2);
- }
+ mThumbnailCache.onTrimMemory(level);
}
private BroadcastReceiver mCacheReceiver = new BroadcastReceiver() {
diff --git a/src/com/android/documentsui/ThumbnailCache.java b/src/com/android/documentsui/ThumbnailCache.java
index ad7cbf6..25cf806 100644
--- a/src/com/android/documentsui/ThumbnailCache.java
+++ b/src/com/android/documentsui/ThumbnailCache.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2016 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.
@@ -16,17 +16,243 @@
package com.android.documentsui;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.ComponentCallbacks2;
import android.graphics.Bitmap;
+import android.graphics.Point;
import android.net.Uri;
import android.util.LruCache;
+import android.util.Pair;
+import android.util.Pools;
-public class ThumbnailCache extends LruCache<Uri, Bitmap> {
- public ThumbnailCache(int maxSizeBytes) {
- super(maxSizeBytes);
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.TreeMap;
+
+/**
+ * An LRU cache that supports finding the thumbnail of the requested uri with a different size than
+ * the requested one.
+ */
+public class ThumbnailCache {
+
+ private static final SizeComparator SIZE_COMPARATOR = new SizeComparator();
+
+ /**
+ * A 2-dimensional index into {@link #mCache} entries. Pair<Uri, Point> is the key to
+ * {@link #mCache}. TreeMap is used to search the closest size to a given size and a given uri.
+ */
+ private final HashMap<Uri, TreeMap<Point, Pair<Uri, Point>>> mSizeIndex;
+ private final Cache mCache;
+
+ /**
+ * Creates a thumbnail LRU cache.
+ *
+ * @param maxCacheSizeInBytes the maximum size of thumbnails in bytes this cache can hold.
+ */
+ public ThumbnailCache(int maxCacheSizeInBytes) {
+ mSizeIndex = new HashMap<>();
+ mCache = new Cache(maxCacheSizeInBytes);
}
- @Override
- protected int sizeOf(Uri key, Bitmap value) {
- return value.getByteCount();
+ /**
+ * Obtains thumbnail given a uri and a size.
+ *
+ * @param uri the uri of the thumbnail in need
+ * @param size the desired size of the thumbnail
+ * @return the thumbnail result
+ */
+ public Result getThumbnail(Uri uri, Point size) {
+ Result result = Result.obtain(Result.CACHE_MISS, null, null);
+
+ TreeMap<Point, Pair<Uri, Point>> sizeMap;
+ sizeMap = mSizeIndex.get(uri);
+ if (sizeMap == null || sizeMap.isEmpty()) {
+ // There is not any thumbnail for this uri.
+ return result;
+ }
+
+ // Look for thumbnail of the same size.
+ Pair<Uri, Point> cacheKey = sizeMap.get(size);
+ if (cacheKey != null) {
+ Bitmap thumbnail = mCache.get(cacheKey);
+ if (thumbnail != null) {
+ result.mStatus = Result.CACHE_HIT_EXACT;
+ result.mThumbnail = thumbnail;
+ result.mSize = size;
+ return result;
+ }
+ }
+
+ // Look for thumbnail of bigger sizes.
+ Point otherSize = sizeMap.higherKey(size);
+ if (otherSize != null) {
+ cacheKey = sizeMap.get(otherSize);
+
+ if (cacheKey != null) {
+ Bitmap thumbnail = mCache.get(cacheKey);
+ if (thumbnail != null) {
+ result.mStatus = Result.CACHE_HIT_LARGER;
+ result.mThumbnail = thumbnail;
+ result.mSize = otherSize;
+ return result;
+ }
+ }
+ }
+
+ // Look for thumbnail of smaller sizes.
+ otherSize = sizeMap.lowerKey(size);
+ if (otherSize != null) {
+ cacheKey = sizeMap.get(otherSize);
+
+ if (cacheKey != null) {
+ Bitmap thumbnail = mCache.get(cacheKey);
+ if (thumbnail != null) {
+ result.mStatus = Result.CACHE_HIT_SMALLER;
+ result.mThumbnail = thumbnail;
+ result.mSize = otherSize;
+ return result;
+ }
+ }
+ }
+
+ // Cache miss.
+ return result;
+ }
+
+ public void putThumbnail(Uri uri, Point size, Bitmap thumbnail) {
+ Pair<Uri, Point> cacheKey = Pair.create(uri, size);
+
+ TreeMap<Point, Pair<Uri, Point>> sizeMap;
+ synchronized (mSizeIndex) {
+ sizeMap = mSizeIndex.get(uri);
+ if (sizeMap == null) {
+ sizeMap = new TreeMap<>(SIZE_COMPARATOR);
+ mSizeIndex.put(uri, sizeMap);
+ }
+ }
+
+ mCache.put(cacheKey, thumbnail);
+ synchronized (sizeMap) {
+ sizeMap.put(size, cacheKey);
+ }
+ }
+
+ public void onTrimMemory(int level) {
+ if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
+ synchronized (mSizeIndex) {
+ mSizeIndex.clear();
+ }
+ mCache.evictAll();
+ } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
+ mCache.trimToSize(mCache.size() / 2);
+ }
+ }
+
+ /**
+ * A class that holds thumbnail and cache status.
+ */
+ public static final class Result {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CACHE_MISS, CACHE_HIT_EXACT, CACHE_HIT_SMALLER, CACHE_HIT_LARGER})
+ @interface Status {}
+
+ /**
+ * Indicates there is no thumbnail for the requested uri. The thumbnail will be null.
+ */
+ public static final int CACHE_MISS = 0;
+ /**
+ * Indicates the thumbnail matches the requested size and requested uri.
+ */
+ public static final int CACHE_HIT_EXACT = 1;
+ /**
+ * Indicates the thumbnail is in a smaller size than the requested one from the requested
+ * uri.
+ */
+ public static final int CACHE_HIT_SMALLER = 2;
+ /**
+ * Indicates the thumbnail is in a larger size than the requested one from the requested
+ * uri.
+ */
+ public static final int CACHE_HIT_LARGER = 3;
+
+ private static final Pools.SimplePool<Result> sPool = new Pools.SimplePool<>(1);
+
+ private @Status int mStatus;
+
+ private @Nullable Bitmap mThumbnail;
+
+ private @Nullable Point mSize;
+
+ private static Result obtain(@Status int status, @Nullable Bitmap thumbnail,
+ @Nullable Point size) {
+ Result instance = sPool.acquire();
+ instance = (instance != null ? instance : new Result());
+
+ instance.mStatus = status;
+ instance.mThumbnail = thumbnail;
+ instance.mSize = size;
+
+ return instance;
+ }
+
+ private Result() {
+ }
+
+ public void recycle() {
+ mStatus = -1;
+ mThumbnail = null;
+ mSize = null;
+
+ boolean released = sPool.release(this);
+ // This assert is used to guarantee we won't generate too many instances that can't be
+ // held in the pool, which indicates our pool size is too small.
+ //
+ // Right now one instance is enough because we expect all instances are only used in
+ // main thread.
+ assert (released);
+ }
+
+ public @Status int getStatus() {
+ return mStatus;
+ }
+
+ public @Nullable Bitmap getThumbnail() {
+ return mThumbnail;
+ }
+
+ public @Nullable Point getSize() {
+ return mSize;
+ }
+
+ public boolean isHit() {
+ return (mStatus != CACHE_MISS);
+ }
+
+ public boolean isExactHit() {
+ return (mStatus == CACHE_HIT_EXACT);
+ }
+ }
+
+ private static final class Cache extends LruCache<Pair<Uri, Point>, Bitmap> {
+ private Cache(int maxSizeBytes) {
+ super(maxSizeBytes);
+ }
+
+ @Override
+ protected int sizeOf(Pair<Uri, Point> key, Bitmap value) {
+ return value.getByteCount();
+ }
+ }
+
+ private static final class SizeComparator implements Comparator<Point> {
+ @Override
+ public int compare(Point size0, Point size1) {
+ // Assume all sizes are roughly square, so we only compare them in one dimension.
+ return size0.x - size1.x;
+ }
}
}
diff --git a/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
index c4f6f11..8b10257 100644
--- a/src/com/android/documentsui/dirlist/GridDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
@@ -135,8 +135,7 @@
mIconThumb.setAlpha(0f);
final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
- mIconHelper.loadThumbnail(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMimeLg,
- mIconMimeSm);
+ mIconHelper.load(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMimeLg, mIconMimeSm);
if (mHideTitles) {
mTitle.setVisibility(View.GONE);
diff --git a/src/com/android/documentsui/dirlist/IconHelper.java b/src/com/android/documentsui/dirlist/IconHelper.java
index ff0f4b1..d1f792e 100644
--- a/src/com/android/documentsui/dirlist/IconHelper.java
+++ b/src/com/android/documentsui/dirlist/IconHelper.java
@@ -34,6 +34,7 @@
import android.provider.DocumentsContract.Document;
import android.support.annotation.Nullable;
import android.util.Log;
+import android.view.View;
import android.widget.ImageView;
import com.android.documentsui.DocumentsApplication;
@@ -45,21 +46,33 @@
import com.android.documentsui.State;
import com.android.documentsui.State.ViewMode;
import com.android.documentsui.ThumbnailCache;
+import com.android.documentsui.ThumbnailCache.Result;
+
+import java.util.function.BiConsumer;
/**
* A class to assist with loading and managing the Images (i.e. thumbnails and icons) associated
* with items in the directory listing.
*/
public class IconHelper {
- private static String TAG = "IconHelper";
+ private static final String TAG = "IconHelper";
+
+ // Two animations applied to image views. The first is used to switch mime icon and thumbnail.
+ // The second is used when we need to update thumbnail.
+ private static final BiConsumer<View, View> ANIM_FADE_IN = (mime, thumb) -> {
+ float alpha = mime.getAlpha();
+ mime.animate().alpha(0f).start();
+ thumb.setAlpha(0f);
+ thumb.animate().alpha(alpha).start();
+ };
+ private static final BiConsumer<View, View> ANIM_NO_OP = (mime, thumb) -> {};
private final Context mContext;
+ private final ThumbnailCache mThumbnailCache;
- // Updated when icon size is set.
- private ThumbnailCache mCache;
- private Point mThumbSize;
// The display mode (MODE_GRID, MODE_LIST, etc).
private int mMode;
+ private Point mCurrentSize;
private boolean mThumbnailsEnabled = true;
/**
@@ -69,7 +82,7 @@
public IconHelper(Context context, int mode) {
mContext = context;
setViewMode(mode);
- mCache = DocumentsApplication.getThumbnailsCache(context, mThumbSize);
+ mThumbnailCache = DocumentsApplication.getThumbnailCache(context);
}
/**
@@ -83,14 +96,14 @@
}
/**
- * Sets the current display mode. This affects the thumbnail sizes that are loaded.
+ * Sets the current display mode. This affects the thumbnail sizes that are loaded.
+ *
* @param mode See {@link State.MODE_LIST} and {@link State.MODE_GRID}.
*/
public void setViewMode(@ViewMode int mode) {
mMode = mode;
int thumbSize = getThumbSize(mode);
- mThumbSize = new Point(thumbSize, thumbSize);
- mCache = DocumentsApplication.getThumbnailsCache(mContext, mThumbSize);
+ mCurrentSize = new Point(thumbSize, thumbSize);
}
private int getThumbSize(int mode) {
@@ -111,6 +124,7 @@
/**
* Cancels any ongoing load operations associated with the given ImageView.
+ *
* @param icon
*/
public void stopLoading(ImageView icon) {
@@ -129,14 +143,19 @@
private final ImageView mIconMime;
private final ImageView mIconThumb;
private final Point mThumbSize;
+
+ // A callback to apply animation to image views after the thumbnail is loaded.
+ private final BiConsumer<View, View> mImageAnimator;
+
private final CancellationSignal mSignal;
public LoaderTask(Uri uri, ImageView iconMime, ImageView iconThumb,
- Point thumbSize) {
+ Point thumbSize, BiConsumer<View, View> animator) {
mUri = uri;
mIconMime = iconMime;
mIconThumb = iconThumb;
mThumbSize = thumbSize;
+ mImageAnimator = animator;
mSignal = new CancellationSignal();
if (DEBUG) Log.d(TAG, "Starting icon loader task for " + mUri);
}
@@ -150,8 +169,9 @@
@Override
protected Bitmap doInBackground(Uri... params) {
- if (isCancelled())
+ if (isCancelled()) {
return null;
+ }
final Context context = mIconThumb.getContext();
final ContentResolver resolver = context.getContentResolver();
@@ -163,9 +183,8 @@
resolver, mUri.getAuthority());
result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
if (result != null) {
- final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
- context, mThumbSize);
- thumbs.put(mUri, result);
+ final ThumbnailCache cache = DocumentsApplication.getThumbnailCache(context);
+ cache.putThumbnail(mUri, mThumbSize, result);
}
} catch (Exception e) {
if (!(e instanceof OperationCanceledException)) {
@@ -185,16 +204,14 @@
mIconThumb.setTag(null);
mIconThumb.setImageBitmap(result);
- float alpha = mIconMime.getAlpha();
- mIconMime.animate().alpha(0f).start();
- mIconThumb.setAlpha(0f);
- mIconThumb.animate().alpha(alpha).start();
+ mImageAnimator.accept(mIconMime, mIconThumb);
}
}
}
/**
* Load thumbnails for a directory list item.
+ *
* @param uri The URI for the file being represented.
* @param mimeType The mime type of the file being represented.
* @param docFlags Flags for the file being represented.
@@ -204,9 +221,9 @@
* @param subIconMime The second itemview's mime icon. Always visible.
* @return
*/
- public void loadThumbnail(Uri uri, String mimeType, int docFlags, int docIcon,
+ public void load(Uri uri, String mimeType, int docFlags, int docIcon,
ImageView iconThumb, ImageView iconMime, @Nullable ImageView subIconMime) {
- boolean cacheHit = false;
+ boolean loadedThumbnail = false;
final String docAuthority = uri.getAuthority();
@@ -215,39 +232,59 @@
|| MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mimeType);
final boolean showThumbnail = supportsThumbnail && allowThumbnail && mThumbnailsEnabled;
if (showThumbnail) {
- final Bitmap cachedResult = mCache.get(uri);
- if (cachedResult != null) {
- iconThumb.setImageBitmap(cachedResult);
- cacheHit = true;
- } else {
- iconThumb.setImageDrawable(null);
- final LoaderTask task = new LoaderTask(uri, iconMime, iconThumb, mThumbSize);
- iconThumb.setTag(task);
- ProviderExecutor.forAuthority(docAuthority).execute(task);
- }
+ loadedThumbnail = loadThumbnail(uri, docAuthority, iconThumb, iconMime);
}
- final Drawable icon = getDocumentIcon(mContext, docAuthority,
+ final Drawable mimeIcon = getDocumentIcon(mContext, docAuthority,
DocumentsContract.getDocumentId(uri), mimeType, docIcon);
if (subIconMime != null) {
- subIconMime.setImageDrawable(icon);
+ setMimeIcon(subIconMime, mimeIcon);
}
- if (cacheHit) {
- iconMime.setImageDrawable(null);
- iconMime.setAlpha(0f);
- iconThumb.setAlpha(1f);
+ if (loadedThumbnail) {
+ hideImageView(iconMime);
} else {
- // Add a mime icon if the thumbnail is being loaded in the background.
- iconThumb.setImageDrawable(null);
- iconMime.setImageDrawable(icon);
- iconMime.setAlpha(1f);
- iconThumb.setAlpha(0f);
+ // Add a mime icon if the thumbnail is not shown.
+ setMimeIcon(iconMime, mimeIcon);
+ hideImageView(iconThumb);
}
}
+ private boolean loadThumbnail(Uri uri, String docAuthority, ImageView iconThumb,
+ ImageView iconMime) {
+ final Result result = mThumbnailCache.getThumbnail(uri, mCurrentSize);
+
+ final Bitmap cachedThumbnail = result.getThumbnail();
+ iconThumb.setImageBitmap(cachedThumbnail);
+
+ if (!result.isExactHit()) {
+ final BiConsumer<View, View> animator =
+ (cachedThumbnail == null ? ANIM_FADE_IN : ANIM_NO_OP);
+ final LoaderTask task =
+ new LoaderTask(uri, iconMime, iconThumb, mCurrentSize, animator);
+
+ iconThumb.setTag(task);
+
+ ProviderExecutor.forAuthority(docAuthority).execute(task);
+ }
+ result.recycle();
+
+ return result.isHit();
+ }
+
+ private void setMimeIcon(ImageView view, Drawable icon) {
+ view.setImageDrawable(icon);
+ view.setAlpha(1f);
+ }
+
+ private void hideImageView(ImageView view) {
+ view.setImageDrawable(null);
+ view.setAlpha(0f);
+ }
+
/**
* Gets a mime icon or package icon for a file.
+ *
* @param context
* @param authority The authority string of the file.
* @param id The document ID of the file.
@@ -263,5 +300,4 @@
return IconUtils.loadMimeIcon(context, mimeType, authority, id, mMode);
}
}
-
}
diff --git a/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
index ace53e0..98916a1 100644
--- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
@@ -133,7 +133,7 @@
mIconThumb.setAlpha(0f);
final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
- mIconHelper.loadThumbnail(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMime, null);
+ mIconHelper.load(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMime, null);
mTitle.setText(docDisplayName, TextView.BufferType.SPANNABLE);
mTitle.setVisibility(View.VISIBLE);