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

ソフトウェアエンジニア 谷本
こんにちは。ソフトウェア開発部でエンジニアをしている谷本です。諸般の事情で読書感想文でなくAndroid関連のネタを書くことにしました。

この記事は「JapanTaxiアドベントカレンダー 」21日目の記事です。

はじめに

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

今回は「UIはこちらで、バックグラウンド処理は他のアプリで」というパターンの実装方法の一つ「Messengerによる方法」を紹介します。

サンプルアプリ

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

サンプルアプリのAndroidプロジェクトはこちらです。

https://github.com/Shinichi-Tanimoto/ServiceCooperationInAnotherApkSample

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

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

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にそれを送ります。コードにすると以下の通りです。


   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
        }
    }

Activity側で生成したMessengerをServiceに渡す

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


    /**
     * 外部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を使用する側)のコード


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 PACKAGE_NAME_HOST_APP = "com.lyricaloriginal.samplehostapp"

        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)
            it.`package` = PACKAGE_NAME_HOST_APP
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundService(intent)
        }else{
            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)
            it.`package` = PACKAGE_NAME_HOST_APP
        }

        unbindService(serviceConnection)
        stopService(intent)
    }

    private fun appendMessage(msg: String) {
        findViewById(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を提供する側)のコード


import android.app.Service
import android.content.Intent
import android.os.*
import android.util.Log
import android.support.v4.app.NotificationCompat
import android.app.NotificationManager
import android.app.NotificationChannel
import android.annotation.TargetApi
import android.app.Notification
import android.os.Build


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

    companion object {
        const val NOTIFICATION_CHANNEL_ID = "notification"
        const val NOTIFICATION_ID = 1

        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()
        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)

        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()
        stopForeground(true)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val nm = getSystemService(NotificationManager::class.java)
            nm!!.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID)
        }
    }

    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)
        }
    }

    @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)
    }

    /**
     * 外部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)
        }
    }
}

このようにすることでServiceを他アプリからAPIと同じような感覚で使ってもらうように提供することができます。

JapanTaxiでは、ITの力で「移動で人を幸せに。」を実現するための一連のサービス開発に取り組んでいます。全部署にてメンバーも積極的に募集しているので、興味のある方はWantedlyもぜひご覧ください!

JapanTaxiに興味を持ったら、まずはお話しませんか?