この記事は「JapanTaxiアドベントカレンダー 」21日目の記事です。
他のアプリとActivityを使った暗黙的Intentの連携はよく聞くパターンでいくつか文献が揃っていると思いますが、他のアプリのServiceを使って「UIはこちらで、バックグラウンド処理は他のアプリで」というパターンを実現するのはなかなか見ないかと思います。
今回は「UIはこちらで、バックグラウンド処理は他のアプリで」というパターンの実装方法の一つ「Messengerによる方法」を紹介します。
今回はサンプルアプリをベースに紹介していきます。
サンプルアプリのAndroidプロジェクトはこちらです。
https://github.com/Shinichi-Tanimoto/ServiceCooperationInAnotherApkSample
このアプリを動かしてみたい方はREADME.mdを見てやってみてください。
Service, Activity間でプロセス間通信を行うには以下の二つの方法があります。
後者の方が型安全だったりしますが、初学者には敷居がちょっと高めなのと、インターフェイスサイドの修正が入ると、使用するクライアント側も修正しなければいけないのでちょっとめんどくさいです。これらの問題を回避した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と同じような感覚で使ってもらうように提供することができます。
Mobility Technologies では共に日本のモビリティを進化させていくエンジニアを募集しています。話を聞いてみたいという方は、是非 募集ページ からご相談ください!