ROKID Staton2 ADB Crash 问题解决记录

很奇怪啊,adb 在 Rokid 上崩了

报错只有这个信息

fdsan: failed to exchange ownership of file descriptor: fd 11 is owned by unique_fd 0x76e2500458, was expected to be unowned

我个人对于这种有着及大量 C/C++ 代码的程序出现问题,基本就是用很土的方法

二分日志法

就是从 main 函数的开始和结尾打上一个日志,然后在中间的位置打上一个日志

看下崩溃的日志是在哪个区间,然后逐渐缩小范围

看一下加上日志后 crash 的输出

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
Launching lib/main.dart on RG station2 in debug mode...
✓ Built build/app/outputs/flutter-apk/app-debug.apk
***
I/flutter (12482): [I] ADB polling error : Exception: * daemon not running; starting now at tcp:5037
I/flutter (12482): [I] * daemon started successfully
I/flutter (12482): [I] error: protocol fault (couldn't read status): Connection reset by peer
I/flutter (12482): [I]
D/ProfileInstaller(12482): Installing profile for com.nightmare.remote
I/flutter (12482): [I] ADB polling error : Exception: * daemon not running; starting now at tcp:5037
I/flutter (12482): [I] * daemon started successfully
I/flutter (12482): [I] error: protocol fault (couldn't read status): Connection reset by peer
I/flutter (12482): [I]
I/flutter (12482): [I] ADB polling error : Exception: * daemon not running; starting now at tcp:5037
I/flutter (12482): [I] ADB server didn't ACK
I/flutter (12482): [I] Full server startup log: /data/data/com.nightmare.remote/files/usr/bin/adb.10132.log
I/flutter (12482): [I] Server had pid: 12598
I/flutter (12482): [I] --- adb starting (pid 12598) ---
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb : main.cpp:63 Android Debug Bridge version 1.0.41
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb : main.cpp:63 Version 35.0.3-3.35.001-20241010-130201
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb : main.cpp:63 Installed as /data/app/~~C4RvqyAAdViMpMm8iMumHA==/com.nightmare.remote-D61hRw9Vp3k2uSteY-mqyQ==/lib/arm64/libadb.so
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb : main.cpp:63 Running on Linux 5.10.198-qki-consolidate-android12-9-gd70a73cbc90f (aarch64)
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb : main.cpp:63
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb : main.cpp:132 setup_daemon_logging invoke done atexit will be called
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb : main.cpp:136 init_reconnect_handler will called
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb : main.cpp:140 init_reconnect_handler invoke done
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb : main.cpp:143 init_mdns_transport_discovery invoke
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12598 I adb : task_runner.cpp:57 tasks_.emplace called now + delay -> 3369119 ms since epoch
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12598 I adb : main.cpp:145 init_mdns_transport_discovery invoke done
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12598 I adb : main.cpp:152 usb_init invoke
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12600 I adb : fdevent.cpp:276 9.1
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12600 I adb : fdevent.cpp:211 8.1.xx
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12598 I adb : main.cpp:154 usb_init invoke done
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12598 I adb : main.cpp:161 local_init invoke
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12598 I adb : main.cpp:163 local_init invoke done
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 E adb : usb_linux.cpp:143 find_usb_device bus_name-> /dev/bus/usb/003
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> .
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> ..
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> 001
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12598 I adb : auth.cpp:419 adb_auth_init...
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:170 unix_open failed d_name-> 001
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 E adb : usb_linux.cpp:143 find_usb_device bus_name-> /dev/bus/usb/002
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> .
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> ..
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> 002
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:170 unix_open failed d_name-> 002
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> 001
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:170 unix_open failed d_name-> 001
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 E adb : usb_linux.cpp:143 find_usb_device bus_name-> /dev/bus/usb/001
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> .
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> ..
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:149 find_usb_device d_name-> 001
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12601 I adb : usb_linux.cpp:170 unix_open failed d_name-> 001
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12602 I adb : fdevent.cpp:276 9.1
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12602 I adb : fdevent.cpp:211 8.1.xx
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : auth.cpp:152 loaded new key from '/data/data/com.nightmare.remote/files/usr/bin/.android/adbkey' with fingerprint 1472BA4C908DFCB4B03AB6912355A3B83BBF5EF322142123E9D2D6C51CA55536
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : auth.cpp:391 adb_auth_inotify_init...
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : auth.cpp:393 1...
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : auth.cpp:395 2... infd = 8
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : auth.cpp:412 3
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : auth.cpp:436 4...
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : main.cpp:185 5
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : main.cpp:200 6
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : main.cpp:243 7
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : main.cpp:259 8
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:281 8.1
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent_epoll.cpp:112 8.1.1
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent_epoll.cpp:118 8.1.2
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent_epoll.cpp:125 8.1.3
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent_epoll.cpp:146 8.1.4
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent_epoll.cpp:178 8.1.5
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent_epoll.cpp:197 8.1.6
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:155 8.1.6.1
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:157 8.1.6.2(fdevent 0: fd 5 R) got events 1
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:161 8.1.6.3
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:166 8.1.6.4
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:175 8.1.6.4.1
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:180 8.1.6.4.2
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:186 8.1.6.4.4
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:188 8.1.6.4.5
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:190 8.1.6.4.6
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : fdevent.cpp:207 Executing fn
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : task_runner.cpp:120 waitable_task called
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : task_runner.cpp:113 task called
I/flutter (12482): [I] fdsan: failed to exchange ownership of file descriptor: fd 7 is owned by unique_fd 0x7959de91b8, was expected to be unowned

起初我只定位到崩溃的地方在 fdevent.cpp:207,也就是还没有这两行日志

1
2
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb     : task_runner.cpp:120 waitable_task called
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : task_runner.cpp:113 task called

fdevent.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void fdevent_context::Run(std::function<void()> fn) {
auto wrapped_fn = [fn]() {
LOG(INFO) << "Executing fn";
// fdevent.cpp:207
fn();
};
{
LOG(INFO) << "8.1.xx ";
std::lock_guard<std::mutex> lock(run_queue_mutex_);
run_queue_.push_back(std::move(wrapped_fn));
}

Interrupt();
}

207 行调用了一个回调函数,姑且这么称它吧,这种就棘手一点,因为你不知道回调函数是谁

看下崩溃的链路,从入口函数到崩溃的整条链路如下

adb_server_main -> fdevent_loop -> fdevent_get_ambient()->Loop()
-> fdevent_context::HandleEvents -> FlushRunQueue

FlushRunQueue 函数如下

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

void fdevent_context::FlushRunQueue() {
// We need to be careful around reentrancy here, since a function we call can queue up another
// function.

LOG(INFO) << "8.1.6.4.1";
while (true) {
std::function<void()> fn;
{
std::lock_guard<std::mutex> lock(this->run_queue_mutex_);
LOG(INFO) << "8.1.6.4.2";
if (this->run_queue_.empty()) {
LOG(INFO) << "8.1.6.4.3";
break;
}
fn = std::move(this->run_queue_.front());
LOG(INFO) << "8.1.6.4.4";
this->run_queue_.pop_front();
LOG(INFO) << "8.1.6.4.5";
}
LOG(INFO) << "8.1.6.4.6";
fn();
LOG(INFO) << "8.1.6.4.7";
}
LOG(INFO) << "8.1.6.10";
}

看起来像是一个执行队列里面的任务的函数

而任务的注入是 fdevent_run_on_looper -> fdevent_get_ambient()->Run(std::move(fn))

所以这个时候的任务就是找到是哪个函数注入的这个任务(二分日志)

执行 fn 的时候崩了,是很难找打是哪儿注入的函数崩的,这个时候只有全局搜索 fdevent_run_on_looper 的调用,然后加日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void fdevent_context::Run(std::function<void()> fn) {
auto wrapped_fn = [fn]() {
LOG(INFO) << "Executing fn";
fn();
};
{
LOG(INFO) << "8.1.xx ";
std::lock_guard<std::mutex> lock(run_queue_mutex_);
run_queue_.push_back(std::move(wrapped_fn));
}

Interrupt();
}

void fdevent_run_on_looper(std::function<void()> fn) {
LOG(INFO) << "9.1";
fdevent_get_ambient()->Run(std::move(fn));
}

于是就有了这两行日志

1
2
3
4
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb     : task_runner.cpp:120 waitable_task called
I/flutter (12482): [I] 10-27 13:45:19.842 12598 12598 I adb : task_runner.cpp:113 task called
I/flutter (12482): [I] fdsan: failed to exchange ownership of file descriptor: fd 7 is owned by unique_fd 0x7959de91b8, was
expected to be unowned

最后崩溃函数是 task ,不难发现,无论是这个 task_runner 还是 event_poll ,都是 Event Loop,事件循环

也可称为生产消费模型?比较经典的事件队列,只要找到注入的地方就行

发现是 task_runner.cpp

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void AdbOspTaskRunner::TaskExecutorWorker() {
// ***
CHECK(!running_tasks.empty());
std::packaged_task<int()> waitable_task([&] {
for (Task& task : running_tasks) {
// task_runner.cpp:113
LOG(INFO) << "task called";
task();
}
return 0;
});

fdevent_run_on_looper([&]() {
// task_runner.cpp:120
LOG(INFO) << "waitable_task called";
waitable_task();
});
// ***

fdevent_run_on_looper -> waitable_task -> task

而这个 task 又是一个任务队列,又得找是哪儿注入的 task,执行的时候崩掉的

所以找注入 task 的地方的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void AdbOspTaskRunner::PostPackagedTask(Task task) {
PostPackagedTaskWithDelay(std::move(task), openscreen::Clock::duration::zero());
}

void AdbOspTaskRunner::PostPackagedTaskWithDelay(Task task, Clock::duration delay) {
auto now = std::chrono::steady_clock::now();
auto target_time = now + delay;
{
std::lock_guard<std::mutex> lock(mutex_);
auto target_time_ms = std::chrono::duration_cast<std::chrono::milliseconds>(target_time.time_since_epoch()).count();
LOG(INFO) << "tasks_.emplace called now + delay -> " << target_time_ms << " ms since epoch";
tasks_.emplace(now + delay, std::move(task));
}
cv_.notify_one();
}

仍然二分日志找调用

根据日志和代码

1
2
3
I/flutter (12482): [I] 10-27 13:45:19.840 12598 12598 I adb     : main.cpp:143 init_mdns_transport_discovery invoke 
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12598 I adb : task_runner.cpp:57 tasks_.emplace called now + delay -> 3369119 ms since epoch
I/flutter (12482): [I] 10-27 13:45:19.841 12598 12598 I adb : main.cpp:145 init_mdns_transport_discovery invoke done
1
2
3
4
5
6
7
8
int adb_server_main(/**/) {
/**/
if (!getenv("ADB_MDNS") || strcmp(getenv("ADB_MDNS"), "0") != 0) {
LOG(INFO) << "init_mdns_transport_discovery invoke ";
init_mdns_transport_discovery();
LOG(INFO) << "init_mdns_transport_discovery invoke done";
}
/**/

位于 init_mdns_transport_discovery() 函数中

具体不知道什么原因这个函数崩了,但 adb 的 mdns 在 rokid 上本来也没用上

根据源代码,定义一个环境变量 ADB_MDNS=0 就可以关闭这个功能

OVER

切换刷新率调研

调研资料

https://www.youtube.com/watch?v=YSDBKqoL0O8

这个视频中的设备,是能够直接在系统中切换外接显示器的刷新率的,后续我的小米如果能 Root 的话,我也想研究下 AOSP 官方镜像(就是刷 ROM)支不支持切换
也就是系统层,其实是有入口可以切换刷新率的,这个入口指 Android 比较底层的 API

而国内厂商嘛,任何 Google 本身系统有的东西,当然是全砍了,不然怎么能让你知道用的他们的手机

部分调研源码

https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/display/DisplayManagerShellCommand.java

https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/services/core/java/com/android/server/display/DisplayManagerService.java

https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/display/LocalDisplayAdapter.java

https://android.googlesource.com/platform/frameworks/base.git/+/master/core/java/android/view/SurfaceControl.java

前言

最后调研时间为2024.10.01

中途每每发现一些名称上看着可行的函数,便又会开始一波又一波大安卓源代码的阅读

结局都已失败告终

安卓 15 原生可支持切换副屏刷新率和大小,好像也能移动鼠标

https://www.androidauthority.com/android-15-external-display-settings-3474503/

目前我已知的是红魔和努比亚的设备可以切换刷新率和分辨率,这俩其实是一家的,技术应该是公用的

据朋友下载 ROM 加一系列的反编译,大概推断出是对内核做了支持

为什么要做这个调研? 如果成功,意义又是什么?

国内现在支持 USB3 的设备已经开始变多了,基本有 USB3 就会有 DP 输出视频的功能

虽然硬件支持了,但各个厂商,包括小米,对这个功能的软件适配就是一坨屎

鼠标不能再多个显示器间移动,不能设置外接显示器的刷新率,更甚者,像小米,把一个 Activity 在外接显示器上启动的时候(am start -n com.xxx.xxx/.xxxActivity --display x),

还不能占满全屏,真的是狗屎,换其他的支持 DP 输出的设备都没有这个问题

所以如果能有基于 SHELL 的方式,更改外接显示器的刷新率,外接显示器可用性也会大大提高

60hz -> 120hz,这个提升是非常明显的,并且像小米14,无线串流跑满 1080p 120hz 还是没问题的

其中最显著的例子是手机直连 AR 眼镜,很多 AR 眼镜都是支持 120hz 的,但是接上手机都只有 60hz

基本每一家 AR 眼镜都有对应的 App,他们可以通过硬件协议,快速的调整 AR 眼镜的刷新率,分辨率,例如 2D 切换到 3D 等,但这些 API 他们不愿意开放给开发者,目前 ROKID 是唯一一个愿意开放的,但是他们的 API 也不支持切换刷新率

更狗屎的是,类似于 XREAL,不愿意给你提供接口就算了,对开发者还及其的不尊重,后面我必贴完整聊天记录

几种方式总结

1.App 普通权限模式下设置

第一种,就是不借助任何的 SHELL 权限,直接在应用内通过 Android 给的方式进行切换

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

void changeDisplay() {
WindowManager windowManager = getWindowManager();
Display display = windowManager.getDefaultDisplay();
float[] supportedRefreshRates = display.getSupportedRefreshRates();
for (float rate : supportedRefreshRates) {
Log.d(TAG, "Supported refresh rate: " + rate + "Hz");
}
float desiredRefreshRate = 120.00001f;
boolean isSupported = false;
for (float rate : supportedRefreshRates) {
if (rate == desiredRefreshRate) {
isSupported = true;
break;
}
}
if (isSupported) {
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
layoutParams.preferredRefreshRate = desiredRefreshRate;
getWindow().setAttributes(layoutParams);
Log.d(TAG, "Refresh rate set to: " + desiredRefreshRate + "Hz");
} else {
Log.d(TAG, "Desired refresh rate is not supported.");
}

}

包括设置 layoutParams.preferredDisplayModeId ,都没有效果

2.SHELL 下反射 DisplayManager/DisplayManagerGlobal

这两个类是可以直接反射出来的,并且一些调用链路是 DisplayManager -> DisplayManagerGlobal

主要函数
下面都是反射能看到的函数

DisplayManagerGlobal.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
android.view.DisplayInfo getDisplayInfoLocked(int arg0)
float getBrightness(int arg0)
int[] getDisplayIds()
int[] getDisplayIds(boolean arg0)
android.view.DisplayInfo getDisplayInfo(int arg0)
android.view.Display getRealDisplay(int arg0)
int getRefreshRateSwitchingType()
android.view.Display$Mode getSystemPreferredDisplayMode(int arg0)
android.view.Display$Mode getUserPreferredDisplayMode(int arg0)
boolean shouldAlwaysRespectAppRequestedMode()
void setBrightness(int arg0,float arg1)
void setRefreshRateSwitchingType(int arg0)
void setShouldAlwaysRespectAppRequestedMode(boolean arg0)
void setUserPreferredDisplayMode(int arg0,android.view.Display$Mode arg1)
void setVirtualDisplaySurface(interface android.hardware.display.IVirtualDisplayCallback arg0,android.view.Surface arg1)

DisplayManager.java

1
2
3
4
5
6
7
8
9
10
android.view.Display getOrCreateDisplay(int arg0,boolean arg1)
float getBrightness(int arg0)
android.view.Display getDisplay(int arg0)
android.view.Display[] getDisplays()
android.view.Display[] getDisplays(String arg0)
android.view.Display$Mode getGlobalUserPreferredDisplayMode()
int getMatchContentFrameRateUserPreference()
void setGlobalUserPreferredDisplayMode(android.view.Display$Mode arg0)
void setShouldAlwaysRespectAppRequestedMode(boolean arg0)
void setRefreshRateSwitchingType(int arg0)

一些调用链路

1
2
3
4
5
6
DisplayManager.createVirtualDisplay -> DisplayManagerGlobal.createVirtualDisplay
DisplayManager.setBrightness -> DisplayManagerGlobal.setBrightness
DisplayManager.getBrightness -> DisplayManagerGlobal.getBrightness
DisplayManager.setGlobalUserPreferredDisplayMode -> DisplayManagerGlobal.setUserPreferredDisplayMode
DisplayManager.setRefreshRateSwitchingType -> DisplayManagerGlobal.setRefreshRateSwitchingType
DisplayManager.setShouldAlwaysRespectAppRequestedMode -> DisplayManagerGlobal.setShouldAlwaysRespectAppRequestedMode

然后我把跟切换刷新率有用的调了个遍,没有效果~

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
68
69
70
71
72
73
74
public static Object invokeMethod(Object object, String methodName, Object... args) {
try {
Class[] classes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Integer) {
classes[i] = int.class;
} else if (args[i] instanceof Boolean) {
classes[i] = boolean.class;
} else {
classes[i] = args[i].getClass();
}
}
Method method = object.getClass().getDeclaredMethod(methodName, classes);
method.setAccessible(true);
return method.invoke(object, args);
} catch (Exception e) {
e.printStackTrace();

}
return null;
}
/** @noinspection DataFlowIssue*/
@SuppressLint("PrivateApi")
void tryChangeDisplayConfig() {
L.d("bindServer invoke");
Class<?> clazz = null;
try {
clazz = Class.forName("android.hardware.display.DisplayManagerGlobal");
@SuppressLint("DiscouragedPrivateApi") java.lang.reflect.Method getInstanceMethod = clazz.getDeclaredMethod("getInstance");
Object dmg = getInstanceMethod.invoke(null);
ReflectUtil.invokeMethod(dmg, "setRefreshRateSwitchingType", 2);
//getRefreshRateSwitchingType
int type = (int) ReflectUtil.invokeMethod(dmg, "getRefreshRateSwitchingType");
L.d("RefreshRateSwitchingType -> " + type);
//noinspection JavaReflectionMemberAccess
DisplayManager displayManager = DisplayManager.class.getDeclaredConstructor(Context.class).newInstance(FakeContext.get());
int matchContentFrameRateUserPreference = (int) ReflectUtil.invokeMethod(displayManager, "getMatchContentFrameRateUserPreference");
L.d("matchContentFrameRateUserPreference -> " + matchContentFrameRateUserPreference);
// getSystemPreferredDisplayMode
Object systemMode = ReflectUtil.invokeMethod(dmg, "getSystemPreferredDisplayMode", 0);
L.d("SystemPreferredDisplayMode -> " + systemMode);
// same with adb shell cmd display get-user-preferred-display-mode 0
Object userMode = ReflectUtil.invokeMethod(dmg, "getUserPreferredDisplayMode", 0);
L.d("UserPreferredDisplayMode -> " + userMode);
// getGlobalUserPreferredDisplayMode
Object globalUserMode = ReflectUtil.invokeMethod(displayManager, "getGlobalUserPreferredDisplayMode");
L.d("GlobalUserPreferredDisplayMode -> " + globalUserMode);
ReflectUtil.invokeMethod(dmg, "setShouldAlwaysRespectAppRequestedMode", true);
boolean should = (boolean) ReflectUtil.invokeMethod(dmg, "shouldAlwaysRespectAppRequestedMode");
L.d("shouldAlwaysRespectAppRequestedMode -> " + should);
for (Display display : displayManager.getDisplays()) {
if (display.getDisplayId() != 0) {
L.d("display -> " + display);
L.d("");
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Display.Mode[] supportedModes = display.getSupportedModes();
for (Display.Mode mode : supportedModes) {
L.d("mode -> " + mode);
L.d("");
if (mode.getRefreshRate() > 60) {
L.d("set mode -> " + mode);
ReflectUtil.invokeMethod(dmg, "setUserPreferredDisplayMode", display.getDisplayId(), mode);
ReflectUtil.invokeMethod(displayManager, "setGlobalUserPreferredDisplayMode", mode);
}
}
}
}
}

} catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException |
IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}

3.SHELL 下反射 DisplayManagerService

安卓上有一系列的 CMD,例如执行 adb shell cmd display 会得到

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
➜  ~ adb shell cmd display
Display manager commands:
help
Print this help text.

set-brightness BRIGHTNESS
Sets the current brightness to BRIGHTNESS (a number between 0 and 1).
reset-brightness-configuration
Reset the brightness to its default configuration.
ab-logging-enable
Enable auto-brightness logging.
ab-logging-disable
Disable auto-brightness logging.
dwb-logging-enable
Enable display white-balance logging.
dwb-logging-disable
Disable display white-balance logging.
dmd-logging-enable
Enable display mode director logging.
dmd-logging-disable
Disable display mode director logging.
dwb-set-cct CCT
Sets the ambient color temperature override to CCT (use -1 to disable).
set-user-preferred-display-mode WIDTH HEIGHT REFRESH-RATE DISPLAY_ID (optional)
Sets the user preferred display mode which has fields WIDTH, HEIGHT and REFRESH-RATE. If DISPLAY_ID is passed, the mode change is applied to displaywith id = DISPLAY_ID, else mode change is applied globally.
clear-user-preferred-display-mode DISPLAY_ID (optional)
Clears the user preferred display mode. If DISPLAY_ID is passed, the mode is cleared for display with id = DISPLAY_ID, else mode is cleared globally.
get-user-preferred-display-mode DISPLAY_ID (optional)
Returns the user preferred display mode or null if no mode is set by user.If DISPLAY_ID is passed, the mode for display with id = DISPLAY_ID is returned, else global display mode is returned.
get-active-display-mode-at-start DISPLAY_ID
Returns the display mode which was found at boot time of display with id = DISPLAY_ID
set-match-content-frame-rate-pref PREFERENCE
Sets the match content frame rate preference as PREFERENCE
adb shell cmd display get-match-content-frame-rate-pref
get-match-content-frame-rate-pref
Returns the match content frame rate preference
set-user-disabled-hdr-types TYPES...
Sets the user disabled HDR types as TYPES
get-user-disabled-hdr-types
Returns the user disabled HDR types
get-displays [CATEGORY]
Returns the current displays. Can specify string category among
DisplayManager.DISPLAY_CATEGORY_*; must use the actual string value.
dock
Sets brightness to docked + idle screen brightness mode
undock
Sets brightness to active (normal) screen brightness mode
...more
➜ ~

而这个命令的实现,就是来自 DisplayManagerShellCommand.java

像 setMatchContentFrameRateUserPreference 的调用链路是

DisplayManagerShellCommand.setMatchContentFrameRateUserPreference -> DisplayManager.setMatchContentFrameRateUserPreference

而其他的一些设置,则来自 DisplayManagerService.java

DisplayManagerService 有一个构造函数

1
2
3
public DisplayManagerService(Context context) {
this(context, new Injector());
}

但是这个类是在系统框架里面的,也就是对应 ROM 中的 /system/framework/services.jar

需要这样拿实例

1
2
3
4
5
6
7
8
9
10
11
Class<?> classLoaderFactoryClass = Class.forName("com.android.internal.os.ClassLoaderFactory");
Method createClassLoaderMethod = classLoaderFactoryClass.getDeclaredMethod(
"createClassLoader", String.class, String.class, String.class,ClassLoader.class, int.class, boolean.class, String.class
);
ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, "/system/framework/services.jar", null, null,
ClassLoader.getSystemClassLoader(), 0, true, null);
Method loadMethod = Runtime.class.getDeclaredMethod("loadLibrary0", Class.class, String.class);
loadMethod.setAccessible(true);
Class cl = classLoader.loadClass("com.android.server.display.DisplayManagerService");
loadMethod.invoke(Runtime.getRuntime(), cl, "android_servers");
Object dms = cl.getDeclaredConstructor(android.content.Context.class).newInstance(FakeContext.get());

