Android端末内マイクロサービスという謎アーキテクチャの提案

# Android + Microservices
Androidアプリのバックエンドをマイクロサービスにする話ではありません。
**Android端末内で複数アプリを用いたマイクロサービスアプリ開発**です！！
つまり、~~変態~~近未来アーキテクチャの紹介です。(普通のアプリを書いている人には恐らく無縁の技術です。)
でも、一部有用な内容もあるので、とりあえず読んでいってください。


## 導入
Androidは、ほかのアプリのActivityやServiceにIntentを飛ばせます。
暗黙的IntentとActionを使う方法(特定の機能を持つアプリに遷移する)は有名だと思います。

しかし、この暗黙的Intentを使う方法には一つ大きな欠点があります。

**Serviceを叩けない**という最大の問題です。
起動しているアプリの裏で複数のアプリが連携するためにはActivityではなく、Serviceを動かす必要があります。

**どうしても複数のServiceが連携し、Microservice Android Applicationを実現したい・・・！！**

この野望に答える手法がありました。
実は明示的Intent(特定のクラスに遷移する)で、他のアプリのActivityやServiceを直接呼ぶことができます。

実際のところ、他のアプリのActivityやServiceを明示的Intentで呼び出すことなど、普通はありえません。
何故ならば、**ユーザが呼び出し先のアプリをインストールしてくれている保証が無い**からです。
しかし、この手法を使えば複数のアプリがデータをやり取りして一つの機能を実現するマイクロサービスAndroidアプリケーションが作れます！
有用なユースケースは思いつきませんが、面白いと思いませんか？

あと、**Microservice Android Application**がかっこいい・・・

