Skip to content

fix(android): open the macOS emulator window behind the focused app - #3617

Merged
thymikee merged 5 commits into
mainfrom
fix/android-emulator-background-window
Oct 15, 2026
Merged

thymikee merged 5 commits into
mainfrom
fix/android-emulator-background-window

Conversation

@thymikee

@thymikee thymikee commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

On macOS, a GUI Android emulator launch (boot --platform android --device <avd>, or open booting a stopped AVD) took focus from the app you were using. The emulator has no flag to stop this; -qt-hide-window hides the window, and Qt's background-only mode removes the Dock icon and Cmd-Tab entry. The launch host now reads the frontmost app with lsappinfo before spawning the emulator, then starts a JXA watcher for the remaining startup budget. When the emulator becomes frontmost, the watcher reactivates the previous app once and exits. The window lands directly below that app, which matches how iOS opens Simulator with open -g. Headless and non-macOS launches are unchanged. The androidEmulator.launch port is async now and receives the boot budget.

The second commit removes ensureAndroidEmulatorBooted. Production boots emulators only through readiness/runtime.ts and the host launch port, so this other launch path had no caller outside its own tests.

Validation

Tested head 3ad7668d79, macOS 26.6.2, emulator 36.2.12, AVD Pixel_9_Pro_XL_API_37. Frontmost app polled every 20 ms during agent-device boot:

base (emulator launched directly) head
1s front: qemu-system-aarch64, focus stays stolen 3s front: qemu-system-aarch64, then back to Claude within the same second; window order: Claude, then qemu-system-aarch64

No watcher process was left after the run.

  • pnpm check:affected --run: passed on 8e1b826052. CI passed in full on d5562c845b, and 8e1b826052 only changes docs wording. The later commits only make the frontmost-app read best-effort and change docs wording, so the live run on 3ad7668d79 still covers the success path.
  • Unit tests cover the frontmost-pid capture, an unreadable or failing frontmost-app read, headless and non-darwin launches, and passing the boot budget from readiness. The budget test fails when the passed value is changed.

Remaining risk: the emulator holds focus for up to ~50 ms (one watcher poll).

thymikee and others added 2 commits October 14, 2026 17:56
The emulator activates itself when its window opens, and it has no flag to stop
that. A GUI launch on macOS now starts a short JXA watcher. When the emulator
becomes frontmost, the watcher reactivates the app that was frontmost before it,
so the window lands directly below that app.

Co-Authored-By: Claude <noreply@anthropic.com>
…launch path

Production boots emulators through readiness/runtime.ts and the host launch
port. This second launch path had no caller outside its own tests.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 14, 2026 •

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

@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.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/platform-runtime-android-emulator-host.test.ts
Comment thread src/platform-runtime-android-emulator-host.ts Outdated
Comment thread src/platform-runtime-android-emulator-host.ts Outdated
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.22 MB 5.22 MB -1.2 kB
Package (unpacked) 5.22 MB 5.22 MB -1.2 kB
Package (download) 1.57 MB 1.57 MB -331 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 21.3 ms 21.5 ms +0.1 ms
CLI --help 59.4 ms 59.7 ms +0.3 ms

Read the frontmost app before spawning the emulator, so the watcher has an app to
restore even if the emulator activates before its first poll. Bound the watcher
by the remaining startup budget instead of a fixed 30 seconds. The launch port is
async now because it reads the frontmost app.

Co-Authored-By: Claude <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

The code in 2cb3c31 looks right to me, and I found no defect in the change itself. Checks are still pending: Smoke Tests and Analyze are queued, and Compatibility & Provenance was cancelled, probably because a newer run replaced it. The diff only touches the Android emulator launch host and dead platform-android code, and CI does not run the macOS GUI emulator route, so nothing points to a failure from this PR. I did not reproduce the live macOS GUI boot. I relied on the base-vs-head table in the PR body (macOS 26.6.2, emulator 36.2.12). I assumed the emulator launcher execs qemu in place, so the pid from runCmdDetached is the process that becomes frontmost. I did not check this in the emulator source, but the live run (qemu-system-aarch64 frontmost on base, Safari kept on head) supports it. After an abort the emulator is terminated, but the watcher keeps polling until its 30 s deadline and then exits without doing anything. I checked this by reading the code. It is bounded and harmless.

Not blocking: the new sentence at https://github.com/callstack/agent-device/blob/2cb3c31/website/docs/docs/commands.md#L69 says the window "returns focus to it", but the watcher returns focus, not the window. It also sits only under boot, while open on a stopped AVD takes the same route. Something like "On macOS the window opens behind the focused app, which keeps focus (also when open boots a stopped AVD)." would fix both. You can take this or leave it.

Two open inline threads from the other review still hold, and I would like them addressed before this is ready. The first is that the unit test only checks that the script contains activateWithOptions, so the JXA logic is not unit-tested (#3617 (comment)). The live run in the PR body covers that behavior. The second is that the first loop iteration sleeps 50 ms before it samples the frontmost app, so focus is not restored if the emulator activates in that window (#3617 (comment)). This is unlikely given the roughly 1 s activation seen live, and sampling frontmostApplication once before the loop is a cheap fix. One thread does not apply and you can resolve it: #3617 (comment) argues the 30 s window is too short for a boot of up to 120 s. Activation happens when the emulator's Qt window is created, about 1 s after launch, and the 30 s limit is a best-effort budget that is shorter than the boot it accompanies.

Before merge, please address those two threads; the queued Smoke Tests and Analyze jobs need to finish green, and Compatibility & Provenance needs a rerun. No conflicts.

@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.

All reported issues were addressed across 13 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/platform-runtime-android-emulator-host.ts Outdated
…pp read

runCmd rejects on a timeout or a missing tool even with allowFailure, so a failed
lsappinfo read stopped the AVD from booting. Treat the read as best-effort, give
it a 1 s budget, and say in the docs that open takes the same route as boot.

Co-Authored-By: Claude <noreply@anthropic.com>
@thymikee

Copy link
Copy Markdown
Member Author

Replies to the review of 2cb3c31. Current head is d5562c8.

  • First-poll race (r4273206946): fixed in 3ad7668. I took a different approach from sampling once before the loop: launch reads the frontmost app with lsappinfo before it spawns the emulator, and the watcher starts with that pid. A sample taken inside the watcher would still miss an activation that happens while osascript is starting up.
  • Test only checks activateWithOptions (r4273206928): the test now checks that the pid captured before launch reaches the watcher's argv. A unit test can't check what the JXA does on screen because that needs a window server, so the live macOS run in the PR body covers it.
  • 30 s window (r4273206959): I agree activation happens at Qt window creation. I bound the watcher to the remaining startup budget anyway, because the readiness deadline already exists and it removes a second, unrelated constant. After an abort, the emulator pid is gone, so the watcher never matches and exits at its deadline.
  • Docs: I took your wording, including the note about open.
  • New, from cubic: a timeout or missing tool in lsappinfo rejected launch and stopped the boot. The read is now best-effort with a 1 s budget.

pnpm check:affected --run passes on d5562c8. CI reran on the new head, so Compatibility & Provenance is running again.

@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.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread website/docs/docs/commands.md Outdated
@thymikee

Copy link
Copy Markdown
Member Author

The code looks good at d5562c8, and I found nothing that blocks it. No conflicts. CI is still pending: CodeQL, both Smoke Tests, deploy-preview and Coverage are queued or running, and none has failed. The Smoke Tests do exercise the Android readiness launch route that this diff makes async. If one fails on a Linux or headless runner, look at the await and the boot budget change first, because the watcher only runs for windowed macOS launches. I did not rerun the live macOS boot. I relied on the PR body's run at 3ad7668, and the later change only touches the failure branch of the frontmost read. I also did not verify that the emulator launcher pid is the same pid that becomes frontmost. The reported live run on emulator 36.2.12 implies it is.

Not blocking, and you can take or leave these: the detached osascript watcher in https://github.com/callstack/agent-device/blob/d5562c8/src/platform-runtime-android-emulator-host.ts#L50 keeps its 50 ms wakeups after a cancelled boot until the boot budget ends, because the emulator is terminated but the watcher is not. It could exit when the emulator pid is no longer running. Also, the watcher stops at the boot deadline, so an emulator that activates after a boot_timeout can still take focus. Please say that in the same best-effort wording as the docs fix below.

On the open threads, the Cubic thread on the unqualified docs claim still applies: #3617 (comment). The four other Cubic threads are fixed at this head, so please resolve them: #3617 (comment) (failed frontmost read is now caught), #3617 (comment) (pid is read before spawn and passed as argv), #3617 (comment) (boot budget replaces the 30s cap), and #3617 (comment) (test asserts the pid and budget reach argv).

Before merge, please qualify the wording in website/docs/docs/commands.md as best-effort, then wait for the queued Smoke Tests and Coverage to pass on d5562c8.

@thymikee

Copy link
Copy Markdown
Member Author

Replies to the review of d5562c8. Current head is 8e1b826, which only changes docs.

  • Best-effort wording (r4273699237): fixed. The docs name both gaps: the focused app can't be read, or the window appears after the startup budget runs out. I replied to the thread and resolved it.
  • The four other Cubic threads were already resolved.
  • Watcher outlives a cancelled boot: leaving it as is. It is bounded by the boot budget, does nothing after the emulator is gone (the pid can't become frontmost), and costs one 50 ms run-loop tick. To exit early, it would need either a liveness check through a C binding in JXA, or a second registry of watcher pids in the host for terminate. Both add more than they save.
  • CI: all checks passed on d5562c8 (Smoke Tests, Coverage, Integration, CodeQL, Compatibility & Provenance). pnpm check:affected --run passes on 8e1b826.

@thymikee

Copy link
Copy Markdown
Member Author

The code in 8e1b826 looks good to me. The three problems from the earlier review at d5562c8 are fixed: a failed frontmost read is now caught, the frontmost pid is read before spawn and passed as argv, and the boot budget replaces the fixed cap. The docs now describe the behavior as best-effort and name both gaps, which matches the host code.

CI is green: 22 checks, none failing. The delta since d5562c8 is docs-only, and the author reports Smoke Tests, Coverage, Integration, CodeQL and Compatibility & Provenance passed on that commit. There are no conflicts, and nothing else stands in the way of merge.

I did not rerun the live macOS windowed boot. I relied on the run in the PR body at 3ad7668, and later commits only touched the frontmost-read failure branch and the docs. I also did not verify that the emulator launcher pid is the same pid that becomes frontmost, though the reported live run implies it is. Unit tests cannot observe the JXA activation behavior, which needs a window server. I did not check whether other docs or help text repeat the unqualified focus claim.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 15, 2026
@thymikee
thymikee merged commit 11a07d6 into main Oct 15, 2026
22 checks passed
@thymikee
thymikee deleted the fix/android-emulator-background-window branch October 15, 2026 08:14
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