Merge "Evict thumbnail caches and delay dismissing spinner on refresh finish."
diff --git a/packages/DocumentsUI/src/com/android/documentsui/ThumbnailCache.java b/packages/DocumentsUI/src/com/android/documentsui/ThumbnailCache.java
index ecde685..639d4fb 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/ThumbnailCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/ThumbnailCache.java
@@ -111,6 +111,13 @@
         return Result.obtainMiss();
     }
 
+    /**
+     * Puts a thumbnail for the given uri and size in to the cache.
+     * @param uri the uri of the thumbnail
+     * @param size the size of the thumbnail
+     * @param thumbnail the thumbnail to put in cache
+     * @param lastModified last modified value of the thumbnail to track its validity
+     */
     public void putThumbnail(Uri uri, Point size, Bitmap thumbnail, long lastModified) {
         Pair<Uri, Point> cacheKey = Pair.create(uri, size);
 
@@ -130,14 +137,33 @@
         }
     }
 
+    /**
+     * Removes all thumbnail cache associated to the given uri.
+     * @param uri the uri which thumbnail cache to remove
+     */
+    public void removeUri(Uri uri) {
+        TreeMap<Point, Pair<Uri, Point>> sizeMap;
+        synchronized (mSizeIndex) {
+            sizeMap = mSizeIndex.get(uri);
+        }
+
+        if (sizeMap != null) {
+            // Create an array to hold all values to avoid ConcurrentModificationException because
+            // removeKey() will be called by LruCache but we can't modify the map while we're
+            // iterating over the collection of values.
+            for (Pair<Uri, Point> index : sizeMap.values().toArray(new Pair[0])) {
+                mCache.remove(index);
+            }
+        }
+    }
+
     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);
+        assert(sizeMap != null);
         synchronized (sizeMap) {
             sizeMap.remove(size);
         }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 2e1b1d6..db19881 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -42,6 +42,7 @@
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
@@ -92,6 +93,7 @@
 import com.android.documentsui.Snackbars;
 import com.android.documentsui.State;
 import com.android.documentsui.State.ViewMode;
+import com.android.documentsui.ThumbnailCache;
 import com.android.documentsui.clipping.DocumentClipper;
 import com.android.documentsui.clipping.UrisSupplier;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
@@ -140,6 +142,9 @@
     private static final String TAG = "DirectoryFragment";
     private static final int LOADER_ID = 42;
 
+    private static final int CACHE_EVICT_LIMIT = 100;
+    private static final int REFRESH_SPINNER_DISMISS_DELAY = 500;
+
     private Model mModel;
     private MultiSelectManager mSelectionMgr;
     private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
@@ -1610,6 +1615,17 @@
 
     @Override
     public void onRefresh() {
+        // Remove thumbnail cache. We do this not because we're worried about stale thumbnails as it
+        // should be covered by last modified value we store in thumbnail cache, but rather to give
+        // the user a greater sense that contents are being reloaded.
+        ThumbnailCache cache = DocumentsApplication.getThumbnailCache(getContext());
+        String[] ids = mModel.getModelIds();
+        int numOfEvicts = Math.min(ids.length, CACHE_EVICT_LIMIT);
+        for (int i = 0; i < numOfEvicts; ++i) {
+            cache.removeUri(mModel.getItemUri(ids[i]));
+        }
+
+        // Trigger loading
         getLoaderManager().restartLoader(LOADER_ID, null, this);
     }
 
@@ -1679,7 +1695,11 @@
 
         mTuner.onModelLoaded(mModel, mType, mSearchMode);
 
-        mRefreshLayout.setRefreshing(false);
+        if (mRefreshLayout.isRefreshing()) {
+            new Handler().postDelayed(
+                    () -> mRefreshLayout.setRefreshing(false),
+                    REFRESH_SPINNER_DISMISS_DELAY);
+        }
     }
 
     @Override
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ThumbnailCacheTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/ThumbnailCacheTest.java
index dda4918..ee6ab01 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/ThumbnailCacheTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/ThumbnailCacheTest.java
@@ -176,6 +176,18 @@
         assertSame(SMALL_BITMAP, result.getThumbnail());
     }
 
+    @Test
+    public void testRemoveUri() {
+        mCache.putThumbnail(URI_0, MID_SIZE, MIDSIZE_BITMAP, LAST_MODIFIED);
+        mCache.putThumbnail(URI_0, SMALL_SIZE, SMALL_BITMAP, LAST_MODIFIED);
+        mCache.putThumbnail(URI_1, MID_SIZE, MIDSIZE_BITMAP, LAST_MODIFIED);
+
+        mCache.removeUri(URI_0);
+
+        assertMiss(mCache.getThumbnail(URI_0, MID_SIZE));
+        assertHitExact(mCache.getThumbnail(URI_1, MID_SIZE));
+    }
+
     private static void assertMiss(Result result) {
         assertEquals(Result.CACHE_MISS, result.getStatus());
         assertFalse(result.isExactHit());