Skip to content

Commit 1c51b24

Browse files
sjuddglide-copybara-robot
authored andcommittedOct 23, 2019
Add Q ModelLoader to load unredacted data when possible to avoid HEIC failures.
EXIF redaction in MediaStore on Q breaks HEIC/HEIF decoding. This class does it's best to obtain unredacted file data on Q depending on the state the hosting application is in with regards to storage. It's not a complete fix. In particular applications that target Q, do not opt in to legacy storage and do not have the ACCESS_MEDIA_LOCATION permission will still be unable to decode HEIC/HEIF after this change. PiperOrigin-RevId: 276302007
1 parent 42d3f07 commit 1c51b24

File tree

5 files changed

+284
-5
lines changed

5 files changed

+284
-5
lines changed
 

‎.travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ before_install:
66
- mkdir "$ANDROID_HOME/licenses" || true
77
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55\nd56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license"
88
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd\n504667f4c0de7af1a06de9f4b1727b84351f2910" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
9-
- yes | $ANDROID_HOME/tools/bin/sdkmanager "build-tools;28.0.3" "platforms;android-28"
9+
- yes | $ANDROID_HOME/tools/bin/sdkmanager "build-tools;29.0.2" "platforms;android-29"
1010

1111
android:
1212
components:

‎gradle.properties

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ OK_HTTP_VERSION=3.9.1
2424
ANDROID_GRADLE_VERSION=3.3.0
2525
DAGGER_VERSION=2.15
2626

27-
JUNIT_VERSION=4.13-SNAPSHOT
27+
JUNIT_VERSION=4.13-beta-3
2828
# Matches the version in Google.
2929
MOCKITO_VERSION=2.23.4
3030
MOCKITO_ANDROID_VERSION=2.24.0
31-
ROBOLECTRIC_VERSION=4.3-beta-1
31+
ROBOLECTRIC_VERSION=4.3.1
3232
MOCKWEBSERVER_VERSION=3.0.0-RC1
3333
TRUTH_VERSION=0.45
3434
JSR_305_VERSION=3.0.2
@@ -42,7 +42,7 @@ ERROR_PRONE_VERSION=2.3.1
4242
ERROR_PRONE_PLUGIN_VERSION=0.0.13
4343
VIOLATIONS_PLUGIN_VERSION=1.8
4444

45-
COMPILE_SDK_VERSION=28
45+
COMPILE_SDK_VERSION=29
4646
TARGET_SDK_VERSION=28
4747
MIN_SDK_VERSION=14
4848

