他のアプリに「バックグラウンドで動く処理をやるService」を提供する方法(Android O対応版)


# はじめに

他のアプリとActivityを使った暗黙的Intentの連携はよく聞くパターンでいくつか文献が揃っていると思いますが、他のアプリのServiceを使って「UIはこちらで、バックグラウンド処理は他のアプリで」というパターンを実現するのはなかなか見ないかと思います。

今回は「UIはこちらで、バックグラウンド処理は他のアプリで」というパターンの実装方法の一つ「`Messenger`による方法」を紹介します。そして、AndroidL, Oから発生したServiceを使った他apkとの連携に関する制約に対してどういうアプローチで回避していったかも紹介していきます。

何かのプラスになれば幸いです。

# 今回のサンプルアプリ

今回もサンプルアプリをベースに紹介していきます。

サンプルアプリのAndroidプロジェクトはこちらです(Kotlinで書いています)

https://github.com/LyricalMaestro/ServiceCooperationInAnotherApkSample

このアプリを動かしてみたいかたはREADME.mdを見てやってみてください。

# `Messenger` を使ってService, Activity  間でプロセス間通信をする。

## プロセス間通信をするための準備

Service, Activity間でプロセス間通信を行うには以下の二つの方法があります。

- Messengerを用いる方法。
- AIDLを用いる方法。

後者の方が型安全だったりしますが、初学者には敷居がちょっと高めなのと、インターフェイスサイドの修正が入ると、使用するクライアント側も修正しなければいけないのでちょっとめんどくさいです。
これらの問題を回避したMessengerを使った方法で以下やっていきます。

http://yuki312.blogspot.com/2013/02/androidmessenger.html
のサイトの通りActivityがServiceにメッセージを送るためのMessengerを作って、onBindの時点でMessengerに紐づくBinderをActivityに渡しています。

これで、ActivityからServiceへメッセージを送ることはできますが、Serviceから定期的にActivityへメッセージを送るのには対応していません。これを実現するためにActivity側でMessengerを新たに作り `ServiceConnection#onServiceConnected` メソッドの引数として渡ってくる「onBindで渡したBinder」をベースにしたMessengerにそれを送ります。コードにすると以下の通り

```kotlin:Activity側で生成したMessengerをServiceに渡す
    inner class ServiceConnectionImpl : ServiceConnection {
        override fun onServiceConnected(p0: ComponentName?, binder: IBinder?) {
            sendToServerMessenger = Messenger(binder)

            val message = Message.obtain(null, REQUEST_RESIST).also {
                it.replyTo = receiveFromServiceMessenger
            }
            sendToServerMessenger!!.send(message)
        }

        override fun onServiceDisconnected(p0: ComponentName?) {
            sendToServerMessenger = null
        }
    }
```

これを受けて、Server側はActivityから渡ってきたMessengerオブジェクトを保管します。

```kotlin
    /**
     * 外部apkのClientから送信されたメッセージを受信しそれに応じて処理を行うクラス
     */
    inner class ReceiveFromClientHandler : Handler() {

        override fun handleMessage(msg: Message?) {
            try {
                when (msg?.what) {
                    REQUEST_RESIST -> {
                        Log.e(TAG, "REQUEST_RESIST  received.")
                        if (msg!!.replyTo != null) {
                           // ここで渡ってきたMessengerを保存。
                            sendToClientMessenger = msg!!.replyTo                             sendToClientMessenger!!.send(Message.obtain(null, RESPONSE_RESIST)) 
                        }
                    }
                }
            } catch (ex: RemoteException) {
                Log.e(TAG, ex.localizedMessage, ex)
            }
            super.handleMessage(msg)
        }
    }

```

これで通信する準備が完了です。

## データのやり取りをするための実装を行なった完全版

プロセス間ではBundleオブジェクトを使ってデータのやり取りをします。
そこに文字列、整数など入れることができますが、Serializedオブジェクトだけは入れることができませんので注意。

これらを踏まえて実装を完了させたコードを以下の載せます。(Activity側をクライアント側,Service側をホスト側とよんでいます)

クライアント側(Serviceを使用する側)のコード

