Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storage updates in Android 11 #426

Closed
HarelM opened this issue Oct 1, 2020 · 142 comments · Fixed by #513
Closed

Storage updates in Android 11 #426

HarelM opened this issue Oct 1, 2020 · 142 comments · Fixed by #513
Labels
android-scoped-storage Issues relating to Android's Scoped storage changed introduced in API 29/30 help wanted Extra attention is needed platform: android

Comments

@HarelM
Copy link

HarelM commented Oct 1, 2020

Feature Request

Support the android 11 new file system based policy.

Motivation Behind Feature

Support the changes that are planned for android 11 (API level 30)

Feature Description

I'm not sure how to fully describe this, but basically support what's documented here:
https://developer.android.com/about/versions/11/privacy/storage

Alternatives or Workarounds

Not targeting API level 30 until this is supported?

@HarelM
Copy link
Author

HarelM commented Oct 26, 2020

Seems like the fix that was merge #417 in order to solve #408 will no longer work for android 11 (API level 30).
I'm not entirely sure I understand what needs to be done in android in order to get access to the file system, something like opening a file picker in order to get write permissions to a folder?
I'd love to help progress this, maybe send a PR if someone can guide me to the relevant place in the code and define the appropriate solution...

@breautek
Copy link
Contributor

Seems like the fix that was merge #417 in order to solve #408 will no longer work for android 11 (API level 30).

Yes, you're correct. In API 29, there is a new file system policy. #417 utilised an available flag to request the old file system system, but API 30 will ignore that flag, forcing app developers to implement the new policy model.

To learn about the new android model, you should probably read up on Scoped Storage. I briefly read up on this last weekend and it appears that some apps may require to migrate their data locations depending on where they store data. Scoped storage means just that, you're access to the filesystem is limited to a certain scope. Parts of the file system that you might have had access before may be impossible to access in API 30. This means app developers should take this time to determine if they need to migrate data. Read the android notice for more information on migration. Given the usual pattern, users have probably until August 2021 before Google force API 30 for new apps, and November 2021 before users are forced to use API 30 on existing apps.

The new MANAGE_EXTERNAL_STORAGE permission I believe will essentially make the app behave pre API 29 without the legacy storage flag, but will likely be highly scrutinised by Google if the app uses it without a justified reason. This could be an opt in feature by documenting how one can add this permission using config-file or edit-config. The users must be targeting API 30 in order to use this permission, it isn't available in API 29.

Lastly, getting familiar with the MediaStore APIs sounds like is quite important, because apps can access some external filesystem storages, for accessing pictures, videos, and other media-related resources.

What needs to be changed exactly in the Apache codebase I'm not so certain myself, I'm not that familiar with the code base.

@olsbg
Copy link

olsbg commented Dec 3, 2020

Adding ' android:requestLegacyExternalStorage="true"' to AndroidManifest.xml solves the problem.

Read more at https://developer.android.com/training/data-storage/use-cases.

@HarelM
Copy link
Author

HarelM commented Dec 3, 2020

@olsbg this won't work for android 11, it does and will work for android 10.
I've migrated my entire code that saves file to the external file system to share files that are created in memory to avoid any future limitations...

@Vatsov
Copy link

Vatsov commented Dec 28, 2020

@olsbg this won't work for android 11, it does and will work for android 10.
I've migrated my entire code that saves file to the external file system to share files that are created in memory to avoid any future limitations...

Can you share how you achieved this?

@HarelM
Copy link
Author

HarelM commented Dec 29, 2020

Instead of saving files to the file system I use the social sharing plugin. This way a user is prompted with options on what he wants to do with the file I just created in memory:
https://github.com/IsraelHikingMap/Site/blob/4e479a5a92051ae5ec84136ca8c20547a4d11f26/IsraelHiking.Web/sources/application/services/file.service.ts#L152

@Vatsov
Copy link

Vatsov commented Dec 29, 2020

Thanks, @HarelM ,
Unfortunately this won't work in my case because I'm trying to read a file

@HarelM
Copy link
Author

HarelM commented Dec 29, 2020

@Vatsov Are you reading a file a user selects? If so I don't think there's an issue that is related to Android 11, or at least I hope not as you open the file picker and this should be OK even in Android 11 as far as I know.
The other way I solved the file open is by registering my app to open certain file types and allowing the user to share these files with the app.
https://github.com/IsraelHikingMap/Site/blob/4e479a5a92051ae5ec84136ca8c20547a4d11f26/IsraelHiking.Web/config.xml#L45
And:
https://github.com/IsraelHikingMap/Site/blob/4e479a5a92051ae5ec84136ca8c20547a4d11f26/IsraelHiking.Web/sources/application/services/open-with.service.ts#L85
Hope that helps... :-)

@Vatsov
Copy link

Vatsov commented Dec 29, 2020

@HarelM Yes, I’m trying to read the file which the user selects with this.fileChooser.open()
The interesting thing is that this.file.checkFile returns true, but when I try with this.file.readAsText it returns null

So I decided firstly to copy the file to this.file.dataDirectory and after that to read it but the this.file.copyFile but returns FileError {code: 1, message: "NOT_FOUND_ERR"}

@smartyw
Copy link

smartyw commented Jan 8, 2021

