cordova-plugin-file
cordova-plugin-file copied to clipboard
Cordova 11 Android throwing FileError Code 12 trying to access fileSystem
Bug Report
Trying to write to localStorage is breaking on Android with Cordova 11.1.0 (cordova-android 11)
Problem
I am using plugin cordova-plugin-file-downloader
which is essentially a wrapper for other various plugins. My app code worked on Cordova 10 but is now breaking on Cordova 11. I tracked everything down to a FileError Code 12
which implies the system/folder I am trying to write to doesn't exist, can't be created or doesn't have permission, and the error is being generated from this cordova-plugin-file
plugin.
The cordova-plugin-file-downloader
call is simply:
downloader.init({folder: 'CustomFolderName', fileSystem: 'file:///storage/emulated/0/'}) ;
or the default fileSystem:
downloader.init({folder: 'CustomFolderName''}) ;
It fails to initialize and immediately throws the FileError code 12. In <= Cordova 10, this same code worked and on Android devices I could navigate to the devices general Documents folder, open the designated CustomFolderName
folder and view the file. I looked at Cordova 10 cordova-plugin-file fileSystem specs and they look the same as cordova 11 fileSystem specs; this I think this is a bug. Or if it has changed, what should I be using now because I don't am not seeing any differences?
What is expected to happen?
Folder created and document written to that folder
What does actually happen?
FileError Code 12 is generated.
Information
Command or Code
Environment, Platform, Device
Windows 10, Android - all devices
Version information
[email protected] cordova-android@11 [email protected]
Checklist
- [x ] I searched for existing GitHub issues
- [ x] I updated all Cordova tooling to most recent version
- [x ] I included all the necessary information above
Android's scoped storage forbids creating custom directories in external storage root and some other directories. Scoped storage is enforced on all API 29 devices and later.
Additionally, API 29 SDK lacks the FileSystem API for scoped storage, which effectively breaks this plugin as well as any library that depends on using the Java's File APIs when trying to access the external storage.
So instead of using 'file:///storage/emulated/0/'
, (which may not even exist on all android devices, you should use cordova.file.externalRootDirectory
to refer to this directory), try using your application's external storage directory instead, cordova.file.externalDataDirectory
.
If using the external storage is not a requirement, it may be better to use the internal storage instead (and avoid all the external storage limitations, including the API 29 problem explained above): cordova.file.dataDirectory
.
Let me know if this helps.
@breautek - thanks for quick response.
On both Simulator (Pixel 3a Android 10) and on real Samsung Flip 4 (Android 13) the cordova.file.externalRootDirectory
resolves as file:///storage/emulated/0/
causing the FileError Code 12 on both.
On both, externalDataDirectory
resolves as: file:///storage/emulated/0/Android/data/com.myApp/files/
And changing it from externalRootDirectory
to externalDataDirectory
got past that issue, But still working on other related issues now - just concerned the final saved document is not going to be in a place where the users can access them.
Question: Looking to transition off of cordova-plugin-file-transfer and came across this: https://cordova.apache.org/blog/2017/10/18/from-filetransfer-to-xhr2.html - I presume the LocalFileSystem.PERSISTENT
can be replaced with cordova.file.externalDataDirectory
or similar? Or is PERSISTENT
the only way to make this work?
@breautek - a follow up on this. Could use your input.
For Android, I cannot write directly to: cordova.file.externalRootDirectory
anymore. I guess it changed in API 29/30. However, targeting API 32, I can write to cordova.file.externalRootDirectory + "MyNewFolder"
and download my file there. Ok, so I got all that working again.
However for iOS, I am continually perplexed. For iOS I have now used both cordova.file.externalDataDirectory
and cordova.file.dataDirectory
- in BOTH cases, my app can read the root directories and create the MyNewFolder
in both and then write files to that new folder. In subsequent passes, my app can then read the contents of MyNewFolder
(in both locations) and see the downloaded file there....ALL via console.log messages.
However, on the device itself, the MyNewFolder
and its file are not accessible in any place that I can find. The folder and new file now exists, but how does the user access them? In the device app Files
, then in the On my iPhone
option, it takes me to an empty folder. But if I manually download a file from Safari, a new folder Downloads
appears in the On My iPhone
section and the manually downloaded file is now in that folder. Safari must be creating that Downloads
folder on the first manual download.
In my app, I then try to find this new Downloads
folder (printing the contents of every cordova.file.PATH
to read its contents (and maybe use it as the future directory to create MyNewFolder
and write my files there) but I can't find the Downloads
folder anywhere.
Please help me resolve this. I have spent days on this and am pulling out what little hair I have left.
I presume the LocalFileSystem.PERSISTENT can be replaced with cordova.file.externalDataDirectory or similar? Or is PERSISTENT the only way to make this work?
Not quite. First I want to make clear of two filesystem concepts that Android has:
-
Internal
storage. This is a embedded chip that is tied to the device itself. -
External
storage. This is an independent storage medium that may or may not exist on the device, or it may change at any time. Most android devices will emulate external storage (in which case it will have a/storage/emulated/
path), but if the user inserts an SD card, then the external storage path may change to/sdcard
.
Now Internal
storage is protected but every application has it's own internal storage sandbox that it basically as free reign over for the most part. Some directories or files may be read-only, such as the cordova.file.applicationStorageDirectory
, which is the installation directory of your app, which you don't have permission to update, however you can create folders in this path.
LocalFileSystem.PERSISTENT
is part of the (now defunct) W3C FileSystem API, which the file plugin implements. This is an implementation detail, but currently this uses the Internal
storage directory. It's the equivalent to cordova.file.dataDirectory + "files/"
. If you were to use the File Explorer tool, you'll see the following folder path: /data/data/<app_id>/files/files/
.
For Android, all of the cordova.file
directory APIs will refer to a Internal
storage path, while all cordova.file.external*
constants will refer to an External
storage path.
You're pretty much free to create any directory structure inside internal storage without permissions, within your app_id sandbox.
External
storage has gone through several changes (and further changes are incoming in API 33). And access depends on several factors. For example, every Android app has an external data directory, cordova.file.externalDataDirectory
, which like Internal storage, you could read and write to without any permissions.
Inside cordova.file.externalRootDirectory
you'll see the several directories for different media as well as Downloads
directory, and a few others, which I'm going to refer to these as "External Media" directories.
Android API 28 and earlier, your app could do a lot of different things, everywheres on external storage, including messing around with other app's external storage, as long as you had the READ/WRITE_EXTERNAL_STORAGE
permission.
Android API 29 have made changes to make external storage slightly more secure, which including locking down some directories. WRITE_EXTERNAL_STORAGE
no longer does anything, and you may write to External Media directories without any special permissions however you may not overwrite files that already exists if it's owned by a different application. Reading files still requires the READ_EXTERNAL_STORAGE
permission. On API 29, developers could make use of a requestLegacyExternalStorage
flag to revert the behaviour back to API 28 behaviour, but this is a request which the OS may reject. I believe it only honours it for users that already had your app installed, so in otherwords, fresh app installs will not honour this request. Lastly, API 29 is specifically broken
with the file plugin because Android does not implement a filesystem API for external storage. This means if the app wants to access the external file storage, it must use the native MediaStorage APIs, which the file plugin does not and cannot really implement.
Starting with API 30, Android does implement filesystem API for external storage which allows for third-party libraries and native code to access the external storage again, which also "fixes" this plugin so to speak. The scoped storage changes that came in API 29 however still applies.
Starting with API 33 (currently not supported), READ_EXTERNAL_STORAGE
will no longer work and instead they have READ_MEDIA_*
permissions for images, audio, and video.
I can write to cordova.file.externalRootDirectory + "MyNewFolder" and download my file there. Ok, so I got all that working again.
Android doesn't make clear but they have said that some directories will be inaccessible. Therefore I wouldn't rely on using custom directories in external root, and instead either use your app's internal or external directory. Therefore I'd consider preparing a migration strategy if necessary.
However for iOS, I am continually perplexed. For iOS I have now used both cordova.file.externalDataDirectory and cordova.file.dataDirectory.....
I'm significantly less knowledgable with iOS, and I'm not sure how externalDataDirectory
behaves for iOS (it's not documented...) but I do know that iOS does not have an internal/external storage concept like Android. iOS storage model is far more simple. Your app has an filesystem sandbox that is completely private. It's comparable to Android's internal storage concept. Sharing files between apps happens via non-filesystem APIs I believe, there is no way so share files between apps using purely the filesystem. So in otherwords, a browser app downloading a file may write to it's app storage sandbox, but use a non-filesystem API to share it in a more public place... For example, in order for the iOS to move a file to a shared location, it must use something like UIDocumentPickerViewController which brings up a save dialog and the user chooses where to put the file. The iOS app itself does not have direct access to anything outside of it's sandbox. I don't believe anything like this is implemented in the file plugin, as it's aimed to be... a filesystem API.
Hope this knowledge helps somehow.
@breautek - well after a VERY long time of frustration, I finally found the answer for iOS.
Please update the cordova-plugin-file
documentation. Honestly, I can't believe its not listed anywhere in official Cordova documentation; especially the cordova-plugin-file
docs. For anyone wanting to use cordova-plugin-file
(and maybe cordova-plugin-file-transfer
) to save files to the public On My iPhone --> Downloads
folder, they will need to add the following TWO keys to their apps info.plist
file:
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
Yup...THATS IT. What this does is create symlink to everything in the apps documents folder (ie: cordova.file.documentsDirectory
). When the user goes to the On My iPhone
root folder, they will see a Downloads
folder AND individual app folders for every app that has these values set to true
. This allows users to find/navigate every apps root Documents folder as if it were a part of their general Downloads
folder.
Unreal....spun my wheels on this one...well....for at least 3 years. Always coming up with some ugly hybrid that was always breaking; esp with each new version of iOS. But again....in all the years of searching for a solution, I never once saw any reference to these keys before. Unreal its not included in any Apache Cordova docs.
Actually, there is an OPEN request to document this very thing....its three years old! https://github.com/apache/cordova-plugin-file/issues/392 - in fact, if you do a google search for cordova UIFileSharingEnabled
VERY little comes back. How come, in general, the community simply does not know about these keys?
Sorry, on a bit of rant....mind blowing how this has escaped so many for so long.
Now...this makes me wonder....are there similar and/or corresponding setting/preferences in Android that make the apps internal directories available to users to browse so we don't have to depend on externalRootDirectory
storage locations?
@breautek - well after a VERY long time of frustration, I finally found the answer for iOS.
Please update the
cordova-plugin-file
documentation. Honestly, I can't believe its not listed anywhere in official Cordova documentation; especially thecordova-plugin-file
docs. For anyone wanting to usecordova-plugin-file
(and maybecordova-plugin-file-transfer
) to save files to the publicOn My iPhone --> Downloads
folder, they will need to add the following TWO keys to their appsinfo.plist
file:<key>UIFileSharingEnabled</key> <true/> <key>LSSupportsOpeningDocumentsInPlace</key> <true/>
Yup...THATS IT. What this does is create symlink to everything in the apps documents folder (ie:
cordova.file.documentsDirectory
). When the user goes to theOn My iPhone
root folder, they will see aDownloads
folder AND individual app folders for every app that has these values set totrue
. This allows users to find/navigate every apps root Documents folder as if it were a part of their generalDownloads
folder.Unreal....spun my wheels on this one...well....for at least 3 years. Always coming up with some ugly hybrid that was always breaking; esp with each new version of iOS. But again....in all the years of searching for a solution, I never once saw any reference to these keys before. Unreal its not included in any Apache Cordova docs.
Actually, there is an OPEN request to document this very thing....its three years old! #392 - in fact, if you do a google search for
cordova UIFileSharingEnabled
VERY little comes back. How come, in general, the community simply does not know about these keys?Sorry, on a bit of rant....mind blowing how this has escaped so many for so long.
Now...this makes me wonder....are there similar and/or corresponding setting/preferences in Android that make the apps internal directories available to users to browse so we don't have to depend on
externalRootDirectory
storage locations?
OMG finally found the answer to my problem on which I have been banging my head on since last 4 days. Thank you so much @rolinger for such a detailed answer, really appreciate your effort.