
この記事は「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と同じような感覚で使ってもらうように提供することができます。