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:
-
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.
-
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):
-
Build.VERSION.SDK_INT
is 22, the same value as Android 5.1 devices use -
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.