I'm trying to adapt a File-based document system to something using DocumentFile in order to allow external storage read/write access on API >= 29.
I get the user to select the SD card root using Intent.ACTION_OPEN_DOCUMENT_TREE, and I get back a Uri as expected, which I can then handle using:
getContentResolver().takePersistableUriPermission(resultData.getData(),
Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
I can browse successfully through the external storage contents up to the selected root. All good.
But what I need to be able to do is write an arbitrary file in the chosen (sub)folder, and that's where I'm running into problems.
DocumentFile file = DocumentFile.fromSingleUri(mContext, Uri.parse(toPath));
Uri uri = file.getUri();
FileOutputStream output = mContext.getContentResolver().openOutputStream(uri);
Except on the openOutputStream() call I get:
java.io.FileNotFoundException: Failed to open for writing: java.io.FileNotFoundException: open failed: EISDIR (Is a directory)
That's slightly confusing to me, but the "file not found" part suggests I might need to create the blank output file first, so I try that, like:
DocumentFile file = DocumentFile.fromSingleUri(mContext, Uri.parse(toPath));
Uri uri = file.getUri();
if (file == null) {
return false;
}
if (file.exists()) {
file.delete();
}
DocumentFile.fromTreeUri(mContext, Uri.parse(getParentPath(toPath))).createFile("", uri.getLastPathSegment());
FileOutputStream output = mContext.getContentResolver().openOutputStream(uri);
I get a java.io.IOException:
java.lang.IllegalStateException: Failed to touch /mnt/media_rw/0B07-1910/Testing.tmp: java.io.IOException: Read-only file system
at android.os.Parcel.createException(Parcel.java:2079)
at android.os.Parcel.readException(Parcel.java:2039)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:188)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
at android.content.ContentProviderProxy.call(ContentProviderNative.java:658)
at android.content.ContentResolver.call(ContentResolver.java:2042)
at android.provider.DocumentsContract.createDocument(DocumentsContract.java:1327)
at androidx.documentfile.provider.TreeDocumentFile.createFile(TreeDocumentFile.java:53)
at androidx.documentfile.provider.TreeDocumentFile.createFile(TreeDocumentFile.java:45)
Which doesn't make sense to me, since the tree should be writeable.
For what it's worth, the Uri I get back from Intent.ACTION_OPEN_DOCUMENT_TREE looks like this:
content://com.android.externalstorage.documents/tree/0B07-1910%3A
Interestingly, when I use that Uri to create a DocumentFile object to browse, using documentFile = DocumentFile.fromTreeUri(context, uri), then documentFile.getURI().toString() looks like:
content://com.android.externalstorage.documents/tree/0B07-1910%3A/document/0B07-1910%3A
i.e., it's had something appended to the end of it.
Then, I descend into what should be a writeable folder (like "Download"), and try creating a writeable file as described above. The "Download" folder gets the Uri:
content://com.android.externalstorage.documents/tree/0B07-1910%3A/document/0B07-1910%3ADownload
and the Uri I'm using for toPath, above, is then:
content://com.android.externalstorage.documents/tree/0B07-1910%3A/document/0B07-1910%3ADownload/Testing.tmp
which leads to the problems described previously trying to create it.
I haven't actually found any decent information about writing an arbitrary file under Storage Access Framework restrictions.
What am I doing wrong? Thanks. :)
Uri uri = uri obtained from ACTION_OPEN_DOCUMENT_TREE
String folderName = "questions.59189631";
DocumentFile documentDir = DocumentFile.fromTreeUri(context, uri);
DocumentFile folder = documentDir.createDirectory(folderName);
return folder.getUri();
Use createFile() for a writable file.
Related
In my app, I did code for selecting a directory with persistence permission using android ACTION_OPEN_DOCUMENT_TREE. I did everything successfully, but the problem is I can not create a file inside the sub-directory and cannot get a list of files from the sub-directory. It gives me errors like Permission Denial: writing com.android.externalstorage.ExternalStorageProvider uri
but according to android official doc, it says Your app can then access any file in the selected directory and any of its sub-directories.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
activity.startActivityForResult(intent, PERMISSION_CODE);
in onActivityResult
if (requestCode == PERMISSION_CODE) {
if (resultData != null) {
Uri treeUri = resultData.getData();
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
if(DocumentFile.fromTreeUri(context, treeUri).findFile("backup") == null){
Uri backupDirUri = DocumentFile.fromTreeUri(context, treeUri)
.createDirectory("backup").getUri();
//this statement gives me error
DocumentFile.fromTreeUri(context, backupDirUri)
.createFile("text/plain", "34234234.txt")
.getUri();
}
}
}
Suppose an app user select a directory inside SDcard named with MyFolder then I have created a directory backup inside MyFolder but I can not create a file inside the backup directory using the backup directory URI.
For some reason using getUri() on a DocumentFile instance and then attempt to create another DocumentFile from that Uri fails.
Solution: don't attempt to operate on a Uri. Work with the DocumentFile instance.
The first call to fromTreeUri must be using an Uri or Uri from String. But from that point create new files by using the returned DocumentFile.
For example, you can try this:
if (requestCode == PERMISSION_CODE) {
if (resultData != null) {
Uri treeUri = resultData.getData();
final int takeFlags = resultData.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
DocumentFile root = DocumentFile.fromTreeUri(context, treeUri);
if (root.findFile("backup") == null) {
DocumentFile backupDirUri = root.createDirectory("backup");
// this statement should not give error now
DocumentFile f = backupDirUri.createFile("text/plain", "34234234.txt");
// Write to your just created file.
// openOutputStream with f.getUri() will work at this point.
// For this example, plain OutputStream to write bytes,
// wrap it with writers or something more functional.
OutputStream os = context.getContentResolver().openOutputStream(f.getUri());
os.write("It works!".getBytes(Charset.forName("US-ASCII"));
os.flush();
os.close();
}
}
}
It's not only that going back and forward from DocumetFile to Uri then to DocumentFile again is very inefficient if done in the same function call, but that it always fails.
Why it fails?
Well... sorry, I don't know. I just know that in my test devices it fails. I don't know why.
You can safely going from Uri to DocumentFile in future iterations, or with files that already exists in the file system (I mean, not just created during that same function call).
You cannot go from Uri to DocumentFile if you just created a file during that same function call. But you can safely operate on the returned DocumentFile of the just created file. That's why you must remember the returned DocumentFile, not its Uri, if you plan to immediately operate on that just created file.
If you just want to operate on the file at a later time, then you can save the Uri. And you probably want to remember it, because I'm not sure of what happens to a DocumentFile instance once the app is suspended then resumed. In that case, Uri may be safer. Also the string representation of a Uri is what you can save to preferences or any kind of data base/custom file.
But again, if you want to immediately do something with the created file, don't try to get its Uri and then create a DocumentFile. Just use the DocumentFile you already got.
To me, it looks like DocumentFile.createDirectory and DocumentFile.createFile returns before the device file system is updated. This is a guess. I still has to hear the true explanation.
That would explain why you can operate with the returned DocumentFile instance, of a just created child file/directory, but if you save the Uri and try to call any DocumentFile function that accepts an Uri, with the Uri of a just created file/directory, then it fails.
Another possibility is that you cannot create a second DocumentFile while another refers to the same uri. Remember that you may not have references to it, but it exists until garbage collection.
I'm trying to copy a document with the SAF framework in Android by using DocumentsContract.copyDocument(ContentResolver, Uri, Uri) however this doesn't work, android returns error "Failed to copy document".
By narrowing the issue down, the FLAG_SUPPORTS_COPY is off on that document (according to DocumentsContract.Document#COLUMN_FLAGS)
(COLUMN_FLAGS value is 326 in decimal). So this explains the error.
However moving the document is allowed (flag FLAG_SUPPORTS_MOVE is on) and file is really moved when calling DocumentsContract.moveDocument(ContentResolver, Uri, Uri, Uri)
Access to the document tree (both the root of the drive, and DCIM folder) have been granted through Intent.ACTION_OPEN_DOCUMENT_TREE
Why is the FLAG_SUPPORTS_COPY set to false for the Document ? Am I missing something ?
Note: I believe I fullfill the requirements from this post https://stackoverflow.com/a/58147682/15401262
Thank you
Code (java)
// docFilesToProcess if of type "DocumentFile[]" and contains "regular files, like images" (not directories).
// Create destination dir
Uri destUri = DocumentsContract.createDocument(this.getContentResolver(), docFilesToProcess[i].getParentFile().getUri(), DocumentsContract.Document.MIME_TYPE_DIR, "destDir");
Log.i("M", "destUri: "+ destUri.toString());
// Create document
Uri docToMove = DocumentsContract.createDocument(this.getContentResolver(), docFilesToProcess[i].getParentFile().getUri(), "text/plain", "text");
Log.i("M", "docToMove: "+ docToMove.toString());
// copy document
DocumentsContract.copyDocument(this.getContentResolver(), docToMove, destUri);
Output
I/M: destUri: content://com.android.externalstorage.documents/tree/primary%3ADCIM/document/primary%3ADCIM%2FdestDir
I/M: docToMove: content://com.android.externalstorage.documents/tree/primary%3ADCIM/document/primary%3ADCIM%2Ftext.txt
W/DocumentsContract: Failed to copy document
java.lang.UnsupportedOperationException: Copy not supported
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:172)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
at android.content.ContentProviderProxy.call(ContentProviderNative.java:658)
at android.content.ContentResolver.call(ContentResolver.java:2042)
at android.provider.DocumentsContract.copyDocument(DocumentsContract.java:1442)
at com.example.exifthumbnailadder.MainActivity.addThumbs(MainActivity.java:1036)
at java.lang.reflect.Method.invoke(Native Method)
Persistant permission request
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
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);
In general, you cannot ever rely on any SAF provider to implement any optional feature. You should check whether the feature is supported, and you should have a viable fallback plan for when it is not.
ContentResolver cr = getContentResolver();
if((flags & FLAG_SUPPORTS_MOVE) == FLAG_SUPPORTS_MOVE)
Uri newDoc = DocumentsContract.copyDocument(cr, docToCopy, destDir);
else {
Uri newDoc = DocumentsContract.createDocument(cr, destDir, mimeType, name);
manuallyCopyBytes(docToCopy, newDoc);
}
It's important to give the provider a chance to do it, because something like Google Drive may be able to perform the copy separately on the device and on the server, instead of copying it on the device and then having to upload the whole thing to the server again.
I am having a problem with selecting image file from external storage using file picker in Android. This question is the consequence of this question - No such file or diectory error in image file upload using Retrofit in Android. What my problem is opening and reading file from external storage on activity result. I want to convert result URI into File.
I read a pdf file from download folder on activity result
Uri bookUri = data.getData();
if(bookUri!=null)
{
String filePath = bookUri.toString();//bookUri.toString()
String mime = app.getMimeType(filePath);
if(mime!=null && !mime.isEmpty() && (mime.toLowerCase()=="application/pdf" || mime.toLowerCase()=="application/txt" || mime.toLowerCase()=="application/text"))
{
bookFile = new File(bookUri.getPath());
ivBookFile.setImageResource(R.drawable.book_selected);
}
else{
Toast.makeText(getBaseContext(),"Unable to process file you have chosen.",Toast.LENGTH_SHORT).show();
}
}
As you can see I used new File(bookUri.getPath()); to convert into File. The above code works well. It is working. The problem is now I am trying to open an image file in DCIM/Camera folder on activity result.
This is the code I used
Uri selectedImageUri = data.getData();
if(selectedImageUri!=null)
{
try{
bmpCoverImage = MediaStore.Images.Media.getBitmap(getContentResolver(), selectedImageUri);
imageFile = new File(selectedImageUri.getPath());
if(bmpCoverImage!=null)
{
ivCoverImage.setImageBitmap(bmpCoverImage);
}
}
catch (IOException e)
{
Toast.makeText(getBaseContext(),"An error occurred with the file selected",Toast.LENGTH_SHORT).show();
}
}
As you can see I used new File(selectedImageUri.getPath()); like I did in reading pdf file. This time the code is not working. When I do operation with the file like in previous question, it gives me error.
I used this way also
imageFile = new File(Environment.getExternalStorageDirectory(),selectedImageUri.getPath());
I got the same error. How can I open the image file correctly from external storage? How can I convert the chosen file URI from external storage into File?
I am having a problem with selecting image file from external storage using file picker in Android
If you are referring to the code that you are using in this question, you are not "using file picker". You are using ACTION_GET_CONTENT, which has never been a "file picker", nor will it ever be a "file picker".
I want to convert result URI into File.
Usually, that is not necessary. But, if that is what you want to do:
use ContentResolver and openInputStream() to get an InputStream on the content represented by the Uri
create a FileOutputStream on your desired file
use Java I/O to copy the bytes from the InputStream into the FileOutputStream
The above code works well. It is working.
It works for the small number of devices that you tested, for the specific activities that the user chose to handle the ACTION_GET_CONTENT request. It will not work on most Android devices, and it will not work in most circumstances. The only time that code will work is if the Uri has a file scheme. Most of the time, it will not. Instead, it will have a content scheme, representing content supplied by a ContentProvider.
Please how can I open the image file correctly from external storage?
If you wish to continue using ACTION_GET_CONTENT, please understand that this has nothing to do with external storage specifically. You are not getting a file, on external storage or elsewhere. You are getting a Uri. This is akin to a URL, such as the URL for this Web page. Just as a URL does not necessarily point to a file on your hard drive, a Uri does not necessarily point to a file on the filesystem. Use a ContentResolver and DocumentFile to work with the Uri and the content that it identifies.
If you want to always get files on external storage (and nowhere else), then use an actual file picker library.
I am trying to port my app to use the new api DocumentFile. I have implemented calling the new intent ACTION_OPEN_DOCUMENT_TREE. Then the code
Uri treeUri = resultData.getData();
DocumentFile folder = DocumentFile.fromTreeUri(context, treeUri);
folder.listFiles[0].delete(); // THIS IS NOT DELETING the correct directory.
How is it failing? I select a folder in the sdcard, e.g. /storage/sdcard1/temp. I have checked that folder.listFile[0] returns something like /storage/sdcard1/temp/test, BUT the folder that is deleted is /storage/sdcard1/temp when I would expect that it was /storage/sdcard1/temp/test.
What is going wrong?
Regards,
Francis.
I have a situation in Lollipop where I have:
Directory tree (DocumentFile) which is granted from user.
A collection of audio files that need to modify retrieved from media provider.
Uri mediaUri = Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
"45");
Then, how to write a stream into one of those files?
I think, this https://stackoverflow.com/a/30514269/615025 gives a work around. Copy bytes of the file to cache directory, do modification on the temporary file and then copy the bytes back to the original file.
DocumentFile is a new mechanism which allows us to modify files in Lollipop. It is not the same case with this post how to get contact photo URI
Thanks
You can open an outputStream directly from a Uri using ContentResolver.openOutputStream(Uri)
For example,
OutputStream fos;
fos = new BufferedOutputStream(context.getContentResolver().openOutputStream(uri));
Optionally, openOutputStream takes a string to specify mode.
i.e
openOutputStream(uri, "w");
to write or
openOutputStream(uri, "wa");
to append.