FakeContext 是来自 Scrcpy 的代码

其中一些有用的函数

1
2
3
4
5
6
7
8
9
10
11
android.view.Display$Mode getActiveDisplayModeAtStart(int arg0)
com.android.server.display.DisplayDeviceInfo getDisplayDeviceInfoInternal(int arg0)
int getRefreshRateSwitchingTypeInternal()
android.view.Display$Mode getSystemPreferredDisplayModeInternal(int arg0)
android.view.Display$Mode getUserPreferredDisplayModeInternal(int arg0)
void setUserPreferredModeForDisplayLocked(int arg0,android.view.Display$Mode arg1)
void setVirtualDisplayStateInternal(interface android.os.IBinder arg0,boolean arg1)
void setVirtualDisplaySurfaceInternal(interface android.os.IBinder arg0,android.view.Surface arg1)
void setRefreshRateSwitchingTypeInternal(int arg0)
void setShouldAlwaysRespectAppRequestedModeInternal(boolean arg0)
void setUserPreferredDisplayModeInternal(int arg0,android.view.Display$Mode arg1)

然后我又掉了个通,无果~

4.SHELL 下反射 SurfaceControl.setBootDisplayMode

上一节发现 DisplayManagerService 有 getActiveDisplayModeAtStart 函数,
我们也可以使用 DisplayManagerShellCommand 的 get-active-display-mode-at-start

