woshidan's blog

あいとゆうきとITと、とっておきの話。

Androidの通知チャンネルの振る舞いをプロジェクトのtargetSDKVersionを変えて少し試してみた

Android OのEaster Eggはoctopusでした*1が、OのコードネームはOreoでしたね。さて、Android Oではユーザーが通知をより便利に管理するために通知チャンネルが導入されました。

通知チャンネルがなんたるかや主要な扱い方については公式他に任せるとして、この記事については、通知チャンネルについて自分が試してみた少しマイナーなことについてまとめようと思います。

TL;DR

  • Android Oの端末でも、アプリのtargetSDKVersionが25以下なら通知チャンネルを作成せずとも通知は届く
  • Android Oの端末でも、アプリのtargetSDKVersionが25以下かつライブラリのtargetSDKVersionが26で、ライブラリ側で通知チャンネルを作成しているならば、その通知チャンネルが作成&利用可能であると同時に「Uncategorized」 という通知チャンネルが作成される
  • Android Oの端末で、アプリのtargetSDKVersionが26なら、通知チャンネルを作成しないと通知が届かないし、その旨のToastが出る
  • 通知チャンネルを作成したあと、同じIDを指定して作成し直すことで「チャンネル名」などのチャンネルの設定が更新できるが、バッジの有無の設定は更新できない

以下、検証用のコードなどを置いておきます。

Android Oの端末でも、アプリのtargetSDKVersionが25以下の場合

// アプリのプロジェクトのbuild.gradle
android {
    compileSdkVersion 25
    buildToolsVersion '25.0.3'

    defaultConfig {
        applicationId 'com.example.woshidan.test'
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1"
    }
...
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final Notification.Builder builder = new Notification.Builder(this)
                .setSmallIcon(android.R.drawable.ic_btn_speak_now)
                .setContentTitle("Title")
                .setContentText("Text");

        final NotificationManager notificationManager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify("tag", 1234, builder.build());

届いた通知は以下のような形で表示され、通知チャンネルの表示は出ないようです。

Android Oの端末でも、アプリのtargetSDKVersionが25以下かつライブラリのtargetSDKVersionが26で、ライブラリ側で通知チャンネルを作成している場合

のように、ライブラリプロジェクトを作成して、ライブラリプロジェクト側で targetSdkVersion を26にします。

// libraryプロジェクトのbuild.gradle
apply plugin: 'com.android.library'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.1"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
// アプリ側のプロジェクトのbuild.gradle
dependencies {
    ...
    compile project(path: ':mylibrary')
}
package com.example.woshidan.mylibrary;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.support.annotation.RequiresApi;

/**
 * Created by woshidan on 2017/08/22.
 */

public class NotificationChannelCreator {
    @RequiresApi(api = 26)
    public static void createChannel(final Context context) {
        NotificationChannel channel = new NotificationChannel("my_channel_id", "通知テストチャンネル", NotificationManager.IMPORTANCE_DEFAULT);
        channel.setDescription("テストの説明です");
        channel.setShowBadge(true);

        // create or update the Notification channel
        final NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);
    }
}
// アプリ側のプロジェクト
public class MainActivity extends Activity {

    @RequiresApi(api = 26)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        NotificationChannelCreator.createChannel(this);

        // 通知チャンネルを指定しないで通知を送る
        // ここで通知を送っていない場合でもUncategorizedチャンネルはできる
        final Notification.Builder builder = new Notification.Builder(this)
                .setSmallIcon(android.R.drawable.ic_btn_speak_now)
                .setContentTitle("Title")
                .setContentText("Text");

