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.
Related
I meet some problems when I'm trying to share some files among apps. In app A, one can choose to open an existing file via openDocument():
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) {
// In short, preserve returned uri
uri = it
}
...
onClick = {
launcher.launch(arrayOf("text/plain"))
},
Then, app A can share the uri to other apps by:
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, uri)
type = "text/plain"
}
context.startActivity(Intent.createChooser(shareIntent, "Share test"))
Since Uri returned from SAF just starts with "content://", FileProvider is not used here.
Then something weired happened. For some apps, it works just fine; however for others, I always receive a message saying the file does not exist, or it cannot be shared properly. For example, I get an uri like: content://com.android.providers.downloads.documents/document/442
Let's say its correct file name is some.txt, But in app B which accepts the shared file, the file name is mistaken as 442 with no extension name.
I wonder what causes this problem. Is it my code's, SAF's, or those other apps' fault? And how can I correct it?
My SDK version is 32, and I'm using jetpack compose for app A.
however for others, I always receive a message saying the file does not exist, or it cannot be shared properly
You are not doing anything to grant permission to other apps to be able to read the content identified by that Uri. The best approach is to use ShareCompat.IntentBuilder to create your Intent. Alternatively, use addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION). See this blog post for more.
Is it my code's, SAF's, or those other apps' fault?
In terms of "a message saying the file does not exist", that is because of the aforementioned permission problem.
In terms of "the file name is mistaken as 442 with no extension name", if that does not clear up once you fix the permission problem, the bug is in the other app.
In my APP I received the Uri of a pdf file from any other app that can share pdfs.
Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
Anyway I cannot full-use this Uri, I image because of it is from a FileProvider external to my app.
For example when I try to create a FileDescriptor:
ParcelFileDescriptor fd = context.getContentResolver().openFileDescriptor(uri, "r");
I get this error:
W/System.err: java.lang.SecurityException: Permission Denial: opening provider androidx.core.content.FileProvider from ProcessRecord{...} (pid=23606, uid=10525) that is not exported from UID 10123
It is really necessary get the file path, copy the file, and get a new Uri?
Or I can simply manage the original Uri in some way ?
As per my knowledge the best thing to do is to read/copy the data from content provider immediately and then use that data to do whatever you want. so don't waste your time trying to manage the original one. I hope its helpful to you
I try to write an Android Instrumentation test where a user has to select a folder to save some files to.
But I don't know how to create the result required for intending.
The following code is what I came up with until yet.
val testUri = Uri.parse("content://com.android.providers.downloads.documents/tree/downloads")
val openDocumentTreeResult =
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE, testUri)
openDocumentTreeResult.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION and Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT_TREE))
.respondWith(
Instrumentation.ActivityResult(Activity.RESULT_OK, openDocumentTreeResult
)
)
Of course the use of Uri.parse to create a content Uri is a bit naive. In the logs I have the following warning (probably caused by DocumentFile.fromTreeUri() using the above naive Uri):
W/DocumentFile: Failed query: java.lang.SecurityException:
Permission Denial: opening provider com.android.providers.downloads.DownloadStorageProvider from ProcessRecord{edb1c64 4157:xyz.com.abc/u0a87} (pid=4157, uid=10087)
requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
and later on DocumentFile.createFile() fails by giving me a null. The corresponding code:
val treeUri = intent.data
val pickedDir = DocumentFile.fromTreeUri(this, treeUri)
val target = pickedDir.createFile(mimetype, f.name)
When I manually test this scenario first, the test runs then without error. I guess because then my app already has the permission for the downloads directory.
How do I create a result intent for intending which includes the necessary permission.
If you are going to return a Uri that you want DocumentFile to be able to use, the Uri has to work. It will need to point to a ContentProvider that:
Exists
Complies with the DocumentsContract API that DocumentFile will be calling (or at least the parts of it that you need for your test)
Returns what you want for your tests
In terms of permissions... ideally, your test provider would not require any, just to simplify matters. If that is impractical, you can try addFlags() on your Intent to grant what you need (e.g., FLAG_GRANT_READ_URI_PERMISSION), though I do not know how well that works in your Espresso test scenario.
After about a week of pulling my hair out, I'm finally done and ready to ask for some help.
Basically in my app I use the Intent below to create a new PDF, which is done via Storage Access Framework.
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/pdf"
intent.putExtra(Intent.EXTRA_TITLE, title)
startActivityForResult(intent, 1234)
After that I get the Uri on the onActivityResult() method, like so:
uri = dataIntent.data
if (uri != null) {
val takeFlags = data.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.takePersistableUriPermission(uri, takeFlags)
generatePdf(uri)
}
PDF generation is ok, the problem comes when I need to call ACTION_VIEW for the user to see the generated file or to share the file using ACTION_SEND.
Example of ACTION_VIEW usage (Yes, I'm using both Kotlin and Java):
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(uri, mimeType);
startActivity(intent);
I can't for the life of me figure out how to get an Uri that another app can use.
What I tried so far:
This answer, but the following exception is thrown: java.lang.IllegalArgumentException: column '_data' does not exist. Available columns: [_display_name, _size]
DocumentFile, using DocumentFile.fromFile(file), which turns the Uri from content://com.myapp.provider/root/document/primary:folder-created-by-the-user/generated-pdf.pdf to file:///root/document/primary:folder-created-by-the-user/generated-pdf.pdf, and still no app can open it
Many many other things that I can't even remember anymore
If someone could shed some light on this issue would be truly appreciated.
In principle use the same uri as obtained at creating the file. But ...you cannot grant a read uri permission on that uri. You got it. But you cannot forward such a permission to a viewer of your document.
Instead you should implement a ContentProvider. Then you can serve the content of your file.
Like blackapps said in his response, what I had to do was implement a ContentProvider, more specifically a DocumentProvider.
Following this link and this link is what finally did the trick. I implemented a CustomDocumentProvider that exposes a folder inside my app's private files (context.getFilesDir().getAbsolutePath() + "/folderToExpose"), after that all files created in this folder were exposed to other apps and I could use ACTION_VIEW and ACTION_SEND normally.
If someone happens to come across this issue, just make sure that the folder you want to expose doesn't contain any files that are crucial to your app, like database files, since users will have full access to all of its contents. And if it is a new folder, make sure to create it by calling mkdirs().
what is the correct way how I should form the intent to show content from my app in 3rd party viewers? I need to show images in gallery (or any other image viewer), pdfs in some pdf reader,..
Data gets server through a content provider which implements the openFile() method and returns a output pipe..
ParcelFileDescriptor[] pipe=ParcelFileDescriptor.createPipe();
...
ParcelFileDescriptor.AutoCloseOutputStream stream = new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]);
PipeThread pipeThread = new PipeThread(fileContents, stream);
pipeThread.start();
return pipe[0];
For images I use this:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_STREAM, uri);
I'm creating then a chooser for this intent as usual that's not the issue..
My problem is that although I see for example the Photos app in the chooser, I just cannot open the file in it..It just only opens the gallery with my images.
It's working when I use the action send, apps like gmail, drive, dropbox,..all of them are able to correctly read the image from the provider.
Also Skitch seems to be the only one app which I have tested it on that is able to open the image also using the Intent.ACTION_VIEW action..
Please don't tell me I should just send the URI, I really need to provide the file as a stream, or somehow as a serie of bytes (IPC limitations would be probably against this). I can't save the file to a public directory.
So the issue was that have been setting Intent type and data in two separate method calls..
What I didn't know is that Intent.setType() clears its data and Intent.setData() clears its type..
When I set both data and type through the Intent.setDataAndType() method call, it works even for URI pointing to a stream.
Unfortunately the final implementation is still not working flawlessly everywhere.
It works in default android gallery app, in G+ Photos app, in QuickPic, in Sony gallery app, but it does not work in default HTC gallery neither in default Samsung gallery.
Its just a pity, that its actually not that much dependent on my implementation as on how is it implemented in the 3rd party viewer app.