diff options
author | 2016-05-02 12:01:30 -0700 | |
---|---|---|
committer | 2016-05-17 10:59:31 -0700 | |
commit | c8099c02f9294119835d1d5efd1505ce62371d74 (patch) | |
tree | dd3a8f999e660400ff5165ec35a23cdeaff5d112 | |
parent | 6220c7ab3a9551fce2bca40740d949bad0f2491e (diff) |
Use thumbnail of other sizes if it's missing in current size.
Bug: 26881628
Change-Id: Id7aa6f5c8c1a415f7dd97143a088ba89fae43eea
5 files changed, 318 insertions, 67 deletions
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java index 5ea6cfa73..cb9ce2515 100644 --- a/src/com/android/documentsui/DocumentsApplication.java +++ b/src/com/android/documentsui/DocumentsApplication.java @@ -24,7 +24,6 @@ import android.content.ContentResolver; 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 @@ public class DocumentsApplication extends Application { 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 @@ public class DocumentsApplication extends Application { 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 class DocumentsApplication extends Application { 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 ad7cbf697..25cf80604 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); + } + + /** + * 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(); + } } - @Override - protected int sizeOf(Uri 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 c4f6f11b3..8b1025718 100644 --- a/src/com/android/documentsui/dirlist/GridDocumentHolder.java +++ b/src/com/android/documentsui/dirlist/GridDocumentHolder.java @@ -135,8 +135,7 @@ final class GridDocumentHolder extends DocumentHolder { 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 ff0f4b10b..d1f792e89 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; 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.R; 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 class IconHelper { public IconHelper(Context context, int mode) { mContext = context; setViewMode(mode); - mCache = DocumentsApplication.getThumbnailsCache(context, mThumbSize); + mThumbnailCache = DocumentsApplication.getThumbnailCache(context); } /** @@ -83,14 +96,14 @@ public class IconHelper { } /** - * 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 @@ public class IconHelper { /** * Cancels any ongoing load operations associated with the given ImageView. + * * @param icon */ public void stopLoading(ImageView icon) { @@ -129,14 +143,19 @@ public class IconHelper { 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 @@ public class IconHelper { @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 @@ public class IconHelper { 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 @@ public class IconHelper { 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 @@ public class IconHelper { * @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 @@ public class IconHelper { || 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 @@ public class IconHelper { 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 ace53e0b5..98916a15a 100644 --- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java +++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java @@ -133,7 +133,7 @@ final class ListDocumentHolder extends DocumentHolder { 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); |