devtools/madb: initial version of madb install command

"madb install" is added for the first time, which can figure our the
best matching .apk for each device, based on abis supported by the
device.

What to do next:
* handle device screen density filters as well.
* --build, --rebuild flags
* decide what to do with Flutter projects.

Change-Id: I2d3d817f3c5c5112bb36e99fdc6c9ce2856a4b34
diff --git a/clear_data.go b/clear_data.go
index 788dfea..3e0f269 100644
--- a/clear_data.go
+++ b/clear_data.go
@@ -16,7 +16,7 @@
 }
 
 var cmdMadbClearData = &cmdline.Command{
-	Runner: subCommandRunner{initMadbClearData, runMadbClearDataForDevice},
+	Runner: subCommandRunner{initMadbClearData, runMadbClearDataForDevice, true},
 	Name:   "clear-data",
 	Short:  "Clear your app data from all devices",
 	Long: `
@@ -42,11 +42,11 @@
 `,
 }
 
-func initMadbClearData(env *cmdline.Env, args []string) ([]string, error) {
-	return initMadbCommand(env, args, false, false)
+func initMadbClearData(env *cmdline.Env, args []string, properties variantProperties) ([]string, error) {
+	return initMadbCommand(env, args, properties, false, false)
 }
 
-func runMadbClearDataForDevice(env *cmdline.Env, args []string, d device) error {
+func runMadbClearDataForDevice(env *cmdline.Env, args []string, d device, properties variantProperties) error {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
diff --git a/doc.go b/doc.go
index bb3834a..ecd2329 100644
--- a/doc.go
+++ b/doc.go
@@ -18,6 +18,7 @@
    clear-data  Clear your app data from all devices
    exec        Run the provided adb command on all devices and emulators
                concurrently
+   install     Install your app on all devices
    name        Manage device nicknames
    start       Launch your app on all devices
    stop        Stop your app on all devices
@@ -121,6 +122,61 @@
    '@' sign followed by the index of the device in the output of 'adb devices'
    command, starting from 1. Command will be run only on specified devices.
 
+Madb install - Install your app on all devices
+
+Installs your app on all devices.
+
+To install your app for a specific user on a particular device, use 'madb user
+set' command to set the default user ID for that device. (See 'madb help user'
+for more details.)
+
+If the working directory contains a Gradle Android project (i.e., has
+"build.gradle"), this command will run a small Gradle script to extract the
+variant properties, which will be used to find the best matching .apk for each
+device.
+
+In this case, the extracted properties are cached, so that "madb install" can be
+repeated without even running the Gradle script again. The IDs can be
+re-extracted by clearing the cache by providing "-clear-cache" flag.
+
+This command is similar to running "gradlew :<moduleName>:<variantName>Install",
+but the gradle command is limited in that 1) it always installs the app to all
+connected devices, and 2) it installs the app on one device at a time
+sequentially.
+
+To install a specific .apk file to all devices, use "madb exec install
+<path_to_apk>" instead.
+
+Usage:
+   madb install [flags]
+
+The madb install flags are:
+ -clear-cache=false
+   Clear the cache and re-extract the variant properties such as the application
+   ID and the main activity name. Only takes effect when no arguments are
+   provided.
+ -module=
+   Specify which application module to use, when the current directory is the
+   top level Gradle project containing multiple sub-modules. When not specified,
+   the first available application module is used. Only takes effect when no
+   arguments are provided.
+ -r=true
+   Replace the existing application. Same effect as the '-r' flag of 'adb
+   install' command.
+ -variant=
+   Specify which build variant to use. When not specified, the first available
+   build variant is used. Only takes effect when no arguments are provided.
+
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name'). A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1. Command will be run only on specified devices.
+
 Madb name - Manage device nicknames
 
 Manages device nicknames, which are meant to be more human-friendly compared to
