如何从底层Linux系统点击安卓屏幕
近年来,手机屏幕似乎只会变得更大。这很棒,因为它可以让您在屏幕上看到更多内容,但它也有一些缺点。其中一个对我来说非常烦人:我无法再以舒适的方式触及屏幕左上角的按钮。 在某种程度上,我会将屏幕分为三个区域: 容易触及:握住手机时,拇指可以触及的区域。 不舒适:您_可以_到达该地区,但不如前面提到的舒适。 无法到达:如果不将我的手重新定位在手机边缘,则此区域不在我的拇指可及范围内。 [](https:
近年来,手机屏幕似乎只会变得更大。这很棒,因为它可以让您在屏幕上看到更多内容,但它也有一些缺点。其中一个对我来说非常烦人:我无法再以舒适的方式触及屏幕左上角的按钮。
在某种程度上,我会将屏幕分为三个区域:
-
容易触及:握住手机时,拇指可以触及的区域。
-
不舒适:您_可以_到达该地区,但不如前面提到的舒适。
-
无法到达:如果不将我的手重新定位在手机边缘,则此区域不在我的拇指可及范围内。
[](https://res.cloudinary.com/practicaldev/image/fetch/s--HHww3rkC--/c_limit% 2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://xarantolus.github.io/blog/assets/taptap/Phone-Reachable-Areas.png)
所以对我来说最烦人的按钮是左上角的那些。虽然仍然可以通过一点努力到达右上角的那些,但左上角的那些需要更多的努力。
那么我们如何解决这个问题呢?
我想出的解决这个问题的最佳方法是一个简单的想法:如果有一种方法可以在不离开“易于到达”类别的情况下点击左上角怎么办?
我的手机背面有一个指纹扫描仪,很容易够到。此扫描仪在手机解锁时也没有任何功能。
检测传感器上的手指
所以我查看了Android系统日志,发现手指在传感器上和离上时有以下几行:
fpc_fingerprint_hal: report_input_event - Reporting event type: 1, code: 96, value:1
fpc_fingerprint_hal: report_input_event - Reporting event type: 1, code: 96, value:0
进入全屏模式 退出全屏模式
这些行之间唯一相关的区别是末尾的数字——“手指向下”为1
,“手指向上”为0
。
所以这很容易——只需编写一个程序来扫描logcat
输出,检测这些行,然后运行input tap x y
shell 命令以点击特定点。正确的?
不。
好慢
输入命令对我来说似乎很慢。从点击传感器到对点击做出反应需要相当长的时间。在测试时,它似乎至少需要 300 毫秒,通常更糟糕的是大约 400 毫秒。
根据大量轶事证据,花费 100 毫秒或更短时间的动作被认为是即时的。所以这个命令肯定没有达到“即时”的所有期望(无论如何,它可能不是为了快速而设计的)。但这是为什么呢?
“输入”命令
Android 在/system/bin
中附带了许多不同的命令。其中大多数是在典型的 Linux 环境中预期的(如tail
、cat
等),其中一些是特定于 Android 的。
令我惊讶的是,input
命令只是一个 shell 脚本:
#!/system/bin/sh
# Script to start "input" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/input.jar
exec app_process $base/bin com.android.commands.input.Input "$@"
进入全屏模式 退出全屏模式
如果我没看错的话,它基本上会启动一个Java 程序可以模拟点击。它还可以执行其他操作,但对于这篇文章,我不在乎。
减少延迟
一种避免长时间、明显延迟的方法是——很简单——不依赖input
命令。它只是写入一些数据,复制起来应该不会太难。因此,与其启动一个脚本来启动一个写入一小段数据的程序,我们可以自己编写它。
但是我们应该写什么以及数据应该写在哪里?
我不知道为什么,但我从来没有真正看过文档(还有这个现在更有意义)并开始对这个......开源协议进行逆向工程。是的......无论如何。
尝试重现行为的第一步是观察它。那么我们如何才能在屏幕上看到点击发生时呢?
getevent
实用程序允许我们实时观察某些事件的发生。它还可以轻松列出与这些事件相关的设备文件。
使用getevent -pl
(在手机的 root shell 中)我们可以很好地了解设备、它们的事件和设备文件路径:
chiron:/ $ getevent -pl
add device 1: /dev/input/event6
name: "msm8998-tasha-snd-card Button Jack"
events:
KEY (0001): KEY_VOLUMEDOWN KEY_VOLUMEUP KEY_MEDIA BTN_3
BTN_4 BTN_5
input props:
INPUT_PROP_ACCELEROMETER
add device 2: /dev/input/event5
name: "msm8998-tasha-snd-card Headset Jack"
events:
SW (0005): SW_HEADPHONE_INSERT SW_MICROPHONE_INSERT SW_LINEOUT_INSERT SW_JACK_PHYSICAL_INS
SW_PEN_INSERTED 0010 0011 0012
input props:
<none>
add device 3: /dev/input/event4
name: "uinput-fpc"
events:
KEY (0001): KEY_KPENTER KEY_UP KEY_LEFT KEY_RIGHT
KEY_DOWN BTN_GAMEPAD BTN_EAST BTN_C
BTN_NORTH BTN_WEST
input props:
<none>
add device 4: /dev/input/event3
name: "gpio-keys"
events:
KEY (0001): KEY_VOLUMEUP
SW (0005): SW_LID
input props:
<none>
add device 5: /dev/input/event0
name: "qpnp_pon"
events:
KEY (0001): KEY_VOLUMEDOWN KEY_POWER
input props:
<none>
add device 6: /dev/input/event2
name: "uinput-goodix"
events:
KEY (0001): KEY_HOME
input props:
<none>
add device 7: /dev/input/event1
name: "synaptics_dsx"
events:
KEY (0001): KEY_WAKEUP BTN_TOOL_FINGER BTN_TOUCH
ABS (0003): ABS_X : value 0, min 0, max 1079, fuzz 0, flat 0, resolution 0
ABS_Y : value 0, min 0, max 2159, fuzz 0, flat 0, resolution 0
ABS_MT_SLOT : value 9, min 0, max 9, fuzz 0, flat 0, resolution 0
ABS_MT_TOUCH_MAJOR : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
ABS_MT_TOUCH_MINOR : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
ABS_MT_POSITION_X : value 0, min 0, max 1079, fuzz 0, flat 0, resolution 0
ABS_MT_POSITION_Y : value 0, min 0, max 2159, fuzz 0, flat 0, resolution 0
ABS_MT_TRACKING_ID : value 0, min 0, max 65535, fuzz 0, flat 0, resolution 0
input props:
INPUT_PROP_DIRECT
进入全屏模式 退出全屏模式
起初它看起来令人困惑,但特别是最后一个设备很有趣:它具有与多点触控设备相关的各种事件。那是我们的屏幕。所以现在我们知道哪里写入数据,设备文件/dev/input/event1
。
我们应该写什么**这个问题可以通过观察getevent -l
输出来回答:
/dev/input/event1: EV_ABS ABS_MT_TRACKING_ID 0000504c
/dev/input/event1: EV_KEY BTN_TOUCH DOWN
/dev/input/event1: EV_KEY BTN_TOOL_FINGER DOWN
/dev/input/event1: EV_ABS ABS_MT_POSITION_X 00000037
/dev/input/event1: EV_ABS ABS_MT_POSITION_Y 0000008d
/dev/input/event1: EV_SYN SYN_REPORT 00000000
/dev/input/event1: EV_ABS ABS_MT_TOUCH_MAJOR 00000006
/dev/input/event1: EV_SYN SYN_REPORT 00000000
/dev/input/event1: EV_ABS ABS_MT_TRACKING_ID ffffffff
/dev/input/event1: EV_KEY BTN_TOUCH UP
/dev/input/event1: EV_KEY BTN_TOOL_FINGER UP
/dev/input/event1: EV_SYN SYN_REPORT 00000000
进入全屏模式 退出全屏模式
这是在显示屏左上角单击时的输出。请注意,ABS_MT_POSITION_{X,Y}
旁边的数字是我刚刚点击的坐标。所以问题是:我们如何翻译这个?一点也不,我们只是删除-l
(“以纯文本标记事件类型和名称”)选项以获得更“原始”的数据流:
/dev/input/event1: 0003 0039 0000504d # ABS_MT_TRACKING_ID
/dev/input/event1: 0001 014a 00000001 # BTN_TOUCH
/dev/input/event1: 0001 0145 00000001 # BTN_TOOL_FINGER
/dev/input/event1: 0003 0035 00000037 # ABS_MT_POSITION_X
/dev/input/event1: 0003 0036 0000008d # ABS_MT_POSITION_Y
/dev/input/event1: 0000 0000 00000000 # SYN_REPORT
/dev/input/event1: 0003 0030 00000006 # ABS_MT_TOUCH_MAJOR
/dev/input/event1: 0000 0000 00000000 # SYN_REPORT
/dev/input/event1: 0003 0039 ffffffff # ABS_MT_TRACKING_ID
/dev/input/event1: 0001 014a 00000000 # BTN_TOUCH
/dev/input/event1: 0001 0145 00000000 # BTN_TOOL_FINGER
/dev/input/event1: 0000 0000 00000000 # SYN_REPORT
进入全屏模式 退出全屏模式
好的,这就是数据。我们知道在哪里写它。但是还是......怎么办?
我们来看看sendevent
命令的源码。它似乎基本上是input
命令的低级版本(不是真的,但仍然有点)。
最有趣的部分是input_event
结构体,它填充了数据,然后写入设备文件:
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
进入全屏模式 退出全屏模式
所以在我们的输出中有三列数字之前,现在我们有三个要填充数据的无符号整数:type
、code
和value
。getevent
命令输出十六进制数字,所以我们必须确保我们不会意外在程序中指定它们时使用错误的数字格式(绝对从未发生在我身上......当然;))。
放在一起
现在我们要做的就是将之前观察到的十二个事件依次写入设备文件,然后测试程序。
虽然用任何语言都可以实现这一点,但我选择了Go来完成任务,因为它能够轻松地从 Windows 交叉编译到 Arm64 Android。它还使定义单击所需的事件变得更加容易:
// Define the input_event struct, but in Go
type InputEvent struct {
Time syscall.Timeval
Type EventType
Code EventCode
Value uint32
}
// Some const definitions, names are from the getevent output
type EventType uint16
const (
EV_ABS EventType = 0x0003
EV_KEY EventType = 0x0001
EV_SYN EventType = 0x0000
)
// Known event codes for a touch sequence
type EventCode uint16
const (
ABS_MT_TRACKING_ID EventCode = 0x0039
BTN_TOUCH EventCode = 0x014a
BTN_TOOL_FINGER EventCode = 0x0145
ABS_MT_POSITION_X EventCode = 0x0035
ABS_MT_POSITION_Y EventCode = 0x0036
ABS_MT_TOUCH_MAJOR EventCode = 0x0030
SYN_REPORT EventCode = 0x0000
)
// Value field of BTN_TOUCH, BTN_TOOL_FINGER
const (
TOUCH_VALUE_DOWN = 0x00000001
TOUCH_VALUE_UP = 0x00000000
)
// This event happens more often; marks the start/end of a sequence
var eventSynReport = InputEvent{
Type: EV_SYN,
Code: SYN_REPORT,
Value: 0x00000000,
}
// touch is the whole sequence of events that simulates a single tap
// While testing it seemed like not all SYN_REPORT events are necessary,
// but we will just use the same sequence as observed above
var touch = []InputEvent{
{
Type: EV_ABS,
Code: ABS_MT_TRACKING_ID,
Value: 0x0000e800, // Touch tracking ID, seems like we don't need to care about it
},
// Pretend to put the finger down
{
Type: EV_KEY,
Code: BTN_TOUCH,
Value: TOUCH_VALUE_DOWN,
},
{
Type: EV_KEY,
Code: BTN_TOOL_FINGER,
Value: TOUCH_VALUE_DOWN,
},
// Top right corner
{
Type: EV_ABS,
Code: ABS_MT_POSITION_X,
Value: 0x00000071,
},
{
Type: EV_ABS,
Code: ABS_MT_POSITION_Y,
Value: 0x000000a3,
},
eventSynReport,
{
Type: EV_ABS,
Code: ABS_MT_TOUCH_MAJOR,
Value: 0x00000005,
},
eventSynReport,
{
Type: EV_ABS,
Code: ABS_MT_TRACKING_ID,
Value: 0xffffffff,
},
// Now put the finger up again
{
Type: EV_KEY,
Code: BTN_TOUCH,
Value: TOUCH_VALUE_UP,
},
{
Type: EV_KEY,
Code: BTN_TOOL_FINGER,
Value: TOUCH_VALUE_UP,
},
eventSynReport,
}
进入全屏模式 退出全屏模式
现在我们只需将序列写入设备文件f
:
// Assumption: f is the opened display device file /dev/input/event1
for _, ievent := range touch {
err := binary.Write(f, binary.LittleEndian, ievent)
if err != nil {
panic("writing input event: " + err.Error())
}
}```
Now we just write our sequence to the device file `f`:
```go
// Assumption: f is the opened display device file /dev/input/event1
for _, ievent := range touch {
err := binary.Write(f, binary.LittleEndian, ievent)
if err != nil {
panic("writing input event: " + err.Error())
}
}
进入全屏模式 退出全屏模式
你可以在这里找到整个程序。
关于序列的一个有趣的细节是它并不总是相同的。有时,一个序列中有更多SYN_REPORT
个事件,但有趣的是,它们似乎并没有改变结果。根据文档,如果两个事件之间没有发送SYN_REPORT
,则它们被视为在同一时刻发送;所以这个事件类型充当分隔符。
现在我们有了单击的代码,我们当然可以通过简单地更改x
和y
的值来调整代码以便能够点击任何位置。
在我的测试中,这个程序比使用input
命令的方法快了很多,这是一个不错的结果。
实际使用
现在我们已经完成了所有工作来获得一个工作的敲击程序,我们只需要将它集成到一个检测指纹按压的程序中,然后发送这些事件。我不会详细说明,你可以在GitHub上查看整个程序。
它基本上是一个后台运行的守护进程,它检测上述日志行以对点击做出反应。它还有一些更多的命令,但它们在技术上不如 tap 有趣。
我还将程序打包到Magisk(带有插件的根解决方案)模块中,因为这样我可以在启动时轻松运行它。
进一步的想法
可以使用getevent
和这种编写事件的方法来创建一个事件记录器,该记录器可以准确地重放事件序列。因此,如果您想在锁定屏幕上自动输入pin,那应该是可能的(屏幕设备文件对_何时_可以发生点击没有任何限制,我认为input
命令仅限于解锁手机,没有锁屏访问)。
谢谢
如果您发现这很有趣并想要创建类似的东西或为您的手机调整程序,请查看存储库。
如果这篇文章有任何错误,请随时指出。谢谢 :)
更多推荐
所有评论(0)