apache / cordova-plugin-file

Apache Cordova File Plugin
https://cordova.apache.org/
Apache License 2.0
740 stars 757 forks source link

Unable to write file error code 2 #603

Closed willPHPwork closed 8 months ago

willPHPwork commented 8 months ago

Bug Report

Problem

Unable to write files to download folder. Receiving error code 2 but I have accept permissions. (READ_MEDIA_IMAGES). Was working in Android 12 with WRITE_EXTERNAL_STORAGE.

What is expected to happen?

Save image to download folder

What does actually happen?

Creates empty file no contents. Tried with base64 and blob.

Information

permissions.checkPermission(permissions.READ_MEDIA_IMAGES, function( status ){
                    if(!status.hasPermission) 
                    {
                        // Ask for permission
                        permissions.requestPermission(permissions.READ_MEDIA_IMAGES, function() {
                            //
                        }, function() {
                            //
                        });
                    }

                }, function() {
                    //
                });

Command or Code

fileEntry.createWriter(function (fileWriter) {
      fileWriter.onwriteend = function () {
          // success
      }

      fileWriter.onerror = function (error) {
          // error
      }
      fileWriter.write(blob); // or base64
  })

Environment, Platform, Device

Android 13 SDK 33

Version information

Checklist

breautek commented 8 months ago

You don't need READ permission to write.

There is no specific WRITE permission. All apps can write to external storage now. However they cannot modify files owned by other apps. Pre scoped storage devices, with WRITE_EXTERNAL_STORAGE permission could, but scoped storage (effective since API 29) that became a limitation.

Have a read https://github.com/apache/cordova-plugin-file#androids-external-storage-quirks for more detailed information surrounding Android's Scoped storage model. if you need to modify/overwrite a file not owned by your app, then a media store plugin is now required.

willPHPwork commented 8 months ago

You don't need READ permission to write.

There is no specific WRITE permission. All apps can write to external storage now. However they cannot modify files owned by other apps. Pre scoped storage devices, with WRITE_EXTERNAL_STORAGE permission could, but scoped storage (effective since API 29) that became a limitation.

Have a read https://github.com/apache/cordova-plugin-file#androids-external-storage-quirks for more detailed information surrounding Android's Scoped storage model. if you need to modify/overwrite a file not owned by your app, then a media store plugin is now required.

Thanks for the response. Could you point me in the direction on how to write files to the download folder? This was working perfectly fine before Android 33.

breautek commented 8 months ago

Under the scoped storage model, which is effective since API 29 (However existing apps could opt out untli API 30), permissions are not needed to write to external storage but you are limited to files that you have created only.

If the file already exists, it must be a file created by your app to be writable. If the file is owned by another app, then writing to it is not possible with the File API. This is a restriction at the native File API level.

Generally speaking if you need to interact with the external file system, you should probably do so via a MediaStore plugin instead. At the native level, the MediaStore API is the replacement, which a MediaStore plugin would interface with.

willPHPwork commented 8 months ago

I really do appreciate you taking the time to explain this. I'm just trying to write a base64 image with a unique new file name created by my app to the download folder. Any idea why this isn't possible? Would I really need mediastore for such a simple task? Is there any updated documentation showing how to save an image to the download folder since Android 33?

breautek commented 8 months ago

My apologies, there's a couple of important detail I forgot, but yes the situation with Android is messy. I'd still recommend using a MediaStore plugin. I foresee support for the external file system being completely dropped in the future.

For context here is a sample file write to the external download directory:

function onDeviceReady() {
    // Cordova is now initialized. Have fun!

    console.log('Running cordova-' + cordova.platformId + '@' + cordova.version);
    document.getElementById('deviceready').classList.add('ready');

    window.resolveLocalFileSystemURL(cordova.file.externalRootDirectory + 'Download', (directoryEntry) => {
        directoryEntry.getFile('myFile.txt', {create: true, exclusive: false}, (entry) => {

        entry.createWriter((writer) => {
            writer.onwriteend = () => {
                console.log('write success');
            };

            writer.onerror = (e) => {
                console.error('write error', e);
            };

            let blob = new Blob(['test123'], { type: 'text/plain' });
            writer.write(blob);
        }, (e) => {
            console.error('writer init error', e);
        });

        }, (e) => {
            console.error('creation error', e);
        });
    }, (e) => {
        console.error('resolve error', e);
    });
}

Whether this works will depend on several factors, and depending on the API level of the device running this code.

API 24-28 Devices (Android 7-9)

