Search code examples
cordovaandroid-manifestandroid-permissionscordova-plugin-camera

Duplicate WRITE_EXTERNAL_STORAGE permissions in AndroidManifest using Cordova 12 for Android 13


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?


Solution

  • 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