Tracing the Android Build System's Manifest Merger, or "Where'd that permission come from?"

A "Merge" road sign
Photo by Albert Stoynov / Unsplash

On a project at work, we’ve been doing a review of unnecessary permissions while we’re working towards adopting Android 6.0’s runtime permission model.

For instance, we had some app functionality which would immediately invoke a phone call (using ACTION_CALL),  which requires the CALL_PHONE permission.  It seemed like a good idea to just change this to use ACTION_DIAL and drop the need for the permission altogether.

So yesterday, after dropping the CALL_PHONE permission and rebuilding, what did we still see in the permissions screen on an M device?

How is this here?

Turns out if you looked at it on an older device, it’s the READ_PHONE_STATEpermission.  Which our manifest never even requested!

This scares the heck out of users and can lead to trouble. Even if you legitimately need READ_PHONE_STATE.  Case in point, a streaming radio app I wrote some time ago that used READ_PHONE_STATE to work around lack of audio ducking pre-Froyo, where I didn’t know to include conditionals in the manifest:

So we know we gotta get that READ_PHONE_STATE out of there.  But where did it come from?  It’s in none of the manifests for our dependencies.

The Manifest Merger Log File

Android’s Gradle-based build system includes a new Manifest Merger that handles generating the final manifest for your build, by reconciling everything specified in all your dependencies, and also in all your flavors and build types.   So anything that’s in your app’s manifest as built, but not in the AndroidManifest.xml in your app root, was put there by the merger.

And nicely enough, the manifest merger gives us a log file that tells us what it did, and the docs have some info on how to read through the logs.

Check out ./app/build/outputs/logs in your build directory, and you’ll find a manifest merger report waiting for you.

I’ve gone and simulated the issue with a simpler build (which you can find on Github here), and sure enough here’s the issue in the logs.

android:uses-permission#android.permission.READ_PHONE_STATE IMPLIED from /Users/ben/git/ManifestMergerPermissionDemo/app/src/main/AndroidManifest.xml:2:1-23:12 reason: de.benlikestoco.badpermissionlibrary has a targetSdkVersion < 4  

Aha!  So we’ve got a library that we’re depending upon, that doesn’t define a targetSdkVersion (or it’s less than 4).   I guess I’ll have to send a pull request to the owner of de.benlikestoco.badpermissionlibrary :)

If for some reason you can’t get it fixed in the dependency (maybe the maintainer is AWOL, or you can’t fork the project yourself) you can give a hint to the manifest merger to exclude that permission.

<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove"/>  

I would classify this as totally a last resort because it looks kind of hacky, but if you absolutely positively need to do it, it’s there for you.

Why does this even happen with these permissions?  READ_PHONE_STATE and WRITE_EXTERNAL_STORAGE permissions were added in API level 4 (aka Android 1.6, 6+ years ago).  Prior to API 4, you basically got this for free, without having to ask for permission to do it.  If you don’t define a target SDK version, it assumes you’re declaring it to be good all the way back to API 3. To support backward compatibility with the permitted behavior in API 3, the manifest merger adds this permission.