Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 643e99ef authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

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
parent c72a391e
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -36791,6 +36791,7 @@ package android.provider {
    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";
+2 −0
Original line number Diff line number Diff line
@@ -731,6 +731,8 @@ public final class DocumentsContract {
    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)
+48 −68
Original line number Diff line number Diff line
@@ -46,9 +46,6 @@ import android.util.Log;

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 final class MediaStore {
     */
    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 @@ public final class MediaStore {
    }

    /**
     * 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) {

        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 {

        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 {
    /**
     * 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(
                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();
        }
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -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">
+25 −4
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import android.provider.DocumentsContract;
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 @@ public class ExternalStorageProvider extends FileSystemProvider {
                    }
                    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 @@ public class ExternalStorageProvider extends FileSystemProvider {
                    } 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);