        final NotificationManager notificationManager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify("tag", 1234, builder.build());

Android Oの端末で、アプリのtargetSDKVersionが26の場合

// アプリのプロジェクトのbuild.gradle
android {
    compileSdkVersion 26
    buildToolsVersion '26.0.1'

    defaultConfig {
        applicationId 'com.example.woshidan.test'
        minSdkVersion 16
        targetSdkVersion 26
        versionCode 1
        versionName "1"
    }
...
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final Notification.Builder builder = new Notification.Builder(this)
                .setSmallIcon(android.R.drawable.ic_btn_speak_now)
                .setContentTitle("Title")
                .setContentText("Text");

        final NotificationManager notificationManager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify("tag", 1234, builder.build());

通知チャンネルの更新について

        NotificationChannel channel = new NotificationChannel("my_channel_id", "通知テストチャンネル", NotificationManager.IMPORTANCE_DEFAULT);
        channel.setDescription("テストの説明です");
        channel.setShowBadge(true);

        // create or update the Notification channel
        final NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);

一度上記のコードで通知チャンネルを作成すると、下記のようなチャンネルができます。

その後、下記のコードで更新すると

        NotificationChannel channel = new NotificationChannel("my_channel_id", "通知テストチャンネル Updated", NotificationManager.IMPORTANCE_HIGH);
        channel.setDescription("テストの説明を変えました");
        channel.setShowBadge(false);

        // create or update the Notification channel
        final NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.createNotificationChannel(channel);

こうなります。

現場からは以上です。

Android SDK Toolsのバージョンを25.2.xから25.3.xにあげたらcocos runコマンドが実行できなくなった

症状

Android SDK Toolsのバージョンをあげて cocos runコマンドを実行した際、以下のメッセージが出てビルドできなくなりました。

$ cocos run -p android --android-studio
Building mode: debug
Using Android Studio project : /Users/woshidan/path/to/Cocos2dxProject/proj.android-studio
running: '/Users/woshidan/Library/Android/sdk/tools/android update project -t android-21 -p /Users/woshidan/path/to/Cocos2dxProject/proj.android-studio/app'

*************************************************************************
The "android" command is deprecated.
For manual SDK, AVD, and project management, please use Android Studio.
For command-line tools, use tools/bin/sdkmanager and tools/bin/avdmanager
*************************************************************************
Invalid or unsupported command "update project -t android-21 -p /Users/woshidan/path/to/Cocos2dxProject/proj.android-studio/app"

Supported commands are:
android list target
android list avd
android list device
android create avd
android move avd
android delete avd
android list sdk
android update sdk
Error running command, return code: 2.

原因

cocos コマンドは Android SDK Tools のバージョン 25.2.x 以下でサポートされている android コマンドに依存しており、そのコマンドの一部が Android SDK Tools25.3.0 でサポートされなくなりました。

サポートされなくなったコマンドは Ant( =Eclipse) 用のコマンドで、GoogleEclipse のサポートをやめると言っているので今後もサポートされることはないでしょう。

対応

本当は、この辺りを読んで、NDKでcocos コマンドでビルドしていた部分を書き換えて行くのが良いと思いますが、さしあたっては 公式リポジトリの関連issue にある、こちらのコメント からDLできる古めのAndroid Toolsを使ってしのぐかなーという感じです。

ソフトウェアテスト技法ドリルを読んだ

ソフトウェアテスト技法ドリル―テスト設計の考え方と実際

ソフトウェアテスト技法ドリル―テスト設計の考え方と実際

Web系のソフトウェアエンジニアだとある程度TDDの文脈でテストコードを書くと思います。

自分は今アプリ用のSDKの担当でSDKについてリリース前のQAも行うのですが、サーバや外部サービスとの連携部分で膨らみがちなテストを漏れが出ないようにしつつ、手早く設計&実行できるようにするか、みたいなところが気になって読みました。

前書きによると、この本は、Boris Beizerの「ソフトウェアテスト技法」に書いてあることの一部を初級者でも読めるように噛み砕きつつ、設計の際に利用できるツールの紹介をしたり、日本人の考えた手法も合わせて取り上げたり、といった感じです。

もともと敷居が高いものをさらっと取り上げた結果薄い、みたいな感じなので、TDD少しやったことあります、程度の自分にとっては、HAYST法あたりから結構噛み砕くのが重たいな、といった感じでした。

「異常値は他の異常値を隠す」、状態遷移表、Nスイッチカバレッジ(N=2~3)、タイミングも水準、あたりが明日からすぐ使えそうな感じでした。

以下、印象に残ったことをいくらかメモします。

