1 /* 2 * Copyright (C) 2021 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.providers.media.photopicker.ui; 18 19 import android.content.Context; 20 import android.graphics.ImageDecoder; 21 import android.graphics.drawable.Drawable; 22 import android.net.Uri; 23 import android.provider.CloudMediaProviderContract; 24 import android.provider.MediaStore; 25 import android.util.Log; 26 import android.widget.ImageView; 27 28 import androidx.annotation.NonNull; 29 30 import com.android.providers.media.photopicker.data.model.Category; 31 import com.android.providers.media.photopicker.data.model.Item; 32 33 import com.bumptech.glide.Glide; 34 import com.bumptech.glide.load.Option; 35 import com.bumptech.glide.request.RequestOptions; 36 import com.bumptech.glide.signature.ObjectKey; 37 38 /** 39 * A class to assist with loading and managing the Images (i.e. thumbnails and preview) associated 40 * with item. 41 */ 42 public class ImageLoader { 43 44 public static final Option<Boolean> THUMBNAIL_REQUEST = 45 Option.memory(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB, false); 46 private static final String TAG = "ImageLoader"; 47 private final Context mContext; 48 ImageLoader(Context context)49 public ImageLoader(Context context) { 50 mContext = context; 51 } 52 53 /** 54 * Load the thumbnail of the {@code category} and set it on the {@code imageView} 55 * 56 * @param category the album 57 * @param imageView the imageView shows the thumbnail 58 */ loadAlbumThumbnail(@onNull Category category, @NonNull ImageView imageView)59 public void loadAlbumThumbnail(@NonNull Category category, @NonNull ImageView imageView) { 60 // Always show all thumbnails as bitmap images instead of drawables 61 // This is to ensure that we do not animate any thumbnail (for eg GIF) 62 // TODO(b/194285082): Use drawable instead of bitmap, as it saves memory. 63 Glide.with(mContext) 64 .asBitmap() 65 .load(category.getCoverUri()) 66 .apply(RequestOptions.option(THUMBNAIL_REQUEST, true)) 67 .into(imageView); 68 } 69 70 /** 71 * Load the thumbnail of the photo item {@code item} and set it on the {@code imageView} 72 * 73 * @param item the photo item 74 * @param imageView the imageView shows the thumbnail 75 */ loadPhotoThumbnail(@onNull Item item, @NonNull ImageView imageView)76 public void loadPhotoThumbnail(@NonNull Item item, @NonNull ImageView imageView) { 77 Uri uri = item.getContentUri(); 78 // Always show all thumbnails as bitmap images instead of drawables 79 // This is to ensure that we do not animate any thumbnail (for eg GIF) 80 // TODO(b/194285082): Use drawable instead of bitmap, as it saves memory. 81 Glide.with(mContext) 82 .asBitmap() 83 .load(uri) 84 .signature(getGlideSignature(item, /* prefix */ "")) 85 .apply(RequestOptions.option(THUMBNAIL_REQUEST, true)) 86 .into(imageView); 87 } 88 89 /** 90 * Load the image of the photo item {@code item} and set it on the {@code imageView} 91 * 92 * @param item the photo item 93 * @param imageView the imageView shows the image 94 */ loadImagePreview(@onNull Item item, @NonNull ImageView imageView)95 public void loadImagePreview(@NonNull Item item, @NonNull ImageView imageView) { 96 if (item.isGif()) { 97 Glide.with(mContext) 98 .asGif() 99 .load(item.getContentUri()) 100 .signature(getGlideSignature(item, /* prefix */ "")) 101 .into(imageView); 102 return; 103 } 104 105 if (item.isAnimatedWebp()) { 106 loadAnimatedWebpPreview(item, imageView); 107 return; 108 } 109 110 // Preview as bitmap image for all other image types 111 Glide.with(mContext) 112 .asBitmap() 113 .load(item.getContentUri()) 114 .signature(getGlideSignature(item, /* prefix */ "")) 115 .into(imageView); 116 } 117 loadAnimatedWebpPreview(@onNull Item item, @NonNull ImageView imageView)118 private void loadAnimatedWebpPreview(@NonNull Item item, @NonNull ImageView imageView) { 119 final Uri uri = item.getContentUri(); 120 final ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), 121 uri); 122 Drawable drawable = null; 123 try { 124 drawable = ImageDecoder.decodeDrawable(source); 125 } catch (Exception e) { 126 Log.d(TAG, "Failed to decode drawable for uri: " + uri, e); 127 } 128 129 // If we failed to decode drawable for a source using ImageDecoder, then try using uri 130 // directly. Glide will show static image for an animated webp. That is okay as we tried our 131 // best to load animated webp but couldn't, and we anyway show the GIF badge in preview. 132 Glide.with(mContext) 133 .load(drawable == null ? uri : drawable) 134 .signature(getGlideSignature(item, /* prefix */ "")) 135 .into(imageView); 136 } 137 138 /** 139 * Loads the image from first frame of the given video item 140 */ loadImageFromVideoForPreview(@onNull Item item, @NonNull ImageView imageView)141 public void loadImageFromVideoForPreview(@NonNull Item item, @NonNull ImageView imageView) { 142 Glide.with(mContext) 143 .asBitmap() 144 .load(item.getContentUri()) 145 .apply(new RequestOptions().frame(1000)) 146 .signature(getGlideSignature(item, "Preview")) 147 .into(imageView); 148 } 149 getGlideSignature(Item item, String prefix)150 private ObjectKey getGlideSignature(Item item, String prefix) { 151 // TODO(b/224725723): Remove media store version from key once MP ids are stable. 152 return new ObjectKey( 153 MediaStore.getVersion(mContext) + prefix + item.getContentUri().toString() + 154 item.getGenerationModified()); 155 } 156 } 157