Skip to content

feat(hardware-button): add Android mute, sleep, wake and media keys - #3866

Merged
thymikee merged 4 commits into
mainfrom
feat/hardware-button-phone-keys
Oct 21, 2026
Merged

thymikee merged 4 commits into
mainfrom
feat/hardware-button-phone-keys

Conversation

@thymikee

@thymikee thymikee commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

Extend hardware-button with eight Android keys: mute, sleep, wake, media-play-pause, media-next, media-previous, media-rewind, and media-fast-forward. ADB/helper, Limrun Android, and Appium use the existing button family. Sleep/wake are idempotent; power toggles.

Android phone and TV presses now share the contracts keycode table. Deleted both consumer tables and numeric capability notes; simplified capability descriptions and derived the Android cell list. Scope: 33 files; follow-up to #3859 and #3848. Existing iOS volume routes remain. WebDriver already rejects TV targets; added regression coverage and corrected the earlier admission claim.

agent-device hardware-button mute
agent-device hardware-button sleep
agent-device hardware-button media-next

Validation

Head: f3263517019688f5bdc580361b9836a680832ad3.

  • pnpm check:affected --run --head <sha> passed on reviewed base 1a1adcd953 and this head, both exactCommit:true: receipts, review dispositions, live evidence.
  • Final gate: 12 checks passed; 10,011 related tests passed. 929 focused tests passed. I cold-reviewed this fixup myself; no independent review of this fixup.
  • Live Android 15: all eleven buttons dispatched; mute, volume restoration, repeated sleep/wake and power states verified. Session/daemon closed; emulator shut down.
  • No live Appium or physical iPhone verification. Media playback effects without an active player are unverified. GitHub owns full coverage and device lanes; local skips are not passes.

View guided diff Turn on auto-fix

Follow-up to #3859: hardware-button now names eight more phone keys —
mute, sleep, wake, media-play-pause, media-next, media-previous,
media-rewind, media-fast-forward — each a single dispatch-only press on
Android phones and tablets across the local adb/helper leg, the Limrun
Android leg, and Appium's mobile: pressKey route. sleep and wake are the
explicit Android events; power stays the toggle. Every other owner keeps
the family denial: Apple volume is unchanged and refuses the rest at
typed fact admission, TV targets still use tv-remote.

The mute/sleep/wake/media cells join SYSTEM_BUTTONS, so admission, the
catalog binder, the daemon row table, and the CLI/SDK/MCP/journal
vocabulary extend as pure table rows; the two Android owners state the
phone/TV rule once through the shared facts builder.

Follow-up to #3848 and #3859.
- The daemon conformance table derives one refusal row per
  hardware-button name from the command vocabulary instead of three
  hand-written rows, so every extended key proves its own refusal
  before binding.
- The WebDriver leg sends every hardware key through one keycode table
  and one pressAndroidKeycode helper; the eight extended legs share one
  keyed press.
- The daemon route test is one test.each over all eleven names on an
  Android fixture, pinned against the vocabulary; the contracts family
  equality is restored to toEqual over every cell.
- Fact-builder and label types and comments tightened; dangling
  follow-up references and duplicate magic counts removed.
- Key every derived hardware-button conformance row as
  `hardware-button <name>`; the bare route key carried no extra rule.
- Fix the inverted sentinel texts in the iOS refusal tests and the
  Android-only comment on the extended-key cell type.
- Drop the remaining dangling follow-up references.
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-21 16:41 UTC

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.50 MB 5.50 MB +1.8 kB
Package (unpacked) 5.50 MB 5.50 MB +1.8 kB
Package (download) 1.67 MB 1.67 MB +416 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 38.4 ms 38.9 ms +0.5 ms
CLI --help 77.8 ms 79.9 ms +2.1 ms

@thymikee

thymikee commented Oct 21, 2026 •

Copy link
Copy Markdown
Member Author

Final validation and review evidence for 1a1adcd9536f81f45e06d2675ae9609eb118e586.

pnpm check:affected --run --head 1a1adcd953 passed. Receipt: .tmp/validation/run-lfXnMD/receipt.json.

{
  "status": "passed",
  "exitCode": 0,
  "exactCommit": true,
  "checkoutChanged": false,
  "startedAt": "2026-10-21T14:45:54.061Z",
  "finishedAt": "2026-10-21T14:57:57.820Z",
  "head": "1a1adcd9536f81f45e06d2675ae9609eb118e586",
  "mergeBase": "7922864e7beabd57ef563081ab6d78a69e95172b",
  "checks": {
    "passed": 12,
    "failed": 0,
    "skipped": 8,
    "not-run": 0
  }
}

Independent review: two fresh rounds. The first round's conformance, WebDriver dispatch, and test findings were fixed; the second found no blockers, majors, or minors. Its nits were applied, then final-head validation passed. The code uses the existing system-button family and small keycode tables; Android and Limrun share their fact-row construction, and Appium key presses share one helper. No parallel dispatcher or generic framework was added.

Live checks were performed on Android emulator emulator-5554, using the pinned released launcher with one-step replay files against a rebuilt isolated source HTTP daemon (0.21.30-dev). Snapshot reported the matching Android helper version. The local Android execution and Limrun routing code are unchanged between live-tested c7f9ad9e84 and this final head.

  • mute: logcat recorded KEYCODE_VOLUME_MUTE.
  • sleep / wake: read-only dumpsys power observations confirmed the explicit, idempotent sleep/wake transitions.
  • media-fast-forward: replay dispatch succeeded and session events preserved its button name. Mapping tests cover all five media keys. No active player was configured, so successful playback behavior is not claimed.
  • The owned session/daemon were closed, temporary state removed, and device state restored.

Appium was verified by exact mobile: pressKey transcript tests, not a live driver session. No physical iPhone was used. Correction after tracing the existing runtime owner: WebDriver already requires a mobile target before admitting hardware-button cells. The earlier claim that Android TV hardware buttons were admitted was incorrect and is withdrawn; WebDriver tv-remote remains unsupported.

CI coverage and device lanes remain separate obligations; locally skipped checks are not claimed as passes.

@thymikee
thymikee marked this pull request as ready for review October 21, 2026 14:59
@thymikee

Copy link
Copy Markdown
Member Author

The Android keycodes need one owner before this can merge. I reviewed 1a1adcd. All checks pass, and there are no conflicts.

The new ANDROID_HARDWARE_BUTTON_KEYCODES table in webdriver-interactor.ts is a verbatim copy of the 11-entry table in input-actions.ts, which this PR grows from 3 entries to 11. Both copy TV_REMOTE_BUTTON_DEFINITIONS in tv-remote.ts, which already maps the same names to androidKeyevent and exports toAndroidTvRemoteKeyevent. The keycodes are typed a fourth time in the capability notes in capabilities.ts. If someone corrects a keycode in one table, hardware-button can press one key on adb, another on Appium, and tv-remote a third, and no test compares them. The rule should be that every Android hardware-button press takes its keycode from the one contracts table that owns the name-to-keyevent mapping. Please derive it in both packages with toAndroidTvRemoteKeyevent(HARDWARE_BUTTON_NAME_LABELS[cell]) (platform-android already imports that function, and provider-webdriver already depends on contracts). Then delete both local tables and drop the keycodes from the capability notes, or build the notes from the same table.

Could the change be smaller still? pressAndroidHardwareButton(device, cell) would use the contracts keycode, and WebDriver's pressKey would take Number of the same value. The eight WebDriver capability rows for the extended keys could come from a loop over the cell list instead of 16 handwritten entries, and the eight interactor methods could sit in one bound map like the lambdas in src/core/interactors/android.ts. A single parameterized pressHardwareKey(button) operation with per-button facts, like tvRemote(button), would go further. That would need an ADR 0019 §9 amendment, so it does not block this PR. The keycode fix needs nothing first.

Not blocking, and you can take or leave these: the help text, cliDetail and commands.md say Android TV and tvOS targets refuse these keys, but the WebDriver owner admits all eleven on an Appium Android TV session, so scope that sentence to the ADB-backed and Limrun backends or gate the WebDriver cells on device.target; the comments at input-actions.ts lines 261-264 and in the Limrun interaction-operations.ts still say "three" hardware-button presses; and ANDROID_HARDWARE_BUTTON_CELLS in system-button-runtime.ts is one more copy of the button list that may be derivable from SYSTEM_BUTTONS once the keycode fix lands.

I did not reproduce your live Android run (mute logcat, sleep and wake through dumpsys power, media-fast-forward replay). I only checked that the Android and Limrun execution code did not change since c7f9ad9. There was no live Appium session, so the WebDriver presses are proven only by recording-client transcripts. I also did not check that keycodes 223 and 224 work on every supported API level, or run the registry and conformance gates.

@thymikee

Copy link
Copy Markdown
Member Author

Addressed in f326351701.

  • One keycode owner: deleted both ANDROID_HARDWARE_BUTTON_KEYCODES tables. ADB/helper and Appium now resolve HARDWARE_BUTTON_NAME_LABELS[button] through toAndroidTvRemoteKeyevent. Appium converts that value to the numeric mobile: pressKey payload. The contracts TV key table owns the numbers once.
  • Smaller capability declarations: removed numeric keycodes from capability notes and replaced repeated note objects with one Android hardware-button description and the existing unsupported description. Existing iOS volume routes are preserved. The explicit interactor methods remain small wrappers around one press helper; no new keyed runtime or ADR change.
  • Derived cell list: Android hardware-button cells now come from SYSTEM_BUTTONS with the three separately routed buttons excluded. The compile-time vocabulary coverage check remains.
  • Comments: removed the stale three-button mapping comment and corrected both Limrun references.
  • TV correction: I traced the existing WebDriver owner. webDriverInteractorDevice already requires device.target === 'mobile', so an Android TV target cannot reach these facts. A new test covers all eleven denied cells, confirms no bound hardware operations, and confirms no interactor is acquired. The earlier PR limitation claiming WebDriver admits TV hardware buttons was incorrect and is withdrawn.

I made this fixup myself and re-read its full diff cold. No independent reviewer was used for this fixup, per the request to handle it myself. The focused run passed 929 tests, including exact Android/Appium payload tests, iOS refusal tests, contract coverage, the TV regression, and eager-loading budgets. Typecheck, lint, layering and Fallow passed.

Live verification used a new isolated Android 15 emulator (pr3866_phone_4ddbcf74) with the rebuilt source daemon and helper 0.21.30-dev. The pinned launcher replayed all eleven button names successfully. Media volume returned to 5; mute changed false → true → false; repeated sleep produced Asleep → Asleep, repeated wake Awake → Awake, and power Asleep → Awake. The session closed, source daemon stopped gracefully, and the emulator shut down. No live Appium session or physical iPhone was available; media playback without an active player is not claimed.

Final-head pnpm check:affected --run --head f326351701 passed. The same gate passed on the reviewed base 1a1adcd953 (historical receipt above). Final local receipt: .tmp/validation/run-olIodJ/receipt.json. Related tests: 1,119 files, 10,011 tests passed. Unit and provider-integration selections were deduplicated by the runner; GitHub-owned coverage and device lanes were skipped locally and remain CI obligations.

{
  "status": "passed",
  "exitCode": 0,
  "exactCommit": true,
  "checkoutChanged": false,
  "startedAt": "2026-10-21T15:53:58.424Z",
  "finishedAt": "2026-10-21T15:59:24.624Z",
  "head": "f3263517019688f5bdc580361b9836a680832ad3",
  "mergeBase": "7922864e7beabd57ef563081ab6d78a69e95172b",
  "checks": {
    "passed": 12,
    "deduplicated": 2,
    "skipped": 8
  },
  "passedChecks": [
    "format",
    "lint",
    "typecheck",
    "layering",
    "fallow",
    "build",
    "integration-node",
    "macos-coverage",
    "vitest-related",
    "command-docs",
    "docs-links",
    "docs-prose"
  ],
  "deduplicatedChecks": [
    "unit",
    "provider-integration"
  ]
}

@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready. The keycode duplication from the earlier review at 1a1adcd is fixed at f326351, and nothing from that review remains open. All 19 checks pass at this commit, and I know of no conflicts. Nothing else blocks maintainer review.

Not blocking: ANDROID_HARDWARE_BUTTON_CELLS in https://github.com/callstack/agent-device/blob/f326351/packages/contracts/src/system-button-runtime.ts#L147 is a deny-list over SYSTEM_BUTTONS (it excludes home, appSwitcher and actionButton). The satisfies clause keeps it safe at runtime, but a new non-hardware button would only fail at compile time and would not join the list on its own. You could derive the list from the HARDWARE_BUTTON_NAME_LABELS keys if the import budget allows, or leave it as is.

For evidence, I took the live Android 15 emulator run from your comment and did not reproduce it. There was no live Appium session, so the WebDriver pressKey payloads are covered only by the recording-client transcripts in the unit tests. I did not run tsc locally for the SYSTEM_BUTTONS.filter predicate, and I rely on the green CI for that typecheck.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 21, 2026
@thymikee
thymikee merged commit 86637b2 into main Oct 21, 2026
19 checks passed
@thymikee
thymikee deleted the feat/hardware-button-phone-keys branch October 21, 2026 16:39
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.

1 participant