Bug 1411654 - Part 4: Use flavorDimensions to simplify {with,without}GeckoBinaries logic. r=maliu
authorNick Alexander <nalexander@mozilla.com>
Thu, 09 Nov 2017 16:47:05 -0800
changeset 451132 01836fd98c6351667c70cfd187cf1e3c437e1f94
parent 451131 730a70767743b74a7e3a1fcf4018540edfdf30a3
child 451133 fde9bf9c14c31c11b4e9279ae7b56b94b5a02d53
push id8543
push userryanvm@gmail.com
push dateTue, 16 Jan 2018 14:33:22 +0000
treeherdermozilla-beta@a6525ed16a32 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmaliu
bugs1411654
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1411654 - Part 4: Use flavorDimensions to simplify {with,without}GeckoBinaries logic. r=maliu MozReview-Commit-ID: 2rbsP6A0BY0
mobile/android/app/build.gradle
mobile/android/base/Makefile.in
mobile/android/geckoview/build.gradle
mobile/android/geckoview_example/build.gradle
mobile/android/gradle.configure
mobile/android/gradle/debug_level.gradle
mobile/android/gradle/product_flavors.gradle
mobile/android/gradle/with_gecko_binaries.gradle
taskcluster/ci/build/android.yml
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -1,15 +1,17 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/app"
 
 apply plugin: 'com.android.application'
 apply plugin: 'checkstyle'
 apply plugin: 'com.getkeepsafe.dexcount'
 apply plugin: 'findbugs'
 
+apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle"
+
 dexcount {
     format = "tree"
 }
 
 android {
     compileSdkVersion project.ext.compileSdkVersion
 
     defaultConfig {
@@ -78,45 +80,23 @@ android {
     // The "audience" flavour dimension distinguishes between _local_ builds (intended for
     // development) and _official_ builds (intended for testing in automation and to ship in one of
     // the Fennec distribution channels).
     //
     // The "skin" flavor dimension distinguishes between different user interfaces.  We sometimes
     // want to develop significant new user interface pieces in-tree that don't ship (even in the
     // Nightly channel) while under development.  A new "skin" flavour allows us to develop such
     // pieces in Gradle without changing the mainline configuration.
-    flavorDimensions "audience", "skin"
+
+    project.configureProductFlavors.delegate = it
+    project.configureProductFlavors()
+
+    flavorDimensions "audience", "geckoBinaries", "minApi", "skin"
 
     productFlavors {
-        // For API 21+ - with pre-dexing, this will be faster for local development.
-        local {
-            dimension "audience"
-
-            // For pre-dexing, setting `minSdkVersion 21` allows the Android gradle plugin to
-            // pre-DEX each module and produce an APK that can be tested on
-            // Android Lollipop without time consuming DEX merging processes.
-            minSdkVersion 21
-            dexOptions {
-                preDexLibraries true
-            }
-        }
-        // For API < 21 - does not support pre-dexing because local development
-        // is slow in that case.
-        localOld {
-            dimension "audience"
-        }
-
-        // Automation builds.  We use "official" rather than "automation" to drive these builds down
-        // the list of configurations that Android Studio offers, thereby making it _not_ the
-        // default.  This avoids a common issue with "omni.ja" not being packed into the default APK
-        // built and deployed by Android Studio.
-        official {
-             dimension "audience"
-        }
-
         // Since Firefox 57, the mobile user interface has followed the Photon design.
         // Before Firefox 57, the user interface followed the Australis design.
         photon {
             dimension "skin"
         }
     }
 
     sourceSets {
@@ -246,26 +226,21 @@ dependencies {
 
     if (mozconfig.substs.MOZ_ANDROID_GCM) {
         implementation "com.google.android.gms:play-services-basement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
         implementation "com.google.android.gms:play-services-base:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
         implementation "com.google.android.gms:play-services-gcm:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
         implementation "com.google.android.gms:play-services-measurement:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
     }
 
-    // Include LeakCanary in most gradle based builds. LeakCanary adds about 5k methods, so we disable
-    // it for the (non-proguarded, non-predex) localOld builds to allow space for other libraries.
-    // Gradle based tests include the no-op version.  Mach based builds only include the no-op version
-    // of this library.
-    // It doesn't seem like there is a non-trivial way to be conditional on 'localOld', so instead we explicitly
-    // define a version of leakcanary for every flavor:
-    localImplementation 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
-    localOldImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
-    officialImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
-    officialImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
+    // Include LeakCanary in most gradle based builds.  Gradle based tests
+    // include the no-op version.  Mach based builds only include the no-op
+    // version of this library.
+    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
+    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
     testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
 
     implementation project(path: ':geckoview')
     implementation project(path: ':thirdparty')
 
     testImplementation 'junit:junit:4.12'
     testImplementation 'org.robolectric:robolectric:3.5.1'
     testImplementation 'org.simpleframework:simple-http:6.0.1'
@@ -356,28 +331,25 @@ android.applicationVariants.all { varian
     // re-entrant.  Official builds are driven by the moz.build system and
     // should never be re-entrant in this way.
     if (!((variant.productFlavors*.name).contains('official'))) {
         syncPreprocessedJava.dependsOn rootProject.generateCodeAndResources
         syncPreprocessedRes.dependsOn rootProject.generateCodeAndResources
         rewriteManifestPackage.dependsOn rootProject.generateCodeAndResources
     }
 
-    // Official automation builds don't include Gecko binaries, since those binaries are not
-    // produced until after build time (at package time).  official Therefore, automation builds
-    // include the Gecko binaries into the APK at package time.  The "withGeckoBinaries" variant of
-    // the :geckoview project also does this.  (It does what it says on the tin!)  For notes on this
-    // approach, see mobile/android/gradle/with_gecko_binaries.gradle.
-
-    // Like 'local' or 'localOld'.
-    def audienceDimension = variant.productFlavors[0].name
-
-    // :app uses :geckoview:release and handles it's own Gecko binary inclusion,
-    // even though this would be most naturally done in the :geckoview project.
-    if (!audienceDimension.equals('official')) {
+    // When driven from moz.build via |mach build|, Gradle does not require or
+    // use Gecko binaries.  It's only |mach package| that packs the Gecko
+    // binaries into the resulting APK.  The "withoutGeckoBinaries" variants
+    // handle this.  When driven from Android Studio or Gradle, the
+    // "withGeckoBinaries" variants handle packing the Gecko binaries into the
+    // resulting APK (for on-device deployment).  They also update the Omnijars
+    // as necessary, smoothing out the edit-compile-test development cycle.
+    // They do what they say on the tin!
+    if ((variant.productFlavors*.name).contains('withGeckoBinaries')) {
         configureVariantWithGeckoBinaries(variant)
     }
 }
 
 android.applicationVariants.all { variant ->
     configureApplicationVariantWithJNIWrappers(variant, "Fennec")
 }
 
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -239,17 +239,17 @@ define gradle_command
 	$$(topsrcdir)/mach android assemble-app
 endef
 
 # .gradle.deps: .aapt.deps FORCE
 $(eval $(call gradle_command,.gradle.deps,.aapt.deps FORCE))
 
 classes.dex: .gradle.deps
 	$(REPORT_BUILD)
-	cp $(gradle_dir)/app/intermediates/transforms/dexMerger/officialPhoton/debug/0/classes.dex classes.dex
+	cp $(gradle_dir)/app/intermediates/transforms/dexMerger/officialWithoutGeckoBinariesNoMinApiPhoton/debug/0/classes.dex classes.dex
 
 GeneratedJNIWrappers.cpp GeneratedJNIWrappers.h GeneratedJNINatives.h : .gradle.deps
 	$(REPORT_BUILD)
 
 FennecJNIWrappers.cpp FennecJNIWrappers.h FennecJNINatives.h: .gradle.deps
 	$(REPORT_BUILD)
 
 else
@@ -486,17 +486,17 @@ ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRAD
 # strings.xml.
 
 # .gradle.nodeps: AndroidManifest.xml generated/preprocessed/org/mozilla/gecko/AppConstants.java ... FORCE
 $(eval $(call gradle_command,.gradle.nodeps,AndroidManifest.xml $(constants_PP_JAVAFILES) FORCE))
 
 .aapt.nodeps: .gradle.nodeps FORCE
 	@$(TOUCH) $@
 	cp $(GRADLE_ANDROID_APP_APK) gecko-nodeps.ap_
-	cp $(gradle_dir)/app/intermediates/transforms/dex/officialPhoton/debug/folders/1000/1f/main/classes.dex classes.dex
+	cp $(gradle_dir)/app/intermediates/transforms/dexMerger/officialWithoutGeckoBinariesNoMinApiPhoton/debug/0/classes.dex classes.dex
 else
 # .aapt.nodeps: AndroidManifest.xml FORCE
 $(eval $(call aapt_command,.aapt.nodeps,AndroidManifest.xml FORCE,gecko-nodeps.ap_,gecko-nodeps/,gecko-nodeps/))
 endif
 
 # Override the Java settings with some specific android settings
 include $(topsrcdir)/config/android-common.mk
 
--- a/mobile/android/geckoview/build.gradle
+++ b/mobile/android/geckoview/build.gradle
@@ -1,12 +1,14 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
 
 apply plugin: 'com.android.library'
 
+apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle"
+
 // This converts MOZ_APP_VERSION into an integer
 // version code.
 //
 // We take something like 58.1.2a1 and come out with 5800102
 // This gives us 3 digits for the major number, and 2 digits
 // each for the minor and build number. Beta and Release
 def computeVersionCode() {
     String appVersion = mozconfig.substs.MOZ_APP_VERSION
@@ -94,24 +96,18 @@ android {
 
         // Official corresponds, roughly, to whether this build is performed on
         // Mozilla's continuous integration infrastructure. You should disable
         // developer-only functionality when this flag is set.
         // This makes no sense for GeckoView and should be removed as soon as possible.
         buildConfigField 'boolean', 'MOZILLA_OFFICIAL', mozconfig.substs.MOZILLA_OFFICIAL ? 'true' : 'false';
     }
 
-    buildTypes {
-        withGeckoBinaries {
-            initWith release
-        }
-        withoutGeckoBinaries { // For clarity and consistency throughout the tree.
-            initWith release
-        }
-    }
+    project.configureProductFlavors.delegate = it
+    project.configureProductFlavors()
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
 
     dexOptions {
         javaMaxHeapSize "2g"
@@ -152,24 +148,19 @@ android {
 dependencies {
     implementation "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
     implementation "com.android.support:palette-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
 }
 
 apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
 
 android.libraryVariants.all { variant ->
-    // Like 'debug', 'release', or 'withGeckoBinaries'.
-    def buildType = variant.buildType.name
-
-    // It would be most natural for :geckoview to always include the Gecko
-    // binaries, but that's difficult; see the notes in
-    // mobile/android/gradle/with_gecko_binaries.gradle.  Instead :app uses
-    // :geckoview:release and handles it's own Gecko binary inclusion.
-    if (buildType.equals('withGeckoBinaries')) {
+    // See the notes in mobile/android/app/build.gradle for details on including
+    // Gecko binaries and the Omnijar.
+    if ((variant.productFlavors*.name).contains('withGeckoBinaries')) {
         configureVariantWithGeckoBinaries(variant)
     }
 
     // Javadoc and Sources JAR configuration cribbed from
     // https://github.com/mapbox/mapbox-gl-native/blob/d169ea55c1cfa85cd8bf19f94c5f023569f71810/platform/android/MapboxGLAndroidSDK/build.gradle#L85
     // informed by
     // https://code.tutsplus.com/tutorials/creating-and-publishing-an-android-library--cms-24582,
     // and amended from numerous Stackoverflow posts.
@@ -193,22 +184,22 @@ android.libraryVariants.all { variant ->
         options.source = 7
 
         options.docTitle = "GeckoView ${mozconfig.substs.MOZ_APP_VERSION} API"
         options.header = "GeckoView ${mozconfig.substs.MOZ_APP_VERSION} API"
         options.addStringOption('noindex');
         options.addStringOption('noqualifier', 'java.lang');
     }
 
-    task "javadocJar${name.capitalize()}"(type: Jar, dependsOn: javadoc) {
+    def javadocJar = task("javadocJar${name.capitalize()}", type: Jar, dependsOn: javadoc) {
         classifier = 'javadoc'
         from javadoc.destinationDir
     }
 
-    task "sourcesJar${name.capitalize()}"(type: Jar) {
+    def sourcesJar = task("sourcesJar${name.capitalize()}", type: Jar) {
         classifier 'sources'
         description = "Generate Javadoc for build variant $name"
         destinationDir = new File(destinationDir, variant.baseName)
         from files(variant.javaCompile.source)
     }
 }
 
 android.libraryVariants.all { variant ->
@@ -236,32 +227,32 @@ uploadArchives {
 }
 
 // This is all related to the withGeckoBinaries approach; see
 // mobile/android/gradle/with_gecko_binaries.gradle.
 afterEvaluate {
     // The bundle tasks are only present when the particular configuration is
     // being built, so this task might not exist.  (This is due to the way the
     // Android Gradle plugin defines things during configuration.)
-    def bundleWithGeckoBinaries = tasks.findByName('bundleWithGeckoBinaries')
+    def bundleWithGeckoBinaries = tasks.findByName('bundleOfficialWithGeckoBinariesNoMinApiRelease')
     if (!bundleWithGeckoBinaries) {
         return
     }
 
     // Remove default configuration, which is the release configuration, when
     // we're actually building withGeckoBinaries.  This makes `gradle install`
     // install the withGeckoBinaries artifacts, not the release artifacts (which
     // are withoutGeckoBinaries and not suitable for distribution.)
     def Configuration archivesConfig = project.getConfigurations().getByName('archives')
     archivesConfig.artifacts.removeAll { it.extension.equals('aar') }
 
     artifacts {
         // Instead of default (release) configuration, publish one with Gecko binaries.
-        archives bundleWithGeckoBinaries
+        archives bundleOfficialWithGeckoBinariesNoMinApiRelease
         // Javadoc and sources for developer ergononomics.
-        archives javadocJarWithGeckoBinaries
-        archives sourcesJarWithGeckoBinaries
+        archives javadocJarOfficialWithGeckoBinariesNoMinApiRelease
+        archives sourcesJarOfficialWithGeckoBinariesNoMinApiRelease
     }
 }
 
 // Bug 1353055 - Strip 'vars' debugging information to agree with moz.build.
 apply from: "${topsrcdir}/mobile/android/gradle/debug_level.gradle"
 android.libraryVariants.all configureVariantDebugLevel
--- a/mobile/android/geckoview_example/build.gradle
+++ b/mobile/android/geckoview_example/build.gradle
@@ -1,64 +1,44 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/geckoview_example"
 
 apply plugin: 'com.android.application'
 
+apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle"
+
 android {
     compileSdkVersion project.ext.compileSdkVersion
 
     defaultConfig {
         targetSdkVersion project.ext.targetSdkVersion
         minSdkVersion project.ext.minSdkVersion
         manifestPlaceholders = project.ext.manifestPlaceholders
 
         applicationId "org.mozilla.geckoview_example"
         versionCode 1
         versionName "1.0"
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
     }
 
-    // This is extremely frustrating, but the only way to do it automation for
-    // now.  Without this, we only get a "debugAndroidTest" configuration; we
-    // have no "withoutGeckoBinariesAndroidTest" configuration.
-    testBuildType "withoutGeckoBinaries"
     buildTypes {
         release {
             minifyEnabled false
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
-        withGeckoBinaries { // For consistency with :geckoview project in Task Cluster invocations.
-            initWith debug
-        }
-        withoutGeckoBinaries { // Logical negation of withGeckoBinaries.
-            initWith debug
-        }
     }
+
+    project.configureProductFlavors.delegate = it
+    project.configureProductFlavors()
 }
 
 dependencies {
     testImplementation 'junit:junit:4.12'
 
     implementation 'com.android.support:support-annotations:23.4.0'
 
     androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2'
     androidTestImplementation 'com.android.support.test:runner:0.5'
     // Not defining this library again results in test-app assuming 23.1.1, and the following errors:
     // "Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (23.4.0) and test app (23.1.1) differ."
     androidTestImplementation 'com.android.support:support-annotations:23.4.0'
 
     implementation project(path: ':geckoview')
 }
-
-apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
-
-android.applicationVariants.all { variant ->
-    // Like 'debug', 'release', or 'withoutGeckoBinaries'.
-    def buildType = variant.buildType.name
-
-    // It would be most natural for :geckoview to always include the Gecko
-    // binaries, but that's difficult; see the notes in
-    // mobile/android/gradle/with_gecko_binaries.gradle.  Instead we handle our
-    // own Gecko binary inclusion.
-    if (!buildType.equals('withoutGeckoBinaries')) {
-        configureVariantWithGeckoBinaries(variant)
-    }
-}
--- a/mobile/android/gradle.configure
+++ b/mobile/android/gradle.configure
@@ -41,30 +41,33 @@ set_config('GRADLE', gradle)
 def gradle_android_build_config():
     def capitalize(s):
         # str.capitalize lower cases trailing letters.
         if s:
             return s[0].upper() + s[1:]
         else:
             return s
 
-    # It's not really possible to abstract the GeckoView details just yet; post
-    # Android-Gradle plugin 3.0+, the configurations can be more sensible and
-    # we'll do this work.
     def variant(productFlavors, buildType):
         return namespace(
             productFlavors=productFlavors,
             buildType=buildType,
             # Like 'OfficialWithoutGeckoBinariesPhotonDebug'
             name = ''.join(capitalize(t) for t in chain(productFlavors, (buildType, )))
         )
 
     return namespace(
         app=namespace(
-            variant=variant(('official', 'photon'), 'debug'),
+            variant=variant(('official', 'withoutGeckoBinaries', 'noMinApi', 'photon'), 'debug'),
+        ),
+        geckoview=namespace(
+            variant=variant(('official', 'withGeckoBinaries', 'noMinApi'), 'release'),
+        ),
+        geckoview_example=namespace(
+            variant=variant(('official', 'withGeckoBinaries', 'noMinApi'), 'debug'),
         ),
     )
 
 
 @depends(gradle_android_build_config)
 def gradle_android_app_variant_name(build_config):
     '''Like "officialPhotonDebug".'''
     def uncapitalize(s):
@@ -77,17 +80,17 @@ def gradle_android_app_variant_name(buil
 
 set_config('GRADLE_ANDROID_APP_VARIANT_NAME', gradle_android_app_variant_name)
 
 
 @depends(gradle_android_build_config)
 def gradle_android_app_tasks(build_config):
     '''Gradle tasks run by |mach android assemble-app|.'''
     return [
-        'geckoview:generateJNIWrappersForGeneratedRelease',
+        'geckoview:generateJNIWrappersForGenerated{geckoview.variant.name}'.format(geckoview=build_config.geckoview),
         'app:generateJNIWrappersForFennec{app.variant.name}'.format(app=build_config.app),
         'app:assemble{app.variant.name}'.format(app=build_config.app),
         'app:assemble{app.variant.name}AndroidTest'.format(app=build_config.app),
     ]
 
 set_config('GRADLE_ANDROID_APP_TASKS', gradle_android_app_tasks)
 
 
@@ -170,19 +173,19 @@ def gradle_android_findbugs_tasks(build_
 
 set_config('GRADLE_ANDROID_FINDBUGS_TASKS', gradle_android_findbugs_tasks)
 
 
 @depends(gradle_android_build_config)
 def gradle_android_archive_geckoview_tasks(build_config):
     '''Gradle tasks run by |mach android archive-geckoview|.'''
     return [
-        'geckoview:assembleWithGeckoBinaries',
-        'geckoview_example:assembleWithGeckoBinaries',
-        'geckoview_example:assembleWithGeckoBinariesAndroidTest',
+        'geckoview:assemble{geckoview.variant.name}'.format(geckoview=build_config.geckoview),
+        'geckoview_example:assemble{geckoview_example.variant.name}'.format(geckoview_example=build_config.geckoview_example),
+        'geckoview_example:assemble{geckoview_example.variant.name}AndroidTest'.format(geckoview_example=build_config.geckoview_example),
         'geckoview:uploadArchives',
     ]
 
 set_config('GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS', gradle_android_archive_geckoview_tasks)
 
 
 @depends(
     gradle_android_app_tasks,
--- a/mobile/android/gradle/debug_level.gradle
+++ b/mobile/android/gradle/debug_level.gradle
@@ -1,20 +1,17 @@
 /* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Bug 1353055 - Strip 'vars' debugging information to agree with moz.build.
 ext.configureVariantDebugLevel = { variant ->
-    // Like 'debug', 'release', or 'withGeckoBinaries'.
+    // Like 'debug' or 'release'.
     def buildType = variant.buildType.name
 
-    // For :app, like 'local', 'localOld', or 'official'.  For other projects, null.
-    def audienceDimension = variant.productFlavors[0]?.name
-
     // The default is 'lines,source,vars', which includes debugging information
     // that is quite large: roughly 500kb for Fennec.  Therefore we remove
     // 'vars' unless we're producing a debug build, where it is useful.
-    if (!'debug'.equals(buildType) || 'official'.equals(audienceDimension)) {
+    if (!'debug'.equals(buildType) || (variant.productFlavors*.name).contains('official')) {
         variant.javaCompile.options.debugOptions.debugLevel = 'lines,source'
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle/product_flavors.gradle
@@ -0,0 +1,48 @@
+/* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ext.configureProductFlavors = {
+    flavorDimensions "audience", "geckoBinaries", "minApi"
+    productFlavors {
+        local {
+            dimension "audience"
+        }
+
+        // Automation builds.  We use "official" rather than "automation" to drive these builds down
+        // the list of configurations that Android Studio offers, thereby making it _not_ the
+        // default.  This avoids a common issue with "omni.ja" not being packed into the default APK
+        // built and deployed by Android Studio.
+        official {
+             dimension "audience"
+        }
+
+        withGeckoBinaries {
+            dimension "geckoBinaries"
+        }
+
+        withoutGeckoBinaries {
+            dimension "geckoBinaries"
+        }
+
+        // For API 21+ - with pre-dexing, this will be faster for local development.
+        minApi21 {
+            dimension "minApi"
+
+            // For pre-dexing, setting `minSdkVersion 21` allows the Android gradle plugin to
+            // pre-DEX each module and produce an APK that can be tested on
+            // Android Lollipop without time consuming DEX merging processes.
+            minSdkVersion 21
+            dexOptions {
+                preDexLibraries true
+            }
+        }
+
+        // For API < 21 - does not support pre-dexing because local development
+        // is slow in that case.
+        noMinApi {
+            dimension "minApi"
+        }
+    }
+}
--- a/mobile/android/gradle/with_gecko_binaries.gradle
+++ b/mobile/android/gradle/with_gecko_binaries.gradle
@@ -1,51 +1,27 @@
 /* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-// We run fairly hard into a fundamental limitation of the Android Gradle
-// plugin.  There are many bugs filed about this, but
-// https://code.google.com/p/android/issues/detail?id=216978#c6 is a reason one.
-// The issue is that we need fine-grained control over when to include Gecko's
-// binary libraries into the GeckoView AAR and the Fennec APK, and that's hard
-// to achieve.  In particular:
-//
-// * :app:official* wants :geckoview to not include Gecko binaries (official
-// *  automation build, before package)
-//
-// * :geckoview:withLibraries wants :geckoview to include Gecko binaries
-// * (automation build, after package)
-//
-// * non-:app:official* wants :geckoview to include Gecko binaries (local
-// * build, always after package)
-//
-// publishNonDefault (see
-// http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Library-Publication)
-// is intended to address this, but doesn't handle our case.  That option always
-// builds *all* configurations, which fails when the required Gecko binaries
-// don't exist (automation build, before package).  So instead, we make both
-// :app and :geckoview both know how to include the Gecko binaries, and use a
-// non-default, non-published :geckoview:withGeckoBinaries configuration to
-// handle automation's needs.  Simple, right?
-
 // The omnijar inputs are listed as resource directory inputs to a dummy JAR.
 // That arrangement labels them nicely in IntelliJ.  See the comment in the
 // :omnijar project for more context.
 evaluationDependsOn(':omnijar')
 
-task buildOmnijar(type:Exec) {
+task buildOmnijars(type:Exec) {
     dependsOn rootProject.generateCodeAndResources
 
     // See comment in :omnijar project regarding interface mismatches here.
     inputs.file(project(':omnijar').sourceSets.main.resources.srcDirs).skipWhenEmpty() 
 
-    // Produce a single output file.
+    // Produce both the Fennec and the GeckoView omnijars.
     outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
+    outputs.file "${topobjdir}/dist/geckoview/assets/omni.ja"
 
     workingDir "${topobjdir}"
 
     commandLine mozconfig.substs.GMAKE
     args '-C'
     args "${topobjdir}/mobile/android/base"
     args 'gradle-omnijar'
 
@@ -55,80 +31,79 @@ task buildOmnijar(type:Exec) {
     errorOutput = standardOutput
     doLast {
         if (execResult.exitValue != 0) {
             throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
         }
     }
 }
 
-task syncOmnijarFromDistDir(type: Sync) {
-    // :app needs the full Fennec omni.ja, whereas other projects need the GeckoView-specific omni.ja.
+ext.configureVariantWithGeckoBinaries = { variant ->
+    // :app needs the full Fennec omni.ja, whereas other projects need the
+    // GeckoView-specific omni.ja.
     def omnijar_dir = "app".equals(project.name) ? "fennec" : "geckoview"
-    into("${project.buildDir}/generated/omnijar")
-    from("${topobjdir}/dist/${omnijar_dir}/omni.ja",
-         "${topobjdir}/dist/${omnijar_dir}/assets/omni.ja") {
-        // Throw an exception if we find multiple, potentially conflicting omni.ja files.
-        duplicatesStrategy 'fail'
-    }
-}
+    def distDir = "${topobjdir}/dist/${omnijar_dir}"
 
-task checkLibsExistInDistDir {
-    doLast {
-        if (syncLibsFromDistDir.source.empty) {
-            throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib.  Have you built and packaged?")
+    def syncOmnijarFromDistDir = task("syncOmnijarFromDistDirFor${variant.name.capitalize()}", type: Sync) {
+        doFirst {
+            if (source.empty) {
+                throw new GradleException("Required omnijar not found in ${source.asPath}.  Have you built and packaged?")
+            }
         }
-    }
-}
 
-task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
-    into("${project.buildDir}/generated/jniLibs")
-    from("${topobjdir}/dist/fennec/lib")
-}
-
-task checkAssetsExistInDistDir {
-    doLast {
-        if (syncAssetsFromDistDir.source.empty) {
-            throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets.  Have you built and packaged?")
+        into("${project.buildDir}/moz.build/src/${variant.name}/omnijar")
+        from("${distDir}/omni.ja",
+             "${distDir}/assets/omni.ja") {
+            // Throw an exception if we find multiple, potentially conflicting omni.ja files.
+            duplicatesStrategy 'fail'
         }
     }
-}
-
-task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
-    into("${project.buildDir}/generated/assets")
-    from("${topobjdir}/dist/fennec/assets") {
-        exclude 'omni.ja'
-    }
-}
 
-ext.configureVariantWithGeckoBinaries = { variant ->
-    // Like 'localPhoton' or 'localOldPhoton'; may be null.
-    def productFlavor = ""
-    def productFlavorNames = variant.productFlavors.collect { it.name.capitalize() }
-    if (!productFlavorNames.isEmpty()) {
-        productFlavor = productFlavorNames.join()
-        // Groovy's `uncapitilize` is not yet available.
-        def c = productFlavor.toCharArray()
-        c[0] = Character.toLowerCase(c[0])
-        productFlavor = new String(c)
+    def syncLibsFromDistDir = task("syncLibsFromDistDirFor${variant.name.capitalize()}", type: Sync) {
+        doFirst {
+            if (source.empty) {
+                throw new GradleException("Required JNI libraries not found in ${source.asPath}.  Have you built and packaged?")
+            }
+        }
+
+        into("${project.buildDir}/moz.build/src/${variant.name}/jniLibs")
+        from("${distDir}/lib")
     }
 
-    // Like 'debug' or 'release'.
-    def buildType = variant.buildType.name
+    def syncAssetsFromDistDir = task("syncAssetsFromDistDirFor${variant.name.capitalize()}", type: Sync) {
+        doFirst {
+            if (source.empty) {
+                throw new GradleException("Required assets not found in ${source.asPath}.  Have you built and packaged?")
+            }
+        }
+
+        into("${project.buildDir}/moz.build/src/${variant.name}/assets")
+        from("${distDir}/assets") {
+            exclude 'omni.ja'
+        }
+    }
 
-    syncOmnijarFromDistDir.dependsOn buildOmnijar
-    def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
-    generateAssetsTask.dependsOn syncOmnijarFromDistDir
-    generateAssetsTask.dependsOn syncLibsFromDistDir
-    generateAssetsTask.dependsOn syncAssetsFromDistDir
+    // Local (read, not 'official') builds want to reflect developer changes to
+    // the Omnijar sources.  To do this, the Gradle build calls out to the
+    // moz.build system, which can be re-entrant.  Official builds are driven by
+    // the moz.build system and should never be re-entrant in this way.
+    if (!((variant.productFlavors*.name).contains('official'))) {
+        syncOmnijarFromDistDir.dependsOn buildOmnijars
+    }
 
-    def sourceSet = productFlavor ? "${productFlavor}${buildType.capitalize()}" : buildType
-    android.sourceSets."${sourceSet}".assets.srcDir syncOmnijarFromDistDir.destinationDir
-    android.sourceSets."${sourceSet}".assets.srcDir syncAssetsFromDistDir.destinationDir
-    android.sourceSets."${sourceSet}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
+    def assetGenTask = tasks.findByName("generate${variant.name.capitalize()}Assets")
+    if ((variant.productFlavors*.name).contains('withGeckoBinaries')) {
+        assetGenTask.dependsOn syncOmnijarFromDistDir
+        assetGenTask.dependsOn syncLibsFromDistDir
+        assetGenTask.dependsOn syncAssetsFromDistDir
+
+        android.sourceSets."${variant.name}".assets.srcDir syncOmnijarFromDistDir.destinationDir
+        android.sourceSets."${variant.name}".assets.srcDir syncAssetsFromDistDir.destinationDir
+        android.sourceSets."${variant.name}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
+    }
 }
 
 ext.configureLibraryVariantWithJNIWrappers = { variant, module ->
     // Library variants have two essentially independent transform* tasks:
     //
     // - ...WithSyncLibJars... is used by assemble* and bundle*
     // - ...WithPrepareIntermediateJars... is used by consuming applications.
     //
--- a/taskcluster/ci/build/android.yml
+++ b/taskcluster/ci/build/android.yml
@@ -16,17 +16,17 @@ android-api-16/debug:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -68,17 +68,17 @@ android-x86/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -125,17 +125,17 @@ android-x86-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -173,17 +173,17 @@ android-api-16/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -225,17 +225,17 @@ android-api-16-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -278,17 +278,17 @@ android-x86-old-id/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -334,17 +334,17 @@ android-x86-old-id-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -383,17 +383,17 @@ android-api-16-old-id/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -434,17 +434,17 @@ android-api-16-old-id-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -483,17 +483,17 @@ android-api-16-gradle/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -529,17 +529,17 @@ android-aarch64/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config:
@@ -581,17 +581,17 @@ android-aarch64-nightly/opt:
         artifacts:
             - name: public/android/R
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
               type: directory
             - name: public/android/maven
               path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
               type: directory
             - name: public/build/geckoview_example.apk
-              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/geckoview_example-withGeckoBinaries.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
               type: file
             - name: public/build
               path: /builds/worker/artifacts/
               type: directory
     run:
         using: mozharness
         actions: [get-secrets build multi-l10n update]
         config: