Load up-to-date thumbnail if the cached one is out of date.

Bug: 28557412
Change-Id: Ib3ef9962249305be22b7a1e49e26350f3596e430
diff --git a/src/com/android/documentsui/ThumbnailCache.java b/src/com/android/documentsui/ThumbnailCache.java
index 25cf806..53f5092 100644
--- a/src/com/android/documentsui/ThumbnailCache.java
+++ b/src/com/android/documentsui/ThumbnailCache.java
@@ -65,24 +65,19 @@
      * @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;
+            return Result.obtainMiss();
         }
 
         // 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;
+            Entry entry = mCache.get(cacheKey);
+            if (entry != null) {
+                return Result.obtain(Result.CACHE_HIT_EXACT, size, entry);
             }
         }
 
@@ -92,12 +87,9 @@
             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;
+                Entry entry = mCache.get(cacheKey);
+                if (entry != null) {
+                    return Result.obtain(Result.CACHE_HIT_LARGER, otherSize, entry);
                 }
             }
         }
@@ -108,21 +100,18 @@
             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;
+                Entry entry = mCache.get(cacheKey);
+                if (entry != null) {
+                    return Result.obtain(Result.CACHE_HIT_SMALLER, otherSize, entry);
                 }
             }
         }
 
         // Cache miss.
-        return result;
+        return Result.obtainMiss();
     }
 
-    public void putThumbnail(Uri uri, Point size, Bitmap thumbnail) {
+    public void putThumbnail(Uri uri, Point size, Bitmap thumbnail, long lastModified) {
         Pair<Uri, Point> cacheKey = Pair.create(uri, size);
 
         TreeMap<Point, Pair<Uri, Point>> sizeMap;
@@ -134,17 +123,28 @@
             }
         }
 
-        mCache.put(cacheKey, thumbnail);
+        Entry entry = new Entry(thumbnail, lastModified);
+        mCache.put(cacheKey, entry);
         synchronized (sizeMap) {
             sizeMap.put(size, cacheKey);
         }
     }
 
+    private void removeKey(Uri uri, Point size) {
+        TreeMap<Point, Pair<Uri, Point>> sizeMap;
+        synchronized (mSizeIndex) {
+            sizeMap = mSizeIndex.get(uri);
+        }
+
+        // LruCache tells us to remove a key, which should exist, so sizeMap can't be null.
+        assert (sizeMap != null);
+        synchronized (sizeMap) {
+            sizeMap.remove(size);
+        }
+    }
+
     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);
@@ -159,7 +159,6 @@
         @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.
          */
@@ -182,30 +181,38 @@
         private static final Pools.SimplePool<Result> sPool = new Pools.SimplePool<>(1);
 
         private @Status int mStatus;
-
         private @Nullable Bitmap mThumbnail;
-
         private @Nullable Point mSize;
+        private long mLastModified;
+
+        private static Result obtainMiss() {
+            return obtain(CACHE_MISS, null, null, 0);
+        }
+
+        private static Result obtain(@Status int status, Point size, Entry entry) {
+            return obtain(status, entry.mThumbnail, size, entry.mLastModified);
+        }
 
         private static Result obtain(@Status int status, @Nullable Bitmap thumbnail,
-                @Nullable Point size) {
+                @Nullable Point size, long lastModified) {
             Result instance = sPool.acquire();
             instance = (instance != null ? instance : new Result());
 
             instance.mStatus = status;
             instance.mThumbnail = thumbnail;
             instance.mSize = size;
+            instance.mLastModified = lastModified;
 
             return instance;
         }
 
-        private Result() {
-        }
+        private Result() {}
 
         public void recycle() {
             mStatus = -1;
             mThumbnail = null;
             mSize = null;
+            mLastModified = -1;
 
             boolean released = sPool.release(this);
             // This assert is used to guarantee we won't generate too many instances that can't be
@@ -228,6 +235,10 @@
             return mSize;
         }
 
+        public long getLastModified() {
+            return mLastModified;
+        }
+
         public boolean isHit() {
             return (mStatus != CACHE_MISS);
         }
@@ -237,14 +248,33 @@
         }
     }
 
