はじめに
他のアプリとActivityを使った暗黙的Intentの連携はよく聞くパターンでいくつか文献が揃っていると思いますが、他のアプリのServiceを使って「UIはこちらで、バックグラウンド処理は他のアプリで」というパターンを実現するのはなかなか見ないかと思います。
今回は「UIはこちらで、バックグラウンド処理は他のアプリで」というパターンの実装方法の一つ「Messenger
による方法」を紹介します。そして、AndroidL, Oから発生したServiceを使った他apkとの連携に関する制約に対してどういうアプローチで回避していったかも紹介していきます。
何かのプラスになれば幸いです。
今回のサンプルアプリ
今回もサンプルアプリをベースに紹介していきます。
サンプルアプリのAndroidプロジェクトはこちらです(Kotlinで書いています)
このアプリを動かしてみたいかたは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にそれを送ります。コードにすると以下の通り
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オブジェクトを保管します。
/**
* 外部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を使用する側)のコード
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を提供する側)のコード
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やらができなくなったことに起因します。
による回避策で対応します。Intentに起動対象のapkのパッケージ名を指定すれば問題なくなります。
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による分岐で対応する)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
}else{
startService(intent)
}
(2) ホストサイドは Service#onCreate
のところで startForeground
を実行します。
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を作成しています。
@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