Android: fix sensor callback race condition in MotionHandler (#43781)

* Android: fix sensor callback race in MotionHandler using tryResume

* call completeResume before unregisterListener to avoid coroutine leak

* replace internal tryResume/completeResume with AtomicBoolean guard

* use CancellableContinuation.resume(value, onCancellation) for cancellation safety

* use non-deprecated resume overload for kotlinx.coroutines 1.10+

* fix: simplify Android motion continuation resume

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
Kaneki
2026-03-22 11:11:02 +08:00
committed by GitHub
parent e7814f7ba0
commit d6346aaf63

View File

@@ -10,6 +10,7 @@ import android.os.SystemClock
import androidx.core.content.ContextCompat
import ai.openclaw.app.gateway.GatewaySession
import java.time.Instant
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.Json
@@ -18,7 +19,6 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlin.coroutines.resume
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.sqrt
@@ -142,19 +142,18 @@ private object SystemMotionDataSource : MotionDataSource {
val averageDelta: Double,
)
@OptIn(InternalCoroutinesApi::class)
private suspend fun readStepCounter(sensorManager: SensorManager, sensor: Sensor): Int? {
val sample =
withTimeoutOrNull(1200L) {
suspendCancellableCoroutine<Float?> { cont ->
var resumed = false
val listener =
object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent?) {
if (resumed) return
val value = event?.values?.firstOrNull()
resumed = true
val token = cont.tryResume(value) ?: return
cont.completeResume(token)
sensorManager.unregisterListener(this)
cont.resume(value)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit
@@ -162,8 +161,7 @@ private object SystemMotionDataSource : MotionDataSource {
val registered = sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
if (!registered) {
sensorManager.unregisterListener(listener)
resumed = true
cont.resume(null)
cont.resume(null) { _, _, _ -> }
return@suspendCancellableCoroutine
}
cont.invokeOnCancellation { sensorManager.unregisterListener(listener) }
@@ -172,6 +170,7 @@ private object SystemMotionDataSource : MotionDataSource {
return sample?.toInt()?.takeIf { it >= 0 }
}
@OptIn(InternalCoroutinesApi::class)
private suspend fun readAccelerometerSample(
sensorManager: SensorManager,
sensor: Sensor,
@@ -181,7 +180,6 @@ private object SystemMotionDataSource : MotionDataSource {
suspendCancellableCoroutine<AccelerometerSample?> { cont ->
var count = 0
var sumDelta = 0.0
var resumed = false
val listener =
object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent?) {
@@ -195,15 +193,14 @@ private object SystemMotionDataSource : MotionDataSource {
).toDouble()
sumDelta += abs(magnitude - SensorManager.GRAVITY_EARTH.toDouble())
count += 1
if (count >= ACCELEROMETER_SAMPLE_TARGET && !resumed) {
resumed = true
sensorManager.unregisterListener(this)
cont.resume(
AccelerometerSample(
samples = count,
averageDelta = if (count == 0) 0.0 else sumDelta / count,
),
if (count >= ACCELEROMETER_SAMPLE_TARGET) {
val result = AccelerometerSample(
samples = count,
averageDelta = sumDelta / count,
)
val token = cont.tryResume(result) ?: return
cont.completeResume(token)
sensorManager.unregisterListener(this)
}
}
@@ -211,8 +208,7 @@ private object SystemMotionDataSource : MotionDataSource {
}
val registered = sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
if (!registered) {
resumed = true
cont.resume(null)
cont.resume(null) { _, _, _ -> }
return@suspendCancellableCoroutine
}
cont.invokeOnCancellation { sensorManager.unregisterListener(listener) }