skip to content

In a Flutter Android release, why can Google Play flag a permission your AndroidManifest.xml never declares, and how do you remove it?

level: middleimportance: should knowfreq 42%

answer

  1. manifest merger at build time
  2. each plugin ships its own manifest
  3. camera plugin brings RECORD_AUDIO
  4. Merged Manifest view or the bundle
  5. tools:node remove on uses-permission

basics

~20 s

Gradle's manifest merger folds every plugin's AndroidManifest.xml into the app's, so plugin permissions ship in your bundle. Inspect the merged manifest, then drop unused ones with tools:node="remove", but only when no code path still needs them.

solid answer

~40 s

Each Flutter plugin is an Android library with its own `android/src/main/AndroidManifest.xml`, and the manifest merger combines all of them with `android/app/src/main/AndroidManifest.xml`. Play checks the **merged** manifest inside the AAB, not the file you edited. The camera plugin's CameraX implementation, for instance, declares `CAMERA`, `RECORD_AUDIO` and `WRITE_EXTERNAL_STORAGE` capped at API 28, so a photo-only app ships a microphone permission it must justify. Find these in Android Studio's Merged Manifest view or the built bundle's manifest, then add a `<uses-permission ... tools:node="remove"/>` entry with the `tools` namespace declared. Remove only what no code path uses: dropping `RECORD_AUDIO` also means creating `CameraController` with `enableAudio: false`, because it defaults to `true`.

code

xml · 12 lines
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission
        android:name="android.permission.RECORD_AUDIO"
        tools:node="remove" />
    <application
        android:label="Parcel Scanner"
        android:name="${applicationName}">
        <!-- activities as generated by flutter create -->
    </application>
</manifest>

go deeper

for a junior

Recall that plugins bring their own Android manifests and that the merged result is what ships and what Play reviews.

for a middle

Explain the manifest merger, how to read the merged manifest, and how tools:node="remove" plus a matching Dart change drops an unused permission.

for a senior

Show you audit the merged manifest per flavor on every plugin upgrade and keep permissions, Data safety answers and runtime code consistent.

for a principal

Weigh plugin choice by the permissions and data it drags in, and make permission review a gate in the dependency-upgrade process.

## Where the extra permission comes from A Flutter plugin that has Android code is an **Android library module**, and every library may carry its own `AndroidManifest.xml` under `android/src/main/`. When you run `flutter build appbundle`, Gradle's **manifest merger** combines the app manifest (`android/app/src/main/AndroidManifest.xml`) with every library manifest into one **merged manifest**, which is what ends up in the AAB. Google Play reads that merged manifest, so it can flag a permission you never typed. Some real examples from current plugin sources: | Plugin (Android implementation) | Permissions its manifest adds | |---|---| | camera (`camera_android_camerax`) | `CAMERA`, `RECORD_AUDIO`, `WRITE_EXTERNAL_STORAGE` with `maxSdkVersion` 28 | | firebase_messaging | `INTERNET`, `WAKE_LOCK`, `ACCESS_NETWORK_STATE`, `POST_NOTIFICATIONS` | | connectivity_plus | `ACCESS_NETWORK_STATE` | | local_auth (`local_auth_android`) | `USE_BIOMETRIC` | A photo-capture screen in a Flutter app therefore ships `RECORD_AUDIO` unless you act. Sensitive permissions draw questions at review and must be reflected in the Play **Data safety** answers. ## How to see what actually ships - Open `android/app/src/main/AndroidManifest.xml` in Android Studio and switch to the **Merged Manifest** tab; each entry is attributed to the library that contributed it. - Or inspect the built bundle itself, for example with `bundletool dump manifest` on the `.aab`, which shows exactly what Play receives. - Check every build flavor you publish, since each variant is merged separately. ## How to remove a permission you do not use The manifest merger honours **merge rule markers** from the `tools` namespace. Declaring the permission in the app manifest with `tools:node="remove"` deletes it from the merged result: 1. Add `xmlns:tools="http://schemas.android.com/tools"` to the `<manifest>` element. 2. Add `<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />`. 3. Rebuild and confirm in the merged manifest that the entry is gone. 4. Make the Dart side agree, so no code path asks for what is no longer declared. That last step is where teams break their own app. The camera plugin's `CameraController` takes `enableAudio`, which **defaults to `true`**; with it on, the Android implementation requests the microphone alongside the camera when the camera is created, and on a device where that is refused it reports `AudioAccessDenied` as a `CameraException`. A permission missing from the manifest can never be granted, so the camera screen fails. Pass `enableAudio: false` when you strip `RECORD_AUDIO`. ## Why Play cares about the merged list Google Play treats some permissions as **sensitive**: microphone, precise location, SMS and call logs, background location and similar. An app that declares one is expected to use it for a feature the user can see and, for some of them, to fill in an extra declaration explaining that use. A reviewer who finds `RECORD_AUDIO` in a photo scanner that never records sound has good reason to ask why. The cheapest answer is not to ship the permission at all. The merged list also drives what you must say elsewhere. The Data safety answers should be consistent with the data that declared permissions give the app access to, and iOS has its own equivalent in Info.plist purpose strings. Keeping the Android manifest honest is the first step in keeping all of those answers honest. ## A related trap: INTERNET in release The Flutter app template's **debug** and **profile** manifests declare `INTERNET` so the tool can talk to the running app; the **main** manifest does not. Many networking plugins add `INTERNET` through the merger, which hides the gap, but an app whose plugins do not will make network calls fine in debug and fail in release. Declare `INTERNET` in the main manifest yourself when the app needs it. ## Judgement - Removing a permission is a statement that no code path needs it; verify with the plugin's source or docs, not a guess. - If a permission is needed, keep it and justify it truthfully rather than hiding it. - Re-check the merged manifest whenever you add or upgrade a plugin, since a new version can bring new entries.

  • You removed RECORD_AUDIO but left the Flutter CameraController on its defaults. What happens on Android?
    `enableAudio` defaults to `true`, so creating the camera requests the microphone along with the camera. A permission missing from the manifest cannot be granted, the request comes back denied, and the Android implementation reports `AudioAccessDenied` as a `CameraException`, so the camera screen fails to open.
  • A Flutter app's network calls work in debug but fail in the release APK. What manifest detail explains it?
    The app template declares `INTERNET` only in the debug and profile manifests, because the tool needs it to talk to the running app. If the main manifest lacks it and no plugin merges it in, the release build has no network permission. Declare `INTERNET` in `android/app/src/main/AndroidManifest.xml`.

saying these in an interview costs you the question

  • Only the permissions in my own AndroidManifest.xml end up in the bundle.
  • Deleting the line from the plugin's manifest in the pub cache is a fine fix.
  • tools:node="remove" works without declaring the tools namespace.
  • Stripping a permission is safe even if the plugin still requests it.
  • Plugins add their Android permissions only at runtime when first called.