diff --git a/exec.go b/exec.go
index 42b182b..79239e6 100644
--- a/exec.go
+++ b/exec.go
@@ -30,7 +30,7 @@
 `,
 }
 
-func runMadbExecForDevice(env *cmdline.Env, args []string, d device) error {
+func runMadbExecForDevice(env *cmdline.Env, args []string, d device, properties variantProperties) error {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
diff --git a/install.go b/install.go
new file mode 100644
index 0000000..133e430
--- /dev/null
+++ b/install.go
@@ -0,0 +1,174 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	"v.io/x/lib/cmdline"
+	"v.io/x/lib/gosh"
+)
+
+var (
+	replaceFlag bool
+)
+
+func init() {
+	initializePropertyCacheFlags(&cmdMadbInstall.Flags)
+	cmdMadbInstall.Flags.BoolVar(&replaceFlag, "r", true, `Replace the existing application. Same effect as the '-r' flag of 'adb install' command.`)
+}
+
+var cmdMadbInstall = &cmdline.Command{
+	Runner: subCommandRunner{nil, runMadbInstallForDevice, true},
+	Name:   "install",
+	Short:  "Install your app on all devices",
+	Long: `
+Installs your app on all devices.
+
+To install your app for a specific user on a particular device, use 'madb user set' command to set
+the default user ID for that device. (See 'madb help user' for more details.)
+
+If the working directory contains a Gradle Android project (i.e., has "build.gradle"), this command
+will run a small Gradle script to extract the variant properties, which will be used to find the
+best matching .apk for each device.
+
+In this case, the extracted properties are cached, so that "madb install" can be repeated without
+even running the Gradle script again. The IDs can be re-extracted by clearing the cache by providing
+"-clear-cache" flag.
+
+This command is similar to running "gradlew :<moduleName>:<variantName>Install", but the gradle
+command is limited in that 1) it always installs the app to all connected devices, and 2) it
+installs the app on one device at a time sequentially.
+
+To install a specific .apk file to all devices, use "madb exec install <path_to_apk>" instead.
+`,
+}
+
+func runMadbInstallForDevice(env *cmdline.Env, args []string, d device, properties variantProperties) error {
+	sh := gosh.NewShell(nil)
+	defer sh.Cleanup()
+
+	sh.ContinueOnError = true
+	if isGradleProject(wd) {
+		// Get the necessary device properties.
+		// TODO(youngseokyoon): get the device density.
+		deviceAbis, err := getSupportedAbisForDevice(d)
+		if err != nil {
+			return err
+		}
+
+		bestOutput := computeBestOutput(properties.VariantOutputs, properties.AbiFilters, 0, deviceAbis)
+		if bestOutput == nil {
+			return fmt.Errorf("Could not find the matching .apk for device %q", d.displayName())
+		}
+
+		// Run the install command.
+		cmdArgs := []string{"-s", d.Serial, "install"}
+		if replaceFlag {
+			cmdArgs = append(cmdArgs, "-r")
+		}
+		if d.UserID != "" {
+			cmdArgs = append(cmdArgs, "--user", d.UserID)
+		}
+		cmdArgs = append(cmdArgs, bestOutput.OutputFilePath)
+		cmd := sh.Cmd("adb", cmdArgs...)
+		return runGoshCommandForDevice(cmd, d, true)
+	}
+
+	// TOOD(youngseokyoon): support Flutter projects.
+	if isFlutterProject(wd) {
+		return fmt.Errorf("Not implemented for flutter yet.")
+	}
+
+	return fmt.Errorf("Could not find the target app to be installed. Try running 'madb install' from a Gradle or Flutter project directory.")
+}
+
+// getSupportedAbisForDevice returns all the abis supported by the given device.
+func getSupportedAbisForDevice(d device) ([]string, error) {
+	sh := gosh.NewShell(nil)
+	defer sh.Cleanup()
+
+	sh.ContinueOnError = true
+
+	cmd := sh.Cmd("adb", "-s", d.Serial, "shell", "am", "get-config")
+	output := cmd.Stdout()
+
+	if sh.Err != nil {
+		return nil, sh.Err
+	}
+
+	return parseSupportedAbis(output)
+}
+
+// parseSupportedAbis takes the output of "adb shell am get-config" command, and extracts the supported abis.
+func parseSupportedAbis(output string) ([]string, error) {
+	prefix := "abi: "
+	lines := strings.Split(output, "\n")
+	for _, line := range lines {
+		if strings.HasPrefix(line, prefix) {
+			abis := strings.Split(line[len(prefix):], ",")
+			return abis, nil
+		}
+	}
+
+	return nil, fmt.Errorf("Could not extract the abi list from the device configuration output.")
+}
+
+// computeBestOutput returns the pointer of the best matching output among the multiple variant outputs,
+// given the device density and the abis supported by the device.
+// The logic of this function is similar to that of SplitOutputMatcher.java in the Android platform tools.
+func computeBestOutput(variantOutputs []variantOutput, variantAbiFilters []string, deviceDensity int, deviceAbis []string) *variantOutput {
+	matches := map[*variantOutput]bool{}
+
+VariantOutputLoop:
+	for i, vo := range variantOutputs {
+
+	FilterLoop:
+		for _, filter := range vo.Filters {
+
+			// TODO(youngseokyoon): check the density filter too.
+			switch filter.FilterType {
+			case "ABI":
+				for _, supportedAbi := range deviceAbis {
+					if filter.Identifier == supportedAbi {
+						// This filter is satisfied. Check for the next filter.
+						continue FilterLoop
+					}
+				}
+
+				// If the abi filter is not in the device supported abi list,
+				// this variant output is not compatible with the device.
+				// Check the next variant output.
+				continue VariantOutputLoop
+			}
+		}
+
+		matches[&variantOutputs[i]] = true
+	}
+
+	// Return nil, if there are no matching variant outputs.
+	if len(matches) == 0 {
+		return nil
+	}
+
+	// Find the matching variant output with the maximum version code.
+	// Iterate "variantOutputs" slice instead of "matches" map, in order to tie-break the matches
+	// with same version codes by the order they are provided. (earlier defined output wins)
+	var result, cur *variantOutput
+	for i := range variantOutputs {
+		cur = &variantOutputs[i]
+		// Consider only the matching outputs
+		if _, ok := matches[cur]; !ok {
+			continue
+		}
+
+		if result == nil || result.VersionCode < cur.VersionCode {
+			result = cur
+		}
+	}
+
+	return result
+}
diff --git a/install_test.go b/install_test.go
new file mode 100644
index 0000000..32f11bc
--- /dev/null
+++ b/install_test.go
@@ -0,0 +1,78 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"path/filepath"
+	"reflect"
+	"testing"
+)
+
+func TestParseSupportedAbis(t *testing.T) {
+	tests := []struct {
+		output string
+		want   []string
+	}{
+		{
+			`config: en-rUS-ldltr-sw411dp-w411dp-h659dp-normal-notlong-notround-port-notnight-420dpi-finger-keysexposed-nokeys-navhidden-nonav-v23
+abi: arm64-v8a,armeabi-v7a,armeabi
+`,
+			[]string{"arm64-v8a", "armeabi-v7a", "armeabi"},
+		},
+	}
+
+	for i, test := range tests {
+		got, err := parseSupportedAbis(test.output)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !reflect.DeepEqual(got, test.want) {
+			t.Fatalf("unmatched results for tests[%v]: got %v, want %v", i, got, test.want)
+		}
+	}
+}
+
+// TODO(youngseokyoon): add tests for the density splits too.
+func TestComputeBestOutputForAbiSplits(t *testing.T) {
+	// Read the variant properties from the "testAndroidAbiSplit" project.
+	key := variantKey{
+		Dir:     filepath.Join("testdata", "projects", "testAndroidAbiSplit"),
+		Module:  "app",
+		Variant: "Debug",
+	}
+
+	props, err := extractPropertiesFromGradle(key)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Make sure that it has three outputs for each Abi.
+	if len(props.VariantOutputs) != 3 {
+		t.Fatalf("The number of extracted variant outputs does not match the expected value.")
+	}
+
+	outputX86 := &props.VariantOutputs[0]
+	outputArmv7a := &props.VariantOutputs[1]
+	outputMips := &props.VariantOutputs[2]
+
+	// Now create fake device properties and test whether the computeBestOutput returns the correct variantOutput.
+	deviceDensity := 240
+	tests := []struct {
+		deviceAbis []string
+		want       *variantOutput
+	}{
+		{[]string{"x86"}, outputX86},
+		{[]string{"armeabi-v7a"}, outputArmv7a},
+		{[]string{"mips"}, outputMips},
+		{[]string{"x86", "armeabi-v7a"}, outputX86},
+		{[]string{"x86_64"}, nil},
+	}
+
+	for i, test := range tests {
+		if got := computeBestOutput(props.VariantOutputs, props.AbiFilters, deviceDensity, test.deviceAbis); got != test.want {
+			t.Fatalf("unmatched results for tests[%v]: got %v, want %v", i, got, test.want)
+		}
+	}
+}
diff --git a/madb.go b/madb.go
index 502e3f8..546bf7f 100644
--- a/madb.go
+++ b/madb.go
@@ -61,6 +61,7 @@
 	Children: []*cmdline.Command{
 		cmdMadbClearData,
 		cmdMadbExec,
+		cmdMadbInstall,
 		cmdMadbName,
 		cmdMadbStart,
 		cmdMadbStop,
@@ -330,10 +331,13 @@
 	// be performed once, before directing the command to all the devices.
 	// The returned string slice becomes the new set of arguments passed into
 	// the sub command.
-	init func(env *cmdline.Env, args []string) ([]string, error)
+	init func(env *cmdline.Env, args []string, properties variantProperties) ([]string, error)
 	// subCmd defines the behavior of the sub command which will run on all the
 	// devices in parallel.
-	subCmd func(env *cmdline.Env, args []string, d device) error
+	subCmd func(env *cmdline.Env, args []string, d device, properties variantProperties) error
+	// extractProperties indicates whether this subCommand needs the extracted
+	// project properties.
+	extractProperties bool
 }
 
 var _ cmdline.Runner = (*subCommandRunner)(nil)
@@ -349,9 +353,18 @@
 		return err
 	}
 
+	// Extract the properties if needed.
+	properties := variantProperties{}
+	if r.extractProperties && isGradleProject(wd) {
+		properties, err = getProjectPropertiesUsingDefaultCache()
+		if err != nil {
+			return err
+		}
+	}
+
 	// Run the init function when provided.
 	if r.init != nil {
-		newArgs, err := r.init(env, args)
+		newArgs, err := r.init(env, args, properties)
 		if err != nil {
 			return err
 		}
@@ -370,7 +383,7 @@
 
 		wg.Add(1)
 		go func() {
-			if err := r.subCmd(env, args, deviceCopy); err != nil {
+			if err := r.subCmd(env, args, deviceCopy, properties); err != nil {
 				errs = append(errs, err)
 				errDevices = append(errDevices, deviceCopy)
 			}
@@ -412,7 +425,7 @@
 	return cmd.Shell().Err
 }
 
-func initMadbCommand(env *cmdline.Env, args []string, flutterPassthrough bool, activityNameRequired bool) ([]string, error) {
+func initMadbCommand(env *cmdline.Env, args []string, properties variantProperties, flutterPassthrough bool, activityNameRequired bool) ([]string, error) {
 	var numRequiredArgs int
 	var requiredArgsStr string
 
@@ -435,23 +448,22 @@
 
 	// Try to extract the application ID and the main activity name from the Gradle scripts.
 	if isGradleProject(wd) {
-		cacheFile, err := getDefaultCacheFilePath()
-		if err != nil {
-			return nil, err
-		}
-
-		key := variantKey{wd, moduleFlag, variantFlag}
-		properties, err := getProjectProperties(extractPropertiesFromGradle, key, clearCacheFlag, cacheFile)
-		if err != nil {
-			return nil, err
-		}
-
 		args = []string{properties.AppID, properties.Activity}[:numRequiredArgs]
 	}
 
 	return args, nil
 }
 
+func getProjectPropertiesUsingDefaultCache() (variantProperties, error) {
+	cacheFile, err := getDefaultCacheFilePath()
+	if err != nil {
+		return variantProperties{}, err
+	}
+
+	key := variantKey{wd, moduleFlag, variantFlag}
+	return getProjectProperties(extractPropertiesFromGradle, key, clearCacheFlag, cacheFile)
+}
+
 type variantProperties struct {
 	AppID          string
 	Activity       string
diff --git a/start.go b/start.go
index e1ee391..e724b1d 100644
--- a/start.go
+++ b/start.go
@@ -22,7 +22,7 @@
 }
 
 var cmdMadbStart = &cmdline.Command{
-	Runner: subCommandRunner{initMadbStart, runMadbStartForDevice},
+	Runner: subCommandRunner{initMadbStart, runMadbStartForDevice, true},
 	Name:   "start",
 	Short:  "Launch your app on all devices",
 	Long: `
@@ -58,11 +58,11 @@
 `,
 }
 
-func initMadbStart(env *cmdline.Env, args []string) ([]string, error) {
-	return initMadbCommand(env, args, true, true)
+func initMadbStart(env *cmdline.Env, args []string, properties variantProperties) ([]string, error) {
+	return initMadbCommand(env, args, properties, true, true)
 }
 
-func runMadbStartForDevice(env *cmdline.Env, args []string, d device) error {
+func runMadbStartForDevice(env *cmdline.Env, args []string, d device, properties variantProperties) error {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
diff --git a/stop.go b/stop.go
index 509dd37..4cabfbd 100644
--- a/stop.go
+++ b/stop.go
@@ -16,7 +16,7 @@
 }
 
 var cmdMadbStop = &cmdline.Command{
-	Runner: subCommandRunner{initMadbStart, runMadbStartForDevice},
+	Runner: subCommandRunner{initMadbStop, runMadbStopForDevice, true},
 	Name:   "stop",
 	Short:  "Stop your app on all devices",
 	Long: `
@@ -45,17 +45,17 @@
 `,
 }
 
-func initMadbStop(env *cmdline.Env, args []string) ([]string, error) {
-	return initMadbCommand(env, args, true, false)
+func initMadbStop(env *cmdline.Env, args []string, properties variantProperties) ([]string, error) {
+	return initMadbCommand(env, args, properties, true, false)
 }
 
-func runMadbStopForDevice(env *cmdline.Env, args []string, d device) error {
+func runMadbStopForDevice(env *cmdline.Env, args []string, d device, properties variantProperties) error {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
 	sh.ContinueOnError = true
 
-	if len(args) == 2 {
+	if len(args) == 1 {
 		appID := args[0]
 
 		// More details on the "adb shell am" command can be found at: http://developer.android.com/tools/help/shell.html#am
diff --git a/testdata/projects/testAndroidAbiSplit/app/build.gradle b/testdata/projects/testAndroidAbiSplit/app/build.gradle
new file mode 100644
index 0000000..9d6a2c9
--- /dev/null
+++ b/testdata/projects/testAndroidAbiSplit/app/build.gradle
@@ -0,0 +1,47 @@
+buildscript {
+    repositories {
+        jcenter()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:1.3.0'
+        classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.+'
+    }
+}
+
+apply plugin: 'android-sdk-manager'
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    defaultConfig {
+        applicationId "io.v.testProjectId"
+        minSdkVersion 23
+        targetSdkVersion 23
+        versionCode 1
+        versionName "1.0"
+    }
+    splits {
+        abi {
+            enable true
+            reset()
+            include 'x86', 'armeabi-v7a', 'mips'
+            universalApk false
+        }
+    }
+}
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+}
diff --git a/testdata/projects/testAndroidAbiSplit/app/src/main/AndroidManifest.xml b/testdata/projects/testAndroidAbiSplit/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..51518e6
--- /dev/null
+++ b/testdata/projects/testAndroidAbiSplit/app/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+    package="io.v.testProjectPackage"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <uses-sdk android:minSdkVersion="23"/>
+
+    <application
+        android:allowBackup="true"
+        android:label="Test Project"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity
+            android:name=".LauncherActivity"
+            android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".SecondActivity"
+            android:label="@string/app_name" >
+        </activity>
+        <activity android:name=".ThirdActivity" />
+    </application>
+
+</manifest>
diff --git a/testdata/projects/testAndroidAbiSplit/build.gradle b/testdata/projects/testAndroidAbiSplit/build.gradle
new file mode 100644
index 0000000..1b7886d
--- /dev/null
+++ b/testdata/projects/testAndroidAbiSplit/build.gradle
@@ -0,0 +1,19 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:1.3.0'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+    }
+}
diff --git a/testdata/projects/testAndroidAbiSplit/gradle.properties b/testdata/projects/testAndroidAbiSplit/gradle.properties
new file mode 100644
index 0000000..1d3591c
--- /dev/null
+++ b/testdata/projects/testAndroidAbiSplit/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
\ No newline at end of file
diff --git a/testdata/projects/testAndroidAbiSplit/gradle/wrapper/gradle-wrapper.jar b/testdata/projects/testAndroidAbiSplit/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
--- /dev/null
+++ b/testdata/projects/testAndroidAbiSplit/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/testdata/projects/testAndroidAbiSplit/gradle/wrapper/gradle-wrapper.properties b/testdata/projects/testAndroidAbiSplit/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..c83a3ba
--- /dev/null
+++ b/testdata/projects/testAndroidAbiSplit/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Nov 02 17:11:51 PST 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
diff --git a/testdata/projects/testAndroidAbiSplit/gradlew b/testdata/projects/testAndroidAbiSplit/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/testdata/projects/testAndroidAbiSplit/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/testdata/projects/testAndroidAbiSplit/settings.gradle b/testdata/projects/testAndroidAbiSplit/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/testdata/projects/testAndroidAbiSplit/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/uninstall.go b/uninstall.go
index 9843a16..1cb8a2b 100644
--- a/uninstall.go
+++ b/uninstall.go
@@ -21,7 +21,7 @@
 }
 
 var cmdMadbUninstall = &cmdline.Command{
-	Runner: subCommandRunner{initMadbUninstall, runMadbUninstallForDevice},
+	Runner: subCommandRunner{initMadbUninstall, runMadbUninstallForDevice, true},
 	Name:   "uninstall",
 	Short:  "Uninstall your app from all devices",
 	Long: `
@@ -47,11 +47,11 @@
 `,
 }
 
-func initMadbUninstall(env *cmdline.Env, args []string) ([]string, error) {
-	return initMadbCommand(env, args, false, false)
+func initMadbUninstall(env *cmdline.Env, args []string, properties variantProperties) ([]string, error) {
+	return initMadbCommand(env, args, properties, false, false)
 }
 
-func runMadbUninstallForDevice(env *cmdline.Env, args []string, d device) error {
+func runMadbUninstallForDevice(env *cmdline.Env, args []string, d device, properties variantProperties) error {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()