devtools/madb: handle build variants explicitly and cache the project ids.

1) Now the sub-module name and the build variant name of an Android
   project can be explicitly provided with "-module" and "-variant"
   flags of madb start.  When these are not specified, the first
   available application module and the first build variant is chosen
   with a warning message.

2) The returned project ids are now kept in a cache, so that they can
   be reused without running Gradle scripts again.  The cache is a map
   of (variantKey, projectIds) pair, and implemented in id_cache.go.
   Users can choose to clear the cache and extract the ids again by
   providing "-clear-cache" flag.

3) madb start now also provides "-force-stop" flag which defaults to
   true. Users can set it to false if they just want to bring the
   running app to the front, instead of restarting the app.

Change-Id: I4bdfd34bf7710477a4ff2af3d16c36bec8d1860d
diff --git a/doc.go b/doc.go
index 6ba1a00..0f73147 100644
--- a/doc.go
+++ b/doc.go
@@ -69,7 +69,7 @@
 Launches your app on all devices.
 
 Usage:
-   madb start [flags] <application_id> <activity_name>
+   madb start [flags] [<application_id> <activity_name>]
 
 <application_id> is usually the package name where the activities are defined.
 (See:
@@ -80,7 +80,38 @@
 activity name must be a fully-qualified name (e.g.,
 com.yourcompany.yourapp.MainActivity).
 
+If either <application_id> or <activity_name> is provided, the other must be
+provided as well.
+
+If no arguments are specified, madb automatically determines which app to
+launch, 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 start
+--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"), this command will run a small Gradle script to extract the
+application ID and the main activity name. In this case, the extracted IDs are
+cached, so that "madb start" 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.
+
 The madb start 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.
+ -force-stop=true
+   Force stop the target app before starting the activity.
+ -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
diff --git a/id_cache.go b/id_cache.go
new file mode 100644
index 0000000..df72541
--- /dev/null
+++ b/id_cache.go
@@ -0,0 +1,110 @@
+// 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 (
+	"encoding/gob"
+	"fmt"
+	"os"
+	"path/filepath"
+)
+
+// variantKey specifies a build variant in an Android Gradle project.
+type variantKey struct {
+	// Dir indicates the project directory where "build.gradle" resides.
+	Dir string
+	// Module indicates the name of the sub-module. Can be an empty string.
+	Module string
+	// Variant is the name of the build variant in an Android application module.
+	// When there are no product flavors, there are only two build variants: "debug" and "release".
+	Variant string
+}
+
+// projectIds contains the application ID and the activity name extracted from the Gradle scripts.
+type projectIds struct {
+	AppID, Activity string
+}
+
+// idCache is a map used for caching the ids extracted from the Gradle scripts, so that apps can be
+// launched more quickly without running Gradle tasks.
+type idCache map[variantKey]projectIds
+
+func getIDCache(cacheFile string) (idCache, error) {
+	return readIDCacheMap(cacheFile)
+}
+
+// Clears the cache entry from the given cacheFile.
+func clearIDCacheEntry(key variantKey, cacheFile string) error {
+	cache, err := getIDCache(cacheFile)
+	if err != nil {
+		return err
+	}
+
+	delete(cache, key)
+	return writeIDCacheMap(cache, cacheFile)
+}
+
+// Adds a new entry in the id cache located at cacheFile and save the cache back to the file.
+func writeIDCacheEntry(key variantKey, ids projectIds, cacheFile string) error {
+	cache, err := getIDCache(cacheFile)
+	if err != nil {
+		return err
+	}
+
+	cache[key] = ids
+	return writeIDCacheMap(cache, cacheFile)
+}
+
+// Reads the id cache map from the given file using gob-encoding.
+func readIDCacheMap(filename string) (idCache, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		// If the file does not exist, return an empty map without an error.
+		if os.IsNotExist(err) {
+			return idCache{}, nil
+		}
+
+		// An unexpected error occurred and should be returned.
+		return nil, err
+	}
+	defer f.Close()
+
+	decoder := gob.NewDecoder(f)
+	result := idCache{}
+
+	// Decoding might fail when the cache file is somehow corrupted, or when the cache schema is
+	// updated.  In such cases, move on after resetting the cache file instead of exiting the app.
+	if err := decoder.Decode(&result); err != nil {
+		fmt.Fprintln(os.Stderr, "WARNING: Could not decode the id cache file.  Resetting the cache.")
+		if err := os.Remove(f.Name()); err != nil {
+			return nil, err
+		}
+
+		return idCache{}, nil
+	}
+
+	return result, nil
+}
+
+// Writes the id cache map to the given file using gob-encoding.
+func writeIDCacheMap(cache idCache, filename string) error {
+	f, err := os.Create(filename)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	encoder := gob.NewEncoder(f)
+	return encoder.Encode(cache)
+}
+
+func getDefaultCacheFilePath() (string, error) {
+	configDir, err := getConfigDir()
+	if err != nil {
+		return "", err
+	}
+
+	return filepath.Join(configDir, "id_cache"), nil
+}
diff --git a/madb.go b/madb.go
index 88bc3e0..04b719c 100644
--- a/madb.go
+++ b/madb.go
@@ -160,7 +160,12 @@
 // Gets all the devices specified by the device specifier flags.
 // Intended to be used by most of the madb sub-commands except for 'madb name'.
 func getSpecifiedDevices() ([]device, error) {
-	allDevices, err := getDevices(getDefaultNameFilePath())
+	nicknameFile, err := getDefaultNameFilePath()
+	if err != nil {
+		return nil, err
+	}
+
+	allDevices, err := getDevices(nicknameFile)
 	if err != nil {
 		return nil, err
 	}
@@ -227,6 +232,21 @@
 	return false
 }
 
