misc/androidstudio: add support for the android gradle plugin

The gobind plugin exposes a set of Go packages as a AAR file ready to be
used by and Android project. Unfortunately, being a library project
limits the Go packages to access only the Java API from the standard
library and the Android SDK.

This CL tightens the integration with the Android plugin to support access
to project dependencies such as the Android Support Library, to the generated
R.* resource classes and finally to the Android databinding classes.

When the gradle project has loaded the Android plugin, the generation of
the Go library is split in two.
First, the gobind tool generates the Java classes for the bound Go
packages. In this step, Go packages can access the standard Java and Android
libraries as well as project dependencies. After this step, Android
databinding layout files can refer to Go classes.
In step two, the gomobile tool generates the JNI libraries
with the Go implementation of the generated Java classes. In this
step, Go can access the standard Java and Android libraries,
dependencies as well as R.* and generated databinding classes.

Change-Id: If853ecabdbd01eec5f89d064a6bc715cb20a4d83
Reviewed-on: https://go-review.googlesource.com/30094
Reviewed-by: David Crawshaw <crawshaw@golang.org>
diff --git a/misc/androidstudio/README.md b/misc/androidstudio/README.md
index d9abe0d..cbd187c 100644
--- a/misc/androidstudio/README.md
+++ b/misc/androidstudio/README.md
@@ -18,14 +18,17 @@
   // Optional list of architectures. Defaults to all supported architectures.
   GOARCH="arm amd64"
 
-  // Absolute path to the gomobile binary
+  // Absolute path to the gomobile binary. Optional.
   GOMOBILE "/mypath/bin/gomobile"
 
-  // Absolute path to the go binary
+  // Absolute path to the gomobile binary. Optional.
+  GOBIND "/mypath/bin/gobind"
+
+  // Absolute path to the go binary. Optional.
   GO "/usr/local/go/bin/go"
 
-  // Pass extra parameters to command line
-  // GOMOBILEFLAGS "-javapkg my.java.package"
+  // Pass extra parameters to command line. Optional.
+  GOMOBILEFLAGS "-javapkg my.java.package"
 }
 </pre>
 
diff --git a/misc/androidstudio/build.gradle b/misc/androidstudio/build.gradle
index 545a885..2f04c3a 100644
--- a/misc/androidstudio/build.gradle
+++ b/misc/androidstudio/build.gradle
@@ -27,7 +27,7 @@
   testCompile 'junit:junit:4.11'
 }
 
-version = '0.2.6'
+version = '0.2.7'
 
 pluginBundle {
   website = 'https://golang.org/x/mobile'
diff --git a/misc/androidstudio/src/main/groovy/org/golang/mobile/GobindPlugin.groovy b/misc/androidstudio/src/main/groovy/org/golang/mobile/GobindPlugin.groovy
index 3f1a332..37f57d4 100644
--- a/misc/androidstudio/src/main/groovy/org/golang/mobile/GobindPlugin.groovy
+++ b/misc/androidstudio/src/main/groovy/org/golang/mobile/GobindPlugin.groovy
@@ -11,7 +11,11 @@
 import org.gradle.api.Project
 import org.gradle.api.Plugin
 import org.gradle.api.Task
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.OutputDirectory
 import org.gradle.api.tasks.TaskAction
 
 import org.golang.mobile.OutputFileTask
@@ -21,31 +25,81 @@
  * GobindPlugin configures the default project that builds .AAR file
  * from a go package, using gomobile bind command.
  * For gomobile bind command, see https://golang.org/x/mobile/cmd/gomobile
+ *
+ * If the project has the android or android library plugin loaded, GobindPlugin
+ * hooks into the android build lifecycle in two steps. First, the Java classes are
+ * generated and registered with the android plugin. Then, when the databinding
+ * classes and the R classes are generated and compiled, the GobindPlugin generates
+ * the JNI libraries. By splitting the binding in two steps, the Android databinding
+ * machinery can resolve Go classes, and Go code can access the resulting databinding
+ * classes as well as the R resource classes.
  */
 class GobindPlugin implements Plugin<Project> {
 	void apply(Project project) {
-		project.configurations.create("default")
 		project.extensions.create('gobind', GobindExtension)
+		// If the android or android library plugin is loaded, integrate
+		// directly with the android build cycle
+		if (project.plugins.hasPlugin("android") ||
+				project.plugins.hasPlugin("com.android.application")) {
+			project.android.applicationVariants.all { variant ->
+				handleVariant(project, variant)
+			}
+			return
+		}
+		if (project.plugins.hasPlugin("android-library") ||
+				project.plugins.hasPlugin("com.android.library")) {
+			project.android.libraryVariants.all { variant ->
+				handleVariant(project, variant)
+			}
+			return
+		}
 
-		Task gobindTask = project.tasks.create("gobind", GobindTask)
-		gobindTask.outputFile = project.file(project.name+".aar")
+		// Library mode: generate and declare the .aar file for parent
+		// projects to include.
+		project.configurations.create("default")
+
+		Task gomobileTask = project.tasks.create("gobind", GomobileTask)
+		gomobileTask.outputFile = project.file(project.name+".aar")
 		project.artifacts.add("default", new AARPublishArtifact(
 			'mylib',
 			null,
-			gobindTask))
+			gomobileTask))
 
 		Task cleanTask = project.tasks.create("clean", {
 			project.delete(project.name+".aar")
 		})
 	}