  • 怪しい箇所の探し方
    • 間・対象・類推・外側、意地悪な条件を意識
  • 知っているものほど危うい
    • 外部連携などした時に、前提が異なってデータの移行ができないなどのトラブルの元IOS基準など外部基準を調べたり(日付のiCalenderなど)
  • 同値分割・境界値分析は、ふだん「テストや動作確認をする」という言葉を使う時に連想するようなことに近い
  • 有効な値は複数同時に確認できるが、無効な値については一回に一つずつ(=異常値は他の異常値を隠す)
  • 関係性がある複数の変数を同時にテストする「ドメイン分析テスト」
    • on/off/in/outで、ある因子(=関数に入れる一つの変数orテスト時に変化させる項目)をon/offさせるときは、他の因子はin(Binderのドメイン分析マトリクス)
    • 複数の条件の組み合わせをチェックしたいとき、原因結果グラフを使い、それに対応したツールを使うことでディシジョンテーブル(因子の値とその結果を表す表。)が自動生成できる
    • これが成り立たなかったら次はこの条件を考える… みたいなことをしたいときはCFD法で書くとスッキリしやすい
  • 仕様としては機能と機能の間に関連がないことになっているシステム上の要因どうしを組み合わせて「想定外」のバグがないかのテストを効率よくしたい
    • HAYST法やペアワイズを使う
    • 直交表-> 複数の条件のうち、任意の2~3つの因子の組を選んだら、因子の取りうる代表値(水準という)の全ての組合わせがテストができるようなテストを作る
    • HAYST法の詳細は、 https://www.fujixerox.co.jp/company/technical/tr/2015/pdf/p_04.pdf
    • HAYST法に関連して紹介された「エキスパートの知見を必要最小限とする」ためのプロセスは度々見返すと良さそう
    • 因子同士の組み合わせにより発生する、一緒に実行できない水準の組み合わせを禁則と言って、禁則がある場合の処理として、その因子の組み合わせを一つの因子として、禁則の部分を除いて水準を作り直す(相互排他因子融合手法)
    • 直交表の割り当ては因子の組み合わせが決まれば既存の割り当て表から選ぶ感じ?
    • ペアワイズはなんらかの行列的演算を持って、直交表が持っている「全組み合わせがどう数回あらわれるという性質」を崩してテストケース減らしたい時に使う
    • それぞれの機能で必要とわかっている前提の条件がいくつもあるような場合の成否をするのはこの章のテスト設計手法の話ではない
  • 状態遷移のテスト
    • C0 パスカバレッジ 100% 全ての状態を少なくとも一度以上
    • C1 パスカバレッジ 100% 全ての遷移も少なくとも一度以上
      • では足りないことがあるので、状態遷移表から「イベントが適用できない」(=遷移できない)場合、N/A(Not Applicable)を探す
    • ある遷移のあと、内部状態が変化したことももれなくテストしたい -> Nスイッチカバレッジ
      • 状態遷移表を関係行列に… それをN乗-> N回のイベントで到達できるすべてのパスがわかる
    • 状態が多すぎる場合はどうするか?
      • ラルフチャートから内部変数をリストアップしてやる。状態の遷移については、典型的な遷移パターンを因子としてまとめてしまう。タイミングも因子として扱うことが
  • 身体測定とソースコードの静的解析はいままで取ったことがある周りの状態と比較して現在のプロダクト(人間)の状態がやばいか早めに判断する、という点で似てるよね

直交表の例

3つの因子があるとき、そのうち2つの因子を選んで、2つ同士は、すべての水準の値の組み合わせがテストできるようにする表。

No コイン1の表裏 コイン2の表裏 コイン3の表裏
1
2
3
4

水準の値の組み合わせについて確認すると、