+// Returns the config dir located at "~/.madb"
+func getConfigDir() (string, error) {
+	home := os.Getenv("HOME")
+	if home == "" {
+		return "", fmt.Errorf("Could not find the HOME directory.")
+	}
+
+	configDir := filepath.Join(home, ".madb")
+	if err := os.MkdirAll(configDir, 0755); err != nil {
+		return "", err
+	}
+
+	return configDir, nil
+}
+
 type subCommandRunner struct {
 	// init is an optional function that does some initial work that should only
 	// be performed once, before directing the command to all the devices.
@@ -307,6 +327,41 @@
 	return cmd.Shell().Err
 }
 
+type idExtractorFunc func(variantKey) (projectIds, error)
+
+// Returns the project ids for the given build variant.  It returns the cached values when the
+// variant is found in the cache file, unless the clearCache argument is true.  Otherwise, it calls
+// extractIdsFromGradle to extract those ids by running Gradle scripts.
+func getProjectIds(extractor idExtractorFunc, key variantKey, clearCache bool, cacheFile string) (projectIds, error) {
+	if clearCache {
+		clearIDCacheEntry(key, cacheFile)
+	} else {
+		// See if the current configuration appears in the cache.
+		cache, err := getIDCache(cacheFile)
+		if err != nil {
+			return projectIds{}, err
+		}
+
+		if ids, ok := cache[key]; ok {
+			fmt.Println("NOTE: Cached IDs are being used.  Use '-clear-cache' flag to clear the cache and extract the IDs from Gradle scripts again.")
+			return ids, nil
+		}
+	}
+
+	fmt.Println("Running Gradle to extract the application ID and the main activity name...")
+	ids, err := extractor(key)
+	if err != nil {
+		return projectIds{}, err
+	}
+
+	// Write these ids to the cache.
+	if err := writeIDCacheEntry(key, ids, cacheFile); err != nil {
+		return projectIds{}, fmt.Errorf("Could not write ids to the cache file: %v", err)
+	}
+
+	return ids, nil
+}
+
 func isFlutterProject(dir string) bool {
 	_, err := os.Stat(filepath.Join(dir, "flutter.yaml"))
 	return err == nil
@@ -350,7 +405,7 @@
 }
 
 // TODO(youngseokyoon): find a better way to distribute the gradle script.