1
2
$ adb shell cmd display get-active-display-mode-at-start 2
Boot display mode: 1920 1080 60.000004

这个结果即为 AR 眼镜连接到手机时的分辨率和刷新率
如果能更改这个值,是不是也是可行的?

然后发现 SurfaceControl 有一个 setBootDisplayMode 函数

函数原型

1
2
3
4
5
6
7
public static void setBootDisplayMode(IBinder displayToken, int displayModeId) {
if (displayToken == null) {
throw new IllegalArgumentException("displayToken must not be null");
}

nativeSetBootDisplayMode(displayToken, displayModeId);
}

要两个东西,一个是 Token,一个是 ModeId

这里也有坑,Token 要从 DisplayControl.getPhysicalDisplayToken 获取

DisplayControl 也来自框架层,不过 scrcpy 中有现成的代码

而后者的 displayModeId 也并不是 DisplayInfo 中的 Display.Mode [] 数组中的 ModeId

先来看 Token 的获取吧,参考 scrcpy 的代码,这个 token 非常好获取

注意传入的 ID 是 DisplayControl.getPhysicalDisplayIds() 得到的 ID,普通的 Display ID 是 int,这个数据类型是 long

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
if (getPhysicalDisplayTokenMethod == null) {
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
}
return getPhysicalDisplayTokenMethod;
}

