From 5ebb770b3a6fff5e1e8f0f43cf12a0b847d5fb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 2 Sep 2026 14:36:54 +0200 Subject: [PATCH 1/3] fix(android): apply settings airplane through the connectivity service 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 --- CHANGELOG.md | 7 + .../src/__tests__/settings-airplane.test.ts | 137 ++++++++++++++++++ .../platform-android/src/settings-airplane.ts | 87 +++++++++++ packages/platform-android/src/settings.ts | 17 +-- src/commands/capture/settings.ts | 2 +- website/docs/docs/commands.md | 1 + 6 files changed, 235 insertions(+), 16 deletions(-) create mode 100644 packages/platform-android/src/__tests__/settings-airplane.test.ts create mode 100644 packages/platform-android/src/settings-airplane.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b75f5c0..ffd8555d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- Fixed: `settings airplane on|off` now takes an Android device offline. It is applied through + the connectivity service (`cmd connectivity airplane-mode`), which drives the radios, instead of + writing `airplane_mode_on` and broadcasting `ACTION_AIRPLANE_MODE_CHANGED` — a broadcast Android + refuses for non-system callers, so the old path failed *after* writing the setting and left the + device reporting airplane mode with the network still up (#2223). The response now reports the + `airplaneMode` the connectivity service holds after the change, and an Android build that does not + expose that command is refused with `UNSUPPORTED_OPERATION` before anything is written. - Breaking (0.21): iOS Appium/WebDriver snapshots now expose engine-owned acquisition facts and typed fidelity warnings. The SDK snapshot `truncated` field is optional when Appium cannot report hierarchy completeness; regular snapshots fail closed without valid viewport evidence, while diff --git a/packages/platform-android/src/__tests__/settings-airplane.test.ts b/packages/platform-android/src/__tests__/settings-airplane.test.ts new file mode 100644 index 000000000..5772dc05c --- /dev/null +++ b/packages/platform-android/src/__tests__/settings-airplane.test.ts @@ -0,0 +1,137 @@ +import { test } from 'vitest'; +import assert from 'node:assert/strict'; +import { setAndroidSetting } from '../settings.ts'; +import type { AndroidAirplaneMode } from '../settings-airplane.ts'; +import { assertRejectsAppError } from './test-utils/app-error.ts'; +import { withFakeAdb, type FakeAdbScript } from './test-utils/fake-adb.ts'; + +const READ = 'shell cmd connectivity airplane-mode'; + +/** + * An Android build whose connectivity service owns airplane mode. `honorWrites: false` models the + * service accepting the command and leaving the state alone, which is what a silently ineffective + * airplane-mode path looks like from the outside. + */ +function connectivityService( + initial: AndroidAirplaneMode, + options: { honorWrites?: boolean } = {}, +): FakeAdbScript { + let state = initial; + return (args) => { + const flat = args.join(' '); + if (flat === READ) return state; + if (flat === `${READ} enable` || flat === `${READ} disable`) { + if (options.honorWrites !== false) state = flat.endsWith('enable') ? 'enabled' : 'disabled'; + return ''; + } + return { stderr: `unexpected args: ${flat}`, exitCode: 1 }; + }; +} + +test('setAndroidSetting airplane on enables through connectivity and reports its state', async () => { + await withFakeAdb(connectivityService('disabled'), async ({ calls, device }) => { + const result = await setAndroidSetting(device, 'airplane', 'on'); + assert.deepEqual(result, { airplaneMode: 'enabled' }); + assert.deepEqual( + calls.map((args) => args.join(' ')), + [READ, `${READ} enable`, READ], + ); + }); +}); + +test('setAndroidSetting airplane off disables through connectivity and reports its state', async () => { + await withFakeAdb(connectivityService('enabled'), async ({ calls, device }) => { + const result = await setAndroidSetting(device, 'airplane', 'off'); + assert.deepEqual(result, { airplaneMode: 'disabled' }); + assert.deepEqual( + calls.map((args) => args.join(' ')), + [READ, `${READ} disable`, READ], + ); + }); +}); + +test('setAndroidSetting airplane reports the state connectivity holds, not the one requested', async () => { + await withFakeAdb( + connectivityService('disabled', { honorWrites: false }), + async ({ calls, device }) => { + await assertRejectsAppError(() => setAndroidSetting(device, 'airplane', 'on'), { + code: 'COMMAND_FAILED', + message: /reads disabled after requesting enabled/, + }); + assert.deepEqual( + calls.map((args) => args.join(' ')), + [READ, `${READ} enable`, READ], + ); + }, + ); +}); + +test('setAndroidSetting airplane refuses builds without the connectivity command before writing', async () => { + await withFakeAdb( + () => ({ stdout: 'Unknown command: airplane-mode', exitCode: 255 }), + async ({ calls, device }) => { + await assertRejectsAppError(() => setAndroidSetting(device, 'airplane', 'on'), { + code: 'UNSUPPORTED_OPERATION', + message: /does not report airplane mode through the connectivity service/, + hint: /Android 11 \(API 30\)/, + }); + assert.deepEqual( + calls.map((args) => args.join(' ')), + [READ], + ); + }, + ); +}); + +test('setAndroidSetting airplane refuses unreadable state before writing', async () => { + await withFakeAdb( + () => 'Airplane mode: who knows', + async ({ calls, device }) => { + await assertRejectsAppError(() => setAndroidSetting(device, 'airplane', 'off'), { + code: 'UNSUPPORTED_OPERATION', + message: /does not report airplane mode through the connectivity service/, + }); + assert.deepEqual( + calls.map((args) => args.join(' ')), + [READ], + ); + }, + ); +}); + +test('setAndroidSetting airplane fails the change when the state cannot be read back', async () => { + const service = connectivityService('disabled'); + let changed = false; + await withFakeAdb( + (args) => { + const flat = args.join(' '); + if (flat === READ && changed) + return { stdout: 'Unknown command: airplane-mode', exitCode: 255 }; + if (flat === `${READ} enable`) changed = true; + return service(args); + }, + async ({ device }) => { + await assertRejectsAppError(() => setAndroidSetting(device, 'airplane', 'on'), { + code: 'COMMAND_FAILED', + message: /Failed to read Android airplane mode after changing it/, + }); + }, + ); +}); + +test('setAndroidSetting airplane keeps adb transport failures typed as command failures', async () => { + await withFakeAdb( + () => ({ stderr: 'error: device offline', exitCode: 1 }), + async ({ calls, device }) => { + await assertRejectsAppError(() => setAndroidSetting(device, 'airplane', 'on'), { + code: 'COMMAND_FAILED', + message: /Failed to read Android airplane mode/, + hint: /adb reconnect/, + }); + assert.deepEqual( + calls.map((args) => args.join(' ')), + [READ], + ); + }, + ); +}); diff --git a/packages/platform-android/src/settings-airplane.ts b/packages/platform-android/src/settings-airplane.ts new file mode 100644 index 000000000..4f7b9b08f --- /dev/null +++ b/packages/platform-android/src/settings-airplane.ts @@ -0,0 +1,87 @@ +import { AppError } from '@agent-device/kernel/errors'; +import type { DeviceInfo } from '@agent-device/kernel/device'; +import { runAndroidAdb } from './adb.ts'; +import { + androidAdbResultError, + classifyAdbFailure, + type AndroidAdbExecutorResult, +} from './adb-executor.ts'; + +export type AndroidAirplaneMode = 'enabled' | 'disabled'; + +const AIRPLANE_MODE_ARGS = ['shell', 'cmd', 'connectivity', 'airplane-mode'] as const; + +/** + * Android's connectivity service owns airplane mode: it reports the state and it drives the radios + * behind it. Writing `airplane_mode_on` and broadcasting `ACTION_AIRPLANE_MODE_CHANGED` instead + * moves the setting while connectivity stays up, and the broadcast is refused for non-system + * callers (#2223). + * + * Support is proven before the write and the state is read after it, so a build that cannot answer + * for airplane mode is refused unmutated and the reported mode is the one connectivity holds rather + * than the one that was asked for. + */ +export async function setAndroidAirplaneMode( + device: DeviceInfo, + enabled: boolean, +): Promise<{ airplaneMode: AndroidAirplaneMode }> { + const requested: AndroidAirplaneMode = enabled ? 'enabled' : 'disabled'; + await requireAndroidAirplaneModeSupport(device); + await runAndroidAdb(device, [...AIRPLANE_MODE_ARGS, enabled ? 'enable' : 'disable']); + const airplaneMode = await readAndroidAirplaneMode(device); + if (airplaneMode !== requested) { + throw new AppError( + 'COMMAND_FAILED', + `Android airplane mode reads ${airplaneMode} after requesting ${requested}.`, + { deviceId: device.id, requested, airplaneMode }, + ); + } + return { airplaneMode }; +} + +async function requireAndroidAirplaneModeSupport(device: DeviceInfo): Promise { + const { state, result } = await probeAndroidAirplaneMode(device); + if (state) return; + if (result.exitCode !== 0 && classifyAdbFailure(result.stderr, result.stdout)) { + throw androidAdbResultError('Failed to read Android airplane mode', result, { + deviceId: device.id, + }); + } + throw new AppError( + 'UNSUPPORTED_OPERATION', + 'This Android build does not report airplane mode through the connectivity service, so nothing was changed.', + { + deviceId: device.id, + hint: 'settings airplane needs cmd connectivity airplane-mode, which requires Android 11 (API 30) or newer. Use a newer device or emulator image.', + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + }, + ); +} + +async function readAndroidAirplaneMode(device: DeviceInfo): Promise { + const { state, result } = await probeAndroidAirplaneMode(device); + if (state) return state; + throw androidAdbResultError('Failed to read Android airplane mode after changing it', result, { + deviceId: device.id, + }); +} + +async function probeAndroidAirplaneMode(device: DeviceInfo): Promise<{ + state: AndroidAirplaneMode | undefined; + result: AndroidAdbExecutorResult; +}> { + const result = await runAndroidAdb(device, [...AIRPLANE_MODE_ARGS], { allowFailure: true }); + return { + state: result.exitCode === 0 ? parseAndroidAirplaneMode(result.stdout) : undefined, + result, + }; +} + +function parseAndroidAirplaneMode(stdout: string): AndroidAirplaneMode | undefined { + const value = stdout.trim().toLowerCase(); + if (value === 'enabled') return 'enabled'; + if (value === 'disabled') return 'disabled'; + return undefined; +} diff --git a/packages/platform-android/src/settings.ts b/packages/platform-android/src/settings.ts index 29bee0dba..30aed7ebc 100644 --- a/packages/platform-android/src/settings.ts +++ b/packages/platform-android/src/settings.ts @@ -9,6 +9,7 @@ import { type CommandAttemptFailure, } from './settings-parsing.ts'; import { runAndroidAdb } from './adb.ts'; +import { setAndroidAirplaneMode } from './settings-airplane.ts'; import { androidAdbResultError } from './adb-executor.ts'; import { resolveAndroidApp } from './app-deployment-resolution.ts'; import { setAndroidPermission } from './settings-permission.ts'; @@ -35,21 +36,7 @@ export async function setAndroidSetting( return; } case 'airplane': { - const enabled = parseSettingState(state); - const flag = enabled ? '1' : '0'; - const bool = enabled ? 'true' : 'false'; - await runAndroidAdb(device, ['shell', 'settings', 'put', 'global', 'airplane_mode_on', flag]); - await runAndroidAdb(device, [ - 'shell', - 'am', - 'broadcast', - '-a', - 'android.intent.action.AIRPLANE_MODE', - '--ez', - 'state', - bool, - ]); - return; + return await setAndroidAirplaneMode(device, parseSettingState(state)); } case 'location': { if (state.toLowerCase() === 'set') { diff --git a/src/commands/capture/settings.ts b/src/commands/capture/settings.ts index 9e2673b01..04983a105 100644 --- a/src/commands/capture/settings.ts +++ b/src/commands/capture/settings.ts @@ -60,7 +60,7 @@ export const settingsCommandFacet = defineCommandFacet({ text: { summary: 'Change OS settings and app permissions', cliDetail: - 'macOS supports only settings appearance and settings permission ; wifi|airplane|location|animations remain unsupported on macOS. Mobile permission actions use the active session app. On Android, deny|reset of a permission the app currently holds kills a running app; the response reports priorGrantState (granted|not_granted|unknown) and warns for granted and unknown, with open --relaunch to restore it. Permission changes require a resolvable foreground user and fail without mutating if adb cannot report one.', + 'macOS supports only settings appearance and settings permission ; wifi|airplane|location|animations remain unsupported on macOS. Mobile permission actions use the active session app. On Android, deny|reset of a permission the app currently holds kills a running app; the response reports priorGrantState (granted|not_granted|unknown) and warns for granted and unknown, with open --relaunch to restore it. Permission changes require a resolvable foreground user and fail without mutating if adb cannot report one. Android settings airplane on|off is applied by the connectivity service (Android 11+) and reports the airplaneMode that service holds; older builds fail without changing device state.', }, metadata: settingsCommandMetadata, definition: settingsCommandDefinition, diff --git a/website/docs/docs/commands.md b/website/docs/docs/commands.md index 293e1cae2..43f7e7293 100644 --- a/website/docs/docs/commands.md +++ b/website/docs/docs/commands.md @@ -703,6 +703,7 @@ agent-device settings permission reset screen-recording --platform macos - `settings location set ` sets precise coordinates on iOS simulators and Android emulators. - `settings clear-app-state [app-id]` clears the active session app data, or the provided app id. Android uses `pm clear`, which removes SharedPreferences, databases, files, and cache. iOS simulator removes the app data container contents. iOS physical devices and macOS are unsupported. - Face ID and Touch ID controls are iOS simulator-only. +- Android `settings airplane on|off` is applied by the connectivity service (`cmd connectivity airplane-mode`, Android 11+), which drives the radios rather than only writing the `airplane_mode_on` setting. The response reports the `airplaneMode` that service holds after the change, and Android builds without that command fail without changing device state. Connectivity takes a moment to settle after the switch, so poll the app under test rather than asserting offline behavior immediately. - Fingerprint simulation is supported on Android targets where `cmd fingerprint` or `adb emu finger` is available. On physical Android devices, only `cmd fingerprint` is attempted. - Permission actions are scoped to the active session app. From 6985d5dbf7a024499583739081f003766ac176c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 2 Sep 2026 15:17:58 +0200 Subject: [PATCH 2/3] test(android): pin the mechanics eager closure at 178 modules 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. --- scripts/__tests__/eager-closure-budgets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/__tests__/eager-closure-budgets.ts b/scripts/__tests__/eager-closure-budgets.ts index 3460cfe41..5c66a02cf 100644 --- a/scripts/__tests__/eager-closure-budgets.ts +++ b/scripts/__tests__/eager-closure-budgets.ts @@ -310,7 +310,7 @@ export const FACADE_BUDGETS: Readonly> = Object.freeze({ 'packages/platform-android/src/adb-host.ts': 1, // The named mechanics facet is intentionally implementation-eager once selected. Its exact // closure is pinned so a future facade expansion is visible in review. - 'packages/platform-android/src/mechanics.ts': 177, + 'packages/platform-android/src/mechanics.ts': 178, // --- @agent-device/platform-apple --- 'packages/platform-apple/src/index.ts': 1, From e9a241721864dd7737f281c42e59cb9bc5e6935b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Wed, 2 Sep 2026 15:17:58 +0200 Subject: [PATCH 3/3] fix(android): report only capability absence as unsupported airplane mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/__tests__/settings-airplane.test.ts | 26 +++++++++++-- packages/platform-android/src/adb.ts | 12 +++--- .../src/device-input-state.ts | 4 +- packages/platform-android/src/mechanics.ts | 2 +- .../platform-android/src/settings-airplane.ts | 38 +++++++++---------- src/platform-runtime-android-tool-host.ts | 4 +- 6 files changed, 51 insertions(+), 35 deletions(-) diff --git a/packages/platform-android/src/__tests__/settings-airplane.test.ts b/packages/platform-android/src/__tests__/settings-airplane.test.ts index 5772dc05c..a0f9a3081 100644 --- a/packages/platform-android/src/__tests__/settings-airplane.test.ts +++ b/packages/platform-android/src/__tests__/settings-airplane.test.ts @@ -72,7 +72,7 @@ test('setAndroidSetting airplane refuses builds without the connectivity command async ({ calls, device }) => { await assertRejectsAppError(() => setAndroidSetting(device, 'airplane', 'on'), { code: 'UNSUPPORTED_OPERATION', - message: /does not report airplane mode through the connectivity service/, + message: /no airplane-mode command/, hint: /Android 11 \(API 30\)/, }); assert.deepEqual( @@ -88,8 +88,28 @@ test('setAndroidSetting airplane refuses unreadable state before writing', async () => 'Airplane mode: who knows', async ({ calls, device }) => { await assertRejectsAppError(() => setAndroidSetting(device, 'airplane', 'off'), { - code: 'UNSUPPORTED_OPERATION', - message: /does not report airplane mode through the connectivity service/, + code: 'COMMAND_FAILED', + message: /Failed to read Android airplane mode/, + }); + assert.deepEqual( + calls.map((args) => args.join(' ')), + [READ], + ); + }, + ); +}); + +test('setAndroidSetting airplane keeps a refused read a command failure, not an unsupported build', async () => { + await withFakeAdb( + () => ({ + stdout: '', + stderr: 'java.lang.SecurityException: Permission Denial: not allowed to change airplane mode', + exitCode: 255, + }), + async ({ calls, device }) => { + await assertRejectsAppError(() => setAndroidSetting(device, 'airplane', 'on'), { + code: 'COMMAND_FAILED', + message: /Failed to read Android airplane mode/, }); assert.deepEqual( calls.map((args) => args.join(' ')), diff --git a/packages/platform-android/src/adb.ts b/packages/platform-android/src/adb.ts index 6d00630bb..6449fe4a4 100644 --- a/packages/platform-android/src/adb.ts +++ b/packages/platform-android/src/adb.ts @@ -16,18 +16,18 @@ export async function runAndroidAdb( } /** - * Whether an adb `cmd clipboard` invocation was refused because this build ships no shell - * implementation for the clipboard service, rather than because the call itself failed. + * Whether an adb `cmd ` invocation was refused because this build ships no + * shell implementation for it, rather than because the call itself failed. * * adb reports this condition in its output and nowhere else — no exit code or structured field * separates "service has no shell command" from any other non-zero result — so this is the one * place that reads that prose, and it hands every caller a typed answer instead. * - * Only ever ask this about a call that *failed*. A successful `clipboard get text` returns the - * clipboard's contents on stdout, which is arbitrary user text and may quote these very phrases; - * callers must settle a zero exit as success before reaching for this. + * Only ever ask this about a call that *failed*. A command that succeeds prints its own payload + * on stdout — `clipboard get text` returns arbitrary user text, which may quote these very + * phrases — so callers must settle a zero exit before reaching for this. */ -export function isClipboardShellUnsupported(stdout: string, stderr: string): boolean { +export function isAndroidShellCommandUnsupported(stdout: string, stderr: string): boolean { const haystack = `${stdout}\n${stderr}`.toLowerCase(); return ( haystack.includes('no shell command implementation') || haystack.includes('unknown command') diff --git a/packages/platform-android/src/device-input-state.ts b/packages/platform-android/src/device-input-state.ts index b8afbe2ca..546a2285f 100644 --- a/packages/platform-android/src/device-input-state.ts +++ b/packages/platform-android/src/device-input-state.ts @@ -3,7 +3,7 @@ import { emitDiagnostic } from '@agent-device/host-kit/diagnostics'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; -import { isClipboardShellUnsupported, sleep } from './adb.ts'; +import { isAndroidShellCommandUnsupported, sleep } from './adb.ts'; import { androidAdbResultError, resolveAndroidAdbExecutor, @@ -325,7 +325,7 @@ async function runAndroidClipboardShellCommand( // the clipboard's contents, and a user who has copied one of the missing-shell phrases must not // have their own text mistaken for adb refusing the command. if (result.exitCode === 0) return result.stdout; - if (isClipboardShellUnsupported(result.stdout, result.stderr)) { + if (isAndroidShellCommandUnsupported(result.stdout, result.stderr)) { throw new AppError( 'UNSUPPORTED_OPERATION', `Android shell clipboard ${operation} is not supported on this device.`, diff --git a/packages/platform-android/src/mechanics.ts b/packages/platform-android/src/mechanics.ts index 71a4aebd1..7c68ee358 100644 --- a/packages/platform-android/src/mechanics.ts +++ b/packages/platform-android/src/mechanics.ts @@ -35,7 +35,7 @@ export { androidAdbForwardsDeviceExitStatus, resetAndroidAdbShellProtocolProbes, } from './adb-shell-protocol.ts'; -export { isClipboardShellUnsupported, runAndroidAdb, sleep } from './adb.ts'; +export { isAndroidShellCommandUnsupported, runAndroidAdb, sleep } from './adb.ts'; export { handleAndroidAlert, type AndroidAlertResult } from './alert.ts'; export { classifyAndroidAlertIdentifier, diff --git a/packages/platform-android/src/settings-airplane.ts b/packages/platform-android/src/settings-airplane.ts index 4f7b9b08f..7fa5fd4fa 100644 --- a/packages/platform-android/src/settings-airplane.ts +++ b/packages/platform-android/src/settings-airplane.ts @@ -1,11 +1,7 @@ import { AppError } from '@agent-device/kernel/errors'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import { runAndroidAdb } from './adb.ts'; -import { - androidAdbResultError, - classifyAdbFailure, - type AndroidAdbExecutorResult, -} from './adb-executor.ts'; +import { isAndroidShellCommandUnsupported, runAndroidAdb } from './adb.ts'; +import { androidAdbResultError, type AndroidAdbExecutorResult } from './adb-executor.ts'; export type AndroidAirplaneMode = 'enabled' | 'disabled'; @@ -42,22 +38,22 @@ export async function setAndroidAirplaneMode( async function requireAndroidAirplaneModeSupport(device: DeviceInfo): Promise { const { state, result } = await probeAndroidAirplaneMode(device); if (state) return; - if (result.exitCode !== 0 && classifyAdbFailure(result.stderr, result.stdout)) { - throw androidAdbResultError('Failed to read Android airplane mode', result, { - deviceId: device.id, - }); + if (result.exitCode !== 0 && isAndroidShellCommandUnsupported(result.stdout, result.stderr)) { + throw new AppError( + 'UNSUPPORTED_OPERATION', + 'The connectivity service on this Android build has no airplane-mode command, so nothing was changed.', + { + deviceId: device.id, + hint: 'settings airplane needs cmd connectivity airplane-mode, which requires Android 11 (API 30) or newer. Use a newer device or emulator image.', + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + }, + ); } - throw new AppError( - 'UNSUPPORTED_OPERATION', - 'This Android build does not report airplane mode through the connectivity service, so nothing was changed.', - { - deviceId: device.id, - hint: 'settings airplane needs cmd connectivity airplane-mode, which requires Android 11 (API 30) or newer. Use a newer device or emulator image.', - stdout: result.stdout, - stderr: result.stderr, - exitCode: result.exitCode, - }, - ); + throw androidAdbResultError('Failed to read Android airplane mode', result, { + deviceId: device.id, + }); } async function readAndroidAirplaneMode(device: DeviceInfo): Promise { diff --git a/src/platform-runtime-android-tool-host.ts b/src/platform-runtime-android-tool-host.ts index 2cde1148b..9041bc4f1 100644 --- a/src/platform-runtime-android-tool-host.ts +++ b/src/platform-runtime-android-tool-host.ts @@ -18,13 +18,13 @@ export function createAndroidToolHost(): AndroidToolHost { */ probeClipboardShellSupport: async (device, signal) => { try { - const { runAndroidAdb, isClipboardShellUnsupported } = await loadAndroidMechanics(); + const { runAndroidAdb, isAndroidShellCommandUnsupported } = await loadAndroidMechanics(); const result = await runAndroidAdb(device, ['shell', 'cmd', 'clipboard', 'get', 'text'], { allowFailure: true, signal, }); if (result.exitCode === 0) return 'supported'; - return isClipboardShellUnsupported(result.stdout, result.stderr) + return isAndroidShellCommandUnsupported(result.stdout, result.stderr) ? 'unsupported' : 'probe-failed'; } catch {