Supporting the M Developer Preview... And Previous Versions

Google, as they did with the L Developer Preview last year, is playing games with the development process.

The documentation states that you need the following in your build.gradle file to build against the M Developer Preview (a.k.a., MNC):

  • a compileSdkVersion value of ‘android-MNC’
  • a minSdkVersion value of ‘MNC’
  • a targetSdkVersion value of ‘MNC’

This means your app will not run on older devices, as the minSdkVersion is set to MNC. If you try lowering either minSdkVersion or targetSdkVersion, there will be no effect — simply having compileSdkVersion set to android-MNC causes the build tools to put MNC in the generated manifest for minSdkVersion and targetSdkVersion.

This is short-sighted.

Android developers need to be able to test their apps on MNC. They also need to ensure that the same app can work on older devices, as relatively few developers will be creating apps that ship only to Android M or higher devices. Yet Google tries to prevent you from testing the same code in both environments via this silly MNC trick.

My only guess as to why Google is doing this is to prevent Android developers from shipping apps compiled against the developer preview. That’s a noble goal. Screwing around with the build process is not the answer.

IMHO, the answer is two-fold:

  1. Block MNC-compiled apps at the Play Store. Instead of preventing the apps from running on older devices, make sure that the Play Store can detect APKs that are compiled against MNC and block them, just as the Play Store detects signing keystore problems and such. Google could even document the process of detecting an MNC-compiled app, so other distribution channels (e.g., Amazon AppStore for Android) can add in similar blocks if they chose.

  2. Shame developers, as I am about to do:

Only drooling idiots ship apps compiled against preview SDKs. Are you a drooling idiot?

With that warning in mind, let’s solve two problems: how to compile against MNC yet still test on older devices, and how your code can detect that it is running on MNC.

NOTE: The recipes described below work for the first version of the M Developer Preview. This recipe may well require adjustments in future developer preview updates.


Through what’s known as the “manifest merger” process, the manifest that goes in your APK is synthesized out of several sources:

  • your main sourceset’s manifest
  • your androidTest sourceset’s manifest, if you are testing
  • any other manifests in other build variants’ sourcesets
  • stuff in build.gradle
  • manifests from any library modules or AARs you have as dependencies

In the case of the M Developer Preview, the compileSdkVersion value of android-MNC causes the build process to put MNC in as the minSdkVersion and targetSdkVersion in the generated manifest.

Fortunately, manifests are text files.

So, here is an android closure that can build an app module that compiles against MNC but will run on API Level 15+ devices:

android {
    compileSdkVersion 'android-MNC'
    buildToolsVersion "23.0.0 rc1"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 15
    }

    // based on http://stackoverflow.com/a/27372806/115145

    applicationVariants.all { variant ->
        variant.outputs.each { output ->
            output.processManifest.doLast {
                def manifestOutFile = output.processManifest.manifestOutputFile
                def newFileContents = manifestOutFile.getText('UTF-8').replace("MNC", "15")
                manifestOutFile.write(newFileContents, 'UTF-8')
            }
        }
    }
}

This takes a very “caveman” approach to the problem, reading in the generated manifest, replacing all occurrences of MNC with 15, and writing the adjusted manifest back out. This will fail on projects that have MNC somewhere else, like an activity’s class name. It also sets both minSdkVersion and targetSdkVersion to the same value. A more sophisticated script would replace those individual attributes — the proof of this is left as an exercise for the reader. Similarly, a more powerful script would read the desired values out of defaultConfig and apply them. And, a safety-conscious edition of this would only apply this for debuggable variants, thereby helping to reduce the impact of a drooling idiot trying to ship a release build that performs this override. This is merely a proof of concept, not implementing all possible bells and whistles.

Again, doing this and releasing the results to the Play Store or elsewhere is monumentally idiotic. Use this for testing only.


However, we then run into a related problem: how do we determine that we are running on MNC, so we can call MNC-specific methods and avoid them on older devices?

Since there is a Build.VERSION_CODES.MNC in the SDK, you might think that you could just compare it with Build.VERSION.SDK_INT, the way we do for any other Android version.

And you would be mistaken.

There are two problems with MNC, at least in terms of the hardware images (haven’t tried this on an emulator just yet):

  1. Build.VERSION.SDK_INT is 22, the same value as Android 5.1 devices use

  2. Build.VERSION_CODES.MNC is 10000, because, according to Google, “this is WAI. there is no official API level yet.”

(WAI presumably means “working as intended”)

As usual, there’s a workaround. The following static method will return true if your code is running on MNC; false otherwise:

static private boolean putMarzipanInYourPiePlateBingo() {
  boolean result=false;

  if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP_MR1) {
    try {
      Field f=Build.VERSION.class.getField("PREVIEW_SDK_INT");

      if (f.getInt(null)>0) {
        result=true;
      }
    }
    catch (NoSuchFieldException e) {
      // no problem, must really be API 22
    }
    catch (IllegalAccessException e) {
      // ummm... this shouldn't happen
    }
  }

  return(result);
}

It takes advantage of the fact that PREVIEW_SDK_INT was added to the MNC SDK. If we are older than API Level 22, we can’t be on MNC. If we are on API Level 22, and we cannot find or access PREVIEW_SDK_INT, or it is 0, presumably we are really on API Level 22. But, if Build.VERSION.SDK_INT returns 22 or higher (higher if they actually set it to a better value in future developer preview updates), and PREVIEW_SDK_INT exists and is higher than zero, we should be on MNC.

This has been tested on a Nexus 5 running MNC, a Nexus 5 running Android 5.1, and a Galaxy Nexus running Android 4.1 (or thereabouts, something old), and it seems to hold up. That being said, YMMV, unless you are on the metric system, in which case YKMV.

Once again, this is the sort of thing that should only be used in testing. Don’t ship anything that refers to marzipan.

UPDATE (12 June 2015): Google code samples show a different option: "MNC".equals(Build.VERSION.CODENAME).

Once Android M ships “for realz”, you should be able to switch back to normal Build.VERSION.SDK_INT checks, when you ship your M-ready app.