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, final BitmapReceiver receiver)113     public void decodeBitmapRegion(final Rect rect, int targetWidth, int targetHeight,
114                                    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, 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