SQLite のデータをコマンドプロンプトから参照する

概要

AndroidSQLite を使う際に、コマンドプロンプトから SQL 文を実行してデータを確認したい場合があります。 実機に sqlite3 がインストールされていない場合の実行方法について書きます。 但し、実機で base64 コマンドを実行できる場合の方法です。

確認環境

参考情報

解説

AndroidSQLite を使っていれば、コマンドプロンプトから SQL 文を実行してデータを確認したくなるもの。 しかし、実機に sqlite3 コマンドがインストールされていない。 sqlite3 を実機にインストールするには root 権限が必要だが、root 取得はしたくない。 その場合には、データベースファイルを PC にコピーする。

コピーをするために下記のコマンドを実行する。

$ adb shell run-as <アプリのパッケージ名> toybox base64 databases/<データベースファイル名> | base64 -D > <データベースファイル名>
  1. adb shell コマンドで、実機において shell を実行する。実行するコマンドは run-as <アプリのパッケージ名> toybox base64 databases/<データベースファイル名>
  2. run-as コマンドは、アプリのユーザーとしてコマンドを実行する。デバッグモードでのみ機能するらしい。
  3. toybox は様々なコマンドを集約したバイナリ。toybox に含まれる様々なコマンドの一つとして base64 コマンドがある。
  4. base64 コマンドはバイナリデータを文字データに変換する。
  5. run-as から | (パイプ) の前までが実機側で実行され、| (パイプ) 以降は PC 側で実行される。| (パイプ) によりデータを受け渡す。
  6. base64 -D により、文字データを再びバイナリデータに変換する。
  7. > <データベースファイル名> により、ファイルに保存する。

データベースファイルを取得した後は PC で操作する。

$ sqlite3 <データベースファイル名>
sqlite> .table
...

toybox base64 を試す前に cat で試したが、データが一部変わってしまったので文字データに変換する方法を探したところ、 Xperia Z5 には toybox がインストールされていたので問題を解決できた。

ADB (Android Debug Bridge) のプロトコル 後編

概要

ADB の Java クライアントを試作してみようと取り組んでみたら深みにはまっていったお話。 とても長いので前後編に分けました。

雑談

nosix.hatenablog.com

幸せになれたと思ったのに

コマンドを実行できるようになった。 しかし、再度接続を試みる際に署名を送信しても認証に失敗する。 署名による認証に失敗すると公開鍵を再送信することになり、ユーザーに確認を求めるダイアログが表示される。 これは煩わしい。 ダイアログ表示はなくしたい。 署名で認証せねば。 ここから調べ物の旅が始まる。

署名を生成する処理を JavaAPI を使用して Kotlin でコードを書くと次のようになる。 token はサーバーから送信されたトークン。 NONE はダイジェストを使用しない指定。 (標準アルゴリズム名のドキュメント)

fun sign(token: ByteArray, key: PrivateKey): ByteArray {
    Signature.getInstance("NONEwithRSA").run {
        initSign(key)
        update(token)
        return sign()
    }
}

実行しても認証に成功しない。

署名の仕組み

ADB の署名はどのように行われているのか。

github.com

adb_auth_host.cpp に adb_auth_sign 関数がある。 下記を行うだけの簡単なお仕事。

RSA_sign(NID_sha1, token, token_size, sig, &len, key)

これは OpenSSL の RSA_sign 関数。 OpenSSL の RSA_sign 関数を調べてみる。 ソースコードを読むが知識なしには何を行っているのかわからない。 RSA_sign manpage も読んでみる。 PKCS #1 v2.0 というキーワードを得た。

RFC 2437 - PKCS #1: RSA Cryptography Specifications Version 2.0

知識不足の状態で英語を読むのは辛いと思っていたら、日本語で読み解いてくれている記事を発見。

qiita.com

記事を読み進めていくと、他記事へのリンク発見。 これこそが求めていた情報。 OpenSSL の RSA_sign 関数の中身はこれではなかろうか。

自堕落な技術者の日記 : 図説RSA署名の巻 - livedoor Blog(ブログ)

記事中の DigestInfo と RFC2437 の内容を較べてみる。 ダイジェストアルゴリズムは様々あるが RSA_sign 関数に NID_sha1 を渡していることから SHA-1 (Secure Hash Algorithm) と推測される。 RFC2437 中には他にも MD2, MD5 (Message Digest Algorithm) の記述がある。 RFC2437 から関係のありそうな部分のみを抜粋する。

