• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         } catch (SecurityException e) {
218             Log.w(TAG, "Image file not accessible", e);
219             return null;
220         }
221     }
222 
223     @Override
getExifOrientation()224     public int getExifOrientation() {
225         if (mExifOrientation != ExifInterfaceCompat.EXIF_ORIENTATION_UNKNOWN) {
226             return mExifOrientation;
227         }
228 
229         mExifOrientation = readExifOrientation();
230         return mExifOrientation;
231     }
232 
233     /**
234      * Returns the EXIF rotation for the content URI asset. This method should only be called off
235      * the main UI thread.
236      */
readExifOrientation()237     private int readExifOrientation() {
238         ensureExifInterface();
239         if (mExifCompat == null) {
240             Log.w(TAG, "Unable to read EXIF rotation for content URI asset with content URI: "
241                     + mUri);
242             return ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL;
243         }
244 
245         return mExifCompat.getAttributeInt(ExifInterfaceCompat.TAG_ORIENTATION,
246                 ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL);
247     }
248 
249     @Override
loadDrawable(Context context, ImageView imageView, int placeholderColor)250     public void loadDrawable(Context context, ImageView imageView,
251                              int placeholderColor) {
252         Glide.with(context)
253                 .asDrawable()
254                 .load(mUri)
255                 .apply(mRequestOptions
256                         .placeholder(new ColorDrawable(placeholderColor)))
257                 .transition(DrawableTransitionOptions.withCrossFade())
258                 .into(imageView);
259     }
260 
261     @Override
loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, BitmapTransformation transformation)262     public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor,
263             BitmapTransformation transformation) {
264         MultiTransformation<Bitmap> multiTransformation =
265                 new MultiTransformation<>(new FitCenter(), transformation);
266         Glide.with(activity)
267                 .asDrawable()
268                 .load(mUri)
269                 .apply(RequestOptions.bitmapTransform(multiTransformation)
270                         .placeholder(new ColorDrawable(placeholderColor)))
271                 .into(imageView);
272     }
273 
274     @Override
loadDrawableWithTransition(Context context, ImageView imageView, int transitionDurationMillis, @Nullable DrawableLoadedListener drawableLoadedListener, int placeholderColor)275     public void loadDrawableWithTransition(Context context, ImageView imageView,
276             int transitionDurationMillis, @Nullable DrawableLoadedListener drawableLoadedListener,
277             int placeholderColor) {
278         Glide.with(context)
279                 .asDrawable()
280                 .load(mUri)
281                 .apply(mRequestOptions
282                         .placeholder(new ColorDrawable(placeholderColor)))
283                 .transition(DrawableTransitionOptions.withCrossFade(transitionDurationMillis))
284                 .listener(new RequestListener<Drawable>() {
285                     @Override
286                     public boolean onLoadFailed(GlideException e, Object model,
287                             Target<Drawable> target, boolean isFirstResource) {
288                         return false;
289                     }
290 
291                     @Override
292                     public boolean onResourceReady(Drawable resource, Object model,
293                             Target<Drawable> target, DataSource dataSource,
294                             boolean isFirstResource) {
295                         if (drawableLoadedListener != null) {
296                             drawableLoadedListener.onDrawableLoaded();
297                         }
298                         return false;
299                     }
300                 })
301                 .into(imageView);
302     }
303 
getUri()304     public Uri getUri() {
305         return mUri;
306     }
307 }
308