深入研究DisplayManager.requestDisplayModes(未完)
阅读更多整个系统服务开始
由静态函数开始所有系统服务的启动
1 | public static void main(String[] args) { |
Flutter Platform View 的文章,几乎都是猫大的
TODO
尝试一下 Flutter Platform View 的 HCPP,一个页面加载多个 Platform View,能不能切换层架,平台视图是 SurfaceView 的情况
Flutter 和 Neo 在虚拟显示器的使用其实是不同的,Flutter 借助的是 Presentation 通过 presentation.show() 将 View 显示到虚拟显示器
在这个文档中,Flutter 解释了这个方式实现的一些复杂性,所以目前更多的是使用 Hy
在最新的文档中 https://docs.flutter.dev/platform-integration/android/platform-views, Flutter 已经不把 Virtual Display 当做推荐的使用方式了
Flutter 文档对这两种的性能解释
1 | Platform views in Flutter come with performance trade-offs. |
Platform Views are rendered as they are normally. Flutter content is rendered into a texture. SurfaceFlinger composes the Flutter content and the platform views.
Platform Views are rendered into a texture. Flutter draws the platform views (via the texture). Flutter content is rendered directly into a Surface.
1 | getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); |
这个 displayImePolicy 是0, 也就是说某个显示器标记为0则会在这个显示器上展示键盘,如果是1,则会在主显示器上展示键盘
1.触控板中点进程序的输入窗口,输入法(小键盘)只能被唤醒一次,输入一次以后,再点输入框就没反应了
2.双指触碰触控板以后(本来是为了退出全屏),鼠标的光标消失了,要大退才能重新用。这个问题不是每次都是这样,有时是正常的,但是出bug的频率蛮高的
其中3是比较诡异的,猜测是 VD 触发 resize 后,焦点自动切换到了窗口内的 task 上
在 Neo 的窗口中,渲染的是 Flutter App 还是其他的 App,弹出键盘的行为有差异
可能是 Flutter App 的一些处理导致的,而且很诡异的,Debug 模式的 Flutter App,键盘都能正常弹出,猜想是 Debug 运行的代码执行比较慢,在很玄学的层面,绕开了焦点在多个显示器间切换的问题
Flutter 的虚拟显示器没有焦点,是因为它是普通的应用权限,无法添加VIRTUAL_DISPLAY_FLAG_OWN_FOCUS flag,而 Neo 是 shell 权限,所以并不是 VD 没有焦点,而是焦点不在期望的显示器上
使用 scrcpy 投屏 Neo 中的窗口,点击 Flutter App 输入框,键盘是能弹出的,这个跟 ime_policy 还没关系
测试5验证了这一点
因为此时在于 VD 内的应用交互的时候,并没有同时点击设备的主屏幕,所以此时的焦点就在 VD 上
结论: Neo 点击应用内窗口无法弹出软键盘,是因为事件同时在以下几个地方生效
1.手指正在触摸主屏幕,无论是按下、移动、抬起,Activity 中的 View(FlutterView) 在被点击的时候,就会申请焦点
FlutterView.java#L384C1-L386C35
1 | // FlutterView needs to be focusable so that the InputMethodManager can interact with it. |
2.Neo 将这些事件同时下发到了 VD 中
此时的焦点应该会来回在多个显示器间切换
TODO 用 AAS 验证一下
ACTION_DOWN -> FlutterView(Focus) -> InjectEvent(VD) -> VD’s Task(Focus)
1 | adb logcat -v time | grep --line-buffered -E "InputMethodManagerService|InputMethodManager|onRequestShow|SHOW_SOFT_INPUT|setImeWindowStatus|displayId|InputDispatcher|WindowManager" |
下面是使用 Neo 在内置显示器上点击 ADBKIT 的窗口内的输入框
使用外置显示器,通过触控板,点击 ADB KIT 窗口内的输入框
此时用 scrcpy 直接操作 ADB KIT 所在的显示器,点击输入框
猜测还是焦点导致的
在 Android 13 模拟器上测试,第二次打开 Neo,鼠标消失,原因是软件其实并没有完全退出,怎么解?
所以只在我一加 Pad2 Pro 上出现?
1 | E/WindowManager(3097): isInputMethodClientFocus: display ID mismatch. from client: 106 from window: 0 |
IME(输入法服务)判断请求来源于 display 106,
而它要附着(attach)的窗口 token 属于 display 0(主屏)。
系统检查不通过,于是直接忽略。
scrcpy server 在 该 displayId=106 上注入事件;
同时 焦点窗口(WindowState)与 IME client 的 displayId 一致(都为 106);
因此 isInputMethodClientFocus() 检查通过,键盘可显示。
1 | E/WindowManager: isInputMethodClientFocus: display ID mismatch. from client: 111 from window: 0 |
1 | V/InputMethodManager: Display ID mismatch found. |
1 | void onRequestShow(InputMethodClient client, int reason, boolean fromUser) { |
在 Neo 中点击窗口内应用的输入框
1 | com.nightmare.neo:f85ed5fc: onRequestShow at ORIGIN_CLIENT reason SHOW_SOFT_INPUT fromUser false |
程序调用的代码
1 | InputMethodManager.showSoftInput(view, 0); |
通过在一加 Pad2 Pro 上点击 Neo 内部的 ADB KIT 输入框,键盘无法弹出
AAS 的 onTaskFocusChanged 并未回调,也就是说虽然通过 InjectEvent 注入了点击事件,但是焦点并没有切换到 VD 上(很奇怪)
onTaskStackChanged 关键
代码调用 VD Resize 并不触发 onTaskStackChanged
点击 ADB KIT
1 | · onRecentTaskListUpdated |
点击 ADB KIT 输入框,输入法正常弹出
1 | 10-08 17:18:57.282 W/InputDispatcher( 617): Focused display #0 does not have a focused window. |
1 | · onTaskCreated: taskId=2142, componentName=null |
1 | 10-08 17:51:49.880 W/WindowManager( 3201): obj = Display{#0 state=ON size=3392x2400 ROTATION_90}, willRemove = true |
10-08 17:51:49.916 I/ImeTracker(23156): com.nightmare.adbkit:758ea094: onRequestShow at ORIGIN_CLIENT reason SHOW_SOFT_INPUT fromUser false
10-08 17:51:49.916 D/InputMethodManager(23156): showSoftInput() view=p3.t{b5eeff8 VFE…… .F…… 0,0-1575,1181 #1 aid=1073741824 alpha=1.0 viewInfo = } flags=0 reason=SHOW_SOFT_INPUT
10-08 17:51:49.917 E/WindowManager( 3201): : display ID mismatch. from client: 50 from window: 0
10-08 17:51:49.917 W/InputMethodManagerService( 3201): Ignoring showSoftInput of uid 10315 : com.android.internal.inputmethod.IInputMethodClient$Stub$Proxy@8c4f9d9
无界点击 adbkit 输入框
10-08 18:22:24.728 I/ImeTracker( 9277): com.nightmare.adbkit:21d90019: onRequestShow at ORIGIN_CLIENT reason SHOW_SOFT_INPUT fromUser false
10-08 18:22:24.728 D/InputMethodManager( 9277): showSoftInput() view=p3.t{1d8fa3f VFE…… .F…… 0,0-3392,2400 #1 aid=1073741824 alpha=1.0 viewInfo = } flags=0 reason=SHOW_SOFT_INPUT
· add handler -> DisplayManagerPlugin
· add handler -> ActivityManagerPlugin
· add handler -> ActivityTaskManagerPlugin
· add handler -> FilePlugin
· add handler -> DeviceInfoPlugin
· Display 0 (内置屏幕) IME Policy: 0
· add handler -> InputManagerPlugin
· onTaskCreated: taskId=2144, componentName=null
· onTaskDisplayChanged: taskId=2144, newDisplayId=5
· onTaskCreated: taskId=2144, componentName=ComponentInfo{com.nightmare.adbkit/com.nightmare.adbkit.MainActivity}
· onTaskDescriptionChanged
· onTaskMovedToFront: taskId=2144
· onTaskFocusChanged: taskId=2144, focused=true
· onRecentTaskListUpdated
· onRecentTaskListUpdated
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskStackChanged
· onTaskDescriptionChanged
{
"id": 2144,
"taskId": 2144,
"persistentId": 2144,
"displayId": 5,
"affiliatedTaskId": 0,
"topActivityType": 1,
"topActivityInfo": "ActivityInfo{56262f8 com.nightmare.adbkit.MainActivity}",
"isVisible": true,
"isRunning": true,
"isFocused": true,
"topPackage": "com.nightmare.adbkit",
"topActivity": "com.nightmare.adbkit.MainActivity",
"label": "ADB KIT"
},
{
"id": 2143,
"taskId": 2143,
"persistentId": 2143,
"displayId": 0,
"affiliatedTaskId": 0,
"topActivityType": 1,
"topActivityInfo": "ActivityInfo{8dbe3d1 com.nightmare.neo.MainActivity}",
"isVisible": true,
"isRunning": true,
"isFocused": true,
"topPackage": "com.nightmare.neo",
"topActivity": "com.nightmare.neo.MainActivity",
"label": "TheNeoDesktop"
},
· onTaskFocusChanged: taskId=2150, focused=false
· onTaskFocusChanged: taskId=5, focused=true
焦点没有发生变化
moveTaskToBack 有没可能可以暂停task
在 Neo Activity 中加入以下代码
1 | getWindow().setFlags( |
这个时候,不管是启动 Flutter App 还是其他,通过 adb tap 或者 scrcpy 都无法弹起输入法
所以这个是不是依赖当前 Activity 的焦点?
scrcpy的指针鼠标可以出现在虚拟显示器上
用模拟器试试各个版本的情况
用 Pixso 画一个一模一样的光标
Code LFA 启动速度比较慢
将 VD 的 Surface 置空,Task 会暂停吗?
com/android/server/inputmethod/InputMethodManagerService.java -> showSoftInputLocked -> canInteractWithImeLocked
-> isImeClientFocused ->
com/android/server/wm/WindowManagerInternal.java -> hasInputMethodClientFocus
com/android/server/wm/WindowManagerService.java -> hasInputMethodClientFocus
1 |
|
1 |
|
一加启动失败的打印
Ignoring showSoftInput of uid 10315 : com.android.internal.inputmethod.IInputMethodClient$Stub$Proxy@8c4f9d9
单独打开 adbkit 唤起输入法
10-08 18:19:19.613 I/ImeTracker( 5797): com.nightmare.adbkit:7d8691a: onRequestShow at ORIGIN_CLIENT reason SHOW_SOFT_INPUT fromUser false
10-08 18:19:19.614 D/InputMethodManager( 5797): showSoftInput() view=p3.t{585e08c VFE…… .F….ID 0,0-3392,2400 #1 aid=1073741824 alpha=1.0 viewInfo = } flags=0 reason=SHOW_SOFT_INPUT
10-08 18:19:19.614 D/InputMethodManagerService( 3201): isSecurity: attribute.packageName = com.nightmare.adbkit mCurMethodId = com.sohu.inputmethod.sogouoem/.SogouIME isSecurity = false ( enable = true needShow = false inBlackList = false isExist = true )
无界点击 adbkit 输入框
10-08 18:22:24.728 I/ImeTracker( 9277): com.nightmare.adbkit:21d90019: onRequestShow at ORIGIN_CLIENT reason SHOW_SOFT_INPUT fromUser false
10-08 18:22:24.728 D/InputMethodManager( 9277): showSoftInput() view=p3.t{1d8fa3f VFE…… .F…… 0,0-3392,2400 #1 aid=1073741824 alpha=1.0 viewInfo = } flags=0 reason=SHOW_SOFT_INPUT
1 | 10-08 18:21:12.947 D/SurfaceFlinger( 2161): VRR [SurfaceFlinger] setDesiredActiveMode: displayId: 4630946983774026899, renderRate: 120, vsyncRate: 120, event: 0 |
/android/server/wm/WindowManagerService.java
@Override
public void moveDisplayToTopIfAllowed(int displayId) {
WindowManagerService.this.moveDisplayToTopIfAllowed(displayId);
}
@Override
public int getTopFocusedDisplayId() {
synchronized (mGlobalLock) {
return mRoot.getTopFocusedDisplayContent().getDisplayId();
}
}
我在Ontap 的时候,切换 Task 焦点,但是用的是 AAS 框架而不是 Shizuku Wrapper
其中的 onTaskStackChanged 误导了我
setFocusedTask no
setFocusedRootTask no
需求很简单,将绿联 NAS 内的东西都备份到极空间内,我先尝试了将硬盘取下来放到极空间,挂载不上
然后就是通过 webdav/ftp/smb 等协议来备份,但是最后我开始尝试用 rsync,这样同步二者的时间戳可以保留,然后我更相信这个命令一点
经过之前的教训,我不相信绿联以及极空间任何 UI 上提供的功能
用 rsync 来完成 绿联 NAS 到极空间的备份,中途我在极空间用 FTP 从绿联备份了一些文件,用 rsync 应该能继续备份
就是中途用 FTP,感觉慢得不行,特别到了一些小文件的时候,感觉 rsync 要快很多
绿联和极空间都需要开启 ssh
极空间是 ubuntu,绿联是 debian,绿联连清华源都没设置
绿联 apt install neofetch 需要执行 apt –fix-broken install
然后极空间默认更换了清华的源
ssh 登录后
1 | Welcome to ZOS (GNU/Linux 6.8.1-z4pro+-generic x86_64) |
然后输入 sudo passwd 可以设置极空间 root 密码
随后可以
1 | ➜ ~ ssh -p 10000 root@192.168.31.64 |
apt 版本和源
1 | root@Z4ProPlus-MEDO:~# apt -v |
neofetch
1 | root@Z4ProPlus-MEDO:~# neofetch |
1 | root@Z4ProPlus-MEDO:~# uname -a |
1 | root@Z4ProPlus-MEDO:~# hostnamectl |
1 | root@Z4ProPlus-MEDO:~# cat /etc/os-release |
绿联 ssh 登录后
1 | ➜ ~ ssh nightmare@192.168.31.70 |
1 | nightmare@DXP2800-JECT:~$ neofetch |
1 | nightmare@DXP2800-JECT:~$ cat /etc/os-release |
1 | nightmare@DXP2800-JECT:~$ lsb_release -a |
1 | nightmare@DXP2800-JECT:~$ uname -a |
绿联 sudo passwd 设置 root 密码,然后执行 su 仍然进不到 root
/data_s001/data/udata/real/${phone}
在极空间上执行
1 | rsync -avhP nightmare@192.168.31.70:/home/nightmare/ /data_s001/data/udata/real/${phone}/rsync_test |
尝试将绿联的文件往本地拉
总是跑不通,home/nightmare/ 文件夹是存在的
1 | ug_start_server, check access user: 1000, group: 10 |
但是极空间是可以设置 root 密码的
在绿联上
1 | screen -S backup |
用 screen 命令是为了断开 ssh 还能继续拷贝
ssh-copy-id nightmare@192.168.31.70 作用?
一些 QA
推送:
rsync -avhP -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
含义:
-a 等价 -rlptgoD,保留权限/时间戳/符号链接等
-v 显示传输文件
-h 人类可读大小
-P 显示进度并支持断点续传(含 –partial –progress)
加删除同步(镜像,谨慎):
rsync -avhP –delete -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
后台(screen):
screen -S backup
rsync -avhP -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
Ctrl+A D 分离
screen -r backup 重新进入
后台(nohup 日志):
nohup rsync -avhP -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/ > /home/nightmare/rsync_$(date +%Y%m%d_%H%M%S).log 2>&1 &
-a 已包含 -t,完成后用 stat 对比:
stat /home/nightmare/某文件
stat /data_s001/data/udata/real/${phone}/rsync_test/某文件
Modify 行一致即成功。
若看到 failed to set times 说明目标权限或挂载阻止设置时间。
默认判定:文件大小 + 修改时间
两者都相同直接跳过,不读取文件内容。
需要内容级别核对时临时用 –checksum(-c)。
首次全量同步完成后执行(不写入,仅验证,慢):
rsync -avhP -e “ssh -p 10000” –dry-run -ic /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
如果没有出现以 >f 开头的文件行,表示内容一致。
之后日常继续用不带 -c 的命令(快):
rsync -avhP -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
rsync -avhP -e “ssh -p 10000” –dry-run -i /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
-i 输出变化标志:
f.st…… 文件大小和时间变
f+++++++++ 新文件
f..t…… 仅时间不同
-c 基于内容校验和决定是否需要同步,需读取两端所有文件,慢。
组合 –dry-run -ic 用于严格一次性内容验证。
仅列出真正传输的文件(时间 + 标志 + 名字):
rsync -aP -e “ssh -p 10000” -i –out-format=’%t %i %n’ /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
写日志:
rsync -aP -e “ssh -p 10000” -i –out-format=’%t %i %n’ –log-file=/home/nightmare/rsync_transfer.log /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
排除缓存与无意义文件:
rsync -avhP -e “ssh -p 10000” –exclude ‘.cache/‘ –exclude ‘.DS_Store’ /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
/home/nightmare/push_backup.sh:
#!/usr/bin/env bash
rsync -avhP -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
后台版本 /home/nightmare/push_backup_bg.sh:
#!/usr/bin/env bash
LOG=”/home/nightmare/rsync_$(date +%Y%m%d_%H%M%S).log”
echo “Log => $LOG”
nohup rsync -avhP -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/ > “$LOG” 2>&1 &
echo “PID=$!”
stat /home/nightmare/Release.zip
stat /data_s001/data/udata/real/${phone}/rsync_test/Release.zip
需要让目标与源完全一致(删除目标多余文件)时再加:
rsync -avhP –delete -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
先用 –dry-run 演练:
rsync -avhP –dry-run –delete -i -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
1 初次同步:
rsync -avhP -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
2 一次性内容验证:
rsync -avhP –dry-run -ic -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
3 日常增量:
rsync -avhP -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
4 定期快速检查:
rsync -avhP –dry-run -i -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
5 需要镜像:
rsync -avhP –delete -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
sent 424.68G bytes received 63.57K bytes 136.93M bytes/sec
total size is 1.95T speedup is 4.59
rsync -avhP -e “ssh -p 10000” /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/
rsync -avhP -e “ssh -p 10000” /Volumes/4T-雷克沙/2T海康威视 /tmp/zfsv3/sata15/17628626769/data/2T-海康威视
rsync -avhP /tmp/zfsv3/sata12/17628626769/data/ /tmp/zfsv3/sata15/17628626769/data/
NAS 上装 Code-server
rsync -avhP -e “ssh -p 10000” root@192.168.31.64:/tmp/zfsv3/sata11/17628626769/data/ /home/nightmare/
rsync -avhP -e “ssh -p 10000” root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/ /home/nightmare/
注意: 2025.07.11 的版本重构了很多的底层,需要卸载重装才行
修复
优化
新增
已知Bug
不是故意写出来了,最近在大量重构底层,不知道什么原因导致的,后续会修复
修复
优化
新增
优化
新增
修复
注意
接下来的计划
最近在究极攻克 termux 和 termux-x11,准备直接集成到 NeoDesktop,后面也许就有开箱即用的 PC 软件,可以有开箱即用的 PC 游戏
sh /storage/emulated/0/Android/data/com.nightmare.neo/files/start.sh来激活目前任务无法调整过层级关系,等下一个版本
这个版本重新写了触控事件的下发方式,可能会存在异常触控失效的情况,但将触控向下支持到了 Android12
目前仍发现的问题:
[重要更新]
[前言]
这个版本更新日志前面的一些碎碎念,在 Sula 收费以前,我希望我所有的开发都是自由的
大家也不应该以“这是我的执念”来绑架我
我会尽全力不让 Sula 很快就走向终点,后面还有非常多的事需要做,编写技术分享文章(其中包括画各种架构图),编写官网,国际化,录制B站,YouTube 视频等,这些都应该会对 Sula 有正向的输入,一旦有更多的人发现并使用这个软件,我自身的投入也会更有动力
目前人在上海,花了几天时间在酒店,实际上更改的代码远不止日志中的内容
[XREAL那事儿]
跟XREAL合作闹崩还没有结束,起初我就是想问他们要 Android API,可以实现调用后,将眼镜切换到 2D 120hz 的模式,这样 Sula 运行的软件也都有 120 hz,60 -> 120 的提升是非常明显的,但是后来他们不仅不愿意给,还以极高傲的态度,让此事成了最后的样子
后续我会发一个完整的视频,只是觉得,这些事应该常有发生,只是很多人因为很多原因,不敢说,不能说,我现在孑然一身,亲人就剩一个姐姐,没有什么朋友,死了便死了
[新版本建议]
目前仍然需要同时依赖 Sula Server 的激活和 Shizuku
我后面会想办法可以实现二选一即可
建议打开设置,将 PlatfromView 的模式切换到 hybridComposition
这个切换是做了本地存储的,下次启动仍然有效
这样虽然窗口层叠关系会有问题,但是性能是最好的
后续我会尝试解决这种模式下的窗口层叠问题
[更新日志]
后续会支持外接物理鼠标
多页面跳转问题解决了,目前没有多余的精力开发一个引导页面
这个版本开始需要 Shizuku + Sula Server
后续只用一种方式即可
NeoDesktop 致力于将锤子 TNT 或者三星 Dex 这种桌面模式变得通用,不限制品牌,不硬限制安卓版本
解决现在带 DP 输出视频设备仅能投屏的问题
也可以搭配投影仪,AR 眼镜使用
这是一个锤子 TNT 桌面模式的一张图

也是 NeoDesktop 的一个目标吧
这个软件的构思在2023年10月30,大概是小米14发布的时候,因为小米14支持 DP 输出
在经过了比较多小版本的迭代,目前 ND 不用同时依赖自由激活和 Shizuku ,当前仅可通过自有服务激活
感谢 PzHown 提供的软件图标
经测试支持 Android 11 - 15
安卓12及以下,比较多的 App 会限制存在 ND 的窗口中,简单说就是比较不可用,但 Moonlight 这类应该没问题
推荐使用 ADB KIT 激活,比较方便,全平台都有,支持有线、无线、无线配对等方式,激活的辅助设备不管是 Windows、Linux、macOS,甚至是 Android,通过数据线,点几个按钮即可激活
激活前需要先启动一次软件
启动软件也有激活的引导
下载 ADB KIT: https://nightmare.press/YanTool/resources/ADBTool/?C=N;O=A
1 | adb shell sh /storage/emulated/0/Android/data/com.nightmare.sula/files/start.sh |
1.有线连接设备
建议用有线连接设备,因为传统无线、无线配对这两种方式都还需要额外的学习

连接后点击对应的设备进入控制面板界面
2.激活 NeoDesktop
点击顶部的服务激活,点第一个按钮激活 Sula 即可完成激活,输出长这样即可

请勿使用晨钟酱的 ADB 工具箱的终端进行激活
由于晨钟酱的终端未使用 PTY,看不到实时的流,无法确认启动状态
能到这一步,我相信你已经有了一定的基础
首先需要启动一次 Sula,然后用 adb 命令启动服务
1 | adb shell sh /storage/emulated/0/Android/data/com.nightmare.sula/files/start.sh |
启动 NeoDesktop 的情况下,手指在左侧上下滑动可以切换触控板、空间鼠标
这里后续补一个 Gif
在这个软件诞生的过程中,我感受到来自 XREAL 产品以及运营的高傲,所以 NeoDesktop 不将视频 XREAL 的眼镜
看下XREAL的说法,他们已经把这定义为一种不可能
“首先,在正常的厂商系统中,应用与应用之间的通讯与访问时被禁止的。这本身是为了防止应用之间篡改或者盗取数据而做的防护措施。但这个隔离系统所产生的问题就是——如果我们想要通过Nebula APP这个身份将另一个应用在AR空间中拉起,在厂商未授予高级系统权限的前提下是无法实现的。”
但我至少让这成为了可能,同时我不建议在 XREAL 眼镜中使用这个软件
1.为什么需要激活?
ND 的窗口管理,事件下发都是调用系统级别的 API 来完成的,普通的 App 是不具备找个权限的,而如果仅限制为 root 用户可用,则更高的加大了使用门槛,更何况现在主流的设备都不能解锁
所以采用了自有服务激活的方式,一次激活设备不重启,不插拔 USB,大概率是不会掉的,如果掉了借助 ADB KIT,也能快速的激活
实际排查过程中,发现并不是阻塞了,而是报错了
1 |
|
int flags = UID_OBSERVER_GONE | UID_OBSERVER_IDLE | UID_OBSERVER_ACTIVE;
这行代码就报错,原因是因为没有在 Android 工程中
subprojects {
plugins.withId(‘com.android.base’) {
plugins.apply(‘dev.rikka.tools.refine’)
android {
compileSdk = 33
defaultConfig {
minSdk = 23
targetSdk = 33
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
aidl true
}
}
}
}
在我个人的认知,需要解释 Shizuku 这个东西,需要从 root 的原理开始
在 Android 上,执行 su 命令并不是切换了当前的 uid,而是打开了一个阻塞的进程,新的进程是 root 权限,然后读取用户的输入,再将输入传递到 su 的子进程中
这样会比较慢,并且,一切的权限都只能提升命令行
而 Shizuku 的诞生,让 root 或者 adb 权限直接提升 Android 的代码变成了可能,这就是 Shizuku 所干的事
并且它的速度会比传统的 su 命令会更快
但经过比较大量的使用和集成,发现里面的魔法远比我想象的多
导包
def shizuku_version = (the version above)
implementation “dev.rikka.shizuku:api:$shizuku_version”
// Add this line if you want to support Shizuku
implementation “dev.rikka.shizuku:provider:$shizuku_version”
这些都没什么说的,都是一些简单的操作,但是我并不准备从 demo 开始介绍它的魔法所在
因为 demo 本身的一些设计,让你很难找到其中关键的魔法代码
在这种模式下,你的任何代码,不仅仅是获取某些 Services,甚至 C/C++ 代码,都可以被提权
这是最魔法的地方,我也正式因为排查问题,才一点一点,弄明白了一点原理
好久不见,我是梦魇兽,距离我上一次写文章已经是3年前
这篇会当我回归技术社区的一个开篇吧
其实中间我还一直在自己的博客写文章,虽然频率不高
本人背书:
本科大二收到比较多大厂的面试邀请,大三下滴滴实习,大四上拿到正式 Offer,入职半年 D5 升 D6,毕业后正式工作了一年多后裸辞,前滴滴高级架构
(也许这也是一个噱头,但我会尽可能减少)
如果你是目前正在滴滴工作,可以在内网社区看到我离职的文章
这是一篇一样的在自己博客的文章 再见滴滴研发,你好梦魇兽
简单说,还是因为我感受到这个环境的不公,我厌恶这一切
其中很多想讽刺的内容,写不出来,也是因为所谓的不公
是速享、无界、ADB KIT、Code FA 等软件的开发者
现有开源代码大概在 10W 行+
目前裸辞已经半年多了
今天分享一个开发挺久的一个库,Android API Server
(跟随流量至上的原则,或许是这样的标题,比 Shizuku 还强?历经6年开发,吐血开源!)
这是一个长期在自用的库,但后来发现它的应用越来越广,涉及到我的 FastShare、ADB KIT、Uncon 等
最近几天对整体架构进行了重构
起初这只是一个很小的库,在很早期我需要在 Flutter 侧获得 Android API 的一些信息时,需要编写大量的 MethodChannel
并且随着后来 ADB KIT、Uncon 等在功能上的需求,我需要在 PC 端也能获取到这些信息
并且我希望在使用上,不管是从 PC 端获取,还是安卓本地获取,都能保持高度一致
于是经过比较大量的 Scrcpy 和 Shizuku 这两个我最仰慕的开源项目的学习
才有了 Android API Server(AAS) 这样的解决方案
后来发现对这个库的需求越来越多,例如支持快速集成到我的速享、ADB KIT、Uncon 等
以及支持 Dex 模式等
在今天对整体架构重新进行了设计,正式作为一个对外的开源
也许听起来很复杂,没关系,我们后面会通过简单的示例代码来看一下他的功能
AAS 是一个为 Android 设备提供 RESTful API 的服务器。它基于 HTTP 协议,可以被任何支持 HTTP 的客户端访问。它设计轻量且易于使用,支持热插拔,你可以通过很简短的代码,来让 AAS 加载你自定义的插件
支持上层框架为 Web 或者 Flutter 或者其他任意不能直接访问 Java 的框架中使用
例如在 Flutter 中,我们几乎需要使用 MethodChannel 来访问安卓的 API
使用 MethodChannel 实现后,无法支持在 Flutter Web 中访问安卓的 MethodChannel
AAS 提供了封装好的开箱即用的 Flutter Plugin,或者你可以根据 API.md 实现任意语言编写的客户端

对上层的应用来说,只有 Address 和 Port 的感知,它不在乎对方是哪种模式运行的
你可以在任何地方,任何设备上,通过 HTTP 获取安卓的信息
由于 HTTP 并不安全,所以 AAS 内置了一个简单的接口鉴权,来防止端口扫描恶意调用
这是 Flutter 编写的示例代码,展示了内置的一些插件和 API 的使用


基于 HTTP 的好处是,你可以通过这样的代码来获取一个 App 的图标(Flutter)
1 | AASClient aasClient = AASClient(); |
AASClient 是多实例,所有的 API 被封装到 AASClient 下
多实例可以让同一个页面加载不同设备的信息,例如 Uncon
获取所有应用信息代码
1 | AppInfos infos = await getAllAppInfos(isSystemApp: false); |
这看起来没什么,但我们可以将这个 Example 编译到 Mac 端
我们只需要更改端口号
1 | AASClient aasClient = AASClient(port: Platform.isMacOS ? 15000 : null); |


这就是在 API 使用上的一致性,我有各种同时运行在 App 内的页面,只需要更改端口号,就可以运行在 PC 端
当然 PC 端需要以 Dex Mode 启动 AAS
并且在这种模式下,我们可以有更多的 API 支持
可以获取到安卓的后台截图,这个功能应该是极少见到的
示例代码中包含了所有的 API 使用方法,示例代码是最好能理解 AAS 的方式
完整代码见 Flutter Example
通过这些例子你应该发现了,PC 端加载对应页面的时候,安卓端的服务是怎么启动的?
AAS 有两种启动模式
这种情况下,AAS 拥有真实的 Activity Context,对于获取应用列表,同普通安卓本身访问 API一样,需要申请权限
启动脚本在 build_and_run.sh
这种模式,会先将 java 编译成 class,再由 dx 或 d8 工具转换成 dex 文件
通过 adb 运行 app_process 启动 dex
这种模式带来的好处是,我们能使用的权限更多,例如获取后台任务缩略图,创建虚拟显示器(带 Group 的)
所有 java 的权限为 shell(uid 2000),你无需再为获取应用列表,创建虚拟显示器等单独申请权限
我们可以通过为连接到 PC 的设备启动这个服务,再通过 adb forward 获得通信的端口(auto.sh 中带有端口转发)
接下来,你仍然只需要像这样就获得 App的图标
1 | AASClient aasClient = AASClient(port: port); |
你还可以自己实现各种各样的 API ,来获得远超 adb 命令行的功能,例如图标,后台应用截图,adb 命令本身不支持
提供 android_api_server_client 来快速的让 Flutter App 拥有这个能力,无需手动启动服务,AAS 随 Flutter Plugin 注册而启动,直接创建 AASClient 则会使用 Flutter Plugin 中启动的端口
1 | dependencies: |
然后直接使用封装好的 Dart API
1 | AASClient aasClient = AASClient(); |
如果你需要在 PC 上访问同样的接口,通过 Dex Mode,你只需要更改端口
1 | AASClient aasClient = AASClient(port: 15000); |
假如我目前有一个 Flutter 编写的展示应用列表的界面是这样
现在我想这个界面在 PC 上展示,亦或者在 Web 中展示
启动 Dex 后,我只需要修改端口号即可
1 | AASClient aasClient = AASClient(port: Platform.isMacOS ? 15000 : null); |
实际上,这样的模式已大量的在无界、速享、ADB KIT中使用
其中的文件管理器、应用列表、任务列表,都是完全的同一份代码,仅仅是端口号不一样
根据仓库的 Tag 版本,引入对应的依赖
1 | implementation 'com.github.nightmare-space.android_api_server:aas_integrated:v0.1.27' |
1 | AASIntegrate aasIntegrate = new AASIntegrate(); |
所以通过这两种模式的了解,在 PC 端获取安卓的信息,通常需要 ADB KIT 或者 Uncon 中一样,需要处理 Dex 的启动流程
详见 ADB KIT
这是获取安卓后台快照的完整插件
1 | public class ActivityTaskManagerPlugin extends AndroidAPIPlugin { |
相关解释
route 方法返回的是这个插件的路由handle 方法是这个插件的处理方法,这里是获取后台任务的缩略图,返回对象参考 NanoHTTPD.Response如果是同一类插件,建议只实现一个 AndroidAPIPlugin
然后增加 param 来区分,例如 action
1 | Object result = ReflectionHelper.invokeHiddenMethod(Object object, String methodName, Object... args); |
1 | Object result = ReflectionHelper.getHiddenField(object, "$name"); |
1 | Context context = ContextStore.getContext(); |
解释一下为什么放在了单例里面,由于 AAS 有两种模式,两种模式下 Context 的获取方式是不一样的,Activity Mode 是直接储存的 Activity Context,Dex Mode 是通过反射获取的 Context
ServiceManager来自 aas_hidden_api
1 | ServiceManager.getService("activity_task"); |
例如我们要一个 PackageManager 的 Service
IPackageManager来自 aas_hidden_api,PackageManager是系统的 API
1 | IPackageManager pms = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); |
这两种方法的选择要根据场景来,如果 IXManager 能实现需求,就用这种,因为在 Dex Mode 中,Context 是不完整的,但是例如 DisplayManager 这种,IDisplayManager 的 API 没有 DisplayManager 好用
更多获取方法详见 SystemServerApi
1 | AndroidAPIServer server = new AndroidAPIServer(); |
1 | server.registerPlugin(new PackageManagerPlugin()); |
1 | public class AASIntegrate { |

可以在 PC 端启动安卓的 App,配合应用流转使用,可实现无需解锁手机,即可在 PC 上运行安卓上的软件
视频极速缓冲播放,100G 的文件都能随意拉动进度条
无需安卓安装额外 App,仅需要开启 USB 调试
/activity_manager?action=start_activity: 打开一个 Activity/activity_manager?action=stop_activity: 关闭 Activity/activity_manager?action=get_app_activities&package=${package}: 获取 App 的 Activty/activity_manager?action=get_app_detail&package=${package}: 获取 App 的详细信息/activity_manager?action=get_all_app_info&is_system_app=false: 获取所有 App 信息/activity_manager?action=get_tasks: 获取后台的 Task 信息/activity_manager?action=app_main_activity&package=${package}: 获取 App 的 Main Activity/task_thumbnail?id=${id}: 获取 Task 的截图/display_manager?action=get_displays: 获取所有的 Display 信息/display_manager?action=create_virtual_display&width=1080&height=1920&dpi=320: 创建虚拟显示器/package_manager?action=get_permissions&package=${package}: 获取 App 的权限/package_manager?action=get_icon&package=${package}: 获取 App 的图标/file?action=dir&path=\${dir_path}: 获取目录下的文件信息/file?action=file&path=\${file_path}: 获取文件,会直接返回整个文件,用浏览器访问则会预览这个文件,如果是视频则支持极速缓冲播放,支持断点续传gradle-8.0 + com.android.tools.build:gradle:8.1.0 组合发布 Jitpack 引入依赖后无法导包,这个组合发到 Jitpack 上,没有 aar,后来用 gradle-7.6.4 + com.android.tools.build:gradle:7.4.2 进行的发布不要给我私信参加任何活动
不要给我任何狗屁创作激励,社区就是被这狗屁激励搞砸的
我也不知道这次回归,能持续多久,也许被某一条评论恶心到,然后再次离开?
世间的变数太多了
几句话与大家共勉
我曾一度讨厌任何变成以流量为目的社区
我们编写文章,记录开发历程,分享经验,变成了,我要如何制造噱头,如何编写更有噱头的标题
怎么改文章,才会有更多流量
包括社区各种各样的活动,无一不是在告诉你,你的文章的唯一目的,就是为了更多的流量
其他的技术本身,文章质量,都是狗屁
反驳拉黑
也有一些人说羡慕我现在的生活,但这都是取舍罢了
你想要什么
你准备好失去什么
时间往前,交换进行
世界可总是不公平的,也许你只得到了一点,但你会失去很多
循环往复
下次见