([Dynamic Feature Module](https://developer.android.com/studio/projects/dynamic-delivery)に似ていますが、よりダイナミックになっています)

## 目次
- Android内マイクロサービスアーキテクチャの説明
  - 基本編: Serviceとデータやり取りする方法
  - 変わった人向け: 他のアプリのServiceやActivityを呼び出す方法
  - 変態向け: 端末内にインストールされているアプリのスキャン方法
- で結局何ができるの？

## Android内マイクロサービスアーキテクチャの説明
### 例えばこんな構成が作れます
<img width="784" alt="スクリーンショット 2019-12-14 14.47.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309112/87934bf2-76f6-44ff-d0ac-8614afa24ad5.png">

Host AppからPlugin Appに対してサーバへの通信を依頼し、最終的にその結果を受け取るアプリが作れます！
(別のアプリに分ける必要がある？というツッコミは野暮ってもんですよ。)

### 基本編: Serviceとデータをやり取りする方法（アプリ内、アプリ間共通)
AndroidのServiceは主に4種類あります。[サービスについて](https://developer.android.com/guide/components/services?hl=ja)
- background Service
- Intent Service
- foreground Service
- bind Service

(実はIntentServiceはbackgroundServiceの仲間だったりするのですが、分かりやすく4種類としています)

このうち、呼び出し元に値を返しやすいのは
- Intent Service
- bind Service
です。
IntentServiceは`resultReceiver`というクラスを使って呼び出し元に値を返すことができます。

以下、resultReceiverのサンプルコードです。

```kotlin
//ResultReceiverの引数は実行スレッドを指定するもの。nullなら任意のスレッド
this.startIntentService(Hoge::class.java, object: ResultReceiver(null) {
    override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
        when (resultCode) {
            // 予めIntent先と決めておいた定数
            RESULT_CODE -> {
                resultData?.getSerializable(Fuga)
                // 以下略
            }
            else -> {}
        }
    }
})
```

BindServiceは3種類の値の返し方がありますが、よく使われるのは`Messenger`クラスを使った方法です。
`Messenger`についてはこの後詳しく説明します。
3種類の返し方については[公式に詳しく書いてあります](https://developer.android.com/guide/components/bound-services?hl=JA#Messenger)。

ここまでは一般的なServiceの話です。ここからアプリ間通信の話に踏み込みます。

### 変わった人向け: 他のアプリのServiceやActivityを呼び出す方法
`bindService`は他のアプリから呼び出せますが、`IntentService`は呼び出せません。
なので、ここから先は`bindService`を使います。[bindServiceについてはこちら](https://developer.android.com/guide/components/bound-services?hl=ja)

基本的にはアプリ内のbindServiceを呼び出す方法と変わりません。
しかし、他アプリゆえに気をつけるポイントが3点あります。

#### serviceの可視性を`exported=true`にする
AndroidManifest.xmlのService項目に`exported=true`を設定しましょう。
この設定が無いとアプリ外からIntentで呼べません。

AndroidManifest.xml

```xml
<service
    android:name=".HogeService"
    android:enabled="true"
    android:exported="true">
</service>
```

#### `Messenger`クラスでデータのやり取りをする
Host AppもPlugin Appも互いのクラスを知りえません(別アプリなので当然)。
なので、`IBinder`を拡張する方法は使用できません。
`Messenger`に`Bundle`を詰めてやり取りします。
HTTP通信で例えるならば、`json`に値を詰めてやり取りするイメージです。

```kotlin
class SampleBindService : Service() {
    private val messageHandler = Handler { msg ->
        when (msg.what) {
            // msg.whatに0が来たらログに吐く
            0 -> {
                Log.d("SampleBindService", "message: ${msg.obj}")
            }
            else -> {}
        }
        val bundle = Bundle().also {
            it.putString("sample", "This is test.")
        }
        // what=0にbundleデータを詰めてreplyする
        msg.replyTo?.send(Message.obtain(null, 0, bundle))
        // replyは何度でも送ることができる
        msg.replyTo?.send(Message.obtain(null, 0, bundle))
        true
    }
    private val messenger: Messenger = Messenger(this.messageHandler)

    override fun onBind(intent: Intent): IBinder {
        return this.messenger.binder
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return super.onStartCommand(intent, flags, startId)
    }
}
```

#### Host AppはPlugin Appの`applicationId`とサービスの`className`(FQDN)を知っていること
Intentで呼び出すときに、呼び出し先の`applicationId`と`className`が必要になります。
つまり、なんらかの方法でHost AppはPlugin Appの上記2つの情報を知る必要があります。
そして、その方法は**変態向け: 端末内にインストールされているアプリのスキャン方法**で解説します。

```kotlin
val intent = Intent().also {
    it.setClassName(applicationId, className)
}
```


### 変態向け: 端末内にインストールされているアプリのスキャン方法
ここから先はほとんど使う人が居なさそうな機能の紹介です。

先ほど、Host AppはPlugin Appの`applicationId`と`className`を知っていなければならない。という話をしました。
1人でHost AppとPlugin Appの両方を作る場合は、これらの情報を知っているはずなので問題ありません。
一方で、Microserviceのように連携する場合やPlugin AppがHost Appの機能拡張アプリであった場合、Host AppはPlugin Appの存在を知りえません。(プラグインは本体を意識するが、本体がプラグインを意識したら変ですよね？)

では、いかにしてHost AppはPlugin Appの存在を知るのか。
その答えは・・・Host Appが端末内にインストールされているアプリをスキャンすれば良いのです。
(実はAndroidのアプリは端末内のアプリをスキャンすることができます。)

詳しくは[この記事](https://qiita.com/xu1718191411/items/25faefe055ebb315d041)をお読み下さい。
ただ、1点上記の記事と異なる点があります。上記の記事ではActivityを呼び出していましたが、今回はServiceを呼び出します。
LAUNCHERとして登録されているActivityはアプリスキャンで検索できるのですが、それ以外のActivityとServiceは検索できません。(少なくとも私は知らないので、知っていたら教えてください。)
ですので、Serviceの`className`はスキャンしても知ることはできません。
なので、苦肉の策として、Plugin Appが提供するServiceは
**`applicationId`と同じパッケージの直下に`PluginService`という名前で置く**
などの約束をして、Host AppがIntentを飛ばせるようにする必要があります。

スキャンをした後にIntentを飛ばすサンプルコードが以下です。

```kotlin
val pm = packageManager
val packageInfoList = pm.getInstalledPackages(
    PackageManager.GET_ACTIVITIES or PackageManager.GET_SERVICES
)
val targetPackageName = packageInfoList
    // 特定のパッケージで始まるアプリを検索対象とする
    .filter { it.packageName.startWith("jp.co.hoge.fuga") }
    .map { it.packageName }
}.first() // 今回は見つかる前提。見つからないとここで落ちる

val intent = Intent().also {
    // 上記で得られたapplicationIdとclassNameを詰める(classNameはパッケージ直下にPluginServiceで固定)
    it.setClassName(targetPackageName, targetPackageName + ".PluginService")
}

val connection = object: ServiceConnection {
    override fun onServiceConnected(name: ComponentName, service: IBinder) {
        val messenger = Messenger(service)
        messenger.send(Message.obtain(null, 0, null).also {
            it.replyTo = Messenger(ReplyHandler())
        })
    }

    override fun onServiceDisconnected(name: ComponentName) {
    }
}

    bindService(intent, connection)
}
```

## で、結局何ができるの？
例えば、私が思いつくのは以下のような物です。
しかし、複数アプリを連携させてまでやるものかと言われると。うーん

### 案1: ニュースまとめアプリ
各種ニュースサイトをまとめて見れるアプリ。
各Plugin Appはそれぞれのニュースサイトからの情報を取ってくる昨日を有し、
Host AppはPlugin Appから得たデータを元に、画面を作る。

ユーザはPlugin Appを入れれば入れるほど、様々なニュースサイトから情報を集められるようになる。

(でも、それってDynamic Feature Moduleでよくね？そもそもプラグイン構成取る必要ある？最初から様々なサイトから集めておけよ。)

### 案2: みんなで情報提供Serviceを公開し合う文化を作り、Microserviceを実現
この記事を読んでくれた人がAndroidを書いたときに、何かしらのデータを提供するbindServiceを作り、それを外部に公開したとする。
そして、その`ApplicationId`や`className`を公開したとする。
これらの情報が蓄積されれば、互いに互いのServiceを叩き合うMicroserviceの誕生！！かっこいい！！

(セキュリティ的に大丈夫なんかとか、依存したいアプリが入っているかわからない問題が課題。)

### 何か使える案下さい
面白そうな機能を見つけたので、変な構想を練って見ましたが、何に使えるのかイメージが湧きません・・・
一番のネックは、ユーザがアプリを入れてくれるとは限らない点ですね。
そこさえ克服できれば、アプリ間をService同士が連携し合う、Android内Microserviceアーキテクチャが陽の目を見る日も近い！