Neo键盘无法弹出记录

Flutter Platform View 相关资料

TODO
尝试一下 Flutter Platform View 的 HCPP,一个页面加载多个 Platform View,能不能切换层架,平台视图是 SurfaceView 的情况

Flutter 对虚拟显示器的处理方式

Virtual-Display.md

Flutter 和 Neo 在虚拟显示器的使用其实是不同的,Flutter 借助的是 Presentation 通过 presentation.show() 将 View 显示到虚拟显示器

在这个文档中,Flutter 解释了这个方式实现的一些复杂性,所以目前更多的是使用 Hy

在最新的文档中 https://docs.flutter.dev/platform-integration/android/platform-views, Flutter 已经不把 Virtual Display 当做推荐的使用方式了

Performance

Flutter 文档对这两种的性能解释

1
2
3
4
5
6
7
8
9
10
11
12
13
Platform views in Flutter come with performance trade-offs.

For example, in a typical Flutter app, the Flutter UI is composed on a dedicated raster thread. This allows Flutter apps to be fast, as the main platform thread is rarely blocked.

While a platform view is rendered with hybrid composition, the Flutter UI is composed from the platform thread, which competes with other tasks like handling OS or plugin messages.

Prior to Android 10, hybrid composition copied each Flutter frame out of the graphic memory into main memory, and then copied it back to a GPU texture. As this copy happens per frame, the performance of the entire Flutter UI might be impacted. In Android 10 or above, the graphics memory is copied only once.

Virtual display, on the other hand, makes each pixel of the native view flow through additional intermediate graphic buffers, which cost graphic memory and drawing performance.

For complex cases, there are some techniques that can be used to mitigate these issues.

For example, you could use a placeholder texture while an animation is happening in Dart. In other words, if an animation is slow while a platform view is rendered, then consider taking a screenshot of the native view and rendering it as a texture.

Hybrid Composition

Platform Views are rendered as they are normally. Flutter content is rendered into a texture. SurfaceFlinger composes the Flutter content and the platform views.

  • best performance and fidelity of Android views.
  • Flutter performance suffers.
  • FPS of application will be lower.
  • Certain transformations that can be applied to Flutter widgets will not work when applied to platform views.

Texture Layer (or Texture Layer Hybrid Composition)

Platform Views are rendered into a texture. Flutter draws the platform views (via the texture). Flutter content is rendered directly into a Surface.

  • good performance for Android Views
  • best performance for Flutter rendering.
  • all transformations work correctly.
  • quick scrolling (e.g. a web view) will be janky
  • SurfaceViews are problematic in this mode and will be moved into a virtual display (breaking a11y)
  • Text magnifier will break unless Flutter is rendered into a TextureView.

Scrcpy 可以将输入法移动到虚拟显示器的实现方案

1
getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy);

这个 displayImePolicy 是0, 也就是说某个显示器标记为0则会在这个显示器上展示键盘,如果是1,则会在主显示器上展示键盘

触控板反馈

1.触控板中点进程序的输入窗口,输入法(小键盘)只能被唤醒一次,输入一次以后,再点输入框就没反应了
2.双指触碰触控板以后(本来是为了退出全屏),鼠标的光标消失了,要大退才能重新用。这个问题不是每次都是这样,有时是正常的,但是出bug的频率蛮高的

测试结果

  • 1.Flutter App 通过 Neo 打开,点击输入框无法打开键盘 ❌
  • 2.其他 App 通过 Neo 打开,能打开键盘 ✅
  • 3.ADB KIT Debug 模式,点输入框,键盘能弹,Release 模式不能弹,Neo Resize 窗口后,点击又能弹一次 ❌
  • 4.用 scrcpy 显示对应的 display,点击输入框能弹 ✅
  • 5.用 adb shell input -d 147 tap 300 300,点击 Neo 内的 Flutter App(ADB KIT),能弹(关键) ✅

其中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
2
3
// FlutterView needs to be focusable so that the InputMethodManager can interact with it.
setFocusable(true);
setFocusableInTouchMode(true);

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 的窗口内的输入框

  • Android 16 软键盘 ✅
  • Android 15 软键盘 ✅
  • Android 14 软键盘 ✅
  • Android 13 软键盘 ✅

使用外置显示器,通过触控板,点击 ADB KIT 窗口内的输入框

  • Android 16 软键盘 ❌
  • Android 15 软键盘 ❌
  • Android 14 软键盘 ❌
  • Android 13 软键盘 ❌

此时用 scrcpy 直接操作 ADB KIT 所在的显示器,点击输入框

  • Android 15 软键盘 ✅

猜测还是焦点导致的

鼠标消失

在 Android 13 模拟器上测试,第二次打开 Neo,鼠标消失,原因是软件其实并没有完全退出,怎么解?

所以只在我一加 Pad2 Pro 上出现?

Neo 点击 Flutter App 日志

1
2
E/WindowManager(3097): isInputMethodClientFocus: display ID mismatch. from client: 106 from window: 0
W/InputMethodManagerService(3097): Ignoring showSoftInput of uid 10315

IME(输入法服务)判断请求来源于 display 106,
而它要附着(attach)的窗口 token 属于 display 0(主屏)。
系统检查不通过,于是直接忽略。

scrcpy 点击 Flutter App 日志

scrcpy server 在 该 displayId=106 上注入事件;

同时 焦点窗口(WindowState)与 IME client 的 displayId 一致(都为 106);

因此 isInputMethodClientFocus() 检查通过,键盘可显示。

1
2
E/WindowManager: isInputMethodClientFocus: display ID mismatch. from client: 111 from window: 0  
W/InputMethodManagerService: Ignoring showSoftInput of uid 10288

Neo 点击系统 App 日志,猜测是其他的 App 在唤起软键盘和 Flutter 是不一样的方式,所以在焦点不对的情况下,键盘也能弹出

1
2
3
V/InputMethodManager: Display ID mismatch found.
ViewRootImpl displayId=111 InputMethodManager displayId=0.
Use the right InputMethodManager instance to avoid performance overhead.

事件原理分析

1
2
3
4
void onRequestShow(InputMethodClient client, int reason, boolean fromUser) {
Slog.v(TAG, client + ": onRequestShow at " + origin + " reason " + reasonToString(reason)
+ " fromUser " + fromUser);
}

在 Neo 中点击窗口内应用的输入框

1
com.nightmare.neo:f85ed5fc: onRequestShow at ORIGIN_CLIENT reason SHOW_SOFT_INPUT fromUser false

程序调用的代码

1
InputMethodManager.showSoftInput(view, 0);

AAS 测试结果

通过在一加 Pad2 Pro 上点击 Neo 内部的 ADB KIT 输入框,键盘无法弹出

AAS 的 onTaskFocusChanged 并未回调,也就是说虽然通过 InjectEvent 注入了点击事件,但是焦点并没有切换到 VD 上(很奇怪)

onTaskStackChanged 关键

代码调用 VD Resize 并不触发 onTaskStackChanged

Android 15(模拟器)

点击 ADB KIT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
· onRecentTaskListUpdated
· onTaskCreated: taskId=36, componentName=null
· onTaskDisplayChanged: taskId=36, newDisplayId=3
· onTaskCreated: taskId=36, componentName=ComponentInfo{com.nightmare.adbkit/com.nightmare.adbkit.MainActivity}
· onTaskDescriptionChanged
· onTaskMovedToFront: taskId=36
· onRecentTaskListUpdated
· onTaskFocusChanged: taskId=36, focused=true
· onTaskDescriptionChanged
· onTaskDisplayChanged: taskId=36, newDisplayId=3
· onTaskStackChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged

点击 ADB KIT 输入框,输入法正常弹出