FYI

window.resolveLocalFileSystemURL(cordova.file.externalRootDirectory + "/Download" used to work but now fails with Code 1 on Android 10 both without specifying android:requestLegacyExternalStorage="true" and with. I can't test on Android 11.

It would be really useful to know if there's a plan for the maintainers to create a new release which makes the plugin work with Android 10 and 11 without the need to manually tweak files like the manifest. Hope there is. This is a really useful plugin.

Thanks

@breautek
Copy link
Contributor

breautek commented Jan 8, 2021

What should work

window.resolveLocalFileSystemURL(cordova.file.externalRootDirectory + "/Download" used to work

  • This should work as is when targeting API 28 (now forbidden by Google Play)
  • This should work when targeting API 29 with the dev version of the plugin (which has the android:requestLegacyExternalStorage="true"), alternatively you can use the edit-config to add this flag.
  • This won't work when targeting API 30, as API 30 ignores the android:requestLegacyExternalStorage attribute.

It's important to read the Android Notes before you target API 30. You may need to migrate your files to another folder to maintain access to them when targeting API 30 using the new APIs.

Who this will affect

This will affect any app that reads or writes to the external file system (aka the cordova.file.external*). If you don't use external file paths, then you probably don't need any changes in your app.

I'll be adding a help wanted label. We don't have a planned solution yet. If a volunteer would take the lead and finding a solution to prepare a PR, I encourage to use our Dev Mailing List so that we can discuss requirements and potential solutions.

Useful Links: https://developer.android.com/about/versions/11/privacy/storage

@breautek breautek added the help wanted Extra attention is needed label Jan 8, 2021
@smartyw
Copy link

smartyw commented Jan 8, 2021

@breautek Thanks for the quick response and useful summary. I'll have to think about this :-)

@HarelM
Copy link
Author

HarelM commented Jan 9, 2021

I've recently implanted a small android app which had to save images to a folder/media storage so I might publish my conclusions here once I'm done...

@jfoclpf
Copy link

jfoclpf commented Jan 11, 2021

window.resolveLocalFileSystemURL(cordova.file.externalRootDirectory + "/Download" used to work

  • This should work when targeting API 29 with the dev version of the plugin (which has the android:requestLegacyExternalStorage="true"),

Did you mean running cordova plugin add https://github.com/apache/cordova-plugin-file.git
Will be this backward compatible for API <= 28 also?

@breautek
Copy link
Contributor

Will be this backward compatible for API <= 28 also?

Yes, but you still need to use build tools version 29, otherwise the build will fail to recognize android:requestLegacyExternalStorage.

@jfoclpf
Copy link

jfoclpf commented Jan 11, 2021

Great, thanks, indeed I made

cordova plugin rm cordova-plugin-file
cordova plugin add https://github.com/apache/cordova-plugin-file.git

I tested it now and it allows me to use cordova.file.externalRootDirectory + "/Download" in Android 10 (API 29)

Yes, but you still need to use build tools version 29, otherwise the build will fail to recognize android:requestLegacyExternalStorage.

Did you mean this in config.xml?

<preference name="android-targetSdkVersion" value="29"/>

Thanks in advance

@breautek
Copy link
Contributor

I tested it now and it allows me to use cordova.file.externalRootDirectory + "/Download" in Android 10 (API 29)

Yes, but you still need to use build tools version 29, otherwise the build will fail to recognize android:requestLegacyExternalStorage.

Did you mean this in config.xml?

"Build Tools" is part of the Android SDK, you can download it using the Android Studio's SDK Manager tool.

Screenshot from 2021-01-11 19-26-05

@jfoclpf
Copy link

jfoclpf commented Jan 12, 2021

@breautek thanks. I also used your answer, citing the source, to reply on stackoverflow the open issue.

@snr-lab
Copy link

snr-lab commented Jan 26, 2021

Adding android:requestLegacyExternalStorage="true" is throwing AAPT: error: attribute android:requestLegacyExternalStorage not found. Because cordova 9.x.x is building with SDK-28 even i mention targetSdkVersion as 29. Is there any workaround for that?

@breautek
Copy link
Contributor

@HarelM
Copy link
Author

HarelM commented Mar 3, 2021

Seems like opening a file in android 11 doesn't work. It does work in android 10. I'm not using any code but simply input with type=file to pick a file.
If anyone has a solution please let me know, I'll see if I can send a PR once I get to trying and fixing it...

@jfoclpf
Copy link

jfoclpf commented Mar 24, 2021

@HarelM do you have any updates on this issue?
Is the social sharing plugin the only option yet to save a file in Android 11?

@HarelM
Copy link
Author

HarelM commented Mar 24, 2021

Not yet, but we are talking about two different issues:

  1. Save to file
  2. Open a file

To summarize my experience:

  1. I have "solved" save to file by using social sharing - both due to security and mainly due to better user experience (IMO of course)
  2. I will be looking into the issue of file open on android 11 soon - see issue Open file fails on Android 11 IsraelHikingMap/Site#1451 in my repo.

Also I've just seen recently that starting from August/November this year Google is changing the policy to target API level 30 for all new/updated applications so this has to be solved soon from my point of view...

@jfoclpf
Copy link

jfoclpf commented Mar 24, 2021

Thank you @HarelM , regarding point 1. could you kindly tell me exactly which social sharing plugin do you use?

@HarelM
Copy link
Author

HarelM commented Mar 24, 2021

cordova-plugin-x-socialsharing
https://github.com/IsraelHikingMap/Site/blob/6b658ae41f5a21d582ac15ee18385af4f128c6ac/IsraelHiking.Web/package.json#L116

@Topiya
Copy link

Topiya commented Nov 18, 2021

Inkedandroid-studio_LI

I have already added sdk as above.

@hanslbua
Copy link

With Android 10 save to persistant has worked but i get this error:

Not allowed to load local resource: file:///storage/emulated/0/app_media/1637263763703.jpg

Not allowed sounds for me like a permission problem. Has someone else the same problem? How can i fix this?

@Topiya
Copy link

Topiya commented Nov 19, 2021

If I add cordova-android 10.1.0 then only it shows Android target: android-30 while ionic cordova platform add android.

PS D:\Project> ionic cordova platform add android@10.1.0

cordova.cmd platform add android@10.1.0
Using cordova-fetch for cordova-android@10.1.0
Adding android project...
Creating Cordova project for the Android platform:
Path: platforms\android
Package: test.test
Name: test
Activity: MainActivity
Android target: android-30

@brunoalex
Copy link

cant acess the file :/ in sdk 30

@sachithd
Copy link

I addressed the issue where the application could not access a file shared into the application by the user by instead forking cordova-plugin-filepath: VIAVI-Solutions/cordova-plugin-filepath@a471420

@chrisjdev Thank you. This is a great workaround for the time being.

@brunoalex
Copy link

brunoalex commented Nov 22, 2021

i still cant acess my file , im using 👍
window.FilePath.resolveNativePath('file:///data/user/0/events.staff.booking.app/cache/FB_IMG_1637505177709.jpg', successCallback, errorCallback); function successCallback(s){ console.log("sucess:")+s; } function errorCallback(e){ console.log("error:")+e;

but the the file path isent acessible , i get no acessible path only console log "sucess:"

@sachithd
Copy link

i still cant acess my file , im using +1 window.FilePath.resolveNativePath('file:///data/user/0/events.staff.booking.app/cache/FB_IMG_1637505177709.jpg', successCallback, errorCallback); function successCallback(s){ console.log("sucess:")+s; } function errorCallback(e){ console.log("error:")+e;

but the the file path isent acessible , i get no acessible path only console log "sucess:"

After making the change to the FilePath.java in plugins folder, have you tried removing and adding the Android platform?

@brunoalex
Copy link

i did, but i just realise that if i run window.open on my path : "file:///data/user/0/events.staff.booking.app/cache/FB_IMG_1637505177709.jpg" .. the image opens ... i just cant get it to upload to my server dont know why i get upload code:1 ... using file-tranfere plugin ...

@breautek
Copy link
Contributor

Addressing a few comments over the past few days:

but the the file path isent acessible , i get no acessible path only console log "sucess:"

You're only getting "success: because that's the only thing you're logging out.

Instead of doing: console.log("sucess:")+s; I think you mean:

console.log("sucess:", s);

cant acess the file :/ in sdk 30

The root / folder is protected. You can only access files under your application internal directory (See the File system directory table. Anything that isn't explicitly marked as external in this table are internal directories.

Not allowed to load local resource: file:///storage/emulated/0/app_media/1637263763703.jpg

Starting with cordova-android@10, cordova-android uses WebViewAssetLoader by default and disables local file system access as recommended by the android docs. This is because this option is deemed insecure, but was a critical component that Cordova that depended on with on other alternative until Android introduced the WebViewAssetLoader. Using the WebViewAssetLoader also overcomes some other same-origin issues as requests appear to come from a true origin rather than the filesystem which has a null origin. More information on this change in our blog release notes.

This also means you can't use use file system urls directly like before. I think I might have to make a blog post about this but you can still read the file (assuming you have access to that file) as a blob and use Object URLs.

Alternatively, if you want the previous cordova-android behaviour, you can enable the AndroidInsecureFileModeEnabled preference in your config.xml file.

This is not related to the cordova-plugin-file, so I would respectfully ask to refrain from posting more on this. If you have further questions on this, you can reach reach out to our Slack community. (Link is a temporary link since the main link is down at the time of writing...)

MANAGE_EXTERNAL_STORAGE

Note that adding the MANAGE_EXTERNAL_STORAGE permission may work for you but it also may limit your ability to deploy to the Google Play store. This permission will require justification and Google reserves the right to block your app from the play store if they feel like your app don't need this permission.

@Topiya
Copy link

Topiya commented Nov 24, 2021

@breautek I have added AndroidInsecureFileModeEnabled preference in config.xml but it is not working for me.

@alex-steinberg
Copy link

Thank you for the explanation @breautek. My issue was that I wasn't using WebViewAssetLoader. Simply replacing file:// with https://localhost in my JavaScript allowed me to access my files from the webview in Android 11.

@brunoalex are you able to access https://localhost/data/user/0/events.staff.booking.app/cache/FB_IMG_1637505177709.jpg in your JavaScript?

@Maheshvy
Copy link

Maheshvy commented Nov 29, 2021

#please let me know I'm also facing same issue for android 11 device (Cordova android@10 ).
code :

window.resolveLocalFileSystemURL(cordova.file.externalRootDirectory, function(dir) {
dir.getFile("test.pdf", {create:true}, function(file) {
file.createWriter(function(fileWriter) {
fileWriter.write("byteArrays data");
alert('Message', 'File Downloaded Successfully');
}, function(){
alert('Error!', "Unable to save the file");
});
},function(e){
alert('Error!', e);
});
},function(e){
console.log(e);
});

https://stackoverflow.com/questions/70110433/cordova-file-externalrootdirectory-not-working-in-android-11-devices

@brunoalex
Copy link

@alex-steinberg i can acess the file, but the funtions i used to upload the file after selecting from the galery to the server arent working in sdk30.. "ft.upload(imageURI, server, function(r)" ...

@danicarla
Copy link

@alex-steinberg i can acess the file, but the funtions i used to upload the file after selecting from the galery to the server arent working in sdk30.. "ft.upload(imageURI, server, function(r)" ...

I have a similar problem.. my app downloads and unzips images in ///storage/emulated/0/Android/data/myappid/files/ but the app cannot access the images by javascript or by code ex. <img src="///storage/emulated/0/Android/data/myappid/files/image.png">

looks very confused

@siddharthaPatil
Copy link

we are facing a similar issue where existing cordova implementation to read a file from downloads folder is working but not in android 11. Have tried all possible changes suggested to use fileentry object to get reference and then copy to a location where application has access to but read itself is failing. Cordova version used is 9.x and cordova-file-plugin has been used to read file. Required feature is to allow user to select a file from device and upload it. Upload to server is in base64 format. Kindly advise

@alex-steinberg - can you please share your code snippet and clarify whether you are using cordova 9 or cordova 10?

@alex-steinberg - We have set targetSdkVersion to 30 in config.xml file and created build using platform 9.1.0.

Below code works fine in lower android version (till android 10). In android 11 while reading a pdf file from download loaction, we get error as GET file:///storage/emulated/0/Download/Test.pdf net::ERR_ACCESS_DENIED

this.fileChooser.open()
.then(uri => {
let fileName = null;
let fileExtension = null;
let fileType = null;
let isvalid = true;
this.filePath.resolveNativePath(uri)
.then(filePath => {
return this.base64EncodeFile(filePath);
}).then((base64Data: any)=>{
console.log(base64Data);
})
.catch(err => console.log(err));
})
.catch(e => console.log(e));
}

base64EncodeFile(src){
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.onload = function () {
let reader = new FileReader();
reader.onloadend = function () {
let result = {
base64: reader.result,
size: xhr.response.size,
type: xhr.response.type
}
resolve(result);
}
reader.readAsDataURL(xhr.response);
};
xhr.open('GET', src);
xhr.responseType = 'blob';
xhr.send();
});
}

We have also tried below code

To copy from download folder to internal storage. It gives {"code":1,"message":"NOT_FOUND_ERR"} error

pathsrc : file:///storage/emulated/0/Download/Test.pdf

fileName : Test.pdf

this.file.copyFile(pathsrc, fileName, this.file.dataDirectory, fileName).
then((result) => { console.log('file copied :' + result);
}).catch(err => console.log("copied err "+ JSON.stringify(err)));;

Below code is using copyTo method, it gives error as FileError {code: 1}

window.resolveLocalFileSystemURL(pathsrc, function (fileSystem) {
fileSystem.getFile(fileName, {
create: false, // try true first to make sure that there are no other problems around
exclusive: false
}, function (fileEntry) {
window.resolveLocalFileSystemURL(window.cordova.file.externalDataDirectory , function (newFileEntry) {
fileEntry.copyTo(newFileEntry, fileName, function (result) {
console.log("save successfully:", result);
}, function (err) {
console.log("err-fileEntry.copyTo: ",err);
});
}, function (err) {
console.log("err-window.resolveLocalFileSystemURL: ",err);
});
}, function (err) {
console.log("err-fileSystem.getFile: ",err);
});
}, function (err) {
console.log("err-resolveLocalFileSystemURL: ",err);
});

@jsBiztech
Copy link

I am trying to write file using following code:

this.file.checkDir(this.directorypath, 'AppFolder').then((dirExists) => {
        this.file.writeFile(this.downloadDirectory, update_file + '.'+saveExtention, fileData, { replace: true }).then((fileEntry) => {
          let mimeTestType = mimeType;
          let path = fileEntry.nativeURL
          this.openingFile(path, mimeTestType)
          resolve(true)
        }).catch((ex) => {
          resolve(true)
            console.error('Error')
        })
      }, (response) => {
        this.file.createDir(this.directorypath, 'AppFolder', true).then((response) => {
          this.file.writeFile(this.downloadDirectory, update_file + '.'+saveExtention, fileData, { replace: true }).then((fileEntry) => {
            let mimeTestType = mimeType;
            let path = fileEntry.nativeURL
            this.openingFile(path, mimeTestType)
            resolve(true)
          }).catch((ex) => {
            resolve(true)
            console.error('Error')
          })
        })
      })
      });

This works till android 10 but not in android 11, it throws error.

@alex-steinberg
Copy link

This simple example app should shed some light on some of the issues raised here. At the very least, it would make a good place to continue the conversation around using files on Android 11 Cordova apps since, as has been mentioned, the problems experienced here aren't caused by the cordova-plugin-file plugin. cc @siddharthaPatil @danicarla @brunoalex

@JaosnHsieh
Copy link

With cordova@10.0.0 and cordova-plugin-file@6.0.2, tested on android version from 12 to 8 on emulator, all version working.

config.xml

<widget ..... xmlns:android="http://schemas.android.com/apk/res/android">

...

        <edit-config file="app/src/main/AndroidManifest.xml" mode="merge" target="/manifest/application">
            <application android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true"/>
        </edit-config>
...

window.resolveLocalFileSystemURL, DirectoryEntry.getFile(....) together with fileEntry.createWriter can write to xx/Download/yyy successfully on every android version.

Without android:requestLegacyExternalStorage="true", API 29 ( android 10 ) would fail to write, other version still work.

@nailnafir
Copy link

nailnafir commented Nov 29, 2022

Add code below to AndroidManifest.xml, and you can write file to storage root directory like cordova.file.externalRootDirectory:

<manifest xmlns:tools="http://schemas.android.com/tools">
     ...
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
     <application android:requestLegacyExternalStorage="true">
     ...
</manifest>

You can use this method:

var folderpath = cordova.file.externalRootDirectory + "Download";
  window.resolveLocalFileSystemURL(folderpath, function (dir) {
    dir.getFile(fileName, { create: true, exclusive: false }, function (fileEntry) {
      fileEntry.createWriter(function (writer) {
        writer.onwrite = function (writing) {
          console.log("WRITING...");
        };
        writer.write(fileData);
        console.log("SUCCESS");
      }, function () {
        console.log("FAIL");
      });
    });
  });

Tested on Android 11

@breautek
Copy link
Contributor

Add code below to AndroidManifest.xml, and you can write file to storage root directory like cordova.file.externalRootDirectory:

<manifest xmlns:tools="http://schemas.android.com/tools">
     ...
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
     <application android:requestLegacyExternalStorage="true">
     ...
</manifest>

Tested on Android 11

This isn't standard behaviour or it works because the build isn't targeting API 30 or later as the Android docs explicitly states WRITE_EXTERNAL_STORAGE when targeting API 30 / Android 11 or later. Additionally when targeting API 30, scoped storage is enforced and requestLegacyExternalStorage is ignored.

@jfoclpf
Copy link

jfoclpf commented Jan 17, 2023

This is becoming a bit messy :)

From updating from SDK v30 upwards the APP breaks exactly by merely reading the file system (cordova v11 and cordova-android v10.1.2)

jquery.min.js:2 Not allowed to load local resource: file:///android_asset/www/json/anomalies.json
$.getJSON(cordova.file.applicationDirectory + 'www/json/anomalies.json', function (data) {
   console.log(data)
})

1 - @breautek just confirm me please, if I remember correctly the issue regarding the transition from SDK 29 upwards was merely related with writing content to the device file system, right?

2 - @alex-steinberg mentions replacing file:// by http://localhost. Does this work? If yes, shouldn't cordova.file.applicationDirectory be updated accordingly?

My package.json:

{
  "name": "in-my-district",
  "displayName": "In my District!",
  "version": "1.2.25",
  "description": "Report to your municipality certain irregularities in your neighborhood or district",
  "homepage": "https://nomeubairro.app/",
  "android-version-code": 10225,
  "scripts": {
    "test": "standard && node test/convertHbsToHtml.js && html-validate test/www/index.html && printf '\\n\\nTest OK\\n\\n'",
    "version": "cordova-set-version --version ${npm_package_version} && git add config.xml && node scripts/setAndroidVersionCode.js -v ${npm_package_version} && git add package.json",
    "push-version": "git push && git push origin v${npm_package_version}",
    "build-release-apk": "npm test && scripts/buildReleaseAPK.sh",
    "build-release-aab": "npm test && scripts/buildReleaseAAB.sh",
    "run-release-on-device": "scripts/runReleaseOnDevice.sh",
    "run-debug-on-device": "adb get-state 1>/dev/null 2>&1 && echo 'DEVICE ATTACHED' || { echo 'No device attached'; exit 1; } && cordova run android --device --debug",
    "regenerate-png-files": "./res/icon/generateIcons.sh && ./res/screen/generateScreens.sh && cp res/icon/android/512.png www/img/logo.png && cp res/icon/android/512.png ../fastlane/metadata/android/en-US/images/icon.png && git add res/* www/img/logo.png ../fastlane/metadata/android/en-US/images/icon.png && git commit -m 'PNGs regenerated'"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/jfoclpf/in-my-district.git"
  },
  "author": "João Pimentel Ferreira",
  "license": "GPL-3.0",
  "dependencies": {
    "@fortawesome/fontawesome-free": "6.2.1",
    "async": "^3.2.4",
    "bootstrap": "^5.2.3",
    "cordova": "^11.1.0",
    "cordova-browser": "^6.0.0",
    "cordova-import-npm": "^1.0.35",
    "cordova-pdf-generator": "^2.1.1",
    "cordova-plugin-cache-clear": "^1.3.8",
    "cordova-plugin-device": "^2.1.0",
    "cordova-plugin-geolocation": "^4.1.0",
    "cordova-plugin-inappbrowser": "^5.0.0",
    "cordova-plugin-is-debug": "^1.0.0",
    "cordova-plugin-screen-orientation": "^3.0.2",
    "cordova-plugin-simple-image-resizer": "0.2.0",
    "cordova-plugin-splashscreen": "^6.0.2",
    "cordova-plugin-statusbar": "^3.0.0",
    "cordova-plugin-whitelist": "^1.3.4",
    "cordova-set-version": "^13.0.1",
    "crypto-js": "^4.1.1",
    "es6-promise-plugin": "^4.2.2",
    "exif-js": "^2.3.0",
    "express-handlebars": "^6.0.6",
    "fs": "0.0.1-security",
    "jAlert": "^4.9.1",
    "jquery": "^3.6.3",
    "leaflet": "^1.9.3",
    "leaflet-image": "^0.4.0",
    "leaflet.markercluster": "^1.5.3",
    "path": "^0.12.7",
    "whitelist": "^1.0.2"
  },
  "cordova": {
    "plugins": {
      "cordova-plugin-geolocation": {
        "GPS_REQUIRED": "true"
      },
      "cordova-plugin-email-composer": {
        "ANDROID_SUPPORT_V4_VERSION": "27.+"
      },
      "cordova-plugin-statusbar": {},
      "cordova-plugin-screen-orientation": {},
      "cordova-plugin-device": {},
      "cordova-plugin-whitelist": {},
      "cordova-pdf-generator": {},
      "cordova-plugin-splashscreen": {},
      "cordova-plugin-inappbrowser": {},
      "cordova-plugin-is-debug": {},
      "cordova-plugin-androidx-adapter": {},
      "cordova-plugin-file": {
        "ANDROIDX_WEBKIT_VERSION": "1.4.0"
      },
      "cordova-plugin-camera": {
        "ANDROID_SUPPORT_V4_VERSION": "27.+",
        "ANDROIDX_CORE_VERSION": "1.6.+"
      },
      "cordova-plugin-network-information": {},
      "cordova-plugin-app-version": {},
      "@globules-io/cordova-plugin-ios-xhr": {},
      "cordova-plugin-simple-image-resizer": {
        "ANDROID_EXIFINTERFACES_VERSION": "27.+"
      }
    },
    "platforms": [
      "ios",
      "android"
    ]
  },
  "devDependencies": {
    "@globules-io/cordova-plugin-ios-xhr": "github:globules-io/cordova-plugin-ios-xhr",
    "command-line-args": "^5.2.1",
    "cordova-android": "^10.1.2",
    "cordova-plugin-androidx-adapter": "^1.1.3",
    "cordova-plugin-app-version": "^0.1.14",
    "cordova-plugin-camera": "^6.0.0",
    "cordova-plugin-email-composer": "github:jfoclpf/cordova-plugin-email-composer#pr-Fix_361-Android_11_support",
    "cordova-plugin-file": "^7.0.0",
    "cordova-plugin-network-information": "^3.0.0",
    "fs-extra": "^11.1.0",
    "handlebars": "^4.7.7",
    "html-minifier": "^4.0.0",
    "html-validate": "^7.13.1",
    "npm-check-updates": "^16.6.2",
    "semver": "^7.3.8",
    "standard": "^17.0.0",
    "uglify-js": "^3.17.4",
    "uglifycss": "0.0.29",
    "walk": "^2.3.15",
    "xml2js": "^0.4.23"
  },
  "standard": {
    "ignore": [
      "www/js/res/*",
      "test/www/*",
      "platforms/",
      "plugins/"
    ]
  }
}

