Published · 9 min read
The merged manifest is not the XML you pasted
The Manifest Validator parses the text in the box. Gradle builds a different document from your main manifest, the debug or release overlay, product flavors, and every library. A clean report means that text has the elements this script looks for. It does not mean the APK will install.
What the page actually checks
The Manifest Validator runs DOMParser with application/xml on the string you paste. If the parser returns a parsererror element, the page stops and prints that message. Binary AndroidManifest.xml from inside an APK is AXML. Pasting those bytes, or a file that is not text XML, fails here even when aapt can read the same entry.
When the text parses, the script looks at a short list:
- The root element must be
manifest. Anything else is an issue, and the rest of the checks are skipped. packageon that root is required. A missing attribute is an issue. A value that fails the page’s pattern (a Java-style name with at least one dot) is a warning.xmlns:androidis read on themanifestelement only, and only for the Android resource namespace. A declaration on another element does not count. A missing declaration is a warning.- One
applicationelement is required. Missing it is an issue. uses-sdkandandroid:minSdkVersionare warnings when absent. The numbers are not compared with a Play target-API deadline, and they are not looked up in the version table.- Each
uses-permissionneedsandroid:name. The page counts the names it finds. It does not look them up. The permissions decoder is a separate paste. - A launcher is an
activitywhose intent-filter contains bothandroid.intent.action.MAINandandroid.intent.category.LAUNCHER. No such activity is a warning. The warning text already says libraries, TV apps, and auto apps may be fine.
That is the whole checklist. android:exported, android:debuggable, android:allowBackup, queries, android:maxSdkVersion, and tools:node are never read. service, receiver, provider, and activity-alias are not counted. A launcher that lives only on an activity-alias produces the “no launcher” warning even when the alias is a real launcher. Pretty-print indents the text already in the box. It does not merge libraries and it does not insert android:exported.
The document Gradle ships
Android Gradle Plugin merges manifests before it compiles resources. The inputs are at least these:
src/main/AndroidManifest.xml- The build-type file, usually
src/debug/AndroidManifest.xmlorsrc/release/AndroidManifest.xml - Product-flavor manifests, when the module has flavors
- The manifest inside each Android library (AAR) on the compile classpath
Markers in the tools namespace change the merge: tools:node="remove", tools:node="replace", tools:replace, and tools:remove. The validator treats those attributes as ordinary text. A permission element with tools:node="remove" still counts as a permission on this page, because android:name is present. The merger drops that permission from the document that ends up in the APK.
Since Android Gradle Plugin 7, the module namespace in Gradle is the package used for the R class, and applicationId can differ from it. A source manifest may omit the package attribute and still build. This validator then reports “Missing required attribute: manifest@package”, because the attribute is not in the text you pasted. The merged file and aapt dump badging still show a package name. applicationIdSuffix (the usual .debug suffix) changes the id the package manager installs. The validator never reads Gradle, so it cannot show that suffix.
Placeholders such as ${applicationId} are expanded by the build. The validator leaves them as characters. A provider whose android:authorities is still ${applicationId}.fileprovider can look tidy in the box and still be wrong if you paste a file the merger has not processed. The page also never inspects provider elements, so android:exported on a FileProvider is invisible here either way.
The stable place to read the result is Android Studio’s manifest editor. Open the module manifest and choose the Merged Manifest tab. The left side is the document Gradle produced for the selected variant. The right side shows which library or source set contributed a line, and merge errors that the text validator cannot see. The file Gradle wrote is under app/build/intermediates/ after a successful assemble. The folder name has moved between Android Gradle Plugin versions (merged_manifest and merged_manifests both appear in the wild). Search that intermediates tree for AndroidManifest.xml for the variant you are about to ship, or copy from the Merged Manifest tab, which does not depend on the folder name.
The APK’s own manifest is the final check. aapt dump badging your.apk prints the package name, version codes, SDK levels, permissions, debuggable state, and the launchable activity. aapt dump xmltree your.apk AndroidManifest.xml prints the binary tree, including android:exported. apktool decodes that binary XML into text when you do not have the Gradle project. DroidXP does not run aapt or apktool. The job table says which question belongs to which tool.
A paste that passes, and a merge that would not
This is the sample already used on the validator page. It is a text fixture, not a merged release manifest and not an app published here.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.notes">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:label="Notes">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Pasted into the validator, that text reports package com.example.notes, one permission, one activity, launcher yes, and no issues. The activity has an intent-filter and no android:exported. Install on API 31 and higher can still reject it. The report will not say so. Adding android:exported="true" and pasting again changes the install behavior and leaves this page’s summary almost unchanged, because the attribute is unread. The screenshot of that pass is on the validator page.
A merged debug manifest for the same sample shape can grow without any of those new lines living in src/main:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.notes">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<application android:label="Notes" android:debuggable="true">
<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>
<activity android:name="com.example.notes.share.ShareActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
</manifest>
That second document is an illustration of merge output, not a file captured from a Play track. If you paste it here, the summary becomes two permissions and two activities, launcher yes, still no issue. CAMERA is counted and not classified. android:debuggable="true" is ignored, so a debug variant looks as healthy as a release variant. ShareActivity has an intent-filter and no android:exported, which is the API 31 install failure, and this page still has nothing to say about it. The only way the extra activity appears in the report is if you pasted the merged text. Pasting src/main hides the library activity completely.
Six passes that are still the wrong file
- Main manifest, library filter.
src/mainhas a launcher activity withandroid:exported="true". A library adds another activity withSENDand no exported value. The paste of main is clean. The merged file fails installation on Android 12 and newer. Lint’s Merged Manifest tab names the library. This page cannot, because the library XML was never in the box. The attribute itself is covered in exported components after Android 12. tools:node="remove"still counts. You removedCAMERAwith a tools marker. The validator countsCAMERAif the element remains in the paste and hasandroid:name.aapt dump permissionson the release APK is the list that shipped. Decoding the paste in the permissions decoder repeats the same mistake if you paste the pre-merge names.- Debug debuggable.
src/debugsetsandroid:debuggable="true". Main does not mention the attribute. A pass on main says nothing about the APK you just installed from the debug variant.aapt dump badgingprints debuggable on the file you actually built. The publishing checklist includes that confirmation before upload. - Application id suffix. The pasted
packageiscom.example.notes. The debug variant installscom.example.notes.debugbecause ofapplicationIdSuffix. The validator will keep printing the attribute you pasted. Badging prints the id the phone used. Deep-link and ADB commands fail when they target the unsuffixed id. That split is the subject of versionCode, versionName, and applicationId. - Package attribute omitted on purpose. A current module keeps
namespacein Gradle and deletespackagefrom the source manifest. The validator reports a missing package. The build is fine, and the merged manifest has the package. Paste the merged text, or read badging, before you “fix” the source by putting the attribute back. - Queries and package visibility. An app that looks up other packages on API 30 and higher needs a
querieselement (or a narrowly justified broader permission). The validator does not readqueries. A full-looking manifest with noqueriesblock still passes. The empty result on a device is a platform rule, not a parser error.
What to paste, in order
- Build the variant you mean. A debug merged manifest is the wrong document for a release upload.
- Copy from the Merged Manifest tab, or open the merged
AndroidManifest.xmlfor that variant underbuild/intermediates. - Paste that text into the validator. Use the report for well-formedness, the package attribute,
uses-sdk, permission name attributes, and the launcher activity. Stop there. - Read
android:exportedyourself on everyactivity,activity-alias,service, andreceiverthat has an intent-filter, whentargetSdkVersionis 31 or higher. The page will not mark a missing one. - Run
aapt dump badgingon the APK. Compare package, versionCode, versionName, SDK levels, debuggable, and the permission list with what you think you merged. - If you only have the APK, decode with apktool or
aapt dump xmltree. Do not paste the rawAndroidManifest.xmlentry from the ZIP into this page. The APK Analyzer can tell you the entry exists. It does not decode it.
When the tab is the wrong tool
Stay on this page when the question is “is this text well-formed XML, and does it contain a root, a package, an application, and a launcher activity?” Leave when the question is “what will the package manager see?” That answer is the merged manifest plus badging. Leave when the question is “will this install on Android 12?” That answer is android:exported on every filtered component, then an install on API 31 or higher. Leave when the question is “will Play accept these permissions?” The decoder’s label and this page’s permission count are both silent on policy.
If lint and the validator disagree, lint is looking at the merged graph and this page is looking at the string in the box. Believe lint about the graph. Believe the validator about whether that string parses. Do not paste src/main, get zero issues, and call the release checked.
Related reading: the validator, exported components after Android 12, the publishing checklist, and how to analyze an APK. Signing is a separate mistake, covered in a file hash is not a signing certificate.