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