On API 28 (and earlier) devices, scoped storage rules do not apply. WRITE_EXTERNAL_STORAGE is required. However the plugin as of v8 no longer manages the permission automatically. You'll need a hook script or use config-file if something isn't already adding this permission. See the release blog for more details on this. This is the first important detail I forgot to mention earlier.

API 29 Devices (Android 10)

On API 29, EXTERNAL STORAGE will not work whatsoever. API 29 is when Google first introduced Scoped Storage and disabled file system access to external storage. This is why using the Mediastore is nearly a requirement, unless if you intend to support API 30+ in your app. This is the second important detail I forgot to mention earlier.

You can read more on this issue on the Android Docs

Android 10 enforced scoped storage rules on file accesses by MediaProvider, but not for direct file path access (for example, using File API and NDK APIs) due to the effort required in intercepting kernel calls. As a result, apps in scoped storage couldn't access files using a direct file path. This restriction impacted app developers' ability to adapt as it required substantial code changes to rewrite File API access to the MediaProvider API.

API 30-32 (Android 11-12)

Starting with API 30, Google introduced a way to allow access to external storage via File APIs once again. Using the same android doc link above...

Android 11 or higher supports Filesystem in Userspace (FUSE), which enables the MediaProvider module to examine file operations in user space and to gate access to files based on the policy to allow, deny, or redact access. Apps in scoped storage that use FUSE get the privacy features of scoped storage and the ability to access files using a direct file path (keeping File APIs working in apps).

As a result, this plugin will begin working again, under the restrictions of Scoped file storage rules.

Those limitations are noted here which I've mentioned previously earlier today.

Starting with API 30, the above JS code will work assuming that you're not writing to a file that your app doesn't own. I've also tested the above code with API 32 (Android 12) device.

Note that while scoped storage doesn't require permission to write, the plugin still requests the READ_EXTERNAL_STORAGE permission, so on your creation/write call it will prompt for a permission grant.

API 33 (and beyond?) / Android 13

I've also explicitly tested this. In your use case the changes in API 33 doesn't matter. But effectively READ_EXTERNAL_STORAGE now doesn't grant you any special permissions, and they added 3 new READ permissions, one for video, audio, and images. As a result you won't see any permission requests anymore since there is an explicit API 33 or greater check. Changes in API 33 doesn't limit or increase your access to scoped storage anymore than what was previously accessible.

So with this information, to answer your questions

Would I really need mediastore for such a simple task?

If you intend to support API 29 devices, then yes, because there is no direct file support on API 29 devices. If you intend to support API 30+, then you could potentially get away with using file plugin, but I'd still recommend using a MediaStore plugin.

Is there any updated documentation showing how to save an image to the download folder since Android 33?

If you're referring documentation in regards to using the MediaStore plugin, Apache has no official plugin that interfaces with the MediaStore, so no we don't have any documentation. Our file plugin hasn't changed, but Android has changed significantly in very awkward ways.

For example, the MediaStore API doesn't fit well with a "Filesystem" API model. It would be very difficult to maintain the API as we have now, but use the MediaStore APIs instead. This is why I foresee Apache dropping support, but there has been no formal vote to move forward with that path at this time of writing. The file plugin itself still works completely intended for internal app data files. The issues is exclusively with Android's external file system.

Alternatively, if these files belong to your app as "app data", then perhaps you should use cordova.file.dataDirectory instead. But I assume that if you were writing to to the Download directory, then you must intend on having the files potentially shared with other apps.

willPHPwork commented 8 months ago

Thanks for the detailed explanation. I am currently looking into MediaStore.

function onDeviceReady() {
    // Cordova is now initialized. Have fun!

    console.log('Running cordova-' + cordova.platformId + '@' + cordova.version);
    document.getElementById('deviceready').classList.add('ready');

    window.resolveLocalFileSystemURL(cordova.file.externalRootDirectory + 'Download', (directoryEntry) => {
        directoryEntry.getFile('myFile.txt', {create: true, exclusive: false}, (entry) => {

        entry.createWriter((writer) => {
            writer.onwriteend = () => {
                console.log('write success');
            };

            writer.onerror = (e) => {
                console.error('write error', e);
            };

            let blob = new Blob(['test123'], { type: 'text/plain' });
            writer.write(blob);
        }, (e) => {
            console.error('writer init error', e);
        });

        }, (e) => {
            console.error('creation error', e);
        });
    }, (e) => {
        console.error('resolve error', e);
    });
}

This was the method I was using as well, right now it creates a file, but the file is empty (0kbs) with API 33 (and beyond?) / Android 13.