我们在使用安卓手机的时候,已经很少有人会不借助触摸操作来操作手机了,也就是说对手机的触摸操作已经是我们使用手机时最常见的方式了。那么我们究竟是如何使用这种方式操控手机的呢?手机又是如何处理这些信息的呢?
我们对于手机的触摸操作产生的信息,由Linux/Android多点触摸协议规定了如何进行组织和传输。而对于不同的手机设备,多点触摸协议又分为TypeA和TypeB两种,我的新手机(嘿嘿嘿开心)OnePlus7 pro是属于TypeB类型的,那么在这里就用TypeB来举例说明。
而选择Go语言来进行编写而不是使用诸如Python这种更加成熟,更加适合写脚本的语言来进行数据的解析和组织的原因是,我最近比较迷golang,仅此而已。而且使用Python现成的包我又有什么能学到呢?
为了满足现在的先进设备在面对多点和多用户时的数据采集,Linux实现了现在的多点触摸协议,多点触摸协议分为TypeA和TypeB两种,分别面对不同的硬件。TypeA是面对没有多用户的设备,通过所有的点的发送报告来跟踪不同的触点行为。而TypeB则是支持多用户的设备,通过使用不同的信号槽(slot)和信号轨道(track)来发送数据。
举一个TypeA设备中多点触摸协议产生的数据的小例子(两个触摸点的最小发送序列):
ABS_MT_POSITION_X x[0]
ABS_MT_POSITION_Y y[0]
SYN_MT_REPORT
ABS_MT_POSITION_X x[1]
ABS_MT_POSITION_Y y[1]
SYN_MT_REPORT
SYN_REPORT
可以很清晰的看出,多点触摸协议的内容是通过报告的形式发送的,即SYN_MT_REPORT和SYN_REPORT。
前者代表一个时刻中一个点发送了一个数据点的报告,也就是说,x[0]y[0]这个点的触摸记录和x[1]y[1]的触摸记录是同时被报告的,它们都通过一个SYN_REPORT被发送到上级应用或者协议。
在报告的时候,有时不仅仅包含坐标点的信息:触摸宽度和压力在某些设备上也会被记录。
接下来是同样的只有两个触摸点的最小发送序列。
ABS_MT_SLOT 0
ABS_MT_TRACKING_ID 45
ABS_MT_POSITION_X x[0]
ABS_MT_POSITION_Y y[0]
ABS_MT_SLOT 1
ABS_MT_TRACKING_ID 46
ABS_MT_POSITION_X x[1]
ABS_MT_POSITION_Y y[1]
SYN_REPORT
可以清晰的看出,每一个单独的触摸轨迹,在一个SYN_REPORT中通过一个信号槽发送。
信号槽是可以复用的,当一个slot中的tracking id为-1的时候,回收一个slot。
Android 调试桥 (adb) 是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试应用,并提供对 Unix shell(可用来在模拟器或连接的设备上运行各种命令)的访问。
可以很简单的在你的电脑上使用ADB和你的手机进行连接。
安装
sudo apt-get install adb
启动
adb shell
更多的使用方法读者可以自己去查找,这里不是专门讲解adb的文章,所以就不赘述了。
文件结构
.
├── analysis
│ ├── analysis.go
│ ├── dataStructure.go
│ ├── factory.go
│ └── out.txt
└── getevent
├── getevent.go
└── out.txt
./getevent/getevent.go
package main
import (
"bufio"
"fmt"
"os/exec"
"os"
"time"
)
func main() {
start := time.Now()
cmd := exec.Command("adb", "shell", "getevent", "-lt")
file, err := os.OpenFile("out.txt", os.O_WRONLY|os.O_APPEND, 0644)
defer file.Close()
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Printf("Error:can not obtain stdout pipe for command:%s\n", err)
return
}
if err := cmd.Start(); err != nil {
fmt.Println("Error:The command is err,", err)
return
}
outputBuf := bufio.NewReaderSize(stdout, 128)
errChan := make(chan error)
count := 0
go func() {
for {
output, _, _ := outputBuf.ReadLine()
file.Write(append(output, '\n'))
go func() {
count += 1
fmt.Printf("%s Now: %d\r", fmtDuration(time.Since(start)), count)
}()
}
}()
var str string
fmt.Scanln(&str)
return
errChan <-cmd.Wait()
if <-errChan != nil {
return
}
return
}
func fmtDuration(d time.Duration) string {
d = d.Round(time.Second)
h := d / time.Hour
d -= h * time.Hour
m := d / time.Minute
d -= m * time.Minute
s := d / time.Second
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
}
./analysis/analysis.go
package main
import (
"fmt"
"os"
"bufio"
"sync"
"strings"
)
func main() {
fmt.Println("[INFO]: Start Running...")
file, err := os.Open("out.txt")
if err != nil {
fmt.Printf("[Error]: %s\n", err)
}
defer file.Close()
store := Storage{
Map: make(map[int]Event),
Lock: new(sync.Mutex),
}
var wg sync.WaitGroup
counter := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
counter += 1
wg.Add(1)
go func(id int, text string) {
defer wg.Done()
if strings.HasPrefix(text, "[") {
newEvent := Factory(text)
store.Lock.Lock()
store.Map[id] = newEvent
store.Lock.Unlock()
}
}(counter, scanner.Text())
}
wg.Wait()
// Now obey the MIT TypeB protocol...
collection := Collection{
Slots: make([]Slot, 10),
}
slotNow := 0
collection.Slots[0] = Slot {
Tracks: []Track{},
Used: true,
}
var trackNow Track
posNow := make([]Position, 10)
widthNow := make([]int64, 10)
trackCounter := 0
pointCounter := 0
maxSlot := 0
for i:=1; i<=counter; i++ {
eventNow := store.Map[i]
if eventNow.Statu == "EV_ABS" {
if eventNow.Info == "ABS_MT_TRACKING_ID" {
if eventNow.Value == -1 {
if i<counter && store.Map[i+1].Statu == "EV_KEY" {
// the up action
trackNow.Reports = append(trackNow.Reports, Report {
Time: store.Map[i+1].Time,
IsBtn: true,
})
collection.Slots[slotNow].Tracks = append(collection.Slots[slotNow].Tracks, trackNow)
trackNow = Track{}
// fmt.Println("Track is done.")
}
collection.Slots[slotNow].Tracks = append(collection.Slots[slotNow].Tracks, trackNow)
// close the slot here
posNow[slotNow] = Position{}
widthNow[slotNow] = 0
trackCounter += 1
} else {
trackNow = Track{
ID: eventNow.Value,
Reports: []Report{},
}
}
} else if eventNow.Info == "ABS_MT_SLOT" {
// update the slot
if int64(slotNow) != eventNow.Value {
slotNow = int(eventNow.Value)
}
// init new slot
if !collection.Slots[slotNow].Used {
collection.Slots[slotNow] = Slot {
Used: true,
Tracks: []Track{},
}
}
} else if eventNow.Info == "ABS_MT_POSITION_X" {
posNow[slotNow].X = eventNow.Value
} else if eventNow.Info == "ABS_MT_POSITION_Y" {
posNow[slotNow].Y = eventNow.Value
} else if eventNow.Info == "ABS_MT_WIDTH_MAJOR" {
widthNow[slotNow] = eventNow.Value
}
} else { // EV_SYN
if i > 1 && strings.HasPrefix(store.Map[i-1].Info, "ABS_MT_POSITION_") {
// report the position
trackNow.Reports = append(trackNow.Reports, Report {
Time: store.Map[i-1].Time,
IsBtn: false,
Pos: Position {
X: posNow[slotNow].X,
Y: posNow[slotNow].Y,
},
Width: widthNow[slotNow],
})
pointCounter += 1
// fmt.Println("New Position:", posNow[slotNow])
}
}
}
for collection.Slots[maxSlot].Used {
maxSlot += 1
}
fmt.Println("+------------------+--------------------+")
fmt.Printf("| 总解析日志行数 | %10d |\n", counter)
fmt.Println("+------------------+--------------------+")
fmt.Printf("| 总触摸经历时间 | %10d |\n", int(store.Map[counter].Time-store.Map[1].Time)/1000)
fmt.Println("+------------------+--------------------+")
fmt.Printf("| 总捕捉触摸点数 | %10d |\n", pointCounter)
fmt.Println("+------------------+--------------------+")
fmt.Printf("| 总触摸轨迹数量 | %10d |\n", trackCounter)
fmt.Println("+------------------+--------------------+")
fmt.Printf("| 最大多点触摸数 | %10d |\n", maxSlot)
fmt.Println("+------------------+--------------------+")
return
}
./analysis/dataStructure.go
package main
import "sync"
type Position struct {
X int64
Y int64
}
type Report struct {
Time float64
IsBtn bool
Pos Position
Width int64
}
type Track struct {
ID int64
Reports []Report
}
type Slot struct {
Tracks []Track
Used bool
}
type Collection struct {
Slots []Slot
}
type Event struct {
Time float64
Statu string
Info string
Value int64
}
type Storage struct {
Map map[int]Event
Lock *sync.Mutex
}
./analysis/factory.go
package main
import (
"strings"
"strconv"
)
func Factory(text string) Event {
var segs []string
pieces := strings.Split(text, " ")
for _, piece := range pieces {
if piece != "" {
segs = append(segs, piece)
}
}
newEvent := Event {
Statu: segs[3],
Info: segs[4],
}
if segs[5] == "DOWN" {
newEvent.Value = -333 // Down
} else if segs[5] == "UP" {
newEvent.Value = -444 // Up
} else if segs[5] == "ffffffff" {
newEvent.Value = -1 // -1, means that's track is done
} else {
num, err := strconv.ParseInt(segs[5], 16, 32)
if err != nil {
panic(err)
}
newEvent.Value = num
}
newEvent.Time, _ = strconv.ParseFloat(strings.TrimRight(segs[1], "]"), 64)
return newEvent
}