Repository navigation
feat: 增加 Legacy/VDM 虚拟屏后端选择 - #295
Conversation
- 设置项默认 LEGACY,Android 14+ 可选 VDM;一次任务只用一个后端,VDM 失败不回退 Legacy - VDM 通过 VirtualDeviceManager + CompanionDevice association 获取独立 display group; 提权服务保持 shell(2000),单 session,Association 命令按 Android 14/15/16 区分 - 校验 displayGroupId > 0 与 Display.STATE_ON;失败按 VirtualDisplay -> VirtualDevice -> Association 逆序清理 - 同后端启动复用已建显示,切换后端先释放旧显示再重建;R8 不新增 keep - 补充 VdmDisplayController / AppSettings 单测
- canReuse 辅助函数按后端派生默认 displayId,消除五参数重复书写 - 合并版本兼容、资源清理与启动分流的重复用例(15 -> 9) - 保留清理顺序/失败继续/suppressed、VDM 不回退(未请求 VDM、Android 13、14 成功、14 失败)等关键覆盖 - 清理失败可能用到的日志 Mock 仅在对应用例内挂载并在 finally 卸载
- RemoteService.aidl:保留上游 getProcessExitRecords = 56,VDM 后端事务改为 57 - RemoteServiceImpl:采用上游 cleanupStep 结构并加入 virtual display 清理步骤;移除上游已删除的 heartbeat 字段与看门狗引用
评审者指南本 PR 在保持 Legacy 默认行为不变的前提下,引入 Android 14+ 可选 VDM 后端:设置层持久化选择,任务启动经 AIDL 传递至提权服务,由 VDM 控制器管理 association、VirtualDevice 与 VirtualDisplay 的完整生命周期,并通过独立 display group 校验、严格清理、显示复用和活动 displayId 统一使用保证后台任务在灭屏场景下继续运行。 选择并启动 VDM 虚拟显示后端的时序图sequenceDiagram
actor User
participant SettingsViewModel
participant AppSettingsManager
participant MaaCompositionService
participant RemoteServiceImpl
participant VdmDisplayController
participant AndroidSystem
User->>SettingsViewModel: setVirtualDisplayBackend(VDM)
SettingsViewModel->>AppSettingsManager: setVirtualDisplayBackend(VDM)
AppSettingsManager-->>SettingsViewModel: virtualDisplayBackend = VDM
MaaCompositionService->>RemoteServiceImpl: setUseVirtualDeviceDisplay(true, userId)
RemoteServiceImpl-->>MaaCompositionService: true
MaaCompositionService->>RemoteServiceImpl: startVirtualDisplay()
RemoteServiceImpl->>VdmDisplayController: start(userId)
VdmDisplayController->>AndroidSystem: cmd companiondevice associate
AndroidSystem-->>VdmDisplayController: AssociationInfo
VdmDisplayController->>AndroidSystem: createVirtualDevice
AndroidSystem-->>VdmDisplayController: VirtualDevice
VdmDisplayController->>AndroidSystem: createVirtualDisplay
AndroidSystem-->>VdmDisplayController: VirtualDisplay
VdmDisplayController->>VdmDisplayController: awaitDisplayReady()
VdmDisplayController-->>RemoteServiceImpl: displayId
RemoteServiceImpl-->>MaaCompositionService: displayId
MaaCompositionService->>MaaCompositionService: asyncConnect(displayId)
虚拟显示复用与后端切换的时序图sequenceDiagram
participant MaaCompositionService
participant RemoteServiceImpl
participant VdmDisplayController
participant VirtualDisplayManager
MaaCompositionService->>RemoteServiceImpl: setUseVirtualDeviceDisplay(enabled, userId)
alt Active display uses the same backend
RemoteServiceImpl-->>MaaCompositionService: true
MaaCompositionService->>RemoteServiceImpl: startVirtualDisplay()
RemoteServiceImpl->>VdmDisplayController: start(userId)
VdmDisplayController-->>RemoteServiceImpl: existing displayId
else Backend changed
RemoteServiceImpl->>RemoteServiceImpl: stopVirtualDisplay()
alt Previous backend is VDM
RemoteServiceImpl->>VdmDisplayController: stop()
else Previous backend is Legacy
RemoteServiceImpl->>VirtualDisplayManager: stop()
end
RemoteServiceImpl-->>MaaCompositionService: true
MaaCompositionService->>RemoteServiceImpl: startVirtualDisplay()
alt Requested backend is VDM
RemoteServiceImpl->>VdmDisplayController: start(userId)
else Requested backend is Legacy
RemoteServiceImpl->>VirtualDisplayManager: start()
end
RemoteServiceImpl-->>MaaCompositionService: displayId
end
VDM 失败与逆序清理的时序图sequenceDiagram
participant RemoteServiceImpl
participant VdmDisplayController
participant AndroidSystem
RemoteServiceImpl->>VdmDisplayController: start(userId)
VdmDisplayController->>AndroidSystem: createVirtualDisplay
AndroidSystem-->>VdmDisplayController: failure
VdmDisplayController->>VdmDisplayController: cleanupFailure()
VdmDisplayController->>AndroidSystem: releaseDisplay
VdmDisplayController->>AndroidSystem: closeVirtualDevice
VdmDisplayController->>AndroidSystem: removeAssociation
VdmDisplayController-->>RemoteServiceImpl: exception
RemoteServiceImpl->>RemoteServiceImpl: startBackgroundDisplay()
RemoteServiceImpl-->>RemoteServiceImpl: DISPLAY_NONE
RemoteServiceImpl-->>RemoteServiceImpl: no Legacy fallback
文件级变更
提示与命令与 Sourcery 交互
自定义使用体验访问你的控制面板以:
获取帮助Original review guide in EnglishReviewer's Guide本 PR 在保持 Legacy 默认行为不变的前提下,引入 Android 14+ 可选 VDM 后端:设置层持久化选择,任务启动经 AIDL 传递至提权服务,由 VDM 控制器管理 association、VirtualDevice 与 VirtualDisplay 的完整生命周期,并通过独立 display group 校验、严格清理、显示复用和活动 displayId 统一使用保证后台任务在灭屏场景下继续运行。 Sequence diagram for selecting and starting the VDM virtual display backendsequenceDiagram
actor User
participant SettingsViewModel
participant AppSettingsManager
participant MaaCompositionService
participant RemoteServiceImpl
participant VdmDisplayController
participant AndroidSystem
User->>SettingsViewModel: setVirtualDisplayBackend(VDM)
SettingsViewModel->>AppSettingsManager: setVirtualDisplayBackend(VDM)
AppSettingsManager-->>SettingsViewModel: virtualDisplayBackend = VDM
MaaCompositionService->>RemoteServiceImpl: setUseVirtualDeviceDisplay(true, userId)
RemoteServiceImpl-->>MaaCompositionService: true
MaaCompositionService->>RemoteServiceImpl: startVirtualDisplay()
RemoteServiceImpl->>VdmDisplayController: start(userId)
VdmDisplayController->>AndroidSystem: cmd companiondevice associate
AndroidSystem-->>VdmDisplayController: AssociationInfo
VdmDisplayController->>AndroidSystem: createVirtualDevice
AndroidSystem-->>VdmDisplayController: VirtualDevice
VdmDisplayController->>AndroidSystem: createVirtualDisplay
AndroidSystem-->>VdmDisplayController: VirtualDisplay
VdmDisplayController->>VdmDisplayController: awaitDisplayReady()
VdmDisplayController-->>RemoteServiceImpl: displayId
RemoteServiceImpl-->>MaaCompositionService: displayId
MaaCompositionService->>MaaCompositionService: asyncConnect(displayId)
Sequence diagram for virtual display reuse and backend switchingsequenceDiagram
participant MaaCompositionService
participant RemoteServiceImpl
participant VdmDisplayController
participant VirtualDisplayManager
MaaCompositionService->>RemoteServiceImpl: setUseVirtualDeviceDisplay(enabled, userId)
alt Active display uses the same backend
RemoteServiceImpl-->>MaaCompositionService: true
MaaCompositionService->>RemoteServiceImpl: startVirtualDisplay()
RemoteServiceImpl->>VdmDisplayController: start(userId)
VdmDisplayController-->>RemoteServiceImpl: existing displayId
else Backend changed
RemoteServiceImpl->>RemoteServiceImpl: stopVirtualDisplay()
alt Previous backend is VDM
RemoteServiceImpl->>VdmDisplayController: stop()
else Previous backend is Legacy
RemoteServiceImpl->>VirtualDisplayManager: stop()
end
RemoteServiceImpl-->>MaaCompositionService: true
MaaCompositionService->>RemoteServiceImpl: startVirtualDisplay()
alt Requested backend is VDM
RemoteServiceImpl->>VdmDisplayController: start(userId)
else Requested backend is Legacy
RemoteServiceImpl->>VirtualDisplayManager: start()
end
RemoteServiceImpl-->>MaaCompositionService: displayId
end
Sequence diagram for VDM failure and reverse-order cleanupsequenceDiagram
participant RemoteServiceImpl
participant VdmDisplayController
participant AndroidSystem
RemoteServiceImpl->>VdmDisplayController: start(userId)
VdmDisplayController->>AndroidSystem: createVirtualDisplay
AndroidSystem-->>VdmDisplayController: failure
VdmDisplayController->>VdmDisplayController: cleanupFailure()
VdmDisplayController->>AndroidSystem: releaseDisplay
VdmDisplayController->>AndroidSystem: closeVirtualDevice
VdmDisplayController->>AndroidSystem: removeAssociation
VdmDisplayController-->>RemoteServiceImpl: exception
RemoteServiceImpl->>RemoteServiceImpl: startBackgroundDisplay()
RemoteServiceImpl-->>RemoteServiceImpl: DISPLAY_NONE
RemoteServiceImpl-->>RemoteServiceImpl: no Legacy fallback
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
你好 - 我发现了 1 个问题
AI 代理提示
请处理此代码审查中的评论:
## 单条评论
### 评论 1
<location path="app/src/main/java/com/aliothmoon/maameow/remote/internal/VdmDisplayController.kt" line_range="108" />
<code_context>
+ @Synchronized
+ fun stop() {
+ val active = session ?: return
+ session = null
+ val displayId = active.display.display.displayId
+ val failure = cleanupInReverseOrder(
</code_context>
<issue_to_address>
**清理失败后无法重试**
在 `stop` 期间,如果释放显示器、关闭虚拟设备或移除关联失败,`stop` 会在清理完成前清除 `session`,因此后续调用无法重试失败的清理步骤。VDM 资源或关联会残留,并可能影响后续启动。
在每项资源成功释放之前保留清理失败状态,以便后续调用 `stop` 时可以重试。
</issue_to_address>Sourcery 评估
需要人工审查。 有 1 个问题需要优先处理;此外,选择启用的 VDM 路径会创建可信虚拟显示器和系统伴侣关联;生命周期缺陷可能导致该关联在应用回滚后仍然残留。残留状态范围有限且可以移除,但仅回滚可能无法清理它。
阻塞性问题:app/src/main/java/com/aliothmoon/maameow/remote/internal/VdmDisplayController.kt:108
Original comment in English
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="app/src/main/java/com/aliothmoon/maameow/remote/internal/VdmDisplayController.kt" line_range="108" />
<code_context>
+ @Synchronized
+ fun stop() {
+ val active = session ?: return
+ session = null
+ val displayId = active.display.display.displayId
+ val failure = cleanupInReverseOrder(
</code_context>
<issue_to_address>
**Failed cleanup cannot be retried**
When display release, virtual-device close, or association removal fails during `stop`, `stop` clears `session` before cleanup finishes, so a later call cannot retry the failed cleanup step. The VDM resource or association remains behind and can interfere with later starts.
Retain failed cleanup state until each resource is successfully released, so later `stop` calls can retry it.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 1 finding to address first, and the opt-in VDM path creates a trusted virtual display and a system companion association; a lifecycle defect could leave that association behind even after the app is reverted. The leftover state is bounded and can be removed, but reverting alone may not clean it up.
Blocking findings: app/src/main/java/com/aliothmoon/maameow/remote/internal/VdmDisplayController.kt:108
- 抽取 startBackgroundDisplay 后,VdmDisplayController.start 的调用点失去 lint 可见的 SDK 守卫 - 新增私有 startVdmDisplay() 显式判断 Build.VERSION.SDK_INT >= 34 - 与 startBackgroundDisplay 内部的 sdkInt 判断形成双重运行时保护
|
看起来是 display group id 分配的问题?看看最新提交的构建看看调试日志里的 display group id 呢,但是按理来说都是应该需要 trusted display 的权限,可以先试试看? |
|
1.VirtualDeviceManager 配合 FLAG_TRUSTED 创建受系统认可的可信显示器。 现在是这样做的,还有什么能优化的吗? |
我的意思是让你直接测试一下新的主分支的代码,我没有你那个测试环境可以复现问题,我想看看主分支上的是不是没办法自动分配新的 display group id |
|
能打个包吗?我这里没电脑不好打包 |
|
原始日志: VD created: id=46, configured=1280x720, actual=1280x720, rotation=0, flags=0x1fd49, tier=0 VD effective: group=0, flags=[TRUSTED, OWN_DISPLAY_GROUP, ALWAYS_UNLOCKED, TOUCH_FEEDBACK_DISABLED, OWN_FOCUS, STEAL_TOP_FOCUS_DISABLED] VD requested OWN_DISPLAY_GROUP but landed in group 0 看上去发生在系统的 LogicalDisplay 分组阶段,而非 VirtualDisplay 创建权限检查阶段。 Legacy 同时设置了 OWN_DISPLAY_GROUP 和 DEVICE_DISPLAY_GROUP,但并未关联 VirtualDevice。可以考虑先单独移除 DEVICE_DISPLAY_GROUP,观察实际分组是否变化。 另外,Android 16 的 LogicalDisplayMapper 存在预先决定显示组的路径,也值得检查 separate_timeouts 等策略是否影响 OWN_DISPLAY_GROUP 的最终分配结果。不过,目前尚未确认 One UI 实际进入了哪条分支。 因此,aa9193f 的 SecurityException fallback 应该无法解决当前问题。建议优先定位为什么 Legacy 请求独立显示组后仍然得到 Group 0,再决定是否只能通过 VDM 实现独立分组。 手机只能测成这样了 再测就要用 MAC 了,等回头看吧 |
- RemoteService.aidl:57 已被 getVirtualDisplayFlagTier 占用,setUseVirtualDeviceDisplay 顺延到 58 - DisplayManager.getDisplayGroupId 沿用 main 的实现(取不到返回 -1) - MaaCompositionService:保留 VDM 失败文案,同时接上 main 的 flag tier 上报 - RemoteServiceImpl:去掉 main 已删除的 VirtualDisplayManager.setMonitorSurface 调用 - values-ko 补上 VDM 新增的 5 条文案
- 新增 BackgroundDisplay 统一分流 Legacy / VDM,去掉 RemoteServiceImpl 与 StaleFrameGuard 里的平行状态 - 运行时不允许切换虚拟屏模式:提权侧有活动后台屏就拒绝;设置项运行中置灰,空闲时先关旧屏再保存 - VDM 换分辨率时重建显示,复用前校验 display 仍有效 - VDM 按 锁屏常亮 × trusted 四档降级,补 TOUCH_FEEDBACK_DISABLED / DESTROY_CONTENT_ON_REMOVAL,旋转修正与 Legacy 共用 - 关联固定在 shell 所在的 user 0,去掉 userId 传参;紧急清理不再同步拆关联 - 反射收进 third/wrappers 的 CompanionDeviceManager / VirtualDeviceManager,命令走 RemoteUtils.shellExec - setVirtualDisplayMode 加锁;遥测补 vd.backend 标签与 VDM_DISPLAY_ERROR - 设置项改名「虚拟屏模式」并移到后台运行最下方,补齐 ko 文案


Pull Request
提交前请阅读 PR 规范 / PR Guidelines。
关联 Issue
Related #273
#273 中后台任务的虚拟屏由
DisplayManager.createVirtualDisplay创建。该方式在 Android 16 上即使设置TRUSTED与OWN_DISPLAY_GROUP,display 仍落在 display group 0:用户按电源键灭屏后虚拟屏一起 OFF,游戏 Surface 被销毁,任务链报错。本 PR 增加可选后端,默认行为保持不变。变更摘要
VirtualDeviceManager.createVirtualDevice→VirtualDevice.createVirtualDisplay,并先通过 CompanionDevice association 建立关联,使 display 进入独立 display group。提权服务在所有后端上保持 shell(2000),同一主服务最多一个 VDM session。cmd companiondevice associate在 Android 14 只有三个参数,15 增加 device profile 参数,16 再增加 selfManaged 标志;15 及以上使用COMPANION_DEVICE_APP_STREAMINGprofile,并清理旧 profile 的关联。display flags 含TRUSTED,满足 API 36 进入独立 display group 的条件。displayGroupId > 0与Display.STATE_ON;失败按VirtualDisplay → VirtualDevice → Association逆序清理,单步失败不中断后续步骤并保留首个异常。VirtualDisplayManager、三个提权 connector、launcher.c与 R8 规则;未引入 router、registry、factory 或 fallback。验证
sh gradlew :app:testDebugUnitTest通过:1426 tests / 0 failures / 0 errors(含VdmDisplayControllerTest9 个新增用例)。sh gradlew :app:assembleDebug、sh gradlew :app:assembleRelease通过;Release mapping 中RemoteServiceImpl、LogcatCaptureServiceImpl保留原类名与<init>(),未为 VDM 新增 keep。virtual display backend=VDM、association created id=67、displayId=18、displayGroupId=9、uid/euid=2000,MaaCore 连接display_id=18。reusing active displayId=18,MaaCore 均连接display_id=18;VdmDisplay started与association created各只出现一次。association removed、VdmDisplay stopped,dumpsys virtualdevice由 START 变为 CLOSE,无 display、device、association 残留。displayId=19,第二次启动already capturing,MaaCore 连接display_id=19。releasing active displayId=22 before backend change→ 释放 association 与 VirtualDevice → 新建 LegacydisplayId=23。DOZE_SUSPEND,MAA_VD仍为state ON并持续接收输入与 MaaCore 回调。实现依据(AOSP)
VirtualDeviceManager.createVirtualDevice(int associationId, VirtualDeviceParams params):android14-release L172-174、android15-release L205-207、android16-release L208-210cmd companiondevice associate参数逐版本:android14 L82-90、android15 L101-111、android16 L103-113TRUSTED):android14 L1432、android15 L1585、android16 L1869LOCK_STATE_ALWAYS_UNLOCKED与ADD_ALWAYS_UNLOCKED_DISPLAY:VirtualDeviceParams.java L856、VirtualDeviceImpl.java android16 L461-468、android14 L292-294截图 / 日志 / 说明
displayGroupId=0;VDM 虚拟屏displayGroupId=9。物理屏 OFF/DOZE_SUSPEND 后 Legacy 跟随 OFF,VDM 保持 ON。awaitDisplayReady超时路径未在真机触发;「失败不回退 Legacy」目前由单元测试覆盖分派逻辑。LOCK_STATE_ALWAYS_UNLOCKED被系统拒绝后的回退路径未在真机触发。Checklist
Sourcery 总结
新增可选的 VDM 虚拟显示支持,使后台会话保持在独立的显示组中,同时默认保留现有的 Legacy 行为。
新功能:
错误修复:
增强:
测试:
Original summary in English
Sourcery 总结
支持为后台会话选择独立的 VDM 虚拟显示后端,同时默认保留现有的 Legacy 行为。
新功能:
错误修复:
增强功能:
测试:
Original summary in English
Sourcery 摘要
新增可选的 VDM 支持,用于创建独立的后台虚拟显示屏,同时默认保留 Legacy 行为。
新功能:
错误修复:
增强:
文档:
测试:
Original summary in English
Summary by Sourcery
Add optional VDM support for independent background virtual displays while preserving Legacy behavior by default.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: