Skip to content

settings airplane is a no-op on modern Android and leaves airplane_mode_on=1 with the network still up #2223

Description

@andrewsjobber

Summary

agent-device settings airplane on does not take an Android device offline. It writes airplane_mode_on=1 and then sends an android.intent.action.AIRPLANE_MODE broadcast, which requires a system permission and fails. Because the settings put runs first and succeeds, the device is left reporting airplane mode while connectivity is fully up.

This fails dirty rather than cleanly: the CLI surfaces an error, but the device state has already been half-mutated, so anything that checks airplane_mode_on sees 1 while traffic still flows.

Repro

Android emulator, ro.build.version.release=17, ro.build.version.sdk=37:

$ agent-device settings airplane on --platform android
Error (COMMAND_FAILED): Exception occurred while executing 'broadcast': Hint: Retry with --debug and inspect diagnostics log for details.

# diagnostics ndjson
{"level":"error","phase":"platform_command","command":"settings","data":{"error":"adb exited with code 255"}}

$ adb shell settings get global airplane_mode_on 1  # <- setting landed

$ adb shell ping -c 2 8.8.8.8
2 packets transmitted, 2 received, 0% packet loss   # <- still online

Expected: the device is offline, or the command fails without changing device state.
Actual: the command fails, airplane_mode_on=1, and the network is unaffected.

Why this happens in agent-device

dist/src/android.js, the settings handler's airplane case:

case `airplane`: {
  let t = A(r), n = t ? `1` : `0`, i = t ? `true` : `false`;
  await d(e, [`shell`, `settings`, `put`, `global`, `airplane_mode_on`, n]),
  await d(e, [`shell`, `am`, `broadcast`, `-a`,
              `android.intent.action.AIRPLANE_MODE`, `--ez`, `state`, i]);
  return
}
  1. settings put global airplane_mode_on <0|1> succeeds — it only writes the setting.
  2. am broadcast -a android.intent.action.AIRPLANE_MODE is rejected; broadcasting that action is restricted to system callers, so adb exits 255.
  3. Nothing ever tells the connectivity stack to act, so the radio state never changes.

Two notes:

  • Not a regression. I diffed the published 0.20.0 and 0.20.10 tarballs and this block is identical (modulo minified identifier renaming), so upgrading does not help.
  • The wifi case has a related gap. settings wifi off runs svc wifi disable, which genuinely disables Wi-Fi, but an emulator then falls back to cellular and stays online — so it also cannot be used to reach an offline state.

Suggested fix

cmd connectivity airplane-mode <enable|disable> works on Android 11+, needs no root, and actually drives the connectivity stack. Verified on the same API 37 emulator:

$ adb shell cmd connectivity airplane-mode enable
$ adb shell cmd connectivity airplane-mode 
enabled
$ adb shell ping -c 2 8.8.8.8
connect: Network is unreachable

Round trip back to disable restores connectivity cleanly. Latency from issuing the command to traffic actually failing was measured at 65–365ms across trials.

Suggested shape: try cmd connectivity airplane-mode first, and fall back to the existing settings put + broadcast pair only if that command is unavailable, so old devices keep whatever behaviour they have today. If the fallback is kept, it would be worth ordering the broadcast before the settings put, or reverting the setting on broadcast failure, so a failure cannot leave airplane_mode_on inconsistent with actual connectivity.

Related: no setAirplaneMode in the Maestro dialect

Possibly a separate issue, but it is the same "Maestro parity" shape as #2159.

Real Maestro agent-device --maestro
setAirplaneMode supported (Android) not implemented
toggleAirplaneMode supported (Android) not implemented

Maestro documents both commands as Android-supported. In --maestro mode, neither is recognized, and the runScript sandbox exposes only output, json, and http.post — no require/process — so a flow cannot reach adb by any other route either.

The practical effect is that a flow cannot change connectivity mid-run at all: offline coverage has to be split into separate flows driven by an external script, or routed through a host-side HTTP sidecar called via http.post. Since settings airplane already exists internally, wiring those two Maestro commands to it looks like a small addition once the underlying toggle works.

Environment

  • agent-device 0.20.0 and 0.20.10 (same behaviour on both)
  • Android emulator, Pixel 10, ro.build.version.release=17 / sdk=37
  • macOS host

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions