Skip to content

feat(settings): add Android mobile-data on|off - #3636

Open
mikolajadamowicz wants to merge 4 commits into
callstack:mainfrom
mikolajadamowicz:feat/android-mobile-data
Open

mikolajadamowicz wants to merge 4 commits into
callstack:mainfrom
mikolajadamowicz:feat/android-mobile-data

Conversation

@mikolajadamowicz

@mikolajadamowicz mikolajadamowicz commented Oct 15, 2026 •

Copy link
Copy Markdown

Summary

Adds settings mobile-data on|off for Android, so a run on a streaming app can move from Wi-Fi to cellular and back and check the "streaming on mobile data" warnings.

agent-device settings wifi off
agent-device settings mobile-data on   # mobileData: enabled

Node: client.settings.update({ setting: 'mobile-data', state: 'off' })

  • Picks the stored row before changing anything: mobile_data<subId> with more than one active modem (persist.radio.multisim.config), otherwise mobile_data. Then runs svc data enable|disable and polls that row for up to 2 s.
  • No match fails with mobile-data-not-applied or mobile-data-unreadable, and says whether the switch was dispatched.
  • Every non-Android target refuses up front with UNSUPPORTED_OPERATION (setting-unsupported-on-leaf). --app is refused with setting_app_not_consumed.
  • 19 files, +752/−44, including help, docs and CHANGELOG.

Test plan

Use an Android emulator or phone with a SIM. S is its adb serial.

  1. pnpm build && pnpm build:android && pnpm clean:daemon
  2. pnpm ad settings mobile-data off --platform android --serial $S --json returns mobileData: disabled, and adb -s $S shell settings get global mobile_data prints 0. on returns enabled and 1.
  3. Run settings wifi off, then mobile-data on. adb -s $S shell ping -c 2 8.8.8.8 gets replies. After mobile-data off it loses every packet.
  4. mobile-data off --app com.example.app fails with INVALID_ARGS and setting_app_not_consumed. The row stays 1.
  5. Restore with settings wifi on and mobile-data on.

Validation

pnpm check:affected --run passed at 6b94f115. Steps 2 to 4 ran at 124d05f5 (same tree) on an Android 37 emulator with a SIM and an empty multisim config. All passed, and the emulator was restored.

Not covered: the multi-modem row choice. It follows AOSP GlobalSettingsHelper and needs a dual-SIM phone.

View guided diff

`settings mobile-data on|off` switches mobile data for the default data
SIM on adb-backed Android targets through `svc data enable|disable`. It
then reads the stored switch back until the value matches the request,
within a 2 s budget, and reports `mobileData`. On one SIM the switch is
read from the global `mobile_data` row. On a multi-SIM device it is read
from `mobile_data<subId>` for the default data subscription
(`multi_sim_data_call`). A switch that never settles fails with
COMMAND_FAILED instead of reporting a change that did not happen.
QA automation can use it to move a streaming session from Wi-Fi to
cellular and back, and to check how the app warns about streaming on
mobile data.

On iOS, tvOS, and visionOS, the daemon refuses the setting before
admission with UNSUPPORTED_OPERATION (reason
setting-unsupported-on-leaf), because agent-device has no mobile-data
control for Apple targets and simulators have no cellular radio.
…very non-Android target

Mobile-data read-back:
- Choose the stored row before `svc data` runs, from the active modem
  count (`persist.radio.multisim.config`), never from which rows exist.
  A stale `mobile_data<subId>` row on a single-modem device, or a seeded
  plain row on a multi-modem device, no longer decides the result.
- Each sample reads one row.
- Sample through absent or unreadable values, using a `Deadline` and the
  request's abort signal.
- Report `mobile-data-not-applied` or `mobile-data-unreadable` as typed
  reasons, with `settingKey` and `storedValue` in the details.
- A failure before `svc data` discloses `dispatched: no`; every failure
  after it discloses `unknown`.

