Trying to takePersistableUriPermission() fails for custom DocumentsProvider via ACTION_OPEN_DOCUMENT
Solution 1
EDIT:
My previous answer wasn't good. You are suppose to use "android.permission.MANAGE_DOCUMENTS" for security reasons.
Only System UI picker will be able to list your documents.
But you don't need this permission in the manifest of the application that opens documents.
Actually you should not to be able to gain this permission as it is system permission.
I've just tested it and call to takePersistableUriPermission form onActivityResult was successful.
I used DocumentProvider with mock data (one root, 3 txt documents).
If it still doesn't work for you there could be some issue with your document provider.
EDIT2:
Sample code
package com.example.test;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsProvider;
import java.io.FileNotFoundException;
public class MyContentProvider extends DocumentsProvider {
private final static String[] rootColumns = new String[]{
"_id", "root_id", "title", "icon"
};
private final static String[] docColumns = new String[]{
"_id", "document_id", "_display_name", "mime_type", "icon"
};
MatrixCursor matrixCursor;
MatrixCursor matrixRootCursor;
@Override
public boolean onCreate() {
matrixRootCursor = new MatrixCursor(rootColumns);
matrixRootCursor.addRow(new Object[]{1, 1, "TEST", R.mipmap.ic_launcher});
matrixCursor = new MatrixCursor(docColumns);
matrixCursor.addRow(new Object[]{1, 1, "a.txt", "text/plain", R.mipmap.ic_launcher});
matrixCursor.addRow(new Object[]{2, 2, "b.txt", "text/plain", R.mipmap.ic_launcher});
matrixCursor.addRow(new Object[]{3, 3, "c.txt", "text/plain", R.mipmap.ic_launcher});
return true;
}
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
return matrixRootCursor;
}
@Override
public Cursor queryDocument(String documentId, String[] projection)
throws FileNotFoundException {
return matrixCursor;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
String sortOrder)
throws FileNotFoundException {
return matrixCursor;
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode,
CancellationSignal signal)
throws FileNotFoundException {
int id;
try {
id = Integer.valueOf(documentId);
} catch (NumberFormatException e) {
throw new FileNotFoundException("Incorrect document ID " + documentId);
}
String filename = "/sdcard/";
switch (id) {
case 1:
filename += "a.txt";
break;
case 2:
filename += "b.txt";
break;
case 3:
filename += "c.txt";
break;
default:
throw new FileNotFoundException("Unknown document ID " + documentId);
}
return ParcelFileDescriptor.open(new File(filename),
ParcelFileDescriptor.MODE_READ_WRITE);
}
}
Note:
You can use constants from DocumentsContract.Document and DocumentsContract.Root.
I'm not sure whether "_id" is required.
EDIT3:
Updated sample code to open documents from /sdcard.
Added read/write external storage permissions.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.example.test"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
<provider
android:name="com.example.test.MyContentProvider"
android:authorities="com.example.test.document"
android:enabled="true"
android:exported="@bool/atLeastKitKat"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER"/>
</intent-filter>
</provider>
</application>
</manifest>
Client app
New project with an empty activity, no permission added.
Open document
Intent openDocumentIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
openDocumentIntent.addCategory(Intent.CATEGORY_OPENABLE);
openDocumentIntent.setType("text/plain");
openDocumentIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
startActivityForResult(openDocumentIntent, 1);
onActivityResult
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case 1: // TODO: Use constant
if (resultCode == RESULT_OK) {
if (data == null) return; // TODO: Show error
Uri uri = data.getData();
if (uri == null) return; // TODO: Show error
getContentResolver().takePersistableUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION);
InputStream is = null;
try {
is = getContentResolver().openInputStream(uri);
// Just for quick sample (I know what I will read)
byte[] buffer = new byte[1024];
int read = is.read(buffer);
String text = new String(buffer, 0, read);
((TextView) findViewById(R.id.text)).setText(text);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
break;
}
}
Solution 2
When working with SAF, the expected behavior on API 19-25 is that a SecurityException
is thrown for URIs from your own DocumentProvider
.
This has changed on API 26 and above which now allows persistable URI permission for URIs even from your own process (no official docs but an observation through testing)
But even if you get a SecurityException
while trying to take persistable URI permission you'd still always have access to URIs exposed from your own DocumentsProvider
.
Thus it'd be a good idea to catch and ignore the SecurityException
when the content authority is from your own process.
Note: If your app contains a DocumentsProvider and also persists URIs returned from ACTION_OPEN_DOCUMENT, ACTION_OPEN_DOCUMENT_TREE, or ACTION_CREATE_DOCUMENT, be aware that you won’t be able to persist access to your own URIs via takePersistableUriPermission() — despite it failing with a SecurityException, you’ll always have access to URIs from your own app. You can add the boolean EXTRA_EXCLUDE_SELF to your Intents if you want to hide your own DocumentsProvider(s) on API 23+ devices for any of these actions.
Here's a note from official Android Developers blog that confirms this behavior - https://medium.com/androiddevelopers/building-a-documentsprovider-f7f2fb38e86a
cgogolin
Updated on July 02, 2022Comments
-
cgogolin almost 2 years
I am trying to write a custom
DocumentsProvider
that allows other apps to take persistable permissions to the Uris it providesI have a
DocumentsProvider
that I declare in myAndroidManufest.xml
as follows<provider android:name="com.cgogolin.myapp.MyContentProvider" android:authorities="com.cgogolin.myapp.MyContentProvider" android:grantUriPermissions="true" android:exported="true" android:permission="android.permission.MANAGE_DOCUMENTS" android:enabled="@bool/atLeastKitKat"> <intent-filter> <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> </intent-filter> </provider>
and my app has the
MANAGE_DOCUMENTS
permission set<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
(apparently this is not necessary but adding/removing it also doesn't matter). I can then see my provider when I open the
ACTION_OPEN_DOCUMENT
picker UI withIntent openDocumentIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); openDocumentIntent.addCategory(Intent.CATEGORY_OPENABLE); openDocumentIntent.setType("application/pdf"); openDocumentIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION|Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); startActivityForResult(openDocumentIntent, EDIT_REQUEST);
and, after picking a file from my provider there, in the
onActivityResult()
method of my App I can then successfully open the file provided by myDocumentsProvider
via theUri
I get fromintent.getData()
.However, trying to persist read or write permissions with
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
or
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
always fails with an exception like
No permission grant found for UID 10210 and Uri content://com.cgogolin.myapp.MyContentProvider/document/tshjhczf.pdf
If I pick a file from the google drive or downloads provider in the picker UI taking permissions in this way works. So I think the problem is in my provider.
Why is there no permission grant created despite me specifying
android:grantUriPermissions="true"
?How can I convince Android to create such a permission grant for me?
After all I don't think I can do it myself, as I cannot know the
UID
of the process that opened the picker UI, or at least not that I knew how. -
cgogolin over 8 yearsI don't see how that could improve the situation. I specifically want that only the System UI picker launched via
ACTION_OPEN_DOCUMENT
is able to access my provider. Removing or changing the name of the permission as suggested doesn't solve the described problem. The error stays the same. -
Milos Fec over 8 yearsIsn't it possible that you use your app for the intent instead of System UI picker? I think System picker should return uri "content://com.android.providers.media.documents/..." but you are opening content provider of your app, not going through system provider.
-
Milos Fec over 8 yearsI admit I didn't test it, but I'm curious so I'm going to test it right now.
-
cgogolin over 8 years"Isn't it possible that you use your app for the intent instead of System UI picker?" Yes, that would be possible, but I primarily want the provider to be accessible via the System UI picker. I am using my own app mostly just to test the provider here.
-
Milos Fec over 8 yearsAs I understand system permission "android.permission.MANAGE_DOCUMENTS" is for the UI picker. So document provider using this permission allow only UI picker to take persistent permission. Then your app get document from UI picker and UI picker takes persistent permission for the document. When you want to reopen document after reboot, you will open UI picker content provider that opens document from your document provider. Can you check logcat after installing your app? Look for "non granting permission" text.
-
cgogolin over 8 years"So document provider using this permission allow only UI picker to take persistent permission." No. Google drive also has it and I can take persistable permissions for documents provided by it. "Then your app get document from UI picker and UI picker takes persistent permission for the document." No. The picker is supposed to create a persistable permission for my app as I specify
android:grantUriPermissions="true"
. "When you want to reopen document after reboot, you will open UI picker content provider that opens document from your document provider." No. I should be able to persist it. -
cgogolin over 8 yearsYes, I also suspect that there is a problem in my provider. Would you mind sharing your code so that I can compare and maybe find my mistake?
-
cgogolin over 8 yearsWow, thanks for your work! Structure wise this looks exactly like what I do . I don't have the "_id" field as it was not required by the docs and google drive doesn't use it. I tried add in but no change. Can you take persistable permissions after picking a document from your provider via the picker UI? I am surprised that this works even though you return
null
fromopenDocument()
. Would you mind to also post yourAndroidManifest.xml
(I suspect my problem lies there) and the code with which you take the persistable permissions? -
cgogolin over 8 yearsWith your provider I get exactly the same error when I try to take the persistable permission:
I/Pen&PDF ( 2298): Failed to take persistable read uri permissions for /document/1 Exception: java.lang.SecurityException: No permission grant found for UID 10210 and Uri content://com.cgogolin.myapp.MyContentProvider/document/1 I/Pen&PDF ( 2298): Failed to take persistable write uri permissions for /document/1 Exception: java.lang.SecurityException: No permission grant found for UID 10210 and Uri content://com.cgogolin.myapp.MyContentProvider/document/1
-
Milos Fec over 8 yearsI didn't use DocumentsProvider before, so it was interesting for me. I've added manifest (it's just like yours) and code of client app. Please note that content provider use files from /sdcard and client app doesn't need any permission. I've tested also reboot, it works.