2014/12/01

中国のグレート・ファイアウォールをすり抜ける方法をいくつか

①VPNで家のルータにつなぐ


家のルータがVPNに対応しているなら、この方法が一番楽です。
プロトコルの制限とかもとくにないし、プロキシと違って、各アプリ側で設定が必要となることもほぼありません。

ただ、相性の問題で、ブチブチ切れることがあるので、そういう場合は②の方法を試しましょう。

②SSHプロキシで1080番ポートをフォワードして、SOCKSプロキシ


家のルータがVPN非対応の場合とか、対応していても不安定の場合はこっちの方法を使います。

ssh -D 1080 xxx.hogehoge.com -N
あとは、ネットワークの設定でSOCKSプロキシを「localhost:1080」に設定します。

VPSは有料契約が必要ですが、持っている人ならこれを利用するのがいいです。
家VPNに比べて速度がでます。
ただ、①とは逆に、アプリごとにプロキシ設定もってたりするとちょっと厄介です。


で、結局、PCはなんとでもなるのですが、
Androidスマートフォン使っていると、GooglePlayもGmailもハングアウトもGoogle検索もTwitterもFacebookもブロックされているので、かなり困ります。
SOCKSなんてAndroidでは使えませんし、VPNはセキュリティロック掛けないとダメとかで、色々面倒です。

で、私は結局どうやっているかというと、Macの「インターネット共有」を利用して、①で接続したルートをWifiで飛ばしてます。
これで意外にもスマートフォンからGooglePlayつながりますし、メールやTwitterもできました。

ふぅ、これで発狂を免れた。

2014/11/12

PythonのCairoでSVGをつくる

最近、Android用のキーボードの開発をやってます。

0からつくるわけにはいかんので、Mozc for Androidってのを使ってるんですが、
こことかにも書かれてるみたいに、Mozcにはキートップの画像が含まれていません。
(正確に言うと、画像はありますが無刻印キーボード的な画像が含まれています)


なので、ビルドするとこんなかんじになります。


キーボードの配列を覚える練習にはなるかもしれませんが、実用的ではないですね。


そんなMozcを↑のようにしたいわけですが、
ここのページで公開されてるキートップ画像では[t][y][u]の上下が微妙にずれていて、若干気持ちが悪いです。そこで、キートップ画像の自作をすることにしました。


ただ、社会人が片手間にやるには、InkScapeでチマチマと作る時間がありません。
そんなわけで、Pythonで一気に作っちゃいました、ってのが今回のお話。


さて、前置きが長くなりました。まずは
https://gist.github.com/yi01/1fe1bffabdc583c09de6
にコードは公開しちゃいます。間違いを発見した方は指摘ください。(笑)


で、あまり世の中には書かれていない?話をちょっとだけ書いておくと

・Mozcのキートップ画像の要件
キートップ用のSVGなんですが、textタグは使えません。pathタグで書く必要があります。
なので、Pythonだと、svgwriteとかは使えなくて、唯一の選択肢がcairoです。

<svg>に、id="style-keyicon-main"が指定されている必要があります。これがないと、スキンを暗い色にした時に、キートップの文字が白抜きにならない不具合が発生します。

QWERTYのキー画像サイズは48x74ピクセルです。cairoはptを単位系として使っているようなので、座標系を変換しなければなりません。
でも、変換がめんどくさすぎたので、今回は成果物のxmlからptの単位がついてるものを表示しなくしてみました、それにより、意図したサイズの画像が得られます。


・Cairoの文字描画
座標系がちょっと特殊(?)です。
xBearing, yBearing, width, height, xAdvance, yAdvance = context.text_extents(text)
で文字の描画位置やサイズが取得できるわけですが、座標系が下向きY軸正方向のものなので、直感とは逆になります。

で、もうひとつ注意点としては、そのへんのサンプルにある(xBearing+width/2, yBearing+height/2)という座標は、文字領域のど真ん中(赤色の四角の重心)であって、全体的な文章のラインの中心とは異なります。これが、冒頭に書いた「ずれていて若干気持ち悪い」ということです。

↑で、赤枠ベースでの配置だと上下して見えますよね。

じゃあ、どうすればいいのか、っていうのが青枠ベースの配置。