1
2
3
4
5
6
7
8
9
10
11
12
10-08 17:18:57.282 W/InputDispatcher(  617): Focused display #0 does not have a focused window.
10-08 17:18:57.282 E/InputDispatcher( 617): But another display has a focused window
10-08 17:18:57.282 E/InputDispatcher( 617): FocusedWindows:
10-08 17:18:57.282 E/InputDispatcher( 617): displayId=8, name='ca56884 com.nightmare.adbkit/com.nightmare.adbkit.MainActivity'
10-08 17:18:57.284 D/SulaInputManager( 2110): action -> 0, pointerId -> 0, position -> Position{point=Point[x=225, y=478], screenSize=1043x782}, pressure -> 1.0, actionButton -> 0, buttons -> 0, source -> 20482, displayId -> 8
10-08 17:18:57.286 D/NeoInputDispatcher( 1989): injectEvent总耗时: 1452708 ns, 总平均耗时: 1126043 ns
10-08 17:18:57.386 D/SulaInputManager( 2110): action -> 1, pointerId -> 0, position -> Position{point=Point[x=225, y=478], screenSize=1043x782}, pressure -> 1.0, actionButton -> 0, buttons -> 0, source -> 20482, displayId -> 8
10-08 17:18:57.386 D/NeoInputDispatcher( 1989): injectEvent总耗时: 417042 ns, 总平均耗时: 1114420 ns
10-08 17:18:57.392 I/ImeTracker( 3464): com.nightmare.adbkit:6c16344f: onRequestShow at ORIGIN_CLIENT reason SHOW_SOFT_INPUT fromUser false
10-08 17:18:57.394 D/InputMethodManager( 3464): showSoftInput() view=p3.t{69cebd4 VFE...... .F....ID 0,0-1043,782 #1 aid=1073741824} flags=0 reason=SHOW_SOFT_INPUT
10-08 17:18:57.455 D/WindowManagerShell( 868): onKeepClearAreasChanged: restricted={}, unrestricted={Rect(0, 1499 - 1080, 2274)}
10-08 17:18:57.491 D/View ( 1120): requestLayout : getStackTrace(Thread.java:1841) <- printStackStrace(View.java:27969) <- requestLayout(View.java:27999) <- setLayoutParams(View.java:20530) <- updateViewLayout(WindowManagerGlobal.java:464) <- updateViewLayout(WindowManagerImpl.java:165) <- notifyUpdateLayoutParams(TaskbarActivityContext.java:1528) <- onTaskbarOrBubblebarWindowHeightOrInsetsChanged(TaskbarInsetsController.kt:186) <- lambda$onIsStashedChanged$7(TaskbarStashController.java:938) <- $r8$lambda$V70CIL_pG5t21J73JvZUvAfU2B0(null:0) <- run(D8$$SyntheticClass:0) <- runAfterInit(TaskbarControllers.java:263) <- onIsStashedChanged(TaskbarStashController.java:935) <- lambda$createAnimToIsStashed$3(TaskbarStashController.java:620) <- $r8$lambda$lICNXpsaTGWd-5rxHyDFfQwiuw4(null:0) <- run(D8$$SyntheticClass:0) <- onAnimationEnd(AnimatorListeners.java:54) <- onAnimationEnd(Animator.java:708) <- call(D8$$SyntheticClass:0) <- callOnList(Animator.java:666) <-

Android 15(一加 Pad2 Pro)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
· onTaskCreated: taskId=2142, componentName=null
· onTaskDisplayChanged: taskId=2142, newDisplayId=4
· onTaskCreated: taskId=2142, componentName=ComponentInfo{com.nightmare.adbkit/com.nightmare.adbkit.MainActivity}
· onTaskDescriptionChanged
· onTaskMovedToFront: taskId=2142
· onRecentTaskListUpdated
· onTaskFocusChanged: taskId=2142, focused=true
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskStackChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
· onTaskDescriptionChanged
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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.880 W/WindowManager( 3201): java.lang.Throwable
10-08 17:51:49.880 W/WindowManager( 3201): at com.android.server.wm.WindowList.remove(WindowList.java:51)
10-08 17:51:49.880 W/WindowManager( 3201): at com.android.server.wm.WindowContainer.positionChildAt(WindowContainer.java:1124)
10-08 17:51:49.880 W/WindowManager( 3201): at com.android.server.wm.WindowManagerService.moveDisplayToTopInternal(WindowManagerService.java:3684)
10-08 17:51:49.880 W/WindowManager( 3201): at com.android.server.wm.WindowState.handleTapOutsideFocusInsideSelf(WindowState.java:7060)
10-08 17:51:49.880 W/WindowManager( 3201): at com.android.server.wm.WindowManagerService.onPointerDownOutsideFocusLocked(WindowManagerService.java:9897)
10-08 17:51:49.880 W/WindowManager( 3201): at com.android.server.wm.WindowManagerService.-$$Nest$monPointerDownOutsideFocusLocked(Unknown Source:0)
10-08 17:51:49.880 W/WindowManager( 3201): at com.android.server.wm.WindowManagerService$H.handleMessage(WindowManagerService.java:6382)
10-08 17:51:49.880 W/WindowManager( 3201): at android.os.Handler.dispatchMessage(Handler.java:112)
10-08 17:51:49.880 W/WindowManager( 3201): at android.os.Looper.loopOnce(Looper.java:288)
10-08 17:51:49.880 W/WindowManager( 3201): at android.os.Looper.loop(Looper.java:393)
10-08 17:51:49.880 W/WindowManager( 3201): at android.os.HandlerThread.run(HandlerThread.java:85)
10-08 17:51:49.880 W/WindowManager( 3201): at com.android.server.ServiceThread.run(ServiceThread.java:46)
10-08 17:51:49.881 D/WindowManager( 3201): NFW_findFocusedWindowIfNeeded:Window{8e376c6 u0 com.nightmare.neo/com.nightmare.neo.MainActivity} mCurrentFocus:null
10-08 17:51:49.881 V/WindowManager( 3201): Changing focus from null to Window{8e376c6 u0 com.nightmare.neo/com.nightmare.neo.MainActivity},diplayid=0
10-08 17:51:49.882 D/WindowManager( 3201): NFW_findFocusedWindowIfNeeded:Window{db8692d u0 com.nightmare.adbkit/com.nightmare.adbkit.MainActivity} mCurrentFocus:Window{db8692d u0 com.nightmare.adbkit/com.nightmare.adbkit.MainActivity}
10-08 17:51:49.882 W/InputDispatcher( 3201): Focused display #0 does not have a focused window.
10-08 17:51:49.882 E/InputDispatcher( 3201): But another display has a focused window
10-08 17:51:49.882 E/InputDispatcher( 3201): FocusedWindows:
10-08 17:51:49.882 E/InputDispatcher( 3201): displayId=50, name='db8692d com.nightmare.adbkit/com.nightmare.adbkit.MainActivity'
10-08 17:51:49.889 D/SulaInputManager(23439): action -> 0, pointerId -> 0, position -> Position{point=Point[x=261, y=246], screenSize=1575x1181}, pressure -> 1.0, actionButton -> 0, buttons -> 0, source -> 4098, displayId -> 50
10-08 17:51:49.889 D/ResourcesManagerExtImpl(23356): applyConfigurationToAppResourcesLocked app.getDisplayId() return callback.displayId:0
10-08 17:51:49.890 I/InputDispatcher( 3201): injectInputEvent: targetUid=<not set>, syncMode=NONE, timeout=30000ms, policyFlags=0x8000000, event=MotionEvent { action=DOWN, id[0]=0, x[0]=261, y[0]=246, eventTime=7356385000000, downTime=7356385000000, deviceId=0, source=TOUCHSCREEN, displayId=50, eventId=0xc537f3b1}
10-08 17:51:49.890 D/NeoInputDispatcher(23356): injectEvent总耗时: 4523646 ns, 总平均耗时: 2913528 ns
10-08 17:51:49.890 D/SulaInputManager(23439): action -> 2, pointerId -> 0, position -> Position{point=Point[x=261, y=246], screenSize=1575x1181}, pressure -> 1.0, actionButton -> 0, buttons -> 0, source -> 4098, displayId -> 50
10-08 17:51:49.890 I/InputDispatcher( 3201): injectInputEvent: targetUid=<not set>, syncMode=NONE, timeout=30000ms, policyFlags=0x8000000, event=MotionEvent { action=MOVE, id[0]=0, x[0]=261, y[0]=246, eventTime=7356385000000, downTime=7356385000000, deviceId=0, source=TOUCHSCREEN, displayId=50, eventId=0xe5fa5935}
10-08 17:51:49.890 D/NeoInputDispatcher(23356): injectEvent总耗时: 404271 ns, 总平均耗时: 2411677 ns
10-08 17:51:49.898 I/InputDispatcher( 3201): NFW_setFocusedWindow, 8e376c6 com.nightmare.neo/com.nightmare.neo.MainActivity on display 0, same as the previous:0
10-08 17:51:49.899 I/InputDispatcher( 3201): updateFocusedWindow, 8e376c6 com.nightmare.neo/com.nightmare.neo.MainActivity on display 0, reason: Window became focusable. Previous reason: NOT_FOCUSABLE, result: FocusedWindows:
10-08 17:51:49.899 I/InputDispatcher( 3201): displayId=0, name='8e376c6 com.nightmare.neo/com.nightmare.neo.MainActivity'
10-08 17:51:49.899 I/InputDispatcher( 3201): displayId=50, name='db8692d com.nightmare.adbkit/com.nightmare.adbkit.MainActivity'
10-08 17:51:49.900 D/InputMethodManagerService( 3201): isSecurity: attribute.packageName = com.nightmare.neo mCurMethodId = com.sohu.inputmethod.sogouoem/.SogouIME isSecurity = false ( enable = true needShow = false inBlackList = false isExist = true )
10-08 17:51:49.914 D/SulaInputManager(23439): action -> 1, pointerId -> 0, position -> Position{point=Point[x=261, y=246], screenSize=1575x1181}, pressure -> 1.0, actionButton -> 0, buttons -> 0, source -> 4098, displayId -> 50
10-08 17:51:49.914 I/InputDispatcher( 3201): injectInputEvent: targetUid=<not set>, syncMode=NONE, timeout=30000ms, policyFlags=0x8000000, event=MotionEvent { action=UP, id[0]=0, x[0]=261, y[0]=246, eventTime=7356409000000, downTime=7356385000000, deviceId=0, source=TOUCHSCREEN, displayId=50, eventId=0xef639bda}
10-08 17:51:49.914 D/NeoInputDispatcher(23356): injectEvent总耗时: 715573 ns, 总平均耗时: 2128993 ns
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): isInputMethodClientFocus: 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

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
2
3
4
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
);

