Retain proximity registrations after partial SDK failures

This commit is contained in:
evgeny committed 2026-10-29 13:34:25 +01:00
1 parent aa9ca53468
commit c5d8307697
3 files changed
+115 -12

No files matched your search

@@ -17,12 +17,31 @@ import kotlinx.coroutines.flow.StateFlow
* near: true — расстояние меньше maximumRange, false — FAR; null — показания ещё нет или монитор остановлен.
* Без датчика start публикует false (экран не блокируется). Ошибка доступа/регистрации оставляет null.
* Runtime-разрешения не нужны; отказы ОС логируются, после отказа регистрации start можно повторить.
* Failed unregister сохраняет listener/manager и повторяется на main через 250 мс..30 с; новый start ждёт его ACK.
*/
class ProximityManager(context: Context) {
class ProximityManager(private val context: Context) {
companion object { private val retained = java.util.concurrent.ConcurrentHashMap.newKeySet<ProximityManager>() }
private val main = Handler(Looper.getMainLooper())
private val sensorManager = context.getSystemService(SensorManager::class.java)
private val sensor: Sensor? = sensorManager?.getDefaultSensor(Sensor.TYPE_PROXIMITY)
private var sensorManager: SensorManager? = null
private var sensor: Sensor? = null
private var lookupFailed = false
private val retired = mutableMapOf<SensorEventListener, SensorManager>()
private var scheduled = false
private var retryDelay = 250L
private val retry = Runnable { scheduled = false; cleanup() }
init { lookupSensor() }
private fun lookupSensor() {
try {
sensorManager = context.getSystemService(SensorManager::class.java)
sensor = sensorManager?.getDefaultSensor(Sensor.TYPE_PROXIMITY)
lookupFailed = sensorManager == null
} catch (e: Exception) {
lookupFailed = true; sensor = null
LogManager.addLog("ERROR", "Proximity", "Sensor lookup failed; proximity remains unknown: $e")
}
}
// null — первое показание ещё не получено; это не подтверждённое FAR.
private val _near = MutableStateFlow<Boolean?>(null)
@@ -39,6 +58,13 @@ class ProximityManager(context: Context) {
fun start() {
checkMain()
if (started) return
cleanup()
if (retired.isNotEmpty()) {
LogManager.addLog("ERROR", "Proximity", "Sensor start blocked by previous unregister; owner retained")
return
}
if (lookupFailed) lookupSensor()
if (lookupFailed && sensorManager != null) return
val manager = sensorManager
if (manager == null) {
LogManager.addLog("ERROR", "Proximity", "sensor service unavailable; proximity remains unknown")
@@ -52,9 +78,18 @@ class ProximityManager(context: Context) {
return
}
val l = object : SensorEventListener {
private var badReading = false
override fun onSensorChanged(event: SensorEvent) {
if (!started || listener !== this) return // Запоздалое событие предыдущей регистрации не относится к текущему ожиданию.
val nearNow = event.values[0] < event.sensor.maximumRange
val value = event.values.firstOrNull()
val range = event.sensor.maximumRange
if (value == null || !value.isFinite() || value < 0 || !range.isFinite() || range <= 0) {
_near.value = null
if (!badReading) LogManager.addLog("WARN", "Proximity", "Invalid reading value=$value maximumRange=$range; remains unknown")
badReading = true; return
}
badReading = false
val nearNow = value < range
if (_near.value != nearNow) {
_near.value = nearNow
LogManager.addLog("INFO", "Proximity",
@@ -68,12 +103,12 @@ class ProximityManager(context: Context) {
val registered = try {
manager.registerListener(l, s, SensorManager.SENSOR_DELAY_NORMAL, main)
} catch (e: Exception) {
listener = null; started = false
stop()
LogManager.addLog("ERROR", "Proximity", "sensor registration failed; proximity remains unknown: ${e.javaClass.simpleName}: ${e.message}")
return
}
if (!registered) {
listener = null; started = false
stop()
LogManager.addLog("WARN", "Proximity", "sensor registration failed; proximity remains unknown")
return
}
@@ -88,13 +123,28 @@ class ProximityManager(context: Context) {
listener = null
started = false
_near.value = null
if (!wasStarted) return
try {
current?.let { sensorManager?.unregisterListener(it) }
if (current != null) retired[current] = checkNotNull(sensorManager)
cleanup()
if (wasStarted) LogManager.addLog("INFO", "Proximity", "stopped cleanupPending=${retired.isNotEmpty()}")
}
private fun cleanup() {
for ((old, manager) in retired.toMap()) try {
manager.unregisterListener(old); retired.remove(old)
LogManager.addLog("INFO", "Proximity", "Sensor listener removal confirmed owner=${System.identityHashCode(this)}")
} catch (e: Exception) {
LogManager.addLog("WARN", "Proximity", "sensor listener removal failed: ${e.javaClass.simpleName}: ${e.message}")
LogManager.addLog("WARN", "Proximity", "sensor listener removal failed; owner retained: $e")
}
if (retired.isEmpty()) {
main.removeCallbacks(retry); scheduled = false; retained.remove(this); retryDelay = 250L
} else {
retained.add(this)
if (!scheduled) {
scheduled = main.postDelayed(retry, retryDelay)
if (!scheduled) LogManager.addLog("ERROR", "Proximity", "Sensor cleanup retry rejected; owner retained")
else retryDelay = (retryDelay * 2).coerceAtMost(30_000)
}
}
LogManager.addLog("INFO", "Proximity", "stopped")
}
private fun checkMain() {
@@ -17,13 +17,15 @@ class SensorManager {
var sensor: Sensor? = Sensor()
var acceptRegistration = true
var failRegistration = false
var failRegistrationAfter = false
var failRemoval = false
var failLookup = false
var registerAttempts = 0
var removeAttempts = 0
val registrations = mutableListOf<Registration>()
val active = mutableSetOf<SensorEventListener>()
fun getDefaultSensor(type: Int): Sensor? { check(type == Sensor.TYPE_PROXIMITY); return sensor }
fun getDefaultSensor(type: Int): Sensor? { check(type == Sensor.TYPE_PROXIMITY && !failLookup); return sensor }
fun registerListener(listener: SensorEventListener, sensor: Sensor, delay: Int,
handler: Handler = Handler(Looper.getMainLooper())): Boolean {
check(delay == SENSOR_DELAY_NORMAL)
@@ -32,6 +34,7 @@ class SensorManager {
check(!failRegistration) { "sensor registration unavailable" }
if (!acceptRegistration) return false
active.add(listener)
check(!failRegistrationAfter) { "sensor registration failed after effect" }
return true
}
fun unregisterListener(listener: SensorEventListener) {
@@ -3,6 +3,7 @@ package com.utun.chat.data
import android.content.Context
import android.hardware.SensorManager
import android.os.Looper
import android.os.Handler
import java.util.concurrent.atomic.AtomicReference
import org.junit.After
import org.junit.Assert.*
@@ -16,6 +17,7 @@ class ProximityManagerLifecycleTest {
private lateinit var proximity: ProximityManager
@Before fun prepare() {
Handler.rejectDelayed = false
assertSame(Looper.getMainLooper(), Looper.myLooper())
context = Context()
manager = requireNotNull(context.sensorManager)
@@ -25,6 +27,7 @@ class ProximityManagerLifecycleTest {
}
@After fun release() {
Handler.rejectDelayed = false
manager.failRemoval = false
proximity.stop()
Looper.getMainLooper().dispatchAll()
@@ -175,6 +178,53 @@ class ProximityManagerLifecycleTest {
assertNull(proximity.near.value)
}
@Test fun failedRemovalIsRetriedWithoutAnotherStartAndKeepsItsOriginalManager() {
proximity.start(); manager.failRemoval = true; proximity.stop()
assertEquals(1, manager.active.size)
context.sensorManager = SensorManager(); manager.failRemoval = false
Looper.getMainLooper().advanceBy(250)
assertTrue(manager.active.isEmpty()); assertEquals(2, manager.removeAttempts)
}
@Test fun unresolvedOldRegistrationBlocksNewStart() {
proximity.start(); manager.failRemoval = true; proximity.stop(); proximity.start()
assertEquals(1, manager.registerAttempts); assertNull(proximity.near.value)
manager.failRemoval = false; proximity.start()
assertEquals(2, manager.registerAttempts); assertEquals(1, manager.active.size)
}
@Test fun partialRegistrationFailureIsRetainedUntilUnregisterAck() {
manager.failRegistrationAfter = true; manager.failRemoval = true; proximity.start()
assertEquals(1, manager.active.size); assertNull(proximity.near.value)
manager.failRemoval = false; manager.failRegistrationAfter = false
Looper.getMainLooper().advanceBy(250)
assertTrue(manager.active.isEmpty()); proximity.start(); assertEquals(1, manager.active.size)
}
@Test fun rejectedRetryTimerStillAllowsExplicitStopToReleaseRegistration() {
proximity.start(); manager.failRemoval = true; Handler.rejectDelayed = true; proximity.stop()
assertTrue(LogManager.entries.any { it.level == "ERROR" && "retry rejected" in it.message })
manager.failRemoval = false; Handler.rejectDelayed = false; proximity.stop()
assertTrue(manager.active.isEmpty())
}
@Test fun sensorLookupExceptionKeepsUnknownAndCanRecoverAtStart() {
manager.failLookup = true; proximity = ProximityManager(context)
assertFalse(proximity.available); proximity.start(); assertNull(proximity.near.value)
manager.failLookup = false; proximity.start(); assertTrue(proximity.available)
manager.deliver(0, 5f); Looper.getMainLooper().dispatchAll(); assertEquals(false, proximity.near.value)
}
@Test fun malformedReadingsRemainUnknownAndAreLoggedOnceUntilValidData() {
proximity.start()
val registration = manager.registrations.single()
for (values in listOf(floatArrayOf(), floatArrayOf(Float.NaN), floatArrayOf(-1f)))
registration.listener.onSensorChanged(android.hardware.SensorEvent(registration.sensor, values))
assertNull(proximity.near.value)
assertEquals(1, LogManager.entries.count { it.level == "WARN" && "Invalid reading" in it.message })
manager.deliver(0, 0f); Looper.getMainLooper().dispatchAll(); assertEquals(true, proximity.near.value)
}
private fun onWorker(action: () -> Unit): Throwable? {
val result = AtomicReference<Throwable?>()
val worker = Thread({ try { action() } catch (error: Throwable) { result.set(error) } }, "proximity-test-worker")