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