Reroute Uri conversions though MediaProvider.
Upcoming changes will prevent apps from reading "_data" columns
directly, which is required to convert between DocumentsProvider and
MediaProvider Uris. To solve this, delegate the call() through
MediaProvider, where it can perform the "_data" lookup on behalf
of the caller.
Also add new getMediaUri() call to offer symmetry.
Bug: 111960973, 117627072, 110961701
Test: atest android.provider.cts.MediaStoreUiTest
Change-Id: I53c640704d86047d7a4bf1702aca027873395abf
diff --git a/api/current.txt b/api/current.txt
index 1c3ee82..7f356af 100755
--- a/api/current.txt
+++ b/api/current.txt
@@ -36791,6 +36791,7 @@
ctor public MediaStore();
method public static android.net.Uri getDocumentUri(android.content.Context, android.net.Uri);
method public static android.net.Uri getMediaScannerUri();
+ method public static android.net.Uri getMediaUri(android.content.Context, android.net.Uri);
method public static java.lang.String getVersion(android.content.Context);
field public static final java.lang.String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
field public static final java.lang.String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE";
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 954d18a..67e52aa 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -731,6 +731,8 @@
public static final String EXTRA_PARENT_URI = "parentUri";
/** {@hide} */
public static final String EXTRA_URI = "uri";
+ /** {@hide} */
+ public static final String EXTRA_URI_PERMISSIONS = "uriPermissions";
/**
* @see #createWebLinkIntent(ContentResolver, Uri, Bundle)
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index f5660b9..57f33f0 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -46,9 +46,6 @@
import com.android.internal.annotations.GuardedBy;
-import libcore.io.IoUtils;
-
-import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -82,6 +79,11 @@
*/
public static final String RETRANSLATE_CALL = "update_titles";
+ /** {@hide} */
+ public static final String GET_DOCUMENT_URI_CALL = "get_document_uri";
+ /** {@hide} */
+ public static final String GET_MEDIA_URI_CALL = "get_media_uri";
+
/**
* This is for internal use by the media scanner only.
* Name of the (optional) Uri parameter that determines whether to skip deleting
@@ -2275,84 +2277,62 @@
}
/**
- * Gets a URI backed by a {@link DocumentsProvider} that points to the same media
- * file as the specified mediaUri. This allows apps who have permissions to access
- * media files in Storage Access Framework to perform file operations through that
- * on media files.
+ * Return a {@link DocumentsProvider} Uri that is an equivalent to the given
+ * {@link MediaStore} Uri.
* <p>
- * Note: this method doesn't grant any URI permission. Callers need to obtain
- * permission before calling this method. One way to obtain permission is through
- * a 3-step process:
- * <ol>
- * <li>Call {@link android.os.storage.StorageManager#getStorageVolume(File)} to
- * obtain the {@link android.os.storage.StorageVolume} of a media file;</li>
+ * This allows apps with Storage Access Framework permissions to convert
+ * between {@link MediaStore} and {@link DocumentsProvider} Uris that refer
+ * to the same underlying item. Note that this method doesn't grant any new
+ * permissions; callers must already hold permissions obtained with
+ * {@link Intent#ACTION_OPEN_DOCUMENT} or related APIs.
*
- * <li>Invoke the intent returned by
- * {@link android.os.storage.StorageVolume#createAccessIntent(String)} to
- * obtain the access of the volume or one of its specific subdirectories;</li>
- *
- * <li>Check whether permission is granted and take persistent permission.</li>
- * </ol>
- * @param mediaUri the media URI which document URI is requested
- * @return the document URI
+ * @param mediaUri The {@link MediaStore} Uri to convert.
+ * @return An equivalent {@link DocumentsProvider} Uri. Returns {@code null}
+ * if no equivalent was found.
+ * @see #getMediaUri(Context, Uri)
*/
public static Uri getDocumentUri(Context context, Uri mediaUri) {
+ final ContentResolver resolver = context.getContentResolver();
+ final List<UriPermission> uriPermissions = resolver.getPersistedUriPermissions();
- try {
- final ContentResolver resolver = context.getContentResolver();
-
- final String path = getFilePath(resolver, mediaUri);
- final List<UriPermission> uriPermissions = resolver.getPersistedUriPermissions();
-
- return getDocumentUri(resolver, path, uriPermissions);
+ try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+ final Bundle in = new Bundle();
+ in.putParcelable(DocumentsContract.EXTRA_URI, mediaUri);
+ in.putParcelableList(DocumentsContract.EXTRA_URI_PERMISSIONS, uriPermissions);
+ final Bundle out = client.call(GET_DOCUMENT_URI_CALL, null, in);
+ return out.getParcelable(DocumentsContract.EXTRA_URI);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
- private static String getFilePath(ContentResolver resolver, Uri mediaUri)
- throws RemoteException {
+ /**
+ * Return a {@link MediaStore} Uri that is an equivalent to the given
+ * {@link DocumentsProvider} Uri.
+ * <p>
+ * This allows apps with Storage Access Framework permissions to convert
+ * between {@link MediaStore} and {@link DocumentsProvider} Uris that refer
+ * to the same underlying item. Note that this method doesn't grant any new
+ * permissions; callers must already hold permissions obtained with
+ * {@link Intent#ACTION_OPEN_DOCUMENT} or related APIs.
+ *
+ * @param documentUri The {@link DocumentsProvider} Uri to convert.
+ * @return An equivalent {@link MediaStore} Uri. Returns {@code null} if no
+ * equivalent was found.
+ * @see #getDocumentUri(Context, Uri)
+ */
+ public static Uri getMediaUri(Context context, Uri documentUri) {
+ final ContentResolver resolver = context.getContentResolver();
+ final List<UriPermission> uriPermissions = resolver.getPersistedUriPermissions();
- try (ContentProviderClient client =
- resolver.acquireUnstableContentProviderClient(AUTHORITY)) {
- final Cursor c = client.query(
- mediaUri,
- new String[]{ MediaColumns.DATA },
- null, /* selection */
- null, /* selectionArg */
- null /* sortOrder */);
-
- final String path;
- try {
- if (c.getCount() == 0) {
- throw new IllegalStateException("Not found media file under URI: " + mediaUri);
- }
-
- if (!c.moveToFirst()) {
- throw new IllegalStateException("Failed to move cursor to the first item.");
- }
-
- path = c.getString(0);
- } finally {
- IoUtils.closeQuietly(c);
- }
-
- return path;
- }
- }
-
- private static Uri getDocumentUri(
- ContentResolver resolver, String path, List<UriPermission> uriPermissions)
- throws RemoteException {
-
- try (ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
- DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
+ try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
final Bundle in = new Bundle();
- in.putParcelableList(
- DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY + ".extra.uriPermissions",
- uriPermissions);
- final Bundle out = client.call("getDocumentId", path, in);
+ in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);
+ in.putParcelableList(DocumentsContract.EXTRA_URI_PERMISSIONS, uriPermissions);
+ final Bundle out = client.call(GET_MEDIA_URI_CALL, null, in);
return out.getParcelable(DocumentsContract.EXTRA_URI);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
}
}
}
diff --git a/packages/ExternalStorageProvider/AndroidManifest.xml b/packages/ExternalStorageProvider/AndroidManifest.xml
index 1072f95..484dbcc 100644
--- a/packages/ExternalStorageProvider/AndroidManifest.xml
+++ b/packages/ExternalStorageProvider/AndroidManifest.xml
@@ -17,6 +17,10 @@
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
+ <!-- Stub that allows MediaProvider to make incoming calls -->
+ <path-permission
+ android:path="/media_internal"
+ android:permission="android.permission.WRITE_MEDIA_STORAGE" />
</provider>
<receiver android:name=".MountReceiver">
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 62207c5..4e52ff6d 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -37,6 +37,7 @@
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Path;
import android.provider.DocumentsContract.Root;
+import android.provider.MediaStore;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
@@ -606,11 +607,16 @@
}
break;
}
- case "getDocumentId": {
- final String path = arg;
- final List<UriPermission> accessUriPermissions =
- extras.getParcelableArrayList(AUTHORITY + ".extra.uriPermissions");
+ case MediaStore.GET_DOCUMENT_URI_CALL: {
+ // All callers must go through MediaProvider
+ getContext().enforceCallingPermission(
+ android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
+ final Uri fileUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
+ final List<UriPermission> accessUriPermissions = extras
+ .getParcelableArrayList(DocumentsContract.EXTRA_URI_PERMISSIONS);
+
+ final String path = fileUri.getPath();
try {
final Bundle out = new Bundle();
final Uri uri = getDocumentUri(path, accessUriPermissions);
@@ -619,7 +625,22 @@
} catch (FileNotFoundException e) {
throw new IllegalStateException("File in " + path + " is not found.", e);
}
+ }
+ case MediaStore.GET_MEDIA_URI_CALL: {
+ // All callers must go through MediaProvider
+ getContext().enforceCallingPermission(
+ android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
+ final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
+ final String docId = DocumentsContract.getDocumentId(documentUri);
+ try {
+ final Bundle out = new Bundle();
+ final Uri uri = Uri.fromFile(getFileForDocId(docId));
+ out.putParcelable(DocumentsContract.EXTRA_URI, uri);
+ return out;
+ } catch (FileNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
}
default:
Log.w(TAG, "unknown method passed to call(): " + method);