devtools/madb: extract Android application variant properties

The Gradle/Go code is refactored so that the Gradle script extracts
more information about the application variant outputs.

One variant can have multiple .apk files, and the properties of each
.apk are extracted and cached, such as the absolute path of the .apk
file and its filters indicating what device configurations (e.g. ABIs,
and screen sizes) are compatible with the specific .apk.

Highlevel changes:

* projectIds -> variantProperties
* idCache -> propertyCache (id_cache.go -> property_cache.go)
* one Gradle task for extracting all the information, instead of
  smaller individual tasks
* Gradle script now prints the results in JSON instead of plaintext,
  which is then parsed back to variantProperty instance in Go.

This CL is meant to be the first pass of implementing "madb install"
command, which needs to know the variant output properties and their
filters, in order to install the best matching .apk (among many) to
the individual devices.

Change-Id: I444253aee291ae69fe59a5c02410631d20030caa
diff --git a/clear_data.go b/clear_data.go
index 4b9aa4a..788dfea 100644
--- a/clear_data.go
+++ b/clear_data.go
@@ -12,7 +12,7 @@
 )
 
 func init() {
-	initializeIDCacheFlags(&cmdMadbClearData.Flags)
+	initializePropertyCacheFlags(&cmdMadbClearData.Flags)
 }
 
 var cmdMadbClearData = &cmdline.Command{
@@ -23,7 +23,7 @@
 Clears your app data from all devices.
 
 To specify which user's data should be cleared, use 'madb user set' command to set the default user
-ID for that device.  (See 'madb help user' for more details.)
+ID for that device. (See 'madb help user' for more details.)
 
 `,
 	ArgsName: "[<application_id>]",
@@ -36,8 +36,8 @@
 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 clear-data" can be repeated without even running the Gradle script again.  The ID can be
+Gradle script to extract the application ID. In this case, the extracted ID is cached, so that
+"madb clear-data" 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.
 `,
 }
diff --git a/doc.go b/doc.go
index 601b647..bb3834a 100644
--- a/doc.go
+++ b/doc.go
@@ -32,9 +32,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 The global flags are:
  -metadata=<just specify -metadata to activate>
@@ -47,7 +47,7 @@
 Clears your app data from all devices.
 
 To specify which user's data should be cleared, use 'madb user set' command to
-set the default user ID for that device.  (See 'madb help user' for more
+set the default user ID for that device. (See 'madb help user' for more
 details.)
 
 Usage:
@@ -62,23 +62,24 @@
 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
+"build.gradle"), run a small Gradle script to extract the application ID. In
 this case, the extracted ID is cached, so that "madb clear-data" can be repeated
-without even running the Gradle script again.  The ID can be re-extracted by
+without even running the Gradle script again. The ID can be re-extracted by
 clearing the cache by providing "-clear-cache" flag.
 
 The madb clear-data 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.
+   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.
+   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.
+   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.
@@ -86,9 +87,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb exec - Run the provided adb command on all devices and emulators concurrently
 
@@ -116,9 +117,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb name - Manage device nicknames
 
@@ -144,9 +145,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb name set
 
@@ -188,9 +189,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb name unset
 
@@ -210,9 +211,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb name list
 
@@ -228,9 +229,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb name clear-all
 
@@ -246,16 +247,16 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb start - Launch your app on all devices
 
 Launches your app on all devices.
 
 To run your app as 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
+command to set the default user ID for that device. (See 'madb help user' for
 more details.)
 
 Usage:
@@ -284,23 +285,24 @@
 "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
+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.
+   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.
  -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.
+   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.
+   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.
@@ -308,16 +310,16 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb stop - Stop your app on all devices
 
 Stops your app on all devices.
 
 To stop 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
+command to set the default user ID for that device. (See 'madb help user' for
 more details.)
 
 Usage:
@@ -335,23 +337,24 @@
 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
+"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.
+   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.
+   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.
+   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.
@@ -359,16 +362,16 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb uninstall - Uninstall your app from all devices
 
 Uninstall your app from all devices.
 
 To uninstall 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'
+set' command to set the default user ID for that device. (See 'madb help user'
 for more details.)
 
 Usage:
@@ -382,26 +385,27 @@
 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
+"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
+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.
+   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.
  -keep-data=false
-   Keep the application data and cache directories.  Equivalent to '-k' flag in
+   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.
+   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.
+   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.
@@ -409,23 +413,23 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb user - Manage default user settings for each device
 
 Manages default user settings for each device.
 
 An Android device can have multiple user accounts, and each user account has a
-numeric ID associated with it.  Certain adb commands accept '--user <user_id>'
-as a parameter to allow specifying which of the Android user account should be
-used when running the command.  The default behavior when the user ID is not
-provided varies by the adb command being run.
+numeric ID associated with it. Certain adb commands accept '--user <user_id>' as
+a parameter to allow specifying which of the Android user account should be used
+when running the command. The default behavior when the user ID is not provided
+varies by the adb command being run.
 
 Some madb commands internally run these adb commands which accept the '--user'
-flag.  You can let madb use different user IDs for different devices by storing
-the default user ID for each device using 'madb user set' command.  If the
+flag. You can let madb use different user IDs for different devices by storing
+the default user ID for each device using 'madb user set' command. If the
 default user ID is not set for a particular device, madb will not provide the
 '--user' flag to the underlying adb command, and the current user will be used
 for that device as a result.
@@ -460,9 +464,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb user set
 
@@ -508,9 +512,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb user unset
 
@@ -534,9 +538,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb user list
 
@@ -552,9 +556,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb user clear-all
 
@@ -573,9 +577,9 @@
    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
+   '@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.
+   command, starting from 1. Command will be run only on specified devices.
 
 Madb help - Display help for commands or topics
 
diff --git a/id_cache.go b/id_cache.go
deleted file mode 100644
index df72541..0000000
--- a/id_cache.go
+++ /dev/null
@@ -1,110 +0,0 @@
-// 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 5b1d77d..502e3f8 100644
--- a/madb.go
+++ b/madb.go
@@ -12,7 +12,6 @@
 	"encoding/json"
 	"flag"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"os/exec"
 	"path"
@@ -41,7 +40,7 @@
 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, 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.`)
+	cmdMadb.Flags.StringVar(&devicesFlag, "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.`)
 
 	// Store the current working directory.
 	var err error
@@ -51,11 +50,11 @@
 	}
 }
 
-// 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.`)
+// initializePropertyCacheFlags sets up the flags related to extracting and caching project properties.
+func initializePropertyCacheFlags(flags *flag.FlagSet) {
+	flags.BoolVar(&clearCacheFlag, "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.`)
+	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{
@@ -442,50 +441,70 @@
 		}
 
 		key := variantKey{wd, moduleFlag, variantFlag}
-		ids, err := getProjectIds(extractIdsFromGradle, key, clearCacheFlag, cacheFile)
+		properties, err := getProjectProperties(extractPropertiesFromGradle, key, clearCacheFlag, cacheFile)
 		if err != nil {
 			return nil, err
 		}
 
-		args = []string{ids.AppID, ids.Activity}[:numRequiredArgs]
+		args = []string{properties.AppID, properties.Activity}[:numRequiredArgs]
 	}
 
 	return args, nil
 }
 
-type idExtractorFunc func(variantKey) (projectIds, error)
+type variantProperties struct {
+	AppID          string
+	Activity       string
+	AbiFilters     []string
+	VariantOutputs []variantOutput
+}
 
