BroadcastReceiverでの非同期処理にはgoAsyncを使おう

Jetpack Glanceを調べていたら、非常に優れたコードを発見したので紹介します。
`goAsync`という拡張関数です。なぜこれがinternalなんでしょう？知見の詰まった素晴らしいコードです。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:glance/glance-appwidget/src/main/java/androidx/glance/appwidget/CoroutineBroadcastReceiver.kt

```CoroutineBroadcastReceiver.kt
internal fun BroadcastReceiver.goAsync(
    coroutineContext: CoroutineContext = Dispatchers.Default,
    block: suspend CoroutineScope.() -> Unit,
) {
    val coroutineScope = CoroutineScope(SupervisorJob() + coroutineContext)
    val pendingResult = goAsync()

    coroutineScope.launch {
        try {
            try {
                block()
            } catch (e: CancellationException) {
                throw e
            } catch (t: Throwable) {
                Log.e(GlanceAppWidgetTag, "BroadcastReceiver execution failed", t)
            } finally {
                // Nothing can be in the `finally` block after this, as this throws a
                // `CancellationException`
                coroutineScope.cancel()
            }
        } finally {
            // This must be the last call, as the process may be killed after calling this.
            try {
                pendingResult.finish()
            } catch (e: IllegalStateException) {
                // On some OEM devices, this may throw an error about "Broadcast already finished".
                // See b/257513022.
                Log.e(GlanceAppWidgetTag, "Error thrown when trying to finish broadcast", e)
            }
        }
    }
}
```

Jetpack Glanceでは以下のように使われています。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:glance/glance-appwidget/src/main/java/androidx/glance/appwidget/GlanceAppWidgetReceiver.kt;l=94-109

```GlanceAppWidgetReceiver.kt
    @CallSuper
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            Log.w(
                TAG,
                "Using Glance in devices with API<23 is untested and might behave unexpectedly."
            )
        }
        goAsync(coroutineContext) {
            updateManager(context)
            appWidgetIds.map { async { glanceAppWidget.update(context, it) } }.awaitAll()
        }
    }
```

# これは何

BroadcastReceiverでIntentを受け取った後、メインスレッド以外で処理を実行したり、suspend関数を実行したい場合がよくあります。このようなシチュエーションでは、以下のように`onReceive`で非同期処理を起動し、そのまま`onReceive`メソッドを終了させてしまっていませんか？私は以前、普通にそうしてしまっていました。

```kotlin
class SomeReceiver: BroadcastReceiver() {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
    override fun onReceive(context: Context, intent: Intent) {
        scope.launch { 
            suspendFoo()
        }
    }
}
```

BroadcastReceiverのライフサイクルは、原則onReceiveメソッドの実行中だけが保証されています。onRecieveで非同期処理を起動し、onReceiveが終了してしまうと、非同期処理が残っていても、OSは既に終了したものと見なしてしまう可能性があります。
ほとんどの場合、起動される非同期処理は1秒とかからず終了するでしょうから、このような実装でも問題となることはあまりないとは思いますが、良い実装とは言えません。

BroadcastReceiverで非同期処理を行う仕組みは、API 11（Android 3.0）から用意されています。

https://developer.android.com/reference/android/content/BroadcastReceiver#goAsync()

このメソッドを実行すると、PendingResultが返却され、`PendingResult.finish()`が呼び出されるまでの間、BroadcastReceiverの実行を保留させることができます。

---

以上を踏まえて、CoroutineBroadcastReceiver.ktの実装を見てみましょう。

```CoroutineBroadcastReceiver.kt
internal fun BroadcastReceiver.goAsync(
    coroutineContext: CoroutineContext = Dispatchers.Default,
    block: suspend CoroutineScope.() -> Unit,
) {
    val coroutineScope = CoroutineScope(SupervisorJob() + coroutineContext)
    val pendingResult = goAsync()

    coroutineScope.launch {
        try {
            try {
                block()
            } catch (e: CancellationException) {
                throw e
            } catch (t: Throwable) {
                Log.e(GlanceAppWidgetTag, "BroadcastReceiver execution failed", t)
            } finally {
                // Nothing can be in the `finally` block after this, as this throws a
                // `CancellationException`
                coroutineScope.cancel()
            }
        } finally {
            // This must be the last call, as the process may be killed after calling this.
            try {
                pendingResult.finish()
            } catch (e: IllegalStateException) {
                // On some OEM devices, this may throw an error about "Broadcast already finished".
                // See b/257513022.
                Log.e(GlanceAppWidgetTag, "Error thrown when trying to finish broadcast", e)
            }
        }
    }
}
```

CoroutineScopeはメソッド内で作成され、実行が完了するとcancelされています。
BroadcastReceiverのライフサイクルが1メソッドの実行中に限られるので、scopeもその範囲内に閉じています。

処理の実行はtryブロックが二重になっています。
内側のブロックでは、CancellationExceptionは外部に投げ、それ以外のThrowableは握りつぶしています。（ここが汎用メソッドになっていない理由かもしれません。）そして、finallyでcoroutineScopeをcancelしています。CancellationExceptionが投げられるので外側のfinallyにまとめられないとコメントされています。
外側のfinallyで`pendingResult.finish()`を実行します。このとき、一部デバイスでIllegalStateExceptionが投げられるため、それをcatchしています。これも貴重な知見ですね。

## 注意点

**2回目の`BroadcastReceiver.goAsync()`はnullが返るようになります。**
`BroadcastReceiver.goAsync()`を実行すると、`BroadcastReceiver`自体の結果を受けとる機能をPendingResultに委譲してしまいます。2回目はすでに委譲が行われているため、新たにPendingResultを返すことはできないわけですね。
非同期実行を行う場合は、処理を一つにまとめて、すべて完了してから`PendingResult.finish()`を呼び出す必要があります。複雑な処理を実行する場合は、Workerなど他の仕組みを使うべきですね。

ちょっと困ってしまうのが、Jetpack GlanceのonUpdateなどで、Glanceのsuspend関数をつかってロギングなど軽い処理を行いたい場合です。`goAsync()`は`GlanceAppWidgetReceiver`側で使われてしまっているので、PendingResultを受け取れません。別のBroadcastReceiverに処理を渡すか、Workerを起動するかが必要ですが、本当に小さな処理なのに、と悩ましいところです。