Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Flags:
- `--device <name>`
- `--udid <udid>` (iOS)
- `--serial <serial>` (Android)
- `--activity <component>` (Android; package/Activity or package/.Activity)
- `--out <path>` (screenshot)
- `--session <name>`
- `--verbose` for daemon and runner logs
Expand Down
2 changes: 2 additions & 0 deletions skills/agent-device/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ npx -y agent-device

```bash
agent-device open [app] # Boot device/simulator; optionally launch app
agent-device open [app] --activity com.example/.MainActivity # Android: open specific activity
agent-device close [app] # Close app or just end session
agent-device session list # List active sessions
```
Expand Down Expand Up @@ -145,6 +146,7 @@ agent-device apps --platform android --user-installed
- `open <app>` can be used within an existing session to switch apps and update the session bundle id.
- If AX returns the Simulator window or empty tree, restart Simulator or use `--backend xctest`.
- Use `--session <name>` for parallel sessions; avoid device contention.
- Use `--activity <component>` on Android to launch a specific activity (e.g. TV apps with LEANBACK).

## References

Expand Down
3 changes: 2 additions & 1 deletion src/core/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type CommandFlags = {
udid?: string;
serial?: string;
out?: string;
activity?: string;
verbose?: boolean;
snapshotInteractiveOnly?: boolean;
snapshotCompact?: boolean;
Expand Down Expand Up @@ -94,7 +95,7 @@ export async function dispatchCommand(
await interactor.openDevice();
return { app: null };
}
await interactor.open(app);
await interactor.open(app, { activity: context?.activity });
return { app };
}
case 'close': {
Expand Down
2 changes: 2 additions & 0 deletions src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ function contextFromFlags(
traceLogPath?: string,
): {
appBundleId?: string;
activity?: string;
verbose?: boolean;
logPath?: string;
traceLogPath?: string;
Expand All @@ -96,6 +97,7 @@ function contextFromFlags(
} {
return {
appBundleId,
activity: flags?.activity,
verbose: flags?.verbose,
logPath,
traceLogPath,
Expand Down
43 changes: 38 additions & 5 deletions src/platforms/android/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,25 +148,58 @@ function parseAndroidFocus(text: string): { package?: string; activity?: string
return null;
}

export async function openAndroidApp(device: DeviceInfo, app: string): Promise<void> {
export async function openAndroidApp(
device: DeviceInfo,
app: string,
activity?: string,
): Promise<void> {
if (!device.booted) {
await waitForAndroidBoot(device.id);
}
const resolved = await resolveAndroidApp(device, app);
if (resolved.type === 'intent') {
if (activity) {
throw new AppError('INVALID_ARGS', 'Activity override requires a package name, not an intent');
}
await runCmd('adb', adbArgs(device, ['shell', 'am', 'start', '-a', resolved.value]));
return;
}
if (activity) {
const component = activity.includes('/')
? activity
: `${resolved.value}/${activity.startsWith('.') ? activity : `.${activity}`}`;
await runCmd(
'adb',
adbArgs(device, [
'shell',
'am',
'start',
'-a',
'android.intent.action.MAIN',
'-c',
'android.intent.category.DEFAULT',
'-c',
'android.intent.category.LAUNCHER',
'-n',
component,
]),
);
return;
}
await runCmd(
'adb',
adbArgs(device, [
'shell',
'monkey',
'-p',
resolved.value,
'am',
'start',
'-a',
'android.intent.action.MAIN',
'-c',
'android.intent.category.DEFAULT',
'-c',
'android.intent.category.LAUNCHER',
'1',
'-p',
resolved.value,
]),
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/utils/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type ParsedArgs = {
snapshotBackend?: 'ax' | 'xctest';
appsFilter?: 'launchable' | 'user-installed' | 'all';
appsMetadata?: boolean;
activity?: string;
noRecord?: boolean;
recordJson?: boolean;
help: boolean;
Expand Down Expand Up @@ -125,6 +126,9 @@ export function parseArgs(argv: string[]): ParsedArgs {
case '--session':
flags.session = value;
break;
case '--activity':
flags.activity = value;
break;
default:
throw new AppError('INVALID_ARGS', `Unknown flag: ${key}`);
}
Expand Down Expand Up @@ -208,6 +212,7 @@ Flags:
--device <name> Device name to target
--udid <udid> iOS device UDID
--serial <serial> Android device serial
--activity <component> Android activity to launch (package/Activity)
--out <path> Output path for screenshots
--session <name> Named session
--verbose Stream daemon/runner logs
Expand Down
4 changes: 2 additions & 2 deletions src/utils/interactors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
} from '../platforms/ios/index.ts';

export type Interactor = {
open(app: string): Promise<void>;
open(app: string, options?: { activity?: string }): Promise<void>;
openDevice(): Promise<void>;
close(app: string): Promise<void>;
tap(x: number, y: number): Promise<void>;
Expand All @@ -45,7 +45,7 @@ export function getInteractor(device: DeviceInfo): Interactor {
switch (device.platform) {
case 'android':
return {
open: (app) => openAndroidApp(device, app),
open: (app, options) => openAndroidApp(device, app, options?.activity),
openDevice: () => openAndroidDevice(device),
close: (app) => closeAndroidApp(device, app),
tap: (x, y) => pressAndroid(device, x, y),
Expand Down
Loading