Search code examples
javaandroidfile-descriptorstorage-access-framework

ContentResolver.openOutputStream() throws strage FileNotFoundException


My purpose is to be able to edit files on removable external storage on Android 21+. To do so, it must be used the Storage Access Framework.

First I granted the read and write permissions to the folder I'm interested in with:

startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), 123);

@Override
public void onActivityResult(int requestCode,int resultCode,Intent resultData) {
    if (resultCode != RESULT_OK)
        return;
    Uri treeUri = resultData.getData();

    PreferenceUtils.setPersistedUri(this, treeUri);

    grantUriPermission(getPackageName(), treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}

I'm sure this is done correctly because I double check it:

for(UriPermission uri : getContentResolver().getPersistedUriPermissions())
            if(uri.isReadPermission() && uri.isWritePermission())
                // This prints the correct folder

If relevant, I also declared in manifest and requested runtime permissions for READ_EXTERNAL_STORAGEand WRITE_EXTERNAL_STORAGE.

DocumentFile parentDocumentFile = DocumentFile.fromTreeUri(getActivity(), PreferenceUtils.getPersistedUri());
if(parentDocumentFile == null) return;
for(DocumentFile file : parentDocumentFile.listFiles()) {
    if(file.canRead() && file.canWrite() && file.exists()) {
        try 
        {
             OutputStream outStream = getActivity().getContentResolver().openOutputStream(file.getUri());

             // I/O operations
        } (catch FileNotFoundException e) {}
    }        
}

Later in the code, I get a reference to the file I'm interested in with a content URI wrapped by DocumentFile. I want to underline that DocumentFile.exists(), DocumentFile.canRead(), DocumentFile.canWrite() return positive result, also a check with system File.exists() has been done. For instance, the URI I get with DocumentFile.getUri() looks like:

content://com.android.externalstorage.documents/tree/486E-2542%3A/document/486E-2542%3ADownload%2FFildoDownloads%2FLed%20Zeppelin%2FLed%20Zeppelin%20-%20Kashmir.mp3

Alright here. But if I wanted to open it for writing, I can't and this is the only available method to write a file which permissions are granted through the Storage Access Framework:

OutputStream outStream = getContentResolver().openOutputStream(DocumentFile.getUri());

Some devices (and I cannot reproduce) throw a FileNotFoundExcpetion with the following stacktrace:

java.io.FileNotFoundException: Failed opening content provider: content://com.android.externalstorage.documents/tree/486E-2542%3A/document/486E-2542%3ADownload%2FFildoDownloads%2FLed%20Zeppelin%2FLed%20Zeppelin%20-%20Kashmir.mp3
    at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1032)
    at android.content.ContentResolver.openOutputStream(ContentResolver.java:718)
    at android.content.ContentResolver.openOutputStream(ContentResolver.java:694)
    at helpers.c.b(SourceFile:405)

Some other with this one:

java.io.FileNotFoundException: Failed to open for writing: java.io.FileNotFoundException: Read-only file system
    at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:144)
    at android.content.ContentProviderProxy.openAssetFile(ContentProviderNative.java:621)
    at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1011)
    at android.content.ContentResolver.openOutputStream(ContentResolver.java:753)
    at android.content.ContentResolver.openOutputStream(ContentResolver.java:729)
    at helpers.c.b(SourceFile:405)

This doesn't happen very often, let's say about 1 device out 200, but it still a relevant problem since breaks the main purpose of the app so the user can just uninstall it.

All past questions on StackOverflow looks similar to this but the solution there was not to write in the root of the storage, which is obviously not the problem here. I really have no clue with these exceptions in this particular scenario, so I might ask if anyone is aware of possible situations that make impossible to obtain a file descriptor with write access to a file.


Solution

  • The message in your exception says "Read-only file system". That message is typically associated with native errno "EROFS", which happens when a Linux process tries to open a file on read-only storage for writing.

    There are multiple reasons, why a storage can be mounted read-only. For example, most file systems are automatically re-mounted read-only by kernel in case of severe data corruption (Android system might not even realize, that device SD card has switched to read-only mode).

    It also might happen because of bugs in Android system code. In that case, knowing device vendor and model might be useful for debugging.

    The simplest solution: try to open the file in read-only mode if opening for writing fails. If read-only open succeeds, the problem can probably be solved by ejecting and re-inserting SD card (that should cause Android to automatically run fsck on it).

    Uri uri = file.getUri();
    
    try (OutputStream o = context.getContentResolver().openOutputStream(uri))
    {
      // I/O operations
    } (catch FileNotFoundException e) {
        try (OutputStream s = context.getContentResolver().openInputStream(uri)) {
             // the file can be opened for reading, but not for writing
             // ask user to reinsert SD card and/or disable
             // write protection toggle on it
             Toast.makeText(context, "Please reinsert SD card", LENGTH_SHORT);
        } catch (FileNotFoundException err) {
             // This is hopeless, just report error to user
             Toast.makeText(context, err.toString(), LENGTH_SHORT);
        }
    }
    

    If you want to determine, whether a filesystem is actually mounted read-only or not, consider reading from special file /proc/self/mountinfo. You can send it's contents to Crashlytics for debugging or even parse them in your code. You can learn more about per-process mountinfo file from Linux kernel documentation.