blob: 4434f8682ffd86779f2d0c73f2c61d6a1c9b7136 [file] [log] [blame]
Jon Miranda16ea1b12017-12-12 14:52:48 -08001/*
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 */
16package com.android.wallpaper.asset;
17
18import android.app.Activity;
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Point;
22import android.graphics.Rect;
23import android.graphics.drawable.ColorDrawable;
24import android.net.Uri;
25import android.os.AsyncTask;
Jon Miranda16ea1b12017-12-12 14:52:48 -080026import android.util.Log;
27import android.widget.ImageView;
28
29import com.bumptech.glide.Glide;
30import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
31import com.bumptech.glide.request.RequestOptions;
32
33import java.io.FileNotFoundException;
34import java.io.IOException;
35import java.io.InputStream;
36
Sunny Goyal8600a3f2018-08-15 12:48:01 -070037import androidx.annotation.Nullable;
38
Jon Miranda16ea1b12017-12-12 14:52:48 -080039/**
40 * Represents an asset located via an Android content URI.
41 */
42public final class ContentUriAsset extends StreamableAsset {
43 private static final String TAG = "ContentUriAsset";
44 private static final String JPEG_MIME_TYPE = "image/jpeg";
45 private static final String PNG_MIME_TYPE = "image/png";
Jon Miranda16ea1b12017-12-12 14:52:48 -080046
47 private final Context mContext;
48 private final Uri mUri;
49
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -070050 private ExifInterfaceCompat mExifCompat;
Jon Miranda16ea1b12017-12-12 14:52:48 -080051 private int mExifOrientation;
52
53 /**
54 * @param context The application's context.
55 * @param uri Content URI locating the asset.
56 */
57 public ContentUriAsset(Context context, Uri uri) {
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -070058 mExifOrientation = ExifInterfaceCompat.EXIF_ORIENTATION_UNKNOWN;
Jon Miranda16ea1b12017-12-12 14:52:48 -080059 mContext = context.getApplicationContext();
60 mUri = uri;
61 }
62
63 @Override
64 public void decodeBitmapRegion(final Rect rect, int targetWidth, int targetHeight,
65 final BitmapReceiver receiver) {
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -070066 // BitmapRegionDecoder only supports images encoded in either JPEG or PNG, so if the content
67 // URI asset is encoded with another format (for example, GIF), then fall back to cropping a
68 // bitmap region from the full-sized bitmap.
Jon Miranda16ea1b12017-12-12 14:52:48 -080069 if (isJpeg() || isPng()) {
70 super.decodeBitmapRegion(rect, targetWidth, targetHeight, receiver);
71 return;
72 }
73
74 decodeRawDimensions(null /* activity */, new DimensionsReceiver() {
75 @Override
76 public void onDimensionsDecoded(@Nullable Point dimensions) {
77 if (dimensions == null) {
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -070078 Log.e(TAG, "There was an error decoding the asset's raw dimensions with " +
79 "content URI: " + mUri);
Jon Miranda16ea1b12017-12-12 14:52:48 -080080 receiver.onBitmapDecoded(null);
81 return;
82 }
83
84 decodeBitmap(dimensions.x, dimensions.y, new BitmapReceiver() {
85 @Override
86 public void onBitmapDecoded(@Nullable Bitmap fullBitmap) {
87 if (fullBitmap == null) {
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -070088 Log.e(TAG, "There was an error decoding the asset's full bitmap with " +
89 "content URI: " + mUri);
Jon Miranda16ea1b12017-12-12 14:52:48 -080090 receiver.onBitmapDecoded(null);
91 return;
92 }
93
94 BitmapCropTask task = new BitmapCropTask(fullBitmap, rect, receiver);
95 task.execute();
96 }
97 });
98 }
99 });
100 }
101
102 /**
103 * Returns whether this image is encoded in the JPEG file format.
104 */
105 public boolean isJpeg() {
106 String mimeType = mContext.getContentResolver().getType(mUri);
107 return mimeType != null && mimeType.equals(JPEG_MIME_TYPE);
108 }
109
110 /**
111 * Returns whether this image is encoded in the PNG file format.
112 */
113 public boolean isPng() {
114 String mimeType = mContext.getContentResolver().getType(mUri);
115 return mimeType != null && mimeType.equals(PNG_MIME_TYPE);
116 }
117
118 /**
119 * Reads the EXIF tag on the asset. Automatically trims leading and trailing whitespace.
120 *
121 * @return String attribute value for this tag ID, or null if ExifInterface failed to read tags
122 * for this asset, if this tag was not found in the image's metadata, or if this tag was
123 * empty (i.e., only whitespace).
124 */
125 public String readExifTag(String tagId) {
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -0700126 ensureExifInterface();
127 if (mExifCompat == null) {
128 Log.w(TAG, "Unable to read EXIF tags for content URI asset");
129 return null;
Jon Miranda16ea1b12017-12-12 14:52:48 -0800130 }
131
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -0700132
133 String attribute = mExifCompat.getAttribute(tagId);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800134 if (attribute == null || attribute.trim().isEmpty()) {
135 return null;
136 }
137
138 return attribute.trim();
139 }
140
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -0700141 private void ensureExifInterface() {
142 if (mExifCompat == null) {
143 try (InputStream inputStream = openInputStream()) {
144 if (inputStream != null) {
145 mExifCompat = new ExifInterfaceCompat(inputStream);
146 }
147 } catch (IOException e) {
148 Log.w(TAG, "Couldn't read stream for " + mUri, e);
149 }
150 }
151
152 }
153
Jon Miranda16ea1b12017-12-12 14:52:48 -0800154 @Override
155 protected InputStream openInputStream() {
156 try {
157 return mContext.getContentResolver().openInputStream(mUri);
158 } catch (FileNotFoundException e) {
159 Log.w(TAG, "Image file not found", e);
160 return null;
161 }
162 }
163
164 @Override
165 protected int getExifOrientation() {
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -0700166 if (mExifOrientation != ExifInterfaceCompat.EXIF_ORIENTATION_UNKNOWN) {
Jon Miranda16ea1b12017-12-12 14:52:48 -0800167 return mExifOrientation;
168 }
169
170 mExifOrientation = readExifOrientation();
171 return mExifOrientation;
172 }
173
174 /**
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -0700175 * Returns the EXIF rotation for the content URI asset. This method should only be called off
176 * the main UI thread.
Jon Miranda16ea1b12017-12-12 14:52:48 -0800177 */
178 private int readExifOrientation() {
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -0700179 ensureExifInterface();
180 if (mExifCompat == null) {
181 Log.w(TAG, "Unable to read EXIF rotation for content URI asset with content URI: "
182 + mUri);
183 return ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL;
Jon Miranda16ea1b12017-12-12 14:52:48 -0800184 }
185
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -0700186 return mExifCompat.getAttributeInt(ExifInterfaceCompat.TAG_ORIENTATION,
187 ExifInterfaceCompat.EXIF_ORIENTATION_NORMAL);
Jon Miranda16ea1b12017-12-12 14:52:48 -0800188 }
189
190 @Override
191 public void loadDrawable(Activity activity, ImageView imageView,
192 int placeholderColor) {
193 Glide.with(activity)
194 .asDrawable()
195 .load(mUri)
196 .apply(RequestOptions.centerCropTransform()
197 .placeholder(new ColorDrawable(placeholderColor)))
198 .transition(DrawableTransitionOptions.withCrossFade())
199 .into(imageView);
200 }
201
202 /**
203 * Custom AsyncTask which crops a bitmap region from a larger bitmap.
204 */
205 private static class BitmapCropTask extends AsyncTask<Void, Void, Bitmap> {
206
207 private Bitmap mFromBitmap;
208 private Rect mCropRect;
209 private BitmapReceiver mReceiver;
210
211 public BitmapCropTask(Bitmap fromBitmap, Rect cropRect, BitmapReceiver receiver) {
212 mFromBitmap = fromBitmap;
213 mCropRect = cropRect;
214 mReceiver = receiver;
215 }
216
217 @Override
218 protected Bitmap doInBackground(Void... unused) {
219 if (mFromBitmap == null) {
220 return null;
221 }
222
223 return Bitmap.createBitmap(
Santiago Etchebehere8e9a8472018-07-24 14:30:21 -0700224 mFromBitmap, mCropRect.left, mCropRect.top, mCropRect.width(),
225 mCropRect.height());
Jon Miranda16ea1b12017-12-12 14:52:48 -0800226 }
227
228 @Override
229 protected void onPostExecute(Bitmap bitmapRegion) {
230 mReceiver.onBitmapDecoded(bitmapRegion);
231 }
232 }
233
234}