1 /* 2 * Copyright (C) 2017 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 package com.android.wallpaper.asset; 17 18 import android.app.Activity; 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.Point; 22 import android.graphics.Rect; 23 import android.graphics.drawable.ColorDrawable; 24 import android.graphics.drawable.Drawable; 25 import android.net.Uri; 26 import android.util.Log; 27 import android.widget.ImageView; 28 29 import androidx.annotation.Nullable; 30 31 import com.bumptech.glide.Glide; 32 import com.bumptech.glide.load.DataSource; 33 import com.bumptech.glide.load.MultiTransformation; 34 import com.bumptech.glide.load.engine.DiskCacheStrategy; 35 import com.bumptech.glide.load.engine.GlideException; 36 import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; 37 import com.bumptech.glide.load.resource.bitmap.FitCenter; 38 import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; 39 import com.bumptech.glide.request.RequestListener; 40 import com.bumptech.glide.request.RequestOptions; 41 import com.bumptech.glide.request.target.Target; 42 43 import java.io.FileNotFoundException; 44 import java.io.IOException; 45 import java.io.InputStream; 46 import java.util.concurrent.ExecutorService; 47 import java.util.concurrent.Executors; 48 49 /** 50 * Represents an asset located via an Android content URI. 51 */ 52 public final class ContentUriAsset extends StreamableAsset { 53 private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor(); 54 private static final String TAG = "ContentUriAsset"; 55 private static final String JPEG_MIME_TYPE = "image/jpeg"; 56 private static final String PNG_MIME_TYPE = "image/png"; 57 58 private final Context mContext; 59 private final Uri mUri; 60 private final RequestOptions mRequestOptions; 61 62 private ExifInterfaceCompat mExifCompat; 63 private int mExifOrientation; 64 65 /** 66 * @param context The application's context. 67 * @param uri Content URI locating the asset. 68 * @param requestOptions {@link RequestOptions} to be applied when loading the asset. 69 * @param uncached If true, {@link #loadDrawable(Context, ImageView, int)} and 70 * {@link #loadDrawableWithTransition(Context, ImageView, int, DrawableLoadedListener, int)} 71 * will not cache data, and fetch it each time. 72 */ ContentUriAsset(Context context, Uri uri, RequestOptions requestOptions, boolean uncached)73 public ContentUriAsset(Context context, Uri uri, RequestOptions requestOptions, 74 boolean uncached) { 75 mExifOrientation = ExifInterfaceCompat.EXIF_ORIENTATION_UNKNOWN; 76 mContext = context.getApplicationContext(); 77 mUri = uri; 78 79 if (uncached) { 80 mRequestOptions = requestOptions.apply(RequestOptions 81 .diskCacheStrategyOf(DiskCacheStrategy.NONE) 82 .skipMemoryCache(true)); 83 } else { 84 mRequestOptions = requestOptions; 85 } 86 } 87 88 /** 89 * @param context The application's context. 90 * @param uri Content URI locating the asset. 91 * @param requestOptions {@link RequestOptions} to be applied when loading the asset. 92 */ ContentUriAsset(Context context, Uri uri, RequestOptions requestOptions)93 public ContentUriAsset(Context context, Uri uri, RequestOptions requestOptions) { 94 this(context, uri, requestOptions, /* uncached */ false); 95 } 96 97 /** 98 * @param context The application's context. 99 * @param uri Content URI locating the asset. 100 * @param uncached If true, {@link #loadDrawable(Context, ImageView, int)} and 101 * {@link #loadDrawableWithTransition(Context, ImageView, int, DrawableLoadedListener, int)} 102 * will not cache data, and fetch it each time. 103 */ ContentUriAsset(Context context, Uri uri, boolean uncached)104 public ContentUriAsset(Context context, Uri uri, boolean uncached) { 105 this(context, uri, RequestOptions.centerCropTransform(), uncached); 106 } 107 108 /** 109 * @param context The application's context. 110 * @param uri Content URI locating the asset. 111 */ ContentUriAsset(Context context, Uri uri)112 public ContentUriAsset(Context context, Uri uri) { 113 this(context, uri, /* uncached */ false); 114 } 115 116 117 118 @Override decodeBitmapRegion(final Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, final BitmapReceiver receiver)119 public void decodeBitmapRegion(final Rect rect, int targetWidth, int targetHeight, 120 boolean shouldAdjustForRtl, final BitmapReceiver receiver) { 121 // BitmapRegionDecoder only supports images encoded in either JPEG or PNG, so if the content 122 // URI asset is encoded with another format (for example, GIF), then fall back to cropping a 123 // bitmap region from the full-sized bitmap. 124 if (isJpeg() || isPng()) { 125 super.decodeBitmapRegion(rect, targetWidth, targetHeight, shouldAdjustForRtl, receiver); 126 return; 127 } 128 129 decodeRawDimensions(null /* activity */, new DimensionsReceiver() { 130 @Override 131 public void onDimensionsDecoded(@Nullable Point dimensions) { 132 if (dimensions == null) { 133 Log.e(TAG, "There was an error decoding the asset's raw dimensions with " + 134 "content URI: " + mUri); 135 receiver.onBitmapDecoded(null); 136 return; 137 } 138 139 decodeBitmap(dimensions.x, dimensions.y, new BitmapReceiver() { 140 @Override 141 public void onBitmapDecoded(@Nullable Bitmap fullBitmap) { 142 if (fullBitmap == null) { 143 Log.e(TAG, "There was an error decoding the asset's full bitmap with " + 144 "content URI: " + mUri); 145 decodeBitmapCompleted(receiver, null); 146 return; 147 } 148 sExecutorService.execute(()-> { 149 decodeBitmapCompleted(receiver, Bitmap.createBitmap( 150 fullBitmap, rect.left, rect.top, rect.width(), rect.height())); 151 }); 152 } 153 }); 154 } 155 }); 156 } 157 158 /** 159 * Returns whether this image is encoded in the JPEG file format. 160 */ isJpeg()161 public boolean isJpeg() { 162 String mimeType = mContext.getContentResolver().getType(mUri); 163 return mimeType != null && mimeType.equals(JPEG_MIME_TYPE); 164 } 165 166 /** 167 * Returns whether this image is encoded in the PNG file format. 168 */ isPng()169 public boolean isPng() { 170 String mimeType = mContext.getContentResolver().getType(mUri); 171 return mimeType != null && mimeType.equals(PNG_MIME_TYPE); 172 } 173 174 /** 175 * Reads the EXIF tag on the asset. Automatically trims leading and trailing whitespace. 176 * 177 * @return String attribute value for this tag ID, or null if ExifInterface failed to read tags 178 * for this asset, if this tag was not found in the image's metadata, or if this tag was 179 * empty (i.e., only whitespace). 180 */ readExifTag(String tagId)181 public String readExifTag(String tagId) { 182 ensureExifInterface(); 183 if (mExifCompat == null) { 184 Log.w(TAG, "Unable to read EXIF tags for content URI asset"); 185 return null; 186 } 187 188 189 String attribute = mExifCompat.getAttribute(tagId); 190 if (attribute == null || attribute.trim().isEmpty()) { 191 return null; 192 } 193 194 return attribute.trim(); 195 } 196 ensureExifInterface()197 private void ensureExifInterface() { 198 if (mExifCompat == null) { 199 try (InputStream inputStream = openInputStream()) { 200 if (inputStream != null) { 201 mExifCompat = new ExifInterfaceCompat(inputStream); 202 } 203 } catch (IOException e) { 204 Log.w(TAG, "Couldn't read stream for " + mUri, e); 205 } 206 } 207 208 } 209 210 @Override openInputStream()211 protected InputStream openInputStream() { 212 try { 213 return mContext.getContentResolver().openInputStream(mUri); 214 } catch (FileNotFoundException e) { 215 Log.w(TAG, "Image file not found", e); 216 return null; 217 } 218 } 219 220 @Override getExifOrientation()221 public int getExifOrientation() { 222 if (mExifOrientation != ExifInterfaceCompat.EXIF_ORIENTATION_UNKNOWN) { 223 return mExifOrientation; 224 } 225 226 mExifOrientation = readExifOrientation(); 227 return mExifOrientation; 228 } 229 230 /** 231 * Returns the EXIF rotation for the content URI asset. This method should only be called off 232 * the main UI thread. 233 */ readExifOrientation()234 private int readExifOrientation() { 235 ensureExifInterface(); 236 if (mExifCompat == null) { 237 Log.w(TAG, "Unable to read EXIF rotation for content URI asset with content URI: " 238 + mUri); 239 return ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL; 240 } 241 242 return mExifCompat.getAttributeInt(ExifInterfaceCompat.TAG_ORIENTATION, 243 ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL); 244 } 245 246 @Override loadDrawable(Context context, ImageView imageView, int placeholderColor)247 public void loadDrawable(Context context, ImageView imageView, 248 int placeholderColor) { 249 Glide.with(context) 250 .asDrawable() 251 .load(mUri) 252 .apply(mRequestOptions 253 .placeholder(new ColorDrawable(placeholderColor))) 254 .transition(DrawableTransitionOptions.withCrossFade()) 255 .into(imageView); 256 } 257 258 @Override loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, BitmapTransformation transformation)259 public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, 260 BitmapTransformation transformation) { 261 MultiTransformation<Bitmap> multiTransformation = 262 new MultiTransformation<>(new FitCenter(), transformation); 263 Glide.with(activity) 264 .asDrawable() 265 .load(mUri) 266 .apply(RequestOptions.bitmapTransform(multiTransformation) 267 .placeholder(new ColorDrawable(placeholderColor))) 268 .into(imageView); 269 } 270 271 @Override loadDrawableWithTransition(Context context, ImageView imageView, int transitionDurationMillis, @Nullable DrawableLoadedListener drawableLoadedListener, int placeholderColor)272 public void loadDrawableWithTransition(Context context, ImageView imageView, 273 int transitionDurationMillis, @Nullable DrawableLoadedListener drawableLoadedListener, 274 int placeholderColor) { 275 Glide.with(context) 276 .asDrawable() 277 .load(mUri) 278 .apply(mRequestOptions 279 .placeholder(new ColorDrawable(placeholderColor))) 280 .transition(DrawableTransitionOptions.withCrossFade(transitionDurationMillis)) 281 .listener(new RequestListener<Drawable>() { 282 @Override 283 public boolean onLoadFailed(GlideException e, Object model, 284 Target<Drawable> target, boolean isFirstResource) { 285 return false; 286 } 287 288 @Override 289 public boolean onResourceReady(Drawable resource, Object model, 290 Target<Drawable> target, DataSource dataSource, 291 boolean isFirstResource) { 292 if (drawableLoadedListener != null) { 293 drawableLoadedListener.onDrawableLoaded(); 294 } 295 return false; 296 } 297 }) 298 .into(imageView); 299 } 300 getUri()301 public Uri getUri() { 302 return mUri; 303 } 304 } 305