Retain proximity registrations after partial SDK failures
This commit is contained in:
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) {
|
||||
|
||||
+50
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user