2013/02/27

AndroidにUSB ADB経由でキーイベントとかタッチイベントを送り込む

かれこれ仕事でAndroidをやり始めて1年半がたちました。
そんな1年半目の発見として(?)
タッチイベントのリピーター(ユーザが入力したイベントをそっくりそのまま、コマンドを送り込んで再現させる)を作ってみました。

(とるのがヘタでごめんなさい)

目的は
 ・何度も何度も試験をしないといけないときに、PCとスマホをUSB接続して、試験ツール的なものをかけて夜帰って、朝に結果を見る、ということができるようにする。
 ・そのために、入力の基本である、キーイベントやタッチイベントをUSB経由でインジェクションする。

しかし
 ・キーイベントにくらべてタッチイベントのシナリオって、書くのがすごく難しい

なので
 ・タッチイベントの再現シナリオを簡単に作れるようにしました。

というストーリー。



キーイベントをUSB経由で送り込む

 すでに多くのサイトで書かれているので、省略。

 adb shell input text 文字列
 adb shell input keyevent キーコード


タッチイベントの意味を解析しよう

タッチイベントのシナリオを作るには、最低限のタッチイベントがどういうものかくらいは知っておく必要がある。


Nexus 7で、
adb shell getevent /dev/input/event0
とやって、タッチパネルから上がってくるイベントを観察してみた。

iwaki-yuusuke-no-MacBook-Air:~ yi01$ adb shell getevent /dev/input/event0
0003 0039 00000001
0003 0030 00000005
0003 003a 00000011
0003 0035 000000b4
0003 0036 000000fd
0000 0000 00000000
0003 0039 ffffffff
0000 0000 00000000

コマンドを打って、画面の左上の方を軽くタップした様子

うん、よくわからん
少し移動を加えてみた。

iwaki-yuusuke-no-MacBook-Air:~ yi01$ adb shell getevent /dev/input/event0
0003 0039 00000005
0003 0030 00000007
0003 003a 0000000d
0003 0035 000000ba
0003 0036 00000189
0000 0000 00000000
0003 003a 00000054
0003 0035 000000bf
0003 0036 0000018e
0000 0000 00000000
0003 003a 00000058
0003 0035 000000c5
0000 0000 00000000
0003 003a 0000004b
0003 0035 000000cb
0000 0000 00000000
0003 0039 ffffffff
0000 0000 00000000
うん。よくわからんけど、00000000・・・00ってのが区切り文字的なものに見える。あと、3つの値が常に並んでいるようだ。

少し解析スクリプトを書いてみた。
# -*- coding:utf-8 -*-

import os

pipe=os.popen("adb shell getevent /dev/input/event0")
for line in iter(pipe.readline,""):
    data = line.strip().split()
    if len(data)!=3: continue
    kind,key,value = [int(a,16) for a in data]

    if kind==0:
        if key==0 and value==0:
            print "="*20
        else:
            print "cmd",key,value
    else:
        print kind,key,value

すると。
iwaki-yuusuke-no-MacBook-Air:~ yi01$ python analyze_touchevent.py 
3 57 10
3 48 9
3 58 32
3 53 517
3 54 393
====================
3 57 4294967295
====================
おお、だいぶ見やすくなった。

さて、次に気になるのは、「3 57」から始まるやつ。↑の結果を出すまでに何度かタップ試していたのだが
3 57 8
3 48 6
3 58 18
3 53 630
3 54 1442
====================
3 57 4294967295
====================
3 57 9
3 48 4
3 58 21
3 53 479
3 54 542
====================
3 58 84
3 54 547
====================
3 57 4294967295
====================

なんかカウントアップしているように見える。あと、4294967295=0xffffffffである。
あとは、タッチ関連のイベントは、左1桁目はかならず「3」のようだ。
    if kind==0:
        if key==0 and value==0:
            print "="*20
        else:
            print "cmd",key,value
    elif kind==3:
        if key==57:
            if value==0xffffffff:
                print "touch","end","-"
            else:
                print "touch","start","seq_No."+str(value)
        else:
            print "touch",key,value

    else:
        print kind,key,value

ちょっとこんなかんじにスクリプトの条件文を書き換えたら
touch start seq_No.13
touch 58 26
touch 53 387
touch 54 1106
====================
touch 58 91
touch 53 382
touch 54 1103
====================
touch end -
====================
touch start seq_No.14
touch 58 30
touch 53 257
touch 54 473
====================
touch end -
====================
touch start seq_No.15
touch 58 28
touch 53 470
touch 54 725
====================
touch end -
====================

こんなかんじになった。だんだんそれっぽくなってきた。

次に目につくのは、53と54。これはどうもx座標とy座標のようだ。タップした座標に近いからだ。58ってのはよくわからないので、保留。