-func findGradleInitScript(dir string) (string, error) {
+func findGradleInitScript() (string, error) {
 	jiriRoot := os.Getenv("JIRI_ROOT")
 	if jiriRoot == "" {
 		return "", fmt.Errorf("JIRI_ROOT environment variable is not set")
@@ -364,7 +419,7 @@
 	return initScript, nil
 }
 
-func extractIdsFromGradle(dir string) (appID, activity string, err error) {
+func extractIdsFromGradle(key variantKey) (ids projectIds, err error) {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
@@ -374,12 +429,12 @@
 	sh.PropagateChildOutput = true
 	sh.ContinueOnError = true
 
-	wrapper, err := findGradleWrapper(dir)
+	wrapper, err := findGradleWrapper(key.Dir)
 	if err != nil {
 		return
 	}
 
-	initScript, err := findGradleInitScript(dir)
+	initScript, err := findGradleInitScript()
 	if err != nil {
 		err = fmt.Errorf("Could not find the madb_init.gradle script: %v", err)
 		return
@@ -389,7 +444,19 @@
 	outputFile := sh.MakeTempFile()
 
 	// Run the gradle wrapper to extract the application ID and the main activity name from the build scripts.
-	cmdArgs := []string{"--daemon", "-p", dir, "-q", "-I", initScript, "-PmadbOutputFile=" + outputFile.Name(), "madbExtractApplicationId", "madbExtractMainActivity"}
+	cmdArgs := []string{"--daemon", "-q", "-I", initScript, "-PmadbOutputFile=" + outputFile.Name()}
+
+	// Specify the project directory. If the module name is explicitly set, combine it with the base directory.
+	cmdArgs = append(cmdArgs, "-p", filepath.Join(key.Dir, key.Module))
+
+	// Specify the variant
+	if key.Variant != "" {
+		cmdArgs = append(cmdArgs, "-PmadbVariant="+key.Variant)
+	}
+
+	// Specify the tasks
+	cmdArgs = append(cmdArgs, "madbExtractApplicationId", "madbExtractMainActivity")
+
 	cmd := sh.Cmd(wrapper, cmdArgs...)
 	cmd.Run()
 
@@ -410,6 +477,6 @@
 		return
 	}
 
-	appID, activity = lines[0], lines[1]
+	ids = projectIds{lines[0], lines[1]}
 	return
 }
diff --git a/madb_init.gradle b/madb_init.gradle
index 4b6b4fd..5c1cdf0 100644
--- a/madb_init.gradle
+++ b/madb_init.gradle
@@ -29,12 +29,16 @@
     def id = null
 
     // See if this project is an Android application module.
-    if (isApplicationModule(project)) {
-        id = extractor(project)
-    } else {
-        // This project is NOT an Android application module.
-        // Retrieve the id from the first application sub-module.
-        id = extractor(project.subprojects.find { isApplicationModule(it) })
+    try {
+        if (isApplicationModule(project)) {
+            id = extractor(project)
+        } else {
+            // This project is NOT an Android application module.
+            // Retrieve the id from the first application sub-module.
+            id = extractor(project.subprojects.find { isApplicationModule(it) })
+        }
+    } catch (Throwable t) {
+        throw new GradleException('Failed to extract the ' + debugName + ': ' + t.message, t)
     }
 
     if (id) {
@@ -53,31 +57,45 @@
 }
 
 String getAppId(project) {
-    try {
-        return project.android.defaultConfig.applicationId
-    } catch (all) {
-        return null
+    if (project.properties.containsKey('madbVariant')) {
+        def variantName = project.properties['madbVariant']
+        def allVariants = project.android.applicationVariants
+        def matchingVariants = allVariants.matching { variantName.equalsIgnoreCase(it.name) }
+        if (matchingVariants.size() != 1) {
+            throw new GradleException('Variant "' + variantName + '" is not found.')
+        }
+
+        def targetVariant = matchingVariants.getAt(0)
+
+        def suffix = targetVariant.buildType.applicationIdSuffix
+        if (suffix == null) {
+            suffix = ""
+        }
+
+        return targetVariant.mergedFlavor.applicationId + suffix
+    } else {
+        def targetVariant = project.android.applicationVariants.getAt(0)
+        print 'Build variant not specified.  '
+        println 'The first variant "' + targetVariant.name + '" is chosen automatically.'
+        println '(NOTE: Variant can be explicitly specified using -variant=<variant name> flag.)'
+        return project.android.applicationVariants.getAt(0).mergedFlavor.applicationId
     }
 }
 
 String getMainActivity(project) {
-    try {
-        def manifestFile = getAndroidManifestLocation(project)
+    def manifestFile = getAndroidManifestLocation(project)
 
-        // Parse the xml file and find the main activity.
-        def manifest = new XmlSlurper().parse(manifestFile)
-        def mainActivity = manifest.application.activity.find { isMainActivity(it) }
-        def name = mainActivity.'@android:name'.text()
+    // Parse the xml file and find the main activity.
+    def manifest = new XmlSlurper().parse(manifestFile)
+    def mainActivity = manifest.application.activity.find { isMainActivity(it) }
+    def name = mainActivity.'@android:name'.text()
 
-        // If the activity name is using the shorthand syntax starting with a dot,
-        // make it a fully-qualified name by prepending it with the package name.
-        if (name.startsWith('.')) {
-            return manifest.'@package'.text() + name
-        } else {
-            return name
-        }
-    } catch (all) {
-        return null
+    // If the activity name is using the shorthand syntax starting with a dot,
+    // make it a fully-qualified name by prepending it with the package name.
+    if (name.startsWith('.')) {
+        return manifest.'@package'.text() + name
+    } else {
+        return name
     }
 }
 
diff --git a/madb_test.go b/madb_test.go
index 44a2bf0..261c592 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -5,11 +5,23 @@
 package main
 
 import (
-	"path"
+	"io/ioutil"
+	"os"
+	"path/filepath"
 	"reflect"
 	"testing"
 )
 
+func tempFilename(t *testing.T) string {
+	f, err := ioutil.TempFile("", "madb_test")
+	if err != nil {
+		t.Fatalf("could not open a temp file: %v", err)
+	}
+	f.Close()
+
+	return f.Name()
+}
+
 func TestParseDevicesOutput(t *testing.T) {
 	var output string
 
@@ -193,14 +205,14 @@
 		projectDir string
 		want       bool
 	}{
-		{"projects/testProject", false},
-		{"projects/testProject/android", false},
-		{"projects/testProject/android/app", false},
-		{"projects/testProject/flutter", true},
+		{"testMultiPlatform", false},
+		{"testMultiPlatform/android", false},
+		{"testMultiPlatform/android/app", false},
+		{"testMultiPlatform/flutter", true},
 	}
 
 	for i, testCase := range testCases {
-		dir := path.Join("testdata", testCase.projectDir)
+		dir := filepath.Join("testdata", "projects", testCase.projectDir)
 		if got := isFlutterProject(dir); got != testCase.want {
 			t.Fatalf("unmatched results for testCases[%v]: got %v, want %v", i, got, testCase.want)
 		}
@@ -212,14 +224,14 @@
 		projectDir string
 		want       bool
 	}{
-		{"projects/testProject", false},
-		{"projects/testProject/android", true},
-		{"projects/testProject/android/app", true},
-		{"projects/testProject/flutter", false},
+		{"testMultiPlatform", false},
+		{"testMultiPlatform/android", true},
+		{"testMultiPlatform/android/app", true},
+		{"testMultiPlatform/flutter", false},
 	}
 
 	for i, testCase := range testCases {
-		dir := path.Join("testdata", testCase.projectDir)
+		dir := filepath.Join("testdata", "projects", testCase.projectDir)
 		if got := isGradleProject(dir); got != testCase.want {
 			t.Fatalf("unmatched results for testCases[%v]: got %v, want %v", i, got, testCase.want)
 		}
@@ -228,33 +240,104 @@
 
 func TestExtractIdsFromGradle(t *testing.T) {
 	testCases := []struct {
-		projectDir string
-		want       []string
+		key  variantKey
+		want projectIds
 	}{
 		{
-			"projects/testProject/android",
-			[]string{
-				"io.v.testProjectId",
-				"io.v.testProjectPackage.LauncherActivity",
-			}},
+			variantKey{"testMultiPlatform/android", "", ""},
+			projectIds{"io.v.testProjectId", "io.v.testProjectPackage.LauncherActivity"},
+		},
 		{
-			"projects/testProject/android/app",
-			[]string{
-				"io.v.testProjectId",
-				"io.v.testProjectPackage.LauncherActivity",
-			}},
+			variantKey{"testMultiPlatform/android", "app", "debug"},
+			projectIds{"io.v.testProjectId", "io.v.testProjectPackage.LauncherActivity"},
+		},
+		{
+			variantKey{"testMultiPlatform/android/app", "", ""},
+			projectIds{"io.v.testProjectId", "io.v.testProjectPackage.LauncherActivity"},
+		},
+		{
+			variantKey{"testAndroidMultiFlavor", "", ""},
+			projectIds{"io.v.testProjectId.lite", "io.v.testProjectPackage.LauncherActivity"},
+		},
+		{
+			variantKey{"testAndroidMultiFlavor", "app", "liteDebug"},
+			projectIds{"io.v.testProjectId.lite.debug", "io.v.testProjectPackage.LauncherActivity"},
+		},
+		{
+			variantKey{"testAndroidMultiFlavor/app", "", "proRelease"},
+			projectIds{"io.v.testProjectId.pro", "io.v.testProjectPackage.LauncherActivity"},
+		},
 	}
 
 	for i, testCase := range testCases {
-		dir := path.Join("testdata", testCase.projectDir)
-
-		appID, activity, err := extractIdsFromGradle(dir)
+		testCase.key.Dir = filepath.Join("testdata", "projects", testCase.key.Dir)
+		got, err := extractIdsFromGradle(testCase.key)
 		if err != nil {
 			t.Fatalf("error occurred while extracting ids for testCases[%v]: %v", i, err)
 		}
 
-		if got := []string{appID, activity}; !reflect.DeepEqual(got, testCase.want) {
+		if !reflect.DeepEqual(got, testCase.want) {
 			t.Fatalf("unmatched results for testCases[%v]: got %v, want %v", i, got, testCase.want)
 		}
 	}
 }
+
+func TestGetProjectIds(t *testing.T) {
+	cacheFile := tempFilename(t)
+	defer os.Remove(cacheFile)
+
+	called := false
+
+	// See if it runs the extractor for the first time.
+	extractor := func(key variantKey) (projectIds, error) {
+		called = true
+		return projectIds{"testAppID", "Activity"}, nil
+	}
+
+	want := projectIds{"testAppID", "Activity"}
+	got, err := getProjectIds(extractor, variantKey{"testDir", "mod", "var"}, false, cacheFile)
+
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	if !called {
+		t.Fatalf("extractor was not called when expected to be called.")
+	}
+
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+
+	// The second run should not invoke the extractor.
+	called = false
+	got, err = getProjectIds(extractor, variantKey{"testDir", "mod", "var"}, false, cacheFile)
+
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	if called {
+		t.Fatalf("extracted was called when not expected.")
+	}
+
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+
+	// Run with clear cache flag.
+	called = false
+	got, err = getProjectIds(extractor, variantKey{"testDir", "mod", "var"}, true, cacheFile)
+
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	if !called {
+		t.Fatalf("extractor was not called when expected to be called.")
+	}
+
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+}
diff --git a/name.go b/name.go
index c8773af..bc5c7f0 100644
--- a/name.go
+++ b/name.go
@@ -185,8 +185,13 @@
 	return os.Remove(filename)
 }
 
-func getDefaultNameFilePath() string {
-	return filepath.Join(os.Getenv("HOME"), ".madb_names")
+func getDefaultNameFilePath() (string, error) {
+	configDir, err := getConfigDir()
+	if err != nil {
+		return "", err
+	}
+
+	return filepath.Join(configDir, "nicknames"), nil
 }
 
 func isValidDeviceSerial(serial string) bool {
@@ -262,5 +267,10 @@
 // Run implements the cmdline.Runner interface by providing the default name file path
 // as the third string argument of the underlying run function.
 func (f runnerFuncWithFilepath) Run(env *cmdline.Env, args []string) error {
-	return f(env, args, getDefaultNameFilePath())
+	p, err := getDefaultNameFilePath()
+	if err != nil {
+		return err
+	}
+
+	return f(env, args, p)
 }
diff --git a/name_test.go b/name_test.go
index 21f002b..31480f2 100644
--- a/name_test.go
+++ b/name_test.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"reflect"
 	"testing"
@@ -17,16 +16,6 @@
 	b bool
 }
 
-func tempFilename(t *testing.T) string {
-	f, err := ioutil.TempFile("", "madb_test")
-	if err != nil {
-		t.Fatalf("could not open a temp file: %v", err)
-	}
-	f.Close()
-
-	return f.Name()
-}
-
 func TestMadbNameSet(t *testing.T) {
 	filename := tempFilename(t)
 	defer os.Remove(filename)
diff --git a/start.go b/start.go
index ca3282f..a112362 100644
--- a/start.go
+++ b/start.go
@@ -14,10 +14,21 @@
 )
 
 var (
+	forceStopFlag  bool
+	clearCacheFlag bool
+	moduleFlag     string
+	variantFlag    string
+
 	wd string // working directory
 )
 
 func init() {
+	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 {
@@ -33,13 +44,29 @@
 Launches your app on all devices.
 
 `,
-	ArgsName: "<application_id> <activity_name>",
+	ArgsName: "[<application_id> <activity_name>]",
 	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)
 
 <activity_name> is the Java class name for the activity you want to launch.
-If the package name of the activity is different from the application ID, the activity name must be a fully-qualified name (e.g., com.yourcompany.yourapp.MainActivity).
+If the package name of the activity is different from the application ID, the activity name must be
+a fully-qualified name (e.g., com.yourcompany.yourapp.MainActivity).
+
+If either <application_id> or <activity_name> is provided, the other must be provided as well.
+
+
+If no arguments are specified, madb automatically determines which app to launch, 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 start --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"), this
+command will run a small Gradle script to extract the application ID and the main activity name.
+In this case, the extracted IDs are cached, so that "madb start" 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.
 `,
 }
 
@@ -54,16 +81,19 @@
 	}
 
 	// Try to extract the application ID and the main activity name from the Gradle scripts.
-	// TODO(youngseokyoon): cache the ids, since the ids are not supposed to be changed very often.
 	if isGradleProject(wd) {
-		fmt.Println("Running Gradle to extract the application ID and the main activity name...")
-
-		appID, activity, err := extractIdsFromGradle(wd)
+		cacheFile, err := getDefaultCacheFilePath()
 		if err != nil {
 			return nil, err
 		}
 
-		args = []string{appID, activity}
+		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
@@ -85,9 +115,12 @@
 			activity = "." + activity
 		}
 
-		// TODO(youngseokyoon): add a flag for not stopping the activity when it is currently running.
 		// 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", "am", "start", "-S", "-n", appID + "/" + activity}
+		cmdArgs := []string{"-s", d.Serial, "shell", "am", "start"}
+		if forceStopFlag {
+			cmdArgs = append(cmdArgs, "-S")
+		}
+		cmdArgs = append(cmdArgs, "-n", appID+"/"+activity)
 		cmd := sh.Cmd("adb", cmdArgs...)
 		return runGoshCommandForDevice(cmd, d)
 	}
diff --git a/testdata/projects/testAndroidMultiFlavor/app/build.gradle b/testdata/projects/testAndroidMultiFlavor/app/build.gradle
new file mode 100644
index 0000000..ba9bb1e
--- /dev/null
+++ b/testdata/projects/testAndroidMultiFlavor/app/build.gradle
@@ -0,0 +1,53 @@
+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"
+    }
+    productFlavors {
+        lite {
+            applicationId "io.v.testProjectId.lite"
+        }
+        pro {
+            applicationId "io.v.testProjectId.pro"
+        }
+    }
+    buildTypes {
+        debug {
+            applicationIdSuffix ".debug"
+        }
+    }
+}
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+} 
+
diff --git a/testdata/projects/testProject/android/app/src/main/AndroidManifest.xml b/testdata/projects/testAndroidMultiFlavor/app/src/main/AndroidManifest.xml
similarity index 100%
copy from testdata/projects/testProject/android/app/src/main/AndroidManifest.xml
copy to testdata/projects/testAndroidMultiFlavor/app/src/main/AndroidManifest.xml
diff --git a/testdata/projects/testProject/android/build.gradle b/testdata/projects/testAndroidMultiFlavor/build.gradle
similarity index 100%
copy from testdata/projects/testProject/android/build.gradle
copy to testdata/projects/testAndroidMultiFlavor/build.gradle
diff --git a/testdata/projects/testProject/android/gradle.properties b/testdata/projects/testAndroidMultiFlavor/gradle.properties
similarity index 100%
copy from testdata/projects/testProject/android/gradle.properties
copy to testdata/projects/testAndroidMultiFlavor/gradle.properties
diff --git a/testdata/projects/testProject/android/gradle/wrapper/gradle-wrapper.jar b/testdata/projects/testAndroidMultiFlavor/gradle/wrapper/gradle-wrapper.jar
similarity index 100%
copy from testdata/projects/testProject/android/gradle/wrapper/gradle-wrapper.jar
copy to testdata/projects/testAndroidMultiFlavor/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/testdata/projects/testProject/android/gradle/wrapper/gradle-wrapper.properties b/testdata/projects/testAndroidMultiFlavor/gradle/wrapper/gradle-wrapper.properties
similarity index 100%
copy from testdata/projects/testProject/android/gradle/wrapper/gradle-wrapper.properties
copy to testdata/projects/testAndroidMultiFlavor/gradle/wrapper/gradle-wrapper.properties
diff --git a/testdata/projects/testProject/android/gradlew b/testdata/projects/testAndroidMultiFlavor/gradlew
similarity index 100%
copy from testdata/projects/testProject/android/gradlew
copy to testdata/projects/testAndroidMultiFlavor/gradlew
diff --git a/testdata/projects/testProject/android/settings.gradle b/testdata/projects/testAndroidMultiFlavor/settings.gradle
similarity index 100%
copy from testdata/projects/testProject/android/settings.gradle
copy to testdata/projects/testAndroidMultiFlavor/settings.gradle
diff --git a/testdata/projects/testProject/android/app/build.gradle b/testdata/projects/testMultiPlatform/android/app/build.gradle
similarity index 100%
rename from testdata/projects/testProject/android/app/build.gradle
rename to testdata/projects/testMultiPlatform/android/app/build.gradle
diff --git a/testdata/projects/testProject/android/app/src/main/AndroidManifest.xml b/testdata/projects/testMultiPlatform/android/app/src/main/AndroidManifest.xml
similarity index 100%
rename from testdata/projects/testProject/android/app/src/main/AndroidManifest.xml
rename to testdata/projects/testMultiPlatform/android/app/src/main/AndroidManifest.xml
diff --git a/testdata/projects/testProject/android/build.gradle b/testdata/projects/testMultiPlatform/android/build.gradle
similarity index 100%
rename from testdata/projects/testProject/android/build.gradle
rename to testdata/projects/testMultiPlatform/android/build.gradle
diff --git a/testdata/projects/testProject/android/gradle.properties b/testdata/projects/testMultiPlatform/android/gradle.properties
similarity index 100%
rename from testdata/projects/testProject/android/gradle.properties
rename to testdata/projects/testMultiPlatform/android/gradle.properties
diff --git a/testdata/projects/testProject/android/gradle/wrapper/gradle-wrapper.jar b/testdata/projects/testMultiPlatform/android/gradle/wrapper/gradle-wrapper.jar
similarity index 100%
rename from testdata/projects/testProject/android/gradle/wrapper/gradle-wrapper.jar
rename to testdata/projects/testMultiPlatform/android/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/testdata/projects/testProject/android/gradle/wrapper/gradle-wrapper.properties b/testdata/projects/testMultiPlatform/android/gradle/wrapper/gradle-wrapper.properties
similarity index 100%
rename from testdata/projects/testProject/android/gradle/wrapper/gradle-wrapper.properties
rename to testdata/projects/testMultiPlatform/android/gradle/wrapper/gradle-wrapper.properties
diff --git a/testdata/projects/testProject/android/gradlew b/testdata/projects/testMultiPlatform/android/gradlew
similarity index 100%
rename from testdata/projects/testProject/android/gradlew
rename to testdata/projects/testMultiPlatform/android/gradlew
diff --git a/testdata/projects/testProject/android/settings.gradle b/testdata/projects/testMultiPlatform/android/settings.gradle
similarity index 100%
rename from testdata/projects/testProject/android/settings.gradle
rename to testdata/projects/testMultiPlatform/android/settings.gradle
diff --git a/testdata/projects/testProject/flutter/flutter.yaml b/testdata/projects/testMultiPlatform/flutter/flutter.yaml
similarity index 100%
rename from testdata/projects/testProject/flutter/flutter.yaml
rename to testdata/projects/testMultiPlatform/flutter/flutter.yaml