+
+	private static void handleVariant(Project project, def variant) {
+		File outputDir = project.file("$project.buildDir/generated/source/gobind/$variant.dirName")
+		// First, generate the Java classes with the gobind tool.
+		Task bindTask = project.tasks.create("gobind${variant.name.capitalize()}", GobindTask)
+		bindTask.outputDir = outputDir
+		bindTask.classpath = variant.javaCompile.classpath
+		bindTask.bootClasspath = variant.javaCompile.options.bootClasspath
+		// TODO: Detect when updating the Java classes is redundant.
+		bindTask.outputs.upToDateWhen { false }
+		variant.registerJavaGeneratingTask(bindTask, outputDir)
+		// Then, generate the JNI libraries with the gomobile tool.
+		Task libTask = project.tasks.create("gomobile${variant.name.capitalize()}", GomobileTask)
+		libTask.bootClasspath = variant.javaCompile.options.bootClasspath
+		// Add the R and databinding classes to the gomobile classpath.
+		libTask.classpath = project.files(variant.javaCompile.classpath, variant.javaCompile.destinationDir)
+		// Dump the JNI libraries in the known project jniLibs directory.
+		// TODO: Use a directory below build for the libraries instead. Adding a jni directory to the jniLibs
+		// property of android.sourceSets only works, but only if the directory changes every build.
+		libTask.libsDir = project.file("src/main/jniLibs")
+		// TODO: Detect when building the existing JNI libraries is redundant.
+		libTask.outputs.upToDateWhen { false }
+		libTask.dependsOn(bindTask)
+		variant.javaCompile.finalizedBy(libTask)
+	}
 }
 
