1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui.dirlist;
18 
19 import static com.android.documentsui.base.Shared.VERBOSE;
20 import static com.android.documentsui.base.State.MODE_GRID;
21 import static com.android.documentsui.base.State.MODE_LIST;
22 
23 import android.content.ContentProviderClient;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.graphics.Bitmap;
27 import android.graphics.Point;
28 import android.graphics.drawable.Drawable;
29 import android.net.Uri;
30 import android.os.AsyncTask;
31 import android.os.CancellationSignal;
32 import android.os.OperationCanceledException;
33 import android.provider.DocumentsContract;
34 import android.provider.DocumentsContract.Document;
35 import android.support.annotation.Nullable;
36 import android.util.Log;
37 import android.view.View;
38 import android.widget.ImageView;
39 
40 import com.android.documentsui.DocumentsApplication;
41 import com.android.documentsui.IconUtils;
42 import com.android.documentsui.ProviderExecutor;
43 import com.android.documentsui.ProviderExecutor.Preemptable;
44 import com.android.documentsui.R;
45 import com.android.documentsui.ThumbnailCache;
46 import com.android.documentsui.ThumbnailCache.Result;
47 import com.android.documentsui.base.DocumentInfo;
48 import com.android.documentsui.base.MimeTypes;
49 import com.android.documentsui.base.State;
50 import com.android.documentsui.base.State.ViewMode;
51 
52 import java.util.function.BiConsumer;
53 
54 /**
55  * A class to assist with loading and managing the Images (i.e. thumbnails and icons) associated
56  * with items in the directory listing.
57  */
58 public class IconHelper {
59     private static final String TAG = "IconHelper";
60 
61     // Two animations applied to image views. The first is used to switch mime icon and thumbnail.
62     // The second is used when we need to update thumbnail.
63     private static final BiConsumer<View, View> ANIM_FADE_IN = (mime, thumb) -> {
64         float alpha = mime.getAlpha();
65         mime.animate().alpha(0f).start();
66         thumb.setAlpha(0f);
67         thumb.animate().alpha(alpha).start();
68     };
69     private static final BiConsumer<View, View> ANIM_NO_OP = (mime, thumb) -> {};
70 
71     private final Context mContext;
72     private final ThumbnailCache mThumbnailCache;
73 
74     // The display mode (MODE_GRID, MODE_LIST, etc).
75     private int mMode;
76     private Point mCurrentSize;
77     private boolean mThumbnailsEnabled = true;
78 
79     /**
80      * @param context
81      * @param mode MODE_GRID or MODE_LIST
82      */
IconHelper(Context context, int mode)83     public IconHelper(Context context, int mode) {
84         mContext = context;
85         setViewMode(mode);
86         mThumbnailCache = DocumentsApplication.getThumbnailCache(context);
87     }
88 
89     /**
90      * Enables or disables thumbnails. When thumbnails are disabled, mime icons (or custom icons, if
91      * specified by the document) are used instead.
92      *
93      * @param enabled
94      */
setThumbnailsEnabled(boolean enabled)95     public void setThumbnailsEnabled(boolean enabled) {
96         mThumbnailsEnabled = enabled;
97     }
98 
99     /**
100      * Sets the current display mode. This affects the thumbnail sizes that are loaded.
101      *
102      * @param mode See {@link State.MODE_LIST} and {@link State.MODE_GRID}.
103      */
setViewMode(@iewMode int mode)104     public void setViewMode(@ViewMode int mode) {
105         mMode = mode;
106         int thumbSize = getThumbSize(mode);
107         mCurrentSize = new Point(thumbSize, thumbSize);
108     }
109 
getThumbSize(int mode)110     private int getThumbSize(int mode) {
111         int thumbSize;
112         switch (mode) {
113             case MODE_GRID:
114                 thumbSize = mContext.getResources().getDimensionPixelSize(R.dimen.grid_width);
115                 break;
116             case MODE_LIST:
117                 thumbSize = mContext.getResources().getDimensionPixelSize(
118                         R.dimen.list_item_thumbnail_size);
119                 break;
120             default:
121                 throw new IllegalArgumentException("Unsupported layout mode: " + mode);
122         }
123         return thumbSize;
124     }
125 
126     /**
127      * Cancels any ongoing load operations associated with the given ImageView.
128      *
129      * @param icon
130      */
stopLoading(ImageView icon)131     public void stopLoading(ImageView icon) {
132         final LoaderTask oldTask = (LoaderTask) icon.getTag();
133         if (oldTask != null) {
134             oldTask.preempt();
135             icon.setTag(null);
136         }
137     }
138 
139     /** Internal task for loading thumbnails asynchronously. */
140     private static class LoaderTask
141             extends AsyncTask<Uri, Void, Bitmap>
142             implements Preemptable {
143         private final Uri mUri;
144         private final ImageView mIconMime;
145         private final ImageView mIconThumb;
146         private final Point mThumbSize;
147         private final long mLastModified;
148 
149         // A callback to apply animation to image views after the thumbnail is loaded.
150         private final BiConsumer<View, View> mImageAnimator;
151 
152         private final CancellationSignal mSignal;
153 
LoaderTask(Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize, long lastModified, BiConsumer<View, View> animator)154         public LoaderTask(Uri uri, ImageView iconMime, ImageView iconThumb,
155                 Point thumbSize, long lastModified, BiConsumer<View, View> animator) {
156             mUri = uri;
157             mIconMime = iconMime;
158             mIconThumb = iconThumb;
159             mThumbSize = thumbSize;
160             mImageAnimator = animator;
161             mLastModified = lastModified;
162             mSignal = new CancellationSignal();
163             if (VERBOSE) Log.v(TAG, "Starting icon loader task for " + mUri);
164         }
165 
166         @Override
preempt()167         public void preempt() {
168             if (VERBOSE) Log.v(TAG, "Icon loader task for " + mUri + " was cancelled.");
169             cancel(false);
170             mSignal.cancel();
171         }
172 
173         @Override
doInBackground(Uri... params)174         protected Bitmap doInBackground(Uri... params) {
175             if (isCancelled()) {
176                 return null;
177             }
178 
179             final Context context = mIconThumb.getContext();
180             final ContentResolver resolver = context.getContentResolver();
181 
182             ContentProviderClient client = null;
183             Bitmap result = null;
184             try {
185                 client = DocumentsApplication.acquireUnstableProviderOrThrow(
186                         resolver, mUri.getAuthority());
187                 result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
188                 if (result != null) {
189                     final ThumbnailCache cache = DocumentsApplication.getThumbnailCache(context);
190                     cache.putThumbnail(mUri, mThumbSize, result, mLastModified);
191                 }
192             } catch (Exception e) {
193                 if (!(e instanceof OperationCanceledException)) {
194                     Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
195                 }
196             } finally {
197                 ContentProviderClient.releaseQuietly(client);
198             }
199             return result;
200         }
201 
202         @Override
onPostExecute(Bitmap result)203         protected void onPostExecute(Bitmap result) {
204             if (VERBOSE) Log.v(TAG, "Loader task for " + mUri + " completed");
205 
206             if (mIconThumb.getTag() == this && result != null) {
207                 mIconThumb.setTag(null);
208                 mIconThumb.setImageBitmap(result);
209 
210                 mImageAnimator.accept(mIconMime, mIconThumb);
211             }
212         }
213     }
214 
215     /**
216      * Load thumbnails for a directory list item.
217      *
218      * @param doc The document
219      * @param iconThumb The itemview's thumbnail icon.
220      * @param iconMime The itemview's mime icon. Hidden when iconThumb is shown.
221      * @param subIconMime The second itemview's mime icon. Always visible.
222      * @return
223      */
load( DocumentInfo doc, ImageView iconThumb, ImageView iconMime, @Nullable ImageView subIconMime)224     public void load(
225             DocumentInfo doc,
226             ImageView iconThumb,
227             ImageView iconMime,
228             @Nullable ImageView subIconMime) {
229         load(doc.derivedUri, doc.mimeType, doc.flags, doc.icon, doc.lastModified,
230                 iconThumb, iconMime, subIconMime);
231     }
232 
233     /**
234      * Load thumbnails for a directory list item.
235      *
236      * @param uri The URI for the file being represented.
237      * @param mimeType The mime type of the file being represented.
238      * @param docFlags Flags for the file being represented.
239      * @param docIcon Custom icon (if any) for the file being requested.
240      * @param docLastModified the last modified value of the file being requested.
241      * @param iconThumb The itemview's thumbnail icon.
242      * @param iconMime The itemview's mime icon. Hidden when iconThumb is shown.
243      * @param subIconMime The second itemview's mime icon. Always visible.
244      * @return
245      */
load(Uri uri, String mimeType, int docFlags, int docIcon, long docLastModified, ImageView iconThumb, ImageView iconMime, @Nullable ImageView subIconMime)246     public void load(Uri uri, String mimeType, int docFlags, int docIcon, long docLastModified,
247             ImageView iconThumb, ImageView iconMime, @Nullable ImageView subIconMime) {
248         boolean loadedThumbnail = false;
249 
250         final String docAuthority = uri.getAuthority();
251 
252         final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
253         final boolean allowThumbnail = (mMode == MODE_GRID)
254                 || MimeTypes.mimeMatches(MimeTypes.VISUAL_MIMES, mimeType);
255         final boolean showThumbnail = supportsThumbnail && allowThumbnail && mThumbnailsEnabled;
256         if (showThumbnail) {
257             loadedThumbnail =
258                 loadThumbnail(uri, docAuthority, docLastModified, iconThumb, iconMime);
259         }
260 
261         final Drawable mimeIcon = getDocumentIcon(mContext, docAuthority,
262                 DocumentsContract.getDocumentId(uri), mimeType, docIcon);
263         if (subIconMime != null) {
264             setMimeIcon(subIconMime, mimeIcon);
265         }
266 
267         if (loadedThumbnail) {
268             hideImageView(iconMime);
269         } else {
270             // Add a mime icon if the thumbnail is not shown.
271             setMimeIcon(iconMime, mimeIcon);
272             hideImageView(iconThumb);
273         }
274     }
275 
loadThumbnail(Uri uri, String docAuthority, long docLastModified, ImageView iconThumb, ImageView iconMime)276     private boolean loadThumbnail(Uri uri, String docAuthority, long docLastModified,
277             ImageView iconThumb, ImageView iconMime) {
278         final Result result = mThumbnailCache.getThumbnail(uri, mCurrentSize);
279 
280         try {
281             final Bitmap cachedThumbnail = result.getThumbnail();
282             iconThumb.setImageBitmap(cachedThumbnail);
283 
284             boolean stale = (docLastModified > result.getLastModified());
285             if (VERBOSE) Log.v(TAG,
286                     String.format("Load thumbnail for %s, got result %d and stale %b.",
287                             uri.toString(), result.getStatus(), stale));
288             if (!result.isExactHit() || stale) {
289                 final BiConsumer<View, View> animator =
290                         (cachedThumbnail == null ? ANIM_FADE_IN : ANIM_NO_OP);
291                 final LoaderTask task = new LoaderTask(uri, iconMime, iconThumb, mCurrentSize,
292                         docLastModified, animator);
293 
294                 iconThumb.setTag(task);
295 
296                 ProviderExecutor.forAuthority(docAuthority).execute(task);
297             }
298 
299             return result.isHit();
300         } finally {
301             result.recycle();
302         }
303     }
304 
setMimeIcon(ImageView view, Drawable icon)305     private void setMimeIcon(ImageView view, Drawable icon) {
306         view.setImageDrawable(icon);
307         view.setAlpha(1f);
308     }
309 
hideImageView(ImageView view)310     private void hideImageView(ImageView view) {
311         view.setImageDrawable(null);
312         view.setAlpha(0f);
313     }
314 
getDocumentIcon( Context context, String authority, String id, String mimeType, int icon)315     private Drawable getDocumentIcon(
316         Context context, String authority, String id, String mimeType, int icon) {
317         if (icon != 0) {
318             return IconUtils.loadPackageIcon(context, authority, icon);
319         } else {
320             return IconUtils.loadMimeIcon(context, mimeType, authority, id, mMode);
321         }
322     }
323 
324     /**
325      * Returns a mime icon or package icon for a {@link DocumentInfo}.
326      */
getDocumentIcon(Context context, DocumentInfo doc)327     public Drawable getDocumentIcon(Context context, DocumentInfo doc) {
328         return getDocumentIcon(
329                 context, doc.authority, doc.documentId, doc.mimeType, doc.icon);
330     }
331 }
332