devtools/madb: allow specifying default user for each device.

Each Android device may have multiple users, and 'madb user set' and
other sub-commands can be used to specify which user should be used on
a particular device.

Some of the functions which used to reside in 'name.go' are refactored
and moved to 'madb.go', and used by both 'madb name' and 'madb user'.
Some variable are also renamed to make them more meaningful.

Change-Id: I5cb360a7a4da93d12c6a277055c00cfb8658471f
diff --git a/clear_data.go b/clear_data.go
index d058afb..4b9aa4a 100644
--- a/clear_data.go
+++ b/clear_data.go
@@ -22,6 +22,9 @@
 	Long: `
 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.)
+
 `,
 	ArgsName: "[<application_id>]",
 	ArgsLong: `
@@ -53,9 +56,16 @@
 		appID := args[0]
 
 		// TODO(youngseokyoon): maybe do something equivalent for flutter?
-		cmdArgs := []string{"-s", d.Serial, "shell", "pm", "clear", appID}
+		cmdArgs := []string{"-s", d.Serial, "shell", "pm", "clear"}
+
+		// Specify the user ID if applicable.
+		if d.UserID != "" {
+			cmdArgs = append(cmdArgs, "--user", d.UserID)
+		}
+		cmdArgs = append(cmdArgs, appID)
+
 		cmd := sh.Cmd("adb", cmdArgs...)
-		return runGoshCommandForDevice(cmd, d)
+		return runGoshCommandForDevice(cmd, d, true)
 	}
 
 	return fmt.Errorf("No arguments are provided and failed to extract the id from the build scripts.")
diff --git a/doc.go b/doc.go
index 21df511..601b647 100644
--- a/doc.go
+++ b/doc.go
@@ -22,6 +22,7 @@
    start       Launch your app on all devices
    stop        Stop your app on all devices
    uninstall   Uninstall your app from all devices
+   user        Manage default user settings for each device
    help        Display help for commands or topics
 
 The madb flags are:
@@ -45,6 +46,10 @@
 
 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.)
+
 Usage:
    madb clear-data [flags] [<application_id>]
 
@@ -148,8 +153,8 @@
 Sets a human-friendly nickname that can be used when specifying the device in
 any madb commands.
 
-The device serial can be obtain using the 'adb devices -l' command. For example,
-consider the following example output:
+The device serial can be obtained using the 'adb devices -l' command. For
+example, consider the following example output:
 
     HT4BVWV00023           device usb:3-3.4.2 product:volantisg model:Nexus_9 device:flounder_lte
 
@@ -173,7 +178,7 @@
    madb name set [flags] <device_serial> <nickname>
 
 <device_serial> is a device serial (e.g., 'HT4BVWV00023') or an alternative
-device specifier (e.g., 'usb:3-3.4.2') obtained from 'adb devices -l' command
+device qualifier (e.g., 'usb:3-3.4.2') obtained from 'adb devices -l' command
 <nickname> is an alpha-numeric string with no special characters or spaces.
 
 The madb name set flags are:
@@ -249,6 +254,10 @@
 
 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.)
+
 Usage:
    madb start [flags] [<application_id> <activity_name>]
 
@@ -307,6 +316,10 @@
 
 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.)
+
 Usage:
    madb stop [flags] [<application_id>]
 
@@ -354,6 +367,10 @@
 
 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.)
+
 Usage:
    madb uninstall [flags] [<application_id>]
 
@@ -396,6 +413,170 @@
    '@' sign followed by the index of the device in the output of 'adb devices'
    command, starting from 1.  Command will be run only on specified devices.
 
+Madb 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.
+
+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 not provide the
+'--user' flag to the underlying adb command, and the current user will be used
+for that device as a result.
+
+Below is the list of madb commands which are affected by the default user ID
+settings:
+
+    madb clear-data
+    madb start
+    madb stop
+    madb uninstall
+
+For more details on how to obtain the user ID from an Android device, see 'madb
+user help set'.
+
+NOTE: Device specifier flags (-d, -e, -n) are ignored in all 'madb name'
+commands.
+
+Usage:
+   madb user [flags] <command>
+
+The madb user commands are:
+   set         Set a default user ID to be used for the given device.
+   unset       Unset the default user ID set by the 'madb user set' command.
+   list        List all the existing default user IDs.
+   clear-all   Clear all the existing default user settings.
+
+The madb user flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
+
+Madb user set
+
+Sets a default user ID to be used for the specified device, when there are
+multiple user accounts on a single device.
+
+The user IDs can be obtained using the 'adb [<device_serial>] shell pm list
+users' command. Alternatively, you can use 'madb exec' if you want to specify
+the device with a nickname. For example, running the following command:
+
+    madb -n=MyPhone exec shell pm list users
+
+will list the available users and their IDs on the MyPhone device. Consider the
+following example output:
+
+    [MyPhone]       Users:
+    [MyPhone]               UserInfo{0:John Doe:13} running
+    [MyPhone]               UserInfo{10:Work profile:30} running
+
+There are two available users, "John Doe" and "Work profile". Each user is
+assigned a "user ID", which appears on the left of the user name. In this case,
+the user ID of "John Doe" is "0", and the user ID of the "Work profile" is "10".
+
+To use the "Work profile" as the default user when running madb commands on this
+device, run the following command:
+
+    madb user set MyPhone 10
+
+and then madb will use "Work profile" as the default user for device "MyPhone"
+in any of the subsequence madb commands.
+
+Usage:
+   madb user set [flags] <device_serial> <user_id>
+
+<device_serial> is the unique serial number for the device, which can be
+obtained from 'adb devices'. <user_id> is one of the user IDs obtained from 'adb
+shell pm list users' command.
+
+The madb user set flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
+
+Madb user unset
+
+Unsets the default user ID assigned by the 'madb user set' command for the
+specified device.
+
+Running this command without any device specifiers will unset the default users
+only for the currently available devices and emulators, while keeping the
+default user IDs for the other devices.
+
+Usage:
+   madb user unset [flags] <device_serial>
+
+<device_serial> is the unique serial number for the device, which can be
+obtained from 'adb devices'.
+
+The madb user unset flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
+
+Madb user list
+
+Lists all the currently stored default user IDs for devices.
+
+Usage:
+   madb user list [flags]
+
+The madb user list flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
+
+Madb user clear-all
+
+Clears all the currently stored default user IDs for devices.
+
+This command clears the default user IDs regardless of whether the device is
+currently connected or not.
+
+Usage:
+   madb user clear-all [flags]
+
+The madb user clear-all flags are:
+ -d=false
+   Restrict the command to only run on real devices.
+ -e=false
+   Restrict the command to only run on emulators.
+ -n=
+   Comma-separated device serials, qualifiers, device indices (e.g., '@1',
+   '@2'), or nicknames (set by 'madb name').  A device index is specified by an
+   '@' sign followed by the index of the device in the output of 'adb devices'
+   command, starting from 1.  Command will be run only on specified devices.
+
 Madb help - Display help for commands or topics
 
 Help with no args displays the usage of the parent command.
diff --git a/exec.go b/exec.go
index 72c0b56..42b182b 100644
--- a/exec.go
+++ b/exec.go
@@ -38,5 +38,5 @@
 
 	cmdArgs := append([]string{"-s", d.Serial}, args...)
 	cmd := sh.Cmd("adb", cmdArgs...)
-	return runGoshCommandForDevice(cmd, d)
+	return runGoshCommandForDevice(cmd, d, false)
 }
diff --git a/madb.go b/madb.go
index 7078841..5b1d77d 100644
--- a/madb.go
+++ b/madb.go
@@ -9,6 +9,7 @@
 
 import (
 	"bytes"
+	"encoding/json"
 	"flag"
 	"fmt"
 	"io/ioutil"
@@ -58,9 +59,17 @@
 }
 
 var cmdMadb = &cmdline.Command{
-	Children: []*cmdline.Command{cmdMadbClearData, cmdMadbExec, cmdMadbName, cmdMadbStart, cmdMadbStop, cmdMadbUninstall},
-	Name:     "madb",
-	Short:    "Multi-device Android Debug Bridge",
+	Children: []*cmdline.Command{
+		cmdMadbClearData,
+		cmdMadbExec,
+		cmdMadbName,
+		cmdMadbStart,
+		cmdMadbStop,
+		cmdMadbUninstall,
+		cmdMadbUser,
+	},
+	Name:  "madb",
+	Short: "Multi-device Android Debug Bridge",
 	Long: `
 Multi-device Android Debug Bridge
 
