woshidan's blog

そんなことよりコードにダイブ。

Android 4.0, 4.1の対応がまだ必要なので、adb shellを使ってAndroidのUIの手動確認自動化について調べてみた

AndroidのUITest(手動)と自称していた手動の確認作業を自動化したかったのですが、UITest用ツールないしフレームワークで現在有力な選択肢と思しきEspressoやAppiumはAPI18以降対応です。そのため、まだ4.0, 4.1系でのテストが外せない自分の仕事の環境では、それだけではそれなりに手動でぽちぽちやる部分が残ってしまいました。

そこで、アサーションというか結果の確認は人間が目視でやることになるかもしれませんが、 adb shell input を利用して手動でやっている作業の自動化についていくらか調べてみました。

自動化するために必要なこと

まず、自動化するために必要なことは大雑把に下記3点が必要となります。

  1. 自動化する操作を端末に実行させる
  2. 自動化する操作にまつわるパラメータを取得する
  3. 自動化する操作にまつわるパラメータを実行コマンドの引数に渡せるように加工する

この記事では、上記3点をそれぞれ順に検討していきます。

TL;DR

  • 自動化する操作を端末に実行させる
    • タップ、スワイプ程度なら adb shell input command 形式
    • マルチタッチなど複雑な操作をする場合は adb shell sendevent で可能
  • 自動化する操作にまつわるパラメータを取得する
    • 回転やクリックなど特定のイベントの前後、といったタイミングでの操作を取得したいならAndroidソースコード上で Window.Callback を使ってタッチイベントを取得して要所を抽出といったアプローチが良いかも
      • ソースコード上でならタッチイベント以外の情報も一緒に取得しやすいため
    • マルチタッチなどややこしい動作の再現をしたかったり、タッチイベント取得用のコードをプロジェクトのソースコードに作業中でも入れるのに抵抗がある場合、 adb shell getevent で取得した値をすこしいじってそのまま adb shell sendevent に渡してやるのが楽かも
      • 全てのタッチイベントを adb shell sendevent でタッチイベントを実行していくのはやや遅いので、ある程度タイミングがシビアでない操作が許容できたら
  • 自動化する操作にまつわるパラメータを実行コマンドの引数に渡せるように加工する
    • どの方法で実行させるか、どの形式でタッチイベント取得するか決めたら粛々と文字列を加工する

1. 自動化する操作を端末に実行させる

adb shell input でタッチイベントをAndroidで実行する

最初に自分が触ってみるまでよくわかってなかったので確認ですが、以下のように

$ adb shell input tap 100 100

と入力すると、現在ADBと接続されているデバイスの画面上で100x100の座標の位置がタップされます。

Usage: input [<source>] <command> [<arg>...]

The sources are: // これは端末によって対応しているものが異なることも
      mouse
      keyboard
      joystick
      touchnavigation
      touchpad
      trackball
      stylus
      dpad
      touchscreen
      gamepad

The commands and default sources are: 
      text <string> (Default: touchscreen)
      keyevent [--longpress] <key code number or name> ... (Default: keyboard)
      tap <x> <y> (Default: touchscreen)
      swipe <x1> <y1> <x2> <y2> [duration(ms)] (Default: touchscreen)
      press (Default: trackball)
      roll <dx> <dy> (Default: trackball)

複数デバイスがADBと接続されている場合は -s オプションでタッチイベントを実行する端末を指定することが可能です。また、その際は adb devices コマンドで取得できる端末のIDを利用します。

上記の The commands and default sources are:  以下を見ていただくとマルチタッチに相当しそうなコマンドがありませんが、自分が調べた範囲では、基本的に adb shell input command ではマルチタッチを実行することができません。マルチタッチを必要とするピンチイン・ピンチイン、あるいはマルチタッチそのものを実行させたい場合は、

adb shell sendevent input_event_file type key data

のように、adb shell sendeventを利用して、やや生々しい形で送る必要があるようです。input_event_fileと表示している部分の引数は adb shell getevent で表示される /dev/input/eventNのパスにあるファイルです*1

// たとえば手元でいつも検証に使っている端末で表示して見た結果は以下
$ adb shell getevent
add device 1: /dev/input/event8
  name:     "msm8974-taiko-mtp-snd-card Headset Jack"
add device 2: /dev/input/event7
  name:     "msm8974-taiko-mtp-snd-card Button Jack"
add device 3: /dev/input/event6
  name:     "system_als"
add device 4: /dev/input/event5
  name:     "mhl-rcp"
add device 5: /dev/input/event2
  name:     "qpnp_pon"
add device 6: /dev/input/event1
  name:     "clearpad"
add device 7: /dev/input/event0
  name:     "bu520x1nvx"
could not get driver version for /dev/input/mice, Not a typewriter
add device 8: /dev/input/event4
  name:     "qpnp_chg_unplug_key"
add device 9: /dev/input/event3
  name:     "gpio-keys"