どうやら、フォントサイズと文字のラインの高さには3/4の関係があるみたいなので
↑にあるように、64ptのフォント指定の場合は、24px(=フォントサイズの3/8)だけ下に移動してやることで、行の中心をとらえた描画となるのです。




おまけ
E/MessageQueue-JNI(31310): java.lang.IllegalArgumentException: Duplicataed sourceId is found: Binary XML file line #1710
E/MessageQueue-JNI(31310):    at org.mozc.android.inputmethod.japanese.keyboard.KeyboardParser.parseKeyEntity(KeyboardParser.java:772)

みたいなエラーになった時のチェック用スクリプト
grep -r ":sourceId" kbd_*.xml | awk '{a[$2]+=1};END{for(k in a){if(a[k]>1){print k,a[k]}}}'

2014/11/09

Ubuntu 14.04のApache2のconfigを見て「アレ?」となった話

私は、仕事で(プライベートでも?)よくGitlistっていう便利なgitビューワを使っています。
正直、自分一人だったら要らないんですが、共同作業するなら、必ず一人はいますよね。「わたしgit使えないんで(キリッ」みたいな訳の分からないことをいう人が。

まぁ事情はともかく、Gitllistは、Gitビューワとして非常にシンプルで最低限の機能はあるものなので、いろいろなサーバに建てるために
git clone https://github.com/klaussilveira/gitlist.git
cd gitlist/
curl -s http://getcomposer.org/installer | php
php composer.phar install
chmod 777 cache/
sudo a2enmod rewrite
sudo /etc/init.d/apache2 restart
くらいは、いつでもコピペできるようにしてあったりします。

環境によっては
sudo a2enmod userdir
sudo /etc/init.d/apache2 restart
も必要ですね。まぁそれくらいは誤差の範囲です。

ただ、最近になって、Ubuntu 14.04のサーバを手にしたのですが、「アレッ?」ってなりました。

最終的には
http://aoba.web-hack.org/gitlist/
にあるように、ちゃんと動いたのですが、ユーザディレクトリでRewriteがなかなか動かなくてちょっと戸惑いました。


Directory /home/*/public_html/ > AllowOverride Allとか書く場所は…

正直、これだけで躓きました。他はフィーリングで行けます。

今まで(Apache 2.2)だと、/etc/apache2/sites-available/defaultあたりに、それっぽい記載があったので、真似して
<Directory /home/*/public_html>
    AllowOverride All
</Directory>

とかてきとーに書いていました。(セキュリティの云々はともかくとして…)

しかし、Apache 2.4になって、/etc/apache2/sites-available/defaultは見当たりません。
一体どこに行ったのか・・・。

じつは/etc/apache2/apache2.confにそれっぽい記載があります。
でも、本家confに書いちゃうのって微妙ですよね…。

というわけで、いろいろ試してみたところ、

/etc/apache2/conf-available/rewritable-userdir.conf
<Directory /home/*/public_html>
    AllowOverride All
</Directory>
こんなかんじで1ファイル作って、
sudo a2enconf rewritable-userdir
sudo /etc/init.d/apache2 restart
ってやれば、本家のconfigをオーバーライドする感じで、設定が出来ました。(confにするかsiteにするかは悩ましいところですが…)
Apache2で「あれ?AllowOverrideの設定箇所どこ行った??ユーザディレクトリでRewrite効かないぞ?!」ってなった方は、ぜひお試しあれ。



(参考)
以下、完全に個人的な趣向の問題ですが、Gitlistをちょっとだけ手直しして使ってます。

nano src/GitList/Config.php
    public static function fromFile($file)
    {
        if (!file_exists($file)) {
            die(sprintf('Please, create the %1$s file.', $file));
        }

        $data = parse_ini_file($file, true);
+       $repo = parse_ini_file($file.".repos.inc");
+       $data["git"]["repositories"] = $repo["repositories"];
        $config = new static($data);
        $config->validateOptions();

        return $config;
    }


これで、
config.ini
[git]
client = '/usr/bin/git' ; Your git executable path
default_branch = 'master' ; Default branch when HEAD is detached
;repositories[] = '/home/git/repositories/' ; Path to your repositories ★←コメントアウト
                                           ; If you wish to add more repositories, just add a new line

; WINDOWS USERS
;client = '"C:\Program Files (x86)\Git\bin\git.exe"' ; Your git executable path
;repositories[] = 'C:\Path\to\Repos\' ; Path to your repositories


config.ini.repos.inc
;QAEP
repositories[] = /home/yi01/mirror/LA.BF64.1.1-00110-8x94.0/

;AOSP
;repositories[] = /home/yi01/mirror/android-5.0.0_r2/

こうすることで、純粋なconfigと、リポジトリのメンテを別ファイルで行うことができるようになります。

2014/09/30

Samsung SBrowserの小さな小さな工夫


ほんとうは、仕組みが解明できてから公開したかったのですが、もったいぶるメリットもないので、しょうもない内容なの承知でわかっている範囲だけ公開。

といっても、わかってるのは・・・

同じマルチタッチイベントを与えているにもかかわらず、SBrowserは中心軸がずれずに拡大縮小がされる!

以上。・・・分かり次第ちょびちょび付け足しで書いていきます…。

どんなタッチイベントを送ったか

マルチタッチと、その中心軸の遷移

点の座標をログに出してMatplotlibで可視化すると、こんなかんじで中心軸がぶれています。

そもそもAndroidってどうやってマルチタッチを解釈しているのか(書いてる途中・・・)

ScaleGestureDetectorというコンポーネントがAndroidのフレームワークに有ります。
マルチタッチズームの基本をちらっと図解します。

ScaleGestureDetectorのキモは3つです。
 ・[ScaleStart] 親指と人差指の間の距離をSpanといい、その距離が初期距離から16dp以上ずれるとズームを開始
 ・[ScaleBy] ズームの中心は、親指と人差指の中点(リアルタイムに更新)。倍率は、16ms前のSpanと現在のSpanの比率(リアルタイムに更新)
 ・[ScaleEnd] 指が2点じゃなくなるとズーム終了

中心の座標は、開始時のもの固定ではなくてリアルタイムにタッチの点の中心座標に更新するのがAndroid標準です。なので、冒頭で書いたとおりChromeはバカ正直にタッチの中心座標を更新するため、マルチタッチの中心軸がブレブレになって見えたりするのです。

SBrowserはこのようなタッチ中心の座標ブレブレ問題を解決しているようです。

SBrowserの工夫とは・・・(まだわからず)

オープンソースでその実装箇所を探ってる最中です。分かり次第続報をお届けします。



と、長く時間が空きましたが、結局実装箇所はわかりませんでした。
Zoomのジェスチャを判別する部分は、他の機種との差分は(ゼロではないですが)多分ない。 で、気になってSBrowser.apkを引っこ抜いて、apktool d SBrowser.apk とかなんとか探ってみようかなと思いましたが、答えっぽいものにはたどり着けませんでした。

org/chromium/content/browser/ZoomManager.smaliとかを見る限りだとAndroid標準のScaleGestureDetector使ってるし、 多分、MultiTabPinchUtilのperformPinchZoomあたりでなにかゴニョっとやってるんだろうなぁと思いつつ…
若干悔しいながら、今回はこれまで。。

Android標準のScaleGestureDetectorの動作とカスタマイズポイントが見えたくらいでよしとしておきましょう。完全に自己満足ですが。

2014/09/23

"Androidのブラウザをカスタマイズする"ということ

めっちゃ会社でやってるネタなので企業秘密に触れるギリギリラインではありますが、
世の中、ブラウザはオープンソースの時代です( ー`дー´)キリッ ので、タイーホとならない程度に、書きたいことを書いてみようと思います。

そもそもなぜブラウザをカスタマイズするのか?

へたなカスタマイズはフラグメンテーション(改造によって互換性のないめちゃくちゃなものに・・・)のもとです。なので、やるべきではありません。
それなのに、なぜカスタマイズをするのか。

そこにはユーザの小さな小さな不満があるからです。

 ・タップしてからページが表示されるまでの時間が遅い。早くしたい。
 ・タッチスクロールの指に吸い付く度合いがヘボい。もっと指に気持ちよくついてきてほしい。
 ・電池めっちゃくう。ずっとブラウザ使ってても電池くわないようにしてほしい。

ほんとうに地味なことですが、こういうちょっとしたことがユーザの感覚に響くのです。

なくても困らないのだけども、あったほうが俄然いい

これがブラウザカスタマイズのモットーです。


で、ごちゃごちゃ論じるのはこのブログの趣旨には合いませんね。
オープンソースのブラウザコードを少しだけ読み解いてみましょう。


カスタマイズの説明の前に。

「ブラウザって、どうせタッチスクロールしたぶんだけ画面を動かしてるんでしょ?」というイメージをお持ちの方に、
ちょっとだけ認識を改めてほしいので、ブラウザのタッチスクロールの処理の仕組みを簡単に説明します。
まぁたしかにタッチスクロールした分だけ動かしてることには間違いないんですけど、登場人物が2人いて、それをここで明確にしておきたいのです。

登場人物1:タッチをひたすら監視する小人さん
Androidには、タッチイベントをリアルタイムに監視して、
「いまスクロールされたぞ!」「いまタップされたぞ!」って
都度言ってきてくれる小人さんがいます。


登場人物2:WebKit

WebKitの一番の役割は、
HTMLというフォーマットで書かれた文字列から
要求に応じてビットマップ画像を作り出すことです。




AndroidのWebViewは
前述のタッチイベント監視人さんとWebKitをうまく組み合わせて、
ユーザがタッチスクロールしたら画面を書きなおして
あたかも画面がスクロールしたかのように見せています。


ブラウザの「タッチスクロール」をすこしだけ改善してみよう

用意するもの:
 ・Android4.4.2か4.4.4の端末(Nexus 5)
 ・ページ内検索が速いPCブラウザ(IEはダメ、Google Chrome推奨)
 ・Androidのビルド環境


現在のAndroidのブラウザの多くに使われている"WebView"という部品は、じつは厳密にはスクロールした分だけ画面が移動してくれていません。
実際にスクロールされるのは、タッチスクロール量から数ミリメートルだけ引かれたぶんが、スクロールされます。

え?うそでしょ?とおもったあなた。実際にAndroid端末で以下のように動作を見てみてください。


"指に吸い付かないスクロール"を体感しよう


このブログをAndroidで見た時のスクリーンショットで説明してますが、YahooでもGoogleでもどんなページでも同じです。

どんなに頑張ってスクロールしても最終的に指の場所についてきてくれませんね?
今回はこれを、ちゃんと指についてきてくれるよう改善してみたいと思います。



 ・指に吸い付かない理由

正解から言ってしまうと、タッチイベントを監視している小人さんが"躊躇している"タイミングがあるからです。

専門用語で言うと、touch slopとよばれるもので、
「タップ」なのか「タッチスクロール」なのかを見分けるための、しきい値です。


どこまでがタップでどこからがスクロールか


ソースコードを見る前に、絵で説明します。

①は誰が見てもタップですね。これをスクロールだって言うと、手が震えがちな人はタップできませんね。
そして、③は誰が見てもスクロールですね。これをタップだって言われると、スクロールさせるにはどんだけ手を動かさんとダメなの?と思ってしまいますね。
じゃあ、②はどうでしょう?これは微妙ですね。

ただ、タッチ監視をしている小人さんは「微妙」という回答はできません。ユーザが画面に触れて何かアクションをした以上は、かならず「タップ」か「スクロール」かを決めてあげなければなりません。

そこでAndroidは1つバシッと指標を決めていて、
 ・8dp以上動いていない場合はタップ
 ・8dp以上動いている場合はタッチスクロール
としています。



ここでピンときたひとがいるかもしれません。

タッチスクロールしても8dp動いてない場合はタップと認識される。
タッチスクロールと認識され始めるのは、8dp目のスクロール部分から。

そう、まさにこの8dp捨てられているのがAndroidのWebViewの動作なのです。

さて、ソースを見てましょう。
http://tools.oesf.biz/android-4.4w_r1.0/xref/external/chromium_org/content/public/android/java/src/org/chromium/content/browser/third_party/GestureDetector.java

これがタッチを監視してる小人です。

    461     /**
    462      * Analyzes the given motion event and if applicable triggers the
    463      * appropriate callbacks on the {@link OnGestureListener} supplied.
    464      *
    465      * @param ev The current motion event.
    466      * @return true if the {@link OnGestureListener} consumed the event,
    467      *              else false.
    468      */
    469     public boolean onTouchEvent(MotionEvent ev) {

    535         case MotionEvent.ACTION_DOWN:

    553             mDownFocusX = mLastFocusX = focusX;
    554             mDownFocusY = mLastFocusY = focusY;


    558             mCurrentDownEvent = MotionEvent.obtain(ev);


    571             handled |= mListener.onDown(ev);
    572             break;


    574         case MotionEvent.ACTION_MOVE:
    578             final float scrollX = mLastFocusX - focusX;
    579             final float scrollY = mLastFocusY - focusY;


    583             } else if (mAlwaysInTapRegion) {
    584                 final int deltaX = (int) (focusX - mDownFocusX);
    585                 final int deltaY = (int) (focusY - mDownFocusY);
    586                 int distance = (deltaX * deltaX) + (deltaY * deltaY);
    587                 if (distance > mTouchSlopSquare) {
    588                     handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
    589                     mLastFocusX = focusX;
    590                     mLastFocusY = focusY;
    591                     mAlwaysInTapRegion = false;


    595                 }


    599             } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
    600                 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
    601                 mLastFocusX = focusX;
    602                 mLastFocusY = focusY;
    603             }
    604             break;


    606         case MotionEvent.ACTION_UP:


    615             } else if (mAlwaysInTapRegion) {

    616                 handled = mListener.onSingleTapUp(ev);
スクロール量の平方が、mTouchSlopSquareという値を超えているかどうかによって、
 ・onDown()→onScroll(),onScroll(),onScroll(),onScroll(),・・・
 ・onDown()→onSingleTapUp()
mListenerへのコールバックのされ方が2通りあります。

(いうまでもなく、前者はmTouchSlopを超えたタッチスクロール操作で、後者はmTouchSlopを超えないタップ操作。)


    413             final ViewConfiguration configuration = ViewConfiguration.get(context);
    414             touchSlop = configuration.getScaledTouchSlop();
    425         mTouchSlopSquare = touchSlop * touchSlop;
mTouchSlopSquareは↑で定義されていて、元をたどると http://tools.oesf.biz/android-4.4w_r1.0/search?q=config_viewConfigurationTouchSlop このへんから値を持ってきています。
Nexus 7 (2013)とかは8dpではなく、ちょっと大きめの12dpが指定されていますね。
# タッチパネルの精度が悪いからでしょうか…


ちなみに、ここまでのところ、「8dp捨てられている」という実装はありません。
タッチスクロール時、GestureDetector自体は
のように忠実に、判断結果と座標をmListenerへコールバックしています。
じゃあ、誰がonDown~初回onScrollのスクロール量を捨てているかというと、mListenerを使っている人です。
つまり、小人さんに監視を任せている、その司令塔が、捨てているのです。

http://tools.oesf.biz/android-4.4w_r1.0/xref/external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentViewGestureHandler.java
司令塔はこいつです。

     30 class ContentViewGestureHandler implements LongPressDelegate {
     31 

     69     private GestureDetector mGestureDetector;
このとおり、タッチ監視の小人であるGestureDetectorを配下に持っています。
    297     private void initGestureDetectors(final Context context) {
のなかに小人による監視結果をさばくための実装があります。

そこには、このように書かれています。

    304                     @Override
    305                     public boolean onDown(MotionEvent e) {

    308                         mTouchScrolling = false;
    309                         mSeenFirstScrollEvent = false;

    316                         if (sendMotionEventAsGesture(GESTURE_TAP_DOWN, e, null)) {

    318                         }
    319                         // Return true to indicate that we want to handle touch
    320                         return true;
    321                     }
     323                     @Override
    324                     public boolean onScroll(MotionEvent e1, MotionEvent e2,
    325                             float distanceX, float distanceY) {

    327                         if (!mSeenFirstScrollEvent) {
    328                             // Remove the touch slop region from the first scroll event to avoid a
    329                             // jump.
    330                             mSeenFirstScrollEvent = true;
    331                             double distance = Math.sqrt(
    332                                     distanceX * distanceX + distanceY * distanceY);
    333                             double epsilon = 1e-3;
    334                             if (distance > epsilon) {
    335                                 double ratio = Math.max(0, distance - scaledTouchSlop) / distance ;
    336                                 distanceX *= ratio;
    337                                 distanceY *= ratio;
    338                             }
    339                         }
     359                         // distanceX and distanceY is the scrolling offset since last onScroll.
    360                         // Because we are passing integers to webkit, this could introduce
    361                         // rounding errors. The rounding errors will accumulate overtime.
    362                         // To solve this, we should be adding back the rounding errors each time
    363                         // when we calculate the new offset.
    364                         int x = (int) e2.getX();
    365                         int y = (int) e2.getY();
    366                         int dx = (int) (distanceX + mAccumulatedScrollErrorX);
    367                         int dy = (int) (distanceY + mAccumulatedScrollErrorY);
    368                         mAccumulatedScrollErrorX = distanceX + mAccumulatedScrollErrorX - dx;
    369                         mAccumulatedScrollErrorY = distanceY + mAccumulatedScrollErrorY - dy;
    370 
    371                         mExtraParamBundleScroll.putInt(DISTANCE_X, dx);
    372                         mExtraParamBundleScroll.putInt(DISTANCE_Y, dy);
    373                         assert mExtraParamBundleScroll.size() == 2;
    374 
    375                         if ((dx | dy) != 0) {
    376                             sendGesture(GESTURE_SCROLL_BY,
    377                                     e2.getEventTime(), x, y, mExtraParamBundleScroll);
    378                         }
上のコードででっかく太字にしたところが、「8dp捨てられている」実際のコードです。
初回にスクロールイベントを判定した時点で一気にばっこーーん!と8dpスクロールとんでスクロールされるのは不自然なので、滑らかに見せるために、8dp捨ててスクロール量を計算しているようです。

 ・指に吸い付かせる

まずは、馬鹿になって、AndroidのWebViewの心遣い(ばっこーーーん!とスクロールが飛んでしまうのを防ぐために)を無視してしまいましょう。
    335                                 double ratio = Math.max(0, distance - 0) / distance;
これで
mmm external/chromium_org/android_webview && mmm frameworks/webview/chromium/

cd out/target/product/hammerhead/
adb root
adb remount
adb shell stop
adb sync system
adb reboot
端末を焼き帰ると、ぱっこーーーーん!と飛びながらも追従するブラウザが出来上がりましたね?
(一気にステップとばしすぎ!と思われても仕方がない書き方w ビルドの説明まで書いてるとキリがないので・・・)

で・・・
こんなのが製品として成り立つわけがありませんね。

実際に製品にする人々はどういう工夫をしているかというと・・・(以下略

F-06Fのオープンソースを覗いてみてください。


歯切れ悪いですが、以上です(笑)


もっといい方法があるよ!という意見がありましたら、どしどしコメントください。

2014/02/04

MonkeyRunnerとuiautomator

最近のAndroidでは、標準で使える(楽しい)テストツールもだんだんと充実してきました。今回は、そのなかで自動試験に使えそうな、MonkeyRunnerとuiautomatorを簡単に紹介します。
私はPython使いなので、uiautomatorもPythonバインディングの方しか知りません・・・。(Javaでの使い方は他のページもあるし、そちらをみてください)

※重要な心得※
自動試験は手数を減らすのが目的です。
手数が減らないようなものを自動試験しても仕方がないですし、目的が不明瞭なものはかえって自動化しようとしてできなくてハマります。
私の経験上、「なにか定型的な作業やってるなぁ」と感じた時こそが自動化のはじまりです。


・MonkeyRunner
概念的には

 PC…adb接続…→[tcpport:5555 Android端末]

てなかんじで、ソケット通信で命令をうじゃうじゃ流し込んで端末をあれこれ操作します。

PC側主体で端末をうじゃうじゃ動かす形式ゆえ、後述するuiautomatorのように、画面の作りを意識したような試験(「OKボタンをおす」とか「メニューの2番めの項目を選ぶ」とか)には向いていません。

逆に、画面の作りに依存しない試験についてはuiautomatorより簡単にかけます。

ドキュメントはおもに↓を見ておけばよいでしょう。(MonkeyRunnerクラスは、かなり特殊なケースでしか使わないので)
http://developer.android.com/tools/help/MonkeyDevice.html

で、いきなりコードをかいちゃいましょう。
中身はそれなりに読めばわかるでしょう。

# -*- coding:utf-8 -*-

# 決まり文句
import time
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice
d = MonkeyRunner.waitForConnection(deviceId="EP7331C9G7")

# screen on
d.wake()
time.sleep(1)

# swipe up for unlock
x=int(d.getProperty("display.width"),10)/2
y1=int(d.getProperty("display.height"),10)/4
y0=y1*3
d.drag((x,y0),(x,y1),0.2,10)
time.sleep(1)

# launch google chrome
d.press("KEYCODE_HOME",MonkeyDevice.DOWN_AND_UP)
time.sleep(1)
d.shell("am start -n com.android.chrome/com.google.android.apps.chrome.Main")
time.sleep(1)

# browse yahoo.com
d.press("KEYCODE_SEARCH",MonkeyDevice.DOWN_AND_UP)
time.sleep(1)
for i in xrange(200):
 d.press("KEYCODE_DEL",MonkeyDevice.DOWN_AND_UP)
d.type("http://www.yahoo.com/")
time.sleep(1)
d.press("KEYCODE_ENTER",MonkeyDevice.DOWN_AND_UP)
time.sleep(0.2)
d.press("KEYCODE_ENTER",MonkeyDevice.DOWN_AND_UP)
time.sleep(5)

# swipe up 3 times
y0=y1*2
for i in xrange(3):
 d.drag((x,y0),(x,y1),0.2,10)
 time.sleep(0.5)
time.sleep(3)

# swipe up 3 times
y0=y1*2
for i in xrange(3):
 d.drag((x,y1),(x,y0),0.2,10)
 time.sleep(0.5)
time.sleep(3)

# return to HOME
d.press("KEYCODE_HOME",MonkeyDevice.DOWN_AND_UP)
time.sleep(2)

d.press("KEYCODE_POWER",MonkeyDevice.DOWN_AND_UP)
time.sleep(1)

フリックはswipeとか気の利いたメソッドはないので、dragメソッドで点の数をうまく調整して実行してやる必要があります。
VSYNCの周期が60Hzなので、それを意識して1点あたり16msくらいになるようにdurationとstepを決めてやらないと、美しいスワイプ動作にはなりません。

これとは別にadb logcatを監視するスレッドを作ってやれば、
logcatに変なエラーが出た時だけそのスクリーンショットを残す、とか気の利いたことができます。
logcat監視スレッドはこのあたりのソースを拝借して改変すれば割と簡単に作れます。

繰り返し試験をやるときの注意ですが、これ結構重要で、
間違っても
for i in xrange(1000):
    print i,"回目のテスト"
    doTest()
なんてやってはいけません。
17回目くらいの試験でたまたま友達からのメールがとどいて、思ったように操作ができずMonkeyRunnerがエラーになったりしたら、そこで自動試験が終わっちゃいます。
17回目の試験でコケても18回目、それでコケても19回目、とやっていってもらわないと困りますね。
import os,sys
if sys.argv==1:
  for i in xrange(1000):
    os.system("monkeyrunner %s %d"%(sys.argv[0],d))
else:
  i=int(sys.argv[1],10)
  print i,"回目のテスト"
  doTest()
荒業ではありますが、こう書いたほうがはるかに頑健な自動試験スクリプトとなります。


・uiautomator
概念的にはMonkeyRunnerとは対照的で

 PC…実行プログラムを転送→[Android端末]

てなかんじで、Android端末側が自主的に、書かれた命令をうじゃうじゃ実行します。
画面を意識した試験を行うための命令セットもたくさん用意されているので、UIを中心に試験するなら間違いなくこっちでしょう。

Pythonバインディングのインストールは
sudo apt-get install python-pip
sudo pip install uiautomator

ちなみに、ドキュメント(※1)はあまり充実していないので、ソースコード(※2)とAndroidのリファレンス(※3)をページ内検索したり行ったり来たりで読むのがいいと思います。
※1 https://github.com/xiaocong/uiautomator の下の方
※2 https://github.com/xiaocong/uiautomator/blob/master/uiautomator.py
※3 http://developer.android.com/tools/help/uiautomator/UiDevice.html

あ、ちなみにWindowsじゃMonkeyRunnnerは動かない(らしい)ですが、uiautomatorは動きます!私はLinux使うのであんまり気にしてないですが、会社とかだとWindowsオンリー!っていうところもあるでしょうから。

WindowsでnumpyとかMatPlotlibとかひと通り入れた状態でのPython3環境ではありますが、以下のようにすればuiautomatorをインストールせずとも、味見程度に動かすことはできます。


mkdir uiautomator_test
cd uiautomator_test

# urllib3をおとしてくる
git clone https://github.com/shazow/urllib3.git
mv urllib3 _urllib3
mv _urllib3\ullib3 urllib3

# uiautomatorを落としてくる
git clone https://github.com/xiaocong/uiautomator.git

touch mytest.py
explorer .

C:\Users\yi01\Desktop\uiautomator_test>ls
_urllib3  mytest.py  uiautomator  urllib3
こんなかんじのフォルダ構成になったらmytest.pyにスクリプトをゴニョゴニョっと書いていきます。あ、あと一応adb接続でいろいろ送り込むので、以下のようになってる前提です。
C:\Users\yi01>adb devices
List of devices attached
EP7331C9G7      device

こちらもいきなりコードからですが、設定画面を開いて「マップ」のデータ全削除未遂をするという糞シナリオです。
設定画面のなかで「アプリ」というのはどこにあるか、座標的にはわかりませんよね?そういうMonkeyRunnerでは痒いところに手が届かない!というのが、uiautomatorでは手が届いちゃうんです。
# -*-codoing:utf-8 -*-

# 決まり文句
from uiautomator.uiautomator import device as d

#デバイスID指定でやりたいときは↓
#from uiautomator.uiautomator import Device
#d=Device("EP7331C9G7")

import time

for i in range(3):
    print("画面よ、つけー!")
    d.screen.on()
    time.sleep(1)

    print("画面よ、消えろー!")
    d.screen.off()
    time.sleep(1)

print("画面よ、つけー!")
d.screen.on()
time.sleep(2)

print("スワイプして画面ロック解除するぞ")
x=d.displayWidth/2
y0=d.displayHeight/2
y1=y0/2
y0+=y1/2
d.swipe(x, y0, x, y1, steps=12)
time.sleep(1)

print("HOME")
d.press.home()
time.sleep(1)


#リファレンスには書いてないけどソースを見るとadbも使えるっぽい。
def adb_shell(device,*cmd):
    c=["shell"]
    c.extend(cmd)
    print (" ".join(c))
    return [s.decode("utf-8") for s in device.server.adb.cmd(*c).communicate()]

print("設定アプリを立ち上げよう")
adb_shell(d,"am start com.android.settings/.Settings")
time.sleep(2)


d(text="アプリ").click()
time.sleep(3)

d(text="マップ").click()
time.sleep(3)

d(text="データを削除").click()
time.sleep(2)

print("イッヒッヒ!!")

d(text="キャンセル").click()
time.sleep(2)

print("やさしいから消さないもーん")

d.press.home()
time.sleep(2)

print("ほな、さいなら~")
time.sleep(2)

むちゃくちゃ手抜きですけど、こんな具合になります。↓

これだけだと、はて何に使うんかいね?って感じがすると思いますが、
d(text="hogehoge") 以外にもセレクタの条件いろいろ使えるみたいです。

  • texttextContainstextMatchestextStartsWith
  • classNameclassNameMatches
  • descriptiondescriptionContainsdescriptionMatchesdescriptionStartsWith
  • checkablecheckedclickablelongClickable
  • scrollableenabled,focusablefocusedselected
  • packageNamepackageNameMatches
  • resourceIdresourceIdMatches
  • indexinstance
↑公式リファレンスからイタダキしました。

resourceIdとか、自分の作ったアプリ以外だとわかんないじゃん!と思うかもしれません。
そんな人のためにも、Androidはすばらしいツールを用意してくれています。

uiautomatorviewer

FirefoxでいうDOMインスペクタ的なものです。
画面のスクリーンショット上で、この要素のIDなぁに?パッケージ名は?などなど
けっこういろいろ見えます。

使い方は
http://developer.android.com/tools/testing/testing_ui.html
ここに詳しく載ってるのでそちらを・・・。


そんなわけで、
ともかくも、活かすも殺すも、発想次第!

自動試験は思い通りに行かないことも多く、作るのに結構時間はかかりますが、
一度作ってしまえば終夜試験で大幅に工数削減とか、繰り返し試験で耐久性向上!なんてことがいとも簡単にできるようになります。

私も、半年前くらいから、自動試験の虜になってしまいました^ ^;;
さあ、ぜひぜひおためしあれ。