devtools/madb: add "madb stop" command.

"madb stop" command stops the app specified by the application ID on
all devices. The application ID can be automatically inferred as in
other madb sub-commands.

Change-Id: I506985c61a84e7f79f5730f0154bf0b49b30f811
diff --git a/doc.go b/doc.go
index ba62f4f..581ab07 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
+   stop        Stop your app on all devices
    uninstall   Uninstall your app from all devices
    name        Manage device nicknames
    help        Display help for commands or topics
@@ -121,6 +122,51 @@
    Comma-separated device serials, qualifiers, or nicknames (set by 'madb
    name').  Command will be run only on specified devices.
 
+Madb stop - Stop your app on all devices
+
+Stops your app on all devices.
+
+Usage:
+   madb stop [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 stop, based on the build scripts found in the current working directory.
+
+1) If the working directory contains a Flutter project (i.e., has
+"flutter.yaml"), this command will run "flutter stop --android-device-id=<device
+serial>" for all the specified devices.
+
+2) 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 stop" 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 stop 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.
+ -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 uninstall - Uninstall your app from all devices
 
 Uninstall your app from all devices.
diff --git a/madb.go b/madb.go
index c9aab4a..92c2db9 100644
--- a/madb.go
+++ b/madb.go
@@ -57,7 +57,7 @@
 }
 
 var cmdMadb = &cmdline.Command{
-	Children: []*cmdline.Command{cmdMadbExec, cmdMadbStart, cmdMadbUninstall, cmdMadbName},
+	Children: []*cmdline.Command{cmdMadbExec, cmdMadbStart, cmdMadbStop, cmdMadbUninstall, cmdMadbName},
 	Name:     "madb",
 	Short:    "Multi-device Android Debug Bridge",
 	Long: `
@@ -348,6 +348,46 @@
 	return cmd.Shell().Err
 }
 
+func initMadbCommand(env *cmdline.Env, args []string, flutterPassthrough bool, activityNameRequired bool) ([]string, error) {
+	var numRequiredArgs int
+	var requiredArgsStr string
+
+	if activityNameRequired {
+		numRequiredArgs = 2
+		requiredArgsStr = "two arguments"
+	} else {
+		numRequiredArgs = 1
+		requiredArgsStr = "one argument"
+	}
+
+	// Pass the arguments through if all the required arguments are provided, or if it is a flutter project.
+	if len(args) == numRequiredArgs || (flutterPassthrough && isFlutterProject(wd)) {
+		return args, nil
+	}
+
+	if len(args) != 0 {
+		return nil, fmt.Errorf("You mush provide either zero arguments or exactly %v.", requiredArgsStr)
+	}
+
+	// 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}
+		ids, err := getProjectIds(extractIdsFromGradle, key, clearCacheFlag, cacheFile)
+		if err != nil {
+			return nil, err
+		}
+
+		args = []string{ids.AppID, ids.Activity}[:numRequiredArgs]
+	}
+
+	return args, nil
+}
+
 type idExtractorFunc func(variantKey) (projectIds, error)
 
 // Returns the project ids for the given build variant.  It returns the cached values when the
diff --git a/start.go b/start.go
index e096866..297c077 100644
--- a/start.go
+++ b/start.go
@@ -56,32 +56,7 @@
 }
 
 func initMadbStart(env *cmdline.Env, args []string) ([]string, error) {
-	// If both arguments are provided, or if it is a flutter project, simply pass the arguments through.
-	if len(args) == 2 || isFlutterProject(wd) {
-		return args, nil
-	}
-
-	if len(args) != 0 {
-		return nil, fmt.Errorf("You mush provide either zero or exactly two arguments.")
-	}
-
-	// 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}
-		ids, err := getProjectIds(extractIdsFromGradle, key, clearCacheFlag, cacheFile)
-		if err != nil {
-			return nil, err
-		}
-
-		args = []string{ids.AppID, ids.Activity}
-	}
-
-	return args, nil
+	return initMadbCommand(env, args, true, true)
 }
 
 func runMadbStartForDevice(env *cmdline.Env, args []string, d device) error {
diff --git a/stop.go b/stop.go
new file mode 100644
index 0000000..37fa188
--- /dev/null
+++ b/stop.go
@@ -0,0 +1,73 @@
+// 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"
+)
+
+func init() {
+	initializeIDCacheFlags(&cmdMadbStop.Flags)
+}
+
+var cmdMadbStop = &cmdline.Command{
+	Runner: subCommandRunner{initMadbStart, runMadbStartForDevice},
+	Name:   "stop",
+	Short:  "Stop your app on all devices",
+	Long: `
+Stops your app on 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 stop, based on
+the build scripts found in the current working directory.
+
+1) If the working directory contains a Flutter project (i.e., has "flutter.yaml"), this command will
+run "flutter stop --android-device-id=<device serial>" for all the specified devices.
+
+2) 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 stop" 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 initMadbStop(env *cmdline.Env, args []string) ([]string, error) {
+	return initMadbCommand(env, args, true, false)
+}
+
+func runMadbStopForDevice(env *cmdline.Env, args []string, d device) error {
+	sh := gosh.NewShell(nil)
+	defer sh.Cleanup()
+
+	sh.ContinueOnError = true
+
+	if len(args) == 2 {
+		appID := args[0]
+
+		// More details on the "adb shell am" command can be found at: http://developer.android.com/tools/help/shell.html#am
+		cmdArgs := []string{"-s", d.Serial, "shell", "force-stop", appID}
+		cmd := sh.Cmd("adb", cmdArgs...)
+		return runGoshCommandForDevice(cmd, d)
+	}
+
+	// In case of flutter, the application ID is not even needed.
+	// Simply run "flutter stop --android-device-id <device_serial>" on all devices.
+	if isFlutterProject(wd) {
+		cmdArgs := []string{"stop", "--android-device-id", d.Serial}
+		cmd := sh.Cmd("flutter", cmdArgs...)
+		return runGoshCommandForDevice(cmd, d)
+	}
+
+	return fmt.Errorf("No arguments are provided and failed to extract the id from the build scripts.")
+}
diff --git a/uninstall.go b/uninstall.go
index 63c4958..cf2beb7 100644
--- a/uninstall.go
+++ b/uninstall.go
@@ -45,33 +45,7 @@
 }
 
 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
+	return initMadbCommand(env, args, false, false)
 }
 
 func runMadbUninstallForDevice(env *cmdline.Env, args []string, d device) error {