DigestInfo ::= SEQUENCE {
     digestAlgorithm  AlgorithmIdentifier,
     digest OCTET STRING }

sha1Identifier ::= AlgorithmIdentifier { id-sha1, NULL }

id-sha1 OBJECT IDENTIFIER ::=
     { iso(1) identified-organization(3) oiw(14) secsig(3)
       algorithms(2) 26 }

上記の表記法は標準化されており ASN.1 (Abstract Syntax Notation One) と呼ばれている。 ASN.1 はデータ構造(型)を定義しており、実際のデータをこの型にあてはめていく。 データを当てはめるにあたっての規則として BER (Basic Encoding Rules) がある。

ASN.1 バイナリ変換規則 (BER, CER, DER)

自堕落な技術者の日記 : 図説RSA署名の巻 で図示されている DigestInfo の BER によるデータ表現は下記のとおり。

30:21:30:09:06:05:2b:0e:03:02:1a:05:00:04:14

バイトを読み解く。

  1. 0x30 : SEQUENCE のタグ番号は 0x10、構造型なので構造化フラグが 1、よって 0x30
  2. 0x21 : 33 bytes (13 bytes + トークンの 20 bytes)
  3. 0x30 : SEQUENCE
  4. 0x09 : 9 bytes
  5. 0x06 : OBJECT IDENTIFIER
  6. 0x05 : RFC2437 では iso(1) だが?
  7. 0x2b : RFC2437 では identified-organization(3) だが?
  8. 0x0e : oiw(14)
  9. 0x03 : secsig(3)
  10. 0x02 : algorighms(2)
  11. 0x1a : 26
  12. 0x05 : NULL
  13. 0x00 : 終端
  14. 0x04 : OCTET STRING
  15. 0x14 : 20 bytes
  16. 以降、トークンのデータ (20 bytes)

なお、サーバーから受信したトークンは 20 bytes のデータで下記のような内容であった。 SHA-1 は 20 bytes の値を生成するため、トークンは SHA-1ハッシュ値であると確信を深める。

07:ee:71:40:54:fd:98:00:db:38:b5:c4:45:a8:c8:c5:7f:d6:ba:82

署名の内容を確認する

Signature.getInstance において NONEwithRSA を指定して署名を生成したとき、認証に失敗していた。 実際に生成されている署名を確認するために、実験用のコードを書く。 ECB については、理解してるつもりの SSL/TLS でも、もっと理解したら面白かった話 · けんごのお屋敷 の暗号モードが詳しい。

fun toByteArray(buffer: String): ByteArray =
        buffer.split(":").map { Integer.parseInt(it, 16).toByte() }.toByteArray()

fun ByteArray.toHexString(): String =
        joinToString(":") { String.format("%02x", it) }

fun testSign() {
    val keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair()
    val token = toByteArray("07:ee:71:40:54:fd:98:00:db:38:b5:c4:45:a8:c8:c5:7f:d6:ba:82")

    val signature = Signature.getInstance("NONEwithRSA").run {
        initSign(keyPair.private)
        update(token)
        sign()
    }
    val decryptedSignature = Cipher.getInstance("RSA/ECB/NoPadding").run {
        init(Cipher.DECRYPT_MODE, keyPair.public)
        doFinal(signature)
    }
    println("size: ${decryptedSignature.size}")
    println("data: ${decryptedSignature.toHexString()}")
}

実行結果。data は長いので 16 bytes ごとで改行している。

size: 256
data:
00:01:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:00:07:ee:71:40:
54:fd:98:00:db:38:b5:c4:45:a8:c8:c5:7f:d6:ba:82

NONEwithRSA では、DigestInfo がなく、トークンのみ。

次に、NONEwithRSA を SHA1withRSA に変更して再度実行。

size: 256
data:
00:01:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:00:30:21:30:
09:06:05:2b:0e:03:02:1a:05:00:04:14:98:d2:0c:51:
8a:59:30:f8:64:07:5a:55:fd:68:a2:5f:d4:40:8e:b7

SHA1withRSA では、DigestInfo があり、トークンは SHA-1ハッシュ値になっている。

