トレースロイドというAndroid端末上のハードイベントを記録して再現する事により、回帰テスト等を自動化するツールをリリースしているのですが、**「ソースがドキュメントだ!」**状態になっているので主要部分の解説を投稿していこうかと思います。
まず最初にハードイベントの記録と再現部分の解説です。
Input Subsystemを使用して実現しています。
#Input Subsystemとは
Input Subsystem自体はAndroidの仕組みというよりはLinuxでのハードとアプリ間でのイベントの受け渡しの仕組みとなります。
Input Subsystemの概要
Input Subsystemとは、USBやPS/2などの低水準なドライバと、ユーザーの使うプログラムの橋渡しをするLinuxカーネルの一部です。
ユーザーとのやりとりはデバイスファイルの読み書きによって行なわれます。
簡単に言うとドライバからファイルに値を書き込むとそれがイベントとしてアプリ層に伝わっていきます、そしてその逆も可能です。
#Input Subsystemの雰囲気を掴む
Androidではこのイベントを簡単に見れるので見てみましょう。
adb shell geteventコマンドを叩くと最初にInput Subsystemを使用しるデバイスの一覧が表示され、その後各デバイスで発生したイベントがズラズラと出てきます。
手持ちのNexus 9でコマンドを叩いた後タッチパネルを操作すると↓のような感じです。
$ adb shell getevent
add device 1: /dev/input/event0
name: "synaptics_dsx"
add device 2: /dev/input/event3
name: "gpio-keys.6"
add device 3: /dev/input/event2
name: "h2w headset"
add device 4: /dev/input/event1
name: "CwMcuSensor"
/dev/input/event0: 0003 0039 00000001
/dev/input/event0: 0003 0035 0000045d
/dev/input/event0: 0003 0036 000003d6
/dev/input/event0: 0003 003a 0000003b
/dev/input/event0: 0000 0000 00000000
/dev/input/event0: 0003 0035 0000047a
/dev/input/event0: 0003 0036 000003d5
/dev/input/event0: 0003 003a 00000040
/dev/input/event0: 0003 0030 00000005
/dev/input/event0: 0000 0000 00000000
/dev/input/event0: 0003 003a 00000041
/dev/input/event0: 0003 0030 00000006
/dev/input/event0: 0000 0000 00000000
/dev/input/event0: 0003 0035 0000049f
/dev/input/event0: 0003 0036 000003d2
/dev/input/event0がタッチパネルに割り当てられてるからタッチパネルを触ると/dev/input/event0でイベントが発生しているのが分かります。
試しに電源ボタンを押下すると↓のような感じです。
$ adb shell getevent
add device 1: /dev/input/event0
name: "synaptics_dsx"
add device 2: /dev/input/event3
name: "gpio-keys.6"
add device 3: /dev/input/event2
name: "h2w headset"
add device 4: /dev/input/event1
name: "CwMcuSensor"
/dev/input/event3: 0001 0074 00000001
/dev/input/event3: 0000 0000 00000000
/dev/input/event3: 0001 0074 00000000
/dev/input/event3: 0000 0000 00000000
電源ボタンは/dev/input/event3に割り当てられてますね。
さらにgeteventコマンドに対してsendeventコマンドもあります。
↑の電源ボタンのイベントを今度は再現してみましょう。
$ adb shell sendevent /dev/input/event3 1 116 1 && adb shell sendevent /dev/input/event3 0 0 0 && adb shell sendevent /dev/input/event3 1 116 0 && adb shell sendevent /dev/input/event3 0 0 0
叩くたびにデバイスの画面が点いたり消えたりします!
geteventで出力される数字は16進数でsendeventに入力する数字は10進数なのが使いにくいポイントですね!
#どのデバイスのイベントを記録するか
Nexus9はInput Subsystemを使用しているデバイスも少ないので全部記録してしまえば問題無さそうですが、GALAXY S5なんかはもっと沢山デバイスがあり、しかも操作していないのに常に何かのイベントが発生しています。(光度センサぽいです)
なのでアプリのテストを自動化する際には謎のイベントを記録してしまうと記録ファイルが膨大になってしまうし、テストを走らせた際に不測の自体が発生してしまうかもしれません。
幸いadb shell getevent -iコマンドを叩くとデバイスの情報が取得出来るのでそこから記録するべきデバイスを特定する事が出来ます。
↓はNexus9でコマンドを実行した結果です。
$ adb shell getevent -i
add device 1: /dev/input/event0
bus: 0000
vendor 0000
product 0003
version 2002
name: "synaptics_dsx"
location: "synaptics_dsx/touch_input"
id: ""
version: 1.0.1
events:
KEY (0001): 008f
ABS (0003): 002f : value 0, min 0, max 9, fuzz 0, flat 0, resolution 0
0030 : value 0, min 0, max 47, fuzz 0, flat 0, resolution 0
0031 : value 0, min 0, max 47, fuzz 0, flat 0, resolution 0
0035 : value 0, min 0, max 3072, fuzz 0, flat 0, resolution 0
0036 : value 0, min 0, max 2304, fuzz 0, flat 0, resolution 0
0039 : value 0, min 0, max 65535, fuzz 0, flat 0, resolution 0
003a : value 0, min 0, max 255, fuzz 0, flat 0, resolution 0
input props:
INPUT_PROP_DIRECT
add device 2: /dev/input/event3
bus: 0019
vendor 0001
product 0001
version 0100
name: "gpio-keys.6"
location: "gpio-keys/input0"
id: ""
version: 1.0.1
events:
KEY (0001): 0072 0073 0074
input props:
<none>
add device 3: /dev/input/event2
bus: 0000
vendor 0000
product 0000
version 0000
name: "h2w headset"
location: ""
id: ""
version: 1.0.1
events:
KEY (0001): 006b 0071 0072 0073 00a3 00a4 00a5 00a8
00d0 00e2 00e7 0246
input props:
<none>
add device 4: /dev/input/event1
bus: 0000
vendor 0000
product 0000
version 0000
name: "CwMcuSensor"
location: ""
id: ""
version: 1.0.1
events:
SW (0005): 0000 0009*
input props:
<none>
/dev/input/event0がタッチパネルで/dev/input/event3が電源キー(実は音量キーもですが)なのはイベントを監視していて分かってます。
/dev/input/event2は何でしょう?
nameのh2w headsetから予想するとヘッドセットぽいですね。
多分Bluetoothヘッドセットをつなげた場合のイベントですが確認してません。
判断のポイントとして「events」にそのデバイスが発生させるイベントの種類が列挙されているのでそこから判断出来そうです。
身の回りのデバイス数種類を実際に動作させたり、こちらやAndroidのソースを眺めてるとどうやら「EV_KEY」「EV_ABS」を記録しておけば良さそうです。「EV_REL」も記録しても良いのかもしれませんが、身の回りのデバイスで「EV_REL」を発生させるデバイスがなかったのでやめときました。
#コーディング
さて、ハードイベントを記録させる方法が分かったので実際にコーディングします。
adbのコマンドを叩くだけのプログラムで「記録対象のハード選別」「ハードイベント記録」「ハードイベント再現」が出来れば良いのですが、getevent -iの出力フォーマットが変わってしまった場合や、そもそもタッチパネル等の短期間に連続したイベントをsendeventをどんなに早く連続で叩いても再現しきれない問題があるので、それらのコマンドがどのような処理をしているのかソースを見ながらCで実装していきます。
##記録対象のハード選別
getevent -iの中身を見ていきます。
コンソールに出力してる関数はprint_possible_events()ですね。
//★★★ 特定デバイスのfdを受け取る ★★★
static int print_possible_events(int fd, int print_flags)
{
uint8_t *bits = NULL;
ssize_t bits_size = 0;
const char* label;
int i, j, k;
int res, res2;
struct label* bit_labels;
const char *bit_label;
//★★★ getevent -iを打った時に出力される”events:” ★★★
printf(" events:\n");
for(i = EV_KEY; i <= EV_MAX; i++) { // skip EV_SYN since we cannot query its available codes
int count = 0;
while(1) {
//★★★ fdを元にそのデバイスが発生しうるイベントの詳細を一つ一つ問い合わせている ★★★
res = ioctl(fd, EVIOCGBIT(i, bits_size), bits);
if(res < bits_size)
break;
bits_size = res + 16;
bits = realloc(bits, bits_size * 2);
if(bits == NULL) {
fprintf(stderr, "failed to allocate buffer of size %d\n", (int)bits_size);
return 1;
}
}
res2 = 0;
switch(i) {
case EV_KEY:
res2 = ioctl(fd, EVIOCGKEY(res), bits + bits_size);
label = "KEY";
bit_labels = key_labels;
break;
case EV_REL:
label = "REL";
bit_labels = rel_labels;
break;
case EV_ABS:
label = "ABS";
bit_labels = abs_labels;
break;
case EV_MSC:
label = "MSC";
bit_labels = msc_labels;
break;
case EV_LED:
res2 = ioctl(fd, EVIOCGLED(res), bits + bits_size);
label = "LED";
bit_labels = led_labels;
break;
case EV_SND:
res2 = ioctl(fd, EVIOCGSND(res), bits + bits_size);
label = "SND";
bit_labels = snd_labels;
break;
case EV_SW:
res2 = ioctl(fd, EVIOCGSW(bits_size), bits + bits_size);
label = "SW ";
bit_labels = sw_labels;
break;
case EV_REP:
label = "REP";
bit_labels = rep_labels;
break;
case EV_FF:
label = "FF ";
bit_labels = ff_la//★★★ fdを元にそのデバイスが発生しうるイベントの詳細を一つ一つ問い合わせている ★★★bels;
break;
case EV_PWR:
label = "PWR";
bit_labels = NULL;
break;
case EV_FF_STATUS:
label = "FFS";
bit_labels = ff_status_labels;
break;
default:
res2 = 0;
label = "???";
bit_labels = NULL;
}
for(j = 0; j < res; j++) {
for(k = 0; k < 8; k++)
if(bits[j] & 1 << k) {
char down;
if(j < res2 && (bits[j + bits_size] & 1 << k))
down = '*';
else
down = ' ';
if(count == 0)
//★★★ getevent -iを打った時に出力される”ABS (0003):”や”KEY (0001):” ★★★
printf(" %s (%04x):", label, i);
else if((count & (print_flags & PRINT_LABELS ? 0x3 : 0x7)) == 0 || i == EV_ABS)
printf("\n ");
if(bit_labels && (print_flags & PRINT_LABELS)) {
bit_label = get_label(bit_labels, j * 8 + k);
if(bit_label)
printf(" %.20s%c%*s", bit_label, down, (int) (20 - strlen(bit_label)), "");
else
printf(" %04x%c ", j * 8 + k, down);
} else {
printf(" %04x%c", j * 8 + k, down);
}
if(i == EV_ABS) {
struct input_absinfo abs;
if(ioctl(fd, EVIOCGABS(j * 8 + k), &abs) == 0) {
printf(" : value %d, min %d, max %d, fuzz %d, flat %d, resolution %d",
abs.value, abs.minimum, abs.maximum, abs.fuzz, abs.flat,
abs.resolution);
}
}
count++;
}
}
if(count)
printf("\n");
}
free(bits);
return 0;
}
↑のソースを元に実装した関数は↓
//★★★ 特定デバイスのディレクトリ(/dev/input/event0等)を受け取る ★★★
static int is_read_dev(char *devname) {
int evs[2] = {EV_KEY, EV_ABS};
int devFd;
uint8_t *bits = NULL;
ssize_t bits_size = 0;
int evIdx, byteIdx, bitIdx;
int cgbitret;
int ret = 0;
devFd = open_device(devname);
if (devFd < 0) {
#ifdef DEBUG_PRINT
printf("Device open failed!!\n");
#endif
return 0;
}
for (evIdx = 0; evIdx < 2 && ret == 0; evIdx++) {
while (1) {
//★★★ fdを元にそのデバイスが発生しうるイベントの詳細を{EV_KEY, EV_ABS}だけ問い合わせている ★★★
cgbitret = ioctl(devFd, EVIOCGBIT(evs[evIdx], bits_size), bits);
if(cgbitret < bits_size) {
break;
}
bits_size = cgbitret + 16;
bits = realloc(bits, bits_size * 2);
if (bits == NULL) {
fprintf(stderr, "realloc err\n");
exit(EXIT_FAILURE);
}
}
if (evs[evIdx] == EV_KEY) {
ioctl(devFd, EVIOCGKEY(cgbitret), bits + bits_size);
}
for (byteIdx = 0; byteIdx < cgbitret && ret == 0; byteIdx++) {
for (bitIdx = 0; bitIdx < 8 && ret == 0; bitIdx++) {
if (bits[byteIdx] & 1 << bitIdx) {
int code = byteIdx * 8 + bitIdx;
//★★★ EV_KEY, EV_ABSが発生するデバイスなら1を返却 ★★★
if (evs[evIdx] == EV_KEY) {
ret = 1;
} else if (evs[evIdx] == EV_ABS && (code == ABS_MT_POSITION_X
|| code == ABS_MT_POSITION_Y)) {
ret = 1;
}
}
}
}
}
free(bits);
return ret;;
}
##ハードイベント記録
こちらもgeteventの中身を見ていきます。
イベントが発生するたびに出力している関数はgetevent_main()とprint_event()です。
int getevent_main(int argc, char *argv[])
{
// ・・・省略・・・
while(1) {
//int pollres =
//★★★ fdを元にイベントを監視しておく ★★★
poll(ufds, nfds, -1);
//printf("poll %d, returned %d\n", nfds, pollres);
if(ufds[0].revents & POLLIN) {
read_notify(device_path, ufds[0].fd, print_flags);
}
for(i = 1; i < nfds; i++) {
if(ufds[i].revents) {
if(ufds[i].revents & POLLIN) {
res = read(ufds[i].fd, &event, sizeof(event));
if(res < (int)sizeof(event)) {
fprintf(stderr, "could not get event\n");
return 1;
}
if(get_time) {
printf("[%8ld.%06ld] ", event.time.tv_sec, event.time.tv_usec);
}
if(print_device)
//★★★ コンソールに出力される”/dev/input/event3:”の部分 ★★★
printf("%s: ", device_names[i]);
//★★★ ”/dev/input/event3:”以降の出力はこちら ★★★
//★★★ 特に引数を指定しない限りはevent.type, event.code, event.valueを ★★★
//★★★ printf("%04x %04x %08x", type, code, value)で出力するだけ ★★★
print_event(event.type, event.code, event.value, print_flags);
if(sync_rate && event.type == 0 && event.code == 0) {
int64_t now = event.time.tv_sec * 1000000LL + event.time.tv_usec;
if(last_sync_time)
printf(" rate %lld", 1000000LL / (now - last_sync_time));
last_sync_time = now;
}
printf("%s", newline);
if(event_count && --event_count == 0)
return 0;
}
}
}
}
return 0;
}
↑のソースを元に実装した関数は↓
static void *dev_read_loop(void *arg) {
struct pollfd ufds[MAX_WATCH_DEV_NUM];
int eventFds[MAX_WATCH_DEV_NUM];
char tmpDevDir[11 + MAX_WATCH_DEV_LEN + 1];
int nfds = 0;
int res;
struct input_event event;
int i;
char log_one_line[60];
struct timespec monotonic;
#ifdef DEBUG_PRINT
printf("dev_read_loop in \n");
#endif
for (i = 0; i < MAX_WATCH_DEV_NUM && mWatchDevs[i][0] != '\0'; i++) {
snprintf(tmpDevDir, sizeof(tmpDevDir), "%s%s", INPUT_DEVICE_DIR, mWatchDevs[i]);
eventFds[nfds] = open_device(tmpDevDir);
if (eventFds[nfds] < 0) {
fprintf(stderr, "open_device failed!!\n");
stop_script_loop(STOP_ERROR);
return 0;
}
ufds[i].fd = eventFds[i];
ufds[i].events = POLLIN;
nfds++;
}
while (1) {
//★★★ fdを元にイベントを監視しておく ★★★
poll(ufds, nfds, -1);
for (i = 0; i < nfds; i++) {
if (ufds[i].revents & POLLIN) {
res = read(ufds[i].fd, &event, sizeof(event));
if (res < (int)sizeof(event)) {
fprintf(stderr, "could not get event!!\n");
stop_script_loop(STOP_ERROR);
return 0;
}
if (clock_gettime(CLOCK_MONOTONIC, &monotonic) < 0) {
fprintf(stderr, "Could not get monotonic time\n");
stop_script_loop(STOP_ERROR);
return 0;
}
//★★★ 各イベントを発生したタイミングと共に再現するため ★★★
//★★★ 時間情報と一緒に記録している ★★★
snprintf(log_one_line, sizeof(log_one_line),
"%ld,%ld,%x,%x,%x,%s\n",
monotonic.tv_sec, monotonic.tv_nsec / 1000,
event.type, event.code, event.value, mWatchDevs[i]);
write_script(log_one_line);
}
}
}
}
##ハードイベントの再現
こちらはsendeventの中身を見ていきます。
関数は1つだけsendevent_main()ですね。
int sendevent_main(int argc, char *argv[])
{
int fd;
ssize_t ret;
int version;
struct input_event event;
if(argc != 5) {
fprintf(stderr, "use: %s device type code value\n", argv[0]);
return 1;
}
//★★★ デバイスをオープン ★★★
fd = open(argv[1], O_RDWR);
if(fd < 0) {
fprintf(stderr, "could not open %s, %s\n", argv[optind], strerror(errno));
return 1;
}
if (ioctl(fd, EVIOCGVERSION, &version)) {
fprintf(stderr, "could not get driver version for %s, %s\n", argv[optind], strerror(errno));
return 1;
}
memset(&event, 0, sizeof(event));
event.type = atoi(argv[2]);
event.code = atoi(argv[3]);
event.value = atoi(argv[4]);
//★★★ 指定されたevent.type, event.code, event.valueを書き込む ★★★
ret = write(fd, &event, sizeof(event));
if(ret < (ssize_t) sizeof(event)) {
fprintf(stderr, "write event failed, %s\n", strerror(errno));
return -1;
}
return 0;
}
↑のソースを元に実装した関数は↓
void exec_dev_event(int devFd, int sec, int usec, int type, int code, int value, int autoSsMsec) {
int ret;
struct input_event event;
int waitUsec;
//★★★ イベントを発生させるタイミングを見計らう ★★★
waitUsec = wait_exec(sec, usec);
if (autoSsMsec > 0 && waitUsec >= autoSsMsec * 1000) {
take_ss(mSsNo);
mSsNo++;
}
memset(&event, 0, sizeof(event));
event.type = type;
event.code = code;
event.value = value;
//★★★ 記録してあったevent.type, event.code, event.valueを書き込む ★★★
ret = write(devFd, &event, sizeof(event));
if (ret < sizeof(event)) {
fprintf(stderr, "Write event failed!!\n");
exit(EXIT_FAILURE);
}
return;
}
#ビルド&実行
AndroidのアプリってJavaじゃないの?って方のために。
実はAndroidは/data/local/tmp配下に配置すればCで記載したバイナリファイルを実行する事が出来ます。
ビルド方法はこちらを参考にしてみて下さい。