public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
try {
Method method = getGetPhysicalDisplayTokenMethod();
return (IBinder) method.invoke(null, physicalDisplayId);
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return null;
}
}

问题在于 displayModeId

需要先获取 Display 的 DynamicDisplayInfo

SurfaceControl.java

1
2
3
public static DynamicDisplayInfo getDynamicDisplayInfo(long displayId) {
return nativeGetDynamicDisplayInfo(displayId);
}

DynamicDisplayInfo 的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static final class DynamicDisplayInfo {
public DisplayMode[] supportedDisplayModes;
public int activeDisplayModeId;
public float renderFrameRate;

public int[] supportedColorModes;
public int activeColorMode;

public Display.HdrCapabilities hdrCapabilities;

public boolean autoLowLatencyModeSupported;
public boolean gameContentTypeSupported;

public int preferredBootDisplayMode;

// 省略一些代码
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(supportedDisplayModes), activeDisplayModeId,
renderFrameRate, activeColorMode, hdrCapabilities);
}
}

DynamicDisplayInfo 的 supportedDisplayModes 中的 DisplayMode 的 id,才是我们调用 setBootDisplayMode 需要的 ModeId

然后调用后,无果~

adb shell cmd display get-active-display-mode-at-start 2 的结果也没有变化

ROKID API 调研结果

我手上有雷鸟的 AR 眼镜,有 XREAL 的 AR 眼镜

这些眼镜厂商都有自己的安卓 App,然后配合眼镜内的固件

插个题外话,这两家厂商都不开放 Android SDK,硬生生的把自己的生态给堵死

他们从未想过如果开放 API,会有开发者借助 API 开发出各种各样的可以加强他们眼镜功能的 App

也不知道他们怎么想的吧,程序员永远理解不了产品经理

觉得自己有几百人的研发,能把自己的生态弄得很好

大概是制定一些协议,通过串口通信的方式,来控制眼镜

从表现上来说,当接上对应的眼镜进入 App 时,眼镜会自动切换为 3D 模式

这个就是 AR 的 SDK 完成的

我问过 XREAL 要过接口,因为他们只有一些 Unity 的 SDK,可很多功能又不需要引入 Unity,不愿意给态度还及其高傲,声称就算他的 SDK 是汇编写的,也会有人用

抛开 AR 眼镜本身来看,他们的生态就是一坨屎,Beam Pro 的设计也是一坨屎,最基本的功能都没做就发布了

闭关锁国,取死之道

Rokid 是跟我合作的,唯一一个愿意提供 Android SDK 的,但是整个过程也不是很顺畅

通过这个 SDK 可以控制 Rokid 的 AR 眼镜

这个时候,我其实是有点期望的,以为通过 SDK 可以直接修改刷新率

还是由于各种信息差导致的

SDK 给到我后

并不能更改到 2D 1080P 90/120 hz 的刷新率

后来他把他们的研发拉了进来

沟通上也不是很顺畅

并且从研发到研发的沟通,都还有很多信息差

细节后面补上

他们的接口是隐藏的

并且很多参数是枚举类,只有到最底层,才通过枚举的值来调用 JNI 的函数

SDK 导入到 Android 工程后,根本无法看到下面的接口

经过我对 SDK 的逆向,才写出了这些代码

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
RKGlassDevice mRKGlassDevice = RKGlassDevice.getInstance();
Class<?> rkGlassDeviceClass = mRKGlassDevice.getClass();
// 获取mGlassControl字段
Field mGlassControlField = rkGlassDeviceClass.getDeclaredField("mGlassControl");
mGlassControlField.setAccessible(true);
// 获取字段值
Object mGlassControl = mGlassControlField.get(mRKGlassDevice);
Log.d(TAG, "mGlassControl:" + mGlassControl);

Field mGlassSDKField = mGlassControl.getClass().getDeclaredField("mGlassSDK");
mGlassSDKField.setAccessible(true);
Object mGlassSDK = mGlassSDKField.get(mGlassControl);

// 反射出mGlassSDK的NativeHandle(long)
Field mNativeHandleField = mGlassControl.getClass().getDeclaredField("NativeHandle");
mNativeHandleField.setAccessible(true);
long mNativeHandle = mNativeHandleField.getLong(mGlassControl);
Log.d(TAG, "mNativeHandle" + mNativeHandle);

// 反射出mGlassSDK的GlassSetDisplayMode方法,返回boolean,参数是long,int
Method glassSetDisplayModeMethod = mGlassSDK.getClass().getDeclaredMethod("GlassSetDisplayMode", long.class, int.class);
glassSetDisplayModeMethod.setAccessible(true);
boolean result = (boolean) glassSetDisplayModeMethod.invoke(mGlassSDK, mNativeHandle, 3);
Log.d(TAG, "result" + result);

// 反射出mGlassSDK的GetDisplayMode方法,返回int,参数是long
Method getDisplayModeMethod = mGlassSDK.getClass().getDeclaredMethod("GetDisplayMode", long.class);
getDisplayModeMethod.setAccessible(true);
int displayMode = (int) getDisplayModeMethod.invoke(mGlassSDK, mNativeHandle);
Log.d(TAG, "displayMode" + displayMode);

通过一系列的反射,终于把这个函数给调通了,但是我再通过 Android API 获取 Display 的刷新率,并没有任何变化

只发现有一些值不一样
adb shell cmd display get-displays

1
2
3
4
5
# 调用接口前
Display id 11: DisplayInfo{\"HDMI 屏幕\", displayId 11, displayGroupId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_PRESENTATION, FLAG_TRUSTED, real 1920 x 1080, largest app 1920 x 1920, smallest app 964 x 964, appVsyncOff 1000000, presDeadline 16666666, mode 6560.000004, defaultMode 65, modes [{id=65, width=1920, height=1080, fps=60.000004, alternativeRefreshRates=[], supportedHdrTypes=[1, 2, 3]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[1, 2, 3], mMaxLuminance=500.0, mMaxAverageLuminance=250.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state ON, committedState ON}, DisplayMetrics{density=1.3312501, width=1920, height=964, scaledDensity=1.3312501, xdpi=304.8, ydpi=304.8}, isValid=true

# 调用接口后
Display id 11: DisplayInfo{\"HDMI 屏幕\", displayId 11, displayGroupId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_PRESENTATION, FLAG_TRUSTED, real 1920 x 1080, largest app 1920 x 1920, smallest app 964 x 964, appVsyncOff 1000000, presDeadline 16666666, mode 6160.000004, defaultMode 61, modes [{id=61, width=1920, height=1080, fps=60.000004, alternativeRefreshRates=[120.00001], supportedHdrTypes=[1, 2, 3]}, {id=62, width=1920, height=1200, fps=120.00001, alternativeRefreshRates=[60.000004], supportedHdrTypes=[1, 2, 3]}, {id=63, width=1920, height=1200, fps=60.000004, alternativeRefreshRates=[120.00001], supportedHdrTypes=[1, 2, 3]}, {id=64, width=1920, height=1080, fps=120.00001, alternativeRefreshRates=[60.000004], supportedHdrTypes=[1, 2, 3]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[1, 2, 3], mMaxLuminance=500.0, mMaxAverageLuminance=250.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state ON, committedState ON}, DisplayMetrics{density=1.3312501, width=1920, height=964, scaledDensity=1.3312501, xdpi=304.8, ydpi=274.32}, isValid=true

啊这,调用接口后,眼镜 DisplayInfo 中的 Mode 变多了,仅此而已

而 XREAL 的眼镜默认就有多种 Mode,我调用接口后得到了其他家眼镜默认的状态?

也就是说,这个接口并不能切换刷新率

折腾半天,得出一个结论

调用这个接口和长按眼镜的音量+键,效果一样

我直接 What,那最初是不是就应该告知我这个信息

而为什么调用接口又能设置为 3D 3840x1080 90hz 呢

因为对于 3D 模式,通过串口通信到眼镜内的固件后,这个眼镜就变成了一个只有 3D 模式的眼镜

AR 眼镜对手机还是 PC 来说,都只是一个普通的显示器

如果这个显示器只支持 3840x1080 90hz,你接上手机,它应该亮还是不亮

如果亮的话,眼镜的显示信息也就必然是 3840x1080 90hz

这个对于 PC 来说也是一样

所以安卓自然而然的切换到了 3D,并且有高刷

所以关键是,如果要实现通过 AR SDK 修改刷新率,硬件那边就将设备设置为只有某个刷新率的 Mode 即可,但是很显然,这个就得给他们加需求了

也不知道他们愿不愿意配合,因为大部分 AR 厂商的任何人,包括产品,研发,运营,都会觉得这些都是伪需求,自己的生态已经无敌了,几句沟通就要把同行踩个遍(指XREAL)

所以,这条路,估计也难走通

Termux API深度学习与Termux USB问题解决

需要先理解的一些上下文

termux 为了能在命令行中使用 am 命令,https://github.com/termux/termuxAm

因为 Android 上的 am 会限制只能在 shell 中使用,termux-am 就是一个通用版本

背景

termux 为了能让用户能在 terminal 中调用 Android API,开发了 termux-api

可以看下 termux-api-package 编译会生成的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
termux-api-start.in            termux-media-player.in         termux-sms-inbox.in
termux-api-stop.in termux-media-scan.in termux-sms-list.in
termux-audio-info.in termux-microphone-record.in termux-sms-send.in
termux-battery-status.in termux-nfc.in termux-speech-to-text.in
termux-brightness.in termux-notification-channel.in termux-storage-get.in
termux-call-log.in termux-notification-list.in termux-telephony-call.in
termux-camera-info.in termux-notification-remove.in termux-telephony-cellinfo.in
termux-camera-photo.in termux-notification.in termux-telephony-deviceinfo.in
termux-clipboard-get.in termux-saf-create.in termux-toast.in
termux-clipboard-set.in termux-saf-dirs.in termux-torch.in
termux-contact-list.in termux-saf-ls.in termux-tts-engines.in
termux-dialog.in termux-saf-managedir.in termux-tts-speak.in
termux-download.in termux-saf-mkdir.in termux-usb.in
termux-fingerprint.in termux-saf-read.in termux-vibrate.in
termux-infrared-frequencies.in termux-saf-rm.in termux-volume.in
termux-infrared-transmit.in termux-saf-stat.in termux-wallpaper.in
termux-job-scheduler.in termux-saf-write.in termux-wifi-connectioninfo.in
termux-keystore.in termux-sensor.in termux-wifi-enable.in
termux-location.in termux-share.in termux-wifi-scaninfo.in

以 termux-toast 为例

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
#!@TERMUX_PREFIX@/bin/bash
set -e -u

SCRIPTNAME=termux-toast
show_usage () {
echo "Usage: termux-toast [-b bgcolor] [-c color] [-g gravity] [-s] [text]"
echo "Show text in a Toast (a transient popup)."
echo "The toast text is either supplied as arguments or read from stdin"
echo "if no arguments are given. Arguments will take precedence over stdin."
echo "If toast text is not passed as arguments or with stdin, then there will"
echo "be a 3s delay."
echo " -h show this help"
echo " -b set background color (default: gray)"
echo " -c set text color (default: white)"
echo " -g set position of toast: [top, middle, or bottom] (default: middle)"
echo " -s only show the toast for a short while"
echo "NOTE: color can be a standard name (i.e. red) or 6 / 8 digit hex value (i.e. \"#FF0000\" or \"#FFFF0000\") where order is (AA)RRGGBB. Invalid color will revert to default value"
exit 0
}

PARAMS=""
while getopts :hsc:b:g: option
do
case "$option" in
h) show_usage;;
s) PARAMS+=" --ez short true";;
c) PARAMS+=" --es text_color $OPTARG";;
b) PARAMS+=" --es background $OPTARG";;
g) PARAMS+=" --es gravity $OPTARG";;
?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1;
esac
done
shift $((OPTIND-1))

