diff --git a/.travis.yml b/.travis.yml
index 5fa2230b24..51414ab4bb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,7 +6,7 @@ before_install:
- mkdir "$ANDROID_HOME/licenses" || true
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55\nd56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd\n504667f4c0de7af1a06de9f4b1727b84351f2910" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
- - yes | $ANDROID_HOME/tools/bin/sdkmanager "build-tools;28.0.3" "platforms;android-28"
+ - yes | $ANDROID_HOME/tools/bin/sdkmanager "build-tools;29.0.2" "platforms;android-29"
android:
components:
diff --git a/gradle.properties b/gradle.properties
index d86e155e0d..137c168f9a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -24,11 +24,11 @@ OK_HTTP_VERSION=3.9.1
ANDROID_GRADLE_VERSION=3.3.0
DAGGER_VERSION=2.15
-JUNIT_VERSION=4.13-SNAPSHOT
+JUNIT_VERSION=4.13-beta-3
# Matches the version in Google.
MOCKITO_VERSION=2.23.4
MOCKITO_ANDROID_VERSION=2.24.0
-ROBOLECTRIC_VERSION=4.3-beta-1
+ROBOLECTRIC_VERSION=4.3.1
MOCKWEBSERVER_VERSION=3.0.0-RC1
TRUTH_VERSION=0.45
JSR_305_VERSION=3.0.2
@@ -42,7 +42,7 @@ ERROR_PRONE_VERSION=2.3.1
ERROR_PRONE_PLUGIN_VERSION=0.0.13
VIOLATIONS_PLUGIN_VERSION=1.8
-COMPILE_SDK_VERSION=28
+COMPILE_SDK_VERSION=29
TARGET_SDK_VERSION=28
MIN_SDK_VERSION=14
diff --git a/library/src/main/java/com/bumptech/glide/Glide.java b/library/src/main/java/com/bumptech/glide/Glide.java
index 20a2be95a0..13b9bc10a1 100644
--- a/library/src/main/java/com/bumptech/glide/Glide.java
+++ b/library/src/main/java/com/bumptech/glide/Glide.java
@@ -52,6 +52,7 @@
import com.bumptech.glide.load.model.stream.HttpUriLoader;
import com.bumptech.glide.load.model.stream.MediaStoreImageThumbLoader;
import com.bumptech.glide.load.model.stream.MediaStoreVideoThumbLoader;
+import com.bumptech.glide.load.model.stream.QMediaStoreUriLoader;
import com.bumptech.glide.load.model.stream.UrlLoader;
import com.bumptech.glide.load.resource.bitmap.BitmapDrawableDecoder;
import com.bumptech.glide.load.resource.bitmap.BitmapDrawableEncoder;
@@ -506,7 +507,16 @@ Uri.class, Bitmap.class, new ResourceBitmapDecoder(resourceDrawableDecoder, bitm
ParcelFileDescriptor.class,
new AssetUriLoader.FileDescriptorFactory(context.getAssets()))
.append(Uri.class, InputStream.class, new MediaStoreImageThumbLoader.Factory(context))
- .append(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context))
+ .append(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ registry.append(
+ Uri.class, InputStream.class, new QMediaStoreUriLoader.InputStreamFactory(context));
+ registry.append(
+ Uri.class,
+ ParcelFileDescriptor.class,
+ new QMediaStoreUriLoader.FileDescriptorFactory(context));
+ }
+ registry
.append(Uri.class, InputStream.class, new UriLoader.StreamFactory(contentResolver))
.append(
Uri.class,
diff --git a/library/src/main/java/com/bumptech/glide/load/model/stream/QMediaStoreUriLoader.java b/library/src/main/java/com/bumptech/glide/load/model/stream/QMediaStoreUriLoader.java
new file mode 100644
index 0000000000..4cce1b6cea
--- /dev/null
+++ b/library/src/main/java/com/bumptech/glide/load/model/stream/QMediaStoreUriLoader.java
@@ -0,0 +1,268 @@
+package com.bumptech.glide.load.model.stream;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.data.DataFetcher;
+import com.bumptech.glide.load.data.mediastore.MediaStoreUtil;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+import com.bumptech.glide.signature.ObjectKey;
+import com.bumptech.glide.util.Synthetic;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+/**
+ * Best effort attempt to work around various Q storage states and bugs.
+ *
+ *
In particular, HEIC images on Q cannot be decoded if they've gone through Android's exif
+ * redaction, due to a bug in the implementation that corrupts the file. To avoid the issue, we need
+ * to get at the un-redacted File. There are two ways we can do so:
+ *
+ *
+ * - MediaStore.setRequireOriginal
+ *
- Querying for and opening the file via the underlying file path, rather than via {@code
+ * ContentResolver}
+ *
+ *
+ * MediaStore.setRequireOriginal will only work for applications that target Q and request and
+ * currently have {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}. It's the simplest
+ * change to make, but it covers the fewest applications.
+ *
+ *
Querying for the file path and opening the file directly works for applications that do not
+ * target Q and for applications that do target Q but that opt in to legacy storage mode. Other
+ * options are theoretically available for applications that do not target Q, but due to other bugs,
+ * the only consistent way to get unredacted files is via the file system.
+ *
+ *
This class does not fix applications that target Q, do not opt in to legacy storage and that
+ * don't have {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}.
+ *
+ *
Avoid using this class directly, it may be removed in any future version of Glide.
+ *
+ * @param The type of data this loader will load ({@link InputStream}, {@link
+ * ParcelFileDescriptor}).
+ */
+@RequiresApi(Build.VERSION_CODES.Q)
+public final class QMediaStoreUriLoader implements ModelLoader {
+ private final Context context;
+ private final ModelLoader fileDelegate;
+ private final ModelLoader uriDelegate;
+ private final Class dataClass;
+
+ @SuppressWarnings("WeakerAccess")
+ @Synthetic
+ QMediaStoreUriLoader(
+ Context context,
+ ModelLoader fileDelegate,
+ ModelLoader uriDelegate,
+ Class dataClass) {
+ this.context = context.getApplicationContext();
+ this.fileDelegate = fileDelegate;
+ this.uriDelegate = uriDelegate;
+ this.dataClass = dataClass;
+ }
+
+ @Override
+ public LoadData buildLoadData(
+ @NonNull Uri uri, int width, int height, @NonNull Options options) {
+ return new LoadData<>(
+ new ObjectKey(uri),
+ new QMediaStoreUriFetcher<>(
+ context, fileDelegate, uriDelegate, uri, width, height, options, dataClass));
+ }
+
+ @Override
+ public boolean handles(@NonNull Uri uri) {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && MediaStoreUtil.isMediaStoreUri(uri);
+ }
+
+ private static final class QMediaStoreUriFetcher implements DataFetcher {
+ private static final String[] PROJECTION = new String[] {MediaStore.MediaColumns.DATA};
+
+ private final Context context;
+ private final ModelLoader fileDelegate;
+ private final ModelLoader uriDelegate;
+ private final Uri uri;
+ private final int width;
+ private final int height;
+ private final Options options;
+ private final Class dataClass;
+
+ private volatile boolean isCancelled;
+ @Nullable private volatile DataFetcher delegate;
+
+ QMediaStoreUriFetcher(
+ Context context,
+ ModelLoader fileDelegate,
+ ModelLoader uriDelegate,
+ Uri uri,
+ int width,
+ int height,
+ Options options,
+ Class dataClass) {
+ this.context = context.getApplicationContext();
+ this.fileDelegate = fileDelegate;
+ this.uriDelegate = uriDelegate;
+ this.uri = uri;
+ this.width = width;
+ this.height = height;
+ this.options = options;
+ this.dataClass = dataClass;
+ }
+
+ @Override
+ public void loadData(
+ @NonNull Priority priority, @NonNull DataCallback super DataT> callback) {
+ try {
+ DataFetcher local = buildDelegateFetcher();
+ if (local == null) {
+ callback.onLoadFailed(
+ new IllegalArgumentException("Failed to build fetcher for: " + uri));
+ return;
+ }
+ delegate = local;
+ if (isCancelled) {
+ cancel();
+ } else {
+ local.loadData(priority, callback);
+ }
+ } catch (FileNotFoundException e) {
+ callback.onLoadFailed(e);
+ }
+ }
+
+ @Nullable
+ private DataFetcher buildDelegateFetcher() throws FileNotFoundException {
+ LoadData result = buildDelegateData();
+ return result != null ? result.fetcher : null;
+ }
+
+ @Nullable
+ private LoadData buildDelegateData() throws FileNotFoundException {
+ if (Environment.isExternalStorageLegacy()) {
+ return fileDelegate.buildLoadData(queryForFilePath(uri), width, height, options);
+ } else {
+ Uri toLoad = isAccessMediaLocationGranted() ? MediaStore.setRequireOriginal(uri) : uri;
+ return uriDelegate.buildLoadData(toLoad, width, height, options);
+ }
+ }
+
+ @Override
+ public void cleanup() {
+ DataFetcher local = delegate;
+ if (local != null) {
+ local.cleanup();
+ }
+ }
+
+ @Override
+ public void cancel() {
+ isCancelled = true;
+ DataFetcher local = delegate;
+ if (local != null) {
+ local.cancel();
+ }
+ }
+
+ @NonNull
+ @Override
+ public Class getDataClass() {
+ return dataClass;
+ }
+
+ @NonNull
+ @Override
+ public DataSource getDataSource() {
+ return DataSource.LOCAL;
+ }
+
+ @NonNull
+ private File queryForFilePath(Uri uri) throws FileNotFoundException {
+ Cursor cursor = null;
+ try {
+ cursor =
+ context
+ .getContentResolver()
+ .query(
+ uri,
+ PROJECTION,
+ /*selection=*/ null,
+ /*selectionArgs=*/ null,
+ /*sortOrder=*/ null);
+ if (cursor == null || !cursor.moveToFirst()) {
+ throw new FileNotFoundException("Failed to media store entry for: " + uri);
+ }
+ String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA));
+ if (TextUtils.isEmpty(path)) {
+ throw new FileNotFoundException("File path was empty in media store for: " + uri);
+ }
+ return new File(path);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private boolean isAccessMediaLocationGranted() {
+ return context.checkSelfPermission(android.Manifest.permission.ACCESS_MEDIA_LOCATION)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+ }
+
+ /** Factory for {@link InputStream}. */
+ @RequiresApi(Build.VERSION_CODES.Q)
+ public static final class InputStreamFactory extends Factory {
+ public InputStreamFactory(Context context) {
+ super(context, InputStream.class);
+ }
+ }
+
+ /** Factory for {@link ParcelFileDescriptor}. */
+ @RequiresApi(Build.VERSION_CODES.Q)
+ public static final class FileDescriptorFactory extends Factory {
+ public FileDescriptorFactory(Context context) {
+ super(context, ParcelFileDescriptor.class);
+ }
+ }
+
+ private abstract static class Factory implements ModelLoaderFactory {
+
+ private final Context context;
+ private final Class dataClass;
+
+ Factory(Context context, Class dataClass) {
+ this.context = context;
+ this.dataClass = dataClass;
+ }
+
+ @NonNull
+ @Override
+ public final ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) {
+ return new QMediaStoreUriLoader<>(
+ context,
+ multiFactory.build(File.class, dataClass),
+ multiFactory.build(Uri.class, dataClass),
+ dataClass);
+ }
+
+ @Override
+ public final void teardown() {
+ // Do nothing.
+ }
+ }
+}
diff --git a/samples/gallery/src/main/java/com/bumptech/glide/samples/gallery/MediaStoreDataLoader.java b/samples/gallery/src/main/java/com/bumptech/glide/samples/gallery/MediaStoreDataLoader.java
index 8123cca3d4..1eb5b35252 100644
--- a/samples/gallery/src/main/java/com/bumptech/glide/samples/gallery/MediaStoreDataLoader.java
+++ b/samples/gallery/src/main/java/com/bumptech/glide/samples/gallery/MediaStoreDataLoader.java
@@ -12,6 +12,7 @@
import java.util.List;
/** Loads metadata from the media store for images and videos. */
+@SuppressWarnings("InlinedApi")
public class MediaStoreDataLoader extends AsyncTaskLoader> {
private static final String[] IMAGE_PROJECTION =
new String[] {