  • コイン1の表裏とコイン2の表裏を見ると {コイン1-表, コイン2-表}, {コイン1-表, コイン2-裏}, {コイン1-裏, コイン2-表}, {コイン1-裏, コイン2-裏} の4通りが全て出ている。
  • コイン2の表裏とコイン3の表裏を見ると {コイン2-表, コイン3-表}, {コイン2-表, コイン3-裏}, {コイン2-裏, コイン3-表}, {コイン3-裏, コイン3-裏} の4通りが全て出ている。
  • コイン3の表裏とコイン1の表裏を見ると {コイン3-表, コイン1-表}, {コイン3-表, コイン1-裏}, {コイン3-裏, コイン1-表}, {コイン3-裏, コイン1-裏} の4通りが全て出ている。

API19, API21のAndroid端末で透過GIFを表示すると、透過色の部分が真っ黒になることがある

stackoverflow.com

stackoverflow.com

API19とAPI21のNexus 5のエミュで再現。API18以下、API23以上のエミュでは未確認。

透過色を決めておいて、その色の部分を後から透明にするという対応もありますが、他の画像形式を用いてもよいのではないでしょうか。 一定以下のAPI Level以下ならわかりやすいんですが、確認しててこの二つのバージョンだけだったので焦りました。。

NSURLSessionを使って通信処理を書いてみる

http://woshidan.hatenablog.com/entry/2017/06/18/014734 の記事の続きで、先の記事の中の

2. Foregroundでダウンロード処理を開始する場合は、ダウンロード中にアプリが停止ないし終了してもよいようにそのダウンロード処理の管理をシステム側へ手渡すことができる

の部分についてメモします。

ポイント

  • ファイルをダウンロードするときは、NSURLSessionを使う
    • そうすることでアプリがバックグラウンドでデータ通信している間はiOSのシステム側がアプリを停止してもダウンロード処理はシステム側でしてくれる
  • ダウンロード/アップロードが終わったり、アプリに注目させる必要がある場合はアプリを起動させることができる
  • どのNSURLSessionオブジェクトによるデータ通信処理か、どのNSURLSessionオブジェクトによるapplication(_:handleEventsForBackgroundURLSession:completionHandler:コールバックかを判定するためには、NSURLSessionとNSURLSessionConfigurationを作成する際に idenrifier(NSString) を使う

試し書き

試しに書いてみたときのメモと置いておきます。

class ViewController: UIViewController, URLSessionTaskDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        let button = UIButton();
        button.frame = CGRect(x: 200, y: 200, width: 120, height: 50);
        button.setTitle("download test", for: UIControlState.normal);
        button.backgroundColor = UIColor.blue;
        button.addTarget(self, action: #selector(onClick(sender:)), for: UIControlEvents.touchUpInside);
        self.view.addSubview(button);
    }
    
    func onClick(sender: UIButton) {
        // let session = URLSession();
        // -> [NSURLSession downloadTaskForRequest:downloadFilePath:resumeData:completion:]: unrecognized selector sent to instance 0x6180000103a0
        // see: https://stackoverflow.com/questions/32905874/nsurlsession-datataskforrequestcompletion-unrecognized-selector-sent-to-instan
        // let config = URLSessionConfiguration.default;
        let config = URLSessionConfiguration.background(withIdentifier: "test");
        // ここのidentiferが同じで、続けてボタン押してDLしようとした場合
        // 2017-07-05 06:58:17.522 NSURLSessionTest[52291:6981685] A background URLSession with identifier test already exists!
        config.sessionSendsLaunchEvents = true;
        // config.isDiscretionary = true;
        
        // URLSessionのコンストラクタでDelegateの設定をすること
        // let session = URLSession(configuration: config);
        let session = URLSession(configuration: config, delegate: self, delegateQueue: nil);
        let url = URL(string: "https://some.page.html");
        
        // let config = URLSessionConfiguration.background(withIdentifier: "test"); <- URLSessionConfiguration.background.default の変更
        // 上記変更はバックグラウンドで処理ができるようにするため
        // この場合は下記のようにタスクを生成する際に、Completion Handlerを渡すのではなく
        // URLSessionTaskDelegateを渡し、レスポンス取得後の処理はDelegateに書く
        // Terminating app due to uncaught exception 'NSGenericException', reason: 'Completion handler blocks are not supported in background sessions. Use a delegate instead.'
        //        let task =  session.downloadTask(with: url!, completionHandler: { (data, response, error) in
        //            DispatchQueue.main.async(execute: {
        //                print(response);
        //                print("DONE");
        //            })
        //        });
        let task =  session.downloadTask(with: url!);
        task.resume();
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    
    // URLSessionTaskDelegateのメソッド
    // データ通信完了時の処理
    // https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate/1411610-urlsession
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if (error != nil) {
            print("HTTPリクエスト失敗処理");
        } else {
            print(task.response);
        }
    }
    
}

参考

久しぶりだったので試しに少し書いて見るだけで力尽きてしまった。もう一回調べたほうがよさそう。 URLSessionTaskDelegate と URLSessionDelegateの違いとか、各Delegateメソッドの違いとか。

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

データベーススペシャリストの試験を受けて面白かったのでどんな試験だったか呟いてみる 

こないだデータベーススペシャリストに合格して、解いてる間は自分が冗談を本気に受け取る性質なところもありIPAの試験は受けることすら揶揄される感じがあってつらかったのですが、年度によっては偏ったものもあると思ったものの全体として解いたり調べたりしてて楽しい問題だなぁと思ったので、その内容についてのポエムです。

書いてる人について

一ヶ月で合格しました系の記事を求めている人に申し訳ないんですが、元々DB周りの話が好きで結構アプリケーションで触るSQLとかindexの付け方とかテーブル設計のための業務知識の眺め方の基礎みたいなのは新人並みには勉強してました。

それでもやっぱり全然足りなくて悔しい思いをすることがあったのですが、どこから手をつけていいかわからず、もう少し系統立てて勉強してみたいなという気持ちがあって受験しました。

事前に読んでいた本は

〔入門〕はじめてのデータベース

〔入門〕はじめてのデータベース

書き込み式SQLのドリル 改訂新版

書き込み式SQLのドリル 改訂新版

SQL実践入門──高速でわかりやすいクエリの書き方 (WEB+DB PRESS plus)

SQL実践入門──高速でわかりやすいクエリの書き方 (WEB+DB PRESS plus)

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

SQLアンチパターン

SQLアンチパターン

あたりで、中二病により

エキスパートのためのMySQL[運用+管理]トラブルシューティングガイド

エキスパートのためのMySQL[運用+管理]トラブルシューティングガイド

を持っていましたが、鍵本を使うような事態まで担当したことがあまりなく、幾らかのテーブル設計とindex設計して失敗したーしなかったーって一年くらいやったあと、偉い人の鶴の一声でAndroidに転向した感じで、今回の受験は趣味です。

試験用の勉強期間は2ヶ月くらい。

試験の概要

問題の構成はIPA情報処理技術者の高度試験におなじみの、午前1, 2, 午後1, 2の四部構成です。

基本的には午前を除き、問題で具体的に取り扱うのは、RDBMSだけです。特に午後はそうです。いま、NoSQLのデータベースを全く使っていないサービスは少ないのではないかという時代にRDBMSだけという気持ちもありますが、自分にとっては、十分価値はありました。

午前1は受けていないのでおいておいて、午前2は他の分野も合わせた用語問題と簡単な正規化やトランザクション排他制御などについて問われる形となっていてNoSQLについての用語はここに少しだけ出ます。

個人的に一番豪華なのは午後1で、午後1は試験範囲のいろんな分野がぎゅっと濃縮されており、

  • テーブルの正規化・非正規化に関する検討。具体的に言うと、まず、中途半端に正規化されたテーブルが提示されてそれをどこまで正規化してるか答えさせてから正規化。正規化されてない場合の問題点をいちいち説明させる
  • データベースの移行について、移行用のクエリの形や移行方法を2~3種並べてどの程度時間がかかるか計算させる
  • データベースのトランザクション排他制御について、それっぽいクエリを5つくらい並べてロックがどの組み合わせでかかるか具体的に指摘させる
  • 各種indexの場合のデータ探索時の特性に関して、たとえばクラスタ索引と非クラスタ索引の場合、ページの読み込まれかたが違うから分析用のバッチ処理に使うデータの規模考えて効率的なのどっちですか

などなど、多分、自分と似たような「1~2年開発してみて、ある程度モデル同士の関係を考慮しつつ正規化されたテーブルを設計してn+1などの問題がないクエリは書け、問題のあるテーブルの修正も提案できてrailsの上でSQLをいじって怒られることは減ってきたが、次はなにをやってみたらいいのか具体的によくわからないwebエンジニア」にとって、格好の目標となるのではないでしょうか。いや、いまはあっぷあっぷでこれ解いてるけど、いつかスラスラ説明できるようになりたいと思ったのでなったというか。。

正直時間的にも午後1は制限時間の45分で解くのがしんどいのですが、その分、一問解いてわからなかったことを調べるたびに、わからなかったことに関する用語はこれか、こういう観点があるのかって探検するための地図をもらっているような気持ちでした。鍵本も初めて机上ではあるかもしれませんが、具体的な問題のために取り出せたので満足です。

さて、午後2なんですけど、データベーススペシャリストの試験が受けやすいと言われる同時に国語の試験と揶揄される原因でして、だいたい論理設計1本勝負か、物理設計そのほかを絡めた論理設計から一題選ぶ方式となっています。そして、この論理設計一本勝負の方が問題形式としては大胆で、

  • まず、業務について書かれた長文10ページを読みます
  • 次にそれをふまえて答案用紙にありったけの時間をかけてカラム名とテーブル名を書きなぐります

というワイルドな問題構成であることが多いです。図の記入欄の余白、足りないんですけど??

これよりもっと少ないページ数に収まる要件の一部をなんだかんだ一週間、少なくとも数日は議論して決める自分の知ってる実務からすると、これ120分で読ませて書かせちゃうかーって感じなんですが。。

喉元過ぎた今眺めると「ドメインロジックとデータベースの知識の両方があって初めて正しい設計ができる。知識を生かすために、後々困らないように必要な知識をくまなく汲み取ってこそのプロである。それも、急に人が相談しにきて今日中にって言われた、みたいな場合でもある程度の方向性を示せる上級エンジニアたれ」みたいな、強い圧力を感じます。痺れる(腕が)。

なお、この設定、実際にそのままrailsでアプリケーションがかけちゃいそうな勢いで細かいので、設計の演習や練習用のアプリの設定に困っている人も、無料で公開されていますし一回みてみるといいのではないでしょうか。リレーショナルモデルに沿ったある一派の設計に浴びるように触れる、という体験としても非常に楽しいです。時々でもなく文章量にうんざりしますが、いろんな職場や業界で使われるシステムについて書かれた文章を読むのは社会見学みたいで不謹慎ながら面白かったです。

まとめ

午後1はサーバサイドに戻ることになったら一通り解き直したいレベルでよかったです。午後2で問われている領域には知識以上にビジネスロジックに噛んでいくコミュ力とか信頼とかを築く力といった部分が先立つんだろうな、ということでちょいと虚しさもありましたが。。

やっぱりめっちゃ怖い人からこんなもんって言われそうであれなんですが、闇雲にやるよりこの試験問題で知らないことを教えてもらってよかったよ!っていうのを言いたかったので、現場からは以上です。