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.
This commit is contained in:
pj committed 2026-06-10 18:59:23 +05:30
1 parent 229f015281
commit 542bf11a02
3 files changed
+112

No files matched your search

+76
View File
@@ -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()
+31
View File
@@ -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))
+5
View File
@@ -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 {