-class GobindTask extends DefaultTask implements OutputFileTask {
-	@OutputFile
-	File outputFile
+class BindTask extends DefaultTask {
+	String bootClasspath
 
-	@TaskAction
-	def gobind() {
+	def run(String cmd, String cmdPath, List<String> cmdArgs) {
 		def pkg = project.gobind.pkg.trim()
 		def gopath = (project.gobind.GOPATH ?: System.getenv("GOPATH"))?.trim()
 		if (!pkg || !gopath) {
@@ -61,16 +115,15 @@
 			paths = paths + "/usr/local/go/bin"
 		}
 
-		def gomobile = (project.gobind.GOMOBILE ?: findExecutable("gomobile", paths))?.trim()
+		def exe = (cmdPath ?: findExecutable(cmd, paths))?.trim()
 		def gobin = (project.gobind.GO ?: findExecutable("go", paths))?.trim()
 		def gomobileFlags = project.gobind.GOMOBILEFLAGS?.trim()
-		def goarch = project.gobind.GOARCH?.trim()
 
-		if (!gomobile || !gobin) {
-			throw new GradleException('failed to find gomobile/go tools. Set gobind.GOMOBILE and gobind.GO')
+		if (!exe || !gobin) {
+			throw new GradleException('failed to find ${cmd}/go tools. Set gobind.GOBIND, gobind.GOMOBILE, and gobind.GO')
 		}
 
-		paths = [findDir(gomobile), findDir(gobin), paths].flatten()
+		paths = [findDir(exe), findDir(gobin), paths].flatten()
 
 		def androidHome = ""
 		try {
@@ -86,20 +139,16 @@
 		}
 
 		project.exec {
-			executable(gomobile)
+			executable(exe)
 
-			def cmd = ["bind", "-i", "-o", project.name+".aar", "-target"]
-			if (goarch) {
-				cmd = cmd+goarch.split(" ").collect{ 'android/'+it }.join(",")
-			} else {
-				cmd << "android"
-			}
+			if (bootClasspath)
+				cmdArgs.addAll(["-bootclasspath", bootClasspath])
 			if (gomobileFlags) {
-				cmd.addAll(gomobileFlags.split(" "))
+				cmdArgs.addAll(gomobileFlags.split(" "))
 			}
-			cmd.addAll(pkg.split(" "))
+			cmdArgs.addAll(pkg.split(" "))
 
-			args(cmd)
+			args(cmdArgs)
 			if (!androidHome?.trim()) {
 				throw new GradleException('Neither sdk.dir or ANDROID_HOME is set')
 			}
@@ -136,6 +185,65 @@
 	}
 }
 
+class GobindTask extends BindTask {
+	@OutputDirectory
+	File outputDir
+
+	FileCollection classpath
+
+	@TaskAction
+	def gobind() {
+		run("gobind", project.gobind.GOBIND, ["-lang", "java", "-classpath", classpath.join(File.pathSeparator), "-outdir", outputDir.getAbsolutePath()])
+	}
+}
+
+class GomobileTask extends BindTask implements OutputFileTask {
+	@Optional
+	@OutputFile
+	File outputFile
+
+	@Optional
+	@OutputDirectory
+	File libsDir
+
+	FileCollection classpath
+
+	@TaskAction
+	def gomobile() {
+		if (outputFile == null) {
+			outputFile = File.createTempFile("gobind-", ".aar")
+		}
+		def cmd = ["bind", "-i"]
+		if (classpath) {
+			cmd << "-classpath"
+			cmd << classpath.join(File.pathSeparator)
+		}
+		cmd << "-o"
+		cmd << outputFile.getAbsolutePath()
+		cmd << "-target"
+		def goarch = project.gobind.GOARCH?.trim()
+		if (goarch) {
+			cmd = cmd+goarch.split(" ").collect{ 'android/'+it }.join(",")
+		} else {
+			cmd << "android"
+		}
+		run("gomobile", project.gobind.GOMOBILE, cmd)
+		// If libsDir is set, unpack (only) the JNI libraries to it.
+		if (libsDir != null) {
+			project.delete project.fileTree(dir: libsDir, include: '*/libgojni.so')
+			def zipFile = new java.util.zip.ZipFile(outputFile)
+			zipFile.entries().findAll { !it.directory && it.name.startsWith("jni/") }.each {
+				def libFile = new File(libsDir, it.name.substring(4))
+				libFile.parentFile.mkdirs()
+				zipFile.getInputStream(it).withStream {
+					libFile.append(it)
+				}
+			}
+			outputFile.delete()
+		}
+	}
+}
+
 class GobindExtension {
 	// Package to bind. Separate multiple packages with spaces. (required)
 	def String pkg = ""
@@ -152,6 +260,9 @@
 	// GOMOBILE: path to gomobile binary. (can omit if 'gomobile' is under GOPATH)
 	def String GOMOBILE = ""
 
+	// GOBIND: path to gobind binary. (can omit if 'gobind' is under GOPATH)
+	def String GOBIND = ""
+
 	// GOMOBILEFLAGS: extra flags to be passed to gomobile command. (optional)
 	def String GOMOBILEFLAGS = ""
 }