Signature では期待する結果を得られないので、Cipher を使用して署名を生成する。

fun testSign() {
    val keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair()
    val token = toByteArray("07:ee:71:40:54:fd:98:00:db:38:b5:c4:45:a8:c8:c5:7f:d6:ba:82")

    val signature = Cipher.getInstance("RSA/ECB/PKCS1Padding").run {
        init(Cipher.ENCRYPT_MODE, keyPair.private);
        doFinal(token)
    }
    val decryptedSignature = Cipher.getInstance("RSA/ECB/NoPadding").run {
        init(Cipher.DECRYPT_MODE, keyPair.public)
        doFinal(signature)
    }
    println("size: ${decryptedSignature.size}")
    println("data: ${decryptedSignature.toHexString()}")
}

実行結果。

decr size: 256
decr data:
00:01:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:00:07:ee:71:40:
54:fd:98:00:db:38:b5:c4:45:a8:c8:c5:7f:d6:ba:82

RSA/ECB/PKCS1Padding では、DigestInfo がない。

DIGEST_INFO を用意して追加。

    val DIGEST_INFO = arrayOf(
            0x30,0x21,0x30,0x09,0x06,0x05,0x2b,0x0e,0x03,0x02,0x1a,0x05,0x00,
            0x04,0x14).map { it.toByte() }.toByteArray()

    val signature = Cipher.getInstance("RSA/ECB/PKCS1Padding").run {
        init(Cipher.ENCRYPT_MODE, keyPair.private);
        update(DIGEST_INFO) // 追加
        doFinal(token)
    }

実行結果。

decr size: 256
decr data:
00:01:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:00:30:21:30:
09:06:05:2b:0e:03:02:1a:05:00:04:14:07:ee:71:40:
54:fd:98:00:db:38:b5:c4:45:a8:c8:c5:7f:d6:ba:82

望む結果が得られた。 得られた署名をサーバーに送信したところ、認証を通過できた。

公開鍵の形式

署名の問題は解決できたが、生成した公開鍵を送付すると署名の認証で失敗する。 配布されいる ADB コマンドが生成した公開鍵を送付して、秘密鍵で署名を行うと認証に成功する。 (macOS の環境では ~/.android ディレクトリの adbkey (秘密鍵), adbkey.pub (公開鍵)。) 送付している公開鍵に問題があるようだ。

再び、ソースコードを読んでみる。

platform_system_core/adb_auth_host.cpp at master · android/platform_system_core · GitHub

android_pubkey_encode 関数を呼んでいる。

platform_system_core/android_pubkey.c at master · android/platform_system_core · GitHub

公開鍵は下記の形式にエンコードしたうえで BASE64 エンコードされている。 ANDROID_PUBKEY_MODULUS_SIZE は (2048 / 8)。 鍵サイズは 2048 bits のようだ。

typedef struct RSAPublicKey {
    // Modulus length. This must be ANDROID_PUBKEY_MODULUS_SIZE.
    uint32_t modulus_size_words;

    // Precomputed montgomery parameter: -1 / n[0] mod 2^32
    uint32_t n0inv;

    // RSA modulus as a little-endian array.
    uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE];

    // Montgomery parameter R^2 as a little-endian array of little-endian words.
    uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE];

    // RSA modulus: 3 or 65537
    uint32_t exponent;
}

android_pubkey_encode 関数を参考にして、Kotlin で書いてみる。

    fun adbEncode(key: PublicKey): ByteArray {
        if (key !is RSAPublicKey) throw IllegalArgumentException("PublicKey is not RSAPublicKey.")

        val modulesSize = 2048 / 8
        val moduleSizeWords = modulesSize / 4

        // 2^32
        val r32 = BigInteger.ZERO.setBit(32)

        // -1 / N[0] mod 2^32
        val n0inv = r32.subtract(
                key.modulus.remainder(r32).modInverse(r32)).toInt()

        val modules = key.modulus
                .toByteArray().reversedArray()

        // (2^(rsa_size)) ^ 2 mod N
        val rr = BigInteger.ZERO.setBit(modulesSize * 8)
                .modPow(BigInteger.valueOf(2), key.modulus)
                .toByteArray().reversedArray()

        val exponent = key.publicExponent.toInt()

        val encodedPubKey = ByteArray(4 * 3 + modulesSize * 2).apply {
            ByteBuffer.wrap(this).run {
                order(ByteOrder.LITTLE_ENDIAN)
                putInt(moduleSizeWords)
                putInt(n0inv)
                if (modules.size < modulesSize) {
                    put(modules)
                    put(ByteArray(modulesSize - modules.size)) // 桁が不足する場合は 0 で埋める
                } else {
                    put(modules, 0, modulesSize) // 符号の桁は除く
                }
                if (rr.size < modulesSize) {
                    put(rr)
                    put(ByteArray(modulesSize - rr.size)) // 桁が不足する場合は 0 で埋める
                } else {
                    put(rr, 0, modulesSize) // 符号の桁は除く
                }
                putInt(exponent)
            }
        }

        return encodedPubKey
    }

