apache / cordova-plugin-file

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

Android API 32-33 Open NON Media Files #568

Closed gtaormina closed 8 months ago

gtaormina commented 1 year ago

Problem

Hi everyone, i recently noticed that since the update to Android 12-13 / API 32-33, the readAsDataURL and readAsDataText methods are no longer working (return NULL) in case of reading PDF/TXT files, while they work no problem for pictures or videos. The folder I'm reading from is the Downloads folder of my Internal Storage.

What is expected to happen?

The result should return the Text or the Base64 Version (as Images)

What does actually happen?

The result is always null

Command or Code

` Hi, this is my actual code for reading NON MEDIA File in MyFileService in Ionic 6; this code works fine when targetSDK is 29-30, but the Google Playstore no longer accepts this SDK. When i changed to target 32-33 it didn't work anymore:

`//PROPERTY import { Injectable } from '@angular/core'; import { File } from '@ionic-native/file/ngx'; import { Device } from '@awesome-cordova-plugins/device/ngx'; import { FileChooser } from '@ionic-native/file-chooser/ngx'; import { FilePath } from '@awesome-cordova-plugins/file-path/ngx'; import { Platform } from '@ionic/angular'; import { HttpService } from './http.service'; import { Chooser } from '@awesome-cordova-plugins/chooser/ngx';

//CONSTRUCTOR public file: File, public device: Device, private platform: Platform, public fileChooser: FileChooser, private chooser: Chooser, public http: HttpService, private filePath: FilePath

....

//SNIPPET WITH ERROR this.fileChooser .open() .then((uri) => { this.filePath .resolveNativePath(uri) .then((url) => { this.file .resolveLocalFilesystemUrl(url) .then((fileEntry: any) => { this.platform.ready().then(() => { debugger; this.file.checkFile(fileEntry['nativeURL'].replace(fileEntry['name'], ''), fileEntry['name']).then(response => { debugger; if (response === true) { this.file .readAsText( fileEntry['nativeURL'].replace(fileEntry['name'], ''), fileEntry['name'] ) .then((result) => { if (result) { ----> //RESULT NULL FOR PDF and TXT <--------- debugger; resolve(result); } else { reject('Errore'); } }) .catch((err) => { console.log('err-->' + JSON.stringify(err)); }); } }).catch(err => { debugger; }); }); }); }) .catch((err_3) => { reject(err_3); }); }) .catch((err_2) => { reject(err_2); });``

Environment, Platform, Device

Ionic Cordova Android Platform 11

Version information

Ionic:

Ionic CLI : 6.20.3 (/usr/local/lib/node_modules/@ionic/cli) Ionic Framework : @ionic/angular 6.4.1 @angular-devkit/build-angular : 12.0.5 @angular-devkit/schematics : 12.2.18 @angular/cli : 12.0.5 @ionic/angular-toolkit : 4.0.0

Cordova:

Cordova CLI : 11.0.0 Cordova Platforms : android 11.0.0, ios 6.2.0 Cordova Plugins : cordova-plugin-ionic-keyboard 2.2.0, cordova-plugin-ionic-webview 4.2.1, (and 27 other plugins)

Utility:

cordova-res : not installed globally native-run (update available: 1.7.2) : 1.7.1

System:

Android SDK Tools : 26.1.1 (/Users/giuseppetaormina/Library/Android/sdk) ios-sim : 8.0.2 NodeJS : v18.12.0 (/usr/local/bin/node) npm : 8.19.2 OS : macOS Xcode : Xcode 14.3 Build version 14E222b

breautek commented 1 year ago

The folder I'm reading from is the Downloads folder of my Internal Storage.

Are you sure you don't mean external storage?

Android, Internal Storage refers to to the storage medium guaranteed to be on the device, it usually contains the app install, and the data partition is private to the app only.

External Storage on the other hand may or may not be on the device. If the device has a physical removable storage, like an sdcard, then external storage will be that storage medium, but android also emulates external storage for devices that either don't have an attached storage medium or just plainly doesn't support one. The Download/ directory is found on the External Storage medium. It may have a path such as /sdcard/Download/or /storage/emulated/0/Download.

If the latter, then it's bit of a known issue unfortunately. As of API 30, Scoped Storage rules are fully enforced and there is a caveat on API 29 devices which I'll explain a bit later.

What is Scoped Storage

Scoped Storage is a privacy-focused storage system introduced by Android. It applies to External Storage medium and apps no longer have broad access to the external storage. WRITE_EXTERNAL_STORAGE no longer gives any permission, instead apps may freely read and write to the external storage. However they can only read and write to files that the app has created. If a file already exists but was created from from App A, then App B cannot read or write to that file using the Filesystem API.

Natively to gain read & write access to these files, there is a non-filesystem API called the MediaStore API. The owner of the file must implement a file provider service so it can grant permission to a third-party app trying to read or write to it. I want to emphasize that this API isn't filesystem-like at all so it will be difficult to treat it as such which is why it hasn't really been addressed or resolved in this plugin. For example, you cannot progrommatically list or view the contents of a directory (you can but it will be filtered to only files owned/created by your app) and instead the MediaStore will open the system's file picker and the user may choose the file they want to open.

I believe with the READ_EXTERNAL_STORAGE, some media is granted for reads even if they aren't owned by your app, but that doesn't include Document files. Starting API 33+, there will be 3 new permission replacing READ_EXTERNAL_STORAGE, but likewise, those 3 permissions are only for specific media types: video, audio, and images. Document files must go through the app permission grant system through the media store API, and app that owns the file must implement a file provider to grant permission to third-party apps.

Additionally, specificially on API 29 devices which is when Scoped Storage was first introduced (but while targeting API 29, it could be opted out, which is not possible today if you intend to deploy to Google Play store). API 29 devices does not have a File System bridge API into the scoped storage module. So using the Native File APIs to read/write files into external storage simply does not work and the only API available to do those actions is the previously mentioned MediaStore API.

As a workaround for now, you'll need to use a plugin that implements/exposes the MediaStore API. But a plugin that does it's work via MediaStore is only one cog in this machine. If the that app that created that file you're attempting to read doesn't implement a FileProvider to grant permissions to other apps, then the file will not be readable.

gtaormina commented 1 year ago

Hi Norman, thanks a lot for the answer. Hi Norman, thank you so much for your quick reply. I had already seen similar Issues but I wanted to be sure about the presence of valid alternatives. I confirm that the path I access is file:///storage/emulated/0/Download/new.txt , so the problem is the one you explained. I just have a question about the implementation. You said that if the file is created by my app it is likely that it can be read, but if it is external and it is not MEDIA no. Can you confirm it? I'll explain the cases of my app:

Do you have any suggestions for me based on the above points? Many thanks in advance

Giuseppe

breautek commented 1 year ago

You said that if the file is created by my app it is likely that it can be read, but if it is external and it is not MEDIA no. Can you confirm it?

Nope that's not quite what I meant. I'll try to expand and clarify.

On external storage, com.example.app can both read and write to the external storage with no special permission, with some limitations.

Let's assume your app is com.example.app.

If com.example.app writes to /Downloads/document.pdf(full path omitted for brevity) there are 3 situations that may occur:

  1. File doesn't exist, which case the file should be allowed to be created and written to.
  2. File does exist and is owned by com.example.app. In this case the file is owned by the same app that created it, so writing should be permitted.
  3. File does exist but is owned by com.example.foo. In this case the file was created by a different app. The file API will forbid writes to it.

This same can also be said when attempting to read /Downloads/document.pdf:

  1. File exists and is owned by com.example.app, the file API should be able to read the file.
  2. File exists and is owned by com.example.foo, the file API will not be able to read this file or see it when listing the directory contents. The error will be a FILE NOT FOUND kind of error for privacy reasons. (As far as the app can tell, the file doesn't exist, but it also won't be able to write to it either).