CMD="@TERMUX_PREFIX@/libexec/termux-api Toast $PARAMS"

# If toast text was not passed as an argument, then attempt to read from STDIN with a 3s timeout
# Toast text arguments takes precedence over STDIN
if [ $# = 0 ]; then
set +e; IFS= read -t 3 -r -d '' TOAST_TEXT; set -e;
else
TOAST_TEXT="$*"
fi

# Trim trailing newlines
TOAST_TEXT="$(echo "$TOAST_TEXT")"

echo "$TOAST_TEXT" | $CMD

其中关键的调用是 CMD="@TERMUX_PREFIX@/libexec/termux-api Toast $PARAMS"

也就是所有的这一系列的脚本都会调用 termux-api 这个命令

一些命令

1
2
3
4
5
6
$ termux-usb -l
[
"/dev/bus/usb/001/002",
]
$ termux-usb -r /dev/bus/usb/001/002
Access granted.

一些更神奇的命令

1
2
$ termux-usb -e echo -r /dev/bus/usb/001/002
7

没错,这行命令运行后,会先弹出一个对话框,询问是否允许访问 USB 设备,用户按下是后,会在 terminal 中输出 7

这个7就是文件描述符,后续的任意进程拿到这个文件描述符,就能够读写了

对应的 java 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
private static int open(final @NonNull UsbDevice device, final Context context) {
final UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
UsbDeviceConnection connection = usbManager.openDevice(device);
if (connection == null)
return -2;
int fd = connection.getFileDescriptor();
if (fd == -1) {
connection.close();
return -1;
}
openDevices.put(fd, connection);
return fd;
}

adb 不能在 Android 本地运行并且能够访问 OTG 的本质原因是,对 Linux/macOS/Windows 的平台而言,普通应用也是可以读取 USB 串口设备的,但是安卓上访问需要权限

而有了这种方式,我如果把上述的代码换成

1
$ termux-usb -e custom_adb -r /dev/bus/usb/001/002

交给一个特殊的 adb 一个文件描述符,adb 在 Andoird 本机运行且可以访问 OTG 就不是问题了

Uncon、ADB KIT 也是这样做的

研究 termux-api

https://github.com/termux/termux-api-package

里面包含了上述的脚本,还有两个 C 代码

termux-api-broadcast.c 和 termux-api.c

实际在 termux 终端中执行的 termux-api 二进制是由 termux-api-broadcast.c 编译而来

termux-api 有两种模式,一种是通过 broadcast 方式,一种是通过 socket 方式

整体架构设计图

接下来我们分析这两种模式

Socket

socket 方式需要先启动

App 启动 -> SocketListener.createSocketListener -> Bind Address

随即会监听 com.termux.api://listen 这个 unix socket

执行 termux-api 这个命令的时候,会将参数都传递到 socket 中

然后由 SocketListener 来解析参数,再发送一个广播到 TermuxApiReceiver

TermuxApiReceiver 会解析第一个参数以匹配需要调用的 API 类型,例如 Toast、USB

部分代码如下:

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
switch (apiMethod) {
case "Clipboard":
ClipboardAPI.onReceive(this, context, intent);
break;
case "Dialog":
DialogAPI.onReceive(context, intent);
break;
case "Download":
DownloadAPI.onReceive(this, context, intent);
case "Notification":
NotificationAPI.onReceiveShowNotification(this, context, intent);
break;
case "SAF":
SAFAPI.onReceive(this, context, intent);
break;
case "Toast":
ToastAPI.onReceive(context, intent);
break;
case "Usb":
UsbAPI.onReceive(this, context, intent);
break;
case "Volume":
VolumeAPI.onReceive(this, context, intent);
break;
default:
Logger.logError(LOG_TAG, "Unrecognized 'api_method' extra: '" + apiMethod + "'");
}

此时数据也通过套接字回传到 termux-api

Broadcast

termux-api 直接调用 termux-am,然后直接发送广播到 TermuxApiReceiver,同时会告知 TermuxApiReceiver 往哪个 address 回传返回的数据,但是这个在 Android 14上失效了,不知道为啥
并且 termux-api 在代码中

1
2
3
if (android_get_device_api_level() < 34) {
listenfd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
}

也就是只有当 Android 版本小于14的时候,才会使用 socket 方式

大于等于 14 的时候,就会使用 broadcast 方式

而 boardcast 在 Android 14上又是失效的,这就是个大坑

termux 之所以不会遇到这个问题,是因为另一个究极无敌破坏性的改动,导致他们的 targetSdk 一直是 28,升上28以后,termux 就不能用了,所以 Termux 的 targetSdk 一直是 28,并且不能再上架 Google Play

解决

直接把这段 if 代码注释掉就行了,不知为何会加这个限制

1
2
3
if (android_get_device_api_level() < 34) {
listenfd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
}

源码的注释是

1
2
3
4
5
6
7
8
9
// Try to connect over the listen socket first if running on Android `< 14`.
// On Android `>= 14`, if termux-api app process was started previously
// and it started the socket server, but later Android froze the
// process, the socket will still be connectable, but no response
// will be received until the app process is unfrozen agin and
// `read()` call below will hang indefinitely until that happens,
// so use legacy `am broadcast` command, which will also unfreeze
// the app process to deliver the intent.
// - https://github.com/termux/termux-api/issues/638#issuecomment-1813233924

第二个Android14的问题

这是在研究 Android Local ADB 的时候,遇到的问题

解决了上面的问题后,还有一个新的问题

先看 termux-usb 源码

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
68
69
70
71
#!/system/bin/sh
# set -e -u

SCRIPTNAME=termux-usb
show_usage () {
echo "Usage: $SCRIPTNAME [-l | [-r] [-E] [-e command] [device | vendorId productId]]"
echo "List or access USB devices. Devices cannot be accessed directly,"
echo " only using $SCRIPTNAME."
echo " -l list available devices"
echo " -r show permission request dialog if necessary"
echo " -e command execute the specified command with a file descriptor"
echo " referring to the device as an argument (unless -E"
echo " argument is given)"
echo " -E transfer file descriptor as env var instead of as"
echo " command line argument"
exit 0
}

ACTION="permission"
PARAMS=""
while getopts :hlre:E option
do
case "$option" in
h) show_usage;;
l) ACTION="list";;
r) PARAMS="$PARAMS --ez request true";;
e) ACTION="open"; export TERMUX_CALLBACK="$OPTARG";;
E) export TERMUX_EXPORT_FD=true;;
?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1;
esac
done
shift $((OPTIND-1))

