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.
  • package on 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:android is read on the manifest element only, and only for the Android resource namespace. A declaration on another element does not count. A missing declaration is a warning.
  • One application element is required. Missing it is an issue.
  • uses-sdk and android:minSdkVersion are 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-permission needs android: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 activity whose intent-filter contains both android.intent.action.MAIN and android.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.xml or src/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

  1. Main manifest, library filter. src/main has a launcher activity with android:exported="true". A library adds another activity with SEND and 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.
  2. tools:node="remove" still counts. You removed CAMERA with a tools marker. The validator counts CAMERA if the element remains in the paste and has android:name. aapt dump permissions on 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.
  3. Debug debuggable. src/debug sets android: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 badging prints debuggable on the file you actually built. The publishing checklist includes that confirmation before upload.
  4. Application id suffix. The pasted package is com.example.notes. The debug variant installs com.example.notes.debug because of applicationIdSuffix. 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.
  5. Package attribute omitted on purpose. A current module keeps namespace in Gradle and deletes package from 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.
  6. Queries and package visibility. An app that looks up other packages on API 30 and higher needs a queries element (or a narrowly justified broader permission). The validator does not read queries. A full-looking manifest with no queries block still passes. The empty result on a device is a platform rule, not a parser error.

What to paste, in order

  1. Build the variant you mean. A debug merged manifest is the wrong document for a release upload.
  2. Copy from the Merged Manifest tab, or open the merged AndroidManifest.xml for that variant under build/intermediates.
  3. 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.
  4. Read android:exported yourself on every activity, activity-alias, service, and receiver that has an intent-filter, when targetSdkVersion is 31 or higher. The page will not mark a missing one.
  5. Run aapt dump badging on the APK. Compare package, versionCode, versionName, SDK levels, debuggable, and the permission list with what you think you merged.
  6. If you only have the APK, decode with apktool or aapt dump xmltree. Do not paste the raw AndroidManifest.xml entry 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.