adbEncode 関数の戻り値を BASE64 エンコードしてサーバーに送信することで、署名による認証に成功するようになった。

以上で、ユーザーにダイアログを表示することなく、自スマホに ADB 接続できるようになった。

まとめ

  • ADB は USB だけでなく、TCP でも接続できる
  • アプリからコマンドを実行するよりも ADB でコマンドを実行する方が多くの権限を与えられる
  • TCP 接続を使えば、アプリから ADB の権限でコマンドを実行できる
  • 但し、USB 接続で adb tcpip コマンドを実行しておく必要がある
  • ADB Protocol Documentation に従えば、実装できる
    • 但し、CRC32 は使用しておらず、単純なチェックサム
    • データが文字列の場合には最後のバイトを 0 にする
    • 送信する署名は、パディング、DigestInfo、トークンをまとめて秘密鍵で暗号化 (RSA/ECB/PKCS1Padding を指定して、DigestInfo の ERB エンコードしたデータを加える)
    • 送信する公開鍵は、struct RSAPublicKey のバイト列を BASE64 エンコードしたデータ (Little Endian)

たくさんの有益な情報を提供してくださっている、各ブログとプロジェクトに感謝!

ADB (Android Debug Bridge) のプロトコル 前編

概要

ADB の Java クライアントを試作してみようと取り組んでみたら深みにはまっていったお話。 とても長いので前後編に分けました。

雑談

USB ケーブルがなくっても使える ADB

ADB (Android Debug Bridge) は Android アプリ開発者にとってはおなじみのツール。 普段、ADB を使用するときは USB で PC とスマホを接続する。 ADB は USB だけでなく TCP で接続することもできる。 TCP で接続すれば、PC からスマホだけでなく、スマホからスマホにネットワーク経由で接続することができる。 さらに言えば、スマホからスマホ自身に接続することができる。 そんなことをして何が嬉しいのかって?

ADB を使えばできることの幅が拡がる

eligor13.hatenablog.jp

こちらは、スマホからスマホ自身に接続している例。 パッケージの無効化をやっている。 プリセットアプリが邪魔だと思ったことはありませんか。

他にも色々とできます。 例えば、input コマンド。

先ほどの例では、スマホに adb コマンドがインストールされていることが前提となっている。 しかし、Xperia Z5 には adb コマンドはインストールされていない。 そんなときは Terminal Emulator。 Terminal Emulator を使えばコマンドを使える。

play.google.com

input コマンドを Terminal Emulator で使ってみる。

$ input tap 100 300

input コマンドはタップ操作をエミュレートすることができる。 上記の例では、座標 (100, 300) の位置がタップされる。 左上が (0, 0) なので、Terminal Emulator のコマンド入出力画面がタップされる。 うまく使えば自動操作くらいはできる。

ところが、どの座標でもタップされるわけではない。 ソフトウェアキーボードの領域を input コマンドでタップさせてみてもコマンドは機能せず、Terminal Emulator の領域のみ機能する。 コマンドを実行する際には権限が影響してくる。 Terminal Emulator では一般ユーザー権限としてコマンドを実行する。 一般ユーザー権限では使える機能が制限される。

そこで登場するのが ADB。

スマホには ADB がインストールされていないので、PC から接続してみる。 Android アプリ開発者にはおなじみの方法。 スマホChrome を立ち上げた状態で、下記のコマンドを PC で実行してみる。

