Skip to content

Commit 8d6aee7

Browse files
mobile-kevingmegidish
authored andcommitted
feat(android): pass apps launch --arg key=value as string intent extras
Each --arg on Android must be key=value and becomes am start --es key value, which the app reads from its launch intent. An argument without '=' is an error. --env stays iOS-only.
1 parent 0c60b36 commit 8d6aee7

7 files changed

Lines changed: 62 additions & 14 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,12 @@ mobilecli apps foreground --device <device-id>
245245
# Launch an app
246246
mobilecli apps launch <bundle-id> --device <device-id>
247247

248-
# Launch an app with arguments and environment variables (iOS only)
248+
# Launch an app with arguments and environment variables (iOS)
249249
mobilecli apps launch <bundle-id> --device <device-id> --arg -FeatureX --arg YES --env API_URL=https://staging.example.com
250250

251+
# Launch an app with string intent extras (Android, each --arg is key=value and becomes --es key value)
252+
mobilecli apps launch <package-name> --device <device-id> --arg featureX=true --arg apiUrl=https://staging.example.com
253+
251254
# Terminate an app
252255
mobilecli apps terminate <bundle-id> --device <device-id>
253256

‎cli/apps.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func init() {
190190
appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to launch app on")
191191
appsLaunchCmd.Flags().StringVar(&locale, "locale", "", "Comma-separated BCP 47 locale tags (e.g., fr-FR,en-GB)")
192192
appsLaunchCmd.Flags().StringVar(&activity, "activity", "", "Android activity to launch (e.g. .DebugActivity or com.example/.DebugActivity)")
193-
appsLaunchCmd.Flags().StringArrayVar(&launchArgs, "arg", nil, "Argument to pass to the app, repeat for more (iOS only)")
193+
appsLaunchCmd.Flags().StringArrayVar(&launchArgs, "arg", nil, "Argument to pass to the app, repeat for more. On Android it must be key=value and is passed as a string intent extra")
194194
appsLaunchCmd.Flags().StringArrayVar(&launchEnv, "env", nil, "Environment variable KEY=VALUE to set for the app, repeat for more (iOS only)")
195195
appsTerminateCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to terminate app on")
196196
appsListCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to list apps from")

‎devices/android.go‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -478,8 +478,9 @@ func (d *AndroidDevice) resolveLauncherActivity(bundleID string) (string, error)
478478
}
479479