input_event_file として指定するeventファイルによっては、イヤフォンジャックの抜き差しといったことも取得可能です。今回は画面のタップなどの操作を対象としているので、geteventseteventを実行する際は clearpadという name の書かれている /dev/input/event1 を対象として実行することにします。ここの name やタッチパネルに当たるeventファイルのパスは端末によって違う可能性があるので、それぞれの端末で自動化を行う前に確認します。

// たとえば適当なエミュレータで試した結果
$ adb shell getevent
* daemon not running. starting it now at tcp:5037 *
* daemon started successfully *
add device 1: /dev/input/event0
  name:     "Power Button"
add device 2: /dev/input/event1
  name:     "qwerty2"

2. 自動化する操作にまつわるパラメータを取得する

前節でコマンドを adb shell からどう実行すればいいか、といった話をしたので、この節では adb shell inputadb shell sendevent に渡すパラメータをどう取得するか、といった話をします。

記事を書く際、自分の中では「どのタッチイベントを対象とするか」「テスト用にプロジェクトのコードを編集するか」という観点で下記の2つのアプローチを検討しました。

  1. Window.Callback を利用して Android のアプリ側のソースコード上でタッチイベントの内容を取得して一部抽出する
  2. adb shell getevent を利用してeventファイルへの入力をそのまますべて取得する

それでは、以下で各々の手法について紹介します。

自分で書いててこの節は、

  • どうやってタッチイベントを取得するか
  • 取得したイベントの中からどれを利用するか
  • 利用するイベントをどう抽出するか

についてまだ分離して書き直せる気もしますが、今回は一旦こういう感じでお許しください。。

2-1. Window.Callback を利用して Android のアプリ側のソースコード上でタッチイベントの内容を取得して一部抽出する

