コマンドラインからビルド・インストール・apk起動するためのGradleタスク


コマンドラインからのビルドは `assenbleDebug` とか `installDebug` で行うことができる。これらはAndroid Pluginの標準タスクである。ただ、インストールまではできるがapk起動はしないし、接続端末の選択などはできないなど実用とまではいかない。

コマンドラインからビルド・対象端末選択・インストール・アプリ起動ができるタスクを追加してみた。


## 準備

### install peco

端末を選択するために `peco` を使うのでインストール。[peco](https://github.com/peco/peco) は標準出力をインクリメンタルサーチでき、選択することができる便利なツールだ。

```pecoのインストール
$ brew install peco
```

※ pecoを使った非常に便利なadbツールがあります。 ([`adb-pect`](https://github.com/tomorrowkey/adb-peco))


## 実装

対象モジュールの `build.gradle` にタスクを追加する。 (android studioで作ったプロジェクトなら `app/build.gradle`)


### SDKパスを取得する

`adb` や `aapt` など使うのでAndroid SDKのパスを取得するメソッドを用意する

```groovy:app/build.gradle
def getSdkPath = {
    def sdkDir
    def localProperties = new File(rootDir, "local.properties")
    if (localProperties.exists()) {
        Properties properties = new Properties()
        localProperties.withInputStream { instr ->
            properties.load(instr)
        }
        sdkDir = properties.getProperty('sdk.dir')
    } else {
        sdkDir = System.env.ANDROID_HOME;
    }
    return sdkDir;
}
```

### 接続デバイス名を取得

接続デバイスを取得するメソッド。`peco` がなくてもデバイスは自動で選択されるようにする。

```groovy:app/build.gradle
def getConnectedDevice = {
    def sdkDir = getSdkPath();
    def adb = sdkDir + "/platform-tools/adb"
    Process peco = 'peco'.execute()
    Process devices = "$adb devices".execute()
    Process sed = ["sed", "-e", "1,1d"].execute()

    def device = null;
    // pecoがある場合は選択できるようにする
    if ("which peco".execute().text.isEmpty()) {
        def list = devices.pipeTo(sed).text;
        list.eachLine { line, no ->
            if (!line.isEmpty()) {
                device = line.split()[0]     // <device name> device
                return true;
            }
        }
    } else {
        def select = devices.pipeTo(sed).pipeTo(peco).text
        if (!select.isEmpty()) {
            device = select.split()[0]     // <device name> device
        }
    }
    return device;
}
```

### デバイスにapkをインストールするタスク

デバッグビルドの `assembleDebug` を依存タスクとしておけば、このタスクの実行前にビルドが行われる。 選択された `device` と 今回のビルドで生成されたapk場所 `apkPath` は次のタスクでも使えるように拡張プロパティに設定しておく。

```groovy:app/build.gradle
task installToDevice(dependsOn: 'assembleDebug') << {
    android.applicationVariants.all { variant ->
        if (!variant.install) { return }

        // set command
        def sdkDir = getSdkPath();
        def adb = sdkDir + "/platform-tools/adb"
        def apkPath;

        variant.outputs.each { output ->
            if (output.zipAlign.outputFile.path.endsWith('.apk')) {
                apkPath = output.zipAlign.outputFile.path;
                return true;
            }
        }
        if (apkPath == null || apkPath.isEmpty()) { return; }

        // set property
        ext.device = getConnectedDevice();
        ext.apkPath = apkPath;

        if (device != null && !device.isEmpty()) {
            println "$adb -s $device install -r $apkPath".execute().text
        }
    }
}
```

### インストールしたapkを起動するタスク

apkを起動するにはパッケージ名と起動アクティビティが必要で、そのためにビルドapkをダンプして各情報を取得する。なお、`LAUNCHER` を `<activity-alias>` に設定しているなどapkによっては `launchable-activity:` が取得できない場合がある。そういった場合を考慮して、下記コードの `activityName` に初期値を設定しておく。


```groovy:app/build.gradle
task startDebug(dependsOn: installToDevice) << {
    android.applicationVariants.all { variant ->
        if (!variant.install) { return }
        if (ext.device == null || ext.device.isEmpty()) { return; }
        if (ext.apkPath == null || ext.apkPath.isEmpty()) { return; }

        def device = ext.device;
        def apkPath = ext.apkPath;

        // set command
        def sdkDir = getSdkPath();
        def aapt = sdkDir + "/build-tools/$project.android.buildToolsRevision/aapt"
        def adb = sdkDir + "/platform-tools/adb"

        // get apk info
        def result = "$aapt dump badging $apkPath".execute().text
        // package name
        def packageLine = result.readLines().find { it.startsWith "package:" }
        def packageName
        packageLine.eachMatch(".*name='(.*?)'.*") { all, match ->
            packageName = match
        }
        // launcher activity
        def launchableActivityLine = result.readLines().find {
            it.startsWith "launchable-activity:"
        }
        def activityName = "com.hoge.example.MainActivity" // TODO: 取れなかった時のために初期化
        if (launchableActivityLine != null) {
            launchableActivityLine.eachMatch(".*name='(.*?)'.*") { all, match ->
                activityName = match
            }
        }
        def intentAction = "android.intent.action.MAIN"
        def intentCategory = "android.intent.category.LAUNCHER"
        def activity = packageName + "/" + activityName

        println "$adb -s $device shell am start -a $intentAction -c $intentCategory -n $activity".execute().text
    }
}
```

## 使ってみる

`./gradlew startDebug` で今回追加したタスクを実行。複数端末接続でも `peco` により選択することが可能。

```bash
$ ./gradlew startDebug
        :
    :main:assembleDebug
    :main:installToDevice
    [  0%] /data/local/tmp/main-debug.apk
    [100%] /data/local/tmp/main-debug.apk
        pkg: /data/local/tmp/main-debug.apk
    Success

    :main:startDebug
    Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] ....


    BUILD SUCCESSFUL
```

## おしまい

CIで自動スクリプトで全部やりたいとか、コマンドラインから操作したいといった時に使えると思います。個人的にはコマンドラインから色々出来たほうが好きなので、こういった対応は非常に好きです。
コードは[gist](https://gist.github.com/caad1229/b657d9b7c456f159edd1742a38bbf067)にあります。コピペでも多分動くはずです。

[Gradleを使って業務を楽にする](http://techblog.yahoo.co.jp/android/gradle-work-improvement/) の記事を参考させてもらいました。