はじめに
アプリのtargetSdkを31に上げた場合にサービスの起動条件がさらに厳しくなりました。
Serviceを起動する場合は特殊なケースに該当しない場合は、たとえstartForegroundService()を実行したとしてもバックグラウンドからServiceを起動すると例外が発生するようになりました。
やりたかったこと
- 他のアプリから呼び出されたあと、フォアグラウンドサービスのように処理を待機させること。
- Serviceの持つonStartCommand()のようにインスタンスが存在した状態で処理できるようにすること。
できなかったこと
- 一瞬だけActivityを呼び出してServiceを実行
Android12からActivityがないとServiceが起動できませんでした。
そのため、一瞬だけAvtivityを呼び出そうと思いましたが、バックグラウンドからActivityを起動するためには'SYSTEM_ALERT_WINDOW'のパーミッションが必要なため断念しました。 - CoroutineWorkerからServiceを起動
試してみましたが、通知が出ている状態でもやはりダメなようでした。
実際に行ったこと
1. ServiceではなくCoroutineWorkerを使用してServiceの処理を移動
しかし、これでは一瞬で処理が終わってしまうので、Workerを待機させるように
class SampleWorker(context: Context, val param: WorkerParameters) : CoroutineWorker(context, param) {
override suspend fun doWork(): Result {
showNotification()
//onCreateの処理を記述
mainWork()//ここで一回目のonStartCommandの処理
try {
//意図的に終了されるまで待機させておく
synchronized(lock) {
while (!isPreparerFinish) {
lock.wait()
}
}
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
//onDestroyでの処理を記述
return Result.success()
}
private fun mainWork() {
//onStartCommandで行っていた処理を記述
}
companion object {
private val lock = Object()
private var isPreparerFinish:Boolean = false
set(value) {
if (value) {
synchronized(lock) {
lock.notify()
}
}
field = value
}
}
}
2.スレッドが待機中の場合はメインの処理へデータを渡して実行する
ServiceのonStartCommandのような動きを再現するために参照を保持してインスタンスへ値を渡すようにします。
//・・・
override suspend fun doWork(): Result {
refWorker = WeakReference(this)
param.inputData.getString(KEY_COMMAND)?.let {
val data = param.inputData.getString(KEY_DATA)
mainWork(it, data)//データを渡して処理させる
}
//略...
return Result.success()
}
//...
companion object {
//略...
private var refWorker: WeakReference<SampleWorker>? = null
fun sendCommand(context: Context, command: String, data: String) {
refWorker?.get()?.apply {
// instanceが存在する場合は直接処理させる
mainWork(command, data)
} ?: startWorker(context, command, data)//instanceが存在しない場合はworkerを立ち上げる
}
}
注意点
- Workerをきちんとどこかで終了させるようにしましょう。ずっと残り続けてしまいます。
- 現状CoroutineWorkerはバックグラウンドから10分以上の長期的なタスクとして実行できますが、今後できなくなってしまう可能性があります。