@@ -97,6 +106,7 @@
 	Qualifiers []string
 	Nickname   string
 	Index      int
+	UserID     string
 }
 
 // Returns the display name which is intended to be used as the console output prefix.
@@ -110,23 +120,28 @@
 }
 
 // Runs "adb devices -l" command, and parses the result to get all the device serial numbers.
-func getDevices(nicknameFile string) ([]device, error) {
+func getDevices(nicknameFile string, userFile string) ([]device, error) {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
 
 	output := sh.Cmd("adb", "devices", "-l").Stdout()
 
-	nsm, err := readNicknameSerialMap(nicknameFile)
+	nicknameSerialMap, err := readMapFromFile(nicknameFile)
 	if err != nil {
 		return nil, err
 	}
 
-	return parseDevicesOutput(output, nsm)
+	serialUserMap, err := readMapFromFile(userFile)
+	if err != nil {
+		return nil, err
+	}
+
+	return parseDevicesOutput(output, nicknameSerialMap, serialUserMap)
 }
 
 // Parses the output generated from "adb devices -l" command and return the list of device serial numbers
 // Devices that are currently offline are excluded from the returned list.
-func parseDevicesOutput(output string, nsm map[string]string) ([]device, error) {
+func parseDevicesOutput(output string, nicknameSerialMap map[string]string, serialUserMap map[string]string) ([]device, error) {
 	lines := strings.Split(output, "\n")
 
 	result := []device{}
@@ -161,7 +176,7 @@
 		// Determine whether there is a nickname defined for this device,
 		// so that the console output prefix can display the nickname instead of the serial.
 	NSMLoop:
-		for nickname, serial := range nsm {
+		for nickname, serial := range nicknameSerialMap {
 			if d.Serial == serial {
 				d.Nickname = nickname
 				break
@@ -175,6 +190,11 @@
 			}
 		}
 
+		// Determine whether there is a default user ID set by 'madb user'.
+		if userID, ok := serialUserMap[d.Serial]; ok {
+			d.UserID = userID
+		}
+
 		result = append(result, d)
 	}
 
@@ -189,7 +209,12 @@
 		return nil, err
 	}
 
-	allDevices, err := getDevices(nicknameFile)
+	userFile, err := getDefaultUserFilePath()
+	if err != nil {
+		return nil, err
+	}
+
+	allDevices, err := getDevices(nicknameFile, userFile)
 	if err != nil {
 		return nil, err
 	}
@@ -369,9 +394,16 @@
 	return nil
 }
 
-func runGoshCommandForDevice(cmd *gosh.Cmd, d device) error {
-	stdout := textutil.PrefixLineWriter(os.Stdout, "["+d.displayName()+"]\t")
-	stderr := textutil.PrefixLineWriter(os.Stderr, "["+d.displayName()+"]\t")
+func runGoshCommandForDevice(cmd *gosh.Cmd, d device, printUserID bool) error {
+	var prefix string
+	if printUserID && d.UserID != "" {
+		prefix = "[" + d.displayName() + ":" + d.UserID + "]\t"
+	} else {
+		prefix = "[" + d.displayName() + "]\t"
+	}
+
+	stdout := textutil.PrefixLineWriter(os.Stdout, prefix)
+	stderr := textutil.PrefixLineWriter(os.Stderr, prefix)
 	cmd.AddStdoutWriter(stdout)
 	cmd.AddStderrWriter(stderr)
 	cmd.Run()
@@ -574,3 +606,68 @@
 	ids = projectIds{lines[0], lines[1]}
 	return
 }
+
+// readMapFromFile reads the provided file and reconstructs the string => string map.
+// When the file does not exist, it returns an empty map (instead of nil), so that callers can safely add new entries.
+func readMapFromFile(filename string) (map[string]string, error) {
+	result := make(map[string]string)
+
+	// The file may not exist or be empty when there are no stored data.
+	if stat, err := os.Stat(filename); os.IsNotExist(err) || (err == nil && stat.Size() == 0) {
+		return result, nil
+	}
+
+	f, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	decoder := json.NewDecoder(f)
+
+	// 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)
+		if err := os.Remove(f.Name()); err != nil {
+			return nil, err
+		}
+
+		return make(map[string]string), nil
+	}
+
+	return result, nil
+}
+
+// writeMapToFile takes a string => string map and writes it into the provided file name.
+func writeMapToFile(data map[string]string, filename string) error {
+	f, err := os.Create(filename)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	encoder := json.NewEncoder(f)
+	return encoder.Encode(data)
+}
+
+type pathProvider func() (string, error)
+
+// subCommandRunnerWithFilepath is an adapter that turns the madb name/user subcommand functions into cmdline.Runners.
+type subCommandRunnerWithFilepath struct {
+	subCmd func(*cmdline.Env, []string, string) error
+	pp     pathProvider
+}
+
+var _ cmdline.Runner = (*subCommandRunnerWithFilepath)(nil)
+
+// 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 subCommandRunnerWithFilepath) Run(env *cmdline.Env, args []string) error {
+	p, err := f.pp()
+	if err != nil {
+		return err
+	}
+
+	return f.subCmd(env, args, p)
+}
diff --git a/madb_test.go b/madb_test.go
index 6c4ec79..daf797f 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -32,7 +32,7 @@
 
 `
 
-	got, err := parseDevicesOutput(output, nil)
+	got, err := parseDevicesOutput(output, nil, nil)
 	if err != nil {
 		t.Fatalf("failed to parse the output: %v", err)
 	}
@@ -44,6 +44,7 @@
 			Qualifiers: []string{"usb:3-3.4.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
 			Nickname:   "",
 			Index:      1,
+			UserID:     "",
 		},
 		device{
 			Serial:     "emulator-5554",
@@ -51,6 +52,7 @@
 			Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 			Nickname:   "",
 			Index:      2,
+			UserID:     "",
 		},
 	}
 
@@ -63,7 +65,7 @@
 
 `
 
