3

Not sure if what I'm trying to do is possible or not. I have a "proxy" document provider meaning a document provider that exports aliases for other content using the SAF. I have a dialog fragment that allows a user to set up the alias by presenting an OPEN_DOCUMENT_TREE intent, capturing the URI, granting persistable permissions on that URI, and then passing that URI to the document provider to present that content.

The dialog fragment is able to read/write on the URI it receives from the intent but the provider portion is not able to. The error I always get is:

java.lang.SecurityException: Permission Denial: reading com.android.externalstorage.ExternalStorageProvider uri 

I've read many related questions and corresponding replies that folks have posted on working with these persistable READ_URI_PERMISSIONS and have tried all variations suggested in those posts. But I am unable to get this to work. I'm beginning to think that the persistable URI permissions are not available within a provider. Is that the case? Do I need to have the provider invoke an intent so that I can grant these permissions? Perhaps have the provider call into the main activity to access the content. I'd certainly rather access the content directly from the provider. I haven't found anywhere that states once persistable permissions are granted, who are they granted to? Is a provider portion of an app a separate process and is that the issue?

Let me provide relevant code snippets:

The AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.connectedway.connectedsmb">

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <application
        ...
        <activity ...>
            ...
        </activity>
        <provider ...
           android:permission="android.permission.MANAGE_DOCUMENTS">
        </provider>
    </application>
</manifest>

The dialog fragment that obtains the URI is within the main activity and the code that uses the URI is within the provider.

The dialog fragment of the main activity invokes the OPEN_DOCUMENT_TREE intent using:

    StorageManager sm = (StorageManager)
        getContext().getSystemService(Context.STORAGE_SERVICE);
    StorageVolume sv = sm.getPrimaryStorageVolume();
    Intent intent = sv.createOpenDocumentTreeIntent();
    intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    onCreateResultLauncher.launch(intent);

The "onCreateResultLauncher" class with the aswsociatd onActivityResult callback is:

    ActivityResultLauncher<Intent> onCreateResultLauncher =
        registerForActivityResult
        (new ActivityResultContracts.StartActivityForResult(),
         new ActivityResultCallback<ActivityResult>() {
            @Override
                public void onActivityResult(ActivityResult result) {

                if (result.getResultCode() == Activity.RESULT_OK) {
                    Intent data = result.getData();
                    Uri uri = data.getData() ;
                    getActivity().grantUriPermission(
                        getActivity().getPackageName(), uri,                                                      
                        Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
                        Intent.FLAG_GRANT_READ_URI_PERMISSION);

                    final int takeFlags =
                        data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION);

                    getContext().getContentResolver().takePersistableUriPermission(
                        uri, takeFlags);

                    // PASS THE URI OFF TO THE PROVIDER
                    }
                }
            }
        });

Within the dialog fragment, I test the URI to see if I can read it:

    DocumentFile file = DocumentFile.fromTreeUri(getContext(), uri);
    if (file.canRead())
        System.out.println ("Can Read");

We always can read. Now the provider tries to access the URI as follows:

    @Override
        public Cursor queryDocument(String documentId, String[] projection)
        throws FileNotFoundException {

        // convert the documentId passed in to a uri.  The conversion process
        // results in the same URI as that received by the OPEN_DOCUMENT_TREE 
        // intent invoked and tested above.

        DocumentFile file = DocumentFile.fromTreeUri(getContext(), uri);

        if (file.canRead())
            System.out.println ("Can Read");

The provider does not have read access. So when the provider eventually does a query on the URI, it will fail with the SecurityException shown above.

The questions are:

  • Is this supported architecturally?
  • If so, can anyone see any obvious mistake in the above snippets?
  • If not, can anyone think of a way I can work around the architectural limitation?

Thank you for reading through the post.

Rich
  • 76
  • 1
  • 2
  • `// PASS THE URI OFF TO THE PROVIDER` I have no idea what you would do and why? – blackapps Apr 06 '22 at 15:55
  • `t.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);` Those flags make no sense. Please remove. – blackapps Apr 06 '22 at 16:15
  • `getActivity().grantUriPermission( getActivity().getPackageName(), uri, Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);` That statement makes no sense. Please remove. – blackapps Apr 06 '22 at 16:16
  • `public Cursor queryDocument(String` Show the code that causes this member to be called. Also tell why you would do the query and what you expect to get back of this query. – blackapps Apr 06 '22 at 16:18
  • The // PASS THE URI OFF TO THE PROVIDER is telling the provider that this URI is a new root for the provider. When a content resolver does a query on the provider's roots, this URI will be mapped to my provider's root id and passed back to the resolver. – Rich Apr 06 '22 at 16:44
  • The two comments you've made on flags are really just evolution of my code and the state that it's in right now. They are easy enough to remove. They are really just me shooting in the dark a little. And the code that calls the queryDocument is actually Android's Storage Access Framework. My provider is actually a sub-class of Android's DocumentProvider. queryDocument is a callback. – Rich Apr 06 '22 at 16:47
  • eg. ```public class OTGProvider extends DocumentsProvider { ``` – Rich Apr 06 '22 at 16:48
  • `And the code that calls the queryDocument is actually Android's Storage Access Framework. ` Can be. But when? Your code will call it i think. – blackapps Apr 06 '22 at 17:14
  • `The // PASS THE URI OFF TO THE PROVIDER is telling the provider that this URI is a new root for the provider. ....` Sorry to i can not follow you. And i'm missing code that tries to use the obtained uri. – blackapps Apr 06 '22 at 17:17
  • I export it as a provider in my manifest. It will get called by SAF when someone opens up documentsui and browses to one of the roots of my document provider. After selecting the root, my provider get's that callback. I'm not sure what context the provider gets called back in. And there lies my question. – Rich Apr 06 '22 at 17:20
  • "Sorry to i can not follow you. And i'm missing code that tries to use the obtained uri." Perhaps a better description of the use case will clear this up. The app is essentially a document provider. The app builds up a list of aliases used as roots by the provider. Separately, someone may run a document picker that selects one of the roots I added for my provider. The callback in the provider occurs when the alias is selected. That's what I meant by "proxy" provider. – Rich Apr 06 '22 at 17:31

1 Answers1

1

I've been able to figure this out. I had to change a few things but the main issue I had was with how I built the URI's that I passed around. Here's some of the most relevant change:

The app reads in a URI which wants to be exposed in a custom document provider that is part of the app. When launching the

    Intent intent = sv.createOpenDocumentTreeIntent();
    intent.addFlags
        (Intent.FLAG_GRANT_READ_URI_PERMISSION |
         Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
         Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
         Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);

and then launch the intent. After the user selects a directory to export to the custom document provider, the app will recieve the result callback:

    Uri uri = data.getData() ;
    ContentResolver cr = getContext().getContentResolver();
    cr.takePersistableUriPermission
        (uri,
         Intent.FLAG_GRANT_READ_URI_PERMISSION |
         Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

The URI is then passed to the document provider which creates a root for it. The document provider does not need to explicitly take any permissions. It just needs to make sure to build a correct URI. For example:

    Uri treeUri = Uri.parse(otgMap.getPath().getPath());
    String externalDocId = // DocId to query
    Uri externalUri = DocumentsContract.buildDocumentUriUsingTree
        (treeUri, externalDocId);

    ContentResolver cr = getContext().getContentResolver();

    Cursor cursor = cr.query(externalUri, projection,
                             null, null, null);

It all works as I had hoped. It just needed to smash my head on the desk a few times.

Rich
  • 76
  • 1
  • 2