From fc9e78ab2653c9a80bdbdde4f98a426aed2da800 Mon Sep 17 00:00:00 2001 From: Somkiat Khitwongwattana Date: Wed, 16 Sep 2026 03:13:19 +0700 Subject: [PATCH] fix(android): keep scanning mCurrentFocus across every display getForegroundComponent() stopped at the first mCurrentFocus line in `dumpsys window displays`. A device with more than one display reports one mCurrentFocus per display and the unfocused ones read "mCurrentFocus=null", so on such a device the first line is frequently the null and foreground detection failed outright with "could not determine foreground app". Seen on Genymotion (Android 15), where it takes down every webview command, since all of them resolve the foreground package first. Keep scanning until a line actually parses. Extracted the parsing into parseForegroundComponent() so it can be tested without a device. Co-Authored-By: Claude Opus 5 (1M context) --- devices/android.go | 38 ++++++++++++-------- devices/android_foreground_test.go | 56 ++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 devices/android_foreground_test.go 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) + } + }) + } +}