2

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. :)

Phantômaxx
  • 37,901
  • 21
  • 84
  • 115
KT_
  • 978
  • 11
  • 26
  • You cannot make a file uri from a tree uri by appending a filename. Use createFile() or createDocument() on the DocumentFile instance of the tree. (however its called). – blackapps Dec 05 '19 at 07:36
  • Thanks, yeah, that's what the `DocumentFile.fromTreeUri(mContext, Uri.parse(getParentPath(toPath))).createFile("", uri.getLastPathSegment())` does. It separates the tree Uri from the filename and calls `createFile()`. (They're initially combined like that due to the generic code that calls it. The generic code deals with standard paths for any sort of backing storage.) – KT_ Dec 05 '19 at 07:54
  • (To clarify, `getParentPath()` is my own method that gets everything but the parent path of a standard-format path.) – KT_ Dec 05 '19 at 08:04
  • getParentPath() is your function? Throw it away. It will bite you soon. There are no standard formatted paths. Every provider is different. – blackapps Dec 05 '19 at 08:05
  • Thank you. That may turn out to be the case. In the meantime, however, it does seem to be producing the correct parent Uri, meaning presumably that in this case there is something else amiss. – KT_ Dec 05 '19 at 09:12
  • If you choose primary partition or micro sd card then it will work. They have the same provider where document id is the classic file path. But if the SAF gui presents a Download (next to Recent and primary and sd card) then there is already a different provider. – blackapps Dec 05 '19 at 10:58
  • Thank you again, but the problem is that it doesn't work when choosing anything from `Intent.ACTION_OPEN_DOCUMENT_TREE`. So my presumption is that I'm doing something wrong. (And the use of "Download" in this case is from selecting it in my own browser, which will allow me to browse into subfolders using this method. The root of the SD card was still originally selected for access.) – KT_ Dec 05 '19 at 14:06

1 Answers1

1
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.

blackapps
  • 8,011
  • 2
  • 11
  • 25
  • Thank you for your response. However, I don't need to create a subfolder: that already exists, or the user can create it otherwise. They've already been prompted to grant access to the external storage root (i.e. `uri` in your example). `createDirectory()` is therefore an unnecessary step, since they're choosing an existing directory (for which I'm getting the Uri). Other than that, as you can see, I'm trying to `createFile()`, and that's where it's failing, with `java.lang.IllegalStateException: Failed to touch /mnt/media_rw/0B07-1910/Testing.tmp: java.io.IOException: Read-only file system`. – KT_ Dec 05 '19 at 16:27
  • It was just code i had at hand. And again: It does not matter if createDirectory or createFile is used. And again: de directory was choosen with ACTION_OPEN_DOCUMENT_TREE. – blackapps Dec 05 '19 at 16:58
  • If that is not your scenario then explain what is different. – blackapps Dec 05 '19 at 16:59
  • Facing the same problem. once uri is received in onActivityResult, still the path is not writeable to the app, to save any file !! – Meher Aug 18 '21 at 16:41