So I have an android app which opens and displays PDF's, I have the user select pdfs like this
fun openFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/pdf"))
putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Files.getContentUri("external"))
}
activity?.startActivityForResult(intent, REQUEST_CODE_PDF_FILE)
}
And then I retrieve the URI from the activity result display it and save the URI. However the next time the app is opened I want to be open that same file, right now when I try opening the saved URI I get the following:
ava.lang.SecurityException: Permission Denial: opening provider com.android.providers.media.MediaDocumentsProvider from ProcessRecord{c3dfcb2 32587:ca.thing.testapp/u0a237} (pid=32587, uid=10237) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
at android.os.Parcel.createExceptionOrNull(Parcel.java:2425)
at android.os.Parcel.createException(Parcel.java:2409)
at android.os.Parcel.readException(Parcel.java:2392)
at android.os.Parcel.readException(Parcel.java:2334)
at android.app.IActivityManager$Stub$Proxy.getContentProvider(IActivityManager.java:5850)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:6973)
So clearly after closing and reopening the app I no longer have permission to use that selected file. So I imagine what I need to do is make a copy of that file into some cache dir that I do have permissions in so that I can display it when the app is reopened. How would I go about doing that?
You should take persistable uri permission in onActivityResult in order to use the uri later.
Making a copy is not needed.
Related
In my application a user can open the gallery and select their images by using androidX method:
registeredIntent = registerForActivityResult(new ActivityResultContracts.GetMultipleContents(), this);
And the callback for the result is sent in this method:
#Override
public void onActivityResult(List<Uri> result) {
if(result != null && result.size() > 0) {
sliderImageAdapter.addItems(result);
binding.includeResultTask.imageSlider.setSliderAdapter(sliderImageAdapter, true);
}
}
So far this works pretty well, the images are shown in my gallery component by adding the Uris in my adapter, the problem is that I save those in my database and when I restart the application the permissions are revoked and my application crash.
I tried by adding this in my callback for each item:
requireActivity().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
But I get this error:
Permission Denial: opening provider com.android.providers.media.MediaDocumentsProvider from ProcessRecord{41a7dce 26094:it.ilogreco.levelup/u0a482} (pid=26094, uid=10482) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
I have even tried to use this for each item:
context.grantUriPermission(context.getPackageName(), uri, FLAG_GRANT_READ_URI_PERMISSION);
but I get this error:
UID 10482 does not have permission to content://com.android.providers.media.documents/document/image%3A108953 [user 0]; you could obtain access using ACTION_OPEN_DOCUMENT or related APIs
I load the Uris one time and save them to the database with the registerForActivityResult, I can't open the media gallery again with the API because it doesn't make sense, any idea how to get those permissions? I would like to avoid copying the content in the callback.
Switch from ActivityResultContracts.GetMultipleContents to ActivityResultContracts.OpenMultipleDocuments. Then, your takePersistableUriPermission() calls should work. You can only get persistable Uri permissions on documents obtained via the Storage Access Framework, which is what OpenMultipleDocuments uses.
In my Android Manifest I registered my Activity for being able to "open" specific files.
This all works fine. When I receive the new Intent and read the file data from the intent via
BufferedReader(InputStreamReader(contentResolver.openInputStream(intent.data)))
it works fine.
However, for a better view flow, I wanted to store the URI of the intent and show it in another view (asking the user how to proceed with the file). So I store this intent.data as a String and open another view first.
However, once the view is opened, I bascially call the same thing
val br = BufferedReader(InputStreamReader(act.contentResolver.openInputStream(fileUri)))
but here I get an exception
java.lang.SecurityException: Permission Denial: reading [FileBrowserApp I used for "opening the file"] uri content://... from pid=5242, uid=10159 requires the provider be exported, or grantUriPermission()
So it feels like the URI is somehow expired or such thing? Is this actually the case? Do I have to read the file directly when I receive the Intent? I was hoping for a way to keep the URI until I want to read the file.
I found the issue. Turns out, it is not a problem of an expiring Intent or anything like that. Instead my own "processing" changed the path.
What I did was taking the incoming Intent data as Uri. Later I fetched the provider path from that Uri again. However this caused a transformation of the query (instead of leaving it as a String in the first place) breaking the path.
In short: the problem is the URL encoding/decoding.
The original intent path (opened via the TotalComander - hence the com.ghisler path) looked like this:
content://com.ghisler.files/tree/primary%3A/document/primary%3Astorage%2Femulated%2F0%2Fbackup.bak
However getting the path from the Uri the colons were decoded leaving an output path of this:
content://com.ghisler.files/tree/primary:/document/primary:storage/emulated/0/backup.bak
You can clearly see the different encoding. As a consequence the file path was simply not the same after fetching it from the Uri again. This was causing the Exception - not an invalidation of the intent.
I'm making an app where you are able to select images or documents from storage and they are then displayed in a list kinda like Google Drive so you can choose one and it opens and you can view it. This list is implemented using a RecyclerView, and I store the uri and file type in a json in SharedPreferences. I get the content using:
val getFile = registerForActivityResult(
ActivityResultContracts.GetContent()
) {
if (it != null) {
val fileLink = FileLink()
fileLink.setUri(it.toString())
fileLink.setType(contentResolver.getType(it)!!)
fileLink.setName(getFileName(it)!!)
list.add(fileLink)
presenter.saveChangesToList(list)
adapter.notifyDataSetChanged()
}
}
And then I just call:
getFile.launch("image/*|application/pdf")
This didn't let me pick stuff from the internal storage (the files appeared grayed out) and it didn't actually filter the selectable files. It worked perfectly with images taken with the camera though.
To fix the "not being able to pick stuff from storage" I changed GetContent() to OpenDocument():
val getFile = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) {
if (it != null) {
val fileLink = FileLink()
fileLink.setUri(it.toString())
fileLink.setType(contentResolver.getType(it)!!)
fileLink.setName(getFileName(it)!!)
list.add(fileLink)
presenter.saveChangesToList(list)
adapter.notifyDataSetChanged()
}
}
And then I call:
getFile.launch(
arrayOf(
"application/pdf",
"image/*"
)
)
With this, I am able to select files from storage and it also lets me restrict so you can only pick either images or pdfs. My problem now is that, if I get stuff like this, I can open the document once. If I close my app and open it again and try to open the document again, I get this error:
java.lang.SecurityException: UID 10156 does not have permission to content://com.android.providers.downloads.documents/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2FNewPdf
I open the documents using this code:
viewHolder.itemView.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(
Uri.parse(fileList[position].getUri().toString()),
fileList[position].getType()
)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent)
}
Could I get some help with this? I'm guessing it has to do with the permission to read uris, but I have no idea how to fix it. I also don't understand why I can open the files correctly the first time, but not after I restart the app.
You did not call takePersistableUriPermission() on a ContentResolver as soon as you were handed the Uri to the selected content.
As a result, your rights to access the content will expire — your rights to access the content will be limited to the one activity instance that received the Uri, not other activities or future app processes.
Use case: download file from my app open it with appropriate editing application and after user is done with editing upload the file back to my app.
Example: I download the docx file from my app to public folder (eg. 'Documents') and after getting uri with FileProvider I send ACTION_VIEW intent (giving write permissions). Then open it with word app and edit it. But then the problem arives - word app says I need to save the file as a new copy and doesn't let me override the original file. Strange thing is if I open that downloaded file from file browser everything works fine and I can override the file. But when I use Intent from my app word application decides to create a copy inside their private directory. Why are they doing it this way? Is there any possibility to either get the newly saved file back to my app (from startActivityForResult) or somehow make them override the original file in the public folder?
Edit: As blackapps sugested I tried to check intents with 'Intent-Interceptor'. Here are the results:
My application:
intent://com.android.externalstorage.documents/tree/primary%3ADocuments/document/primary%3ADocuments%2FDocument.docx#Intent;scheme=content;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document;launchFlags=0x13000000;end
------------
ACTION: android.intent.action.VIEW
DATA: content://com.android.externalstorage.documents/tree/primary%3ADocuments/document/primary%3ADocuments%2FDocument.docx
MIME: application/vnd.openxmlformats-officedocument.wordprocessingml.document
URI: intent://com.android.externalstorage.documents/tree/primary%3ADocuments/document/primary%3ADocuments%2FDocument.docx#Intent;scheme=content;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document;launchFlags=0x13000000;end
FLAGS:
FLAG_RECEIVER_FOREGROUND
FLAG_ACTIVITY_FORWARD_RESULT
FLAG_ACTIVITY_PREVIOUS_IS_TOP
------------
MATCHING ACTIVITIES:
Word (com.microsoft.office.word - com.microsoft.office.word.WordActivity)
Default file browser (Google 'Files'):
intent://com.google.android.apps.nbu.files.provider/1/file%3A%2F%2F%2Fstorage%2Femulated%2F0%2FDocuments%2FDocument.docx#Intent;scheme=content;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document;launchFlags=0x3000000;end
------------
ACTION: android.intent.action.VIEW
DATA: content://com.google.android.apps.nbu.files.provider/1/file%3A%2F%2F%2Fstorage%2Femulated%2F0%2FDocuments%2FDocument.docx
MIME: application/vnd.openxmlformats-officedocument.wordprocessingml.document
URI: intent://com.google.android.apps.nbu.files.provider/1/file%3A%2F%2F%2Fstorage%2Femulated%2F0%2FDocuments%2FDocument.docx#Intent;scheme=content;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document;launchFlags=0x3000000;end
FLAGS:
FLAG_ACTIVITY_FORWARD_RESULT
FLAG_ACTIVITY_PREVIOUS_IS_TOP
------------
MATCHING ACTIVITIES:
Word (com.microsoft.office.word - com.microsoft.office.word.WordActivity)
Here's how I obtain directory in which I save files (I use ActivityResultContracts):
directoryResultLauncher = registerForActivityResult(OpenDocumentTree()) { uri ->
if (uri == null) return#registerForActivityResult
requireContext().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
authentication.publicDirectory = uri.toString()
}
Download method:
val directoryUriString = authentication.publicDirectory
val directory = DocumentFile.fromTreeUri(context, directoryUriString.toUri())
val file = directory!!.createFile(mimeType, title)
val fileResult = getFileContent(context.contentResolver.openOutputStream(file!!.uri)!!)
if (!fileResult.isError()) {
val intent = Intent(Intent.ACTION_VIEW, file.uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
startActivity(intent)
}
Below, I will try to suggest you possible causes and solutions for your encountered issue:
First, maybe the difference at the URI source level!
It looks like your app use a Uri like this one:
content://com.android.externalstorage.documents/tree/...
which is a TreeDocument uri from a DocumentFile
While the Explorer app uses a FileProvider Uri from a Io.File.
Second, maybe you have not submitted the grand_write_access flag
When you send the Uri to the word app, ensure you to add the flag:
Intent.FLAG_GRANT_WRITE_URI_PERMISSION that will allow the word app to get the write access on the resource.
Maybe the word app behaves differently depending on whether or not it has the write access permission on the submitted file.
Hope this enlightened you a bit!
I have image stored in the internal storage of the app. I can grab the path and I can succesfully set it to image view. But when I am trying to let the user open it using the gallery (intent), it displays black screen.
myIntent.setDataAndType(Uri.fromFile(file), mimetype);
intent = Intent.createChooser(myIntent, "Choose a viewer");
startActivity(intent);
I am pretty sure it has to do with permission that gallery cant access private storage of my app for some reason. But is there way to do that "beside moving the file to external storage"
Thanks
Use FileProvider to serve the file from internal storage. Quoting the documentation:
FileProvider is a special subclass of ContentProvider that facilitates secure sharing of files associated with an app by creating a content:// Uri for a file instead of a file:/// Uri.
A content URI allows you to grant read and write access using temporary access permissions. When you create an Intent containing a content URI, in order to send the content URI to a client app, you can also call Intent.setFlags() to add permissions. These permissions are available to the client app for as long as the stack for a receiving Activity is active.
Here is an easy 'single file based solution'
When ever you add a file, let Media Store Content Provider knows about it using
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
Uri.fromFile(imageAdded)));
i have done some thing like this to show images from gallery.
private void pickFromGallery() {
Crop.pickImage(this);
}
<--->
crop is an android class.
this is a function in crop class.
public static void pickImage(Activity activity) {
Intent intent = (new Intent("android.intent.action.GET_CONTENT")).setType("image/*");
try {
activity.startActivityForResult(intent, 9162);
} catch (ActivityNotFoundException var3) {
Toast.makeText(activity, string.crop__pick_error, 0).show();
}
}