-	got, err = parseDevicesOutput(output, nil)
+	got, err = parseDevicesOutput(output, nil, nil)
 	if err != nil {
 		t.Fatalf("failed to parse the output: %v", err)
 	}
@@ -77,7 +79,7 @@
 deviceid02       device product:sdk_phone_armv7 model:sdk_phone_armv7 device:generic
 
 `
-	got, err = parseDevicesOutput(output, nil)
+	got, err = parseDevicesOutput(output, nil, nil)
 	if err != nil {
 		t.Fatalf("failed to parse the output: %v", err)
 	}
@@ -89,6 +91,7 @@
 			Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 			Nickname:   "",
 			Index:      2,
+			UserID:     "",
 		},
 	}
 
@@ -103,12 +106,16 @@
 
 	`
 
-	nsm := map[string]string{
+	nicknameSerialMap := map[string]string{
 		"MyPhone": "deviceid01",
 		"ARMv7":   "model:sdk_phone_armv7",
 	}
 
-	got, err = parseDevicesOutput(output, nsm)
+	serialUserMap := map[string]string{
+		"deviceid01": "10",
+	}
+
+	got, err = parseDevicesOutput(output, nicknameSerialMap, serialUserMap)
 	if err != nil {
 		t.Fatalf("failed to parse the output: %v", err)
 	}
@@ -120,6 +127,7 @@
 			Qualifiers: []string{"usb:3-3.4.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
 			Nickname:   "MyPhone",
 			Index:      1,
+			UserID:     "10",
 		},
 		device{
 			Serial:     "emulator-5554",
@@ -127,6 +135,7 @@
 			Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 			Nickname:   "ARMv7",
 			Index:      2,
+			UserID:     "",
 		},
 	}
 
@@ -143,6 +152,7 @@
 		Qualifiers: []string{"usb:3-3.4.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
 		Nickname:   "MyPhone",
 		Index:      1,
+		UserID:     "",
 	}
 
 	d2 := device{
@@ -151,6 +161,7 @@
 		Qualifiers: []string{"usb:3-3.4.1", "product:volantisg", "model:Nexus_9", "device:flounder_lte"},
 		Nickname:   "",
 		Index:      2,
+		UserID:     "",
 	}
 
 	e1 := device{
@@ -159,6 +170,7 @@
 		Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 		Nickname:   "ARMv7",
 		Index:      3,
+		UserID:     "",
 	}
 
 	d3 := device{
@@ -167,6 +179,7 @@
 		Qualifiers: []string{"usb:3-3.3", "product:bullhead", "model:Nexus_5X", "device:bullhead"},
 		Nickname:   "SecondPhone",
 		Index:      4,
+		UserID:     "",
 	}
 
 	e2 := device{
@@ -175,6 +188,7 @@
 		Qualifiers: []string{"product:sdk_phone_armv7", "model:sdk_phone_armv7", "device:generic"},
 		Nickname:   "",
 		Index:      5,
+		UserID:     "",
 	}
 
 	allDevices := []device{d1, d2, e1, d3, e2}
diff --git a/name.go b/name.go
index b5fc2b4..3fce437 100644
--- a/name.go
+++ b/name.go
@@ -5,7 +5,6 @@
 package main
 
 import (
-	"encoding/json"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -27,14 +26,14 @@
 }
 
 var cmdMadbNameSet = &cmdline.Command{
-	Runner: runnerFuncWithFilepath(runMadbNameSet),
+	Runner: subCommandRunnerWithFilepath{runMadbNameSet, getDefaultNameFilePath},
 	Name:   "set",
 	Short:  "Set a nickname to be used in place of the device serial.",
 	Long: `
 Sets a human-friendly nickname that can be used when specifying the device in
 any madb commands.
 
-The device serial can be obtain using the 'adb devices -l' command.
+The device serial can be obtained using the 'adb devices -l' command.
 For example, consider the following example output:
 
     HT4BVWV00023           device usb:3-3.4.2 product:volantisg model:Nexus_9 device:flounder_lte
@@ -57,7 +56,7 @@
 `,
 	ArgsName: "<device_serial> <nickname>",
 	ArgsLong: `
-<device_serial> is a device serial (e.g., 'HT4BVWV00023') or an alternative device specifier (e.g., 'usb:3-3.4.2') obtained from 'adb devices -l' command
+<device_serial> is a device serial (e.g., 'HT4BVWV00023') or an alternative device qualifier (e.g., 'usb:3-3.4.2') obtained from 'adb devices -l' command
 <nickname> is an alpha-numeric string with no special characters or spaces.
 `,
 }
@@ -77,30 +76,30 @@
 		return env.UsageErrorf("Not a valid nickname: %v", nickname)
 	}
 
-	nsm, err := readNicknameSerialMap(filename)
+	nicknameSerialMap, err := readMapFromFile(filename)
 	if err != nil {
 		return err
 	}
 
 	// If the nickname is already in use, don't allow it at all.
-	if _, present := nsm[nickname]; present {
+	if _, present := nicknameSerialMap[nickname]; present {
 		return fmt.Errorf("The provided nickname %q is already in use.", nickname)
 	}
 
 	// If the serial number already has an assigned nickname, delete it first.
 	// Need to do this check, because the nickname-serial map should be a one-to-one mapping.
-	if nn, present := reverseMap(nsm)[serial]; present {
-		delete(nsm, nn)
+	if nickname, present := reverseMap(nicknameSerialMap)[serial]; present {
+		delete(nicknameSerialMap, nickname)
 	}
 
 	// Add the nickname serial mapping.
-	nsm[nickname] = serial
+	nicknameSerialMap[nickname] = serial
 
-	return writeNicknameSerialMap(nsm, filename)
+	return writeMapToFile(nicknameSerialMap, filename)
 }
 
 var cmdMadbNameUnset = &cmdline.Command{
-	Runner: runnerFuncWithFilepath(runMadbNameUnset),
+	Runner: subCommandRunnerWithFilepath{runMadbNameUnset, getDefaultNameFilePath},
 	Name:   "unset",
 	Short:  "Unset a nickname set by the 'madb name set' command.",
 	Long: `
@@ -124,15 +123,15 @@
 		return env.UsageErrorf("Not a valid device serial or name: %v", name)
 	}
 
-	nsm, err := readNicknameSerialMap(filename)
+	nicknameSerialMap, err := readMapFromFile(filename)
 	if err != nil {
 		return err
 	}
 
 	found := false
-	for nickname, serial := range nsm {
+	for nickname, serial := range nicknameSerialMap {
 		if nickname == name || serial == name {
-			delete(nsm, nickname)
+			delete(nicknameSerialMap, nickname)
 			found = true
 			break
 		}
@@ -142,11 +141,11 @@
 		return fmt.Errorf("The provided argument is neither a known nickname nor a device serial.")
 	}
 
-	return writeNicknameSerialMap(nsm, filename)
+	return writeMapToFile(nicknameSerialMap, filename)
 }
 
 var cmdMadbNameList = &cmdline.Command{
-	Runner: runnerFuncWithFilepath(runMadbNameList),
+	Runner: subCommandRunnerWithFilepath{runMadbNameList, getDefaultNameFilePath},
 	Name:   "list",
 	Short:  "List all the existing nicknames.",
 	Long: `
@@ -155,7 +154,7 @@
 }
 
 func runMadbNameList(env *cmdline.Env, args []string, filename string) error {
-	nsm, err := readNicknameSerialMap(filename)
+	nicknameSerialMap, err := readMapFromFile(filename)
 	if err != nil {
 		return err
 	}
@@ -164,7 +163,7 @@
 	fmt.Println("Serial          Nickname")
 	fmt.Println("========================")
 
-	for nickname, serial := range nsm {
+	for nickname, serial := range nicknameSerialMap {
 		fmt.Printf("%v\t%v\n", serial, nickname)
 	}
 
@@ -172,7 +171,7 @@
 }
 
 var cmdMadbNameClearAll = &cmdline.Command{
-	Runner: runnerFuncWithFilepath(runMadbNameClearAll),
+	Runner: subCommandRunnerWithFilepath{runMadbNameClearAll, getDefaultNameFilePath},
 	Name:   "clear-all",
 	Short:  "Clear all the existing nicknames.",
 	Long: `
@@ -217,62 +216,3 @@
 
 	return reversed
 }
-
-// readNicknameSerialMap reads the provided file and reconstructs the nickname => serial map.
-// The mapping is written one per each line, in the form of "<nickname> <serial>".
-func readNicknameSerialMap(filename string) (map[string]string, error) {
-	result := make(map[string]string)
-
-	f, err := os.Open(filename)
-	if err != nil {
-		// Nickname file may not exist when there are no nicknames assigned, and it is not an error.
-		if os.IsNotExist(err) {
-			return result, nil
-		}
-
-		return nil, err
-	}
-	defer f.Close()
-
-	decoder := json.NewDecoder(f)
-
-	// Decoding might fail when the nickname 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 nickname file: %q.  Resetting the file.\n", err)
-		if err := os.Remove(f.Name()); err != nil {
-			return nil, err
-		}
-
-		return make(map[string]string), nil
-	}
-
-	return result, nil
-}
-
-// writeNicknameSerialmap takes a nickname => serial map and writes it into the provided file name.
-// The mapping is written one per each line, in the form of "<nickname> <serial>".
-func writeNicknameSerialMap(nsm map[string]string, filename string) error {
-	f, err := os.Create(filename)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-
-	encoder := json.NewEncoder(f)
-	return encoder.Encode(nsm)
-}
-
-// runnerFuncWithFilepath is an adapter that turns the madb name subcommand functions into cmdline.Runners.
-type runnerFuncWithFilepath func(*cmdline.Env, []string, string) error
-
-// 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 {
-	p, err := getDefaultNameFilePath()
-	if err != nil {
-		return err
-	}
-
-	return f(env, args, p)
-}
diff --git a/name_test.go b/name_test.go
index 9a39c27..57038d4 100644
--- a/name_test.go
+++ b/name_test.go
@@ -10,11 +10,6 @@
 	"testing"
 )
 
-type stringBoolPair struct {
-	s string
-	b bool
-}
-
 func TestMadbNameSet(t *testing.T) {
 	filename := tempFilename(t)
 	defer os.Remove(filename)
@@ -27,7 +22,7 @@
 		t.Fatal(err)
 	}
 
-	if got, err = readNicknameSerialMap(filename); err != nil {
+	if got, err = readMapFromFile(filename); err != nil {
 		t.Fatal(err)
 	}
 	want = map[string]string{"NICKNAME1": "SERIAL1"}
@@ -40,7 +35,7 @@
 		t.Fatal(err)
 	}
 
-	if got, err = readNicknameSerialMap(filename); err != nil {
+	if got, err = readMapFromFile(filename); err != nil {
 		t.Fatal(err)
 	}
 	want = map[string]string{"NICKNAME1": "SERIAL1", "NICKNAME2": "SERIAL2"}
@@ -53,7 +48,7 @@
 		t.Fatal(err)
 	}
 
-	if got, err = readNicknameSerialMap(filename); err != nil {
+	if got, err = readMapFromFile(filename); err != nil {
 		t.Fatal(err)
 	}
 	want = map[string]string{"NN1": "SERIAL1", "NICKNAME2": "SERIAL2"}
@@ -83,7 +78,7 @@
 	if err = runMadbNameUnset(nil, []string{"SERIAL1"}, filename); err != nil {
 		t.Fatal(err)
 	}
-	if got, err = readNicknameSerialMap(filename); err != nil {
+	if got, err = readMapFromFile(filename); err != nil {
 		t.Fatal(err)
 	}
 	want = map[string]string{"NICKNAME2": "SERIAL2", "NICKNAME3": "SERIAL3"}
@@ -95,7 +90,7 @@
 	if err = runMadbNameUnset(nil, []string{"NICKNAME2"}, filename); err != nil {
 		t.Fatal(err)
 	}
-	if got, err = readNicknameSerialMap(filename); err != nil {
+	if got, err = readMapFromFile(filename); err != nil {
 		t.Fatal(err)
 	}
 	want = map[string]string{"NICKNAME3": "SERIAL3"}
@@ -132,7 +127,10 @@
 }
 
 func TestIsValidDeviceSerial(t *testing.T) {
-	testCases := []stringBoolPair{
+	tests := []struct {
+		input string
+		want  bool
+	}{
 		// The following strings should be accepted
 		{"HT4BVWV00023", true},
 		{"01023f5e2fd2accf", true},
@@ -148,15 +146,18 @@
 		{"#not_allowed_chars~", false},
 	}
 
-	for _, tc := range testCases {
-		if got, want := isValidDeviceSerial(tc.s), tc.b; got != want {
-			t.Fatalf("unmatched results for serial '%v': got %v, want %v", tc.s, got, want)
+	for _, test := range tests {
+		if got := isValidDeviceSerial(test.input); got != test.want {
+			t.Fatalf("unmatched results for serial '%v': got %v, want %v", test.input, got, test.want)
 		}
 	}
 }
 
 func TestIsValidNickname(t *testing.T) {
-	testCases := []stringBoolPair{
+	tests := []struct {
+		input string
+		want  bool
+	}{
 		// The following strings should be accepted
 		{"Nexus5X", true},
 		{"Nexus9", true},
@@ -170,9 +171,9 @@
 		{"#not_allowed_chars~", false},
 	}
 
-	for _, tc := range testCases {
-		if got, want := isValidNickname(tc.s), tc.b; got != want {
-			t.Fatalf("unmatched results for nickname '%v': got %v, want %v", tc.s, got, want)
+	for _, test := range tests {
+		if got := isValidNickname(test.input); got != test.want {
+			t.Fatalf("unmatched results for nickname '%v': got %v, want %v", test.input, got, test.want)
 		}
 	}
 }
diff --git a/start.go b/start.go
index 297c077..96a3c59 100644
--- a/start.go
+++ b/start.go
@@ -28,6 +28,9 @@
 	Long: `
 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.)
+
 `,
 	ArgsName: "[<application_id> <activity_name>]",
 	ArgsLong: `
@@ -80,9 +83,15 @@
 		if forceStopFlag {
 			cmdArgs = append(cmdArgs, "-S")
 		}
+
+		// Specify the user ID if applicable.
+		if d.UserID != "" {
+			cmdArgs = append(cmdArgs, "--user", d.UserID)
+		}
+
 		cmdArgs = append(cmdArgs, "-n", appID+"/"+activity)
 		cmd := sh.Cmd("adb", cmdArgs...)
-		return runGoshCommandForDevice(cmd, d)
+		return runGoshCommandForDevice(cmd, d, true)
 	}
 
 	// In case of flutter, the application ID is not even needed.
@@ -90,7 +99,7 @@
 	if isFlutterProject(wd) {
 		cmdArgs := []string{"start", "--android-device-id", d.Serial}
 		cmd := sh.Cmd("flutter", cmdArgs...)
-		return runGoshCommandForDevice(cmd, d)
+		return runGoshCommandForDevice(cmd, d, false)
 	}
 
 	return fmt.Errorf("No arguments are provided and failed to extract the ids from the build scripts.")
diff --git a/stop.go b/stop.go
index 37fa188..72c9245 100644
--- a/stop.go
+++ b/stop.go
@@ -22,6 +22,9 @@
 	Long: `
 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.)
+
 `,
 	ArgsName: "[<application_id>]",
 	ArgsLong: `
@@ -56,9 +59,16 @@
 		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}
+		cmdArgs := []string{"-s", d.Serial, "shell", "am", "force-stop"}
+
+		// Specify the user ID if applicable.
+		if d.UserID != "" {
+			cmdArgs = append(cmdArgs, "--user", d.UserID)
+		}
+
+		cmdArgs = append(cmdArgs, appID)
 		cmd := sh.Cmd("adb", cmdArgs...)
-		return runGoshCommandForDevice(cmd, d)
+		return runGoshCommandForDevice(cmd, d, true)
 	}
 
 	// In case of flutter, the application ID is not even needed.
@@ -66,7 +76,7 @@
 	if isFlutterProject(wd) {
 		cmdArgs := []string{"stop", "--android-device-id", d.Serial}
 		cmd := sh.Cmd("flutter", cmdArgs...)
-		return runGoshCommandForDevice(cmd, d)
+		return runGoshCommandForDevice(cmd, d, false)
 	}
 
 	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 cf2beb7..2589e88 100644
--- a/uninstall.go
+++ b/uninstall.go
@@ -27,6 +27,9 @@
 	Long: `
 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.)
+
 `,
 	ArgsName: "[<application_id>]",
 	ArgsLong: `
@@ -61,9 +64,15 @@
 		if keepDataFlag {
 			cmdArgs = append(cmdArgs, "-k")
 		}
+
+		// Specify the user ID if applicable.
+		if d.UserID != "" {
+			cmdArgs = append(cmdArgs, "--user", d.UserID)
+		}
+
 		cmdArgs = append(cmdArgs, appID)
 		cmd := sh.Cmd("adb", cmdArgs...)
-		return runGoshCommandForDevice(cmd, d)
+		return runGoshCommandForDevice(cmd, d, true)
 	}
 
 	return fmt.Errorf("No arguments are provided and failed to extract the id from the build scripts.")
diff --git a/user.go b/user.go
new file mode 100644
index 0000000..10041f3
--- /dev/null
+++ b/user.go
@@ -0,0 +1,207 @@
+// 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"
+	"os"
+	"path/filepath"
+	"strconv"
+
+	"v.io/x/lib/cmdline"
+)
+
+// TODO(youngseokyoon): add a helper command that wraps "madb exec shell pm list users" to show all available users.
+var cmdMadbUser = &cmdline.Command{
+	Children: []*cmdline.Command{cmdMadbUserSet, cmdMadbUserUnset, cmdMadbUserList, cmdMadbUserClearAll},
+	Name:     "user",
+	Short:    "Manage default user settings for each device",
+	Long: `
+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.
+
+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
+not provide the '--user' flag to the underlying adb command, and the current user will be used for
+that device as a result.
+
+Below is the list of madb commands which are affected by the default user ID settings:
+
+    madb clear-data
+    madb start
+    madb stop
+    madb uninstall
+
+For more details on how to obtain the user ID from an Android device, see 'madb user help set'.
+
+NOTE: Device specifier flags (-d, -e, -n) are ignored in all 'madb name' commands.
+`,
+}
+
+var cmdMadbUserSet = &cmdline.Command{
+	Runner: subCommandRunnerWithFilepath{runMadbUserSet, getDefaultUserFilePath},
+	Name:   "set",
+	Short:  "Set a default user ID to be used for the given device.",
+	Long: `
+Sets a default user ID to be used for the specified device, when there are multiple user accounts on
+a single device.
+
+The user IDs can be obtained using the 'adb [<device_serial>] shell pm list users' command.
+Alternatively, you can use 'madb exec' if you want to specify the device with a nickname.
+For example, running the following command:
+
+    madb -n=MyPhone exec shell pm list users
+
+will list the available users and their IDs on the MyPhone device.
+Consider the following example output:
+
+    [MyPhone]       Users:
+    [MyPhone]               UserInfo{0:John Doe:13} running
+    [MyPhone]               UserInfo{10:Work profile:30} running
+
+There are two available users, "John Doe" and "Work profile". Each user is assigned a "user ID",
+which appears on the left of the user name. In this case, the user ID of "John Doe" is "0", and the
+user ID of the "Work profile" is "10".
+
+To use the "Work profile" as the default user when running madb commands on this device, run the
+following command:
+
+    madb user set MyPhone 10
+
+and then madb will use "Work profile" as the default user for device "MyPhone" in any of the
+subsequence madb commands.
+`,
+	ArgsName: "<device_serial> <user_id>",
+	ArgsLong: `
+<device_serial> is the unique serial number for the device, which can be obtained from 'adb devices'.
+<user_id> is one of the user IDs obtained from 'adb shell pm list users' command.
+`,
+}
+
+func runMadbUserSet(env *cmdline.Env, args []string, filename string) error {
+	// Check if the arguments are valid.
+	if len(args) != 2 {
+		return fmt.Errorf("There must be exactly two arguments.")
+	}
+
+	// TODO(youngseokyoon): make it possible to specify the device using its nickname or index.
+	// Validate the device serial
+	serial := args[0]
+	if !isValidDeviceSerial(serial) {
+		return fmt.Errorf("Not a valid device serial: %v", serial)
+	}
+
+	// Validate the user ID.
+	userID := args[1]
+	if id, err := strconv.Atoi(userID); err != nil || id < 0 {
+		return fmt.Errorf("Not a valid user ID: %v", userID)
+	}
+
+	// Get the <device_serial, user_id> mapping.
+	serialUserMap, err := readMapFromFile(filename)
+	if err != nil {
+		return err
+	}
+
+	// Add the <device_serial, user_id> mapping for the specified device.
+	serialUserMap[serial] = userID
+	return writeMapToFile(serialUserMap, filename)
+}
+
+var cmdMadbUserUnset = &cmdline.Command{
+	Runner: subCommandRunnerWithFilepath{runMadbUserUnset, getDefaultUserFilePath},
+	Name:   "unset",
+	Short:  "Unset the default user ID set by the 'madb user set' command.",
+	Long: `
+Unsets the default user ID assigned by the 'madb user set' command for the specified device.
+
+Running this command without any device specifiers will unset the default users only for the
+currently available devices and emulators, while keeping the default user IDs for the other devices.
+`,
+	ArgsName: "<device_serial>",
+	ArgsLong: `
+<device_serial> is the unique serial number for the device, which can be obtained from 'adb devices'.
+`,
+}
+
+func runMadbUserUnset(env *cmdline.Env, args []string, filename string) error {
+	// Check if the arguments are valid.
+	if len(args) != 1 {
+		return fmt.Errorf("There must be exactly one argument.")
+	}
+
+	// TODO(youngseokyoon): make it possible to specify the device using its nickname or index.
+	// Validate the device serial
+	serial := args[0]
+	if !isValidDeviceSerial(serial) {
+		return fmt.Errorf("Not a valid device serial: %v", serial)
+	}
+
+	// Get the <device_serial, user_id> mapping.
+	serialUserMap, err := readMapFromFile(filename)
+	if err != nil {
+		return err
+	}
+
+	// Delete the <device_serial, user_id> mapping for the specified device.
+	delete(serialUserMap, serial)
+	return writeMapToFile(serialUserMap, filename)
+}
+
+var cmdMadbUserList = &cmdline.Command{
+	Runner: subCommandRunnerWithFilepath{runMadbUserList, getDefaultUserFilePath},
+	Name:   "list",
+	Short:  "List all the existing default user IDs.",
+	Long: `
+Lists all the currently stored default user IDs for devices.
+`,
+}
+
+func runMadbUserList(env *cmdline.Env, args []string, filename string) error {
+	// Get the <device_serial, user_id> mapping.
+	serialUserMap, err := readMapFromFile(filename)
+	if err != nil {
+		return err
+	}
+
+	// TODO(youngseokyoon): pretty print this.
+	fmt.Println("Device Serial    User ID")
+	fmt.Println("========================")
+
+	for s, u := range serialUserMap {
+		fmt.Printf("%v\t%v\n", s, u)
+	}
+
+	return nil
+}
+
+var cmdMadbUserClearAll = &cmdline.Command{
+	Runner: subCommandRunnerWithFilepath{runMadbUserClearAll, getDefaultUserFilePath},
+	Name:   "clear-all",
+	Short:  "Clear all the existing default user settings.",
+	Long: `
+Clears all the currently stored default user IDs for devices.
+
+This command clears the default user IDs regardless of whether the device is currently connected or not.
+`,
+}
+
+func runMadbUserClearAll(env *cmdline.Env, args []string, filename string) error {
+	return os.Remove(filename)
+}
+
+func getDefaultUserFilePath() (string, error) {
+	configDir, err := getConfigDir()
+	if err != nil {
+		return "", err
+	}
+
+	return filepath.Join(configDir, "users"), nil
+}
diff --git a/user_test.go b/user_test.go
new file mode 100644
index 0000000..91497d8
--- /dev/null
+++ b/user_test.go
@@ -0,0 +1,109 @@
+// 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 (
+	"os"
+	"reflect"
+	"testing"
+)
+
+func TestMadbUserSet(t *testing.T) {
+	filename := tempFilename(t)
+	defer os.Remove(filename)
+
+	var got, want map[string]string
+	var err error
+
+	// Set a new nickname
+	if err = runMadbUserSet(nil, []string{"SERIAL1", "0"}, filename); err != nil {
+		t.Fatal(err)
+	}
+
+	if got, err = readMapFromFile(filename); err != nil {
+		t.Fatal(err)
+	}
+	want = map[string]string{"SERIAL1": "0"}
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+
+	// Set a second nickname
+	if err = runMadbUserSet(nil, []string{"SERIAL2", "10"}, filename); err != nil {
+		t.Fatal(err)
+	}
+
+	if got, err = readMapFromFile(filename); err != nil {
+		t.Fatal(err)
+	}
+	want = map[string]string{"SERIAL1": "0", "SERIAL2": "10"}
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+
+	// Override an existing nickname to another
+	if err = runMadbUserSet(nil, []string{"SERIAL1", "20"}, filename); err != nil {
+		t.Fatal(err)
+	}
+
+	if got, err = readMapFromFile(filename); err != nil {
+		t.Fatal(err)
+	}
+	want = map[string]string{"SERIAL1": "20", "SERIAL2": "10"}
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+
+	// Try some invalid ids and see if they fail.
+	invalidIds := []string{"-1", "NAME"}
+	for _, id := range invalidIds {
+		if err = runMadbUserSet(nil, []string{"SERIAL", id}, filename); err == nil {
+			t.Fatalf("expected an error but succeeded.")
+		}
+	}
+}
+
+func TestMadbUserUnset(t *testing.T) {
+	filename := tempFilename(t)
+	defer os.Remove(filename)
+
+	// Set up some default users first.
+	runMadbUserSet(nil, []string{"SERIAL1", "0"}, filename)
+	runMadbUserSet(nil, []string{"SERIAL2", "0"}, filename)
+	runMadbUserSet(nil, []string{"SERIAL3", "10"}, filename)
+
+	var got, want map[string]string
+	var err error
+
+	// Unset by serial number.
+	if err = runMadbUserUnset(nil, []string{"SERIAL1"}, filename); err != nil {
+		t.Fatal(err)
+	}
+	if got, err = readMapFromFile(filename); err != nil {
+		t.Fatal(err)
+	}
+	want = map[string]string{"SERIAL2": "0", "SERIAL3": "10"}
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("unmatched results: got %v, want %v", got, want)
+	}
+}
+
+func TestMadbUserClearAll(t *testing.T) {
+	filename := tempFilename(t)
+	defer os.Remove(filename)
+
+	// Set up some default users first.
+	runMadbUserSet(nil, []string{"SERIAL1", "0"}, filename)
+	runMadbUserSet(nil, []string{"SERIAL2", "0"}, filename)
+	runMadbUserSet(nil, []string{"SERIAL3", "10"}, filename)
+
+	// Run the clear-all command. The file should be empty after running the command.
+	runMadbUserClearAll(nil, []string{}, filename)
+
+	// Check if the file is successfully deleted.
+	if _, err := os.Stat(filename); !os.IsNotExist(err) {
+		t.Fatalf("failed to delete file %q", filename)
+	}
+}