$ adb shell input tap 100 100

Chrome の画面左上のホームボタンがタップされた。 ADB Shell を使った場合には、ソフトウェアキーボードの領域であろうがタップできる。 ADB Shell を使った場合の権限は一般ユーザー権限よりも多くの権限を与えられている。 root 権限であれば全てを操作できるが、全ての操作は行えていないので root 権限ではない。

ADB を使えば、アプリからは使えない機能だって使える。

Terminal Emulator の裏側

Terminal Emulator のソースコードを読んだわけではないが、想像するに次のようなことを行っていると思われる。

Runtime.getRuntime().exec("input 1000 100")

Runtime (ProcessBuilder の方がよいかも) を使えば、コマンドを実行できる。

しかし、上記の処理を実行して自アプリ以外の領域 (例えば、ソフトウェアキーボード) の領域をタップすると、 android.permission.INJECT_EVENTS 権限のエラーが表示される。 AndroidManifest.xml に INJECT_EVENTS 権限を加えてみても解決はしない。 権限についてはセキュリティのための仕掛けがあり、普通のアプリでは自アプリ以外の領域に input コマンドによるタップイベントは送れない。

blog.onpu-tamago.net

input コマンドを使って発生させるタップイベントは

  • 自アプリからは自アプリの領域のみに送れる
  • ADB からはどこの領域でも送れる

スマホだって ADB を使いたい

では、ADB を自アプリから接続したらどうだろう。 はじめに話したとおり、ADB は TCP 接続も可能になっている。 但し、一度は USB で接続する必要がある。 USB で接続して次のコマンドを実行する。

$ adb tcpip 5555

5555 はポート番号。スマホ側はこのポート番号で ADB の通信を待ち受けるようになる。 上記だけでスマホ側 (サーバー) の準備は完了。

次は、サーバーに接続するクライアント。 自作するのは面倒なので Java のライブラリを探すと JADB なるライブラリがみつかる。

github.com

いざ使ってみると動かない。おや?使い方が悪いのか? ソースを読むと送信しているデータが随分とシンプルに思える。

ADB のプロトコル文書を探すと、こちらも見つかる。ラッキー。

github.com

そんなに複雑ではなさそうなので、これなら自作できるか。

いざ作り始めると、はまる。 まず、CRC32 と書いてあるから CRC32 を理解するために調べてみた。 CRC32 はデータの破損を検出するための技術。

CRC32 を生成する処理を実装してみた。 配布されている ADB でコマンドを実行し、ADB とスマホの通信を WireShark でキャプチャーする。 キャプチャーしたパケットのデータと CRC32 を取得。 実装した処理でデータの CRC32 を生成してみるが、キャプチャーした CRC32 と一致しない。 何が違う?

ADB Test - Google グループ

文書に書かれている CRC32 は嘘だった。 ただのチェックサムだ。 データの各バイトを加算するだけという簡単な仕様。 いずれは CRC32 にするつもりなのか。 実装を CRC32 からチェックサムに置き換えたところ、キャプチャーしたパケットと一致。 無事に解決。

次のハマりポイントは認証。仕組みは次のとおり。

  1. サーバーはクライアントにトークンを送信
  2. クライアントは RSA秘密鍵トークンを使って署名を生成
  3. クライアントはサーバーに署名を送信
  4. サーバーは署名を検証
  5. 検証した結果、既知のクライアントであると識別できたら接続を確立

サーバーが署名を検証するためには、サーバーに公開鍵をあらかじめ渡す必要がある。 処理が加わる。

  1. サーバーは署名を検証できなかったときに、再びトークンを送信
  2. クライアントは RSA の公開鍵をサーバーに送信
  3. サーバーはユーザーに確認を求める
  4. ユーザーが許可すると接続を確立

送信する公開鍵の後ろには user@host を付与する。 4096 ビットで生成した鍵を送付したところ受け付けられた。 ハマりポイントは最後に 0 (文字ではなく、数値として) を付与する必要がある。 後から思えば不思議ではないが、トークンや署名はバイト列だから最後に 0 は不要、公開鍵は文字列なので 0 が必要という仕様。 C 言語で書くことを思えば納得。

公開鍵のデータは、~/.android/adbkey.pub (macOS 環境の場合) ファイルの内容を送ればよいということだ。

Socket のコンストラクタでは、スマホの IP とポート番号 (adb tcpip コマンドで指定した番号) を指定。 プロトコル文書に従って構築したパケットを送信することで、 クライアントとサーバーの接続が完了して、コマンドを実行する準備が整った。

後編に続く。

画面のスナップショットを撮影する

概要

Android で画面のスナップショットを撮影するアプリを作ります。 他のアプリが起動しているときにスナップショットを撮影するために、サービス経由でスナップショット撮影を行います。

確認環境

参考情報

解説

MediaProjection を使用することで画面のスナップショットを撮影できる。 他アプリを実行中やホーム画面など、様々な画面のスナップショットを撮影するためには Service で実行する必要がある。 MediaProjection を使用するためには startActivityForResult を呼び出す必要があるが、Service では呼び出せない。 そのため、MediaProjection を取得する Activity (サンプルコードでは CaptureActivity) を用意し、Activity を Service から起動する。 この Activity は表示されないようにし、MediaProjection 取得後に終了する。 取得した MediaProjection は、Service から参照できるように static フィールド (Kotlin では companion object) に保持する。 Service では、Activity が保持している MediaProjection を参照して利用する。

サンプルコードを動かすにあたって、スナップショット撮影を Service から起動するためのボタンが欲しい。 下記の内容に従ってボタンを作成する。

nosix.hatenablog.com

ボタンをタップしたら、サンプルコードの CaptureService::enableCapture メソッドを呼ぶようにする。

フローティングアプリを作るためのはじめの一歩

概要

Android で他アプリを実行中でも前面に表示されるアプリ(フローティングアプリ、Xperia のスモールアプリのようなもの)を作るため、 他アプリを実行中にビューを表示できることを確認します。 さらに、フローティングアプリの移動とタップが可能であること、他アプリの操作が行えることを確認します。

確認環境

参考情報

解説

Android の画面表示には層 (Layer) がある。 blog.nagopy.com: Androidユーザー向け 「何となくわかる表示レイヤー講座 ~オーバーレイ表示の仕組み~」 を参考に前面の層から順に並べると下記のようになる。 TYPE_* は後述する WIndowManager.LayoutParams (以後 LayoutParams と呼ぶ) のコンストラクタで指定する定数である。

  1. システムオーバーレイ (TYPE_SYSTEM_OVERLAY)
  2. ステータスバー
  3. キーガード(ロック画面)
  4. システムアラート (TYPE_SYSTEM_ALERT)
  5. トースト (TYPE_TOAST)
  6. 着信画面 (TYPE_PHONE)
  7. 通常のアプリ (TYPE_APPLICATION)

通常のアプリよりも前面の層にビューを表示すれば、他アプリを実行中でも表示されるようになる。 前面の層にビューを表示するためには WindowManager の addView メソッドを使う。 addView メソッドには、表示する View オブジェクトと LayoutParams オブジェクトを渡す。 LayoutParams で TYPE_* を指定することで表示する層を指定できる。 TYPE_* は上記以外にも存在する (WindowManager.LayoutParams | Android Developers) が、 全ての TYPE_* を指定できるわけではない。

上位の層にビューを配置すれば如何なる場合にも表示されるようになるが、 TYPE_SYSTEM_OVERLAY ではタッチイベントを捕捉できない。 TYPE_SYSTEM_ALERT であればタッチイベントを捕捉でき、トーストや着信よりも前面に表示される。 トーストの表示を邪魔したくなければ TYPE_PHONE にすればよい。 LayoutParams で MATCH_PARENT を指定するとタッチイベントが通常のアプリに渡らなくなるので WRAP_CONTENT にする。

フローティングアプリの表示位置は LayoutParams で管理する。 LayoutParams の x, y プロパティを設定した上で、WindowManager の updateViewLayout を呼ぶことで表示位置が変更される。

フローティングアプリは、他アプリが実行中に表示される必要があるのでアクティビティではなくサービスとして実装する。 サービスが他アプリの影響で出来る限り終了しないようにしたければ、startForeground と Service.START_STICKY を忘れずに。 サービスが自動的に再起動する際には、intent が null で onStartCommand が呼ばれるので注意が必要である。 詳しくは 落ちないサービスでアプリの起動を監視する - NOSIX 参照。

