Bug 1291363 - Add geckoview and geckoview_example Gradle projects. r=jchen,sebastian
authorNick Alexander <nalexander@mozilla.com>
Wed, 05 Oct 2016 20:23:38 -0700
changeset 316839 5d8f137ee52eb37c06d4ffd8b272f5543b6de834
parent 316666 ad3b71ecafeb0b0366750eb3a771224e05a194ee
child 316840 80d979342a04c163156c1b74235cb9c24f850658
push id30784
push userphilringnalda@gmail.com
push dateFri, 07 Oct 2016 02:59:12 +0000
treeherdermozilla-central@4b9944879c9a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjchen, sebastian
bugs1291363
milestone52.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 1291363 - Add geckoview and geckoview_example Gradle projects. r=jchen,sebastian We need to bump the Gradle Deps task, which fetches dependencies, to include new test dependencies; and use freshly uploaded tooltool archives (manually uploaded) containing the new test dependencies. MozReview-Commit-ID: 8bNOVQPHlk6
mobile/android/app/build.gradle
mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
mobile/android/config/tooltool-manifests/android-x86/releng.manifest
mobile/android/config/tooltool-manifests/android/releng.manifest
mobile/android/geckoview/build.gradle
mobile/android/geckoview/src/main/AndroidManifest.xml
mobile/android/geckoview_example/build.gradle
mobile/android/geckoview_example/proguard-rules.pro
mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/ApplicationTest.java
mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/GeckoViewActivityTest.java
mobile/android/geckoview_example/src/main/AndroidManifest.xml
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
mobile/android/geckoview_example/src/main/res/values/colors.xml
mobile/android/geckoview_example/src/main/res/values/strings.xml
mobile/android/geckoview_example/src/test/java/org/mozilla/geckoview_example/ExampleUnitTest.java
mobile/android/gradle/with_gecko_binaries.gradle
mobile/android/thirdparty/build.gradle
settings.gradle
testing/mozharness/configs/builds/releng_sub_android_configs/64_api_15_gradle_dependencies.py
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -84,17 +84,16 @@ android {
         main {
             manifest.srcFile "${project.buildDir}/generated/source/preprocessed_manifest/AndroidManifest.xml"
 
             aidl {
                 srcDir "${topsrcdir}/mobile/android/base/aidl"
             }
 
             java {
-                srcDir "${topsrcdir}/mobile/android/geckoview/src/main/java"
                 srcDir "${topsrcdir}/mobile/android/base/java"
                 srcDir "${topsrcdir}/mobile/android/search/java"
                 srcDir "${topsrcdir}/mobile/android/javaaddons/java"
                 srcDir "${topsrcdir}/mobile/android/services/src/main/java"
 
                 if (mozconfig.substs.MOZ_ANDROID_MLS_STUMBLER) {
                     srcDir "${topsrcdir}/mobile/android/stumbler/java"
                 }
@@ -225,16 +224,17 @@ dependencies {
     // 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:
     localCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
     localOldCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
     automationCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
     testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
 
+    compile project(':geckoview')
     compile project(':thirdparty')
 
     testCompile 'junit:junit:4.12'
     testCompile 'org.robolectric:robolectric:3.1.2'
     testCompile 'org.simpleframework:simple-http:6.0.1'
     testCompile 'org.mockito:mockito-core:1.10.19'
 
     // Including the Robotium JAR directly can cause issues with dexing.
@@ -248,50 +248,22 @@ task checkstyle(type: Checkstyle) {
     // TODO: should use sourceSets from project instead of hard-coded str.
     source '../base/java/'
     // TODO: This ignores our pre-processed resources.
     include '**/*.java'
     // TODO: classpath should probably be something.
     classpath = files()
 }
 
-task syncOmnijarFromDistDir(type: Sync) {
-    into("${project.buildDir}/generated/omnijar")
-    from("${topobjdir}/dist/fennec/assets") {
-        include 'omni.ja'
-    }
-}
-
-task checkLibsExistInDistDir<< {
-    if (syncLibsFromDistDir.source.empty) {
-        throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib.  Have you built and packaged?")
-    }
-}
-
-task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
-    into("${project.buildDir}/generated/jniLibs")
-    from("${topobjdir}/dist/fennec/lib")
-}
-
-task checkAssetsExistInDistDir<< {
-    if (syncAssetsFromDistDir.source.empty) {
-        throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets.  Have you built and packaged?")
-    }
-}
-
-task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
-    into("${project.buildDir}/generated/assets")
-    from("${topobjdir}/dist/fennec/assets") {
-        exclude 'omni.ja'
-    }
-}
-
 task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
     into("${project.buildDir}/generated/source/preprocessed_code")
-    from("${topobjdir}/mobile/android/base/generated/preprocessed")
+    from("${topobjdir}/mobile/android/base/generated/preprocessed") {
+        // All other preprocessed code is included in the geckoview project.
+        include '**/AdjustConstants.java'
+    }
 }
 
 // The localization system uses the moz.build preprocessor to interpolate a .dtd
 // file of XML entity definitions into an XML file of elements referencing those
 // entities.  (Each locale produces its own .dtd file, backstopped by the en-US
 // .dtd file in tree.)  Android Studio (and IntelliJ) don't handle these inline
 // entities smoothly.  This filter merely expands the entities in place, making
 // them appear properly throughout the IDE.  Be aware that this assumes that the
@@ -307,82 +279,48 @@ class ExpandXMLEntitiesFilter extends Fi
 task syncPreprocessedResources(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
     into("${project.buildDir}/generated/source/preprocessed_resources")
     from("${topobjdir}/mobile/android/base/res")
     filesMatching('**/strings.xml') {
         filter(ExpandXMLEntitiesFilter)
     }
 }
 
-// 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) {
-    dependsOn rootProject.generateCodeAndResources
-
-    // See comment in :omnijar project regarding interface mismatches here.
-    inputs.source project(':omnijar').sourceSets.main.resources.srcDirs
-
-    // Produce a single output file.
-    outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
-
-    workingDir "${topobjdir}"
-
-    commandLine mozconfig.substs.GMAKE
-    args '-C'
-    args "${topobjdir}/mobile/android/base"
-    args 'gradle-omnijar'
-
-    // Only show the output if something went wrong.
-    ignoreExitValue = true
-    standardOutput = new ByteArrayOutputStream()
-    errorOutput = standardOutput
-    doLast {
-        if (execResult.exitValue != 0) {
-            throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
-        }
-    }
-}
-
 // It's not easy -- see the backout in Bug 1242213 -- to change the <manifest>
 // package for Fennec.  Gradle has grown a mechanism to achieve what we want for
 // Fennec, however, with applicationId.  To use the same manifest as moz.build,
 // we replace the package with org.mozilla.gecko (the eventual package) here.
 task rewriteManifestPackage(type: Copy, dependsOn: rootProject.generateCodeAndResources) {
     into("${project.buildDir}/generated/source/preprocessed_manifest")
     from("${topobjdir}/mobile/android/base/AndroidManifest.xml")
     filter { it.replaceFirst(/package=".*?"/, 'package="org.mozilla.gecko"') }
 }
 
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
 android.applicationVariants.all { variant ->
     variant.preBuild.dependsOn rewriteManifestPackage
     variant.preBuild.dependsOn syncPreprocessedCode
     variant.preBuild.dependsOn syncPreprocessedResources
 
+    // Automation builds don't include Gecko binaries, since those binaries are
+    // not produced until after build time (at package time).  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 productFlavor = variant.productFlavors[0].name
-    // Like 'debug' or 'release'.
-    def buildType = variant.buildType.name
 
-    // We insert omni.ja and the .so libraries into all local builds.
-    if (!productFlavor.startsWith('local')) {
-        return
+    // :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 (!productFlavor.equals('automation')) {
+        configureVariantWithGeckoBinaries(variant)
     }
-
-    syncOmnijarFromDistDir.dependsOn buildOmnijar
-    def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
-    generateAssetsTask.dependsOn syncOmnijarFromDistDir
-    generateAssetsTask.dependsOn syncLibsFromDistDir
-    generateAssetsTask.dependsOn syncAssetsFromDistDir
-
-    android.sourceSets."${productFlavor}${buildType.capitalize()}".assets.srcDir syncOmnijarFromDistDir.destinationDir
-    android.sourceSets."${productFlavor}${buildType.capitalize()}".assets.srcDir syncAssetsFromDistDir.destinationDir
-    android.sourceSets."${productFlavor}${buildType.capitalize()}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
 }
 
 apply plugin: 'spoon'
 
 spoon {
     // For now, let's be verbose.
     debug = true
     // It's not helpful to pass when we don't have a device connected.
@@ -421,37 +359,37 @@ afterEvaluate {
 
 // Bug 1299015: Complain to treeherder if checkstyle, lint, or unittest fails.  It's not obvious
 // how to listen to individual errors in most cases, so we just link to the reports for now.
 def makeTaskExecutionListener(artifactRootUrl) {
     return new TaskExecutionListener() {
         void beforeExecute(Task task) {
             // Do nothing.
         }
-    
+
         void afterExecute(Task task, TaskState state) {
             if (!state.failure) {
                 return
             }
-    
+
             // Link to the failing report.  The task path and the report path
             // depend on the android-lint task in
             // taskcluster/ci/android-stuff/kind.yml.  It's not possible to link
             // directly, so for now consumers will need to copy-paste the URL.
             switch (task.path) {
             case ':app:checkstyle':
                 def url = "${artifactRootUrl}/public/android/checkstyle/checkstyle.xml"
                 println "TEST-UNEXPECTED-FAIL | android-checkstyle | Checkstyle rule violations were found. See the report at: $url"
                 break
-    
+
             case ':app:lintAutomationDebug':
                 def url = "${artifactRootUrl}/public/android/lint/lint-results-automationDebug.html"
                 println "TEST-UNEXPECTED-FAIL | android-lint | Lint found errors in the project; aborting build. See the report at: $url"
                 break
-    
+
             case ':app:testAutomationDebugUnitTest':
                 def url = "${artifactRootUrl}/public/android/unittest/automationDebug/index.html"
                 println "TEST-UNEXPECTED-FAIL | android-test | There were failing tests. See the report at: $url"
                 break
             }
         }
     }
 }
--- a/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
@@ -30,26 +30,26 @@
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "66640e3f77a0f9c0ea52f66c53bee8db3c1a27ea4a11526d15706b9da6a0302cd2d5b088f9addca84f4a962022cba3b76829cb878c90cf9bebb3aab050b4aaa4",
-"size": 47315996
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle-dist.tar.xz",
 "unpack": true,
-"digest": "36f961f85b0be846cc9e72bfa0dd1f74e7da8ef785717ce4fd102fec977f21f8902c233b28a21c1ce3797eb2759c7a74c5f74e47bd8f13c1eec640f8d7bed4ac",
-"size": 51512016
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "dotgradle.tar.xz",
 "unpack": true,
 "digest": "9f082ccd71ad18991eb71fcad355c6990f50a72a09ab9b79696521485656083a72faf5a8d4714de9c4b901ee2319b6786a51964846bb7075061642a8505501c2",
 "size": 512
--- a/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
@@ -45,26 +45,26 @@
 "filename": "gcc.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "66640e3f77a0f9c0ea52f66c53bee8db3c1a27ea4a11526d15706b9da6a0302cd2d5b088f9addca84f4a962022cba3b76829cb878c90cf9bebb3aab050b4aaa4",
-"size": 47315996
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle-dist.tar.xz",
 "unpack": true,
-"digest": "36f961f85b0be846cc9e72bfa0dd1f74e7da8ef785717ce4fd102fec977f21f8902c233b28a21c1ce3797eb2759c7a74c5f74e47bd8f13c1eec640f8d7bed4ac",
-"size": 51512016
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
 },
 {
 "size": 30899096,
 "visibility": "public",
 "digest": "ac9f5f95d11580d3dbeff87e80a585fe4d324b270dabb91b1165686acab47d99fa6651074ab0be09420239a5d6af38bb2c539506962a7b44e0ed4d080bba2953",
 "algorithm": "sha512",
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
--- a/mobile/android/config/tooltool-manifests/android/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android/releng.manifest
@@ -55,26 +55,26 @@
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "66640e3f77a0f9c0ea52f66c53bee8db3c1a27ea4a11526d15706b9da6a0302cd2d5b088f9addca84f4a962022cba3b76829cb878c90cf9bebb3aab050b4aaa4",
-"size": 47315996
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle-dist.tar.xz",
 "unpack": true,
-"digest": "36f961f85b0be846cc9e72bfa0dd1f74e7da8ef785717ce4fd102fec977f21f8902c233b28a21c1ce3797eb2759c7a74c5f74e47bd8f13c1eec640f8d7bed4ac",
-"size": 51512016
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
 },
 {
 "version": "rustc 1.11.0 (9b21dcd6a 2016-08-15) repack",
 "size": 97552448,
 "digest": "272438c1692a46998dc44f22bd1fe18da1be7af2e7fdcf6c52709366c80c73e30637f0c3864f45c64edf46ce6a905538c14b2313983be973f9f29a2f191ec89b",
 "algorithm": "sha512",
 "filename": "rustc.tar.xz",
 "unpack": true
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/build.gradle
@@ -0,0 +1,86 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
+
+apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+    defaultConfig {
+        targetSdkVersion 23
+        minSdkVersion 15
+    }
+
+    buildTypes {
+        withGeckoBinaries {
+            initWith release
+        }
+        withoutGeckoBinaries { // For clarity and consistency throughout the tree.
+            initWith release
+        }
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+
+    dexOptions {
+        javaMaxHeapSize "2g"
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    sourceSets {
+        main {
+            java {
+                srcDir "${topsrcdir}/mobile/android/geckoview/src/thirdparty/java"
+
+                // TODO: support WebRTC.
+                // if (mozconfig.substs.MOZ_WEBRTC) {
+                //     srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
+                //     srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
+                //     srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
+                // }
+
+                // TODO: don't use AppConstants.
+                srcDir "${project.buildDir}/generated/source/preprocessed_code" // See syncPreprocessedCode.
+            }
+
+            assets {
+            }
+        }
+    }
+}
+
+dependencies {
+    compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+}
+
+task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+    into("${project.buildDir}/generated/source/preprocessed_code")
+    from("${topobjdir}/mobile/android/base/generated/preprocessed") {
+        // AdjustConstants is included in the main app project.
+        exclude '**/AdjustConstants.java'
+    }
+}
+
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
+android.libraryVariants.all { variant ->
+    variant.preBuild.dependsOn syncPreprocessedCode
+
+    // 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')) {
+        configureVariantWithGeckoBinaries(variant)
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.mozilla.geckoview">
+
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+    <!-- READ_EXTERNAL_STORAGE was added in API 16, and is only enforced in API
+         19+.  We declare it so that the bouncer APK and the main APK have the
+         same set of permissions. -->
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+    <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"/>
+
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <uses-permission android:name="android.permission.VIBRATE"/>
+
+    <uses-feature android:name="android.hardware.location" android:required="false"/>
+    <uses-feature android:name="android.hardware.location.gps" android:required="false"/>
+    <uses-feature android:name="android.hardware.touchscreen"/>
+
+    <!--#ifdef MOZ_WEBRTC-->
+    <!--<uses-permission android:name="android.permission.RECORD_AUDIO"/>-->
+    <!--<uses-feature android:name="android.hardware.audio.low_latency" android:required="false"/>-->
+    <!--<uses-feature android:name="android.hardware.camera.any" android:required="false"/>-->
+    <!--<uses-feature android:name="android.hardware.microphone" android:required="false"/>-->
+    <!--#endif-->
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-feature android:name="android.hardware.camera" android:required="false"/>
+    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
+
+    <!-- App requires OpenGL ES 2.0 -->
+    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/build.gradle
@@ -0,0 +1,62 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview_example"
+
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+    defaultConfig {
+        applicationId "org.mozilla.geckoview_example"
+        minSdkVersion 15
+        targetSdkVersion 23
+        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
+        }
+    }
+}
+
+dependencies {
+    testCompile 'junit:junit:4.12'
+
+    compile 'com.android.support:support-annotations:23.0.1'
+
+    // Later versions (2.2.2, 0.5) requires newer support libraries, leading to
+    // "Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (23.0.1) and test app (23.1.1) differ."
+    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
+    androidTestCompile 'com.android.support.test:runner:0.4.1'
+
+    compile project(':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)
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/nalexander/.mozbuild/android-sdk-macosx/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/ApplicationTest.java
@@ -0,0 +1,13 @@
+package org.mozilla.geckoview_example;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+    public ApplicationTest() {
+        super(Application.class);
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/GeckoViewActivityTest.java
@@ -0,0 +1,32 @@
+/* -*- Mode: Java; 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/. */
+
+package org.mozilla.geckoview_example;
+
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+@RunWith(AndroidJUnit4.class)
+public class GeckoViewActivityTest {
+
+    @Rule
+    public ActivityTestRule<GeckoViewActivity> mActivityRule = new ActivityTestRule(GeckoViewActivity.class);
+
+    @Test
+    public void testA() throws InterruptedException {
+        onView(withId(R.id.gecko_view))
+                .check(matches(isDisplayed()));
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="org.mozilla.geckoview_example">
+
+    <application android:allowBackup="true"
+                 android:label="@string/app_name"
+                 android:supportsRtl="true">
+
+        <uses-library android:name="android.test.runner" />
+
+        <activity android:name="org.mozilla.geckoview_example.GeckoViewActivity"
+                  android:label="GeckoViewActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -0,0 +1,142 @@
+/* -*- Mode: Java; 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/. */
+
+package org.mozilla.geckoview_example;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.GeckoView;
+import org.mozilla.gecko.PrefsHelper;
+
+public class GeckoViewActivity extends Activity {
+    private static final String LOGTAG = "GeckoViewActivity";
+
+    GeckoView mGeckoView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.geckoview_activity);
+
+        mGeckoView = (GeckoView) findViewById(R.id.gecko_view);
+        mGeckoView.setChromeDelegate(new MyGeckoViewChrome());
+        mGeckoView.setContentDelegate(new MyGeckoViewContent());
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        final GeckoProfile profile = GeckoProfile.get(getApplicationContext());
+
+        GeckoThread.init(profile, /* args */ null, /* action */ null, /* debugging */ false);
+        GeckoThread.launch();
+    }
+
+    private class MyGeckoViewChrome implements GeckoView.ChromeDelegate {
+        @Override
+        public void onReady(GeckoView view) {
+            Log.i(LOGTAG, "Gecko is ready");
+            // // Inject a script that adds some code to the content window
+            // mGeckoView.importScript("resource://android/assets/script.js");
+
+            // Set up remote debugging to a port number
+            PrefsHelper.setPref("layers.dump", true);
+            PrefsHelper.setPref("devtools.debugger.remote-port", 6000);
+            PrefsHelper.setPref("devtools.debugger.unix-domain-socket", "");
+            PrefsHelper.setPref("devtools.debugger.remote-enabled", true);
+
+            // The Gecko libraries have finished loading and we can use the rendering engine.
+            // Let's add a browser (required) and load a page into it.
+            // mGeckoView.addBrowser(getResources().getString(R.string.default_url));
+        }
+
+        @Override
+        public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+            Log.i(LOGTAG, "Alert!");
+            result.confirm();
+            Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
+        }
+
+        @Override
+        public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, final GeckoView.PromptResult result) {
+            Log.i(LOGTAG, "Confirm!");
+            new AlertDialog.Builder(GeckoViewActivity.this)
+                .setTitle("javaScript dialog")
+                .setMessage(message)
+                .setPositiveButton(android.R.string.ok,
+                                   new DialogInterface.OnClickListener() {
+                                       public void onClick(DialogInterface dialog, int which) {
+                                           result.confirm();
+                                       }
+                                   })
+                .setNegativeButton(android.R.string.cancel,
+                                   new DialogInterface.OnClickListener() {
+                                       public void onClick(DialogInterface dialog, int which) {
+                                           result.cancel();
+                                       }
+                                   })
+                .create()
+                .show();
+        }
+
+        @Override
+        public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result) {
+            result.cancel();
+        }
+
+        @Override
+        public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) {
+            Log.i(LOGTAG, "Remote Debug!");
+            result.confirm();
+        }
+
+        @Override
+        public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result) {
+            Log.i(LOGTAG, "Got Script Message: " + data.toString());
+            String type = data.getString("type");
+            if ("fetch".equals(type)) {
+                Bundle ret = new Bundle();
+                ret.putString("name", "Mozilla");
+                ret.putString("url", "https://mozilla.org");
+                result.success(ret);
+            }
+        }
+    }
+
+    private class MyGeckoViewContent implements GeckoView.ContentDelegate {
+        @Override
+        public void onPageStart(GeckoView view, GeckoView.Browser browser, String url) {
+
+        }
+
+        @Override
+        public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success) {
+
+        }
+
+        @Override
+        public void onPageShow(GeckoView view, GeckoView.Browser browser) {
+
+        }
+
+        @Override
+        public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title) {
+            Log.i(LOGTAG, "Received a title: " + title);
+        }
+
+        @Override
+        public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size) {
+            Log.i(LOGTAG, "Received a favicon URL: " + url);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
@@ -0,0 +1,13 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="fill_parent"
+              android:layout_height="fill_parent"
+              android:orientation="vertical">
+
+    <org.mozilla.gecko.GeckoView
+        android:id="@+id/gecko_view"
+        android:layout_width="fill_parent"
+        android:layout_height="match_parent"
+        android:scrollbars="none"
+        />
+
+</LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#3F51B5</color>
+    <color name="colorPrimaryDark">#303F9F</color>
+    <color name="colorAccent">#FF4081</color>
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">geckoview_example</string>
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/test/java/org/mozilla/geckoview_example/ExampleUnitTest.java
@@ -0,0 +1,15 @@
+package org.mozilla.geckoview_example;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * To work on unit tests, switch the Test Artifact in the Build Variants view.
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() throws Exception {
+        assertEquals(4, 2 + 2);
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle/with_gecko_binaries.gradle
@@ -0,0 +1,105 @@
+// 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:automation wants :geckoview to not include Gecko binaries (automation
+// * build, before package)
+//
+// * :geckoview:withLibraries wants :geckoview to include Gecko binaries
+// * (automation build, after package)
+//
+// * non-:app:automation 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) {
+    dependsOn rootProject.generateCodeAndResources
+
+    // See comment in :omnijar project regarding interface mismatches here.
+    inputs.source project(':omnijar').sourceSets.main.resources.srcDirs
+
+    // Produce a single output file.
+    outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
+
+    workingDir "${topobjdir}"
+
+    commandLine mozconfig.substs.GMAKE
+    args '-C'
+    args "${topobjdir}/mobile/android/base"
+    args 'gradle-omnijar'
+
+    // Only show the output if something went wrong.
+    ignoreExitValue = true
+    standardOutput = new ByteArrayOutputStream()
+    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) {
+    into("${project.buildDir}/generated/omnijar")
+    from("${topobjdir}/dist/fennec/assets") {
+        include 'omni.ja'
+    }
+}
+
+task checkLibsExistInDistDir<< {
+    if (syncLibsFromDistDir.source.empty) {
+        throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib.  Have you built and packaged?")
+    }
+}
+
+task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
+    into("${project.buildDir}/generated/jniLibs")
+    from("${topobjdir}/dist/fennec/lib")
+}
+
+task checkAssetsExistInDistDir<< {
+    if (syncAssetsFromDistDir.source.empty) {
+        throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets.  Have you built and packaged?")
+    }
+}
+
+task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
+    into("${project.buildDir}/generated/assets")
+    from("${topobjdir}/dist/fennec/assets") {
+        exclude 'omni.ja'
+    }
+}
+
+ext.configureVariantWithGeckoBinaries = { variant ->
+    // Like 'local' or 'localOld'; may be null.
+    def productFlavor = variant.productFlavors ? variant.productFlavors[0].name : ""
+    // Like 'debug' or 'release'.
+    def buildType = variant.buildType.name
+
+    syncOmnijarFromDistDir.dependsOn buildOmnijar
+    def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
+    generateAssetsTask.dependsOn syncOmnijarFromDistDir
+    generateAssetsTask.dependsOn syncLibsFromDistDir
+    generateAssetsTask.dependsOn syncAssetsFromDistDir
+
+    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
+}
--- a/mobile/android/thirdparty/build.gradle
+++ b/mobile/android/thirdparty/build.gradle
@@ -20,17 +20,16 @@ android {
         abortOnError false
     }
 
     sourceSets {
         main {
             manifest.srcFile 'AndroidManifest.xml'
             java {
                 srcDir '.'
-                srcDir "${topsrcdir}/mobile/android/geckoview/src/thirdparty/java"
 
                 if (!mozconfig.substs.MOZ_INSTALL_TRACKING) {
                     exclude 'com/adjust/**'
                 }
 
                 // Exclude LeakCanary: It will be added again via a gradle dependency. This version
                 // here is only the no-op library for mach-based builds.
                 exclude 'com/squareup/leakcanary/**'
--- a/settings.gradle
+++ b/settings.gradle
@@ -24,20 +24,24 @@ if (json.substs.MOZ_BUILD_APP != 'mobile
 // Set the Android SDK location.  This is the *least specific* mechanism, which
 // is unfortunate: we'd prefer to use the *most specific* mechanism.  That is,
 // local.properties (first 'sdk.dir', then 'android.dir') and then the
 // environment variable ANDROID_HOME will override this.  That's unfortunate,
 // but it's hard to automatically arrange better.
 System.setProperty('android.home', json.substs.ANDROID_SDK_ROOT)
 
 include ':app'
+include ':geckoview'
+include ':geckoview_example'
 include ':omnijar'
 include ':thirdparty'
 
 project(':app').projectDir = new File("${json.topsrcdir}/mobile/android/app")
+project(':geckoview').projectDir = new File("${json.topsrcdir}/mobile/android/geckoview")
+project(':geckoview_example').projectDir = new File("${json.topsrcdir}/mobile/android/geckoview_example")
 project(':omnijar').projectDir = new File("${json.topsrcdir}/mobile/android/app/omnijar")
 project(':thirdparty').projectDir = new File("${json.topsrcdir}/mobile/android/thirdparty")
 
 if (json.substs.MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER) {
     include ':bouncer'
     project(':bouncer').projectDir = new File("${json.topsrcdir}/mobile/android/bouncer")
 }
 
--- a/testing/mozharness/configs/builds/releng_sub_android_configs/64_api_15_gradle_dependencies.py
+++ b/testing/mozharness/configs/builds/releng_sub_android_configs/64_api_15_gradle_dependencies.py
@@ -6,11 +6,16 @@ config = {
     'tooltool_manifest_src': 'mobile/android/config/tooltool-manifests/android-gradle-dependencies/releng.manifest',
     'multi_locale_config_platform': 'android',
     'postflight_build_mach_commands': [
         ['gradle',
          'assembleAutomationRelease',
          'assembleAutomationDebug',
          'assembleAutomationDebugAndroidTest',
          'checkstyle',
+         # Does not include Gecko binaries -- see mobile/android/gradle/with_gecko_binaries.gradle.
+         'geckoview:assembleWithoutGeckoBinaries',
+         # So that we pick up the test dependencies for the builders.
+         'geckoview_example:assembleWithoutGeckoBinaries',
+         'geckoview_example:assembleWithoutGeckoBinariesAndroidTest',
         ],
     ],
 }