这个时候,不管是启动 Flutter App 还是其他,通过 adb tap 或者 scrcpy 都无法弹起输入法

所以这个是不是依赖当前 Activity 的焦点?

scrcpy的指针鼠标可以出现在虚拟显示器上

用模拟器试试各个版本的情况

用 Pixso 画一个一模一样的光标

Code LFA 启动速度比较慢

https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java;drc=5dcfeb3f23e95207838e3e15d76336fe69f896c1;l=8309

将 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@GuardedBy("ImfLock.class")
private boolean canInteractWithImeLocked(int uid, IInputMethodClient client, String methodName,
@Nullable ImeTracker.Token statsToken, @UserIdInt int userId) {
final var userData = getUserData(userId);
if (userData.mCurClient == null || client == null
|| userData.mCurClient.mClient.asBinder() != client.asBinder()) {
// We need to check if this is the current client with
// focus in the window manager, to allow this call to
// be made before input is started in it.
final ClientState cs = mClientController.getClient(client.asBinder());
if (cs == null) {
ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
throw new IllegalArgumentException("unknown client " + client.asBinder());
}
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN);
if (!isImeClientFocused(userData.mImeBindingState.mFocusedWindow, cs)) {
Slog.w(TAG, String.format("Ignoring %s of uid %d : %s", methodName, uid, client));
return false;
}
}
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED);
return true;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Override
public @ImeClientFocusResult int hasInputMethodClientFocus(IBinder windowToken,
int uid, int pid, int displayId) {
if (displayId == Display.INVALID_DISPLAY) {
return ImeClientFocusResult.INVALID_DISPLAY_ID;
}
synchronized (mGlobalLock) {
final DisplayContent displayContent = mRoot.getTopFocusedDisplayContent();
InputTarget target = getInputTargetFromWindowTokenLocked(windowToken);
if (target == null) {
return ImeClientFocusResult.NOT_IME_TARGET_WINDOW;
}
final int tokenDisplayId = target.getDisplayContent().getDisplayId();
if (tokenDisplayId != displayId) {
Slog.e(TAG, "isInputMethodClientFocus: display ID mismatch."
+ " from client: " + displayId
+ " from window: " + tokenDisplayId);
return ImeClientFocusResult.DISPLAY_ID_MISMATCH;
}
if (displayContent == null
|| displayContent.getDisplayId() != displayId
|| !displayContent.hasAccess(uid)) {
return ImeClientFocusResult.INVALID_DISPLAY_ID;
}

if (target.isInputMethodClientFocus(uid, pid)) {
return ImeClientFocusResult.HAS_IME_FOCUS;
}
// Okay, how about this... what is the current focus?
// It seems in some cases we may not have moved the IM
// target window, such as when it was in a pop-up window,
// so let's also look at the current focus. (An example:
// go to Gmail, start searching so the keyboard goes up,
// press home. Sometimes the IME won't go down.)
// Would be nice to fix this more correctly, but it's
// way at the end of a release, and this should be good enough.
final WindowState currentFocus = displayContent.mCurrentFocus;
if (currentFocus != null && currentFocus.mSession.mUid == uid
&& currentFocus.mSession.mPid == pid) {
return currentFocus.canBeImeTarget() ? ImeClientFocusResult.HAS_IME_FOCUS
: ImeClientFocusResult.NOT_IME_TARGET_WINDOW;
}
}
return ImeClientFocusResult.NOT_IME_TARGET_WINDOW;
}

一加启动失败的打印

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
10-08 18:21:12.947 D/SurfaceFlinger( 2161): VRR [SurfaceFlinger] setDesiredActiveMode: displayId: 4630946983774026899, renderRate: 120, vsyncRate: 120, event: 0
10-08 18:21:12.955 I/InputDispatcher( 3201): injectInputEvent: targetUid=<not set>, syncMode=NONE, timeout=30000ms, policyFlags=0x8000000, event=MotionEvent { action=DOWN, id[0]=0, x[0]=766, y[0]=470, eventTime=9119450000000, downTime=9119450000000, deviceId=0, source=TOUCHSCREEN, displayId=58, eventId=0xee1e5ae4}
10-08 18:21:13.014 I/InputDispatcher( 3201): injectInputEvent: targetUid=<not set>, syncMode=NONE, timeout=30000ms, policyFlags=0x8000000, event=MotionEvent { action=UP, id[0]=0, x[0]=766, y[0]=470, eventTime=9119509000000, downTime=9119450000000, deviceId=0, source=TOUCHSCREEN, displayId=58, eventId=0xcea6be40}
10-08 18:21:13.025 I/ImeTracker( 9277): com.nightmare.adbkit:52821b3a: onRequestShow at ORIGIN_CLIENT reason SHOW_SOFT_INPUT fromUser false
10-08 18:21:13.027 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 )
10-08 18:21:13.027 D/InputMethodManager( 9277): showSoftInput() view=p3.t{1d8fa3f VFE...... .F....ID 0,0-3392,2400 #1 aid=1073741824 alpha=1.0 viewInfo = } flags=0 reason=SHOW_SOFT_INPUT
10-08 18:21:13.030 I/[UAH_CLIENT]( 3201): UahEventAcquire, cmdid:39, pkg:none, identity:InputMethodManagerServiceExtImpl
10-08 18:21:13.055 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.055 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.055 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.055 D/OplusWindowManagerServiceEnhance( 3201): setInsetAnimationTid: pid 8911 ,tid 8911 ,enable false ,uafEnable true
10-08 18:21:13.056 D/WindowManagerServiceExtImpl( 3201): extractConfigInfoAndRealFlags rotation=1, screenDp=0, residentWS:0, scenario:0, bounds:3392, serverWinBounds:Rect(0, 0 - 3392, 2400), win=Window{cf2f837 u0 InputMethod}
10-08 18:21:13.056 V/WindowManager( 3201): Relayout Window{cf2f837 u0 InputMethod}: viewVisibility=0, oldvis=4, req=3392x2316, vsysui=FULLSCREEN LAYOUT_FULLSCREEN LIGHT_NAVIGATION_BAR, x=0, y=0
10-08 18:21:13.056 I/WindowManager( 3201): Window{cf2f837 u0 InputMethod} state from NO_SURFACE to DRAW_PENDING; reason: resetDrawState
10-08 18:21:13.056 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.057 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.057 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.058 D/WindowManager( 3201): NFW_findFocusedWindowIfNeeded:Window{b83f938 u0 com.nightmare.adbkit/com.nightmare.adbkit.MainActivity} mCurrentFocus:Window{b83f938 u0 com.nightmare.adbkit/com.nightmare.adbkit.MainActivity}
10-08 18:21:13.058 D/WindowManager( 3201): NFW_findFocusedWindowIfNeeded:null mCurrentFocus:null
10-08 18:21:13.058 D/WindowManager( 3201): NFW_findFocusedWindowIfNeeded:null mCurrentFocus:null
10-08 18:21:13.058 D/CompactWindowManagerService( 3201): shortComponentName = com.nightmare.uncon/.MainActivity
10-08 18:21:13.060 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.061 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.061 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.066 I/WindowManager( 3201): Window{cf2f837 u0 InputMethod} state from DRAW_PENDING to COMMIT_DRAW_PENDING; reason: finishDrawingLocked
10-08 18:21:13.067 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.068 I/WindowManager( 3201): Window{cf2f837 u0 InputMethod} state from COMMIT_DRAW_PENDING to READY_TO_SHOW; reason: commitFinishDrawingLocked
10-08 18:21:13.068 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.068 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.068 D/WindowManagerServiceExtImpl( 3201): extractConfigInfoAndRealFlags rotation=1, screenDp=914, residentWS:0, scenario:0, bounds:3392, serverWinBounds:Rect(0, 0 - 3392, 2400), win=Window{cf2f837 u0 InputMethod}
10-08 18:21:13.068 V/WindowManager( 3201): Relayout Window{cf2f837 u0 InputMethod}: viewVisibility=0, oldvis=0, req=3392x2316, vsysui=FULLSCREEN LAYOUT_FULLSCREEN LIGHT_NAVIGATION_BAR
10-08 18:21:13.068 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.069 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.069 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.069 D/WindowManager( 3201): NFW_findFocusedWindowIfNeeded:Window{b83f938 u0 com.nightmare.adbkit/com.nightmare.adbkit.MainActivity} mCurrentFocus:Window{b83f938 u0 com.nightmare.adbkit/com.nightmare.adbkit.MainActivity}
10-08 18:21:13.069 D/WindowManager( 3201): NFW_findFocusedWindowIfNeeded:null mCurrentFocus:null
10-08 18:21:13.069 D/WindowManager( 3201): NFW_findFocusedWindowIfNeeded:null mCurrentFocus:null
10-08 18:21:13.069 D/CompactWindowManagerService( 3201): shortComponentName = com.nightmare.uncon/.MainActivity
10-08 18:21:13.069 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.070 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.070 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.070 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.070 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.070 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.073 I/OverviewProxyService( 3947): onOplusSystemBarAttributesChanged:appearance 8 packageName com.nightmare.uncon displayId 0 behavior 1
10-08 18:21:13.074 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.074 I/WindowManager( 3201): Window{cf2f837 u0 InputMethod} state from READY_TO_SHOW to HAS_DRAWN; reason: performShowLocked
10-08 18:21:13.074 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.074 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.075 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.075 D/OplusWindowManagerServiceEnhance( 3201): setInsetAnimationTid: pid 9054 ,tid 9054 ,enable true ,uafEnable true
10-08 18:21:13.075 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.075 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.076 D/WindowManagerShell( 3947): onKeepClearAreasChanged: restricted={}, unrestricted={Rect(0, 991 - 1541, 2081)}
10-08 18:21:13.078 D/DisplayModeDirector( 3201): setAppRequest displayId : 58 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.078 D/DisplayModeDirector( 3201): setAppRequest displayId : 0 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.078 D/DisplayModeDirector( 3201): setAppRequest displayId : 57 modeId: 0 requestedRefreshRate: 0.0 requestedMinRefreshRateRange: 0.0 requestedMaxRefreshRateRange: 0.0
10-08 18:21:13.080 D/SurfaceFlinger( 2161): VRR [SurfaceFlinger] setDesiredActiveMode: displayId: 4630946983774026899, renderRate: 120, vsyncRate: 120, event: 0
10-08 18:21:13.225 D/OplusWindowManagerServiceEnhance( 3201): setInsetAnimationTid: pid 9054 ,tid 9054 ,enable false ,uafEnable true
10-08 18:21:13.230 D/SurfaceFlinger( 2161): VRR [SurfaceFlinger] setDesiredActiveMode: displayId: 4630946983774026899, renderRate: 120, vsyncRate: 120, event: 0
10-08 18:21:16.012 D/SurfaceFlinger( 2161): VRR [SurfaceFlinger] setDesiredActiveMode: displayId: 4630946983774026899, renderRate: 60, vsyncRate: 60, 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

AI Agent

绿联NAS往极空间NAS备份分享

前言

需求很简单,将绿联 NAS 内的东西都备份到极空间内,我先尝试了将硬盘取下来放到极空间,挂载不上

然后就是通过 webdav/ftp/smb 等协议来备份,但是最后我开始尝试用 rsync,这样同步二者的时间戳可以保留,然后我更相信这个命令一点

经过之前的教训,我不相信绿联以及极空间任何 UI 上提供的功能

用 rsync 来完成 绿联 NAS 到极空间的备份,中途我在极空间用 FTP 从绿联备份了一些文件,用 rsync 应该能继续备份

就是中途用 FTP,感觉慢得不行,特别到了一些小文件的时候,感觉 rsync 要快很多

准备工作

绿联和极空间都需要开启 ssh

极空间是 ubuntu,绿联是 debian,绿联连清华源都没设置

绿联 apt install neofetch 需要执行 apt –fix-broken install

然后极空间默认更换了清华的源

极空间相关信息

ssh 登录后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 Welcome to ZOS (GNU/Linux 6.8.1-z4pro+-generic x86_64) 

ZZZZZZZ OOO SSSSS
ZZ O O SS
ZZ O O SSSS
ZZ O O SS
ZZ O O SS
ZZZZZZZ OOO SSSS
---------------------------------
System: "V1.0.0440020"
Service: "V1.0.0440102"
---------------------------------


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.


The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

Could not chdir to home directory /home/${phone}: No such file or directory
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

${phone}@Z4ProPlus-MEDO:/$

然后输入 sudo passwd 可以设置极空间 root 密码

随后可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
➜  ~ ssh -p 10000 root@192.168.31.64
root@192.168.31.64's password:


Welcome to ZOS (GNU/Linux 6.8.1-z4pro+-generic x86_64)

ZZZZZZZ OOO SSSSS
ZZ O O SS
ZZ O O SSSS
ZZ O O SS
ZZ O O SS
ZZZZZZZ OOO SSSS
---------------------------------
System: "V1.0.0440020"
Service: "V1.0.0440102"
---------------------------------

Last login: Sun Aug 17 05:27:33 2025 from 127.0.0.1
root@Z4ProPlus-MEDO:~#

apt 版本和源

1
2
3
4
root@Z4ProPlus-MEDO:~# apt -v
apt 2.8.3 (amd64)
root@Z4ProPlus-MEDO:~# cat /etc/apt/sources.list
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu noble main

neofetch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@Z4ProPlus-MEDO:~# neofetch
##### root@Z4ProPlus-MEDO
####### -------------------
##O#O## OS: zos x86_64
####### Kernel: 6.8.1-z4pro+-generic
########### Uptime: 3 days, 4 hours, 52 mins
############# Packages: 238 (dpkg)
############### Shell: bash 5.2.21
################ WM: Openbox
################# Theme: Yaru [GTK3]
##################### Icons: Yaru [GTK3]
##################### Terminal: /dev/pts/0
################# CPU: Intel N150 (4) @ 3.600GHz
GPU: Intel Graphics]
Memory: 1481MiB / 15771MiB
1
2
root@Z4ProPlus-MEDO:~# uname -a
Linux Z4ProPlus-MEDO 6.8.1-z4pro+-generic #13 SMP PREEMPT_DYNAMIC Thu Jun 5 15:03:23 CST 2025 x86_64 x86_64 x86_64 GNU/Linux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@Z4ProPlus-MEDO:~# hostnamectl
Static hostname: Z4ProPlus-MEDO
Icon name: computer-desktop
Chassis: desktop 🖥️
Machine ID: a251d735b2c44028b37bda6e9fddb2f7
Boot ID: de28c0eeb73e45608d22b5d1efe46031
Operating System: zos
Kernel: Linux 6.8.1-z4pro+-generic
Architecture: x86-64
Hardware Vendor: Default string
Hardware Model: Default string
Firmware Version: 5.27
Firmware Date: Thu 2025-04-10
Firmware Age: 4month 1w
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@Z4ProPlus-MEDO:~# cat /etc/os-release
NAME="ZOS"
VERSION="GNU/Linux ZOS"
ID="zos"
ID_LIKE="debian"
PRETTY_NAME="zos"
VERSION_ID="zos"
ZOS_VERSION="V1.0.0440020"
root@Z4ProPlus-MEDO:~# lsb_release -a
No LSB modules are available.
Distributor ID: ZOS
Description: zos
Release: zos
Codename: n/a
root@Z4ProPlus-MEDO:~#

绿联 NAS 相关信息