ここまでやったところで、「マルチタッチ」ということを思いつく。
iwaki-yuusuke-no-MacBook-Air:~ yi01$ python analyze_touchevent.py 
touch start seq_No.17
touch 48 7
touch 58 24
touch 53 404
touch 54 523
====================
touch 58 75
touch 47 1
touch start seq_No.18
touch 48 7
touch 58 29
touch 53 757
touch 54 409
====================
touch 47 0
touch end -
touch 47 1
touch 48 6
touch 58 35
====================
touch end -
====================

touch 47 0
touch start seq_No.19
touch 58 26
touch 53 391
touch 54 631
====================
touch 58 79
touch 47 1
touch start seq_No.20
touch 48 8
touch 58 26
touch 53 939
touch 54 589
touch 47 2
touch start seq_No.21
touch 48 8
touch 58 27
touch 53 674
touch 54 514
====================
touch 47 0
touch 58 35
touch 47 1
touch 48 7
touch 58 32
touch 47 2
touch end -
====================
touch 47 0
touch end -
touch 47 1
touch end -
====================

2点、3点タップを試した結果が上記。ぱっと見だが、「touch 47」というのが「これからX番目の値を言いますよー」という宣言に見える。
    if kind==0:
        if key==0 and value==0:
            print "="*20
        else:
            print "cmd",key,value
    elif kind==3:
        if key==57:
            if value==0xffffffff:
                print "touch","end","-"
            else:
                print "touch","start","seq_No."+str(value)
        
        elif key==53:
            print "touch","x",value
        elif key==54:
            print "touch","y",value

        elif key==47:
            print "touch","index",value

        else:
            print "touch",key,value

    else:
        print kind,key,value

if文はこんなかんじに。だんだんと充実してきた。
しばらくグリグリタッチして遊んでみる。

まだ残っているのは48と58・・・。なんだろう。accuracyとかそういう系の指標っぽくもある。そんなに重要そうではない。
====================
touch 48 18
touch 58 248
touch x 398
touch y 1127
====================
touch 58 226
touch x 394
touch y 1122
====================
touch 58 202
touch x 389
touch y 1117
====================
touch 48 16
touch 58 193
touch x 385
touch y 1112
====================
touch 48 15
touch 58 176
touch x 380
touch y 1107
====================
touch 48 13
touch 58 164
touch x 375
touch y 1104
====================
touch 58 158
touch x 370
====================

まあ、リピーターなので、べつに意味を100%理解してなくてもよかろう。
とか言っているうちに、ソースコードから正解を見つけてしまった。(笑)
48はタッチの面積の半径、58はタッチの圧力のようだ。
うん。やっぱりそんなに重要じゃなさそうだ。

んん??圧力?タッチパネルって感圧なの??
iwaki-yuusuke-no-MacBook-Air:~ yi01$ adb shell getevent /dev/input/event0 | grep 3a
0003 003a 00000007
0003 003a 0000004d
0003 003a 0000008b
0003 003a 00000049
0003 003a 00000009
0003 003a 000000ac
0003 003a 000000b0
0003 003a 000000b9
0003 003a 000000ba
0003 003a 000000b8
0003 003a 000000b6
0003 003a 000000b5
0003 003a 000000b8
0003 003a 000000ba
0003 003a 000000c2
0003 003a 000000cb
0003 003a 000000cd
0003 003a 000000d3

実際見てみた(ぐいっと指を押し込んでみた)が、お世辞にも感度がいいとは言えない。むしろ、接地面積に比例した値を返しているだけのようだ。
ないよりはマシ、おまけ程度と考えていいだろう。
# -*- coding:utf-8 -*-

import os

pipe=os.popen("adb shell getevent /dev/input/event0")
for line in iter(pipe.readline,""):
    data = line.strip().split()
    if len(data)!=3: continue
    kind,key,value = [int(a,16) for a in data]

    if kind==0:
        if key==0 and value==0:
            print "="*20
        else:
            print "cmd",key,value
    elif kind==3:
        if key==57:
            if value==0xffffffff:
                print "touch","end","-"
            else:
                print "touch","start","seq_No."+str(value)
        
        elif key==53:
            print "touch","x",value
        elif key==54:
            print "touch","y",value

        elif key==47:
            print "touch","index",value
        elif key==48:
            print "touch","radius",value
        elif key==58:
            print "touch","pressure",value

        else:
            print "touch",key,value

    else:
        print kind,key,value

さて、突貫工事で、タッチイベントの解析スクリプトはできた。


タッチイベントをUSB経由で送り込む

ここまでできれば、あとは簡単。
adb shell sendevent  type key data
を使って送り込むだけである。
StackOverflowとかの情報を見ると、type, key, dataは16進数ではなく10進数で表記するらしい。

↑のシナリオを逆解析するのは、簡単なので省略。

問題は、adb shellを実行するところだ。
簡単な例があるのでそれだけ示しておく。
# -*- coding: utf-8 -*-

import os

