Agent Skills › dotnetdreamer/open-android-dex › dex-window-debug

dex-window-debug

GitHub

Android DeX窗口调试技能,通过wmd daemon枚举、移动任务及验证SurfaceFlinger状态,解决标题栏、自由窗口几何和层级问题。

.claude/skills/dex-window-debug/SKILL.md dotnetdreamer/open-android-dex

触发场景

调试DeX桌面窗口状态 检查自由窗口几何与z-order 验证Chrome在SurfaceFlinger中的渲染

安装

npx skills add dotnetdreamer/open-android-dex --skill dex-window-debug -g -y
更多选项

非标准路径

npx skills add https://github.com/dotnetdreamer/open-android-dex/tree/main/.claude/skills/dex-window-debug -g -y

不安装直接使用

npx skills use dotnetdreamer/open-android-dex@dex-window-debug

指定 Agent (Claude Code)

npx skills add dotnetdreamer/open-android-dex --skill dex-window-debug -a claude-code -g -y

安装 repo 全部 skill

npx skills add dotnetdreamer/open-android-dex --all -g -y

预览 repo 内 skill

npx skills add dotnetdreamer/open-android-dex --list

SKILL.md

Frontmatter
{
    "name": "dex-window-debug",
    "description": "Inspect and drive Android window state on the Open Android DeX desktop display — build\/push\/run the uid-2000 wmd daemon, find the live scrcpy display, list and move tasks, reserve caption strips, and verify chrome in SurfaceFlinger. Use when debugging titlebars, window chrome, freeform task geometry, z-order, or the CaptionService accessibility overlay."
}

Debugging windows on the DeX desktop display

Everything here talks to openandroiddex-wmd, the shell-uid daemon. It exists because MANAGE_ACTIVITY_TASKS is what lets anything enumerate, move, re-order or inset another app's task, and the launcher (a normal app) can never hold it.

Run commands from PowerShell, not Git Bash

Git Bash rewrites /data/local/tmp/... into a Windows path and the command fails with inaccessible or not found. Every snippet below assumes PowerShell.

Build, push, start the daemon

cmd /c "openandroiddex-wmd\build.cmd push"     # javac + d8 + adb push, no Gradle
adb shell "pkill -f WmDaemon"
adb shell "setsid sh -c 'CLASSPATH=/data/local/tmp/wmd.dex exec app_process /system/bin com.ccrstech.openandroiddex.wmd.WmDaemon > /data/local/tmp/wmd.log 2>&1' &"
adb shell "cat /data/local/tmp/wmd.log"        # expect: wmd listening on 127.0.0.1:7191 uid=2000

setsid is required: a plain background job dies with the adb shell that launched it.

Talk to it

adb shell "printf 'PING`nBYE`n' | toybox nc 127.0.0.1 7191"

Never hard-code the display id — it changes every scrcpy session, and dead sessions leave virtual displays behind that look identical to the live one. Ask the daemon:

adb shell "printf 'DESKTOP com.ccrstech.openandroiddex.launcher`nBYE`n' | toybox nc 127.0.0.1 7191"

Protocol: PING · DESKTOP <pkg> · LIST <display> · STRIP <display> <task> <px> [inset] · UNSTRIP · MOVE <display> <task> <x> <y> · BOUNDS <display> <task> <l> <t> <r> <b> · FRONT · BACK · FOCUSABLE <display> <task> <0|1> · CLOSE <task> · BYE. The phone touchpad's input (doc/phone-touchpad.md): MOUSE <display> <x> <y> <buttons> · WHEEL <display> <x> <y> <h> <v> · LEAVE <display> · KEY <display> <keycode> · FOCUS <display> · TOP <display> · CURSOR <display> <x> <y> <hx> <hy> <png> (the arrow as a root SurfaceFlinger layer, kept only while that connection is open) — see the header of WmDaemon.java for the full list. Where the pad's arrow is: dumpsys SurfaceFlinger | grep -A4 "Layer (Touchpad pointer" (pos=).

LIST returns topmost first, 17 whitespace fields:

TASK ix id display mode actType vis  l t r b   al at ar ab   pkg activity
                                     └ bounds ┘ └ appBounds ┘

appBounds.top > bounds.top means a caption strip is reserved. Field 15 is the package — reading 14 instead renders window titles as a y coordinate.

From the PC instead of the device

adb forward tcp:7191 tcp:7191     # then talk to 127.0.0.1:7191 from Windows
adb forward --remove tcp:7191

Measured RTT over USB: median 2.57 ms, p95 5.30 ms.

One-shot probes without the daemon

adb shell "CLASSPATH=/data/local/tmp/wmd.dex app_process /system/bin com.ccrstech.openandroiddex.wmd.Probe <cmd>"

caps (what is reachable at this uid) · list <display> · move <display> <task> <dx> <dy> <legacy|transition|resize> · strip <display> <task> <px> · stack <display> <low> <high> · focusable · sig <wct|wms|atm|woc|CLASS> [filter] — dump real method signatures, because hidden-API shapes vary by build and OEM. Guessing one throws inside system_server.

Verify chrome actually renders

dumpsys bounds only prove the Configuration changed. To prove pixels, read the layer:

adb shell "dumpsys SurfaceFlinger --list" | Select-String "SurfaceControlViewHost#\d+ parentId"
adb shell "dumpsys SurfaceFlinger" | Select-String "Layer \[<id>\]" -Context 0,3

Read carefully:

  • visible reason= buffer=… — real pixels. invisible reason=hidden by parent or layer flag means the surface exists but nothing shows it.
  • SF prints bounds={left,top,bottom,right} — not l,t,r,b.
  • toDisplayTransform={ tx= ty= } on Task=N is where the window actually is. Use this, not task bounds, to prove a move landed.
  • Per task the platform maintains Decor container of Task=N (z=30000, child of the Task) and sometimes Caption of Task=N. Anything parented inside the app window is below those regardless of its own z.

To check our caption covers One UI's exactly, compare buffer size against composited rect — the two columns disagree when a parent surface crops us:

adb shell "dumpsys SurfaceFlinger" | Select-String "705.0    40.0|648.0    40.0"
   src w/h    |  composited l t r b  | layer
  705.0  40.0 |  397  27 1102  67    | Caption of Task=1005   <- One UI's
  705.0  40.0 |  397  27 1102  67    | #5162                  <- ours, exact cover
  705.0  40.0 |  397  27  693  67    | #13970                 <- ours, CROPPED to a pane

A short coveredRegion on the platform's caption layer is the sliver of old title bar still showing.

A layer with no parentId is an attach that silently failed — the surface exists, is never composited, and nothing reports an error:

adb shell "dumpsys SurfaceFlinger --list" | Select-String "SurfaceControlViewHost#"
# good: SurfaceControlViewHost#22611 parentId=22489 z=2147483647
# bad:  SurfaceControlViewHost#22558 z=2147483647          <- orphaned

Usual cause is the wrong InputTransferToken: a display overlay needs a NEW InputTransferToken(), a window overlay needs one belonging to a live window. Caption churn is invisible from the UI — check files/caption.log grows (wc -l) rather than trusting how it looks; a caption being destroyed and rebuilt 30×/sec looks completely normal on screen.

Caption service (Route A)

adb shell "appops set com.ccrstech.openandroiddex.launcher ACCESS_RESTRICTED_SETTINGS allow"
adb shell "settings put secure enabled_accessibility_services com.ccrstech.openandroiddex.launcher/com.ccrstech.openandroiddex.launcher.CaptionService"
adb shell "settings put secure accessibility_enabled 1"
adb shell "run-as com.ccrstech.openandroiddex.launcher cat files/caption.log"

run-as only works if the launcher was built with -PdebuggableLauncher — the shipped APK is non-debuggable so Android's 16 KB page-size dialog stays away.

Installing the APK clears both the appop and the enabled-services list, and so does am force-stop — so apply the grant after the launcher is (re)started, never before. Force-stop does NOT clear the appop, so the half-wiped state reads as "permission fine, service just isn't running". This is the single most common reason captions "stop working". adb::enable_caption_service does this automatically on every npm run tauri dev; the commands above are for a hand-installed APK, or to check what the automatic path did.

Confirm the service is actually alive before debugging anything else — settings get / appops get returning null / No operations means it never started, and there will be no error anywhere to find:

adb shell "settings get secure enabled_accessibility_services"
adb shell "appops get com.ccrstech.openandroiddex.launcher ACCESS_RESTRICTED_SETTINGS"

If the setting reads back null immediately, it is being filtered, not rejected. Check for an MDM allowlist:

adb shell "dumpsys device_policy | grep -B3 'accessibility services'"
adb shell "pm list users"

accessibility services: empty under a Profile Owner is an empty allowlist: no third-party a11y service can run. Confirm by writing a system service — if that sticks and yours does not, it is policy, not code.

Prefer the breadcrumb file over logcat: app logs from this uid are unreliable on One UI. no host task=… candidates=… in that file lists every window pickHost rejected, with rects — that is the tool for "the service runs but captions=0".

Verify the device is running the APK you just built

Before debugging any behaviour that "did not take". The dev app deploys the copy staged under target/<profile>/resources/bin, not resources/bin, and Tauri does not always refresh it — so a stale APK gets reinstalled over a sideloaded one on every run:

(Get-FileHash open-android-dex-tauri\src-tauri\resources\bin\openandroiddex-launcher.apk -Algorithm MD5).Hash
(Get-FileHash open-android-dex-tauri\src-tauri\target\debug\resources\bin\openandroiddex-launcher.apk -Algorithm MD5).Hash
adb shell "pm path com.ccrstech.openandroiddex.launcher"     # then md5sum that path

All three must match. node open-android-dex-tauri\scripts\build-launcher-apk.mjs rebuilds and refreshes both; it warns if a running dev instance has the staged file locked.

Leave the device clean

adb shell "settings put secure accessibility_enabled 0"    # strips auto-release on service destroy
adb shell "printf 'UNSTRIP <display> <task>`nBYE`n' | toybox nc 127.0.0.1 7191"
adb shell "pkill -f WmDaemon"

A setAppBounds override outlives its inset source and its owner binder — clear it explicitly or apps stay short by the strip height.

Traps that cost hours

  • Never BOUNDS a task to an empty rect and never clear its bounds: matchParentBounds() flips true and every task below it is paused or stopped.
  • Do not prototype nesting a Task under a Task that holds an ActivityRecord — unchecked cast in resumeTopActivityUncheckedLocked kills system_server.
  • setprop persist.wm.debug.* is SELinux-denied to shell, and SystemUI cannot be killed (Knox). Flags latched at SystemUI start need a reboot.
  • settings put global override_desktop_mode_features ALL_DISABLED is accepted but may disable freeform along with desktop mode. Do not leave it set.

版本历史

  • 8a4803a 当前 2026-11-13 18:24

同 Skill 集合

.claude/skills/apk-reverse/SKILL.md
.claude/skills/mobile-reverse/SKILL.md
.claude/skills/reverse-engineering/dsl-vm-reverse/SKILL.md
.claude/skills/reverse-engineering/SKILL.md

元信息

文件数
0
版本
8a4803a
Hash
37bfffba
收录时间
2026-11-13 18:24

首页 - Wiki
Copyright © 2011-2026 iteam. Current version is 2.156.0. UTC+08:00, 2026-11-14 02:24
浙ICP备14020137号-1