Android 6.0 からはセキュリティが厳しくなり、ユーザーが他のアプリの上に表示を許可しなければならない。 Settings.canDrawOverlays メソッドにより許可が与えられていることを確認し、許可されていなければ startActivityForResult で許可設定の画面を表示する。 従来どおり android.permission.SYSTEM_ALERT_WINDOW の権限も必要である。

Kotlin 解説

Kotlin に詳しくない方のために、少しだけ Kotlin の補足。

拡張関数と拡張プロパティ

Extensions.kt で使われている

fun クラス名.関数名()

が拡張関数の定義です。 fun Activity.hasOverlayPermission() とすることで Activity クラスに hasOverlayPermission メソッドがあるかのように Activity クラスを使えます。 MainActivity は Activity クラスを継承しており、MainActivity クラスは hasOverlayPermission メソッドを持っているかのように振る舞います。

FloatingButton.kt では

val クラス名.プロパティ名
var クラス名.プロパティ名

として拡張プロパティを定義しています。 拡張関数と同様に振る舞います。 var WindowManager.LayoutParams.position とすることで WindowManager.LayoutParams クラスが position プロパティを持つように振る舞います。

initial = params.position - e.position の params は WindowManager.LayoutParams のインスタンスです。 params.position として参照することで拡張プロパティで定義した position プロパティの get() が呼び出され、 Position(x.toFloat(), y.toFloat()) が実行されます。 x, y は WindowManager.LayoutParams のプロパティです。

params.position = it + e.position としてプロパティに代入した場合には、position プロパティの set(value) が呼び出されます。

プロパティは Java で言うところのフィールド、セッター、ゲッターをひとつにまとめたものです。 今回の拡張プロパティではセッターとゲッターのみを定義し、フィールドは定義していません。 フィールドを定義する場合には、set(value)field 変数を使います。 FloatingButton.kt の visible プロパティではフィールドを定義しています。

run 関数

下記のコードは同じことを行っています。

// Kotlin
button?.run {
    visible = false
}
// Java
if (button != null) {
    button.setVisible(false);
}

?. は null ではないときのみメソッドを実行します。

run 関数はスコープ関数と呼ばれる関数の一種で、拡張関数として定義されています。 Kotlin ではメソッドではなく関数 (function) と呼ぶようです。 Java のメソッドは Kotlin ではメンバー関数 (member function) と呼ぶことがありますが、 本記事ではクラスに属している関数 (member function) をメソッドと呼んでいます。

apply 関数

下記のコードは同じことを行っています。

// Kotlin
button = FloatingButton(windowManager, this).apply {
    visible = true
}
// Java
button = new FloatingButton(windowManager, this);
button.setVisible(true);

apply 関数も run 関数と同様にスコープ関数と呼ばれる関数の一種で、拡張関数として定義されています。

run 関数との異なる点は関数の戻り値です。 run 関数の戻り値はブロック内で最後に評価された値です。 先の例では、visible = false の結果が Unit 型 (Java で言うところの Void のような型) なので、run 関数は Unit オブジェクトを返します。 apply 関数の戻り値はレシーバーオブジェクトです。 例では、FloatingButton オブジェクトに対して apply 関数を呼び出しているため、レシーバーオブジェクトは FloatingButton オブジェクトです。 つまり、apply 関数の戻り値は FloatingButton オブジェクトになります。

落ちないサービスでアプリの起動を監視する

概要

Android で落ちないサービスを作ろうとした時の記録です。 使用履歴を参照してアプリの起動を監視するサービスを作っています。

確認環境

参考情報

公式の情報。

  • 他、落ちないサービスで検索

解説

下記のソースコードが最終的に作成されたコード。

MainActivity

startService によりサービスを起動するだけ。

StartupReceiver

端末を起動した時にサービスを自動的に起動するために作成。 AndroidManifest.xmlBOOT_COMPLETED アクションを受け取るように設定する必要がある。 RECEIVE_BOOT_COMPLETED 権限の設定も忘れずに。

AppMonitorService

処理の中心は、使用履歴を参照してアプリの起動を検出する処理。 isUsageStatsAllowed, allowUsageStats, getForegroundApps が関係する。 isUsageStatsAllowed メソッドでは使用履歴を参照できることを確認。 使用履歴を参照できない場合は allowUsageStats メソッドを呼んで使用履歴へのアクセス許可を取得。 使用履歴を参照できる場合は getForegroundApps メソッドを呼んで指定した期間の中でフォアグラウンドに移動したアプリのパッケージ名リストを取得。 getForegroundApps(beginTime, endTime).lastOrNull() とすることで、最後にフォアグラウンドに移動したアプリを特定。 指定した期間の中でフォアグラウンドへの移動がなければ null になる。 ?: (エルビス演算子) と let メソッドを使うことで null ではない時のみログに表示。

アプリの起動を検出する処理は繰り返し実行し続ける。 while ループにより処理を継続するためにスレッドを生成する必要がある。 (while ループを使わない別の方法もある。) IntentService を拡張すればスレッドを生成し、複数のサービス起動処理を1つのスレッドで処理させられる。 IntentService を拡張する場合は onHandleIntent メソッドをオーバーライドする。 onHandleIntent はスレッドセーフな実装になっていて、専用のワーカースレッドが生成され、そのワーカースレッドで実行される。 今回は諸事情により、サービス起動中にサービスの再起動を受け付ける。 そのため、onStartCommand メソッドをオーバーライドし、while ループを終了できるようにしている。 while ループはバッテリー消費を考慮して、sleep するようにしている。 interrupt メソッド呼び出しは今回のサンプルでは不要だが、先に述べた諸事情により呼ばれている。 諸事情とは、サービス起動時のパラメーターによりスリープ時間を長くできるようにすること。

繰り返し処理はアプリが終了しても実行し続ける必要があるためサービスとして実装。 サービスは他のアプリの影響によりメモリ不足となった場合に停止されることがある。 サービスを停止されないようにするためにサービスを フォアグラウンド で実行する。 フォラグラウンドで実行されるサービスはユーザーが存在を認知していることを前提とする。 そのため、通知によりサービスの存在を示す必要がある。 通知への表示を行う処理は startNotification メソッドが行う。 通知領域をタップした時に MainActivity が起動されるように setContentIntent によりインテントを設定。 notificationId は通知を識別するための ID。

フォアグラウンドで実行すればサービスは落ちないはずと思いきや、落ちることがある。(なぜ?) 落ちた場合に即座にサービスが復帰されるようにする。 onStartCommand の戻り値を START_STICKY にする。 サービスが復帰する時は intent が null で onStartCommand が実行されるので注意が必要。

Kotlin の場合は、型で null を許容するか、しないかを指定する。 onStartCommandonHandleIntent の引数の型は Intent?(null を許容する)とも Intent(null を許容しない)とも書ける。 Intent と書いていていると null が渡って来た時に例外が発生する。 この例外を補足できず、原因不明でサービスが落ちるという不具合に悩まされた。 (USBで繋いでいない場合に log を見る方法を見つけないと、、、。)

ここまで書いておいて何だが、AlarmManager を使う方法の方がシンプルで良いかもしれない。

Watson に喋らせる

概要

Watson の Text to Speech サービスをブラウザから使う方法です。

実行環境

手順

new-console.au-syd.bluemix.net

  1. IBM Bluemix のページで、登録を選択し、アカウントを作成する
  2. ログインして、組織とスペースを作る
  3. カタログから Text to Speech を検索し、作成を実行する
  4. ダッシュボード > サービス > Text to Speech > サービス資格情報を参照する
  5. API Reference Text to Speech - API | IBM Watson Developer Cloud を参考に実行する

組織とスペースを作るにあたって地域を選択する必要があります。 地域には、英国、米国、シドニーの 3 つがありますが、地域によって提供されているサービスが異なるようです。 提供されているサービスの一覧は IBM Bluemix Notifications で確認できます。

API Reference を参考に、実際に話させてみます。

上記HTMLでは、テキストを入力してボタンを押すと audio タグの src 属性を変更し play を実行するようになっています。 Basic 認証によりユーザー名とパスワードを求められるため、サービス資格情報の username と password を入力します。 src 属性に設定する URL により言語設定や発声させるテキストを指定しています。

var url = "https://stream.watsonplatform.net/text-to-speech/api/v1/synthesize?voice=ja-JP_EmiVoice&accept=audio/wav&text=" + text;