if [ "$ACTION" == "list" ]
then
if [ $# -gt 0 ]; then echo "$SCRIPTNAME: too many arguments"; exit 1; fi
else
if [ $# -lt 1 ]; then
echo "$SCRIPTNAME: missing -l or device path"
exit 1
elif [ $# -eq 1 ]; then
# The device's usbfs path has been provided
PARAMS="$PARAMS --es device $1"
elif [ $# -eq 2 ]; then
# A vendorId and ProductId of the device has been provided
PARAMS="$PARAMS --es vendorId $1"
PARAMS="$PARAMS --es productId $2"
else
echo "$SCRIPTNAME: too many arguments"
exit 1
fi
fi
echo "TERMUX_CALLBACK:$TERMUX_CALLBACK"
echo "TERMUX_EXPORT_FD:$TERMUX_EXPORT_FD"
CMD="termux-api Usb -a $ACTION $PARAMS"
termux-toast "CMD:$CMD"
echo "CMD:$CMD"

if [ "$ACTION" == "permission" ]
then
if [ "$($CMD)" == "yes" ]
then
echo "Access granted."
exit 0
else
echo "Access denied."
exit 1
fi
else
$CMD
fi

当我在 Android 14上运行 termux-usb -l 的时候,一切正常

但是把命令换成 termux-usb -r /dev/bus/usb/001/002 的时候,窗口能正常弹出,权限能正常授予,但是进程却没有返回结果

一直卡住,直到 Android 系统提示应用无响应,再等待一会,进程才返回

这就是第二个坑

还是 termux target sdk 是 28 的问题,他们不会遇到这个情况

关键问题代码

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
private static boolean requestPermission(final @NonNull UsbDevice device, final Context context) {
Looper.prepare();
Looper looper = Looper.myLooper();
final boolean[] result = new boolean[1];

final String ACTION_USB_PERMISSION = TermuxConstants.TERMUX_API_PACKAGE_NAME + ".USB_PERMISSION";
final BroadcastReceiver usbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context usbContext, final Intent usbIntent) {
String action = usbIntent.getAction();
if (ACTION_USB_PERMISSION.equals(action)) {
synchronized (this) {
UsbDevice device = usbIntent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (usbIntent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
if (device != null) {
result[0] = true;
if (looper != null) looper.quit();
}
} else {
result[0] = false;
if (looper != null) looper.quit();
}
}

}
}
};

final UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
PendingIntent permissionIntent = PendingIntent.getBroadcast(context, 0,
new Intent(ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
context.getApplicationContext().registerReceiver(usbReceiver, filter);
usbManager.requestPermission(device, permissionIntent);
Looper.loop();
return result[0];
}

按理说,PendingIntent 请求的弹出,我点击确认后,就会发出一个广播,这个内部的 usbReceiver 就能立马接收到,然后返回结果

但并不会

后来我尝试将 PendingIntent 的构造换成 PendingIntent.getActivity,授予权限后,Activity 马上就被打开了,通过 UsbManager.EXTRA_PERMISSION_GRANTED 字段,也可以知道权限是否被授予

但是整个流程就会有新的 Activity 打开

于是我再将 PendingIntent 的构造换成 PendingIntent.getService,这也是最后的解决方法

关键的修改代码如下

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

public class LocalService extends Service {
private final IBinder binder = new LocalBinder();
private ServiceCallback callback;

public interface ServiceCallback {
void onServiceStarted(boolean state);
}

public class LocalBinder extends Binder {
public LocalService getService() {
return LocalService.this;
}
}

@Override
public IBinder onBind(Intent intent) {
Logger.logError("LocalService", "onBind");
return binder;
}

@Override
public void onCreate() {
super.onCreate();
Logger.logError("LocalService", "Service Created");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final String ACTION_USB_PERMISSION = TermuxConstants.TERMUX_API_PACKAGE_NAME + ".USB_PERMISSION";
Logger.logError("LocalService", "Service Started");
String action = intent.getAction();
Logger.logInfo("action -> " + action);
if (ACTION_USB_PERMISSION.equals(action)) {
synchronized (this) {
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
if (device != null) {
callback.onServiceStarted(true);
}
} else {
callback.onServiceStarted(false);
}
}

}
return START_STICKY;
}

public void setCallback(ServiceCallback callback) {
this.callback = callback;
}
}
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
private static boolean requestPermission(final @NonNull UsbDevice device, final Context context) {
Looper.prepare();
Looper looper = Looper.myLooper();
final boolean[] result = new boolean[1];
Intent intent = new Intent(context, LocalService.class);
final String ACTION_USB_PERMISSION = TermuxConstants.TERMUX_API_PACKAGE_NAME + ".USB_PERMISSION";
final ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Logger.logInfo("onServiceConnected");
LocalService localService;
LocalService.LocalBinder binder = (LocalService.LocalBinder) service;
localService = binder.getService();
localService.setCallback(new LocalService.ServiceCallback() {
@Override
public void onServiceStarted(boolean state) {
result[0] = true;
if (looper != null) looper.quit();
Logger.logInfo(">onServiceStarted");
}
});
final UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
intent.setAction(ACTION_USB_PERMISSION);
PendingIntent permissionIntent = PendingIntent.getService(context, 0, intent, 0);
usbManager.requestPermission(device, permissionIntent);
}

@Override
public void onServiceDisconnected(ComponentName arg0) {
}
};
Logger.logInfo("bindService");
context.getApplicationContext().bindService(intent, connection, Context.BIND_AUTO_CREATE);
Logger.logInfo("bindService done");
Looper.loop();
return result[0];
}

这个代码实现后,ADB KIT 和 Uncon 就能极快的获取到连接的 OTG 设备了

当然文中还有一个地方没有展开,就是那个 custom_adb,到底是如何设计的,也许会再有一篇文章来讲解

玲珑开发学习记录

个人介绍

梦魇兽,大家叫我小梦即可

  • 本科毕业入职滴滴半年升职,滴滴高级架构研发工程师
  • 两年绩效最优
  • 现裸辞,找寻生命的意义
  • 个人有比较多的软件,比如速享、无界、ADBKIT、Code FA,我个人软件涉及的技术栈远比我在公司所接触的多得多

所以我经常会说,技术本身会是各种过程中,最小的问题

再见滴滴研发,你好梦魇兽

这个博客记录我的一些想法和学习的一些资料

玲珑后台服务

需求场景是,控制风扇转速和设置 TDP 功耗都需要管理员权限,所以玲珑管家在编译的时候就有设置请求管理员权限
这是通过 cmakefile 来实现的

1
SET_TARGET_PROPERTIES(${BINARY_NAME} PROPERTIES LINK_FLAGS    "/MANIFESTUAC:\"level='requireAdministrator' uiAccess='false'\" /SUBSYSTEM:WINDOWS")

所以管理员权限是玲珑的一个必备条件,但是刚开机的时候,用户是没有空间给到玲珑管家管理员权限的

所以我编写一个玲珑服务

这个服务依赖 Windows 的任务计划,开机后不用进入系统,就能把服务启起来,并且能够带有管理员权限

这个目前在玲珑打开的时候就会执行,不用用户手动创建任务计划

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
# 获取脚本所在目录
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path

# 要启动的程序路径
$programPath = Join-Path $scriptDir "tiny_server.exe"

# 任务名称
$taskName = "TinyManagerAutoStart"

# 检查任务是否存在
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue

if ($task) {
# 如果任务存在,先删除任务
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
Write-Output "任务已删除。"
}

# 创建新的任务
$action = New-ScheduledTaskAction -Execute $programPath
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable

# 注册任务计划
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings
Write-Output "任务已创建并设置为开机启动。"

后台服务隐藏终端

两种方式

一种是默认配置 VS 工程,调用 FreeConsole 函数隐藏终端,再设置一个循环,让程序不退出

1
2
3
4
FreeConsole();
while (running) {
std::this_thread::sleep_for(std::chrono::seconds(10));
}

另一种是直接更改程序子系统为 /SUBSYSTEM:WINDOWS,这样就不会有终端出现,但是这种方案需要改代码入口

1
2
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
}

Windows 共享库

我尝试编写可以交叉编译的 Cmakefile,但是失败了,后面的编译都是通过 Visual Studio 来编译的

需要有些配置

共享库开发好,后面不管是使用任何的上层框架都可以方便的调用

两种方法,一种是设置宏,一种是 .def 文件

def

所以底层对外的输出应该是一个 dll 和一个 .h 文件

例如 dart,可以直接根据 .h 文件生成 dart 文件,可以很方便的调用 dll 中的函数

简单说就是,.h 文件维护好,我就能以很好的代码在各个地方调用

风扇控制

这个我得到的输入是一大坨 C# 代码和这样的文本消息,有点难理解,不过后面代码已经可以控制了,

1
2
3
4
5
6
7
8
9
ARB33
Fan 控制权:0xF02
写0x6C:手动控制转速
写0x00:自动转速

控制Fan :0x1809
写0~0xB8(0xB8对应100%)

读转速:0x0218~0x0219

https://github.com/GermanAizek/WinRing0

然后调研到更改内存的 WingRing0 库

最后经过好些折腾才写出了对应的 dart 代码,不仅是 dart,其他代码要实现控制风扇也比较麻烦

更优解是做成类似于 parsec 的驱动,需要控制风扇的程序根据驱动协议来调用

功耗设置