绿联 ssh 登录后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  ~ ssh nightmare@192.168.31.70
nightmare@192.168.31.70's password:
nightmare@DXP2800-JECT:~$ apt -v
apt 2.6.1 (amd64)
nightmare@DXP2800-JECT:~$ cat /etc/apt/sources.list
deb https://deb.debian.org/debian/ bookworm contrib main non-free non-free-firmware
deb-src https://deb.debian.org/debian/ bookworm contrib main non-free non-free-firmware
deb https://deb.debian.org/debian/ bookworm-updates contrib main non-free non-free-firmware
deb-src https://deb.debian.org/debian/ bookworm-updates contrib main non-free non-free-firmware
deb https://deb.debian.org/debian/ bookworm-proposed-updates contrib main non-free non-free-firmware
deb-src https://deb.debian.org/debian/ bookworm-proposed-updates contrib main non-free non-free-firmware
deb https://deb.debian.org/debian/ bookworm-backports contrib main non-free non-free-firmware
deb-src https://deb.debian.org/debian/ bookworm-backports contrib main non-free non-free-firmware
deb https://deb.debian.org/debian-security/ bookworm-security contrib main non-free non-free-firmware
deb-src https://deb.debian.org/debian-security/ bookworm-security contrib main non-free non-free-firmware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
nightmare@DXP2800-JECT:~$ neofetch
_,met$$$$$gg. nightmare@DXP2800-JECT
,g$$$$$$$$$$$$$$$P. ----------------------
,g$$P" """Y$$.". OS: Debian GNU/Linux 12 (bookworm) x86_64
,$$P' `$$$. Host: DXP2800 EM_DXP2800_V1.0.25
',$$P ,ggs. `$$b: Kernel: 6.1.27
`d$$' ,$P"' . $$$ Uptime: 9 hours, 44 mins
$$P d$' , $$P Packages: 1225 (dpkg)
$$: $$. - ,d$$' Shell: bash 5.2.15
$$; Y$b._ _,d$P' Terminal: /dev/pts/0
Y$$. `.`"Y$$$$P"' CPU: Intel N100 (4) @ 3.400GHz
`$$b "-.__ GPU: Intel Alder Lake-N [UHD Graphics]
`Y$$ Memory: 1248MiB / 7684MiB
`Y$$.
`$$b.
`Y$$b.
`"Y$b._
`"""
1
2
3
4
5
6
7
8
9
10
11
nightmare@DXP2800-JECT:~$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
OS_VERSION=1.7.0.3125
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
nightmare@DXP2800-JECT:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 12 (bookworm)
Release: 12
Codename: bookworm
nightmare@DXP2800-JECT:~$ hostnamectl
Static hostname: DXP2800-JECT
Icon name: computer-desktop
Chassis: desktop 🖥️
Machine ID: 986ee82ffb1876a93eee9ea5bbeab5a5
Boot ID: 14170c1f5dee40c89519b3b4ef3e6926
Operating System: Debian GNU/Linux 12 (bookworm)
Kernel: Linux 6.1.27
Architecture: x86-64
Firmware Version: EM_DXP2800_V1.0.25
nightmare@DXP2800-JECT:~$ cat /etc/*release
cat /etc/issue
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
OS_VERSION=1.7.0.3125
UGOSPRO Linux \n \l
1
2
nightmare@DXP2800-JECT:~$ uname -a
Linux DXP2800-JECT 6.1.27 #36 SMP PREEMPT_DYNAMIC Mon Apr 14 23:38:49 CST 2025 x86_64 GNU/Linux

绿联 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
2
3
4
5
6
ug_start_server, check access user: 1000, group: 10
getuid(): 1000 geteuid(): 1000
login group is admin, set euid as root
cannot set euid as root
receiving incremental file list
rsync: [sender] change_dir "nightmare" (in home) failed: No such file or directory (2)

但是极空间是可以设置 root 密码的

最后方案

在绿联上

1
2
screen -S backup
rsync -avhP -e "ssh -p 10000" /home/nightmare/ root@192.168.31.64:/data_s001/data/udata/real/${phone}/rsync_test/

用 screen 命令是为了断开 ssh 还能继续拷贝

TODO

ssh-copy-id nightmare@192.168.31.70 作用?

rsync 备份核心命令(绿联 → 极空间)

一些 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 与 -c 解释

-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

何时使用 –delete

需要让目标与源完全一致(删除目标多余文件)时再加:
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/

NeoDesktop更新日志

0.0.21(2025.07.19)

注意: 2025.07.11 的版本重构了很多的底层,需要卸载重装才行

修复

  • 修复窗口左下角和右下角无法缩放窗口的问题(2025.07.19)
  • 修复触控板变得难用的问题(2025.07.18)
  • 修复返回事件无法下发到窗口内的问题(2025.07.11)
  • 修复触控板界面无法加载第二显示器上的应用的问题(2025.07.10)
  • 尝试修复软件列表无法获取的问题
  • 窗口全屏返回后应用大小没恢复的问题
  • 窗口关闭后台任务仍然存在的问题
  • 修复窗口更改大小/切换全屏状态都会闪一下的问题
  • ND 在后台任务只有一个的问题(有外接显示器的情况下)
  • 修复设置窗口无法全屏的问题

优化

  • 增加超级工作台(前台前调度)开关控制,支持全局开关和手动悬挂到屏幕左侧开关(2025.07.19)
  • 优化窗口无极缩放的交互与UI(2025.07.19)
  • 设置显示器页面增加显示器 id 展示(2025.07.12)
  • 优化手柄的支持,现在手柄事件会默认绑定到最前面的窗口,无需单独设置绑定,且向下支持到 Android 10(理论)(2025.07.11)
  • 优化手机竖屏时的桌面,不启用台前调度功能,横屏时才启用
  • 优化触控板界面抽屉 UI,设置5秒未操作自动息屏(2025.07.10)
  • 优化窗口操作响应速度(2025.07.07)
  • 优化设置界面 UI,保持圆角统一,优化背景图标过大的问题(@Joy Ink)(2025.07.07)
  • 优化日志页面,合并服务端日志客户端日志,增加日志复制按钮(后续反馈可以复制日志到群里)(2025.07.07)
  • 软件在 Neo 中切换前后台或切换层叠关系不会导致应用重载(例如 Moonlight 已经打开了串流画面,切换前后台,都不会让Moonlight回到上个页面)
  • 支持将输入设备直接绑定到某个窗口内(例如把鼠标直接绑定到 Moonlight/Termux-x11 等,可以将手柄直接绑定到 Switch 模拟器等)
  • 重新适配了应用全屏显示,可以隐藏状态栏和小白条/导航栏
  • 支持平板触控板的鼠标在多个显示器间切换,往屏幕右侧滑动即可,仅在小米平板 7pro 测试过
  • 优化设置页面UI,增加关于页面
  • 优化设置中的输入设备的 UI,增加一些设备标识(例如 mouse,joystick)
  • 优化窗口性能

新增

  • 台前调度支持按钮触发(2025.07.19)
  • 台前调度支持类 Mac 的切换风格,当前默认开启(2025.07.12)
  • 应用窗口支持更多的鼠标事件,支持触控笔,支持触控笔压感(2025.07.07)
  • 移除了之前着色器动画当背景的选项,增加了一些静态壁纸的选项(2025.07.07)

已知Bug
不是故意写出来了,最近在大量重构底层,不知道什么原因导致的,后续会修复

  • 服务几率会崩溃,需要重新激活
  • 启动会卡住一段时间,原因未知
  • 鼠标在小米15上会必然导致 ND 无响应,原因未知

0.0.19

修复

  • 修复 Dock 栏遮挡应用窗口的问题(上个版本修复的忘记写了)
  • 修复窗口后台后再打开白屏的问题
  • 修复任务台前调度后,点击Dock栏最小化,台前调度的区域会空出一格的问题

优化

  • 优化 Dock 栏的UI
  • 优化窗口在多个显示器的移动,会流畅一点
  • 台前调度列表支持上下滚动
  • 台前调度的任务恢复到普通状态增加了缩放动画
  • 移除了之前的神灯动画,先写了一个简单的缩放动画

新增

  • 新增输入设备的功能,。可以将当前查询到的外设绑定到第二个显示器上,作为鼠标在多个显示器无法移动的第二种实现方式

0.0.18

  • 点击底层应用的任务栏即可将窗口切换至顶层
  • 优化光标在多个屏幕间的移动逻辑,之前光标还未到达第二显示器最左侧就就切换到了第一个显示器,现在可以丝滑在多个显示器间移动

优化

  • 大幅优化触控板体验,支持触发长按事件,拖拽更加顺滑,现在逻辑为: 1.快速单机下发点按事件;2.长按可以下发长按事件(之前不行);3.长按后滑动可以下发拖拽事件,触发时间300ms;4.双指滑动触发滚动事件
  • 全屏后会隐藏Dock栏目
  • 单击窗口顶部任务栏即可将窗口切换至顶层
  • 打开新的应用优化为默认在顶层
  • 减少侧滑切换触控板的区域,防止误触

新增

  • 鼠标在多个显示器之间移动
  • 增加直接切换触控板的按钮
  • 触控板增加了一个小的菜单栏,点击面板图标可以展开,可以直接控制第二显示器的应用状态,点击应用图标会循环切换点击应用的窗口状态(全屏、普通、最小化,注:UI设计只是我随便写的,后面会改)
  • 台前调度又加回来了,做了不少优化,添加了动画,取消了类 Mac 的倾斜效果

修复

  • 修复鼠标滚轮无法在窗口生效的问题
  • 手势或ESC无法退出全屏的问题

注意

  • 当前版本有个测试中的功能,可以将窗口直接移动到第二显示器,但还有非常多的问题,勿反馈
  • 台前调度目前还不支持上下滚动

接下来的计划

  • 优化 GXDE 系统的镜像包,修复不能上网的问题,增加多种硬件加速的设置
  • 台前调度后续可以开关控制

0.1.16

  • 支持更简单的安装 Linux 容器

0.1.15

  • 终端支持多个 session
  • Linux 容器攻克成功,但是目前不能一键安装,正在尝试集成 GXDE 和 灵墨

0.1.14

  • 目前给 NeoDesktop 塞进了一个 termux 环境,但是在尝试启动 termux-x11 连接 proot-distro 的时候,始终不成功,这个如果能攻克的话,Neo 可以实现开箱即用的 Linux 图形环境,和 Windows 模拟器,可以接便携屏,玩儿真正的 PC 游戏,愿意一起研究的联系我QQ:906262255,注明来意
  • 触控板一直比较难用,目前很多手势会冲突,先移除了双指滑动的功能,改为双击再移动触发滑动
  • 修复窗口不能切换层叠关系的问题

0.1.13

最近在究极攻克 termux 和 termux-x11,准备直接集成到 NeoDesktop,后面也许就有开箱即用的 PC 软件,可以有开箱即用的 PC 游戏

  • 台前调度太难用,先移除,后面比较稳定了再放回来,并增加开关关闭的方式
  • 换了个包名,之前的可以卸载了
  • 全屏时隐藏顶部的任务栏,软件能够完全利用显示器
  • 全屏/普通窗口的切换为三指单机窗口,目前还没写触控板从全屏切换到普通窗口的事件
  • 国际化优化,现在激活页面不再是英文了
  • 注意:这个版本不激活会闪退,激活需要配合新版的 ADB KIT(还没发布),或者用旧版,进到对应设备的终端管理器,输入sh /storage/emulated/0/Android/data/com.nightmare.neo/files/start.sh来激活

0.1.11

  • 加回了台前调度的功能
  • 右上角展示当前时间
  • 优化任务栏 UI 样式,增加一些小动画
  • 增加任务最小化和最大化的神灯效果(目前比较卡,还伴随着闪屏)

目前任务无法调整过层级关系,等下一个版本

0.1.10

  • 准备复活
  • 支持切换背景
  • UI 优化

0.1.9

这个版本重新写了触控事件的下发方式,可能会存在异常触控失效的情况,但将触控向下支持到了 Android12

  • 修复安卓12窗口触控失效的问题
  • 更改核心组件的启动逻辑,现在随 App 启动,不再常驻进程,这样会减少没有启动 Neo 对设备的影响
  • 优化文件管理器图标
  • 优化3D视频播放的交互,现在支持整个 UI 镜像而不仅是字幕,点击左右屏幕均可操作
  • 优化淘宝、微信、爱奇艺等 App 强制竖屏导致方向便横屏的问题,不过启动后仍然会自动旋转一下
  • 支持先启动 App,后连接外接显示器,也能多屏异显
  • 字幕居中,对中英字幕友好

目前仍发现的问题:

  • 启动偶尔闪退,启动会先展示一下激活界面
  • 点击窗口内部暂不会交换窗口层级关系
  • 缩放窗口偶先窗口在左上角闪烁,原因未知

0.1.8

  • 修复返回失效的问题
  • 修复触控板双指无法下发返回事件的问题

0.1.7

  • 先暂时取消台前调度
  • 支持 3D 视频播放,用任务栏的文件管理器打开视频,需要准备同名的 srt 文件,播放后在任务栏切换为镜像字幕

0.1.6

  • 支持类苹果的台前调度

0.1.5

  • 支持下发返回事件到最上层的任务窗口
  • 支持窗口下的多指触控
  • 优化窗口无极缩放,现在缩放后窗口只需要100ms即可加载,之前需要200ms,且缩放更丝滑,缩放不再依赖创建新的虚拟显示器,可以解决部分窗口画面消失以及触控失效的问题
  • 修复部分情况缩放后界面消失的问题
  • 优化缩放固定窗口的算法,现在可以完美固定对角线的视图位置

0.1.3

  • 加宽呼出触控板,空间鼠标的区域
  • 支持创建桌面快捷方式,从应用列表拖拽到桌面即可
  • 支持空间鼠标(严重测试版),左侧上下滑动切换

0.1.2

  • 修复触控板失效的问题
  • 支持窗口的全屏和退出全屏

0.1.1

  • 尝试解决与 Shizuku 冲突的问题,经测试,现在 Shizuku 不会影响 Sula 了
  • 修复非桌面上的软件无法打开的问题
  • 移除无用设置
  • 默认使用混合模式加载窗口

0.1.0

[重要更新]

  • 真正的无极缩放: 厂商的无极缩放都是*
  • 更好的应用交互体验
  • 多窗口跳出 Sula 问题解决
  • 无需 Shizuku 强制依赖
  • 全新的窗口事件下发,更丝滑

0.0.6

[前言]
这个版本更新日志前面的一些碎碎念,在 Sula 收费以前,我希望我所有的开发都是自由的
大家也不应该以“这是我的执念”来绑架我

我会尽全力不让 Sula 很快就走向终点,后面还有非常多的事需要做,编写技术分享文章(其中包括画各种架构图),编写官网,国际化,录制B站,YouTube 视频等,这些都应该会对 Sula 有正向的输入,一旦有更多的人发现并使用这个软件,我自身的投入也会更有动力

目前人在上海,花了几天时间在酒店,实际上更改的代码远不止日志中的内容

[XREAL那事儿]
跟XREAL合作闹崩还没有结束,起初我就是想问他们要 Android API,可以实现调用后,将眼镜切换到 2D 120hz 的模式,这样 Sula 运行的软件也都有 120 hz,60 -> 120 的提升是非常明显的,但是后来他们不仅不愿意给,还以极高傲的态度,让此事成了最后的样子

后续我会发一个完整的视频,只是觉得,这些事应该常有发生,只是很多人因为很多原因,不敢说,不能说,我现在孑然一身,亲人就剩一个姐姐,没有什么朋友,死了便死了

[新版本建议]
目前仍然需要同时依赖 Sula Server 的激活和 Shizuku
我后面会想办法可以实现二选一即可

建议打开设置,将 PlatfromView 的模式切换到 hybridComposition

这个切换是做了本地存储的,下次启动仍然有效

这样虽然窗口层叠关系会有问题,但是性能是最好的

后续我会尝试解决这种模式下的窗口层叠问题

[更新日志]

  • feat: 空间鼠标勉强可用
  • feat: 使用 HybridComposition 支持本地存储
  • feat: 桌面默认软件增加 GameViewer、Parsec
  • feat: 设置增加更新日志界面
  • fix: 修复设置中的一些着色器模板无法展示的问题
  • fix: 设置中识别到 Sula 自身创建的 Display 的问题
  • fix: 有 Scrcpy 投屏或者其他虚拟显示器在的时候,Sula 光标无法控制的问题
  • fix: 应用全屏时右侧会有一个很窄的像素宽度不能铺满,会露出桌面的问题

后续会支持外接物理鼠标

0.0.5

  • 修复激活不能重启服务的问题

0.0.4

多页面跳转问题解决了,目前没有多余的精力开发一个引导页面

这个版本开始需要 Shizuku + Sula Server

后续只用一种方式即可

  • 增大左侧切换模式的区域,滑动时,会有区域提示
  • 支持 Shizuku 加强窗口模式

0.0.3

  • 修复 Sula Server 高占用的问题

0.0.2

  • 修复无法激活的问题

0.0.1

  • 重启该项目后的第一个版本

NeoDesktop上手教程

NeoDesktop 简介

NeoDesktop 致力于将锤子 TNT 或者三星 Dex 这种桌面模式变得通用,不限制品牌,不硬限制安卓版本

解决现在带 DP 输出视频设备仅能投屏的问题

也可以搭配投影仪,AR 眼镜使用

这是一个锤子 TNT 桌面模式的一张图

也是 NeoDesktop 的一个目标吧

这个软件的构思在2023年10月30,大概是小米14发布的时候,因为小米14支持 DP 输出

B站动态

在经过了比较多小版本的迭代,目前 ND 不用同时依赖自由激活和 Shizuku ,当前仅可通过自有服务激活

感谢 PzHown 提供的软件图标

Android 版本

经测试支持 Android 11 - 15

安卓12及以下,比较多的 App 会限制存在 ND 的窗口中,简单说就是比较不可用,但 Moonlight 这类应该没问题

下载地址

链接

激活: ADB KIT 或传统命令行二选一

推荐使用 ADB KIT 激活,比较方便,全平台都有,支持有线、无线、无线配对等方式,激活的辅助设备不管是 Windows、Linux、macOS,甚至是 Android,通过数据线,点几个按钮即可激活

激活前需要先启动一次软件

启动软件也有激活的引导

激活方式一: 通过 ADB KIT

下载 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,看不到实时的流,无法确认启动状态

激活方式二: 命令行(需要 ADB 环境)

能到这一步,我相信你已经有了一定的基础

首先需要启动一次 Sula,然后用 adb 命令启动服务

1
adb shell sh /storage/emulated/0/Android/data/com.nightmare.sula/files/start.sh

切换触控板

启动 NeoDesktop 的情况下,手指在左侧上下滑动可以切换触控板、空间鼠标

这里后续补一个 Gif

创建桌面快捷方式

其他

在这个软件诞生的过程中,我感受到来自 XREAL 产品以及运营的高傲,所以 NeoDesktop 不将视频 XREAL 的眼镜

看下XREAL的说法,他们已经把这定义为一种不可能

XREAL社区官方图文

“首先,在正常的厂商系统中,应用与应用之间的通讯与访问时被禁止的。这本身是为了防止应用之间篡改或者盗取数据而做的防护措施。但这个隔离系统所产生的问题就是——如果我们想要通过Nebula APP这个身份将另一个应用在AR空间中拉起,在厂商未授予高级系统权限的前提下是无法实现的。”

但我至少让这成为了可能,同时我不建议在 XREAL 眼镜中使用这个软件

一些 Q&A

1.为什么需要激活?
ND 的窗口管理,事件下发都是调用系统级别的 API 来完成的,普通的 App 是不具备找个权限的,而如果仅限制为 root 用户可用,则更高的加大了使用门槛,更何况现在主流的设备都不能解锁

所以采用了自有服务激活的方式,一次激活设备不重启,不插拔 USB,大概率是不会掉的,如果掉了借助 ADB KIT,也能快速的激活

上手记录(一)

NeoDesktop 依赖 Shizuku 代码,激活阻塞的问题

实际排查过程中,发现并不是阻塞了,而是报错了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

public static void register(ShizukuService shizukuService) {
sShizukuService = shizukuService;
LOGGER.i("register");
try {
LOGGER.i("invoke ActivityManagerApis.registerProcessObserver");
ActivityManagerApis.registerProcessObserver(new ProcessObserver());
LOGGER.i("invoke ActivityManagerApis.registerProcessObserver done");
} catch (Throwable tr) {
LOGGER.e(tr, "registerProcessObserver");
}

LOGGER.i("invoke ActivityManagerApis.registerProcessObserver done1");
if (Build.VERSION.SDK_INT >= 26) {
LOGGER.i("invoke ActivityManagerApis.registerProcessObserver done1.1");
LOGGER.i("invoke ActivityManagerApis.registerProcessObserver done1.1.2");
int flags = UID_OBSERVER_GONE | UID_OBSERVER_IDLE | UID_OBSERVER_ACTIVE;
LOGGER.i("invoke ActivityManagerApis.registerProcessObserver done2");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
flags |= UID_OBSERVER_CACHED;
}
try {
LOGGER.i("invoke ActivityManagerApis.registerUidObserver");
ActivityManagerApis.registerUidObserver(new UidObserver(), flags,
ActivityManagerHidden.PROCESS_STATE_UNKNOWN,
null);
LOGGER.i("invoke ActivityManagerApis.registerUidObserver done");
} catch (Throwable tr) {
LOGGER.e(tr, "registerUidObserver");
}
}
}

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 介绍

在我个人的认知,需要解释 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 本身的一些设计,让你很难找到其中关键的魔法代码

更魔法的 UserService 模式

在这种模式下,你的任何代码,不仅仅是获取某些 Services,甚至 C/C++ 代码,都可以被提权

这是最魔法的地方,我也正式因为排查问题,才一点一点,弄明白了一点原理

激活程序所干的事

Android API Server 开源分享

前言

好久不见,我是梦魇兽,距离我上一次写文章已经是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 模式等

在今天对整体架构重新进行了设计,正式作为一个对外的开源

也许听起来很复杂,没关系,我们后面会通过简单的示例代码来看一下他的功能

Android API Server 介绍

AAS 是一个为 Android 设备提供 RESTful API 的服务器。它基于 HTTP 协议,可以被任何支持 HTTP 的客户端访问。它设计轻量且易于使用,支持热插拔,你可以通过很简短的代码,来让 AAS 加载你自定义的插件

支持上层框架为 Web 或者 Flutter 或者其他任意不能直接访问 Java 的框架中使用

例如在 Flutter 中,我们几乎需要使用 MethodChannel 来访问安卓的 API

使用 MethodChannel 实现后,无法支持在 Flutter Web 中访问安卓的 MethodChannel

AAS 提供了封装好的开箱即用的 Flutter Plugin,或者你可以根据 API.md 实现任意语言编写的客户端

功能特性

  • RESTful: 通过 HTTP 协议,获取安卓 API 相关的信息
  • 插件化: 通过简单的代码编写,可实现自定义插件的支持
  • 内置 API: 内置开箱即用的在 Dex 中获取 Context、Services 的各种 API
  • 内置插件: 内置多个插件,例如获取应用列表、应用图标、创建虚拟显示器等
  • Flutter Plugin 支持: 只需要引入 Flutter 依赖,AAS 会随插件的注册而启动,在 Flutter 侧只需要调用 Dart API 即可
  • 多种模式支持: 支持 Activity Mode 与 Dex Mode
  • 安全: 有一个简单的鉴权,来防止端口扫描恶意调用

架构图

对上层的应用来说,只有 Address 和 Port 的感知,它不在乎对方是哪种模式运行的

你可以在任何地方,任何设备上,通过 HTTP 获取安卓的信息

由于 HTTP 并不安全,所以 AAS 内置了一个简单的接口鉴权,来防止端口扫描恶意调用

从示例代码来理解 AAS

这是 Flutter 编写的示例代码,展示了内置的一些插件和 API 的使用

基于 HTTP 的好处是,你可以通过这样的代码来获取一个 App 的图标(Flutter)

1
2
AASClient aasClient = AASClient();
Image.network(aasClient.iconUrl('com.nightmare'))

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 有两种启动模式

Activity Mode

这种情况下,AAS 拥有真实的 Activity Context,对于获取应用列表,同普通安卓本身访问 API一样,需要申请权限

Dex Mode

启动脚本在 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
2
AASClient aasClient = AASClient(port: port);
Image.network(aasClient.iconUrl('com.nightmare'))

你还可以自己实现各种各样的 API ,来获得远超 adb 命令行的功能,例如图标,后台应用截图,adb 命令本身不支持

在 Flutter 中使用

提供 android_api_server_client 来快速的让 Flutter App 拥有这个能力,无需手动启动服务,AAS 随 Flutter Plugin 注册而启动,直接创建 AASClient 则会使用 Flutter Plugin 中启动的端口

1
2
3
dependencies:
android_api_server_client:
git: https://github.com/nightmare-space/android_api_server_client

然后直接使用封装好的 Dart API

1
2
AASClient aasClient = AASClient();
AppInfos infos = await aasClient.getAppInfos();

如果你需要在 PC 上访问同样的接口,通过 Dex Mode,你只需要更改端口

1
2
AASClient aasClient = AASClient(port: 15000);
AppInfos infos = await aasClient.getAppInfos();

假如我目前有一个 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
2
3
4
5
6
7
8
AASIntegrate aasIntegrate = new AASIntegrate();
try {
int port = aasIntegrate.startServerFromActivity(context);
Log.d(TAG, "port -> " + port);
} catch (Exception e) {
Log.d(TAG, "error -> " + e);
e.printStackTrace();
}

所以通过这两种模式的了解,在 PC 端获取安卓的信息,通常需要 ADB KIT 或者 Uncon 中一样,需要处理 Dex 的启动流程

详见 ADB KIT

使用 AAS API 以及自定义插件

自定义一个插件

这是获取安卓后台快照的完整插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class ActivityTaskManagerPlugin extends AndroidAPIPlugin {
@Override
public String route() {
return "/task_thumbnail";
}

public Bitmap graphicBufferToBitmap(GraphicBuffer graphicBuffer) {
int width = graphicBuffer.getWidth();
int height = graphicBuffer.getHeight();
int format = graphicBuffer.getFormat();

Bitmap.Config config;
if (format == PixelFormat.RGBA_8888) {
config = Bitmap.Config.ARGB_8888;
} else {
throw new IllegalArgumentException("Unsupported format: " + format);
}

Bitmap bitmap = Bitmap.createBitmap(width, height, config);
graphicBuffer.lockCanvas().drawBitmap(bitmap, 0, 0, null);
graphicBuffer.unlockCanvasAndPost(graphicBuffer.lockCanvas());

return bitmap;
}


@Override
public NanoHTTPD.Response handle(NanoHTTPD.IHTTPSession session) {
String id = session.getParms().get("id");
L.d("id -> " + id);
byte[] bytes = null;
try {
long start = System.currentTimeMillis();
IActivityTaskManager activityTaskManager = ActivityTaskManager.getService();
ReflectionHelper.listAllObject(activityTaskManager);
Object snapshot = null;

// Android 12/Android 15
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S || Build.VERSION.SDK_INT == 35) {
L.d("S or VANILLA_ICE_CREAM");
snapshot = ReflectionHelper.invokeHiddenMethod(activityTaskManager, "getTaskSnapshot", Integer.parseInt(id), false);
L.d("snapshot -> " + snapshot);
}
// Android 13/Android 14
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU || Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
L.d("TIRAMISU or UPSIDE_DOWN_CAKE");
snapshot = ReflectionHelper.invokeHiddenMethod(activityTaskManager, "getTaskSnapshot", Integer.parseInt(id), false, false);
L.d("snapshot -> " + snapshot);
}
Object hardBuffer = ReflectionHelper.getHiddenField(snapshot, "mSnapshot");
L.d("hardBuffer -> " + hardBuffer);
Object colorSpace = ReflectionHelper.getHiddenField(snapshot, "mColorSpace");
//
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
HardwareBuffer hardwareBuffer = (HardwareBuffer) hardBuffer;
Bitmap bitmap = Bitmap.wrapHardwareBuffer(hardwareBuffer, (ColorSpace) colorSpace);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
bytes = baos.toByteArray();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "image/png", new ByteArrayInputStream(bytes), bytes.length);
}
}

相关解释

  • route 方法返回的是这个插件的路由
  • handle 方法是这个插件的处理方法,这里是获取后台任务的缩略图,返回对象参考 NanoHTTPD.Response

如果是同一类插件,建议只实现一个 AndroidAPIPlugin
然后增加 param 来区分,例如 action

调用隐藏 API

1
Object result = ReflectionHelper.invokeHiddenMethod(Object object, String methodName, Object... args);

获取隐藏字段

1
Object result = ReflectionHelper.getHiddenField(object, "$name");

获取 Context

1
Context context = ContextStore.getContext();

解释一下为什么放在了单例里面,由于 AAS 有两种模式,两种模式下 Context 的获取方式是不一样的,Activity Mode 是直接储存的 Activity Context,Dex Mode 是通过反射获取的 Context

获取 Service

ServiceManager 来自 aas_hidden_api

1
ServiceManager.getService("activity_task");

例如我们要一个 PackageManager 的 Service

IPackageManager 来自 aas_hidden_api,PackageManager 是系统的 API

1
2
3
IPackageManager pms = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));

PackageManager pm = ContextStore.getContext().getPackageManager();

这两种方法的选择要根据场景来,如果 IXManager 能实现需求,就用这种,因为在 Dex Mode 中,Context 是不完整的,但是例如 DisplayManager 这种,IDisplayManager 的 API 没有 DisplayManager 好用

更多获取方法详见 SystemServerApi

构建实例

1
AndroidAPIServer server = new AndroidAPIServer();

注册插件

1
server.registerPlugin(new PackageManagerPlugin());

AAS Intergated 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class AASIntegrate {

static public void main(String[] args) {
AndroidAPIServer server = new AndroidAPIServer();
server.startServerForShell(args);
registerRoutes(server);
Looper.loop();
}

public static int startServerFromActivity(Context context) {
AndroidAPIServer server = new AndroidAPIServer();
int port = server.startServerFromActivity(context);
registerRoutes(server);
return port;
}

private static void registerRoutes(AndroidAPIServer server) {
server.registerPlugin(new PackageManagerPlugin());
server.registerPlugin(new ChangeDisplayHandler());
server.registerPlugin(new DisplayManagerPlugin());
server.registerPlugin(new ActivityManagerPlugin());
server.registerPlugin(new ActivityTaskManagerPlugin());
server.registerPlugin(new FilePlugin());
}

}

开源仓库介绍

  • ass: 这是框架本身,是一个壳,不带任何插件,你如果需要使用这样的模式,只需要依赖这个
  • aas_hidden_api:一个优雅的可以访问 Android 隐藏 API 的解决方案,通过 compileOnly 依赖到 aas_plugin 和 aas 中
  • aas_integrated: 一个集成了 aas_plugin 的库,如果需要现有的一些插件,直接用这个
  • aas_plugin: 实现了一些个人项目中会用到的插件,例如 ActivityManagerPlugin、DisplayMnagerPlugin 等

更多场景介绍

文件选择、应用选择(ADB KIT、Uncon、Fast Share)

启动器

可以在 PC 端启动安卓的 App,配合应用流转使用,可实现无需解锁手机,即可在 PC 上运行安卓上的软件

文件预览(Uncon)

视频极速缓冲播放,100G 的文件都能随意拉动进度条

无需安卓安装额外 App,仅需要开启 USB 调试

已有插件介绍

ActivityManagerPlugin

  • /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

ActivityTaskManagerPlugin

  • /task_thumbnail?id=${id}: 获取 Task 的截图

DisplayManagerPlugin

  • /display_manager?action=get_displays: 获取所有的 Display 信息
  • /display_manager?action=create_virtual_display&width=1080&height=1920&dpi=320: 创建虚拟显示器

PackageManagerPlugin

  • /package_manager?action=get_permissions&package=${package}: 获取 App 的权限
  • /package_manager?action=get_icon&package=${package}: 获取 App 的图标

FilePlugin

  • /file?action=dir&path=\${dir_path}: 获取目录下的文件信息
  • /file?action=file&path=\${file_path}: 获取文件,会直接返回整个文件,用浏览器访问则会预览这个文件,如果是视频则支持极速缓冲播放,支持断点续传

最后

  • 没有考虑用 HTTPS,因为有类似于文件的服务,HTTPS 会让访问变慢,增加了一个简单的鉴权,来防止端口扫描恶意调用的情况
  • 一点小问题: 小可实力不济, 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 进行的发布

不要给我私信参加任何活动

不要给我任何狗屁创作激励,社区就是被这狗屁激励搞砸的

我也不知道这次回归,能持续多久,也许被某一条评论恶心到,然后再次离开?

世间的变数太多了

几句话与大家共勉

  • “偏信则暗,监听则明”
  • “道阻且长,行则将至”
  • “你看起来很厉害,但一事无成”

我曾一度讨厌任何变成以流量为目的社区

我们编写文章,记录开发历程,分享经验,变成了,我要如何制造噱头,如何编写更有噱头的标题

怎么改文章,才会有更多流量

包括社区各种各样的活动,无一不是在告诉你,你的文章的唯一目的,就是为了更多的流量

其他的技术本身,文章质量,都是狗屁

反驳拉黑

也有一些人说羡慕我现在的生活,但这都是取舍罢了

你想要什么

你准备好失去什么

时间往前,交换进行

世界可总是不公平的,也许你只得到了一点,但你会失去很多

循环往复

下次见