diff --git a/packages/command-registry/src/registry.ts b/packages/command-registry/src/registry.ts index f8a8e4eda5..343659e975 100644 --- a/packages/command-registry/src/registry.ts +++ b/packages/command-registry/src/registry.ts @@ -1646,10 +1646,10 @@ export const RAW_COMMAND_DESCRIPTORS = [ ...(ownerFilesEnabled ? { ownerFiles: ['src/commands/system/index.ts'] as const } : {}), catalog: { group: 'public', key: 'hardwareButton' }, frameworkTier: 'extended', - // The first positional selects the cell (volume-up, volume-down, power), the way `orientation` - // selects between setOrientation and unlockOrientation; admission binds exactly that one fact. - // Like action-button, no post-action observation is declared: the press goes to the system, - // not the session app, and settling would re-capture the app the press leaves alone (#3848). + // The first positional selects one of the eleven named keys, the way `orientation` selects + // between setOrientation and unlockOrientation; admission binds exactly that one fact. Like + // action-button, no post-action observation is declared: the press goes to the system, not + // the session app, and settling would re-capture the app the press leaves alone (#3848). ...GENERIC_MUTATING_COMMAND_TRAITS, platformExecution: { kind: 'device-runtime', uses: hardwareButtonRuntimePlanUses }, }, diff --git a/packages/contracts/src/device-rotation.ts b/packages/contracts/src/device-rotation.ts index 629e21e13c..ae7bdc93ca 100644 --- a/packages/contracts/src/device-rotation.ts +++ b/packages/contracts/src/device-rotation.ts @@ -100,16 +100,40 @@ export function parseOrientationRequest(input: string | undefined): OrientationR * Each name is a cell of {@link SYSTEM_BUTTONS}; system-button-runtime.ts checks that at * compile time. The cells stay module-internal: consumers select a button by its public name. */ -const HARDWARE_BUTTONS = ['volumeUp', 'volumeDown', 'power'] as const; +const HARDWARE_BUTTONS = [ + 'volumeUp', + 'volumeDown', + 'power', + 'mute', + 'sleep', + 'wake', + 'mediaPlayPause', + 'mediaNext', + 'mediaPrevious', + 'mediaRewind', + 'mediaFastForward', +] as const; export type HardwareButton = (typeof HARDWARE_BUTTONS)[number]; /** - * The public positional names of the hardware keys: `volume-up`, `volume-down`, `power`. - * The literals are the same ones SYSTEM_BUTTON_LABELS carries for these buttons; the label - * owner's type-level check and the contracts tests refuse drift between the two lists. + * The public positional names of the hardware keys: the volume/power/mute keys, the explicit + * sleep and wake events, and the five media keys. The literals are the same ones + * SYSTEM_BUTTON_LABELS carries for these buttons; the label owner's type-level check and the + * contracts tests refuse drift between the two lists. */ -export type HardwareButtonName = 'volume-up' | 'volume-down' | 'power'; +export type HardwareButtonName = + | 'volume-up' + | 'volume-down' + | 'power' + | 'mute' + | 'sleep' + | 'wake' + | 'media-play-pause' + | 'media-next' + | 'media-previous' + | 'media-rewind' + | 'media-fast-forward'; /** * The accepted positional values, in command order. `satisfies` turns an out-of-vocabulary @@ -119,6 +143,14 @@ export const HARDWARE_BUTTON_NAMES = [ 'volume-up', 'volume-down', 'power', + 'mute', + 'sleep', + 'wake', + 'media-play-pause', + 'media-next', + 'media-previous', + 'media-rewind', + 'media-fast-forward', ] as const satisfies readonly HardwareButtonName[]; /** The CLI synopsis tail naming the accepted positional names. */ @@ -134,6 +166,14 @@ export const HARDWARE_BUTTON_NAME_LABELS = { volumeUp: 'volume-up', volumeDown: 'volume-down', power: 'power', + mute: 'mute', + sleep: 'sleep', + wake: 'wake', + mediaPlayPause: 'media-play-pause', + mediaNext: 'media-next', + mediaPrevious: 'media-previous', + mediaRewind: 'media-rewind', + mediaFastForward: 'media-fast-forward', } as const satisfies Record; const HARDWARE_BUTTON_NAME_MAP: ReadonlyMap = new Map( diff --git a/packages/contracts/src/interactor-operation-catalog-conformance.test.ts b/packages/contracts/src/interactor-operation-catalog-conformance.test.ts index 14d38343af..ebb019c7ee 100644 --- a/packages/contracts/src/interactor-operation-catalog-conformance.test.ts +++ b/packages/contracts/src/interactor-operation-catalog-conformance.test.ts @@ -39,6 +39,14 @@ const EXPECTATIONS: { volumeUp: { method: 'volumeUp', input: {} }, volumeDown: { method: 'volumeDown', input: {} }, power: { method: 'power', input: {} }, + mute: { method: 'mute', input: {} }, + sleep: { method: 'sleep', input: {} }, + wake: { method: 'wake', input: {} }, + mediaPlayPause: { method: 'mediaPlayPause', input: {} }, + mediaNext: { method: 'mediaNext', input: {} }, + mediaPrevious: { method: 'mediaPrevious', input: {} }, + mediaRewind: { method: 'mediaRewind', input: {} }, + mediaFastForward: { method: 'mediaFastForward', input: {} }, triggerAppEvent: { method: 'open', input: { eventUrl: 'myapp://x' } }, setSetting: { method: 'setSetting', diff --git a/packages/contracts/src/interactor-operation-catalog.ts b/packages/contracts/src/interactor-operation-catalog.ts index f77a73f229..fb0adaaebb 100644 --- a/packages/contracts/src/interactor-operation-catalog.ts +++ b/packages/contracts/src/interactor-operation-catalog.ts @@ -114,6 +114,46 @@ export const INTERACTOR_OPERATIONS = [ label: SYSTEM_BUTTON_LABELS.power, bind: (signal, resolve) => bindSystemButton('power', signal, resolve), }, + { + operation: 'mute', + label: SYSTEM_BUTTON_LABELS.mute, + bind: (signal, resolve) => bindSystemButton('mute', signal, resolve), + }, + { + operation: 'sleep', + label: SYSTEM_BUTTON_LABELS.sleep, + bind: (signal, resolve) => bindSystemButton('sleep', signal, resolve), + }, + { + operation: 'wake', + label: SYSTEM_BUTTON_LABELS.wake, + bind: (signal, resolve) => bindSystemButton('wake', signal, resolve), + }, + { + operation: 'mediaPlayPause', + label: SYSTEM_BUTTON_LABELS.mediaPlayPause, + bind: (signal, resolve) => bindSystemButton('mediaPlayPause', signal, resolve), + }, + { + operation: 'mediaNext', + label: SYSTEM_BUTTON_LABELS.mediaNext, + bind: (signal, resolve) => bindSystemButton('mediaNext', signal, resolve), + }, + { + operation: 'mediaPrevious', + label: SYSTEM_BUTTON_LABELS.mediaPrevious, + bind: (signal, resolve) => bindSystemButton('mediaPrevious', signal, resolve), + }, + { + operation: 'mediaRewind', + label: SYSTEM_BUTTON_LABELS.mediaRewind, + bind: (signal, resolve) => bindSystemButton('mediaRewind', signal, resolve), + }, + { + operation: 'mediaFastForward', + label: SYSTEM_BUTTON_LABELS.mediaFastForward, + bind: (signal, resolve) => bindSystemButton('mediaFastForward', signal, resolve), + }, { operation: 'triggerAppEvent', label: 'trigger-app-event', bind: bindAppEvent }, { operation: 'setSetting', label: 'settings', bind: bindSetSetting }, { operation: 'readSetting', label: 'settings read', bind: bindReadSetting }, diff --git a/packages/contracts/src/interactor-types.ts b/packages/contracts/src/interactor-types.ts index 526ee07c2d..56f63587c4 100644 --- a/packages/contracts/src/interactor-types.ts +++ b/packages/contracts/src/interactor-types.ts @@ -451,6 +451,34 @@ export type Interactor = { * it; an owner without it leaves it undefined and its fact refuses before binding. */ power?(): Promise; + /** + * Optional (parity with `volumeUp`): presses the media mute key once. Android-only today: + * Apple exposes no public mute press, so no Apple leaf carries it. + */ + mute?(): Promise; + /** + * Optional (parity with `volumeUp`): sends the explicit Android sleep event (KEYCODE_SLEEP), + * which turns the screen off without toggling the way `power` does. + */ + sleep?(): Promise; + /** + * Optional (parity with `volumeUp`): sends the explicit Android wake event (KEYCODE_WAKEUP), + * which turns the screen on without toggling the way `power` does. + */ + wake?(): Promise; + /** + * Optional (parity with `volumeUp`): presses the media play/pause key once. The press goes to + * whatever media session the OS routes it to; it does not claim playback started. + */ + mediaPlayPause?(): Promise; + /** Optional (parity with `mediaPlayPause`): presses the media next key once. */ + mediaNext?(): Promise; + /** Optional (parity with `mediaPlayPause`): presses the media previous key once. */ + mediaPrevious?(): Promise; + /** Optional (parity with `mediaPlayPause`): presses the media rewind key once. */ + mediaRewind?(): Promise; + /** Optional (parity with `mediaPlayPause`): presses the media fast-forward key once. */ + mediaFastForward?(): Promise; /** Optional: only Android implements a live status read (see {@link KeyboardStatusResult}). */ keyboardStatus?(): Promise; /** Optional: platforms with no keyboard-dismiss concept leave it undefined. */ diff --git a/packages/contracts/src/navigation.ts b/packages/contracts/src/navigation.ts index 8c2e998c51..b8a0137142 100644 --- a/packages/contracts/src/navigation.ts +++ b/packages/contracts/src/navigation.ts @@ -93,9 +93,9 @@ export type TvRemoteCommandResult = { /** * `hardware-button` — `{ action: 'hardware-button', button, message: 'Pressed hardware button