config.xml:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<widget id="com.in.my.district" version="1.2.25" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
  <name>No meu Bairro!</name>
  <description>
    Comunique ao seu município anomalias no seu bairro, como buracos na calçada ou lixo por recolher
  </description>
  <author email="joao.pimentel.ferreira@gmail.com" href="https://www.joaopimentel.com/">
    João Pimentel Ferreira
  </author>
  <content src="index.html"/>
  <icon height="512" src="res/icon/universal/512.png" width="512"/>
  <icon density="xhdpi" height="196" src="res/icon/universal/196.png" width="196"/>
  <icon density="xxxhdpi" height="192" src="res/icon/universal/192.png" width="192"/>
  <icon density="xxhdpi" height="144" src="res/icon/universal/144.png" width="144"/>
  <icon density="hdpi" height="72" src="res/icon/universal/72.png" width="72"/>
  <icon density="mdpi" height="48" src="res/icon/universal/48.png" width="48"/>
  <access origin="*"/>
  <allow-navigation href="*"/>
  <allow-intent href="http://*/*"/>
  <allow-intent href="https://*/*"/>
  <preference name="OverrideUserAgent" value="APP/com.in.my.district"/>
  <preference name="windows-target-version" value="10.0"/>
  <preference name="windows-phone-target-version" value="10.0"/>
  <preference name="iosExtraFilesystems" value="library,library-nosync,documents,documents-nosync,cache,bundle"/>
  <preference name="AndroidExtraFilesystems" value="files,files-external,documents,sdcard,cache,cache-external,assets"/>
  <preference name="StatusBarOverlaysWebView" value="false"/>
  <preference name="StatusBarBackgroundColor" value="#FFFFFF"/>
  <preference name="StatusBarStyle" value="blacktranslucent"/>
  <preference name="AndroidXEnabled" value="true"/>
  <hook src="hooks/generateJsFiles.js" type="before_prepare"/>
  <hook src="hooks/convertHbsToHtml.js" type="after_prepare"/>
  <hook src="hooks/minifyFiles.js" type="after_prepare"/>
  <hook src="node_modules/cordova-import-npm/scripts/importNpmPackages.js" type="before_prepare"/>
  <platform name="android">
    <preference name="android-minSdkVersion" value="31"/>
    <preference name="android-targetSdkVersion" value="33"/>
    <preference name="AndroidPersistentFileLocation" value="Compatibility"/>
    <allow-intent href="market:*"/>
    <icon height="512" src="res/icon/android/512.png" width="512"/>
    <icon density="xhdpi" height="192" src="res/icon/android/192.png" width="192"/>
    <icon density="xxxhdpi" height="192" src="res/icon/android/192.png" width="192"/>
    <icon density="xxhdpi" height="144" src="res/icon/android/144.png" width="144"/>
    <icon density="hdpi" height="72" src="res/icon/android/72.png" width="72"/>
    <icon density="mdpi" height="48" src="res/icon/android/48.png" width="48"/>
    <splash density="hdpi" src="res/screen/android/screen-hdpi-portrait.png"/>
    <splash density="port-hdpi" src="res/screen/android/screen-hdpi-portrait.png"/>
    <splash density="ldpi" src="res/screen/android/screen-ldpi-portrait.png"/>
    <splash density="port-ldpi" src="res/screen/android/screen-ldpi-portrait.png"/>
    <splash density="mdpi" src="res/screen/android/screen-mdpi-portrait.png"/>
    <splash density="port-mdpi" src="res/screen/android/screen-mdpi-portrait.png"/>
    <splash density="xhdpi" src="res/screen/android/screen-xhdpi-portrait.png"/>
    <splash density="port-xhdpi" src="res/screen/android/screen-xhdpi-portrait.png"/>
  </platform>
  <platform name="ios">
    <allow-intent href="itms:*"/>
    <allow-intent href="itms-apps:*"/>
    <icon src="res/icon/ios/16.png" width="16" height="16"/>
    <icon src="res/icon/ios/20.png" width="20" height="20"/>
    <icon src="res/icon/ios/29.png" width="29" height="29"/>
    <icon src="res/icon/ios/32.png" width="32" height="32"/>
    <icon src="res/icon/ios/40.png" width="40" height="40"/>
    <icon src="res/icon/ios/48.png" width="48" height="48"/>
    <icon src="res/icon/ios/50.png" width="50" height="50"/>
    <icon src="res/icon/ios/55.png" width="55" height="55"/>
    <icon src="res/icon/ios/57.png" width="57" height="57"/>
    <icon src="res/icon/ios/58.png" width="58" height="58"/>
    <icon src="res/icon/ios/60.png" width="60" height="60"/>
    <icon src="res/icon/ios/64.png" width="64" height="64"/>
    <icon src="res/icon/ios/72.png" width="72" height="72"/>
    <icon src="res/icon/ios/76.png" width="76" height="76"/>
    <icon src="res/icon/ios/80.png" width="80" height="80"/>
    <icon src="res/icon/ios/87.png" width="87" height="87"/>
    <icon src="res/icon/ios/88.png" width="88" height="88"/>
    <icon src="res/icon/ios/100.png" width="100" height="100"/>
    <icon src="res/icon/ios/114.png" width="114" height="114"/>
    <icon src="res/icon/ios/120.png" width="120" height="120"/>
    <icon src="res/icon/ios/128.png" width="128" height="128"/>
    <icon src="res/icon/ios/144.png" width="144" height="144"/>
    <icon src="res/icon/ios/152.png" width="152" height="152"/>
    <icon src="res/icon/ios/167.png" width="167" height="167"/>
    <icon src="res/icon/ios/172.png" width="172" height="172"/>
    <icon src="res/icon/ios/180.png" width="180" height="180"/>
    <icon src="res/icon/ios/196.png" width="196" height="196"/>
    <icon src="res/icon/ios/216.png" width="216" height="216"/>
    <icon src="res/icon/ios/256.png" width="256" height="256"/>
    <icon src="res/icon/ios/512.png" width="512" height="512"/>
    <icon src="res/icon/ios/1024.png" width="1024" height="1024"/>
    <preference name="iosPersistentFileLocation" value="Library"/>
    <preference name="NativeXHRLogging" value="full"/>
    <preference name="AllowUntrustedCerts" value="true"/>
    <preference name="InterceptRemoteRequests" value="all"/>
    <preference name="CustomUserAgent" value="APP/com.in.my.district"/>
    <preference name="allowFileAccessFromFileURLs" value="true"/>
    <preference name="allowUniversalAccessFromFileURLs" value="true"/>
    <edit-config target="NSLocationWhenInUseUsageDescription" file="*-Info.plist" mode="merge">
      <string>Necessita da sua localização para indicar o endereço da ocorrência a ser enviada ao seu município</string>
    </edit-config>
    <edit-config target="NSLocationAlwaysAndWhenInUseUsageDescription" file="*-Info.plist" mode="merge">
      <string>Necessita da sua localização para indicar o endereço da ocorrência a ser enviada ao seu município</string>
    </edit-config>
    <edit-config target="NSLocationAlwaysUsageDescription" file="*-Info.plist" mode="merge">
      <string>Necessita da sua localização para indicar o endereço da ocorrência a ser enviada ao seu município</string>
    </edit-config>
    <edit-config target="NSLocationAlwaysAndWhenInUseUsageDescription" file="*-Info.plist" mode="merge">
      <string>Necessita da sua localização para indicar o endereço da ocorrência a ser enviada ao seu município</string>
    </edit-config>
    <edit-config target="NSCameraUsageDescription" file="*-Info.plist" mode="merge">
      <string>Necessita da acesso à câmera para tirar fotos às anomalias</string>
    </edit-config>
    <edit-config target="NSPhotoLibraryUsageDescription" file="*-Info.plist" mode="merge">
      <string>Necessita da acesso à galeria, caso você já tenha tirado anteriormente a foto à ocorrência</string>
    </edit-config>
    <edit-config target="NSPhotoLibraryAddUsageDescription" file="*-Info.plist" mode="merge">
      <string>Necessita da acesso à galeria para guardar algumas fotos, mesmo que temporariamente</string>
    </edit-config>
    <edit-config target="UIFileSharingEnabled" file="*-Info.plist" mode="merge">
      <true/>
    </edit-config>
    <edit-config target="LSSupportsOpeningDocumentsInPlace" file="*-Info.plist" mode="merge">
      <true/>
    </edit-config>
    <edit-config target="UISupportsDocumentBrowser" file="*-Info.plist" mode="merge">
      <true/>
    </edit-config>
  </platform>