Effectively if you're working with files that is created and managed by your app, then external storage should yield no problems, outside of filename collisions. However if you're trying to interact with files created by another app, then the MediaStore API generally needs to be used which this plugin does not have support for (and I don't know how feasible it is to add support).

gtaormina commented 1 year ago

Okay, clear. Based on what you wrote, i exported this text file to the application folder (file:///data/user/0/com.example.app/files/), then copied it to the general download folder of the device. Unfortunately when I try to read it (from the download folder) I get the error {code: 3, message: 'Filesystem permission was denied.'}

It is obvious that, assuming I have to open the file on another device, I cannot use the local folder of the app, I have to use an external one because for example I could download this file from messaging apps (Telegram, WhatsApp, etc..)

What can I do? Thanks a lot Giuseppe

breautek commented 1 year ago

Okay, clear. Based on what you wrote, i exported this text file to the application folder (file:///data/user/0/com.example.app/files/), then copied it to the general download folder of the device. Unfortunately when I try to read it (from the download folder) I get the error {code: 3, message: 'Filesystem permission was denied.'}

This was all done using the same app on the same device? If so then that's strange... I've tested that kind of situation with a plain text file before and I'm pretty certain it has worked before for me. I don't interact with the external storage with my day job however... it is possible that I'm wrong.

If it doesn't work using the Filesystem API, then you'll probably need to fallback to a media store plugin, unfortunately there isn't many options available in the community for that, cordova-plugin-saf-mediastore seems to be the most maintained one, however it's license appears to forbid use without explicit permission. The mention of the plugin is also not an endorsement.

chiraganand commented 10 months ago

@gtaormina It looks like for API 30+ Download directory is anyway not accessible even for listing: https://developer.android.com/training/data-storage/shared/documents-files#document-tree-access-restrictions

Screenshot_20230712_135804

EYALIN commented 10 months ago

@gtaormina i think that latest code will fix this issue

breautek commented 10 months ago

I'm going to shed some more light now that I've understand more about the Android scoped filesystem since the last post in May.

Short answer is unfortunately I don't think the issue is resolvable by the plugin, and you'll likely need to use another plugin that interfaces MediaStore API rather than the File API.

Long answer with history...

History

In API 29, Android introduces a system called Scoped Storage, however on API 29 devices for apps already published to the app store had the ability to opt out to the legacy system via requestLegacyExternalStorage flag. The legacy system gave apps free reign over external storage for as long as WRITE_EXTERNAL_STORAGE was granted.

Scoped Storage (API 29-32)

Scoped Storage makes the WRITE_EXTERNAL_STORAGE permission obsolete as apps have the ability to write to external storage without permission now, but they cannot write to a file that already exists if the file is owned by another app. Apps can also read from external storage without permission but visibility is limited to files only owned by your own app. READ_EXTERNAL_STORAGE will grant you to read media files, but not documents.

Special Notes for API 29

On API 29 specifically, android does not have a File API bridge to Scoped Storage framework. This means if requestLegacyExternalStorage is not enabled, or if you have a brand new app that isn't published to the app store, all file APIs will fail to access external storage. In API 30 and onwards, Scoped Storage framework is forcefully enabled.

Only in API 30 and onward, Android has implemented a File Bridge to allow File APIs to access content in external storage, allowing File APIs (and this file plugin) to somewhat work again. It also means this plugin will not work at all on API 29. It's important to note that even with these file based APIs enabled once again, they only have access to media files.

To help your app work more smoothly with third-party media libraries, Android 11 allows you to use APIs other than the MediaStore API to access media files from shared storage using direct file paths.

Paths like Documents or Downloads are not readable except for media files (images, videos and/or audio). In order to read non-media files, MediaStore API must be used.

What is the MediaStore vs File APIs

To the non-android devs that might be reading this, I'll write a quick explanation on the MediaStore API vs File API.

The File API, is kinda what it sounds. It's a pretty standard filesystem oriented API where you operate on directories and files and you're able to stream data in and out of it. The API offers full programmatic control over the filesystem.

MediaStore API is not a filesystem-oriented API. it's more like a database with a query system. The filesystem details is abstracted and you don't have programmatic control over the filesystem structure, and in some cases you have limited discoverability of files. It's a privacy-focused API so it comes with many restrictions. For example, you generally cannot programmatically read the list of files that may be present on the device. Rather you'll need to open an Intent (e.g. a file picker) where the user selects the file then, and only then your app can become aware that file exists.

Attempting to use the MediaStore in a "file-like" API fashion as this plugin implements I don't think is feasible. Which is why I think a different plugin is required to properly handle accessing external storage on Android.

Notes on API 33+

READ_EXTERNAL_STORAGE is now obsolete. It's still required to support API 32 and earlier, but on API 33, we have 3 new permissions:

You'll notice we still don't have a permission for docs, so we cannot "discover" documentation. We can only open docs via a system file picker intent, like in previous android versions and only if the app that owns the document has a content provider implemented to grant third-party apps access to their document files.

Notes on MANAGE_EXTERNAL_STORAGE Permission

With the introduction of scoped storage, Android introduced MANAGE_EXTERNAL_STORAGE that I believe effectively gives you free reign over external storage similar to the legacy system. However this permission is protected and requires justification. Google will not allow any app with this permission be published to the app store unless if they have a very good reason to use it. An example of that the primary focus of your app is a file manager app, or an anti virus app. Therefore majority of users cannot take advantage of this permission.

So with all that being said, I kinda foresee external storage support being stripped out in favour for a media store oriented plugin, however that is yet to been discussed at the Apache development level... https://www.npmjs.com/package/cordova-plugin-saf-mediastore is one plugin I've heard people had success with, but this isn't an endorsement and I'm not familiar with the NOPL license. So do your own research.

MauriceFrank commented 10 months ago

I'm going to shed some more light now that I've understand more about the Android scoped filesystem since the last post in May.

Short answer is unfortunately I don't think the issue is resolvable by the plugin, and you'll likely need to use another plugin that interfaces MediaStore API rather than the File API.

Long answer with history...

History

In API 29, Android introduces a system called Scoped Storage, however on API 29 devices for apps already published to the app store had the ability to opt out to the legacy system via requestLegacyExternalStorage flag. The legacy system gave apps free reign over external storage for as long as WRITE_EXTERNAL_STORAGE was granted.

Scoped Storage (API 29-32)

Scoped Storage makes the WRITE_EXTERNAL_STORAGE permission obsolete as apps have the ability to write to external storage without permission now, but they cannot write to a file that already exists if the file is owned by another app. Apps can also read from external storage without permission but visibility is limited to files only owned by your own app. READ_EXTERNAL_STORAGE will grant you to read media files, but not documents.

Special Notes for API 29

On API 29 specifically, android does not have a File API bridge to Scoped Storage framework. This means if requestLegacyExternalStorage is not enabled, or if you have a brand new app that isn't published to the app store, all file APIs will fail to access external storage. In API 30 and onwards, Scoped Storage framework is forcefully enabled.

Only in API 30 and onward, Android has implemented a File Bridge to allow File APIs to access content in external storage, allowing File APIs (and this file plugin) to somewhat work again. It also means this plugin will not work at all on API 29. It's important to note that even with these file based APIs enabled once again, they only have access to media files.

To help your app work more smoothly with third-party media libraries, Android 11 allows you to use APIs other than the MediaStore API to access media files from shared storage using direct file paths.

Paths like Documents or Downloads are not readable except for media files (images, videos and/or audio). In order to read non-media files, MediaStore API must be used.

What is the MediaStore vs File APIs

To the non-android devs that might be reading this, I'll write a quick explanation on the MediaStore API vs File API.

The File API, is kinda what it sounds. It's a pretty standard filesystem oriented API where you operate on directories and files and you're able to stream data in and out of it. The API offers full programmatic control over the filesystem.

MediaStore API is not a filesystem-oriented API. it's more like a database with a query system. The filesystem details is abstracted and you don't have programmatic control over the filesystem structure, and in some cases you have limited discoverability of files. It's a privacy-focused API so it comes with many restrictions. For example, you generally cannot programmatically read the list of files that may be present on the device. Rather you'll need to open an Intent (e.g. a file picker) where the user selects the file then, and only then your app can become aware that file exists.

Attempting to use the MediaStore in a "file-like" API fashion as this plugin implements I don't think is feasible. Which is why I think a different plugin is required to properly handle accessing external storage on Android.

Notes on API 33+

READ_EXTERNAL_STORAGE is now obsolete. It's still required to support API 32 and earlier, but on API 33, we have 3 new permissions:

  • READ_MEDIA_AUDIO
  • READ_MEDIA_IMAGES
  • READ_MEDIA_VIDEO

You'll notice we still don't have a permission for docs, so we cannot "discover" documentation. We can only open docs via a system file picker intent, like in previous android versions and only if the app that owns the document has a content provider implemented to grant third-party apps access to their document files.

Notes on MANAGE_EXTERNAL_STORAGE Permission

With the introduction of scoped storage, Android introduced MANAGE_EXTERNAL_STORAGE that I believe effectively gives you free reign over external storage similar to the legacy system. However this permission is protected and requires justification. Google will not allow any app with this permission be published to the app store unless if they have a very good reason to use it. An example of that the primary focus of your app is a file manager app, or an anti virus app. Therefore majority of users cannot take advantage of this permission.

So with all that being said, I kinda foresee external storage support being stripped out in favour for a media store oriented plugin, however that is yet to been discussed at the Apache development level... https://www.npmjs.com/package/cordova-plugin-saf-mediastore is one plugin I've heard people had success with, but this isn't an endorsement and I'm not familiar with the NOPL license. So do your own research.

Has anyone a hint on how to use the cordova-plugin-saf-mediastore properly? I cannot get the function "getURI()" to work when I try passing "Pictures" as folder. I also tried the root-directory path and other combinations.

My file-path I have is: "file:///storage/emulated/0/Pictures/cpcp_capture_65e7de6f.jpg" Goal: delete this file

I appreciate any help! Thank you 💯

breautek commented 8 months ago

Closing because this is not actionable by Cordova in terms of file plugin API.

Android simply doesn't allow the file plugin to read non-media files owned by other apps using the file API.

MediaStore API is required.

This is now noted at https://github.com/apache/cordova-plugin-file#androids-external-storage-quirks