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