class ADBTestShell:
    def __init__(self):
        print "adb shell"

    def execute(self,cmd):
        print "> "+cmd

    def __del__(self):
        self.execute("exit")
        print "EXIT"

class ADBShell:
    def __init__(self):
        self.pipe=os.popen("adb shell","w")

    def execute(self,cmd):
        self.pipe.write(cmd+"\n")

    def __del__(self):
        self.execute("exit")
        self.pipe.close()


adb=ADBShell()
adb.execute("ls /system/app")
del adb

これを組み合わせることで、冒頭に載せたようなキーイベントの送信のシナリオ作成・再現ができる。

2013/02/24

ポップアップでdrag and drop できるカラーピッカー


パソコンの世界では常識だが、スマホ/タブレットの世界ではあまり見ないもの。
そう、「ウインドウ」という概念。

Androidだと、PopupWindowとかいうクラスが標準で用意されているけど、
http://stackoverflow.com/questions/9035678/android-how-to-dragmove-popupwindow
このへんを見る限り、ドラッグ&ドロップ機能を標準で持っているものでは無さそうだなー。
そもそも、Activityの枠を超えて表示ってできんのかな?などいろいろ疑問があり、
練習も兼ねて
・ポップアップ表示
・タイトルバーをつかんでドラッグ&ドロップできる
カラーピッカーを使ってみた。

もとのペイントアプリ側が、FW側のパーミッション改造を要するダメダメアプリなので、カラーピッカーは独立したapkとして、ブロードキャストでやりとりするようにしてみた。



カラーピッカー→ https://docs.google.com/file/d/0B4UHSMVmhaPvZ2xfeEttMUZORDQ/edit?usp=sharing
らくがき→ https://docs.google.com/file/d/0B4UHSMVmhaPvaW1ZakFPQ1hueUU/edit?usp=sharing

といっても、開発中ということもあり、できあがったころにソース公開します。

カラーピッカーだけは単体で使えるようなAPI的なものがあるので、下記に記載しておきます。

 ・カラーピッカーを表示する
Intent intent = new Intent("com.example.colorpicker.intent.show");
intent.putExtra("initialcolor",0xffff0000);//optional 初期色を赤に設定
sendBroadcast(intent);

 ・カラーピッカーを閉じる
Intent intent = new Intent("com.example.colorpicker.intent.hide");
sendBroadcast(intent);

 ・カラーピッカーからの色変更を受け取る
registerReceiver(receiver, new IntentFilter("com.example.colorpicker.intent.colorchanged"));

 BroadcastReceiverはこんな感じ
receiver = new BroadcastReceiver(){
    @Override
    public void onReceive(Content context, Intent intent){
        //エラー処理とかいろいろだいぶ省略
        int color = intent.getIntExtra("color",DEFAULT_COLOR);
    }
}

ひとつのapkにガンガン詰め込むのもありですが、こういう感じでapkを分けてやるのも、再利用をする上では結構重要だったりする。

2013/02/23

Androidの画面サイズ取得はこうする!

DisplayクラスのgetSize()メソッドだと、ナビゲーションバーを含んだサイズが取れない。


すると、困ったことに、アプリから隠しAPIであるSurface.screenshot()を用いてスクリーンキャプチャをとろうとした時に、縦におしつぶされたようなキャプチャ画像となってしまう。これは困った。


そこで、こんなページにぶちあたった。

画面サイズを攻略して機種依存を吸収する


このページでは丁寧にAPIレベルを見て分岐させているが、
getRawSizeというメソッド、じつはICSくらいから実装されている。
ということは・・・


こんなかんじで、思ったよりは簡単にかけるわけだ。

 private Point getDisplaySize(Display display){
  Point p = new Point();
  try {
   Method getRealSize = Display.class.getMethod("getRealSize", Point.class);
   getRealSize.invoke(display, p);
  } catch (Exception e) {
   Log.e(TAG,e.getMessage());
  }
  return p;
 }

実際にNexus 7(Android 4.2.1)で実行させたところ、ちゃんと歪みのないキャプチャがとれた。

ここまでできれば、あとは、getSizeのheightとgetRawSizeのheightの差分から、ナビゲーションバーの部分を消す、といったことができる。


スクリーンショットのスクリーンショットのスクリーンショットのスクリーンショットを取ってみたらこんな感じ(笑)


2013/02/16

iPhoneぽい高速スクロールをAndroidでも。

iPhoneって、ステータスバー触ると、ガララララララララ・・・ ってすんごい高速で上までスクロールしますよね。
あのときって、結構、描画頻度落としてスクロールしてるんですよね。

Androidでも、同様に、高速スクロール時に描画頻度を落とせないかなーと思い、実験。


コード差分は、すんごいテキトーだけど、こんな感じ。
25msec以内の描画要求は、わざと25msecまで待たせて、描画要求をふんづまらせてるだけ。
https://gist.github.com/yi01/c50c0828b4d05cb5e5ea/revisions