devtools/madb: add "madb uninstall" command.

Taking advantage of the id extraction implemented for madb start, madb
uninstall command uninstalls the target app from all devices without
requiring the user to specify the application ID.

Change-Id: I0d21de2d4c08b7a0ad4dd3670f1fdc7995d03290
diff --git a/doc.go b/doc.go
index 0f73147..ba62f4f 100644
--- a/doc.go
+++ b/doc.go
@@ -18,6 +18,7 @@
    exec        Run the provided adb command on all devices and emulators
                concurrently
    start       Launch your app on all devices
+   uninstall   Uninstall your app from all devices
    name        Manage device nicknames
    help        Display help for commands or topics
 
@@ -120,14 +121,55 @@
    Comma-separated device serials, qualifiers, or nicknames (set by 'madb
    name').  Command will be run only on specified devices.
 
+Madb uninstall - Uninstall your app from all devices
+
+Uninstall your app from all devices.
+
+Usage:
+   madb uninstall [flags] [<application_id>]
+
+<application_id> is usually the package name where the activities are defined.
+(See:
+http://tools.android.com/tech-docs/new-build-system/applicationid-vs-packagename)
+
+If the application_id is not specified, madb automatically determines which app
+to uninstall, based on the build scripts found in the current working directory.
+
+If the working directory contains a Gradle Android project (i.e., has
+"build.gradle"), run a small Gradle script to extract the application ID.  In
+this case, the extracted ID is cached, so that "madb uninstall" can be repeated
+without even running the Gradle script again.  The ID can be re-extracted by
+clearing the cache by providing "-clear-cache" flag.
+
+The madb uninstall flags are:
+ -clear-cache=false
+   Clear the cache and re-extract the application ID and the main activity name.
+   Only takes effect when no arguments are provided.
+ -keep-data=false
+   Keep the application data and cache directories.  Equivalent to '-k' flag in
+   'adb uninstall' command.
+ -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.
+ -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, or nicknames (set by 'madb
+   name').  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
 the device serials provided by adb tool.
 
-NOTE: Device specifier flags (-d, -e, -n) are ignored in all 'madb name'
-commands.
-
 Usage:
    madb name [flags] <command>
 
diff --git a/madb.go b/madb.go
index 04b719c..c9aab4a 100644
--- a/madb.go
+++ b/madb.go
@@ -9,6 +9,7 @@
 
 import (
 	"bytes"
+	"flag"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -27,16 +28,36 @@
 	allDevicesFlag   bool
 	allEmulatorsFlag bool
 	devicesFlag      string
+
+	clearCacheFlag bool
+	moduleFlag     string
+	variantFlag    string
+
+	wd string // working directory
 )
 
 func init() {
 	cmdMadb.Flags.BoolVar(&allDevicesFlag, "d", false, `Restrict the command to only run on real devices.`)
 	cmdMadb.Flags.BoolVar(&allEmulatorsFlag, "e", false, `Restrict the command to only run on emulators.`)
 	cmdMadb.Flags.StringVar(&devicesFlag, "n", "", `Comma-separated device serials, qualifiers, or nicknames (set by 'madb name').  Command will be run only on specified devices.`)
+
+	// Store the current working directory.
+	var err error
+	wd, err = os.Getwd()
+	if err != nil {
+		panic(err)
+	}
+}
+
+// initializes flags related to extracting and caching project ids.
+func initializeIDCacheFlags(flags *flag.FlagSet) {
+	flags.BoolVar(&clearCacheFlag, "clear-cache", false, `Clear the cache and re-extract the application ID and the main activity name.  Only takes effect when no arguments are provided.`)
+	flags.StringVar(&moduleFlag, "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.`)
+	flags.StringVar(&variantFlag, "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.`)
 }
 
 var cmdMadb = &cmdline.Command{
-	Children: []*cmdline.Command{cmdMadbExec, cmdMadbStart, cmdMadbName},
+	Children: []*cmdline.Command{cmdMadbExec, cmdMadbStart, cmdMadbUninstall, cmdMadbName},
 	Name:     "madb",
 	Short:    "Multi-device Android Debug Bridge",
 	Long: `
diff --git a/name.go b/name.go
index bc5c7f0..8b2542d 100644
--- a/name.go
+++ b/name.go
@@ -22,8 +22,6 @@
 	Long: `
 Manages device nicknames, which are meant to be more human-friendly compared to
 the device serials provided by adb tool.
-
-NOTE: Device specifier flags (-d, -e, -n) are ignored in all 'madb name' commands.
 `,
 }
 
diff --git a/start.go b/start.go
index a112362..e096866 100644
--- a/start.go
+++ b/start.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"os"
 	"strings"
 
 	"v.io/x/lib/cmdline"
@@ -14,26 +13,12 @@
 )
 
 var (
-	forceStopFlag  bool
-	clearCacheFlag bool
-	moduleFlag     string
-	variantFlag    string
-
-	wd string // working directory
+	forceStopFlag bool
 )
 
 func init() {
+	initializeIDCacheFlags(&cmdMadbStart.Flags)
 	cmdMadbStart.Flags.BoolVar(&forceStopFlag, "force-stop", true, `Force stop the target app before starting the activity.`)
-	cmdMadbStart.Flags.BoolVar(&clearCacheFlag, "clear-cache", false, `Clear the cache and re-extract the application ID and the main activity name.  Only takes effect when no arguments are provided.`)
-	cmdMadbStart.Flags.StringVar(&moduleFlag, "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.`)
-	cmdMadbStart.Flags.StringVar(&variantFlag, "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.`)
-
-	// Store the current working directory.
-	var err error
-	wd, err = os.Getwd()
-	if err != nil {
-		panic(err)
-	}
 }
 
 var cmdMadbStart = &cmdline.Command{
diff --git a/testdata/.gitignore b/testdata/.gitignore
new file mode 100644
index 0000000..75d7694
--- /dev/null
+++ b/testdata/.gitignore
@@ -0,0 +1,2 @@
+local.properties
+
diff --git a/uninstall.go b/uninstall.go
new file mode 100644
index 0000000..63c4958
--- /dev/null
+++ b/uninstall.go
@@ -0,0 +1,96 @@
+// Copyright 2015 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"
+
+	"v.io/x/lib/cmdline"
+	"v.io/x/lib/gosh"
+)
+
+var (
+	keepDataFlag bool
+)
+
+func init() {
+	initializeIDCacheFlags(&cmdMadbUninstall.Flags)
+	cmdMadbUninstall.Flags.BoolVar(&keepDataFlag, "keep-data", false, `Keep the application data and cache directories.  Equivalent to '-k' flag in 'adb uninstall' command.`)
+}
+
+var cmdMadbUninstall = &cmdline.Command{
+	Runner: subCommandRunner{initMadbUninstall, runMadbUninstallForDevice},
+	Name:   "uninstall",
+	Short:  "Uninstall your app from all devices",
+	Long: `
+Uninstall your app from all devices.
+
+`,
+	ArgsName: "[<application_id>]",
+	ArgsLong: `
+<application_id> is usually the package name where the activities are defined.
+(See: http://tools.android.com/tech-docs/new-build-system/applicationid-vs-packagename)
+
+
+If the application_id is not specified, madb automatically determines which app to uninstall, based
+on the build scripts found in the current working directory.
+
+If the working directory contains a Gradle Android project (i.e., has "build.gradle"), run a small
+Gradle script to extract the application ID.  In this case, the extracted ID is cached, so that
+"madb uninstall" can be repeated without even running the Gradle script again.  The ID can be
+re-extracted by clearing the cache by providing "-clear-cache" flag.
+`,
+}
+
+func initMadbUninstall(env *cmdline.Env, args []string) ([]string, error) {
+	// If the argument is provided, simply pass it through.
+	if len(args) == 1 {
+		return args, nil
+	}
+
+	if len(args) != 0 {
+		return nil, fmt.Errorf("You mush provide either zero or exactly one arguments.")
+	}
+
+	// Try to extract the application ID from the Gradle scripts.
+	if isGradleProject(wd) {
+		cacheFile, err := getDefaultCacheFilePath()
+		if err != nil {
+			return nil, err
+		}
+
+		key := variantKey{wd, moduleFlag, variantFlag}
+		ids, err := getProjectIds(extractIdsFromGradle, key, clearCacheFlag, cacheFile)
+		if err != nil {
+			return nil, err
+		}
+
+		// Use only the application ID and ignore the main activity name.
+		args = []string{ids.AppID}
+	}
+
+	return args, nil
+}
+
+func runMadbUninstallForDevice(env *cmdline.Env, args []string, d device) error {
+	sh := gosh.NewShell(nil)
+	defer sh.Cleanup()
+
+	sh.ContinueOnError = true
+
+	if len(args) == 1 {
+		appID := args[0]
+
+		cmdArgs := []string{"-s", d.Serial, "uninstall"}
+		if keepDataFlag {
+			cmdArgs = append(cmdArgs, "-k")
+		}
+		cmdArgs = append(cmdArgs, appID)
+		cmd := sh.Cmd("adb", cmdArgs...)
+		return runGoshCommandForDevice(cmd, d)
+	}
+
+	return fmt.Errorf("No arguments are provided and failed to extract the id from the build scripts.")
+}