-// 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) {
+type variantOutput struct {
+	Name           string
+	OutputFilePath string
+	VersionCode    int
+	Filters        []filter
+}
+
+type filter struct {
+	FilterType string
+	Identifier string
+}
+
+type propertyExtractorFunc func(variantKey) (variantProperties, error)
+
+// getProjectProperties returns the project properties 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 extractPropertiesFromGradle to extract those properties by
+// running Gradle scripts.
+func getProjectProperties(extractor propertyExtractorFunc, key variantKey, clearCache bool, cacheFile string) (variantProperties, error) {
 	if clearCache {
-		clearIDCacheEntry(key, cacheFile)
+		clearPropertyCacheEntry(key, cacheFile)
 	} else {
 		// See if the current configuration appears in the cache.
-		cache, err := getIDCache(cacheFile)
+		cache, err := getPropertyCache(cacheFile)
 		if err != nil {
-			return projectIds{}, err
+			return variantProperties{}, 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
+		if properties, 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 properties, nil
 		}
 	}
 
 	fmt.Println("Running Gradle to extract the application ID and the main activity name...")
-	ids, err := extractor(key)
+	properties, err := extractor(key)
 	if err != nil {
-		return projectIds{}, err
+		return variantProperties{}, 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)
+	// Write these properties to the cache.
+	if err := writePropertyCacheEntry(key, properties, cacheFile); err != nil {
+		return variantProperties{}, fmt.Errorf("Could not write properties to the cache file: %v", err)
 	}
 
-	return ids, nil
+	return properties, nil
 }
 
 func isFlutterProject(dir string) bool {
@@ -545,7 +564,7 @@
 	return initScript, nil
 }
 
-func extractIdsFromGradle(key variantKey) (ids projectIds, err error) {
+func extractPropertiesFromGradle(key variantKey) (variantProperties, error) {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
@@ -557,13 +576,12 @@
 
 	wrapper, err := findGradleWrapper(key.Dir)
 	if err != nil {
-		return
+		return variantProperties{}, err
 	}
 
 	initScript, err := findGradleInitScript()
 	if err != nil {
-		err = fmt.Errorf("Could not find the madb_init.gradle script: %v", err)
-		return
+		return variantProperties{}, fmt.Errorf("Could not find the madb_init.gradle script: %v", err)
 	}
 
 	// Create a temporary file in which Gradle can write the results.
@@ -581,30 +599,24 @@
 	}
 
 	// Specify the tasks
-	cmdArgs = append(cmdArgs, "madbExtractApplicationId", "madbExtractMainActivity")
+	cmdArgs = append(cmdArgs, "madbExtractVariantProperties")
 
 	cmd := sh.Cmd(wrapper, cmdArgs...)
 	cmd.Run()
 
 	if err = sh.Err; err != nil {
-		return
+		return variantProperties{}, err
 	}
 
 	// Read what is written in the temporary file.
-	var bytes []byte
-	bytes, err = ioutil.ReadFile(outputFile.Name())
-	if err != nil {
-		return
+	// The file must be in JSON format.
+	result := variantProperties{}
+	decoder := json.NewDecoder(outputFile)
+	if err = decoder.Decode(&result); err != nil {
+		return variantProperties{}, fmt.Errorf("Could not extract the application ID and the main activity name: %v", err)
 	}
 
-	lines := strings.Split(string(bytes[:]), "\n")
-	if len(lines) != 3 {
-		err = fmt.Errorf("Could not extract the application ID and the main activity name.")
-		return
-	}
-
-	ids = projectIds{lines[0], lines[1]}
-	return
+	return result, nil
 }
 
 // readMapFromFile reads the provided file and reconstructs the string => string map.
@@ -628,7 +640,7 @@
 	// Decoding might fail when the file is somehow corrupted, or when the 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.Fprintf(os.Stderr, "WARNING: Could not decode the file: %q.  Resetting the file.\n", err)
+		fmt.Fprintf(os.Stderr, "WARNING: Could not decode the file: %q. Resetting the file.\n", err)
 		if err := os.Remove(f.Name()); err != nil {
 			return nil, err
 		}
diff --git a/madb_init.gradle b/madb_init.gradle
index 5c1cdf0..7d8ce0c 100644
--- a/madb_init.gradle
+++ b/madb_init.gradle
@@ -1,87 +1,127 @@
+import groovy.json.*;
+
 allprojects {
 
-    // Add the following tasks only for the project in the current directory.
+    // Add the extract task only to the project in the current directory.
     if (project.projectDir == gradle.startParameter.currentDir) {
-
-        // An optional output file name can be specified by a Gradle command-line flag:
-        // -PmadbOutputFile=<file_name>
-        def output = project.properties.containsKey('madbOutputFile')
-            ? new File(project.properties['madbOutputFile'])
-            : null
-
-        // Empty the file, if it exists already.
-        if (output != null) {
-            output.text = ''
-        }
-
-        task madbExtractApplicationId << {
-            extract(project, {p -> getAppId(p)}, output, 'application ID')
-        }
-
-        task madbExtractMainActivity << {
-            extract(project, {p -> getMainActivity(p)}, output, 'main activity')
+        task madbExtractVariantProperties << {
+            extract(project)
         }
     }
-
 }
 
-void extract(project, extractor, output, debugName) {
-    def id = null
-
-    // See if this project is an Android application module.
-    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)
+// Main driver of the property extraction script.
+void extract(project) {
+    project = getApplicationModule(project)
+    if (project == null) {
+        def errMsg = 'The current project is not an Android application module, '
+            + 'nor does it contain any application sub-modules. '
+            + 'Please run the madb command from an Android application project directory.'
+        throw new GradleException(errMsg)
     }
 
-    if (id) {
-        if (output != null) {
-            output.append(id + '\n')
-        } else {
-            println id
-        }
+    // Get the target variant.
+    def targetVariant = getTargetVariant(project)
+
+    // Collect the variant properties in a map, so that it can be printed out as a JSON.
+    def result = [:]
+    result['VariantName'] = targetVariant.name
+    result['AppID'] = getApplicationId(targetVariant)
+    result['Activity'] = getMainActivity(project)
+    result['AbiFilters'] = getAbiFilters(targetVariant)
+    result['VariantOutputs'] = getVariantOutputs(targetVariant)
+
+    // Format the resulting map into JSON and print it.
+    def resultJson = JsonOutput.prettyPrint(JsonOutput.toJson(result))
+    printResult(project, resultJson)
+}
+
+// Prints the given result to the desired output stream.
+// If the output file is specified, write the result to the file.
+// Otherwise, print it out to the console.
+void printResult(project, result) {
+    // An optional output file name can be specified by a Gradle command-line flag:
+    // -PmadbOutputFile=<file_name>
+    def output = project.properties.containsKey('madbOutputFile')
+        ? new File(project.properties['madbOutputFile'])
+        : null
+
+    if (output != null) {
+        // Empty the file, and then print the result.
+        output.text = ''
+        output.append(result)
     } else {
-        throw new GradleException('Failed to extract the ' + debugName)
+        println result
     }
 }
 
+// Returns an Android application module from the given project.
+// The given project is returned immediately, if itself is an application module.
+// Otherwise, the first available application sub-module is returned, if any.
+// Returns null if no Android application modules were found from the given project.
+Object getApplicationModule(project) {
+    if (isApplicationModule(project)) {
+        return project
+    }
+
+    def subApplicationModules = project.subprojects.findAll { isApplicationModule(it) }
+    if (subApplicationModules.isEmpty()) {
+        return null
+    }
+
+    def result = subApplicationModules.first()
+    if (subApplicationModules.size() > 1) {
+        print 'Multiple application sub-modules were detected. '
+        println 'The first application module "' + result.name + '" is chosen automatically.'
+        println '(NOTE: Application module can be explicitly specified using -module=<name> flag.)'
+    }
+
+    return result
+}
+
+// Returns true iff the given project is an Android application.
 boolean isApplicationModule(project) {
     return project.plugins.hasPlugin('com.android.application')
 }
 
-String getAppId(project) {
+// Returns the target application variant for the project.
+// If the 'madbVariant' property was explicitly set from the command line, the
+// matching variant is returned.
+// If there is no variant with the provided name, it throws an exception.
+//
+// If the 'madbVariant' property is not provided, the first available variant is
+// returned. Usually the first available variant would be 'debug'.
+Object getTargetVariant(project) {
+    def allVariants = project.android.applicationVariants
+
     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) {
+        def targetVariant = allVariants.find { variantName.equalsIgnoreCase(it.name) }
+        if (targetVariant == null) {
             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
+        return targetVariant
     } else {
-        def targetVariant = project.android.applicationVariants.getAt(0)
-        print 'Build variant not specified.  '
+        def targetVariant = allVariants.iterator().next()
+        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
+        println '(NOTE: Variant can be explicitly specified using -variant=<name> flag.)'
+
+        return targetVariant
     }
 }
 
+// Returns the application ID for the given variant.
+String getApplicationId(variant) {
+    def suffix = variant.buildType.applicationIdSuffix
+    if (suffix == null) {
+        suffix = ""
+    }
+
+    return variant.mergedFlavor.applicationId + suffix
+}
+
 String getMainActivity(project) {
     def manifestFile = getAndroidManifestLocation(project)
 
@@ -99,6 +139,8 @@
     }
 }
 
+// Returns the location of the "AndroidManifest.xml" file.
+// TODO(youngseokyoon): investigate whether we can obtain the merged manifest.
 File getAndroidManifestLocation(project) {
     try {
         return project.android.sourceSets.main.manifest.srcFile
@@ -107,6 +149,7 @@
     }
 }
 
+// Determines whether the given activity is the main activity or not.
 boolean isMainActivity(activity) {
     try {
         def intentFilter = activity.'intent-filter'
@@ -116,3 +159,31 @@
         return false
     }
 }
+
+// Returns the list of supported ABIs for the given variant.
+// Returns null if there are no ABI filters specified.
+Object getAbiFilters(variant) {
+    return variant.variantData.variantConfiguration.supportedAbis
+}
+
+// Gets the outputs and their properties of the given variant.
+// The returned object is a list of variant outputs, each of which is a map containing the
+// properties of a variant output, such as the absolute path of the .apk file, and its filters.
+Object getVariantOutputs(variant) {
+    def variantOutputs = []
+    for (def variantOutput : variant.outputs) {
+        def result = [:]
+        result['Name'] = variantOutput.name
+        result['OutputFilePath'] = variantOutput.mainOutputFile.outputFile.absolutePath
+        result['VersionCode'] = variantOutput.versionCode
+
+        def filters = []
+        for (def filter : variantOutput.mainOutputFile.filters) {
+            filters.add([FilterType: filter.filterType, Identifier: filter.identifier])
+        }
+        result['Filters'] = filters
+        variantOutputs.add(result)
+    }
+
+    return variantOutputs
+}
diff --git a/madb_test.go b/madb_test.go
index daf797f..34434b1 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -270,63 +270,63 @@
 }
 
 func TestExtractIdsFromGradle(t *testing.T) {
-	testCases := []struct {
+	tests := []struct {
 		key  variantKey
-		want projectIds
+		want variantProperties
 	}{
 		{
 			variantKey{"testMultiPlatform/android", "", ""},
-			projectIds{"io.v.testProjectId", "io.v.testProjectPackage.LauncherActivity"},
+			variantProperties{AppID: "io.v.testProjectId", Activity: "io.v.testProjectPackage.LauncherActivity"},
 		},
 		{
 			variantKey{"testMultiPlatform/android", "app", "debug"},
-			projectIds{"io.v.testProjectId", "io.v.testProjectPackage.LauncherActivity"},
+			variantProperties{AppID: "io.v.testProjectId", Activity: "io.v.testProjectPackage.LauncherActivity"},
 		},
 		{
 			variantKey{"testMultiPlatform/android/app", "", ""},
-			projectIds{"io.v.testProjectId", "io.v.testProjectPackage.LauncherActivity"},
+			variantProperties{AppID: "io.v.testProjectId", Activity: "io.v.testProjectPackage.LauncherActivity"},
 		},
 		{
 			variantKey{"testAndroidMultiFlavor", "", ""},
-			projectIds{"io.v.testProjectId.lite", "io.v.testProjectPackage.LauncherActivity"},
+			variantProperties{AppID: "io.v.testProjectId.lite.debug", Activity: "io.v.testProjectPackage.LauncherActivity"},
 		},
 		{
 			variantKey{"testAndroidMultiFlavor", "app", "liteDebug"},
-			projectIds{"io.v.testProjectId.lite.debug", "io.v.testProjectPackage.LauncherActivity"},
+			variantProperties{AppID: "io.v.testProjectId.lite.debug", Activity: "io.v.testProjectPackage.LauncherActivity"},
 		},
 		{
 			variantKey{"testAndroidMultiFlavor/app", "", "proRelease"},
-			projectIds{"io.v.testProjectId.pro", "io.v.testProjectPackage.LauncherActivity"},
+			variantProperties{AppID: "io.v.testProjectId.pro", Activity: "io.v.testProjectPackage.LauncherActivity"},
 		},
 	}
 
-	for i, testCase := range testCases {
-		testCase.key.Dir = filepath.Join("testdata", "projects", testCase.key.Dir)
-		got, err := extractIdsFromGradle(testCase.key)
+	for i, test := range tests {
+		test.key.Dir = filepath.Join("testdata", "projects", test.key.Dir)
+		got, err := extractPropertiesFromGradle(test.key)
 		if err != nil {
-			t.Fatalf("error occurred while extracting ids for testCases[%v]: %v", i, err)
+			t.Fatalf("error occurred while extracting properties for testCases[%v]: %v", i, err)
 		}
 
-		if !reflect.DeepEqual(got, testCase.want) {
-			t.Fatalf("unmatched results for testCases[%v]: got %v, want %v", i, got, testCase.want)
+		if got.AppID != test.want.AppID || got.Activity != test.want.Activity {
+			t.Fatalf("unmatched results for testCases[%v]: got %v, want %v", i, got, test.want)
 		}
 	}
 }
 
-func TestGetProjectIds(t *testing.T) {
+func TestGetProjectProperties(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) {
+	extractor := func(key variantKey) (variantProperties, error) {
 		called = true
-		return projectIds{"testAppID", "Activity"}, nil
+		return variantProperties{AppID: "testAppID", Activity: "Activity"}, nil
 	}
 
-	want := projectIds{"testAppID", "Activity"}
-	got, err := getProjectIds(extractor, variantKey{"testDir", "mod", "var"}, false, cacheFile)
+	want := variantProperties{AppID: "testAppID", Activity: "Activity"}
+	got, err := getProjectProperties(extractor, variantKey{"testDir", "mod", "var"}, false, cacheFile)
 
 	if err != nil {
 		t.Fatalf(err.Error())
@@ -342,7 +342,7 @@
 
 	// The second run should not invoke the extractor.
 	called = false
-	got, err = getProjectIds(extractor, variantKey{"testDir", "mod", "var"}, false, cacheFile)
+	got, err = getProjectProperties(extractor, variantKey{"testDir", "mod", "var"}, false, cacheFile)
 
 	if err != nil {
 		t.Fatalf(err.Error())
@@ -358,7 +358,7 @@
 
 	// Run with clear cache flag.
 	called = false
-	got, err = getProjectIds(extractor, variantKey{"testDir", "mod", "var"}, true, cacheFile)
+	got, err = getProjectProperties(extractor, variantKey{"testDir", "mod", "var"}, true, cacheFile)
 
 	if err != nil {
 		t.Fatalf(err.Error())
diff --git a/property_cache.go b/property_cache.go
new file mode 100644
index 0000000..22fcdc9
--- /dev/null
+++ b/property_cache.go
@@ -0,0 +1,105 @@
+// 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
+}
+
+// propertyCache is a map used for caching the variant properties extracted from the Gradle scripts,
+// so that apps can be launched more quickly without running Gradle tasks.
+type propertyCache map[variantKey]variantProperties
+
+func getPropertyCache(cacheFile string) (propertyCache, error) {
+	return readPropertyCacheMap(cacheFile)
+}
+
+// Clears the cache entry from the given cacheFile.
+func clearPropertyCacheEntry(key variantKey, cacheFile string) error {
+	cache, err := getPropertyCache(cacheFile)
+	if err != nil {
+		return err
+	}
+
+	delete(cache, key)
+	return writePropertyCacheMap(cache, cacheFile)
+}
+
+// Adds a new entry in the property cache located at cacheFile and save the cache back to the file.
+func writePropertyCacheEntry(key variantKey, props variantProperties, cacheFile string) error {
+	cache, err := getPropertyCache(cacheFile)
+	if err != nil {
+		return err
+	}
+
+	cache[key] = props
+	return writePropertyCacheMap(cache, cacheFile)
+}
+
+// Reads the property cache map from the given file using gob-encoding.
+func readPropertyCacheMap(filename string) (propertyCache, 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 propertyCache{}, nil
+		}
+
+		// An unexpected error occurred and should be returned.
+		return nil, err
+	}
+	defer f.Close()
+
+	decoder := gob.NewDecoder(f)
+	result := propertyCache{}
+
+	// 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 property cache file. Resetting the cache.")
+		if err := os.Remove(f.Name()); err != nil {
+			return nil, err
+		}
+
+		return propertyCache{}, nil
+	}
+
+	return result, nil
+}
+
+// Writes the property cache map to the given file using gob-encoding.
+func writePropertyCacheMap(cache propertyCache, 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/start.go b/start.go
index 96a3c59..e1ee391 100644
--- a/start.go
+++ b/start.go
@@ -17,7 +17,7 @@
 )
 
 func init() {
-	initializeIDCacheFlags(&cmdMadbStart.Flags)
+	initializePropertyCacheFlags(&cmdMadbStart.Flags)
 	cmdMadbStart.Flags.BoolVar(&forceStopFlag, "force-stop", true, `Force stop the target app before starting the activity.`)
 }
 
@@ -29,7 +29,7 @@
 Launches your app on all devices.
 
 To run your app as 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.)
+default user ID for that device. (See 'madb help user' for more details.)
 
 `,
 	ArgsName: "[<application_id> <activity_name>]",
@@ -53,7 +53,7 @@
 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
+running the Gradle script again. The IDs can be re-extracted by clearing the cache by providing
 "-clear-cache" flag.
 `,
 }
@@ -102,5 +102,5 @@
 		return runGoshCommandForDevice(cmd, d, false)
 	}
 
-	return fmt.Errorf("No arguments are provided and failed to extract the ids from the build scripts.")
+	return fmt.Errorf("No arguments are provided and failed to extract the properties from the build scripts.")
 }
diff --git a/stop.go b/stop.go
index 72c9245..509dd37 100644
--- a/stop.go
+++ b/stop.go
@@ -12,7 +12,7 @@
 )
 
 func init() {
-	initializeIDCacheFlags(&cmdMadbStop.Flags)
+	initializePropertyCacheFlags(&cmdMadbStop.Flags)
 }
 
 var cmdMadbStop = &cmdline.Command{
@@ -23,7 +23,7 @@
 Stops your app on all devices.
 
 To stop 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.)
+default user ID for that device. (See 'madb help user' for more details.)
 
 `,
 	ArgsName: "[<application_id>]",
@@ -39,7 +39,7 @@
 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
+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.
 `,
diff --git a/uninstall.go b/uninstall.go
index 2589e88..9843a16 100644
--- a/uninstall.go
+++ b/uninstall.go
@@ -16,8 +16,8 @@
 )
 
 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.`)
+	initializePropertyCacheFlags(&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{
@@ -28,7 +28,7 @@
 Uninstall your app from all devices.
 
 To uninstall 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.)
+the default user ID for that device. (See 'madb help user' for more details.)
 
 `,
 	ArgsName: "[<application_id>]",
@@ -41,8 +41,8 @@
 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
+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.
 `,
 }
diff --git a/user.go b/user.go
index 10041f3..1e7d4cb 100644
--- a/user.go
+++ b/user.go
@@ -22,13 +22,13 @@
 Manages default user settings for each device.
 
 An Android device can have multiple user accounts, and each user account has a numeric ID associated
-with it.  Certain adb commands accept '--user <user_id>' as a parameter to allow specifying which of
-the Android user account should be used when running the command.  The default behavior when the
+with it. Certain adb commands accept '--user <user_id>' as a parameter to allow specifying which of
+the Android user account should be used when running the command. The default behavior when the
 user ID is not provided varies by the adb command being run.
 
-Some madb commands internally run these adb commands which accept the '--user' flag.  You can let
+Some madb commands internally run these adb commands which accept the '--user' flag. You can let
 madb use different user IDs for different devices by storing the default user ID for each device
-using 'madb user set' command.  If the default user ID is not set for a particular device, madb will
+using 'madb user set' command. If the default user ID is not set for a particular device, madb will
 not provide the '--user' flag to the underlying adb command, and the current user will be used for
 that device as a result.