Refusals:
- The daemon refuses mobile-data on every non-Android target before
  admission, through one shared leaf-refusal response, so a HarmonyOS or
  Linux session keeps its ref frame.
- The Apple and Limrun iOS owners throw the same typed refusal.
- `--app` is forwarded and refused with `setting_app_not_consumed`,
  because the switch is device-wide.
- The macOS unsupported list now includes mobile-data.
- Help and the commands and Limrun docs describe the refusal accurately
  for physical iPhones.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 19 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/platform-apple/src/core/app-settings.ts">

<violation number="1" location="packages/platform-apple/src/core/app-settings.ts:66">
P2: macOS exits through the earlier `isMacOs` branch, so this guard never runs there and `mobile-data` gets the generic `INVALID_ARGS` response instead of the shared unsupported-operation refusal. Move the mobile-data guard ahead of the macOS branch.</violation>
</file>

<file name="packages/daemon/src/execution/handlers/__tests__/snapshot-settings-handler.test.ts">

<violation number="1" location="packages/daemon/src/execution/handlers/__tests__/snapshot-settings-handler.test.ts:411">
P3: This list omits the supported `vega` and `web` platforms, so the test does not verify the pre-admission refusal for every non-Android target as its name claims. Add representative devices for both platforms.</violation>
</file>

<file name="packages/platform-apple/src/core/__tests__/app-settings.test.ts">

<violation number="1" location="packages/platform-apple/src/core/__tests__/app-settings.test.ts:193">
P2: This test asserts `dispatched: 'no'` but discards the fake tool's invocation log, so a command could run and be swallowed before the same refusal is returned. Capture `calls` and assert it is empty.

(Based on your team's feedback about typed interaction refusals.) .</violation>
</file>

<file name="packages/contracts/src/settings.ts">

<violation number="1" location="packages/contracts/src/settings.ts:227">
P2: This classifies every state as device-level, so `settings mobile-data cellular --app X` is rejected as `setting_app_not_consumed` before Android can return its invalid-state error. Gate this on `readSettingState(state)` as `locationAppScope` does.</violation>
</file>

<file name="website/docs/docs/commands.md">

<violation number="1" location="website/docs/docs/commands.md:670">
P2: `mobile-data-unreadable` is based on the final sample, not whether the row was unreadable throughout polling; a row that previously read `0` can later become absent and produce this reason. Describe it as applying when the final read is not `0` or `1`.</violation>

<violation number="2" location="website/docs/docs/commands.md:672">
P3: Qualify the `setting_app_not_consumed` statement to adb-backed Android targets. Non-Android `mobile-data --app` requests hit the unsupported-target refusal before the app-scope check and return a different reason.</violation>
</file>

<file name="packages/platform-android/src/settings.ts">

<violation number="1" location="packages/platform-android/src/settings.ts:328">
P1: The abort signal is checked only after `svc data` runs, so cancellation can still switch mobile data. Check before dispatch and pass the signal through every ADB leg, including `svc data`, row resolution, and reads.</violation>

<violation number="2" location="packages/platform-android/src/settings.ts:367">
P2: The settle deadline is checked only after this ADB read, which has no timeout; the normal executor can wait 15 seconds retrying an offline device. Bound each read with the remaining deadline and pass cancellation through.</violation>
</file>

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

View guided diff | Re-trigger cubic

resolveAndroidMobileDataSettingKey(device),
);
return await withDispatchDisclosure('unknown', async () => {
await runAndroidShell(device, ['svc', 'data', enabled ? 'enable' : 'disable']);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The abort signal is checked only after svc data runs, so cancellation can still switch mobile data. Check before dispatch and pass the signal through every ADB leg, including svc data, row resolution, and reads.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/platform-android/src/settings.ts, line 328:

<comment>The abort signal is checked only after `svc data` runs, so cancellation can still switch mobile data. Check before dispatch and pass the signal through every ADB leg, including `svc data`, row resolution, and reads.</comment>

<file context>
@@ -285,6 +294,130 @@ function nearestTextSizeCategory(scale: number): (typeof TEXT_SIZE_CATEGORIES)[n
+    resolveAndroidMobileDataSettingKey(device),
+  );
+  return await withDispatchDisclosure('unknown', async () => {
+    await runAndroidShell(device, ['svc', 'data', enabled ? 'enable' : 'disable']);
+    return { mobileData: await awaitAndroidMobileData(device, settingKey, requested, signal) };
+  });
</file context>

throw new AppError('INVALID_ARGS', getUnsupportedMacOsSettingMessage(setting));
}
const normalized = setting.toLowerCase();
if (normalized === 'mobile-data') throw mobileDataUnsupportedError(device.id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: macOS exits through the earlier isMacOs branch, so this guard never runs there and mobile-data gets the generic INVALID_ARGS response instead of the shared unsupported-operation refusal. Move the mobile-data guard ahead of the macOS branch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/platform-apple/src/core/app-settings.ts, line 66:

<comment>macOS exits through the earlier `isMacOs` branch, so this guard never runs there and `mobile-data` gets the generic `INVALID_ARGS` response instead of the shared unsupported-operation refusal. Move the mobile-data guard ahead of the macOS branch.</comment>

<file context>
@@ -61,9 +62,10 @@ export async function setIosSetting(
     throw new AppError('INVALID_ARGS', getUnsupportedMacOsSettingMessage(setting));
   }
+  const normalized = setting.toLowerCase();
+  if (normalized === 'mobile-data') throw mobileDataUnsupportedError(device.id);
   requireSimulatorDevice(device, 'settings');
   await ensureBootedSimulator(device);
</file context>

Comment on lines +193 to +201
await withFakeAppleTool(unexpectedArgs, async () => {
for (const device of [IOS_TEST_SIMULATOR, TVOS_TEST_SIMULATOR]) {
await assertRejectsAppError(() => setIosSetting(device, 'mobile-data', 'off'), {
code: 'UNSUPPORTED_OPERATION',
reason: 'setting-unsupported-on-leaf',
dispatched: 'no',
});
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This test asserts dispatched: 'no' but discards the fake tool's invocation log, so a command could run and be swallowed before the same refusal is returned. Capture calls and assert it is empty.

(Based on your team's feedback about typed interaction refusals.) .

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/platform-apple/src/core/__tests__/app-settings.test.ts, line 193:

<comment>This test asserts `dispatched: 'no'` but discards the fake tool's invocation log, so a command could run and be swallowed before the same refusal is returned. Capture `calls` and assert it is empty.

(Based on your team's feedback about typed interaction refusals.) .</comment>

<file context>
@@ -189,6 +189,18 @@ test('setIosSetting biometric refuses an Apple TV simulator before posting anyth
 });
 
+test('setIosSetting mobile-data refuses every Apple leaf before spawning anything', async () => {
+  await withFakeAppleTool(unexpectedArgs, async () => {
+    for (const device of [IOS_TEST_SIMULATOR, TVOS_TEST_SIMULATOR]) {
+      await assertRejectsAppError(() => setIosSetting(device, 'mobile-data', 'off'), {
</file context>
Suggested change
await withFakeAppleTool(unexpectedArgs, async () => {
for (const device of [IOS_TEST_SIMULATOR, TVOS_TEST_SIMULATOR]) {
await assertRejectsAppError(() => setIosSetting(device, 'mobile-data', 'off'), {
code: 'UNSUPPORTED_OPERATION',
reason: 'setting-unsupported-on-leaf',
dispatched: 'no',
});
}
});
await withFakeAppleTool(unexpectedArgs, async ({ calls }) => {
for (const device of [IOS_TEST_SIMULATOR, TVOS_TEST_SIMULATOR]) {
await assertRejectsAppError(() => setIosSetting(device, 'mobile-data', 'off'), {
code: 'UNSUPPORTED_OPERATION',
reason: 'setting-unsupported-on-leaf',
dispatched: 'no',
});
}
assert.deepEqual(calls, []);
});

return family === 'macos-host' ? 'device-level' : 'app-scoped';
}
if (normalizedSetting === 'location') return locationAppScope(family, state);
if (normalizedSetting === 'mobile-data') return family === 'mobile' ? 'device-level' : 'unknown';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This classifies every state as device-level, so settings mobile-data cellular --app X is rejected as setting_app_not_consumed before Android can return its invalid-state error. Gate this on readSettingState(state) as locationAppScope does.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/contracts/src/settings.ts, line 227:

<comment>This classifies every state as device-level, so `settings mobile-data cellular --app X` is rejected as `setting_app_not_consumed` before Android can return its invalid-state error. Gate this on `readSettingState(state)` as `locationAppScope` does.</comment>

<file context>
@@ -223,6 +224,7 @@ export function settingsAppScope(
     return family === 'macos-host' ? 'device-level' : 'app-scoped';
   }
   if (normalizedSetting === 'location') return locationAppScope(family, state);
+  if (normalizedSetting === 'mobile-data') return family === 'mobile' ? 'device-level' : 'unknown';
   return 'unknown';
 }
</file context>
Suggested change
if (normalizedSetting === 'mobile-data') return family === 'mobile' ? 'device-level' : 'unknown';
if (normalizedSetting === 'mobile-data') {
return family === 'mobile' && readSettingState(state) !== undefined ? 'device-level' : 'unknown';
}


- `settings animations off|on` (Android) sets the window, transition, and animator scales. Turn animations off to stabilize runs with heavy animation, and back on afterward.
- `settings airplane on|off` (Android) needs the connectivity service's `cmd connectivity airplane-mode` command, available from Android 11. It reports the resulting `airplaneMode`; a build without that command fails with `UNSUPPORTED_OPERATION` without changing anything. Connectivity takes a moment to settle, so poll the app rather than asserting offline behavior immediately.
- `settings mobile-data on|off` (Android over adb) switches mobile data for the default data SIM with `svc data enable|disable` and reports the resulting `mobileData` as the device stores it: in the global `mobile_data` row, or in `mobile_data<subId>` for the default data subscription when more than one modem is active. If the stored value does not settle to the request within 2 seconds, it fails with `COMMAND_FAILED` and `error.details.reason` `mobile-data-not-applied`, or `mobile-data-unreadable` when the row never reads as on or off. Pair it with `settings wifi off` to move a session such as a video stream from Wi-Fi to cellular, and back with `settings wifi on`; poll the app while the network hands over.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: mobile-data-unreadable is based on the final sample, not whether the row was unreadable throughout polling; a row that previously read 0 can later become absent and produce this reason. Describe it as applying when the final read is not 0 or 1.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At website/docs/docs/commands.md, line 670:

<comment>`mobile-data-unreadable` is based on the final sample, not whether the row was unreadable throughout polling; a row that previously read `0` can later become absent and produce this reason. Describe it as applying when the final read is not `0` or `1`.</comment>

<file context>
@@ -665,6 +667,9 @@ Device settings:
 
 - `settings animations off|on` (Android) sets the window, transition, and animator scales. Turn animations off to stabilize runs with heavy animation, and back on afterward.
 - `settings airplane on|off` (Android) needs the connectivity service's `cmd connectivity airplane-mode` command, available from Android 11. It reports the resulting `airplaneMode`; a build without that command fails with `UNSUPPORTED_OPERATION` without changing anything. Connectivity takes a moment to settle, so poll the app rather than asserting offline behavior immediately.
+- `settings mobile-data on|off` (Android over adb) switches mobile data for the default data SIM with `svc data enable|disable` and reports the resulting `mobileData` as the device stores it: in the global `mobile_data` row, or in `mobile_data<subId>` for the default data subscription when more than one modem is active. If the stored value does not settle to the request within 2 seconds, it fails with `COMMAND_FAILED` and `error.details.reason` `mobile-data-not-applied`, or `mobile-data-unreadable` when the row never reads as on or off. Pair it with `settings wifi off` to move a session such as a video stream from Wi-Fi to cellular, and back with `settings wifi on`; poll the app while the network hands over.
+- An emulator or device without an active SIM accepts `svc data` but can leave the stored value unchanged, so there the command fails with `COMMAND_FAILED` unless the value already matches. Such a target has no cellular network either: with Wi-Fi off it is offline rather than on cellular.
+- Mobile data is device-wide, so `--app` is refused with `setting_app_not_consumed`. Every other target refuses `mobile-data` before anything runs, because agent-device switches mobile data only through adb's `svc data`: physical iPhones, iOS, tvOS, and visionOS simulators, Limrun iOS, HarmonyOS, Vega, Linux, and web answer `UNSUPPORTED_OPERATION` with `error.details.reason: setting-unsupported-on-leaf`, and macOS answers `INVALID_ARGS` like its other unsupported settings.
</file context>
Suggested change
- `settings mobile-data on|off` (Android over adb) switches mobile data for the default data SIM with `svc data enable|disable` and reports the resulting `mobileData` as the device stores it: in the global `mobile_data` row, or in `mobile_data<subId>` for the default data subscription when more than one modem is active. If the stored value does not settle to the request within 2 seconds, it fails with `COMMAND_FAILED` and `error.details.reason` `mobile-data-not-applied`, or `mobile-data-unreadable` when the row never reads as on or off. Pair it with `settings wifi off` to move a session such as a video stream from Wi-Fi to cellular, and back with `settings wifi on`; poll the app while the network hands over.
- `settings mobile-data on|off` (Android over adb) switches mobile data for the default data SIM with `svc data enable|disable` and reports the resulting `mobileData` as the device stores it: in the global `mobile_data` row, or in `mobile_data<subId>` for the default data subscription when more than one modem is active. If the stored value does not settle to the request within 2 seconds, it fails with `COMMAND_FAILED`: `error.details.reason` is `mobile-data-unreadable` if the final row read is not `0` or `1`, or `mobile-data-not-applied` if that read is the opposite state. Pair it with `settings wifi off` to move a session such as a video stream from Wi-Fi to cellular, and back with `settings wifi on`; poll the app while the network hands over.

const deadline = Deadline.fromTimeoutMs(MOBILE_DATA_SETTLE_MS);
for (;;) {
signal?.throwIfAborted();
const storedValue = await readAndroidGlobalSetting(device, settingKey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The settle deadline is checked only after this ADB read, which has no timeout; the normal executor can wait 15 seconds retrying an offline device. Bound each read with the remaining deadline and pass cancellation through.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/platform-android/src/settings.ts, line 367:

<comment>The settle deadline is checked only after this ADB read, which has no timeout; the normal executor can wait 15 seconds retrying an offline device. Bound each read with the remaining deadline and pass cancellation through.</comment>

<file context>
@@ -285,6 +294,130 @@ function nearestTextSizeCategory(scale: number): (typeof TEXT_SIZE_CATEGORIES)[n
+  const deadline = Deadline.fromTimeoutMs(MOBILE_DATA_SETTLE_MS);
+  for (;;) {
+    signal?.throwIfAborted();
+    const storedValue = await readAndroidGlobalSetting(device, settingKey);
+    const mobileData = parseAndroidMobileData(storedValue);
+    if (mobileData === requested) return mobileData;
</file context>

kind: 'emulator',
booted: true,
},
{ platform: 'linux', id: 'linux-host', name: 'Linux Desktop', kind: 'device', booted: true },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This list omits the supported vega and web platforms, so the test does not verify the pre-admission refusal for every non-Android target as its name claims. Add representative devices for both platforms.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/daemon/src/execution/handlers/__tests__/snapshot-settings-handler.test.ts, line 411:

<comment>This list omits the supported `vega` and `web` platforms, so the test does not verify the pre-admission refusal for every non-Android target as its name claims. Add representative devices for both platforms.</comment>

<file context>
@@ -372,6 +374,122 @@ test('settings usage hint documents canonical faceid states', async () => {
+    kind: 'emulator',
+    booted: true,
+  },
+  { platform: 'linux', id: 'linux-host', name: 'Linux Desktop', kind: 'device', booted: true },
+];
+
</file context>
Suggested change
{ platform: 'linux', id: 'linux-host', name: 'Linux Desktop', kind: 'device', booted: true },
{ platform: 'linux', id: 'linux-host', name: 'Linux Desktop', kind: 'device', booted: true },
{ platform: 'vega', id: 'vega-tv', name: 'Vega TV', kind: 'device', target: 'tv' },
{ platform: 'web', id: 'web-browser', name: 'Browser', kind: 'device' },

- `settings airplane on|off` (Android) needs the connectivity service's `cmd connectivity airplane-mode` command, available from Android 11. It reports the resulting `airplaneMode`; a build without that command fails with `UNSUPPORTED_OPERATION` without changing anything. Connectivity takes a moment to settle, so poll the app rather than asserting offline behavior immediately.
- `settings mobile-data on|off` (Android over adb) switches mobile data for the default data SIM with `svc data enable|disable` and reports the resulting `mobileData` as the device stores it: in the global `mobile_data` row, or in `mobile_data<subId>` for the default data subscription when more than one modem is active. If the stored value does not settle to the request within 2 seconds, it fails with `COMMAND_FAILED` and `error.details.reason` `mobile-data-not-applied`, or `mobile-data-unreadable` when the row never reads as on or off. Pair it with `settings wifi off` to move a session such as a video stream from Wi-Fi to cellular, and back with `settings wifi on`; poll the app while the network hands over.
- An emulator or device without an active SIM accepts `svc data` but can leave the stored value unchanged, so there the command fails with `COMMAND_FAILED` unless the value already matches. Such a target has no cellular network either: with Wi-Fi off it is offline rather than on cellular.
- Mobile data is device-wide, so `--app` is refused with `setting_app_not_consumed`. Every other target refuses `mobile-data` before anything runs, because agent-device switches mobile data only through adb's `svc data`: physical iPhones, iOS, tvOS, and visionOS simulators, Limrun iOS, HarmonyOS, Vega, Linux, and web answer `UNSUPPORTED_OPERATION` with `error.details.reason: setting-unsupported-on-leaf`, and macOS answers `INVALID_ARGS` like its other unsupported settings.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Qualify the setting_app_not_consumed statement to adb-backed Android targets. Non-Android mobile-data --app requests hit the unsupported-target refusal before the app-scope check and return a different reason.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At website/docs/docs/commands.md, line 672:

<comment>Qualify the `setting_app_not_consumed` statement to adb-backed Android targets. Non-Android `mobile-data --app` requests hit the unsupported-target refusal before the app-scope check and return a different reason.</comment>

<file context>
@@ -665,6 +667,9 @@ Device settings:
 - `settings airplane on|off` (Android) needs the connectivity service's `cmd connectivity airplane-mode` command, available from Android 11. It reports the resulting `airplaneMode`; a build without that command fails with `UNSUPPORTED_OPERATION` without changing anything. Connectivity takes a moment to settle, so poll the app rather than asserting offline behavior immediately.
+- `settings mobile-data on|off` (Android over adb) switches mobile data for the default data SIM with `svc data enable|disable` and reports the resulting `mobileData` as the device stores it: in the global `mobile_data` row, or in `mobile_data<subId>` for the default data subscription when more than one modem is active. If the stored value does not settle to the request within 2 seconds, it fails with `COMMAND_FAILED` and `error.details.reason` `mobile-data-not-applied`, or `mobile-data-unreadable` when the row never reads as on or off. Pair it with `settings wifi off` to move a session such as a video stream from Wi-Fi to cellular, and back with `settings wifi on`; poll the app while the network hands over.
+- An emulator or device without an active SIM accepts `svc data` but can leave the stored value unchanged, so there the command fails with `COMMAND_FAILED` unless the value already matches. Such a target has no cellular network either: with Wi-Fi off it is offline rather than on cellular.
+- Mobile data is device-wide, so `--app` is refused with `setting_app_not_consumed`. Every other target refuses `mobile-data` before anything runs, because agent-device switches mobile data only through adb's `svc data`: physical iPhones, iOS, tvOS, and visionOS simulators, Limrun iOS, HarmonyOS, Vega, Linux, and web answer `UNSUPPORTED_OPERATION` with `error.details.reason: setting-unsupported-on-leaf`, and macOS answers `INVALID_ARGS` like its other unsupported settings.
 - `settings text-size` reads the current size; `settings text-size <category>` sets one: `extra-small`, `small`, `medium`, `large`, `extra-large`, `extra-extra-large`, `extra-extra-extra-large`, `accessibility-medium`, `accessibility-large`, `accessibility-extra-large`, `accessibility-extra-extra-large`, or `accessibility-extra-extra-extra-large`. It works on iPhone and iPad simulators and on Android; every other target refuses both read and write. The read reports `category` and the target's own `platformValue` (iOS may capitalize it, for example `extra-Small`).
 - Android has only a font-scale multiplier, mapped so `large` is `1.0` and `accessibility-extra-extra-extra-large` is `3.2`. A value between rungs reads as the nearest category, with the exact multiplier in `platformValue`; an unset scale reads as `large`.
</file context>
Suggested change
- Mobile data is device-wide, so `--app` is refused with `setting_app_not_consumed`. Every other target refuses `mobile-data` before anything runs, because agent-device switches mobile data only through adb's `svc data`: physical iPhones, iOS, tvOS, and visionOS simulators, Limrun iOS, HarmonyOS, Vega, Linux, and web answer `UNSUPPORTED_OPERATION` with `error.details.reason: setting-unsupported-on-leaf`, and macOS answers `INVALID_ARGS` like its other unsupported settings.
- On adb-backed Android, mobile data is device-wide, so `--app` is refused with `setting_app_not_consumed`. Every other target refuses `mobile-data` before anything runs, because agent-device switches mobile data only through adb's `svc data`: physical iPhones, iOS, tvOS, and visionOS simulators, Limrun iOS, HarmonyOS, Vega, Linux, and web answer `UNSUPPORTED_OPERATION` with `error.details.reason: setting-unsupported-on-leaf`, and macOS answers `INVALID_ARGS` like its other unsupported settings.

@thymikee

Copy link
Copy Markdown
Member

Thanks for this. The feature is useful, but the PR is large for one Android switch: +752 lines, and only about 60 of them switch mobile data. Here is what I think could be smaller, reviewed at 6b94f11.

  1. Multi-SIM row selection. resolveAndroidMobileDataSettingKey copies how the telephony service picks its storage row, and it adds a probe before dispatch, the "refuse before switching" path and 4 tests. Could the read-back use the live data state instead (for example dumpsys telephony.registry)? Or could it read only the plain mobile_data row and say in the docs that multi-SIM is not covered yet? Either way most of the key logic and the split dispatch disclosure would go away.

  2. Refusals outside the daemon. The daemon already refuses mobile-data on every non-Android target before admission. The extra throws in platform-apple/src/core/app-settings.ts and provider-limrun/src/ios-settings.ts, mobileDataUnsupportedError, and their tests only cover callers that skip the daemon. Is there a real path that needs them? If not, I'd drop them, and the AppleSettingLeafRefusal → SettingLeafRefusal rename would no longer be needed either.

  3. withDispatchDisclosure. This is a new local wrapper around discloseDispatch. Other sites call discloseDispatch(error, ...) inline. I'd do the same here, or add one shared helper in kernel errors, not a private copy.

  4. The poll loop. awaitAndroidMobileData is another hand-written Deadline + sleep + for (;;) loop. platform-android already has several (alert.ts, ime-state.ts, fill-verification.ts), and app-lifecycle.ts has probeBeforeDeadline. Can this reuse one of them?

  5. Tests. The 15 Android tests overlap. "Samples again when stored late" and "samples through an absent read" test the same loop, and on/off can be one table-driven test. I think about half would cover the same behavior.

  6. Comments and help. About 40% of the new production lines are comments, and several repeat the help text. The cliDetail addition is about 150 words. Two or three sentences would do: Android only, reports the stored mobileData state, fails with mobile-data-not-applied or mobile-data-unreadable.

None of this changes the behavior. It is about keeping the patch at the size of the feature.

@thymikee

Copy link
Copy Markdown
Member

Thanks for the PR. At 6b94f11 I found one problem that should be fixed before merge. The Android single-modem path is covered by the live run in the PR body, which I accepted from the description and did not re-run. Both Smoke Tests jobs were still pending when I checked, so I cannot say yet what CI shows. I have no conflict information.

The multi-SIM branch in resolveAndroidMobileDataSettingKey picks mobile_data<subId> whenever persist.radio.multisim.config is dsds, dsda or tsts, and no live run has covered it. If that row is wrong on a real dual-SIM phone, the switch is dispatched and then the read-back polls the wrong row. The command fails with mobile-data-not-applied even though data was switched. Most physical Android phones are dual-SIM, so this is the main real-device case. Please either drop the branch and read only the plain mobile_data row, with the docs saying multi-SIM read-back is not covered yet, or post a dual-SIM settings mobile-data off|on --json run whose mobileData matches adb shell settings get global mobile_data<subId>.

Could the feature be much smaller? A mobile-data case in setAndroidSetting could run svc data enable|disable and poll only the plain global mobile_data row. It could use the airplane-style read helper, or a bounded per-read timeoutMs plus the request signal on every adb call. The daemon's pre-admission refusal would then be the single refusal for non-Android targets. That would let you drop the multi-SIM key resolution, the Apple and Limrun owner refusals, mobileDataUnsupportedError, the SettingLeafRefusal rename and withDispatchDisclosure, and cut the cliDetail paragraph to two sentences. It looks like roughly 60-80 production lines. No ADR is needed, as long as the docs say multi-SIM read-back stays uncovered until a dual-SIM run shows which row to read. Is there a reason this does not work?

Not blocking, and you can take or leave it: the owner-side refusals in app-settings.ts and the Limrun iOS settings code repeat the refusal the daemon already makes at snapshot-settings.ts:347, so three sites now refuse one setting. Keeping the daemon refusal and deleting the copies would be simpler, unless a caller reaches setIosSetting without the daemon. I only found the daemon route.

On the open inline threads, the first four hold. The signal is not checked before the key probe or svc data (thread, P1). Bad-state validation runs after the app refusal (thread, P2). The settle error reads only the final sample (thread, P2). The row read has no timeoutMs (thread, P2). One lower-priority thread on missing vega and web test devices also holds (thread). Three threads do not apply and you can resolve them. The macOS INVALID_ARGS refusal is intended and documented (thread). The app-settings.test.ts case runs under a fake tool that throws on any spawn, so its result would not match the typed refusal, and it is moot if the owner refusal goes (thread). The commands.md bullet matches the code (thread).

Before merge, remove the unvalidated multi-SIM branch or post the dual-SIM run, then answer the size questions above.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants