Skip to content

fix(android): apply settings airplane through the connectivity service - #2234

Merged
thymikee merged 3 commits into
mainfrom
claude/issue-2223-fix-0e9f67
Sep 2, 2026
Merged

fix(android): apply settings airplane through the connectivity service#2234
thymikee merged 3 commits into
mainfrom
claude/issue-2223-fix-0e9f67

Conversation

@thymikee

@thymikee thymikee commented Sep 2, 2026

Copy link
Copy Markdown
Member

Summary

settings airplane on|off did not take an Android device offline, and it failed after changing
device state. The Android owner wrote airplane_mode_on and then broadcast
android.intent.action.AIRPLANE_MODE, which Android refuses for non-system callers, so the command
surfaced COMMAND_FAILED while the device reported airplane mode with the radios still up —
anything reading airplane_mode_on saw a state that traffic contradicted.

Airplane mode is now applied by the component that owns it. cmd connectivity airplane-mode both
drives the connectivity stack and reports the state, so the owner:

  • reads the state first, which is what proves the build supports the operation — a build that
    cannot answer is refused with UNSUPPORTED_OPERATION before anything is written, so the write is
    unreachable rather than guarded;
  • reads it again after the change, so the response carries the mode connectivity holds, not the one
    that was requested;
  • issues no settings put and no broadcast on any path.
$ agent-device settings airplane on --platform android --json
{ "setting": "airplane", "state": "on", "airplaneMode": "enabled", "message": "Updated setting: airplane" }

Per triage the old write/broadcast pair is not kept as a compatibility fallback: it is known to fail
dirty. Maestro setAirplaneMode/toggleAirplaneMode parity stays out of scope for a separate issue.

Closes #2223

Validation

Live Android emulator (sdk_gphone64_arm64, release 16 / API 36), through the built CLI at
5ebb770b3a:

  • settings airplane on answered airplaneMode: enabled. Four seconds later the device reported
    cmd connectivity airplane-mode = enabled, airplane_mode_on = 1, and ping 8.8.8.8 =
    connect: Network is unreachable.
  • settings airplane off answered airplaneMode: disabled, and connectivity came back: state
    disabled, airplane_mode_on = 0, ping 2/2 received.
  • The reported cause reproduces on the same device: am broadcast -a android.intent.action.AIRPLANE_MODE fails with SecurityException: Permission Denial … uid=2000.

Seven scenarios in packages/platform-android/src/__tests__/settings-airplane.test.ts drive the
public setAndroidSetting entry: the enable and disable round trips assert the exact adb call list,
which is what a revert to settings put + broadcast fails; a service that accepts the write without
changing state must not be reported as success; unsupported and unreadable builds are refused with
the read as the only call issued; an adb transport failure stays COMMAND_FAILED with its reconnect
hint instead of being mistyped as unsupported; and a read that fails only after the change is a
command failure, not an unsupported build. All seven were observed red against the pre-fix owner.

pnpm check:affected --run is green except for test/integration/provider-scenarios/limrun-ios-snapshot-owner.test.ts,
which times out identically on main at 7ee1a5ded7 with this branch checked out or not — a
pre-existing failure on that iOS provider scenario, untouched by this change.

Notes

  • Six files: the Android settings owner and its new airplane module plus tests, the settings
    command help, the command reference, and the changelog.
  • Residual risk: the UNSUPPORTED_OPERATION leaf is fixture-covered only — no pre-Android-11 device
    was available. Nothing can be written on that path, since the write follows the read that refuses.
  • Connectivity takes a moment to settle after the switch (offline observed within seconds, not
    instantly). The command reference now says so, so callers poll the app under test instead of
    asserting offline behaviour on the next line.

settings airplane wrote airplane_mode_on and then broadcast
android.intent.action.AIRPLANE_MODE, which Android refuses for non-system
callers. The write landed, the broadcast failed, and the device reported
airplane mode with the radios still up.

The connectivity service now owns the change: it is read to prove the build
supports airplane mode before anything is written, driven with
cmd connectivity airplane-mode enable|disable, and read again so the response
reports the mode connectivity holds rather than the one requested. Builds
without that command are refused unmutated with UNSUPPORTED_OPERATION.

Closes #2223
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-02 14:54 UTC

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.58 MB 2.58 MB +1.2 kB
JS gzip 868.0 kB 868.4 kB +362 B
npm bundled raw 2.58 MB 2.58 MB +1.2 kB
npm bundled gzip 868.0 kB 868.4 kB +362 B
npm tarball 1.02 MB 1.02 MB +376 B
npm unpacked 3.46 MB 3.46 MB +1.2 kB
npm clean-installed 3.46 MB 3.46 MB +1.2 kB

npm unpacked components

Component Base Current Diff
JS / dist source 2.74 MB 2.74 MB +1.2 kB
Apple runner source/project 544.4 kB 544.4 kB 0 B
Apple snapshot presentation source 33.8 kB 33.8 kB 0 B
macOS helper source 54.8 kB 54.8 kB 0 B
Android helper artifacts 43.3 kB 43.3 kB 0 B
Other package files 46.3 kB 46.3 kB 0 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.0 ms 26.0 ms -1.0 ms
CLI --help 70.3 ms 67.3 ms -3.0 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/mechanics.js +1.1 kB +281 B
dist/src/registry.js +184 B +78 B
dist/src/platform-runtime-operation-host.js +5 B +3 B

Top changed packed files

Packed file Base Current Diff
dist/src/mechanics.js 154.3 kB 155.4 kB +1.1 kB
dist/src/registry.js 170.3 kB 170.4 kB +184 B
dist/src/platform-runtime-operation-host.js 27.8 kB 27.8 kB +5 B

@thymikee

thymikee commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Reviewed exact head 5ebb770b3a6fff5e1e8f0f43cf12a0b847d5fb5c. Two blockers remain:

  1. settings-airplane.ts currently maps every unrecognized nonzero initial probe failure to UNSUPPORTED_OPERATION unless the generic adb transport classifier happens to recognize it. A permission denial, connectivity-service crash/internal error, or other execution failure would therefore be reported as ‘requires Android 11; use a newer device’ rather than COMMAND_FAILED. Narrow unsupported detection to explicit capability-absence output (Unknown command/missing shell implementation or equivalent); preserve all other nonzero results through androidAdbResultError. Add the closest-negative regression for an unclassified nonzero failure and prove the write remains unreachable.

  2. Exact-head Coverage has a code-owned eager-closure failure: packages/platform-android/src/mechanics.ts evaluates 178 modules versus the pinned 177 because settings.ts statically imports the new airplane module. The extraction itself is reasonable given settings.ts size, but this loading-shape change must be measured and owned: either keep airplane-only code lazy from the broad mechanics surface or justify/update the budget to 178, then rerun Coverage green.

Otherwise the fix is sound: the public Android owner follows connectivity read → write → read, reports observed state, removes the dirty settings/broadcast path, has credible revert-red focused tests and built-CLI emulator round-trip evidence, and updates help/docs/changelog. Size is healthy (+1.2 kB npm unpacked); every other exact-head check, including all smoke lanes, is green. No ready-for-human label until both blockers are resolved.

Splitting the airplane owner out of settings.ts adds one module to the
mechanics facet, which is implementation-eager by design. The row moves to the
measured number in the PR that grows it.
…mode

An unrecognized nonzero probe — a permission denial, a connectivity-service
error — was answered with "requires Android 11; use a newer device". Only the
prose adb prints when a build ships no shell implementation for the command
now selects UNSUPPORTED_OPERATION; every other failed read stays
COMMAND_FAILED with its classified hint, and the write is unreachable from
both.

The predicate that reads that prose already existed for the clipboard service
and is now named for the question it answers, so airplane mode reuses it
instead of adding a second message sniff.
@thymikee

thymikee commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Both blockers are addressed at e9a2417218.

1. Unsupported detection narrowed to capability absence. UNSUPPORTED_OPERATION is now selected only by the prose adb prints when a build ships no shell implementation for a cmd invocation; every other failed read — permission denial, connectivity-service error, transport failure, unparseable output at a clean exit — goes through androidAdbResultError, so it stays COMMAND_FAILED with its classified hint.

The predicate that reads that prose already existed as isClipboardShellUnsupported in adb.ts, doing exactly this job for cmd clipboard. Rather than write a second message sniff, it is renamed isAndroidShellCommandUnsupported and both services use it; its "only ask this about a call that failed" contract is honored on both sides (airplane mode consults it only on a nonzero exit, since a parseable state settles a clean exit first).

Regression coverage, all red against 5ebb770b3a:

  • SecurityException: Permission Denial … at exit 255 → COMMAND_FAILED, and the adb call list is exactly [read], so the write stays unreachable.
  • unreadable output at exit 0 → COMMAND_FAILED, same single-call list (it previously read as unsupported).
  • Unknown command: airplane-mode at exit 255 → still UNSUPPORTED_OPERATION, still one call.
  • error: device offlineCOMMAND_FAILED carrying the reconnect hint, unchanged.

2. Eager-closure budget owned, not dodged. packages/platform-android/src/mechanics.ts moves to 178 in scripts/__tests__/eager-closure-budgets.ts. Keeping the airplane owner lazy would make it the one settings helper behind a function-scoped import while settings-permission.ts and settings-parsing.ts load statically, and the mechanics facet is implementation-eager by design once selected — so the honest answer is the row move the gate's own failure message prescribes. Measured locally: 178 with the pin at 177 fails naming this entry, 178 passes.

Re-validated the round trip on the same emulator (sdk_gphone64_arm64, release 16 / API 36) with the CLI built at e9a2417218: airplane onairplaneMode: enabled, airplane_mode_on=1, ping 8.8.8.8 = connect: Network is unreachable; airplane offairplaneMode: disabled, airplane_mode_on=0, ping 2/2. Session-free run; the isolated daemon was stopped and its state dir removed.

pnpm check:affected --run is green apart from test/integration/provider-scenarios/limrun-ios-snapshot-owner.test.ts, which still times out identically on an unmodified main at 7ee1a5ded7 — pre-existing and unrelated to this branch.

@thymikee

thymikee commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Re-reviewed exact head e9a241721864dd7737f281c42e59cb9bc5e6935b: code review is clean and the PR is ready for human review.

The prior failure-classification blocker is fixed: unsupported detection is limited to explicit missing-shell/unknown-command output, while the SecurityException closest-negative remains COMMAND_FAILED and proves the write is unreachable. The +1 intentionally eager mechanics closure is measured and justified, the successful connectivity read → write → read path is unchanged, all exact-head checks are green, and the live emulator evidence at 5ebb770 remains applicable to that unchanged success path.

Non-blocking PR-body cleanup: it still says seven scenarios and six files; the current head has eight scenarios and 11 changed files. Please update those counts and record the green exact-head CI.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 2, 2026
@thymikee
thymikee merged commit 2c7fb93 into main Sep 2, 2026
20 checks passed
@thymikee
thymikee deleted the claude/issue-2223-fix-0e9f67 branch September 2, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant