From 542bf11a021cbde81800f45c9b383ff7c40db3c7 Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 10 Jun 2026 18:59:23 +0530 Subject: [PATCH] feat(android): force 3-button nav during runs to prevent app drift On gesture navigation a fuzzer swipe can trigger swipe-up-home or edge-back and fling the app off screen. Device-prep now switches to 3-button navigation for the run (no edge gestures; the nav bar's buttons are systemui-owned and already excluded from action candidates) and restores the original navigation mode when the run ends. Best effort: leaves nav untouched if the overlay command is unavailable. --- internal/android/android.go | 76 ++++++++++++++++++++++++++++++++ internal/android/android_test.go | 31 +++++++++++++ internal/testrun/testrun.go | 5 +++ 3 files changed, 112 insertions(+) diff --git a/internal/android/android.go b/internal/android/android.go index d377788..cc59c80 100644 --- a/internal/android/android.go +++ b/internal/android/android.go @@ -136,6 +136,82 @@ func ReinstallApp(ctx context.Context, serial, bundleID, apkPath string, stdout return nil } +const threeButtonNavOverlay = "com.android.internal.systemui.navbar.threebutton" + +// navModeOverlays are the system navigation-mode overlays. Only one is active at +// a time; the active one is restored after the run. +var navModeOverlays = []string{ + "com.android.internal.systemui.navbar.gestural", + threeButtonNavOverlay, + "com.android.internal.systemui.navbar.twobutton", +} + +// ForceThreeButtonNav switches the device to 3-button navigation for the run, so +// the fuzzer's swipes cannot trigger the gesture-nav home/back actions and fling +// the app off screen (the nav bar's own buttons are systemui-owned and already +// dropped from action candidates). It returns a function that restores the +// original navigation mode. Best effort: on any failure it leaves navigation +// untouched and returns a no-op restore. +func ForceThreeButtonNav(ctx context.Context, serial string, stdout io.Writer) func() { + adb, err := AdbBinary() + if err != nil { + return func() {} + } + original := enabledNavOverlay(ctx, adb, serial) + if err := navOverlayCommand(ctx, adb, serial, threeButtonNavOverlay).Run(); err != nil { + fmt.Fprintf(stdout, "device prep: skipping 3-button nav (%v)\n", err) + return func() {} + } + if original == "" || original == threeButtonNavOverlay { + return func() {} + } + return func() { + if err := navOverlayCommand(context.Background(), adb, serial, original).Run(); err != nil { + fmt.Fprintf(stdout, "device prep: could not restore nav mode %s (%v)\n", original, err) + } + } +} + +// enabledNavOverlay returns the currently active navigation-mode overlay, or "" +// when it cannot be determined. +func enabledNavOverlay(ctx context.Context, adb, serial string) string { + args := []string{} + if serial != "" { + args = append(args, "-s", serial) + } + args = append(args, "shell", "cmd", "overlay", "list") + output, err := exec.CommandContext(ctx, adb, args...).Output() + if err != nil { + return "" + } + return parseEnabledNavOverlay(string(output)) +} + +// parseEnabledNavOverlay reads `cmd overlay list` output and returns the +// enabled ("[x]") navigation-mode overlay package. +func parseEnabledNavOverlay(overlayList string) string { + for line := range strings.SplitSeq(overlayList, "\n") { + trimmed := strings.TrimSpace(line) + if !strings.HasPrefix(trimmed, "[x]") { + continue + } + package_ := strings.TrimSpace(strings.TrimPrefix(trimmed, "[x]")) + if slices.Contains(navModeOverlays, package_) { + return package_ + } + } + return "" +} + +func navOverlayCommand(ctx context.Context, adb, serial, overlay string) *exec.Cmd { + args := []string{} + if serial != "" { + args = append(args, "-s", serial) + } + args = append(args, "shell", "cmd", "overlay", "enable-exclusive", overlay) + return exec.CommandContext(ctx, adb, args...) +} + // AdbReverse sets up adb reverse forwarding for a local abstract socket. func AdbReverse(socket string, port int) error { adb, err := AdbBinary() diff --git a/internal/android/android_test.go b/internal/android/android_test.go index a1cbc40..735ee27 100644 --- a/internal/android/android_test.go +++ b/internal/android/android_test.go @@ -201,6 +201,37 @@ func TestParseFocusedWindowPackage(t *testing.T) { } } +func TestParseEnabledNavOverlay(t *testing.T) { + cases := []struct { + name string + listing string + want string + }{ + { + name: "gesture nav active", + listing: "[ ] com.android.internal.systemui.navbar.threebutton\n[x] com.android.internal.systemui.navbar.gestural\n[ ] com.android.internal.systemui.navbar.transparent", + want: "com.android.internal.systemui.navbar.gestural", + }, + { + name: "three-button active", + listing: "[x] com.android.internal.systemui.navbar.threebutton\n[ ] com.android.internal.systemui.navbar.gestural", + want: "com.android.internal.systemui.navbar.threebutton", + }, + { + name: "ignores enabled non-nav overlays", + listing: "[x] com.some.other.overlay\n[ ] com.android.internal.systemui.navbar.gestural", + want: "", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := parseEnabledNavOverlay(tc.listing); got != tc.want { + t.Errorf("parseEnabledNavOverlay = %q, want %q", got, tc.want) + } + }) + } +} + func TestAntiFreezeCommands_DisablesFreezersAndExemptsDriver(t *testing.T) { commands := antiFreezeCommands() joined := make([]string, len(commands)) diff --git a/internal/testrun/testrun.go b/internal/testrun/testrun.go index f7db213..cda97a1 100644 --- a/internal/testrun/testrun.go +++ b/internal/testrun/testrun.go @@ -54,6 +54,11 @@ func Execute(ctx context.Context, options Options, stdout io.Writer) error { if err := android.PrepareDevice(ctx, options.Device, stdout); err != nil { return err } + // Switch to 3-button navigation for the run so fuzzer swipes cannot + // trigger the gesture-nav home/back and fling the app off screen; + // restore the original mode when the run ends. + restoreNav := android.ForceThreeButtonNav(ctx, options.Device, stdout) + defer restoreNav() case "ios": resolved, err := resolveIOSTarget(ctx, options, stdout) if err != nil {