```kotlin:SampleClientActivity.kt
package com.lyricaloriginal.sampleclientapp

import android.app.Service
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.*
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.widget.TextView

class SampleClientActivity : AppCompatActivity() {

    companion object {
        const val TAG = "SampleClientActivity"

        const val ACTION_RUN_HOST_SERVICE = "com.lyricaloriginal.samplehostapp.RUN"

        const val REQUEST_RESIST = 10
        const val RESPONSE_RESIST = 20

        const val REQUEST_SEND_MSG = 30
    }

    private val serviceConnection = ServiceConnectionImpl()

    private lateinit var receiveFromServiceHandler: ReceiveFromServiceHandler
    private lateinit var receiveFromServiceMessenger: Messenger

    private var sendToServerMessenger: Messenger? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        receiveFromServiceHandler = ReceiveFromServiceHandler()
        receiveFromServiceMessenger = Messenger(receiveFromServiceHandler)
    }

    override fun onResume() {
        super.onResume()

        val intent = Intent().also {
            it.setAction(ACTION_RUN_HOST_SERVICE)
                    .addCategory(Intent.CATEGORY_DEFAULT)
        }

        startService(intent)
        bindService(intent, serviceConnection, Service.BIND_AUTO_CREATE)
    }

    override fun onPause() {
        super.onPause()

        val intent = Intent().also {
            it.setAction(ACTION_RUN_HOST_SERVICE)
                    .addCategory(Intent.CATEGORY_DEFAULT)
        }

        unbindService(serviceConnection)
        stopService(intent)
    }

    private fun appendMessage(msg: String) {
        findViewById<TextView>(R.id.msg_text_view).append(msg + "\n")
    }

    inner class ServiceConnectionImpl : ServiceConnection {
        override fun onServiceConnected(p0: ComponentName?, binder: IBinder?) {
            sendToServerMessenger = Messenger(binder)

            val message = Message.obtain(null, REQUEST_RESIST).also {
                it.replyTo = receiveFromServiceMessenger
            }
            sendToServerMessenger!!.send(message)
        }

        override fun onServiceDisconnected(p0: ComponentName?) {
            sendToServerMessenger = null
        }
    }

    inner class ReceiveFromServiceHandler : Handler() {

        override fun handleMessage(msg: Message?) {
            try {
                when (msg?.what) {
                    RESPONSE_RESIST -> {
                        appendMessage("Serviceとの連携準備完了")
                    }
                    REQUEST_SEND_MSG -> {
                        val bundle = msg?.obj as Bundle
                        appendMessage(bundle.getString("msg"))
                    }
                }
            } catch (ex: RemoteException) {
                Log.e(TAG, ex.localizedMessage, ex)
            }
            super.handleMessage(msg)
        }
    }
}
```

ホスト側(Serviceを提供する側)のコード

```kotlin:SampleService.kt
package com.lyricaloriginal.samplehostapp

import android.app.Service
import android.content.Intent
import android.os.*
import android.util.Log

/**
 * 外部apkとやり取りをするためのService.
 */
class SampleService : Service(), SampleModel.Listener {

    companion object {
        const val TAG = "HostService"

        const val REQUEST_RESIST = 10
        const val RESPONSE_RESIST = 20

        const val REQUEST_SEND_MSG = 30
    }

    private lateinit var receiveFromClientHandler: Handler
    private lateinit var receiveFromClientMessenger: Messenger

    private var sendToClientMessenger: Messenger? = null

    private lateinit var model: SampleModel

    override fun onCreate() {
        super.onCreate()

        receiveFromClientHandler = ReceiveFromClientHandler()
        receiveFromClientMessenger = Messenger(receiveFromClientHandler)

        model = SampleModel(this)
        model.start()
    }

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

    override fun onDestroy() {
        super.onDestroy()
        model.stop()
    }

    override fun onProgressNotified(msg: String) {
        val bundle = Bundle()
        bundle.putString("msg", msg)

        try {
            val message = Message.obtain(null, REQUEST_SEND_MSG, bundle)
            sendToClientMessenger?.send(message)
        } catch (ex: RemoteException) {
            Log.e(TAG, ex.localizedMessage, ex)
        }
    }

    /**
     * 外部apkのClientから送信されたメッセージを受信しそれに応じて処理を行うクラス
     */
    inner class ReceiveFromClientHandler : Handler() {

        override fun handleMessage(msg: Message?) {
            try {
                when (msg?.what) {
                    REQUEST_RESIST -> {
                        Log.e(TAG, "REQUEST_RESIST  received.")
                        if (msg!!.replyTo != null) {
                            sendToClientMessenger = msg!!.replyTo
                            sendToClientMessenger!!.send(Message.obtain(null, RESPONSE_RESIST))
                        }
                    }
                }
            } catch (ex: RemoteException) {
                Log.e(TAG, ex.localizedMessage, ex)
            }
            super.handleMessage(msg)
        }
    }
}
```

