diff --git a/README.md b/README.md index 4014c5a5b3..f7a2ced3f2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ -Let your coding agent verify its changes in the running app. `agent-device` lets agents inspect, control, debug, and verify apps on iOS, Android, and HarmonyOS (simulators, emulators, and physical devices), plus tvOS, Android TV, Amazon Vega OS TV (Vega Virtual Device), web, macOS, and Linux. Agents read token-efficient accessibility snapshots instead of reasoning over screenshots alone, act through refs and selectors, and save evidence for review. It also coordinates device access across parallel agent worktrees and connects to remote device clouds. +Let your coding agent verify its changes in the running app. `agent-device` lets agents inspect, control, debug, and verify apps on iOS, Android, and HarmonyOS (simulators, emulators, and physical devices), plus tvOS, Android TV, Fire TV (Fire OS), Amazon Vega OS TV (Vega Virtual Device), web, macOS, and Linux. Agents read token-efficient accessibility snapshots instead of reasoning over screenshots alone, act through refs and selectors, and save evidence for review. It also coordinates device access across parallel agent worktrees and connects to remote device clouds. Works with Claude Code, Codex, Cursor, Windsurf, Cline, Goose, and any agent that can run a CLI or connect over MCP, or as the runtime under agents you build with the [AI SDK](https://oss.callstack.com/agent-device/docs/ai-sdk) or [Eve](https://oss.callstack.com/agent-device/docs/eve). Developers at [Expensify](https://www.callstack.com/blog/how-expensify-uses-agent-device-for-mobile-bug-evidence-and-profiling), [Shopify](https://x.com/mustafa01ali/status/2036577353178943826), and [others](#who-uses-agent-device) use it to verify their apps. @@ -155,7 +155,7 @@ The same session and evidence model works at every step: the agent explores the ## How it works -`agent-device` keeps device state in sessions. It uses a local accessibility bridge for iOS Simulator snapshots and XCTest for iOS interactions, physical iOS, and tvOS; ADB and the snapshot helper on Android; HDC and ArkUI `uitest` on HarmonyOS; Vega CLI/VDA on the Vega Virtual Device; a local helper on macOS, including Electron and other Chromium-based apps through the accessibility tree; and AT-SPI on Linux. +`agent-device` keeps device state in sessions. It uses a local accessibility bridge for iOS Simulator snapshots and XCTest for iOS interactions, physical iOS, and tvOS; ADB and the snapshot helper on Android, Android TV, and Fire OS; HDC and ArkUI `uitest` on HarmonyOS; Vega CLI/VDA on the Vega Virtual Device; a local helper on macOS, including Electron and other Chromium-based apps through the accessibility tree; and AT-SPI on Linux. Support depth varies by target. Some backends, such as HarmonyOS and Vega OS, cover a subset of commands; run `agent-device capabilities --platform ` to see what a target supports. diff --git a/packages/platform-android/src/__tests__/app-helpers.test.ts b/packages/platform-android/src/__tests__/app-helpers.test.ts index 968525bdcb..b3dbe26395 100644 --- a/packages/platform-android/src/__tests__/app-helpers.test.ts +++ b/packages/platform-android/src/__tests__/app-helpers.test.ts @@ -24,6 +24,32 @@ async function withMockedAdbScript(script: string, run: () => Promise): Pr } } +test('listAndroidAppsWithAdb queries both launcher categories for a TV target', async () => { + // Fire OS apps register under either launcher category, so a TV listing merges both queries. + const calls: (readonly string[])[] = []; + const adb: AndroidAdbExecutor = async (args) => { + calls.push(args); + if (args.includes('query-activities')) { + return { + exitCode: 0, + stdout: args.includes('android.intent.category.LAUNCHER') + ? 'com.example.fireosapp/.MainActivity\n' + : '', + stderr: '', + }; + } + return { exitCode: 0, stdout: '', stderr: '' }; + }; + + const apps = await listAndroidAppsWithAdb(adb, { filter: 'all', target: 'tv' }); + + assert.deepEqual(apps, [{ package: 'com.example.fireosapp', name: 'Fireosapp' }]); + assert.deepEqual(calls.map((call) => call.at(-1)).sort(), [ + 'android.intent.category.LAUNCHER', + 'android.intent.category.LEANBACK_LAUNCHER', + ]); +}); + test('listAndroidAppsWithAdb uses an injected executor', async () => { const calls: (readonly string[])[] = []; const adb: AndroidAdbExecutor = async (args) => { diff --git a/packages/platform-android/src/__tests__/app-lifecycle-open.test.ts b/packages/platform-android/src/__tests__/app-lifecycle-open.test.ts index a1d77aeb70..774bb4e243 100644 --- a/packages/platform-android/src/__tests__/app-lifecycle-open.test.ts +++ b/packages/platform-android/src/__tests__/app-lifecycle-open.test.ts @@ -1,6 +1,6 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; -import { openAndroidApp } from '../app-lifecycle.ts'; +import { listAndroidApps, openAndroidApp } from '../app-lifecycle.ts'; import { withAndroidAdbProvider } from '../adb-executor.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; @@ -755,6 +755,122 @@ test('openAndroidApp fallback resolve-activity includes MAIN/LAUNCHER flags', as ); }); +test('openAndroidApp TV fallback carries the resolved LAUNCHER category into the component launch', async () => { + // A Fire OS app that declares no LEANBACK_LAUNCHER activity resolves under LAUNCHER only; the + // fallback launch must carry that category, not the target's LEANBACK_LAUNCHER. + await withFakeAdb( + (args) => { + if (args[0] === 'shell' && args[1] === 'am' && args[2] === 'start') { + if (args.includes('-p')) { + return [ + 'Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.DEFAULT,android.intent.category.LEANBACK_LAUNCHER] pkg=com.example.tvapp }', + 'Error: Activity not started, unable to resolve Intent { act=android.intent.action.MAIN cat=[android.intent.category.DEFAULT,android.intent.category.LEANBACK_LAUNCHER] flg=0x10000000 pkg=com.example.tvapp }', + ].join('\n'); + } + return 'Status: ok'; + } + if ( + args[0] === 'shell' && + args[1] === 'cmd' && + args[2] === 'package' && + args[3] === 'resolve-activity' + ) { + if (args.includes('android.intent.category.LEANBACK_LAUNCHER')) { + return { stdout: '', stderr: 'No activity found', exitCode: 1 }; + } + return 'com.example.tvapp/com.example.tvapp.MainActivity'; + } + return undefined; + }, + async ({ calls, device }) => { + await openAndroidApp(device, 'com.example.tvapp'); + assert.deepEqual(calls, [ + [ + 'shell', + 'am', + 'start', + '-W', + '-a', + 'android.intent.action.MAIN', + '-c', + 'android.intent.category.DEFAULT', + '-c', + 'android.intent.category.LEANBACK_LAUNCHER', + '-p', + 'com.example.tvapp', + ], + // resolve-activity tries LEANBACK_LAUNCHER first on a TV target... + [ + 'shell', + 'cmd', + 'package', + 'resolve-activity', + '--brief', + '-a', + 'android.intent.action.MAIN', + '-c', + 'android.intent.category.LEANBACK_LAUNCHER', + 'com.example.tvapp', + ], + // ...then LAUNCHER, which resolves the component. + [ + 'shell', + 'cmd', + 'package', + 'resolve-activity', + '--brief', + '-a', + 'android.intent.action.MAIN', + '-c', + 'android.intent.category.LAUNCHER', + 'com.example.tvapp', + ], + // The fallback launch uses the category the component resolved under. + [ + 'shell', + 'am', + 'start', + '-W', + '-a', + 'android.intent.action.MAIN', + '-c', + 'android.intent.category.DEFAULT', + '-c', + 'android.intent.category.LAUNCHER', + '-n', + 'com.example.tvapp/com.example.tvapp.MainActivity', + ], + ]); + }, + { device: { ...ANDROID_EMULATOR, target: 'tv' } }, + ); +}); + +test('listAndroidApps on a TV target queries LEANBACK_LAUNCHER then LAUNCHER', async () => { + await withFakeAdb( + (args) => { + if (args.includes('query-activities')) { + return args.includes('android.intent.category.LAUNCHER') + ? 'com.example.fireosapp/.MainActivity\n' + : ''; + } + return undefined; + }, + async ({ calls, device }) => { + const apps = await listAndroidApps(device, 'all'); + assert.deepEqual(apps, [{ package: 'com.example.fireosapp', name: 'Fireosapp' }]); + const queriedCategories = calls + .filter((call) => call.includes('query-activities')) + .map((call) => call.at(-1)); + assert.deepEqual(queriedCategories, [ + 'android.intent.category.LEANBACK_LAUNCHER', + 'android.intent.category.LAUNCHER', + ]); + }, + { device: { ...ANDROID_EMULATOR, target: 'tv' } }, + ); +}); + // In-process replacement for the old PATH-stub default adb script: any // `am start` succeeds with the "Status: ok" marker (so the primary launch // never falls back to resolve-activity), everything else is empty success. diff --git a/packages/platform-android/src/app-helpers.ts b/packages/platform-android/src/app-helpers.ts index 56bf36db2f..92a16c680d 100644 --- a/packages/platform-android/src/app-helpers.ts +++ b/packages/platform-android/src/app-helpers.ts @@ -5,9 +5,7 @@ import { parseAndroidUserInstalledPackages, } from './app-parsers.ts'; import { inferAndroidAppName } from './app-deployment-resolution.ts'; - -const ANDROID_LAUNCHER_CATEGORY = 'android.intent.category.LAUNCHER'; -const ANDROID_LEANBACK_CATEGORY = 'android.intent.category.LEANBACK_LAUNCHER'; +import { androidLauncherCategories } from './launcher-categories.ts'; export type AndroidAppListFilter = AppsFilter; export type AndroidAppListTarget = 'mobile' | 'tv' | 'auto'; @@ -38,28 +36,19 @@ async function listAndroidLaunchablePackagesWithAdb( ): Promise> { const discoveredPackages = ( await Promise.all( - resolveAndroidLaunchCategoriesForAdb(target).map(async (category) => { - const result = await runAdbShell(adb, buildAndroidQueryActivitiesArgs(category), { - allowFailure: true, - }); - return result.exitCode === 0 ? parseAndroidLaunchablePackageOutput(result.stdout) : []; - }), + androidLauncherCategories(target, { includeFallbackCategories: true }).map( + async (category) => { + const result = await runAdbShell(adb, buildAndroidQueryActivitiesArgs(category), { + allowFailure: true, + }); + return result.exitCode === 0 ? parseAndroidLaunchablePackageOutput(result.stdout) : []; + }, + ), ) ).flat(); return new Set(discoveredPackages); } -function resolveAndroidLaunchCategoriesForAdb(target: AndroidAppListTarget): string[] { - switch (target) { - case 'mobile': - return [ANDROID_LAUNCHER_CATEGORY]; - case 'tv': - return [ANDROID_LEANBACK_CATEGORY]; - default: - return [ANDROID_LAUNCHER_CATEGORY, ANDROID_LEANBACK_CATEGORY]; - } -} - function buildAndroidQueryActivitiesArgs(category: string): string[] { return [ 'cmd', diff --git a/packages/platform-android/src/app-lifecycle.ts b/packages/platform-android/src/app-lifecycle.ts index aff2f2b0f3..c0219ce48f 100644 --- a/packages/platform-android/src/app-lifecycle.ts +++ b/packages/platform-android/src/app-lifecycle.ts @@ -23,10 +23,9 @@ import { parseAndroidLaunchablePackages, parseAndroidUserInstalledPackages, } from './app-parsers.ts'; +import { androidLaunchCategory, androidLauncherCategories } from './launcher-categories.ts'; import { getAndroidAppState, getAndroidResumedActivity } from './window-state.ts'; -const ANDROID_LAUNCHER_CATEGORY = 'android.intent.category.LAUNCHER'; -const ANDROID_LEANBACK_CATEGORY = 'android.intent.category.LEANBACK_LAUNCHER'; const ANDROID_DEFAULT_CATEGORY = 'android.intent.category.DEFAULT'; const ANDROID_LOCALHOST_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']); /** Owns localhost URL reverses, so a teardown that removes only owned mappings removes them too. */ @@ -53,8 +52,8 @@ export async function listAndroidApps( async function listAndroidLaunchablePackages(device: DeviceInfo): Promise> { const packages = new Set(); - for (const category of resolveAndroidLaunchCategories(device, { - includeFallbackWhenUnknown: true, + for (const category of androidLauncherCategories(device.target, { + includeFallbackCategories: true, })) { const result = await runAndroidShell( device, @@ -80,26 +79,6 @@ async function listAndroidLaunchablePackages(device: DeviceInfo): Promise { const result = await runAndroidShell(device, ['pm', 'list', 'packages', '-3']); return parseAndroidUserInstalledPackages(result.stdout); @@ -225,7 +204,7 @@ async function startAndroidApp( const helperLaunch = await tryAndroidHelperLaunch(device, app, options); if (helperLaunch) return helperLaunch; const resolved = await resolveAndroidApp(device, app); - const launchCategory = resolveAndroidLauncherCategory(device); + const launchCategory = androidLaunchCategory(device.target); if (resolved.type === 'intent') { return await openAndroidIntent(device, resolved.value, options); } @@ -405,16 +384,22 @@ async function openAndroidPackage( if (primaryResult.exitCode === 0 && !isAmStartError(primaryResult.stdout, primaryResult.stderr)) { return primaryResult; } - const component = await resolveAndroidLaunchComponent(device, packageName); - if (!component) { + const resolvedComponent = await resolveAndroidLaunchComponent(device, packageName); + if (!resolvedComponent) { if (!(await isAndroidPackageInstalled(device, packageName))) { throw buildAndroidPackageNotInstalledError(packageName); } throw androidAdbResultError(`Failed to launch ${packageName}`, primaryResult); } + // The fallback launches under the category the component actually resolved under, which on a TV + // target can be LAUNCHER for an app that declares no LEANBACK_LAUNCHER activity. return await runAndroidShell( device, - buildAndroidActivityLaunchArgs(component, launchCategory, options), + buildAndroidActivityLaunchArgs( + resolvedComponent.component, + resolvedComponent.category, + options, + ), ); } @@ -516,10 +501,10 @@ function looksLikeMissingAndroidPackageOutput(output: string): boolean { async function resolveAndroidLaunchComponent( device: DeviceInfo, packageName: string, -): Promise { - const categories = Array.from( - new Set(resolveAndroidLaunchCategories(device, { includeFallbackWhenUnknown: true })), - ); +): Promise<{ component: string; category: string } | null> { + const categories = androidLauncherCategories(device.target, { + includeFallbackCategories: true, + }); for (const category of categories) { const result = await runAndroidShell( device, @@ -540,7 +525,7 @@ async function resolveAndroidLaunchComponent( continue; } const component = parseAndroidLaunchComponent(result.stdout); - if (component) return component; + if (component) return { component, category }; } return null; } diff --git a/packages/platform-android/src/inventory.test.ts b/packages/platform-android/src/inventory.test.ts index 12b2417eaa..67282f9b3e 100644 --- a/packages/platform-android/src/inventory.test.ts +++ b/packages/platform-android/src/inventory.test.ts @@ -145,6 +145,7 @@ test.each([ test.each([ ['feature:android.software.leanback_only\nfeature:android.hardware.wifi\n', 'tv'], ['feature:android.hardware.type.television=1\n', 'tv'], + ['feature:android.hardware.wifi\nfeature:amazon.hardware.fire_tv\n', 'tv'], ['feature:android.software.leanback_extra\nfeature:android.hardware.wifi\n', 'mobile'], ] as const)( 'Android inventory reads the feature list %j as a %s target', @@ -168,6 +169,36 @@ test.each([ }, ); +test('Android inventory classifies a device by has-feature amazon.hardware.fire_tv before the feature list', async () => { + // The characteristics probe says nothing, no AOSP leanback/television feature answers, and + // only the Amazon Fire TV feature answers true: the device must be a tv target from the + // has-feature probe, without the pm list features fallback deciding it. + const featureListProbeCalls: string[] = []; + const host = createHost(async (request) => { + const key = request.args.join('\0'); + if (key === 'devices\0-l') + return result('List of devices attached\nAFTKMST12 device model:AFTKMST12\n'); + if (key.includes('pm\0list\0features')) { + featureListProbeCalls.push(key); + return result('feature:android.hardware.wifi\n'); + } + if (key.includes('has-feature')) { + return result(key.includes('amazon.hardware.fire_tv') ? 'true\n' : 'false\n'); + } + const probe = RUNNING_EMULATOR_PROBES.find(([fragment]) => key.includes(fragment)); + if (probe) return result(probe[1]); + throw new Error(`Unexpected command: ${request.executable} ${request.args.join(' ')}`); + }); + + const [device] = await createAndroidInventory(host).discover( + { androidAvdSelection: 'running-only' }, + scope, + ); + + assert.equal(device?.target, 'tv'); + assert.deepEqual(featureListProbeCalls, []); +}); + test('Android inventory fails closed when adb is unavailable', async () => { const host = createHost(async () => result(''), { adb: undefined }); await assert.rejects( diff --git a/packages/platform-android/src/inventory.ts b/packages/platform-android/src/inventory.ts index cdb8d143cf..c43285fa25 100644 --- a/packages/platform-android/src/inventory.ts +++ b/packages/platform-android/src/inventory.ts @@ -29,10 +29,14 @@ const DEVICE_DESCRIPTION_TIMEOUT_MS = 2_000; const DEVICE_DESCRIPTION_PROBE = shellFragment( 'getprop ro.product.model; getprop ro.build.version.release', ); +// Fire OS devices declare amazon.hardware.fire_tv and may not declare the AOSP leanback +// features, so TV detection must read it as a TV signal too (Amazon: "Identify Fire TV +// Devices", adb shell pm list features). const TV_FEATURES = [ 'android.software.leanback', 'android.software.leanback_only', 'android.hardware.type.television', + 'amazon.hardware.fire_tv', ] as const; type AndroidInventoryContext = Readonly<{ diff --git a/packages/platform-android/src/launcher-categories.ts b/packages/platform-android/src/launcher-categories.ts new file mode 100644 index 0000000000..442f4c73f4 --- /dev/null +++ b/packages/platform-android/src/launcher-categories.ts @@ -0,0 +1,39 @@ +import type { DeviceTarget } from '@agent-device/kernel/device'; + +const ANDROID_LAUNCHER_CATEGORY = 'android.intent.category.LAUNCHER'; +const ANDROID_LEANBACK_CATEGORY = 'android.intent.category.LEANBACK_LAUNCHER'; + +/** + * What a launcher probe may be keyed on: the discovered `DeviceInfo` targets plus the `auto` + * listing target. A target Android cannot run (desktop) reads the unknown-target probe order. + */ +type AndroidLauncherQuery = 'mobile' | 'tv' | 'auto' | DeviceTarget | undefined; + +/** + * Launcher categories whose MAIN activities count as launchable for a target, in probe order. + * The first entry is the launch category. With `includeFallbackCategories` the list also carries + * the secondary categories, which form the listing union and the launch fallback: a Fire OS app + * can register under either TV launcher category, so a leanback-only probe misses LAUNCHER-only + * apps, and an unknown-target probe keeps LAUNCHER first. + */ +export function androidLauncherCategories( + target: AndroidLauncherQuery, + options: { includeFallbackCategories?: boolean } = {}, +): string[] { + if (target === 'tv') { + return options.includeFallbackCategories + ? [ANDROID_LEANBACK_CATEGORY, ANDROID_LAUNCHER_CATEGORY] + : [ANDROID_LEANBACK_CATEGORY]; + } + if (target === 'mobile') { + return [ANDROID_LAUNCHER_CATEGORY]; + } + return options.includeFallbackCategories + ? [ANDROID_LAUNCHER_CATEGORY, ANDROID_LEANBACK_CATEGORY] + : [ANDROID_LAUNCHER_CATEGORY]; +} + +/** The category a launch for this target starts with, without any fallback categories. */ +export function androidLaunchCategory(target: AndroidLauncherQuery): string { + return androidLauncherCategories(target)[0]!; +} diff --git a/skills/agent-device/SKILL.md b/skills/agent-device/SKILL.md index a2447b3ed5..82c2922e4b 100644 --- a/skills/agent-device/SKILL.md +++ b/skills/agent-device/SKILL.md @@ -1,6 +1,6 @@ --- name: agent-device -description: Automates Apple-platform apps (iOS, tvOS, macOS), Android devices, and Amazon Vega OS TV apps in Vega Virtual Devices. Use when navigating apps, taking snapshots/screenshots where supported, driving TV remotes, tapping, typing, scrolling, extracting UI info, collecting evidence, or planning agent-device CLI commands. +description: Automates Apple-platform apps (iOS, tvOS, macOS), Android devices (including Android TV and Fire TV on Fire OS), and Amazon Vega OS TV apps in Vega Virtual Devices. Use when navigating apps, taking snapshots/screenshots where supported, driving TV remotes, tapping, typing, scrolling, extracting UI info, collecting evidence, or planning agent-device CLI commands. --- # agent-device diff --git a/src/commands/schema/cli-help-first-screen.test.ts b/src/commands/schema/cli-help-first-screen.test.ts index f4ab3f28ec..8e476e6544 100644 --- a/src/commands/schema/cli-help-first-screen.test.ts +++ b/src/commands/schema/cli-help-first-screen.test.ts @@ -56,9 +56,9 @@ const FIRST_SCREEN_RULES: Record = { 'Rare iOS accessibility gap: a row shown disabled/hittable:false where press reports success but no UI change, or a collapsed composite control with no child refs -- run snapshot -i --json, compute the target center from rects, press x y, then diff snapshot -i. Coordinates are fallback-only; document why you used them.', ], tv: [ - 'Use this when the target is Android TV, Apple TV/tvOS, or an Amazon Vega OS TV app running in the Vega Virtual Device (VVD). TV surfaces are focus-first: move focus with remote/D-pad buttons, then activate the focused control', + 'Use this when the target is Android TV (including Fire TV devices on Fire OS over ADB), Apple TV/tvOS, or an Amazon Vega OS TV app running in the Vega Virtual Device (VVD). TV surfaces are focus-first: move focus with remote/D-pad buttons, then activate the focused control', 'Vega OS is driven through the SDK-matched Vega CLI and VDA, not ADB', - 'Initial support is VVD-only. Physical Fire TV devices remain unsupported', + 'Initial support is VVD-only. Physical devices that run Vega OS remain unsupported', 'The VVD is never booted implicitly', 'Snapshot, screenshot, selectors, install, touch/text/gesture, logs, and performance commands remain unsupported until their Vega backends are implemented', ], diff --git a/src/commands/schema/cli-help-topics.test.ts b/src/commands/schema/cli-help-topics.test.ts index 046317fd79..6f1b2febfa 100644 --- a/src/commands/schema/cli-help-topics.test.ts +++ b/src/commands/schema/cli-help-topics.test.ts @@ -359,7 +359,8 @@ test('usageForCommand resolves tv help topic', async () => { assert.match(help, /Vega OS uses the exact hold duration through inputd-cli/); assert.match(help, /Use --platform vega --target tv/); assert.match(help, /Initial support is VVD-only/); - assert.match(help, /Physical Fire TV devices remain unsupported/); + assert.match(help, /Physical devices that run Vega OS remain unsupported/); + assert.match(help, /amazon\.hardware\.fire_tv/); assert.equal(help.match(/\nVega OS:/g)?.length, 1); }); diff --git a/src/commands/schema/cli-help.ts b/src/commands/schema/cli-help.ts index 20cd1b31a8..44b10e9716 100644 --- a/src/commands/schema/cli-help.ts +++ b/src/commands/schema/cli-help.ts @@ -196,10 +196,11 @@ Recording: }, gestures: gestureHelpTopic, tv: { - summary: 'Android TV, tvOS, and Vega VVD focus-first remote navigation', + summary: + 'Android TV (incl. Fire TV on Fire OS), tvOS, and Vega VVD focus-first remote navigation', body: `agent-device help tv -Use this when the target is Android TV, Apple TV/tvOS, or an Amazon Vega OS TV app running in the Vega Virtual Device (VVD). TV surfaces are focus-first: move focus with remote/D-pad buttons, then activate the focused control. +Use this when the target is Android TV (including Fire TV devices on Fire OS over ADB), Apple TV/tvOS, or an Amazon Vega OS TV app running in the Vega Virtual Device (VVD). TV surfaces are focus-first: move focus with remote/D-pad buttons, then activate the focused control. Core loop: agent-device open Settings --platform android --target tv --session tv @@ -211,7 +212,7 @@ Core loop: Vega OS: Vega OS is driven through the SDK-matched Vega CLI and VDA, not ADB. - Initial support is VVD-only. Physical Fire TV devices remain unsupported until their discovery, lifecycle, and remote controls have durable hardware evidence. + Initial support is VVD-only. Physical devices that run Vega OS remain unsupported until their discovery, lifecycle, and remote controls have durable hardware evidence. Use --platform vega --target tv for the running Vega Virtual Device. vega virtual-device start agent-device devices --platform vega --target tv @@ -238,6 +239,7 @@ Buttons: Android TV: Android TV uses ADB keyevents behind agent-device tv-remote. Keep command plans on agent-device; do not switch to raw adb keyevent. Use --target tv when a host has both phone/tablet and TV emulators/devices. + Fire TV devices and emulators running Fire OS (Android-based) connect as Android TV devices over ADB: enable ADB debugging, run adb connect :5555, and use --platform android --target tv. Discovery reads the amazon.hardware.fire_tv system feature, so a Fire TV that declares no leanback feature is still a tv target. tvOS: tvOS is driven by the Siri Remote focus engine, not coordinate taps. diff --git a/website/docs/docs/client-api.md b/website/docs/docs/client-api.md index 18e62a7ec8..0dc404ba97 100644 --- a/website/docs/docs/client-api.md +++ b/website/docs/docs/client-api.md @@ -307,7 +307,7 @@ If the wait times out, check the selector and app state with a fresh snapshot. A `tvRemote` is TV-only. Android TV accepts the navigation buttons, named volume, power, media, channel, and color keys, and `android-keycode:N`. tvOS and Vega OS accept only `up`, `down`, `left`, `right`, `select`, `menu`, `home`, and `back`; other buttons fail with `UNSUPPORTED_OPERATION`. Phones and WebDriver sessions are unsupported. See [TV targets](/docs/commands#tv-targets) for the full button list and hold behavior. -Vega OS client support is VVD-only and covers device discovery, app open/close, `back`, `home`, and `tvRemote`. Physical Fire TV, capture, selector, install, logging, and performance methods report unsupported for Vega targets. +Vega OS client support is VVD-only and covers device discovery, app open/close, `back`, `home`, and `tvRemote`. Physical Vega OS devices, capture, selector, install, logging, and performance methods report unsupported for Vega targets. Fire TV devices that run Fire OS use the Android client methods as Android TV devices. Supported command methods: diff --git a/website/docs/docs/commands.md b/website/docs/docs/commands.md index 8845cccd5a..86d34085c5 100644 --- a/website/docs/docs/commands.md +++ b/website/docs/docs/commands.md @@ -310,16 +310,17 @@ agent-device close com.example.app.main --session vega-tv vega virtual-device stop ``` -- TV targets include Apple TV and Android TV simulators, emulators, and connected devices. Vega OS support is limited to the Vega Virtual Device (VVD). +- TV targets include Apple TV and Android TV simulators, emulators, and connected devices, plus Fire TV devices and emulators running Fire OS (Android-based). Vega OS support is limited to the Vega Virtual Device (VVD). - TV targets are focus-first: move focus with `tv-remote` before selecting a control. Don't use raw `adb shell input keyevent` in command plans. - `tv-remote` is TV-only. Android phones, iPhones, and WebDriver (Appium) sessions refuse it with `UNSUPPORTED_OPERATION`. - All three TV backends support `up`, `down`, `left`, `right`, `select`, `menu`, `home`, and `back`. Android TV also accepts `volume-up`, `volume-down`, `mute`, `power`, `sleep`, `wake`, `media-play-pause`, `media-next`, `media-previous`, `media-rewind`, `media-fast-forward`, `channel-up`, `channel-down`, `last-channel`, `guide`, `info`, `captions`, `tv-input`, `color-red`, `color-green`, `color-yellow`, and `color-blue`. tvOS and Vega OS refuse the additional buttons before dispatch. - For an Android TV remote button without a name, use `android-keycode:` with a decimal code from 1 to 2147483647. Other TV backends refuse raw Android codes. An unknown name or invalid code fails with `INVALID_ARGS`. - `power` and `sleep` can turn the screen off. A successful response confirms that the button was sent; it does not verify playback, volume, or display state, and it does not wait for the app to settle. -- Android TV: app launch and app listing use `LEANBACK_LAUNCHER` activities. `tv-remote` sends ADB keyevents; `longpress