From b91328aba6c34d0590ee0fd66641e0161c95f056 Mon Sep 17 00:00:00 2001 From: Marcello Galhardo Date: Thu, 2 Feb 2023 15:32:04 +0000 Subject: Add `suspendRunCatching` to safely handle exceptions without a coroutine Handling any `CancellationException` by mistake inside a Coroutine may break structured concurrency, `suspendRunCatching` provides a easy way to handle exceptions without interfering in structured concurrency. For example: ```kotlin suspend fun example(context: Context) { val info = suspendRunCatching { context.getSystemService()!!.retrieveSystemUpdateInfo() } .getOrNull() ?: return // a null return is expected, system update info may not be available. // Do something with the info, like trigger an update. } ``` Test: atest SystemUITests:CoroutineResultTest Fixes: b/267486825 Change-Id: I5a98913d26ea2a9e447a16f9da494ae18c4f92f1 --- .../systemui/common/coroutine/CoroutineResult.kt | 63 ++++++++++++++++++++++ .../common/coroutine/CoroutineResultTest.kt | 52 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt b/packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt new file mode 100644 index 000000000000..b9736671b5ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/common/coroutine/CoroutineResult.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.common.coroutine + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.withTimeout + +/** + * Calls the specified function [block] and returns its encapsulated result if invocation was + * successful, catching any [Throwable] exception that was thrown from the block function execution + * and encapsulating it as a failure. + * + * Unlike [runCatching], [suspendRunCatching] does not break structured concurrency by rethrowing + * any [CancellationException]. + * + * **Heads-up:** [TimeoutCancellationException] extends [CancellationException] but catching it does + * not breaks structured concurrency and therefore, will not be rethrown. Therefore, you can use + * [suspendRunCatching] with [withTimeout], and handle any timeout gracefully. + * + * @see link + */ +suspend inline fun suspendRunCatching(crossinline block: suspend () -> T): Result = + try { + Result.success(block()) + } catch (e: Throwable) { + // Ensures the try-catch block will not break structured concurrency. + currentCoroutineContext().ensureActive() + Result.failure(e) + } + +/** + * Calls the specified function [block] and returns its encapsulated result if invocation was + * successful, catching any [Throwable] exception that was thrown from the block function execution + * and encapsulating it as a failure. + * + * Unlike [runCatching], [suspendRunCatching] does not break structured concurrency by rethrowing + * any [CancellationException]. + * + * **Heads-up:** [TimeoutCancellationException] extends [CancellationException] but catching it does + * not breaks structured concurrency and therefore, will not be rethrown. Therefore, you can use + * [suspendRunCatching] with [withTimeout], and handle any timeout gracefully. + * + * @see link + */ +suspend inline fun T.suspendRunCatching(crossinline block: suspend T.() -> R): Result = + // Overload with a `this` receiver, matches with `kotlin.runCatching` functions. + // Qualified name needs to be used to avoid a recursive call. + com.android.systemui.common.coroutine.suspendRunCatching { block(this) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt new file mode 100644 index 000000000000..d552c9d922ff --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/common/coroutine/CoroutineResultTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.common.coroutine + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +/** atest SystemUITests:CoroutineResultTest */ +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidTestingRunner::class) +class CoroutineResultTest : SysuiTestCase() { + + @Test + fun suspendRunCatching_shouldReturnSuccess() = runTest { + val actual = suspendRunCatching { "Placeholder" } + assertThat(actual.isSuccess).isTrue() + assertThat(actual.getOrNull()).isEqualTo("Placeholder") + } + + @Test + fun suspendRunCatching_whenExceptionThrow_shouldResumeWithException() = runTest { + val actual = suspendRunCatching { throw Exception() } + assertThat(actual.isFailure).isTrue() + assertThat(actual.exceptionOrNull()).isInstanceOf(Exception::class.java) + } + + @Test(expected = CancellationException::class) + fun suspendRunCatching_whenCancelled_shouldResumeWithException() = runTest { + suspendRunCatching { cancel() } + } +} -- cgit v1.2.3-59-g8ed1b