</widget>

@jfoclpf
Copy link

jfoclpf commented Jan 17, 2023

@breautek I was reading now your post upwards, too many characters to go through :) and reading also your blog post

Ok, file:// is supposed to be insecure, but my previous question remains: is not possible to adapt the keys inside cordova.file to https://localhost? What am I missing?

@breautek
Copy link
Contributor

@jfoclpf You're using a jquery API that presumably uses an XHR request to fetch data from a file:// url. This API is not related to the file plugin at all.

The webview doesn't have direct access to file:// unless if AndroidInsecureFileModeEnabled preference is enabled. Enabling this will make Cordova uses the same filesystem based strategy for the webview, which is the same behaviour in cordova-android@9 and older. If AndroidInsecureFileModeEnabled is off (the default value), then the webview is handled through a WebViewAssetLoader, which is a way of treating your web app as if it was being loaded from a secure https site, enabling features that requires a secure context. But this also means that the webview and standard browser features don't have direct access to the file system.

While the browser itself doesn't have direct access to the FS, native plugins still does, so if you used the actual cordova plugin API, cordova.file.applicationDirectory + 'www/json/anomalies.json will be readable.

mentions replacing file:// by http://localhost. Does this work? If yes, shouldn't cordova.file.applicationDirectory be updated accordingly?

