diff --git a/internal/android/android.go b/internal/android/android.go index 7af8838..0c7aa26 100644 --- a/internal/android/android.go +++ b/internal/android/android.go @@ -251,8 +251,26 @@ func ForegroundPackage(ctx context.Context) (string, error) { return parseForegroundPackage(string(output)), nil } +// FocusedWindowPackage returns the package owning the currently focused window, +// or "" when no window is focused (e.g. mid-launch, before the app has drawn). +// Unlike ForegroundPackage, this reflects what is actually on screen: +// ResumedActivity flips to a newly launched app before its first frame renders, +// while mCurrentFocus only names the app once its window is up. +func FocusedWindowPackage(ctx context.Context) (string, error) { + adb, err := AdbBinary() + if err != nil { + return "", err + } + output, err := exec.CommandContext(ctx, adb, "shell", "dumpsys", "window").Output() + if err != nil { + return "", err + } + return parseFocusedWindowPackage(string(output)), nil +} + // resumedActivityPackage matches the "/" component name that -// dumpsys prints on ResumedActivity lines, capturing the package. +// dumpsys prints on ResumedActivity and mCurrentFocus lines, capturing the +// package. var resumedActivityPackage = regexp.MustCompile(`([a-zA-Z][a-zA-Z0-9_.]*)/[a-zA-Z0-9_.$]+`) // parseForegroundPackage extracts the foreground package from `dumpsys activity @@ -269,6 +287,21 @@ func parseForegroundPackage(dumpsys string) string { return "" } +// parseFocusedWindowPackage extracts the focused-window package from +// `dumpsys window` output by reading the mCurrentFocus component name. A +// "mCurrentFocus=null" line (no focused window) yields "". +func parseFocusedWindowPackage(dumpsys string) string { + for line := range strings.SplitSeq(dumpsys, "\n") { + if !strings.Contains(line, "mCurrentFocus") { + continue + } + if match := resumedActivityPackage.FindStringSubmatch(line); match != nil { + return match[1] + } + } + return "" +} + func bootCompleted(ctx context.Context) (bool, error) { adb, err := AdbBinary() if err != nil { diff --git a/internal/android/android_test.go b/internal/android/android_test.go index 4bf3747..70f950d 100644 --- a/internal/android/android_test.go +++ b/internal/android/android_test.go @@ -127,3 +127,39 @@ func TestParseForegroundPackage(t *testing.T) { }) } } + +func TestParseFocusedWindowPackage(t *testing.T) { + cases := []struct { + name string + dumpsys string + want string + }{ + { + name: "folio focused", + dumpsys: " mCurrentFocus=Window{e00f63a u0 app.folio/app.folio.MainActivity}\n mFocusedApp=ActivityRecord{c0 u0 app.folio/.MainActivity t202}", + want: "app.folio", + }, + { + name: "settings focused", + dumpsys: " mCurrentFocus=Window{709 u0 com.android.settings/com.android.settings.SubSettings}", + want: "com.android.settings", + }, + { + name: "no focused window mid-launch", + dumpsys: " mCurrentFocus=null\n mFocusedApp=null", + want: "", + }, + { + name: "no focus line", + dumpsys: " some unrelated dumpsys window output\n", + want: "", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := parseFocusedWindowPackage(tc.dumpsys); got != tc.want { + t.Errorf("parseFocusedWindowPackage = %q, want %q", got, tc.want) + } + }) + } +}