Duplicate WRITE_EXTERNAL_STORAGE permissions in AndroidManifest using Cordova 12 for Android 13
Asked Answered
F

1

8

Introducing app using Cordova 12 which requires target SDK Android 13 / API 33. My app depends on the following plugins (among many others)...

cordova-plugin-camera
cordova-plugin-media-capture

Both plugins insert permissions in AndroidManifest.xml. After upgrading to Cordova 12 and setting targetSdk to 33, the build fails trying to merge permissions...

> Task :app:processReleaseMainManifest FAILED
/Users/jmelvin/dev/sizzlescene/repos/mobile/platforms/android/app/src/main/AndroidManifest.xml:47:5-108 Error:
        Element uses-permission#android.permission.WRITE_EXTERNAL_STORAGE at AndroidManifest.xml:47:5-108 duplicated with element declared at AndroidManifest.xml:26:5-81

Here are the properties inserted by the cordova-plugin-camera plugin...

11a12,14
>         <provider android:authorities="${applicationId}.cordova.plugin.camera.provider" android:exported="false" android:grantUriPermissions="true" android:name="org.apache.cordova.camera.FileProvider">
>             <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/camera_provider_paths" />
>         </provider>
22a26,41
>     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
>     <queries>
>         <intent>
>             <action android:name="android.media.action.IMAGE_CAPTURE" />
>         </intent>
>         <intent>
>             <action android:name="android.intent.action.GET_CONTENT" />
>         </intent>
>         <intent>
>             <action android:name="android.intent.action.PICK" />
>         </intent>
>         <intent>
>             <action android:name="com.android.camera.action.CROP" />
>             <data android:mimeType="image/*" android:scheme="content" />
>         </intent>
>     </queries>

Here are the properties added by cordova-plugin-media-capture plugin...

22a23,28
>     <uses-permission android:name="android.permission.RECORD_AUDIO" />
>     <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
>     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
>     <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
>     <uses-permission android:maxSdkVersion="32" android:name="android.permission.READ_EXTERNAL_STORAGE" />
>     <uses-permission android:maxSdkVersion="32" android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

And lastly, when both plugins are included in the build...

11a12,14
>         <provider android:authorities="${applicationId}.cordova.plugin.camera.provider" android:exported="false" android:grantUriPermissions="true" android:name="org.apache.cordova.camera.FileProvider">
>             <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/camera_provider_paths" />
>         </provider>
22a26,47
>     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
>     <queries>
>         <intent>
>             <action android:name="android.media.action.IMAGE_CAPTURE" />
>         </intent>
>         <intent>
>             <action android:name="android.intent.action.GET_CONTENT" />
>         </intent>
>         <intent>
>             <action android:name="android.intent.action.PICK" />
>         </intent>
>         <intent>
>             <action android:name="com.android.camera.action.CROP" />
>             <data android:mimeType="image/*" android:scheme="content" />
>         </intent>
>     </queries>
>     <uses-permission android:name="android.permission.RECORD_AUDIO" />
>     <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
>     <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
>     <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
>     <uses-permission android:maxSdkVersion="32" android:name="android.permission.READ_EXTERNAL_STORAGE" />
>     <uses-permission android:maxSdkVersion="32" android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

The permission request for WRITE_EXTERNAL_STORAGE are not exactly the same. The media-capture comes with an SDK qualifier...

camera:  android:name="android.permission.WRITE_EXTERNAL_STORAGE"
capture: android:maxSdkVersion="32" android:name="android.permission.WRITE_EXTERNAL_STORAGE"

Manually removing the capture plugin entry does allow the build to complete successfully. However, I'm not sure the result is functionally correct.

Question: is this a bug in the manifest merge code or are there workarounds to circumvent the collision and remain functionally the same?

Furnish answered 9/9, 2023 at 23:49 Comment(1)
Additional info... The cordova-plugin-media-capture plugin released a new version 5 some three weeks ago to add the minSdkVersion. See cordova.apache.org/news/2023/08/18/…Furnish
G
21

Question: is this a bug in the manifest merge code or are there workarounds to circumvent the collision and remain functionally the same?

It's not a bug, normally when authoring native projects, if two libraries attempt to declare the same permission with different configurations, it will result similar error.

What is a bug is the fact Cordova's <edit-config>/<config-file> directives will result in conflicts and/or duplicate directives in the end-result, which has been a bug for several years.

A hook however can be used to correct the project after_prepare

Add stripExtraWriteExternalStoragePerm.js file in your hooks/ directory with the script:

const FS = require('fs');
const Path = require('path');

let path = Path.resolve('platforms/android/app/src/main/AndroidManifest.xml');

let manifest = FS.readFileSync(path, {
    encoding: 'utf-8'
});

// Strips ALL occurrences of <uses-permission android:name="androoid.permission.WRITE_EXTERNAL_STORAGE" />
// If you have several conflicts (of different maxSDKVersion, or in different formats) then the regex
// may need to be adjusted, or repeated for each format.
manifest = manifest.replace(/^(\s)+<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" \/>$/gm, '');

FS.writeFileSync(path, manifest);

In your config.xml add:

<widget ...>
    ...
    <platform name="android">
        ...
        <hook type="after_prepare" src="hooks/stripExtraWriteExternalStoragePerm.js" />
    </platform>
</widget>

If you already have a <platform> block for android, then you should add it to your existing <platform> block.

As the JS comment states, the conflict may occur with any combination of plugins, depending on how they declare WRITE_EXTERNAL_STORAGE (e.g. with or without the maxSDKVersion attribute, or with a different maxSDKVersion attribute specified). If that's the case, you may need to add additional manifest = manifest.replace(...) lines for your project.

I originally wrote the hook script for work and so the code is released with the copyright of Total Pave Inc. licensed under the Apache License: https://gist.github.com/breautek/bd157b8598f9a816f2ec0d45e3d932c8

Garbo answered 11/9, 2023 at 0:2 Comment(2)
thanks for the great solution. I had implemented the post-prepare solution using sed script, but your approach is preferred and may apply to other workarounds we've implemented in the past.Furnish
Using sed is probably a valid approach too, which is effectively what the hook does, but using NodeJS. Obviously it's easier to write logic into the hook though.Garbo

© 2022 - 2024 — McMap. All rights reserved.