From 737d30586bc930b84a8f0a5d92fc760336698395 Mon Sep 17 00:00:00 2001 From: neelts Date: Thu, 17 Sep 2026 17:01:41 +0200 Subject: [PATCH 1/3] Make request() cancellable request() used suspendCoroutine, which does not support cancellation. A caller that timed out stayed suspended until the answer arrived, or forever when the target app died after it received the request. request() now uses suspendCancellableCoroutine. The caller can cancel the wait, and a late answer is ignored. --- .../kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt b/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt index d004103..a3ecfe9 100644 --- a/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt +++ b/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt @@ -9,13 +9,13 @@ import io.rebble.pebblekit2.common.SendDataCallback import io.rebble.pebblekit2.common.UniversalRequestResponse import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine public suspend fun UniversalRequestResponse.request( bundle: Bundle, -): Bundle? = suspendCoroutine { cont -> +): Bundle? = suspendCancellableCoroutine { cont -> val callback = object : SendDataCallback.Stub() { override fun onResult(bundle: Bundle) { cont.resume(bundle) From d57e6e7c526f6e9f98655ea466b98ded19874a6d Mon Sep 17 00:00:00 2001 From: neelts Date: Thu, 17 Sep 2026 22:06:45 +0200 Subject: [PATCH 2/3] Resume with null when the target binder dies request() now links to the death of the target binder. When the target app dies after it received the request, the caller resumes with null instead of a permanent suspend. The death link is removed when the call completes or the caller cancels. --- .../pebblekit2/common/util/AidlCoroutines.kt | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt b/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt index a3ecfe9..3954489 100644 --- a/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt +++ b/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt @@ -3,29 +3,44 @@ package io.rebble.pebblekit2.common.util import android.content.Context import android.os.Bundle import android.os.DeadObjectException +import android.os.IBinder +import android.os.RemoteException import co.touchlab.kermit.Logger import io.rebble.pebblekit2.PebbleKitBundleKeys import io.rebble.pebblekit2.common.SendDataCallback import io.rebble.pebblekit2.common.UniversalRequestResponse +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.cancellation.CancellationException -import kotlin.coroutines.resume public suspend fun UniversalRequestResponse.request( bundle: Bundle, -): Bundle? = suspendCancellableCoroutine { cont -> +): Bundle? { + val binder = asBinder() + val result = CompletableDeferred() + val deathRecipient = IBinder.DeathRecipient { result.complete(null) } val callback = object : SendDataCallback.Stub() { override fun onResult(bundle: Bundle) { - cont.resume(bundle) + result.complete(bundle) } } + try { + binder.linkToDeath(deathRecipient, 0) + } catch (ignored: RemoteException) { + // Already dead + return null + } try { request(bundle, callback) } catch (ignored: DeadObjectException) { - cont.resume(null) + result.complete(null) + } + try { + return result.await() + } finally { + runCatching { binder.unlinkToDeath(deathRecipient, 0) } } } From 517f1df2f7f35e8f3845b4e6b2ea66e827371b15 Mon Sep 17 00:00:00 2001 From: neelts Date: Fri, 18 Sep 2026 17:51:33 +0200 Subject: [PATCH 3/3] Use try catch for the unlink runCatching allocates a Result object for no gain here. --- .../io/rebble/pebblekit2/common/util/AidlCoroutines.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt b/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt index 3954489..8808267 100644 --- a/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt +++ b/common/src/main/kotlin/io/rebble/pebblekit2/common/util/AidlCoroutines.kt @@ -40,7 +40,11 @@ public suspend fun UniversalRequestResponse.request( try { return result.await() } finally { - runCatching { binder.unlinkToDeath(deathRecipient, 0) } + try { + binder.unlinkToDeath(deathRecipient, 0) + } catch (ignored: NoSuchElementException) { + // The link is already removed when the binder died + } } }