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に似ていますが、よりダイナミックになっています)

目次

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

Android内マイクロサービスアーキテクチャの説明

例えばこんな構成が作れます

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

基本編: Serviceとデータをやり取りする方法(アプリ内、アプリ間共通)

AndroidのServiceは主に4種類あります。サービスについて
- background Service
- Intent Service
- foreground Service
- bind Service

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

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

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

//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種類の返し方については公式に詳しく書いてあります

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

変わった人向け: 他のアプリのServiceやActivityを呼び出す方法

bindServiceは他のアプリから呼び出せますが、IntentServiceは呼び出せません。
なので、ここから先はbindServiceを使います。bindServiceについてはこちら

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

serviceの可視性をexported=trueにする

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

AndroidManifest.xml

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

Messengerクラスでデータのやり取りをする

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

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で呼び出すときに、呼び出し先のapplicationIdclassNameが必要になります。
つまり、なんらかの方法でHost AppはPlugin Appの上記2つの情報を知る必要があります。
そして、その方法は変態向け: 端末内にインストールされているアプリのスキャン方法で解説します。

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

変態向け: 端末内にインストールされているアプリのスキャン方法

ここから先はほとんど使う人が居なさそうな機能の紹介です。

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

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

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

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

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を作り、それを外部に公開したとする。
そして、そのApplicationIdclassNameを公開したとする。
これらの情報が蓄積されれば、互いに互いのServiceを叩き合うMicroserviceの誕生!!かっこいい!!

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

何か使える案下さい

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