480480
func (d *AndroidDevice) LaunchApp(bundleID string, opts LaunchOptions) error {
481-
if len(opts.Arguments) > 0 {
482-
return fmt.Errorf("--arg is not supported on Android")
481+
extras, err := launchExtras(opts.Arguments)
482+
if err != nil {
483+
return err
483484
}
484485
if len(opts.Env) > 0 {
485486
return fmt.Errorf("--env is not supported on Android")
@@ -499,7 +500,6 @@ func (d *AndroidDevice) LaunchApp(bundleID string, opts LaunchOptions) error {
499500
}
500501

501502
var component string
502-
var err error
503503
if opts.Activity != "" {
504504
component, err = buildLaunchComponent(bundleID, opts.Activity)
505505
} else {
@@ -509,7 +509,7 @@ func (d *AndroidDevice) LaunchApp(bundleID string, opts LaunchOptions) error {
509509
return err
510510
}
511511

512-
output, err := d.runAdbShell(launchArgs(component, d.isAppRunning(bundleID))...)
512+
output, err := d.runAdbShell(launchArgs(component, d.isAppRunning(bundleID), extras)...)
513513
if err != nil {
514514
return fmt.Errorf("failed to launch app %s: %w\nOutput: %s", bundleID, err, string(output))
515515
}
@@ -522,14 +522,30 @@ func (d *AndroidDevice) LaunchApp(bundleID string, opts LaunchOptions) error {
522522
// permission prompt, stays on top: a plain start just resumes it. When the app isn't
523523
// running, clear the task so the launch shows the app. When it is running, keep the
524524
// task so launching brings the app to the front as it was.
525-
func launchArgs(component string, isRunning bool) []string {
525+
func launchArgs(component string, isRunning bool, extras []string) []string {
526526
args := []string{"am", "start", "--display", defaultDisplayID}
527527
if !isRunning {
528528
args = append(args, "--activity-clear-task")
529529
}
530+
args = append(args, extras...)
530531
return append(args, "-n", component)
531532
}
532533

534+
// launchExtras turns key=value launch arguments into `am start --es key value`
535+
// string extras, which the app reads from its launch intent. The value may
536+
// contain '=' and may be empty.
537+
func launchExtras(arguments []string) ([]string, error) {
538+
extras := make([]string, 0, len(arguments)*3)
539+
for _, argument := range arguments {
540+
key, value, found := strings.Cut(argument, "=")
541+
if !found || key == "" {
542+
return nil, fmt.Errorf("invalid --arg %q, on Android each argument must be key=value", argument)
543+
}
544+
extras = append(extras, "--es", key, value)
545+
}
546+
return extras, nil
547+
}
548+
533549
// isAppRunning reports whether the package has a live process. pidof exits non-zero
534550
// when there is none.
535551
func (d *AndroidDevice) isAppRunning(bundleID string) bool {

‎devices/android_launch_test.go‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,17 +140,42 @@ func Test_adbShellArgs_keepsEveryQueryParameterOfAURL(t *testing.T) {
140140
}
141141

142142
func Test_launchArgs_clearsTheTaskWhenTheAppIsNotRunning(t *testing.T) {
143-
got := strings.Join(launchArgs("com.example/.Main", false), " ")
143+
got := strings.Join(launchArgs("com.example/.Main", false, nil), " ")
144144
want := "am start --display " + defaultDisplayID + " --activity-clear-task -n com.example/.Main"
145145
if got != want {
146146
t.Fatalf("launchArgs() = %q, want %q", got, want)
147147
}
148148
}
149149

150150
func Test_launchArgs_keepsTheTaskWhenTheAppIsRunning(t *testing.T) {
151-
got := strings.Join(launchArgs("com.example/.Main", true), " ")
151+
got := strings.Join(launchArgs("com.example/.Main", true, nil), " ")
152152
want := "am start --display " + defaultDisplayID + " -n com.example/.Main"
153153
if got != want {
154154
t.Fatalf("launchArgs() = %q, want %q", got, want)
155155
}
156156
}
157+
158+
func Test_launchArgs_passesExtrasBeforeTheComponent(t *testing.T) {
159+
got := strings.Join(launchArgs("com.example/.Main", true, []string{"--es", "flag", "on"}), " ")
160+
want := "am start --display " + defaultDisplayID + " --es flag on -n com.example/.Main"
161+
if got != want {
162+
t.Fatalf("launchArgs() = %q, want %q", got, want)
163+
}
164+
}
165+
166+
func Test_launchExtras(t *testing.T) {
167+
got, err := launchExtras([]string{"apiUrl=https://x.test/?a=b", "empty="})
168+
if err != nil {
169+
t.Fatalf("unexpected error: %v", err)
170+
}
171+
want := "--es apiUrl https://x.test/?a=b --es empty "
172+
if strings.Join(got, " ") != want {
173+
t.Errorf("launchExtras() = %q, want %q", strings.Join(got, " "), want)
174+
}
175+
176+
for _, bad := range []string{"-FeatureX", "=value"} {
177+
if _, err := launchExtras([]string{bad}); err == nil {
178+
t.Errorf("launchExtras(%q): expected error, got nil", bad)
179+
}
180+
}
181+
}

‎devices/common.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ type FileEntry struct {
153153

154154
// LaunchOptions carries optional parameters for launching an app.
155155
// Activity is Android-only; passing it to an iOS device is an error.
156-
// Arguments and Env are iOS-only; passing them to an Android device is an error.
156+
// Arguments are process arguments on iOS. On Android each one must be key=value
157+
// and is passed as a string intent extra. Env is iOS-only; passing it to an
158+
// Android device is an error.
157159
type LaunchOptions struct {
158160
Locales []string
159161
Activity string

‎devices/simulator_launch_test.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ func Test_simctlLaunchEnv(t *testing.T) {
5050
}
5151
}
5252

53-
func Test_LaunchApp_ArgumentsAndEnvRejectedOnAndroid(t *testing.T) {
53+
func Test_LaunchApp_EnvAndBareArgumentsRejectedOnAndroid(t *testing.T) {
5454
device := &AndroidDevice{}
5555

56-
if err := device.LaunchApp("com.example.app", LaunchOptions{Arguments: []string{"-x"}}); err == nil {
57-
t.Fatal("expected error for arguments, got nil")
56+
if err := device.LaunchApp("com.example.app", LaunchOptions{Arguments: []string{"-FeatureX"}}); err == nil {
57+
t.Fatal("expected error for an argument without '=', got nil")
5858
}
5959

6060
if err := device.LaunchApp("com.example.app", LaunchOptions{Env: map[string]string{"A": "1"}}); err == nil {

‎skills/mobilecli/reference.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@ All commands support the global `--device <id>` flag to specify the target devic
5151
* **Launch / Terminate**:
5252
```bash
5353
mobilecli apps launch <bundle-id> --device <device-id>
54-
# iOS only: repeat --arg and --env as needed
54+
# iOS: repeat --arg and --env as needed
5555
mobilecli apps launch <bundle-id> --device <device-id> --arg -FeatureX --arg YES --env API_URL=https://staging.example.com
56+
# Android: each --arg is key=value and is passed as a string intent extra (--es key value)
57+
mobilecli apps launch <package-name> --device <device-id> --arg featureX=true
5658
mobilecli apps terminate <bundle-id> --device <device-id>
5759
```
5860
* **Install / Uninstall**:

0 commit comments

Comments
 (0)