‎library/src/main/java/com/bumptech/glide/Glide.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import com.bumptech.glide.load.model.stream.HttpUriLoader;
5353
import com.bumptech.glide.load.model.stream.MediaStoreImageThumbLoader;
5454
import com.bumptech.glide.load.model.stream.MediaStoreVideoThumbLoader;
55+
import com.bumptech.glide.load.model.stream.QMediaStoreUriLoader;
5556
import com.bumptech.glide.load.model.stream.UrlLoader;
5657
import com.bumptech.glide.load.resource.bitmap.BitmapDrawableDecoder;
5758
import com.bumptech.glide.load.resource.bitmap.BitmapDrawableEncoder;
@@ -506,7 +507,16 @@ Uri.class, Bitmap.class, new ResourceBitmapDecoder(resourceDrawableDecoder, bitm
506507
ParcelFileDescriptor.class,
507508
new AssetUriLoader.FileDescriptorFactory(context.getAssets()))
508509
.append(Uri.class, InputStream.class, new MediaStoreImageThumbLoader.Factory(context))
509-
.append(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context))
510+
.append(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context));
511+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
512+
registry.append(
513+
Uri.class, InputStream.class, new QMediaStoreUriLoader.InputStreamFactory(context));
514+
registry.append(
515+
Uri.class,
516+
ParcelFileDescriptor.class,
517+
new QMediaStoreUriLoader.FileDescriptorFactory(context));
518+
}
519+
registry
510520
.append(Uri.class, InputStream.class, new UriLoader.StreamFactory(contentResolver))
511521
.append(
512522
Uri.class,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
package com.bumptech.glide.load.model.stream;
2+
3+
import android.content.Context;
4+
import android.content.pm.PackageManager;
5+
import android.database.Cursor;
6+
import android.net.Uri;
7+
import android.os.Build;
8+
import android.os.Environment;
9+
import android.os.ParcelFileDescriptor;
10+
import android.provider.MediaStore;
11+
import android.text.TextUtils;
12+
import androidx.annotation.NonNull;
13+
import androidx.annotation.Nullable;
14+
import androidx.annotation.RequiresApi;
15+
import com.bumptech.glide.Priority;
16+
import com.bumptech.glide.load.DataSource;
17+
import com.bumptech.glide.load.Options;
18+
import com.bumptech.glide.load.data.DataFetcher;
19+
import com.bumptech.glide.load.data.mediastore.MediaStoreUtil;
20+
import com.bumptech.glide.load.model.ModelLoader;
21+
import com.bumptech.glide.load.model.ModelLoaderFactory;
22+
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
23+
import com.bumptech.glide.signature.ObjectKey;
24+
import com.bumptech.glide.util.Synthetic;
25+
import java.io.File;
26+
import java.io.FileNotFoundException;
27+
import java.io.InputStream;
28+
29+
/**
30+
* Best effort attempt to work around various Q storage states and bugs.
31+
*
32+
* <p>In particular, HEIC images on Q cannot be decoded if they've gone through Android's exif
33+
* redaction, due to a bug in the implementation that corrupts the file. To avoid the issue, we need
34+
* to get at the un-redacted File. There are two ways we can do so:
35+
*
36+
* <ul>
37+
* <li>MediaStore.setRequireOriginal
38+
* <li>Querying for and opening the file via the underlying file path, rather than via {@code
39+
* ContentResolver}
40+
* </ul>
41+
*
42+
* <p>MediaStore.setRequireOriginal will only work for applications that target Q and request and
43+
* currently have {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}. It's the simplest
44+
* change to make, but it covers the fewest applications.
45+
*
46+
* <p>Querying for the file path and opening the file directly works for applications that do not
47+
* target Q and for applications that do target Q but that opt in to legacy storage mode. Other
48+
* options are theoretically available for applications that do not target Q, but due to other bugs,
49+
* the only consistent way to get unredacted files is via the file system.
50+
*
51+
* <p>This class does not fix applications that target Q, do not opt in to legacy storage and that
52+
* don't have {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}.
53+
*
54+
* <p>Avoid using this class directly, it may be removed in any future version of Glide.
55+
*
56+
* @param <DataT> The type of data this loader will load ({@link InputStream}, {@link
57+
* ParcelFileDescriptor}).
58+
*/
59+
@RequiresApi(Build.VERSION_CODES.Q)
60+
public final class QMediaStoreUriLoader<DataT> implements ModelLoader<Uri, DataT> {
61+
private final Context context;
62+
private final ModelLoader<File, DataT> fileDelegate;
63+
private final ModelLoader<Uri, DataT> uriDelegate;
64+
private final Class<DataT> dataClass;
65+
66+
@SuppressWarnings("WeakerAccess")
67+
@Synthetic
68+
QMediaStoreUriLoader(
69+
Context context,
70+
ModelLoader<File, DataT> fileDelegate,
71+
ModelLoader<Uri, DataT> uriDelegate,
72+
Class<DataT> dataClass) {
73+
this.context = context.getApplicationContext();
74+
this.fileDelegate = fileDelegate;
75+
this.uriDelegate = uriDelegate;
76+
this.dataClass = dataClass;
77+
}
78+
79+
@Override
80+
public LoadData<DataT> buildLoadData(
81+
@NonNull Uri uri, int width, int height, @NonNull Options options) {
82+
return new LoadData<>(
83+
new ObjectKey(uri),
84+
new QMediaStoreUriFetcher<>(
85+
context, fileDelegate, uriDelegate, uri, width, height, options, dataClass));
86+
}
87+
88+
@Override
89+
public boolean handles(@NonNull Uri uri) {
90+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && MediaStoreUtil.isMediaStoreUri(uri);
91+
}
92+
93+
private static final class QMediaStoreUriFetcher<DataT> implements DataFetcher<DataT> {
94+
private static final String[] PROJECTION = new String[] {MediaStore.MediaColumns.DATA};
95+
96+
private final Context context;
97+
private final ModelLoader<File, DataT> fileDelegate;
98+
private final ModelLoader<Uri, DataT> uriDelegate;
99+
private final Uri uri;
100+
private final int width;
101+
private final int height;
102+
private final Options options;
103+
private final Class<DataT> dataClass;
104+
105+
private volatile boolean isCancelled;
106+
@Nullable private volatile DataFetcher<DataT> delegate;
107+
108+
QMediaStoreUriFetcher(
109+
Context context,
110+
ModelLoader<File, DataT> fileDelegate,
111+
ModelLoader<Uri, DataT> uriDelegate,
112+
Uri uri,
113+
int width,
114+
int height,
115+
Options options,
116+
Class<DataT> dataClass) {
117+
this.context = context.getApplicationContext();
118+
this.fileDelegate = fileDelegate;
119+
this.uriDelegate = uriDelegate;
120+
this.uri = uri;
121+
this.width = width;
122+
this.height = height;
123+
this.options = options;
124+
this.dataClass = dataClass;
125+
}
126+
127+
@Override
128+
public void loadData(
129+
@NonNull Priority priority, @NonNull DataCallback<? super DataT> callback) {
130+
try {
131+
DataFetcher<DataT> local = buildDelegateFetcher();
132+
if (local == null) {
133+
callback.onLoadFailed(
134+
new IllegalArgumentException("Failed to build fetcher for: " + uri));
135+
return;
136+
}
137+
delegate = local;
138+
if (isCancelled) {
139+
cancel();
140+
} else {
141+
local.loadData(priority, callback);
142+
}
143+
} catch (FileNotFoundException e) {
144+
callback.onLoadFailed(e);
145+
}
146+
}
147+
148+
@Nullable
149+
private DataFetcher<DataT> buildDelegateFetcher() throws FileNotFoundException {
150+
LoadData<DataT> result = buildDelegateData();
151+
return result != null ? result.fetcher : null;
152+
}
153+
154+
@Nullable
155+
private LoadData<DataT> buildDelegateData() throws FileNotFoundException {
156+
if (Environment.isExternalStorageLegacy()) {
157+
return fileDelegate.buildLoadData(queryForFilePath(uri), width, height, options);
158+
} else {
159+
Uri toLoad = isAccessMediaLocationGranted() ? MediaStore.setRequireOriginal(uri) : uri;
160+
return uriDelegate.buildLoadData(toLoad, width, height, options);
161+
}
162+
}
163+
164+
@Override
165+
public void cleanup() {
166+
DataFetcher<DataT> local = delegate;
167+
if (local != null) {
168+
local.cleanup();
169+
}
170+
}
171+
172+
@Override
173+
public void cancel() {
174+
isCancelled = true;
175+
DataFetcher<DataT> local = delegate;
176+
if (local != null) {
177+
local.cancel();
178+
}
179+
}
180+
181+
@NonNull
182+
@Override
183+
public Class<DataT> getDataClass() {
184+
return dataClass;
185+
}
186+
187+
@NonNull
188+
@Override
189+
public DataSource getDataSource() {
190+
return DataSource.LOCAL;
191+
}
192+
193+
@NonNull
194+
private File queryForFilePath(Uri uri) throws FileNotFoundException {
195+
Cursor cursor = null;
196+
try {
197+
cursor =
198+
context
199+
.getContentResolver()
200+
.query(
201+
uri,
202+
PROJECTION,
203+
/*selection=*/ null,
204+
/*selectionArgs=*/ null,
205+
/*sortOrder=*/ null);
206+
if (cursor == null || !cursor.moveToFirst()) {
207+
throw new FileNotFoundException("Failed to media store entry for: " + uri);
208+
}
209+
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA));
210+
if (TextUtils.isEmpty(path)) {
211+
throw new FileNotFoundException("File path was empty in media store for: " + uri);
212+
}
213+
return new File(path);
214+
} finally {
215+
if (cursor != null) {
216+
cursor.close();
217+
}
218+
}
219+
}
220+
221+
private boolean isAccessMediaLocationGranted() {
222+
return context.checkSelfPermission(android.Manifest.permission.ACCESS_MEDIA_LOCATION)
223+
== PackageManager.PERMISSION_GRANTED;
224+
}
225+
}
226+
227+
/** Factory for {@link InputStream}. */
228+
@RequiresApi(Build.VERSION_CODES.Q)
229+
public static final class InputStreamFactory extends Factory<InputStream> {
230+
public InputStreamFactory(Context context) {
231+
super(context, InputStream.class);
232+
}
233+
}
234+
235+
/** Factory for {@link ParcelFileDescriptor}. */
236+
@RequiresApi(Build.VERSION_CODES.Q)
237+
public static final class FileDescriptorFactory extends Factory<ParcelFileDescriptor> {
238+
public FileDescriptorFactory(Context context) {
239+
super(context, ParcelFileDescriptor.class);
240+
}
241+
}
242+
243+
private abstract static class Factory<DataT> implements ModelLoaderFactory<Uri, DataT> {
244+
245+
private final Context context;
246+
private final Class<DataT> dataClass;
247+
248+
Factory(Context context, Class<DataT> dataClass) {
249+
this.context = context;
250+
this.dataClass = dataClass;
251+
}
252+
253+
@NonNull
254+
@Override
255+
public final ModelLoader<Uri, DataT> build(@NonNull MultiModelLoaderFactory multiFactory) {
256+
return new QMediaStoreUriLoader<>(
257+
context,
258+
multiFactory.build(File.class, dataClass),
259+
multiFactory.build(Uri.class, dataClass),
260+
dataClass);
261+
}
262+
263+
@Override
264+
public final void teardown() {
265+
// Do nothing.
266+
}
267+
}
268+
}

‎samples/gallery/src/main/java/com/bumptech/glide/samples/gallery/MediaStoreDataLoader.java

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.List;
1313

1414
/** Loads metadata from the media store for images and videos. */
15+
@SuppressWarnings("InlinedApi")
1516
public class MediaStoreDataLoader extends AsyncTaskLoader<List<MediaStoreData>> {
1617
private static final String[] IMAGE_PROJECTION =
1718
new String[] {

0 commit comments

Comments
 (0)
Please sign in to comment.