-    private static final class Cache extends LruCache<Pair<Uri, Point>, Bitmap> {
+    private static final class Entry {
+        private final Bitmap mThumbnail;
+        private final long mLastModified;
+
+        private Entry(Bitmap thumbnail, long lastModified) {
+            mThumbnail = thumbnail;
+            mLastModified = lastModified;
+        }
+    }
+
+    private final class Cache extends LruCache<Pair<Uri, Point>, Entry> {
+
         private Cache(int maxSizeBytes) {
             super(maxSizeBytes);
         }
 
         @Override
-        protected int sizeOf(Pair<Uri, Point> key, Bitmap value) {
-            return value.getByteCount();
+        protected int sizeOf(Pair<Uri, Point> key, Entry value) {
+            return value.mThumbnail.getByteCount();
+        }
+
+        @Override
+        protected void entryRemoved(
+                boolean evicted, Pair<Uri, Point> key, Entry oldValue, Entry newValue) {
+            if (newValue == null) {
+                removeKey(key.first, key.second);
+            }
         }
     }
 
diff --git a/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
index 8b10257..7ba4bdd 100644
--- a/src/com/android/documentsui/dirlist/GridDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
@@ -135,7 +135,8 @@
         mIconThumb.setAlpha(0f);
 
         final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
-        mIconHelper.load(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMimeLg, mIconMimeSm);
+        mIconHelper.load(uri, docMimeType, docFlags, docIcon, docLastModified, 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 d1f792e..78257bd 100644
--- a/src/com/android/documentsui/dirlist/IconHelper.java
+++ b/src/com/android/documentsui/dirlist/IconHelper.java
@@ -143,6 +143,7 @@
         private final ImageView mIconMime;
         private final ImageView mIconThumb;
         private final Point mThumbSize;
+        private final long mLastModified;
 
         // A callback to apply animation to image views after the thumbnail is loaded.
         private final BiConsumer<View, View> mImageAnimator;
@@ -150,12 +151,13 @@
         private final CancellationSignal mSignal;
 
         public LoaderTask(Uri uri, ImageView iconMime, ImageView iconThumb,
-                Point thumbSize, BiConsumer<View, View> animator) {
+                Point thumbSize, long lastModified, BiConsumer<View, View> animator) {
             mUri = uri;
             mIconMime = iconMime;
             mIconThumb = iconThumb;
             mThumbSize = thumbSize;
             mImageAnimator = animator;
+            mLastModified = lastModified;
             mSignal = new CancellationSignal();
             if (DEBUG) Log.d(TAG, "Starting icon loader task for " + mUri);
         }
@@ -184,7 +186,7 @@
                 result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
                 if (result != null) {
                     final ThumbnailCache cache = DocumentsApplication.getThumbnailCache(context);
-                    cache.putThumbnail(mUri, mThumbSize, result);
+                    cache.putThumbnail(mUri, mThumbSize, result, mLastModified);
                 }
             } catch (Exception e) {
                 if (!(e instanceof OperationCanceledException)) {
@@ -216,12 +218,13 @@
      * @param mimeType The mime type of the file being represented.
      * @param docFlags Flags for the file being represented.
      * @param docIcon Custom icon (if any) for the file being requested.
+     * @param docLastModified the last modified value of the file being requested.
      * @param iconThumb The itemview's thumbnail icon.
      * @param iconMime The itemview's mime icon. Hidden when iconThumb is shown.
      * @param subIconMime The second itemview's mime icon. Always visible.
      * @return
      */
-    public void load(Uri uri, String mimeType, int docFlags, int docIcon,
+    public void load(Uri uri, String mimeType, int docFlags, int docIcon, long docLastModified,
             ImageView iconThumb, ImageView iconMime, @Nullable ImageView subIconMime) {
         boolean loadedThumbnail = false;
 
@@ -232,7 +235,8 @@
                 || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mimeType);
         final boolean showThumbnail = supportsThumbnail && allowThumbnail && mThumbnailsEnabled;
         if (showThumbnail) {
-            loadedThumbnail = loadThumbnail(uri, docAuthority, iconThumb, iconMime);
+            loadedThumbnail =
+                loadThumbnail(uri, docAuthority, docLastModified, iconThumb, iconMime);
         }
 
         final Drawable mimeIcon = getDocumentIcon(mContext, docAuthority,
@@ -250,18 +254,21 @@
         }
     }
 
-    private boolean loadThumbnail(Uri uri, String docAuthority, ImageView iconThumb,
-            ImageView iconMime) {
+    private boolean loadThumbnail(Uri uri, String docAuthority, long docLastModified,
+            ImageView iconThumb, ImageView iconMime) {
         final Result result = mThumbnailCache.getThumbnail(uri, mCurrentSize);
 
         final Bitmap cachedThumbnail = result.getThumbnail();
         iconThumb.setImageBitmap(cachedThumbnail);
 
-        if (!result.isExactHit()) {
+        boolean stale = (docLastModified > result.getLastModified());
+        if (DEBUG) Log.d(TAG, String.format("Load thumbnail for %s, got result %d and stale %b.",
+                uri.toString(), result.getStatus(), stale));
+        if (!result.isExactHit() || stale) {
             final BiConsumer<View, View> animator =
                     (cachedThumbnail == null ? ANIM_FADE_IN : ANIM_NO_OP);
-            final LoaderTask task =
-                    new LoaderTask(uri, iconMime, iconThumb, mCurrentSize, animator);
+            final LoaderTask task = new LoaderTask(uri, iconMime, iconThumb, mCurrentSize,
+                    docLastModified, animator);
 
             iconThumb.setTag(task);
 
diff --git a/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
index 98916a1..e88be0c 100644
--- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
@@ -133,7 +133,8 @@
         mIconThumb.setAlpha(0f);
 
         final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
-        mIconHelper.load(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMime, null);
+        mIconHelper.load(uri, docMimeType, docFlags, docIcon, docLastModified, mIconThumb,
+                mIconMime, null);
 
         mTitle.setText(docDisplayName, TextView.BufferType.SPANNABLE);
         mTitle.setVisibility(View.VISIBLE);