Window という表示領域に関する様々なことを管理するクラスがありまして((と書いたものの、Windowは抽象クラスであり、実際管理しているのはそのサブクラスのPhoneWindowですが https://developer.android.com/reference/android/view/Window.html ))、Windowからのコールバック呼び出しを受け取るためのWindow.Callbackインタフェースを利用すると、タッチイベント含むWindow` に関するイベントが呼び出された際に独自の処理を追加して実装することができます。

この Window.CallbackdispatchTouchEvent メソッドをオーバーライドして、タッチイベントの座標などを記録する処理を挟む、というのがこの節で紹介する方法となります。

いろいろ細かいことは置いておいて乱暴に書いてしまうと、下記のようになります。

public class MyWindowCallback implements Window.Callback {

    final static String TAG = MyWindowCallback.class.getSimpleName();
    Window.Callback mOriginalCallback; // 

    public MyWindowCallback(Window.Callback originalCallback) {
        this.mOriginalCallback = originalCallback;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(TAG, "dispatchTouchEvent " + event.toString()); // タッチイベントの内容をログに出す
        return mOriginalCallback.dispatchTouchEvent(event);
    }

    @Override
    public boolean dispatchTrackballEvent(MotionEvent event) {
        return mOriginalCallback.dispatchTrackballEvent(event); // それ以外のタッチイベントは元々のコールバックを呼び出すだけ
    }
// タッチイベントを取りたいアクティビティ
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // ...

        final Window win = getWindow();
        final Window.Callback localCallback = win.getCallback();
        win.setCallback(new MyWindowCallback(localCallback));
    }

この方針でも次の方針でも、画面に対するほぼ全てのタッチイベントが収集できますが、出力先がログにせよ、ファイルにせよ出力される内容が非常に多いです。そのため、見出しでも触れましたが、どの操作が最低限必要なタッチイベントなのか区別する必要があります。しかし、この関数をプロジェクト内に足しているように、Androidソースコード上でタッチイベントを収集するように自動化の際にソースコードを一時的にでも編集することを許容する場合、目印となるログを入れたりすることも比較的容易なのでまぁ大丈夫なのではと思います。

なお、 dispatchTouchEvent から変換する際、マルチタッチについては、POINTER_UP, POINTER_DOWNのイベントとMOVEの引数を見比べながら変換する必要があり、実行コマンドへの変換処理がなかなかややこしくなります。

しかしまぁ、概ね手動で行っている確認作業はシングルタップが実行できれば十分だし、最小限に抽出されたadb shell inputのコマンドは読めなくもないので、タッチイベントの抜け漏れに注意しながら使えば比較的楽そうです。また、 onContentChanged メソッドなど他のメソッドも利用することで、回転など操作の再現に必要な他の情報も手に入れられそうです*2

2-2. adb shell getevent を利用してeventファイルへの入力をそのまま取得する

2-1の方針において、

  • マルチタッチの実行コマンドへの変換処理がややこしい
  • 自動化作業のためにソースコードをいじることに抵抗があるケースもある

といった話をしました。それでは、ソースコードに変更を加えることなくタッチイベントを取得する方法として、 adb shell getevent の話をします。

$ adb shell getevent input_event_file

を実行すると、タッチイベントが発生するたび、標準出力に

0003 0039 00000000
0003 0037 00000000
0003 0035 00000289
0003 0036 0000030b
0003 003a 0000003b
0000 0002 00000000
0003 0039 00000001
0003 0037 00000000
0003 0035 000001ee
0003 0036 000003b1
0003 003a 00000039
0000 0002 00000000
0000 0000 00000000
0003 0039 00000000
0003 0037 00000000
0003 0035 00000289
0003 0036 0000030a

...

といった数字の列が吐き出されます。これは、adb shell sendevent で利用できるパラメータとなっています。-tl オプションを利用してもう少し人間の目にわかりやすくした場合のログが下記です。

[   88406.873926] EV_ABS       ABS_MT_TRACKING_ID   00000000            
[   88406.873930] EV_ABS       ABS_MT_TOOL_TYPE     MT_TOOL_FINGER      
[   88406.873934] EV_ABS       ABS_MT_POSITION_X    00000368            
[   88406.873937] EV_ABS       ABS_MT_POSITION_Y    00000748            
[   88406.873939] EV_ABS       ABS_MT_PRESSURE      00000038            
[   88406.873943] EV_SYN       SYN_MT_REPORT        00000000            
[   88406.873946] EV_SYN       SYN_REPORT           00000000            
[   88406.884306] EV_ABS       ABS_MT_TRACKING_ID   00000000            
[   88406.884309] EV_ABS       ABS_MT_TOOL_TYPE     MT_TOOL_FINGER      
[   88406.884312] EV_ABS       ABS_MT_POSITION_X    00000367            
[   88406.884355] EV_ABS       ABS_MT_POSITION_Y    00000747            
[   88406.884358] EV_ABS       ABS_MT_PRESSURE      00000038            
[   88406.884360] EV_SYN       SYN_MT_REPORT        00000000            
[   88406.884362] EV_SYN       SYN_REPORT           00000000    

...

パラメータに対して、16進数 -> 10進数の変換は必要ですが、このeventファイルに入力されたタッチイベントの内容を文字通りそのままadb shell sendeventに突っ込み直すので、マルチタッチ含めて各種タッチイベントが実行できます。

しかし、全てのタッチイベントを adb shell sendevent で実行すると遅くなるので、タイミングが比較的シビアなケースには不向きですし、細々回転や特定のViewの表示タイミングなど adb shell getevent で取得できない行動がはさまれるものを確認したい場合には面倒です。

3. 自動化する操作にまつわるパラメータを実行コマンドの引数に渡せるように加工する

これについては、どの方式で値を取得するかが決まったらそれに合わせてちょいちょいと正規表現を用いて値を抽出するスクリプトを書く、といった話になるので、今回は 2-2 の方針でタッチイベントを adb shell getevent で取得した値を adb shell sendevent で使えるように加工 & adb shell sendevent で取得・記録していた操作を実行するスクリプトを例として載せて締めます。

$ adb shell getevent input_event_file(cf. /dev/input/event0) > ./record.txt
// ./record.txt
0003 0039 00000000
0003 0037 00000000
0003 0035 00000289
0003 0036 0000030b
0003 003a 0000003b
0000 0002 00000000
0003 0039 00000001
0003 0037 00000000
0003 0035 000001ee
0003 0036 000003b1
0003 003a 00000039
0000 0002 00000000
0000 0000 00000000
0003 0039 00000000
0003 0037 00000000
0003 0035 00000289
0003 0036 0000030a
0003 003a 0000003c
0000 0002 00000000
0003 0039 00000001
0003 0037 00000000
0003 0035 000001ed
0003 0036 000003b1
0003 003a 0000003b
0000 0002 00000000
0000 0000 00000000
0003 0039 00000000
0003 0037 00000000
begin
  File.open("./record.txt", "r") do |file| 
    file.each_line do |line|
      result = line.match(/(\w{4}) (\w{4}) (\w{8})/)
      type = result[1].to_i(16)
      key  = result[2].to_i(16)
      data = result[3].to_i(16)
      # puts "adb shell sendevent /dev/input/event1 #{type} #{key} #{data}" 確認用ログ
      system("adb shell sendevent /dev/input/event1 #{type} #{key} #{data}")
    end
  end
rescue Error => e # 中の人がざっくり利用するスクリプトなので雑
  puts e.to_s
end

まとめ

思ったよりダラダラ長くなったので反省した。繰り返し実行するにあたってはもういくつか考えることはありますが、それに関することは細かいし別記事にします。 Window.Callbackのあたりのコードが雑云々については、ここで書いたコードは production に残すことじゃなくてさっと手作業代替するために値をとることを目的としているのでそれ以外には使わないでください、ということで。

現場からは以上です。

参考

*1:詳しくは http://gihyo.jp/book/2017/978-4-7741-8759-4

*2:そういったタッチイベントを取得するためのライブラリはすでにあるかもしれませんが自分で調べた範囲であまり調べてよさそうなのなかったので中身把握してるのチャーっと書いちゃうのでいいんじゃないでしょうか