这个我没有得到可用的输入,自己找到一个是 ryzenadj-win64
对玲珑也是可用的

虚拟显示器

这个目前找到两个关键的库,一个是 parsec-vdd,一个是 Virtual-Display-Driver
https://github.com/nomi-san/parsec-vdd
https://github.com/itsmikethetech/Virtual-Display-Driver

后者我目前没有学会如何使用,所以目前都是用的 parsec-vdd

parsec-vdd 需要安装驱动,但是这个后面也可以自动安装

目前已经实现了开机不需要进入系统就能创建虚拟显示器。

串流软件相关

我个人 Moonlight+Sunshine,或者 Parsec 都是比较大量使用的,我其实不理解为什么 Sunshine 和 Moonlight 会有这么多的分支

但我后面会再详细了解一些这些原因,例如 Sunshine 有阿姨版本,为什么不能往官方提 PR,因为我自己就是一个极度热爱开源的人

者我通常用来玩儿游戏,后者我通常是非游戏需求,因为他支持剪切板共享

Parsec 使用成本其实相对要更低一点

并且我在玲珑上装了 Parsec 后,另一台电脑 A、B 或者 Android 装了 Parsec

不仅 A、B 可以控制玲珑

玲珑是可以直接控制电脑 A、B 的

所以一部分用户肯定是要使用 Parsec 的,并且使用 Moonlight 和 Parsec 的用户都要同等看待

不能直接说 Parsec 的用户就有解决问题的能力,玲珑管家都不适配 Parsec 了

关于玲珑的一点思考

也是我在使用玲珑的一个感受,我家里有 ITX 主机,平时 Mac 也是不离身的,也就是说,我出门,Mac 是一定会带的

那么玲珑对于我这类用户,就是一个比较尴尬的定位

但是经过一些时间,我发现了他的一些使用场景

就是作为串流机

也就是借助玲珑连接我家里的 ITX,这样我不管在哪儿,都能以家里的性能玩儿到游戏

为什么这个设备不是 Mac 呢,因为我手上测试过 5 款手柄,包括飞智,这些手柄都不支持有线连接 Mac

唯一一款支持有线 Mac 的是 Xbox 手柄,但是 Xbox 手柄有没有其他手柄的震感、陀螺仪等

有线可以让我有一个更低的延迟

所以我经常用玲珑去串流我家里的设备

不要再给用户发任何的demo了

玲珑0.0.2

暂时先放我的博客上,因为有一些注意事项,群消息容易刷没

之前群里有朋友调整功率会花屏,我这一直不能复现

下载地址

http://bkkj.cc/0b

0.0.2更新日志

  • 修复创建虚拟显示器马上就没了的问题
  • 支持开机不用连物理显示器也能串流
  • 支持玲珑服务开机自启,这个在任务管理器中看不到,用的任务计划,开机后不用输密码进系统也能自启

玲珑服务会检测当前是否有连接的显示器,如果没有,则会创建一个

开机后用任务管理器搜进程 tiny_server.exe 有存在即可

安装 parsec vdd 驱动

运行解压后的 parsec-vdd-0.45.0.0.exe

管家启动

打开 tiny_manager.exe 即可

需要先打开一次,再重启才支持创建虚拟显示器

再见滴滴研发,你好梦魇兽

前言

哈喽大家,我是一名个人开发者,常用的昵称是梦魇兽,在入职滴滴之前,也有一些技术相关的文章编写在掘金社区。 一个传送门

我是一个热爱代码,音乐的人,MBTI 是 INTP,很好区分我与猫头鹰的区别,因为我的头不能旋转180度。

我在天空之城 B 座,4楼,可能也不少时间吓到过男厕的兄弟们,因为我留了大概齐肩的长发

我的眼神应该常常是呆滞的,偶尔也会带上我的脑花帽子

今天作为离职前的最后一次分享

此篇有凡尔赛,有装逼,如有介意者可直接退出

离职

简单说下离职的原因

2023年度中度抑郁转了重度抑郁,有较多的躯体症状,例如频繁的心悸,长时间的喘不过气,没有精神,情绪不稳定等

这是打印的病例以及药方

抑郁症的原因是由原生家庭导致的,至今我的生父生母不知道我在哪儿工作,不知道我生病,甚至不知道我是否还活着,刚大学毕业,我因为学费,和实习来北京入不敷出,刚毕业负债10多w,这对于当时的我来说,也是一个很大的压力

我从未主动告诉部门同事自己有抑郁症,有两个原因吧,但是Tom翻到了我写的博客

1.我不希望大家把我当成一个病人
2.如果我有任何工作上的失误,我希望直接指责我,不应该由于我有抑郁症就会对我有额外的包容

我想要的,就是公平的和大家在这个公司,并且能够靠自己的能力换来尊重,而不是抑郁症

这个病给我最大的感受就是,孤独,和绝望,整个抗抑的三年,只有自己独自一人,最后加重的时候,也是自己一人在多个医院来回跑,期间还会遇到态度极差的医生

吃抗抑郁药也有极大的副作用,我在2023中,我的精力、智力、记忆力,直接严重折扣

一大波图带你重新认识一下我

音乐

虽然大家对我的印象主要来自于技术,但我最热爱的,始终是音乐,从大一至今六年,从来没有变过,

我是一个完整的人,我的生命中不只是代码,还有很多热爱的东西,我热爱音乐,非常热爱。

我曾把音乐当做是比我生命更重要的东西,可能也是因为我不觉得生命重要,因为我们,明天可能就会死去,这些都是未知的

临走前,学了一手福禄寿的《马》,因为我吉他寄回去了已经,钢琴目前熟练度还没上来,简单弹唱一段,赠予大家,希望大家能够在满是黑夜的世界中,找到属于自己的光明

只是觉得歌词部分应景

对不起,最后几天,这个谱子对我来说太难了,中间副歌用了原版伴奏

B站传送门

么的后来越听越难听,大家可以去同步发布的博客,可以听到多情种、安河桥、余香

大二的我在讲台

大四在寝室

2023的5.1在成都

2023的不知道哪一天在蓝岸友情合作一手余香

技术

分享了我的音乐,我到我的技术了

在公司内的角色,我直接贴一个自评内容好了

在整个滴滴DiFlutter的建设中,负责多个关键模块,包括Lomo框架,性能采集SDK,以及Flutter热修复平台的全栈开发,凭借自身在Flutter领域的独特经验,能多次且快速的为业务落地遇到的难题,提供正确的解决方案。创新的解决思路为Flutter热修复大幅度减少了包体积,以及支持资源文件的动态加载。极强的自驱力,能够自主,高效学习多个方向的技术,并应用到实际业务落地中,从用户的角度出发,将负责的单一模块当成完整的产品,主动优化各模块在使用上的体验,展现出良好的对技术的深度理解,以及体现出较强的技术广度。

Tom 应该也是比较认可我的,除了在我抑郁犯病状态极差之外,会开始怼一些态度不好的同事

我从大学的时候,就自学多门技术,包括前后端,Android 客户端,Flutter 经验至今已有五年,产品,UI,所以我所有的软件(也可称为产品),我自己一人,便是所有身份的执行者

曾经在酷安上架过6个软件,下载量累计30w,在 Google Play 上架过两个,因为没有精力处理隐私协议,被迫下架,在 App Store 上架一个,没有精力去推广运营,后面会借助自媒体等方式对我的产品进行推广

因为傻逼的我,将我仅有的精力,给了工作

我终究成为了自己讨厌的人,为了几两碎银

在后面的离职生活中,我会复活我所有的软件,开始大量投入 Bug 的修复

开源&个人开发者

我的 Github 主页

我自己的 Organization,里面有基础组件,有完整的 App,有底层的库,整个组织开源代码超过 10W 行,全部由我一人开发维护

个人软件官网 https://nightmare.press

个人软件

这里为大家分享由我自己研发的一些产品,其中部分 App 需求小众,再我将其全部带到应用市场后,

大家有任何问题和建议,可以联系我的邮箱 mengyanshou@gmail.com

其中速享、ADB KIT、Code FA 是开源状态,大家也可以直接提交 issue

整个2023,我无愧于我的工作,朋友,但我有愧于我自己的产品

某一天,小白测评在没有跟我联系的时候,测评了我开发的两款软件,很多极其复杂的情绪一并涌了上来

大部分横跨 Windows、Linux、macOS,像速享则是一套代码横跨 Win/Linux/Mac/iOS/Android/Web

魇工具箱

我最先复活的软件是魇工具箱,这是我大一上学期就开发上架的软件。

可以处理 Google Android 系统的镜像,由于国内的 ROM 都是魔改安卓,所以软件本身支持的是处理 Android ROM,简单形容功能,输入一个 ROM 给这个工具箱,工具箱会吐出一个新的 ROM,新的 ROM 就会带上各种个人很繁琐才能实现的功能。

中间过程有 payload.bin,br 解包,dat 解包,ext4 镜像解包,中间还会有 baksmali 的反编译,framework/services.jar 的核心破解

还有 AVB/AVB2.0 的去除,Root(Magisk) 的集成,全局 Odex(方便XP框架),然后解锁 System 分区等,也可以自定义对一些系统软件进行删除。减少系统分区占用

