Gradleタスクでadbを実行してアプリを起動する
ちょいちょいターミナルからビルドしてアプリを立ち上げたりとかしたくなるときがあり、都度一発でできてなくて面倒なところがありました。
そうすればいいのかーって思ったのでメモと、ちょっと汎用的にしたものを共有。
こちら見かけたやつです
- app/build.gradle
task startDemo(type: Exec) { dependsOn 'installDebug' def adb = new File("${System.env.ANDROID_HOME}", "platform-tools${File.separator}adb") commandLine adb, "shell", "am", "start", "-n", "com.google.maps.android.utils.demo/.MainActivity" }
Execタスクで実行するコマンドは、PATHを通してないとGradleタスクとして実行できないです。
PATHを通すにはコマンドをファイルで渡してあげることで存在確認しつつ実行可能になります。
これを、Kotlinでかつ汎用的に書き直してみました。
環境は
- Android Gradle Plugin v4.0.0
- Gradle v6.1.1
です。
- app/build/gradle.kts
tasks { val sdkDir = project.android.sdkDirectory val adb = file("${sdkDir.path}${File.separator}platform-tools${File.separator}adb") project.android.buildTypes.forEach { buildType -> val typeName = buildType.name.capitalize() create("run$typeName", Exec::class) { dependsOn("install$typeName") setCommandLine("bash", "-e", "-c", """ $adb shell am start -n ${'$'}(adb shell pm dump ${project.android.defaultConfig.applicationId}${buildType.applicationIdSuffix} | grep -A 2 android.intent.action.MAIN | head -2 | tail -1 | awk '{print ${'$'}2}') """.trimIndent()) } } }
SDKのPATHは、環境変数やlocal.propertiesを使わなくてもAGPで取得できます。なので任せてしまうのがいいです。
project.android.sdkDirectory
実際の環境だと、debugとreleaseなど複数のBuildTypeがある場合がほとんどだと思います。なのでBuildTypeごとにタスクを作成しています。今回の例だと runXXX
という名前で作成してます。
create("run$typeName", Exec::class)
installXXXタスクに依存させることで、アプリのビルドとインストールはできている状態にしています。
dependsOn("install$typeName")
アプリを起動させる部分ですが、起動させるにはパッケージ名プレフィックス付きのコンポーネント名を指定します。
コンポーネント名を取得するのは、AndroidManifest.xmlに定義してあるのでそこからXmlSlurperで取り出す例を見かけますが、今回はアプリをビルドしてインストールも済んでいるので、adbコマンドを使って取り出します。
adb shell pm dump ${project.android.defaultConfig.applicationId}${buildType.applicationIdSuffix} | grep -A 2 android.intent.action.MAIN | head -2 | tail -1 | awk '{print ${'$'}2}'
debugを開発版としている場合、パッケージ名を被らせないようにしていると思うのでsuffixをつけて対応させます。
取得したコンポーネント名を、再度adbに渡してアプリを起動させています。
参考
GitHub - googlemaps/android-maps-utils: Maps SDK for Android Utility Library
AndroidのGradleでbuildTypes毎にtaskを作成する方法 - 混沌とした備忘録
Android Debug Bridge(adb) | Android デベロッパー | Android Developers
Kotlin/JSでLINE Botを動かす
Kotlin/JSを扱う題材としてCloud FunctionsでLINE Botを動かしてみたのですが、前回の記事がモリモリになってしまうので導入と実践を分けました。今回が実践編です。
前回の記事はこちら
環境は次のとおりです
題材
LINE Botを動かしてみることにしました。Slackもそうですが、inputとoutputが同じ場所で見えるチャット系サービスはCloud Functionsと相性が良いです。
Messaging API overview | LINE Developers
- ユーザーが、LINE公式アカウントにメッセージを送信します。
- LINEプラットフォームからボットサーバーのWebhook URLに、Webhookイベントが送信されます。
- Webhookイベントに応じて、ボットサーバーからユーザーにLINEプラットフォームを介して応答します。
今回は応答メッセージを使って、ユーザがLINE Botにメッセージを送ったときにオウム返しをするBotを作成します。
LINE Bot作成で必要なこと
LINE Botでは次のことが必要です
- リクエストからデータを受け取り
- 応答メッセージの作成
- LINEサーバにメッセージの送信
Cloud Functionsで動かす部分は同じです。
開発を便利にするためにいくつかの準備をします。
型の用意
せっかくKotlinで書くので型の恩恵を得られるようにします。
デプロイする関数の引数であるRequestとResponseの型を定義してあげます。今回利用する最低限度のものしか用意してないです。
- Wrapper.kt
external interface Request { val body: Any } external interface Response { fun status(code: Int): Response fun send(message: String): Response }
デプロイする関数を用意して、関数型として渡します。
- main.kt
import wrapper.Request import wrapper.Response external val exports: dynamic fun main() { exports.message = ::message } fun message(req: Request, res: Response) { TODO("実装を書く") }
これでエントリーポイントと実装が分けられてスッキリします。
kotlin-extentionsの導入
Kotlin/JSを扱うに当たり、ヘルパーが用意されてるので導入します。
@jetbrains/kotlin-extensions
という名前のnpmパッケージです。
- build.gradle.kts
dependencies { ... implementation(npm("@jetbrains/kotlin-extensions", "^1.0.1-pre.91")) ... }
npmパッケージが用意されてるので利用はできるのですが、デプロイを考えたときにちょっと問題があります。
生成されるJSファイルでは、require('kotlin-extensions')
となってしまうので利用時にエラーになります。
なので、JSファイルを生成する際に一部書き換えて対応します。
- build.gradle.kts
compileKotlinJs { doLast { // workaround for use kotlin-extensions val jsFile = File("build/js/packages/${project.name}/kotlin/${project.name}.js") val text = jsFile.readText() val rep = text.replace("require('kotlin-extensions')", "require('@jetbrains/kotlin-extensions')") jsFile.writeText(rep) } }
正直ちょっと手間ではあるので、利用したい部分のコードをローカルにコピーしてくる運用でも良いかもしれません。
環境変数の利用
トークンなどはソースコード管理に含めたくないので環境変数を経由して利用します。
js
関数でJSのコードを実行して利用します。
例えば
$ export CHANNEL_ACCESS_TOKEN=hogehoge
としていたら、Kotlinからは次のように利用できます。
js("process.env.CHANNEL_ACCESS_TOKEN") // hogehoge
型はdynamicです。
kotlinx.serializationの導入
通信の際にJSONデータを扱うので、手動でSerialize/deserializeしなくていいようにライブラリを利用します。
公式の通りにやれば追加できます。
注意として、バージョンは 0.14.0
と 0.20.0
しか対応していません。
package.jsonには kotlinx-serialization-kotlinx-serialization-runtime
というnpmパッケージで探しに行くので、次のnpmパッケージが一致して解決されます。
ちょっと気になるのは、このnpmパッケージが公式のものなのかどうかわからない点です。
npmパッケージの中身はbuild/package_imported
に生成されるJSファイルと同じなので気のせいかもしれませんが。
issueが閉じられてないのも気になります。
できればextentionと同じようにscopeを切って公式とわかるようにして、苦なく利用できるような形になっててほしいです。
通信ライブラリの導入
Axiosがdukatでいい感じに利用できたので、Axiosを採用します。
dukatについてはこちら
こんな感じでPOSTリクエストを送れます。HeaderにAuthorizationを追加してます。
- main.kt
private fun postReply(reply: Reply): Promise<JsonObject> { val axios = Axios.create(object : AxiosRequestConfig { override var method: String = "POST" override var responseType: String = "json" override var headers: Any? = kotlinext.js.js { this.Authorization = "Bearer ${js("process.env.CHANNEL_ACCESS_TOKEN")}" } as? Any }) return axios.post<Unit, JsonObject>(url = "https://api.line.me/v2/bot/message/reply", data = reply) }
kotlinext.js.js
は前述のkotlin-extentionsです。
JSON.stringifyでの循環参照回避
JSON.stringify(o: Any?)
を利用することで、JSのオブジェクトをJSON文字列に変換できます。
JVMの世界では起きなかったが、JSの世界では起きる問題があります。
stringifyしようとしたJSオブジェクトが、循環参照している場合例外が発生します。
それを解決するために第2引数に replacer: ((key: String, value: Any?) -> Any?)
という関数が用意されています。
こちらで循環参照してしまうものを除外して解決します。
次の例では、 request
というkeyが循環参照していたので除外しています。
- main.kt
fun message(req: Request, res: Response) { val webhookEvent = parseRequest(req) val reply = createReply(webhookEvent) postReply(reply) .then { console.log("then ${JSON.stringify(it) { key: String, value: Any? -> when (key) { "request" -> "" else -> value } }}") res.status(200).send("Success!!") }.catch { console.log("catch ${JSON.stringify(it)}") res.status(400).send("Error!!") } }
普段JSを書いていないので、慣れている人からしたら当たり前かもしれません。自分は困りました。
Cloud Functionsへのデプロイ
デプロイ時に環境変数を渡すのを忘れないようにします。
$ ./gradlew clean cKJ // compileKotlinJsの略 $ cd functions functions$ gcloud functions deploy message --region asia-northeast1 --trigger-http --runtime nodejs8 --update-env-vars CHANNEL_ACCESS_TOKEN="YOUR_ACCESS_TOKEN"
まとめ
JSならではの部分や、エコシステム周りで足りない部分も感じますが、準備さえしてしまえばわりと解決できる問題が多いです。
Kotlin/JSでHello World以上のサンプルがまだ少ないので、いくらか参考になると思います。
READMEにLINE Botの友だち追加のQRを置いてあるので、気になった方は試すことはできます。
参考
Messaging API overview | LINE Developers
Kotlin/JSでCloud Functionsを動かす
最近Kotlin/JSを触ってます。
実はCloud FunctionsがまだJavaをサポートしていないのでKotlinでCloud Functionsを動かそうと思ったらJSに変換する必要があります。
DartでもJSに変換してCloud Functionsを動かしたことがあったので、同じ用に動かせるのではないかと思いトライしました。
現在はEAPが稼働しているので、時期に対応されますが待ちきれない!という方には参考になると思います。
環境は次のとおりです
Cloud Functions
まずイメージを掴むために、JSのCloud Functionsの公式サンプル実装を見てみます。
const escapeHtml = require('escape-html'); /** * HTTP Cloud Function. * * @param {Object} req Cloud Function request context. * More info: https://expressjs.com/en/api.html#req * @param {Object} res Cloud Function response context. * More info: https://expressjs.com/en/api.html#res */ exports.helloHttp = (req, res) => { res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`); };
これはhelloHttp
という関数のサンプルです。helloHttp
に、request
とresponse
を引数に持つ関数を代入して、レスポンスを返しているコードになります。
同じようなコードをKotlinで書いてみます。
ライブラリの追加
Kotlin 1.3.70からdependenciesに直接依存ライブラリを定義できるようになりました。
- build.gradle.kts
dependencies { implementation(kotlin("stdlib-js")) implementation(npm("escape-html", "1.0.3")) implementation(npm("@types/escape-html", "0.0.20")) }
escape-html
はdukatが使えたので利用します。
- build.gradle.kts
kotlin { sourceSets["main"].kotlin.srcDir("src/main/external") target { nodejs {} useCommonJs() } }
$ dukat -d src/main/external/ build/js/node_modules/@types/escape-html/index.d.ts
helloHttp関数を作成
helloHttp
にラムダ式を代入しています。引数の型はJSから渡されるのでdynamicにしています。
req.query
などは存在しなくてもエラーになりません。変数として扱う限りundefinedなだけです。
- Main.kt
external val exports: dynamic fun main() { exports.helloHttp = { req: dynamic, res: dynamic -> res.send(createMessage(req)) } } private fun createMessage(req: dynamic): String { val message = when { req.query.name !== undefined -> escapeHTML(req.query.name) req.body.name !== undefined -> escapeHTML(req.body.name) else -> "World" } return "Hello $message" }
ローカルで実行
次のコマンドでNode.jsで実行できます。
$ ./gradlew nodeRun
ただし先程の例だと何も出力されません。helloHttp関数が呼ばれていないからです。Cloud Functionsをローカルで確認するには @google-cloud/functions-framework
を使うと確認できます。
現状、build.gradle.kts
のdependenciesからはdevDependencies
に追加できないので、npx
を使ってコマンドを実行することで回避します。
$ cd build/js/packages/${PROJECT_NAME} $ npx @google-cloud/functions-framework --target=helloHttp npx: installed 52 in 3.789s Serving function... Function: helloHttp URL: http://localhost:8080/
ローカルに立ち上がるので、アクセスするとhelloHttp関数が呼ばれます。
$ curl -X POST -H "Content-Type: application/json" "http://localhost:8080/" Hello World
nameクエリをつけてアクセスしてみます。
$ curl -X POST -H "Content-Type: application/json" "http://localhost:8080/?name=\"Kotlin/JS\"" Hello "Kotlin/JS"
しっかりescape-htmlも動いています。
Cloud Functionsデプロイのためのプロジェクト構成
Cloud Functionsのデプロイに必要なものはJSのコードと、ライブラリを利用していたらpackage.jsonが必要です。
2つのファイルをまとめるタスクを定義します。
タスク定義
- build.gradle.kts
tasks { val packaging by creating(Copy::class) { from("build/js/packages/${project.name}/kotlin/${project.name}.js", "build/js/packages/${project.name}/package.json") into("functions") rename { it.replace("${project.name}.js", "index.js") } doLast { val jsonFile = file("functions/package.json") val texts = jsonFile.readLines() .map { it.replace("kotlin/${project.name}.js", "index.js") } jsonFile.writeText(texts.joinToString("\n")) } } }
functions
フォルダを生成して配置してます。jsファイルは、Cloud Functionsの制約があるのでindex.js
となるようにリネームしています。合わせて、package.jsonのエントリーポイントをindex.js
となるように書き換えてます。置き換え後のpackage.jsonは次のようになってます。
デプロイ先で依存の解決がされるので、npm install
する必要はないです。
- functions/package.json
{ "main": "index.js", "devDependencies": { "source-map-support": "0.5.16" }, "dependencies": { "kotlin": "1.3.72", "escape-html": "1.0.3", "@types/escape-html": "0.0.20" }, "peerDependencies": {}, "optionalDependencies": {}, "bundledDependencies": [], "name": "kotlin-js-cloud-functions", "version": "1.0.0-SNAPSHOT" }
次のように実行することでfunctions
フォルダに必要なファイルを 設置できます。
$ ./gradlew clean compileKotlinJs $ ./gradlew packaging
JSファイルを生成するだけならnodeRun
ではなくてcompileKotlinJs
で生成できます。
生成してから置き換えタスクを実行します。
実行後は次のような配置になります。
$ tree -L 2 . ├── build │ ├── js │ ├── kotlin │ └── tmp ├── build.gradle.kts ├── functions │ ├── index.js │ └── package.json ├── gradle │ └── wrapper ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main └── test
デプロイ
gcloudでデプロイします。
取り掛かる前に、GCPのConfigurationが意図しているものかどうか確認しておいてください。
$ gcloud config list
functionsフォルダに移動してからコマンドを実行します。
$ cd functions $ gcloud functions deploy helloHttp --region=asia-northeast1 --trigger-http --runtime=nodejs8 --allow-unauthenticated
サクッと確認したいため、--allow-unauthenticated
フラグを付けてpublicにしています。
次のようなURLが排出されると思います。
https://{REGION_NAME}-{PROJECT_NAME}.cloudfunctions.net/{FUNCTION_NAME}
まとめ
コードとしてはシンプルに導入できます。
デバッグも組み込まれてはいないですが、npxを利用することで回避できます。 デプロイに関しても、必要な処理は今回用意したタスクでまかなえるのでそこまで手間にならないと思います。
リポジトリはこちらです。
参考
Google Cloud Blog - News, Features and Announcements
Functions Framework | Cloud Functions Documentation | Google Cloud
gcloud functions deploy | Cloud SDK Documentation | Google Cloud