diff --git a/devices/android.go b/devices/android.go index 8e81733..6e1e980 100644 --- a/devices/android.go +++ b/devices/android.go @@ -1069,20 +1069,30 @@ func (d *AndroidDevice) getForegroundComponent() (string, string, error) { return "", "", fmt.Errorf("failed to get window displays: %w", err) } - // parse package name from mCurrentFocus line - // format: mCurrentFocus=Window{... u0 com.package.name/com.package.name.MainActivity} - lines := strings.Split(string(output), "\n") - for _, line := range lines { - if strings.Contains(line, "mCurrentFocus") { - parts := strings.Fields(line) - if len(parts) >= 3 { - focusPart := strings.TrimSuffix(parts[2], "}") - // split into package name (before the '/') and activity (after) - if idx := strings.Index(focusPart, "/"); idx != -1 { - return focusPart[:idx], focusPart[idx+1:], nil - } - } - break + return parseForegroundComponent(string(output)) +} + +// parseForegroundComponent extracts the focused package and activity from +// `dumpsys window displays` lines of the form: +// +// mCurrentFocus=Window{... u0 com.package.name/com.package.name.MainActivity} +// +// A multi-display device prints one such line per display and the unfocused ones read +// "mCurrentFocus=null", so a line that does not parse means "not this display" rather +// than "no foreground app". +func parseForegroundComponent(output string) (string, string, error) { + for _, line := range strings.Split(output, "\n") { + if !strings.Contains(line, "mCurrentFocus") { + continue + } + parts := strings.Fields(line) + if len(parts) < 3 { + continue + } + focusPart := strings.TrimSuffix(parts[2], "}") + // split into package name (before the '/') and activity (after) + if idx := strings.Index(focusPart, "/"); idx != -1 { + return focusPart[:idx], focusPart[idx+1:], nil } } diff --git a/devices/android_foreground_test.go b/devices/android_foreground_test.go new file mode 100644 index 0000000..e9b59f1 --- /dev/null +++ b/devices/android_foreground_test.go @@ -0,0 +1,56 @@ +package devices + +import "testing" + +func Test_parseForegroundComponent(t *testing.T) { //nolint:funlen + tests := []struct { + name string + input string + wantPackage string + wantActivity string + wantErr bool + }{ + { + name: "single display", + input: " mCurrentFocus=Window{9c8e10c u0 com.example.app/com.example.app.MainActivity}", + wantPackage: "com.example.app", + wantActivity: "com.example.app.MainActivity", + }, + { + name: "multi display with a null before the focused one", + input: " mCurrentFocus=null\n" + + " mCurrentFocus=Window{d0ebdc2 u0 com.example.app/com.example.app.MainActivity}", + wantPackage: "com.example.app", + wantActivity: "com.example.app.MainActivity", + }, + { + name: "every display unfocused", + input: " mCurrentFocus=null\n mCurrentFocus=null", + wantErr: true, + }, + { + name: "no mCurrentFocus line at all", + input: "Display: mDisplayId=0\n mBaseDisplayWidth=1080", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkg, activity, err := parseForegroundComponent(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("parseForegroundComponent() expected an error, got %q/%q", pkg, activity) + } + return + } + if err != nil { + t.Fatalf("parseForegroundComponent() error = %v", err) + } + if pkg != tt.wantPackage || activity != tt.wantActivity { + t.Errorf("parseForegroundComponent() = %q/%q, want %q/%q", + pkg, activity, tt.wantPackage, tt.wantActivity) + } + }) + } +}