从大一到大四,这个软件为我带来了4w元的收入,帮我度过了很困难的一段时期,所以这个软件对我的意义,是不一样的,由于去年精力无法再维护这个软件,是我主动下架了,后续这是第一个复活的软件,最近已经在做大量的新代码的开发

支持分布式的远程调用+工作流的方式,用户想要实现自己自定义的功能,只需要一个 yaml 文件即可

速享

  • 速享: 一款类似于 LocalSend 的软件吧,但做得不及它,为了解决局域网下的文件互传问题,
  • 文件互传
    • 像聊天一样在局域网共享文件
    • 断点续传
    • 图片预览,视频极速在线播放
    • 点对点高速下载,不使用服务器中转
    • 多个设备同时分享与查看
    • 文件夹共享(重构中)
  • 远程文件管理
    • 可视化浏览
    • 删除,重命名
  • 文件分类:将接受到的文件按扩展名进行分类
  • 快速连接
    • UDP 自动连接
    • 扫码连接
    • 输入连接
    • 历史连接
  • 文件静态部署:类似 tomcat 或者 nginx,方便设备间使用浏览器查看文件,访问网页
  • 支持浏览器加入客户端
  • 剪切板极速共享
  • 支持多平台:Android、Windows、macOS、Linux
  • 响应式设计:适配各种尺寸,平板、手机、横竖屏切换自动适配布局
  • 支持Android SAF:可以接收任意App分享的文件
  • 桌面端后台运行

ADB KIT

  • 快捷管理多设备调试
  • 扫码、局域网发现等快速连接设备
  • 快捷上传,安装应用
  • 为设备免 Root 开启 ADB
  • 安卓免root连接另一台安卓
  • 将 ADB 安装到系统
  • 历史记录
  • 应用管理器,桌面启动器
  • 快捷启动
    • scene
    • 黑域
    • 冰箱
    • shizuku


无界(Uncon)

关于这个软件,其实是看不惯厂商生态下的各种东西,例如小米妙想,华为应用流转,因为这种功能可以跨生态的,所以无界诞生了,这个软件有一个介绍视频

无界

类似鸿蒙的应用流转,但是不限制品牌,机型

可以 Android To Android 的镜像/控制/应用流转,可以实现 Android To PC(Windows、macOS、Linux) 的镜像/控制/应用流转

需要特殊提的是,即使是 Android To Android,也能够拥有完整的 adb 能力,也就是说,完整的支持 otg

这是通过 termux_api 实现的

同样支持非 root 实现adb的软件在 google play 需要6美元

Code FA

这个软件有一个掘金的文章

传送门

简单说,就是将 VS Code 带到了安卓,以最小内存占用的方式,本地运行

当然,很多功能不及 PC 的 VS Code,

SuLa

在2023病情加重前的一个东西,实现类似 TNT,三星 DEX 等功能,但是仍然,不限制机型

最后

如开头所说,我同样热爱吉他,钢琴,也爱唱歌

希望大家,能够坚持自己热爱的事,尽管那会异常的艰难

我将在接下来的生活中,一边养病,一边继续开发我的个人软件,博客会持续分享技术。

也可能会直播玩游戏,毕竟失去了固定收入,当很长一段时间远程乞丐,居家讨饭

大家看到可以赏口饭吃,刷到视频点个赞即可。

一直喜欢的一句话,与大家共勉

偏信则暗,兼听则明

偶然拍到的,昏暗的职场,也会有一束亮到刺眼的光,我大抵是想要讽刺些什么,是啊,想讽刺些什么

希望你们能够在昏暗的世界中,找到属于自己的光亮

无Context调用services

无Context调用 Services 的一点经验分享

##从DisplayManagerGlobal入手
这是一个单例,但却能构造出IDisplayManager对象。
public static DisplayManagerGlobal getInstance() {
synchronized (DisplayManagerGlobal.class) {
if (sInstance == null) {
IBinder b = ServiceManager.getService(Context.DISPLAY_SERVICE);
if (b != null) {
sInstance = new DisplayManagerGlobal(IDisplayManager.Stub.asInterface(b));
}
}
return sInstance;
}
}

    private static IServiceManager getIServiceManager() {
    if (sServiceManager != null) {
        return sServiceManager;
    }
    // Find the service manager
    sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
    return sServiceManager;
}

所以在没有Acitivity的Context的时候,我们也可以用这种方法来反射出各种service

也就是平时我们所看到的

其实还有第二种方法,反射出系统的Context,然后通过这个Context getService,

无论是以上哪一种方法,都会有一个限制,

例如createDisplay方法

会校验uid查询出来的packagename和context中拿到的packagename是否一致

而反射出的context的packagename是android

而我们如果通过shell运行的dex,packagename是com.android.shell

实现经历

虽然在无 root 的情况下支持 adb 不是一个特别大众的需求,但是我仍在为此做努力,这个已经是前两周实现了,但是没时间写相关文章。

解释一下 adb 编译成 arm64 二进制后,为什么不能无 root 通过 OTG 去连接另一台设备,因为 adb 在最初设计就没有考虑会运行在安卓设备上,在安卓上,进程不能直接读写 /dev/bus/usb 节点,而在 PC 上是扫描这个节点(linux),这和安卓其它一堆动态权限类似,安卓为了做权限控制,修改了了 Linux 内核,加了 Linux 原本不会有的限制,例如 Linux 上普通进程本就是可以读写 usb 的,但在安卓上,我们只能通过编写 java 代码动态申请安卓权限,然后用 android 的 java api 去读写。
所以c语言层不能读写 usb,而 java 可以,而这二者如何结合,就是问题所在,也是实现的难点所在。

所以在之前的 ADB KIT中,我也是这样做的,并用 java 代码实现了部分 adb 协议,包括握手,鉴权,文件上传,软件安装,终端交互。这部分是开源的,文章在掘金 -> https://juejin.cn/post/7034799230086545445,所以我对 adb 本身的理解,本就已经超出绝大部分人了,你们给我提的思路和建议,都是我四五年前能想到的东西了,我们做东西要的是实现,而不是谁都能想到的想法。

但是这种效率极低,上传文件龟速,不也不太清楚是为什么,可能是我实现得不好,也可能是 java 效率本就不如 c。
并且协议很难实现完整,例如投屏所需要的关键协议,adb forward 等

最终方案,将 termux-adb 进行了改造,结合 termux-api,在命令行中实现动态申请 usb 权限,然后获得文件的描述符,然后 hook adb 在代码中的 open opendir 等方法,让它以为读取到的就是串口设备,这样的效率是极高的。

经过了很久的尝试,花了我的一整个周末,熬了几个大夜,说实话,我不太想把这作为一个免费的功能,但是最终,它是免费的,在当前版本是,我不会限制你使用的版本,如果后续它成为一个付费的功能,你也可以使用历史版本。

总是会有人来说,“都是用的开源的东西你凭什么收费,都是能查到的资料写的你凭什么收费,尽管不是能查到的资料,那你也是凭现有的资料总结出来的,你凭什么收费”,有个人之前在酷安的评论区一直喷我这点(最近又来一个),我屏蔽后去我的B站喷,这世上最可怕的生物,是人。

我就收费了,我写个 hello world 我也收费999元怎么了?你敢说我错了?没逼着你买,我的软件都是我付出过极多精力的东西,很多都一直免费到现在,我上班工资一个月几w,下班还要来伺候这群人(指的态度不好的),真是不讨好。当然友好的用户,我还是很愿意为他们提供好的软件的。

如果我喜欢某一个软件,我希望它是收费的,如果它一直免费,我反而会担心,用爱发电总有终止的那一天,除非开发者财富自由,不然开发者总是会陷入一个困境“写这个又没钱,要不我不写好好上班算了”。

的确,目前我所开发的软件,都给我带来很多的精神内耗,加上最近了解了一下我升职后的工资,大概升两职后,工资大概能到4w一个月,我为什么不把自己写软件的精力也投入到工作上呢?然后一天一天。

哦最后关于我的软件为什么还不收费,是因为一个软件要收费,需要做的事情也是很多的,后端接口,鉴权方式等,最近公司的压力太大,我能拿来自己开发的时间极少。

我不太清楚甲壳虫和bug的方式,但我敢说,目前 ADB KIT OTG 的效率是大于它俩的。

如何使用

1.保证需要调试的设备开启了 USB 调试
2.使用 OTG 数据线连接两个设备
这个地方有个小坑, type c to c 的线总是没办法区分谁连谁,全靠运气,所以有条件的可以用 USB to Type c 再加一个 OTG转接头解决

ADB KIT 将绝大部分的工作都自己完成了,使用 OTG 调试设备几乎和 PC 连接 Android 一样

数据线插上,然后打开 ADB KIT
如图,注意点确定哈,点完后,被调试的设备会出现 ADB 的弹窗,也注意点确定哈(如果是MIUI没点到这个弹窗,后面都不会弹了,重启和开关 USB 调试可以解决)

无 Root 使用 ADB 带来的好处

只要自己有两个设备即可激活 Shizuku、Scene、冰箱等软件,就算需要激活的设备没有自带的无线调试
可以更方便的为其他的设备安装/卸载软件,上传文件,例如车机,手表

无界也马上支持无 Root OTG 控制另一台安卓(车机,手表),最近工作压力稍微有点大,精力缺乏。