さぁ、これで実行できるか・・・というと手持ちのAndroid Oの端末では実行できずクラッシュしました。なぜだ・・・と思い問題点の洗い出しと解決に奔走しました。
その記録を以下に記載します。

# ここで起きた問題点と解決策

## 1. 暗黙的Intentを使ってホスト側のServiceを起動しようとするとエラーが起こる。

`java.lang.IllegalArgumentException: Service Intent must be explicit`というエラーが出てクラッシュします。「Serviceに関するIntentは明示的Intentでやれ」という意味です。

Android5.0以降だと暗黙的Intentを使ってServiceに関する起動・bindやらができなくなったことに起因します。

https://stackoverflow.com/questions/27842430/service-intent-must-be-explicit-intent

による回避策で対応します。Intentに起動対象のapkのパッケージ名を指定すれば問題なくなります。

```kotlin

  val intent = Intent().also {
      it.setAction(ACTION_RUN_HOST_SERVICE)
              .addCategory(Intent.CATEGORY_DEFAULT)
      // intentに起動対象のapkのパッケージ名を指定すればOK
      it.`package` = "com.lyricaloriginal.samplehostapp"
   }

```

ここからは推測ですが、暗黙的Intentの場合、起動するServiceやActivityの候補が複数出てくる場合があり、Activityだと特にどのアプリを起動するかを選ぶ画面出てきます。
Serviceの場合だと、こういう処理をさせたくなかったんでしょうかね。それでAndroid5以降でこのような制約を課したのかなと。

## 2. Android Oの端末だと`startService`ではうまくいかない！

Android Oの端末でクライアントアプリを実行させると `[Android O] IllegalStateException: Not allowed to start service Intent・・・app is in background uid null` というエラーが出てクラッシュしました。

これはAndroid Oからの制約でバックグラウンドサービスを知らない間に起動できないようになったことに起因します。(もともとバックグラウンドサービスって気づかぬうちにいろいろデータ抜かれて、通信量かかったり、電池食ったり、いろいろマイナスな部分が目立ってたんですよね。それに対する対策かしら)

こちらの回避策は
https://qiita.com/nukka123/items/791bc4f9043764789ee6
に書いてある「フォアグラウンド・サービス化で制限回避」という方法を使いました。

(1) クライアントサイドで呼び出している`startService`を`startForegroundService`に変える(ただし、Android O以降にだけこのメソッドがあるのでSDK_INTによる分岐で対応する)


```kotlin:クライアントサイド
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
     startForegroundService(intent)
}else{
     startService(intent)
}
```

(2) ホストサイドは `Service#onCreate`のところで `startForeground`を実行します。

```kotlin:ホストサイド(onCreate)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel();
        }

        val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("処理中")
                .setContentText("処理中")
                .setWhen(System.currentTimeMillis())
                .build()
        startForeground(NOTIFICATION_ID, notification)
```

Android OからはNotificationはNotificationChannelを作ってからその上にNotificationを登録しないといけません。`createNotificationChannel` はNotificationChannelを作成しています。

```kotlin:createNotificaitonChannel
    @TargetApi(26)
    private fun createNotificationChannel() {
        val name = "ホストアプリ通知用" // 通知チャンネル名
        val importance = NotificationManager.IMPORTANCE_HIGH // デフォルトの重要度
        val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance)

        channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
        channel.enableVibration(true)
        channel.enableLights(true)
        channel.setShowBadge(false) // ランチャー上でアイコンバッジを表示するかどうか

        // NotificationManagerCompatにcreateNotificationChannel()は無い。
        val nm = getSystemService(NotificationManager::class.java)
        nm!!.createNotificationChannel(channel)
    }
```

NotificationChannelの作り方、パラメータ設定の詳細は別な記事に譲りますが、とにかくNotificationChannelをAndroid Oでは作らないといけません。

# まとめ

上記トラブルを開始してサンプルのアプリを動かすことができました。

Android L, Oの事情をきちんと理解し適切に対処することで、5年くらい前まで平気でやれていた 「`Messenger`を使ったServiceを介したapkのプロセス間通信」を実現することができます。


# 参考サイト・参考文献

Android:Messengerの基本
http://yuki312.blogspot.com/2013/02/androidmessenger.html

Android Oからのバックグラウンド・サービスの制限事項を実演する。
https://qiita.com/nukka123/items/791bc4f9043764789ee6

Android 8.0 Oreo 通知対応チェックリスト
https://qiita.com/mstssk/items/14e1b94be6c52af3a0a6