The Android manifest mistakes that get Play builds rejected
Almost every Android release problem that isn’t a crash is a line in
AndroidManifest.xml. The manifest is the first thing Play’s own tooling reads, the
thing your permissions are derived from, and — because React Native merges yours together with
every library’s — the file nobody actually looks at until something breaks.
These are the six attributes that show up again and again in rejections, install failures and pre-launch crashes. All six are checkable before you upload.
1. android:exported missing on a component with an intent filter
Since Android 12 (API 31), any activity, service or receiver with an
<intent-filter> must declare android:exported explicitly. Omit it
and the build fails to install on API 31+, which surfaces as an instant crash in pre-launch
reports rather than a clean error.
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Deep-link filters deserve the same care: if a filter is only for your own app://
scheme, exported="false" is usually the correct answer, and it’s the safer default.
2. android:debuggable="true" in a release
Shipping a debug variant is the classic “Play says my app is debuggable” rejection. It usually
comes from building assembleDebug by hand instead of a release variant, or from a
stale debug applicationId surviving in a merged manifest. The merged manifest is the
only place to verify it — it’s the file that actually gets packaged.
3. usesCleartextTraffic="true"
A blanket cleartext allowance means the app may talk HTTP. Play flags it, and it contradicts the “data encrypted in transit” box you ticked in the Data safety form. If a single dev host genuinely needs HTTP, use a network security config scoped to that domain instead of a global flag:
<application android:networkSecurityConfig="@xml/network_security_config">
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
</domain-config>
</network-security-config>
4. allowBackup="true"
Not a rejection on its own, but it lets anyone with adb extract app data from a signed build,
which is awkward to explain in a privacy review. For apps holding credentials or health data,
android:allowBackup="false" (plus a backup rules file if you want partial backup) is
the defensible choice.
5. targetSdkVersion falling behind
Play requires new apps and updates to target an API level no more than one year older than the latest major Android release, with the deadline landing each August. Miss it and every upload is rejected until you bump. It’s boring and it lands annually without warning, which is exactly why it should be a CI check rather than a calendar reminder.
In Expo, the manifest value comes from your build properties, so the fix is one plugin entry:
{
"expo": {
"plugins": [
["expo-build-properties", {
"android": { "compileSdkVersion": 35, "targetSdkVersion": 36 }
}]
]
}
}
Note the asymmetry: targetSdkVersion may be newer than
compileSdkVersion (AGP allows it), but not the reverse. If
expo-modules-core fails to compile after a bump, the compile SDK is the number to
walk back, not the target.
6. Permissions that don’t match your Data safety form
The manifest is the source of truth for what you request, so a mismatch with the Data safety form is trivially detectable — by Play and by anyone reading your AAB. Two failure modes:
- Inherited permissions. Libraries add them for you: ad SDKs add
com.google.android.gms.permission.AD_ID, analytics addsINTERNETandACCESS_NETWORK_STATE, map SDKs add location. They must be declared in Data safety even though you never typed them. - Restricted permissions.
QUERY_ALL_PACKAGES,MANAGE_EXTERNAL_STORAGE,ACCESS_BACKGROUND_LOCATIONandREAD_SMSrequire a declaration form and a use case Play accepts. Pulling one in by accident means a rejection — Expo lets you strip a library’s permission from the final manifest:
{
"expo": {
"android": {
"blockedPermissions": ["com.google.android.gms.permission.AD_ID"]
}
}
}
Do not use blockedPermissions to paper over a permission you actually use,
though. Play’s automated check reads the same manifest you do.
Read the merged manifest, not the one you wrote
Your source manifest is a wish; the merged manifest is what ships. Find it after a release build:
# path varies a little by AGP version
android/app/build/intermediates/merged_manifests/release/*/AndroidManifest.xml
To inspect the AAB itself, bundletool dump manifest --bundle app-release.aab works
(it needs Java), and aapt2 dump xmltree does the same for a binary APK manifest.
If you’d rather not install the Android SDK on every machine and CI runner, that’s what
manifest-check is for — it parses the AAB’s protobuf manifest (or a
text one) with no Java, no SDK, and exits non-zero on the two blocking findings:
npx @njweber/android-manifest-check ./app-release.aab
manifest-check — Android manifest audit
artifact: ./app-release.aab
✓ manifest <manifest> parsed — com.example.app
✓ debuggable not debuggable
✓ cleartext no blanket cleartext traffic
! allowBackup allowBackup="true" — app data can be extracted via adb backup (consider false)
! activity:.LegacyActivity:exported is exported with no <intent-filter> — make sure that is intended
✓ targetSdkVersion targetSdkVersion 36
✓ permissions 15 permission(s) declared, 7 sensitive — each needs a Play data-safety declaration
That’s the whole argument for checking the manifest in CI: these failures are all mechanical, and every one of them is cheaper to catch on the pull request than in a Play review. Pair it with the 16 KB page-size check on the same artifact and the Android side of a release stops being a guessing game. The rest of the recurring reasons live in the rejection checklist.
Get new posts
One email when a new post ships. No spam.
The release checks above, plus the build gates and local iOS/Android release scripts, are packaged in the Expo Production Starter.