http://localhost points to the asset directory, which is effectively the equivalent of file://android_asset/www. The http:// version of the path will work in the webview and other browser APIs. But your real issue is that you're mixing APIs together. No, cordova.file.applicationDirectory shouldn't be updated because that's part of the File plugin API, which still uses the file:// protocol, as that is what used on the native side. The cordova.file.applicationDirectory + 'www/json/anomalies.json' path will resolve into a proper path that the file plugin can read.

Here's an untested example (may have syntax errors or a function name might be wrong, going off memory...)

resolveLocalFilesystemURL(cordova.file.applicationDirectory + 'www/json/anomalies.json', (fileEntry) => {
    fileEntry.file((file) => {
        let reader = new FileReader();
        reader.onloadend = () => {
            console.log('JSON String', reader.result);
        };

        reader.readAsText(file);
    });
}, (error) => {
    console.error('resolve error', error);
});

If you're using the WebViewAssetLoader, the latest version of this plugin as a fileEntry.toURL() API that also produces a http(s):// friendly URLs for standard browser APIs to consume.

@Helaers
Copy link

Helaers commented May 16, 2023

Came to this issue repeatedly when searching for a way to fix opening files on Android that broke since skd 29.
Updated my util to make it work for devices running Android 11 with sdk 33.

export async function openFile(remoteUrl: string, fileName: string, mimeType: string) {
  if (!isCordova(window)) {
    window.open(remoteUrl, '_blank');
    return;
  }

  const transfer = FileTransfer.create();
  const extension = mime.extension(mimeType);
  const devicePath = `${File.dataDirectory}${fileName}.${extension}`;

  await transfer.download(remoteUrl, devicePath);
  const fileEntry = await File.resolveLocalFilesystemUrl(devicePath);
  await FileOpener.open(fileEntry.nativeURL, mimeType);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
android-scoped-storage Issues relating to Android's Scoped storage changed introduced in API 29/30 help wanted Extra attention is needed platform: android
Projects
None yet
Development

Successfully merging a pull request may close this issue.