Skip to content

fix(agent-mode): act on Aether's user instead of the owner user - #103

Closed
yumiru11 wants to merge 2 commits into
Zhou-Shilin:mainfrom
yumiru11:fix/agent-mode-multi-user
Closed

yumiru11 wants to merge 2 commits into
Zhou-Shilin:mainfrom
yumiru11:fix/agent-mode-multi-user

Conversation

@yumiru11

@yumiru11 yumiru11 commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #100.

Root cause

Agent Mode's user service never runs in Aether's user: Shizuku starts it as shell (uid 2000), the su fallback as root (uid 0), and Shizuku deliberately shares one user service process between the same package in every user (UserServiceArgs.tag javadoc: "user service is shared by the same packages installed in all users"). Every API that acts on "the user" therefore acted on the owner user:

  • package resolution, list_apps and the clipboard ran on the user-0 process contexts;
  • PendingIntentRecord starts the activity as the pending intent's own user (key.userId), and the pending intent had been created from a user-0 display context, so the activity was started for user 0;
  • ActivityRecord.showToCurrentUser() → isCurrentProfile(mUserId) keeps another profile's windows invisible, so the virtual display only ever composed black frames — exactly the reported "black image, not an error" — while the task was recorded under the owner user.

Changes

  • AIDL: launchPackage(..., userId), text(..., userId), listInstalledAppsJson(userId); captureImageToFd now returns a capture status.
  • Service: contextForUser(userId) builds a cached per-user context through the hidden createPackageContextAsUser("android", …). The framework special-cases that package (it copies the current context and only replaces the user), which keeps the shell package identity that the clipboard and the virtual-display owner check rely on, and works even where the host package is not installed in the target user. Package resolution, launching and clipboard writes all go through it.
  • Launch: the pending intent is created from the user-scoped display context; Android 9+ stamps that user. On Android 8.x the public overload only stamps the process user, so it stays in use while the requested user is the service process' own user (the owner-user behaviour from before) and the hidden getActivityAsUser is used for other users — failing loudly rather than launching in the wrong user.
  • Clipboard: paste (CJK/emoji text input) writes to the target user's clipboard.
  • Version: ShizukuUserServiceVersion 2 → 3, so a cached user service from an older build is recreated instead of answering the old AIDL.
  • Capture contract: results distinguish real pixels from the black placeholder (screenshot_blank + screenshot_blank_reason = launch_failed / no_launched_content), so a failed launch can no longer masquerade as a rendered screen.

Review

Ran the repo's two-axis review: Standards — 0 hard violations, 5 judgement calls, and the three worth acting on (duplicated blank-frame branch, missing caller/user guard on the clipboard path, API<28 regression risk) are fixed in the second commit. Spec — every point from the issue is covered (the root authorization path shares the same AIDL and now passes the same user id, the twice-resolved package now resolves in one user, and the intentional black-image branch is addressed); the remaining notes are deliberate scope decisions.

Verification

  • CI: :shared:testDebugUnitTest, :app:testDebugUnitTest and :app:assembleDebug are green.
  • Device (Android 13): the secondary-user scenario from the issue now works; the owner-user flow is unchanged.
  • Not verified on Android 8.x (no device available): that path keeps the previous owner-user behaviour and declines cross-user launches when the hidden overload is unavailable.

Known limits

  • Cross-user operation depends on the hidden createPackageContextAsUser; a device that refuses it gets an explicit error instead of a silent owner-user fallback.
  • All-black frames still come back as source=display; only placeholders are annotated.

Summary by CodeRabbit

  • New Features
    • App launches, installed-app listings, and text input now operate within the specified Android user profile.
    • Screenshot results identify black placeholder images and explain whether an app failed to launch or no display content was available.
    • Text input fallback uses the selected profile’s clipboard.
  • Documentation
    • Agent Mode guidance clarifies cursor coordinates and how to interpret placeholder screenshots.

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f6a927c5-6ec3-48cd-ac10-af00c10d8a1e
📥 Commits

Reviewing files that changed from the base of the PR and between 22496bc and 7b01dc7.

📒 Files selected for processing (8)
  • app/src/main/aidl/com/zhousl/aether/agentmode/IAetherAgentModeService.aidl
  • app/src/main/java/com/zhousl/aether/agentmode/AetherAgentModeShizukuService.kt
  • app/src/main/java/com/zhousl/aether/agentmode/AgentModeCaptureStatus.kt
  • app/src/main/java/com/zhousl/aether/agentmode/AgentModeUserScope.kt
  • app/src/main/java/com/zhousl/aether/data/AetherToolExecutor.kt
  • app/src/main/java/com/zhousl/aether/data/AgentModeController.kt
  • app/src/test/java/com/zhousl/aether/agentmode/AgentModeCaptureStatusTest.kt
  • app/src/test/java/com/zhousl/aether/agentmode/AgentModeUserScopeTest.kt
 ________________________________
< GPU-accelerated bug detection. >
 --------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…-Shilin#100)

The Agent Mode user service always runs in user 0: Shizuku starts it as shell
(uid 2000), the su fallback as root (uid 0), and Shizuku even shares one user
service process between the same package in every user. Every API that acts on
"the user" therefore acted on the owner user whenever Aether ran in a secondary
user or a work profile:

- the virtual display stayed black, because the activity was launched for user 0
  while Aether's user was in the foreground. ActivityRecord.showToCurrentUser()
  keeps another profile's windows invisible, so SurfaceFlinger only produced black
  frames and the "screenshot" was a black image instead of an error;
- list_apps returned the owner user's packages;
- pasted text (CJK, emoji) went to the owner user's clipboard.

Aether now passes its own user id (Process.myUid() / 100000) to the service, which
builds a cached per-user context through the hidden createPackageContextAsUser
(the "android" special case keeps this process's shell package identity and does
not need the host package installed in the target user). Package resolution, the
pending intent that starts the activity and the clipboard all use that context.
PendingIntent.getActivity only stamps the context user on Android 9+, so older
releases go through the hidden getActivityAsUser or refuse to launch instead of
silently starting in the wrong user. A caller-user mismatch is logged rather than
accepted, and captures now report black placeholders (screenshot_blank plus
screenshot_blank_reason), so a failed launch cannot masquerade as a rendered
screen.

The Shizuku user service version is bumped, otherwise a cached process from an
older build would keep answering the old AIDL.
- captureImageToFd derives the blank reason first and draws its black placeholder
  once instead of duplicating the call in both branches.
- warnIfCallerUserDiffers moves into contextForUser, so every user-scoped call
  (launchPackage, listInstalledAppsJson and the clipboard path behind text) is
  guarded exactly once, and the user-scoped context builder logs its single line
  after either reflection path.
- On Android 8.x the public PendingIntent.getActivity still stamps the process
  user, so keep using it while the requested user is this service process's own
  user (the owner-user behaviour Agent Mode had before cross-user support) and
  only reach for the hidden getActivityAsUser for other users.

Android 9+ behaviour is unchanged: owner-user launches, list_apps, clipboard and
capture semantics stay exactly as tested on device.
@yumiru11
yumiru11 force-pushed the fix/agent-mode-multi-user branch from 0981a7d to 7b01dc7 Compare October 3, 2026 14:48
@yumiru11
yumiru11 marked this pull request as ready for review October 3, 2026 14:57
@Zhou-Shilin Zhou-Shilin closed this Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] 安卓非机主用户下 Agent Mode 以机主用户身份启动应用,虚拟显示始终黑屏

2 participants