Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 38c22fd5 authored by Garfield Tan's avatar Garfield Tan Committed by Android (Google) Code Review
Browse files

Merge "Use thumbnail of other sizes if it's missing in current size."

parents 66d515c2 c9af00d4
Loading
Loading
Loading
Loading
+6 −16
Original line number Diff line number Diff line
@@ -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() {
+233 −7
Original line number Diff line number Diff line
/*
 * 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) {
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(Uri key, Bitmap value) {
        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;
        }
    }
}
+1 −2
Original line number Diff line number Diff line
@@ -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);
+77 −41
Original line number Diff line number Diff line
@@ -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);
    }

    /**
@@ -84,13 +97,13 @@ public class IconHelper {

    /**
     * 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);
        }
    }

}
+1 −1
Original line number Diff line number Diff line
@@ -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);