From 5b531ddae721462771032917b4b0b60ab285db42 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 31 Oct 2026 18:29:09 +0100 Subject: [PATCH] Improve Android proximity settings and channel scanner layout --- .../java/com/utun/chat/data/CallController.kt | 19 ++++++-- .../java/com/utun/chat/data/ConfigProvider.kt | 24 +++++++++++ .../com/utun/chat/data/ProximityManager.kt | 21 ++++++--- .../utun/chat/ui/screens/JoinChannelDialog.kt | 2 +- .../utun/chat/ui/screens/QrScannerPreview.kt | 15 +++++-- .../utun/chat/ui/screens/SettingsScreen.kt | 34 ++++++++++++++- .../app/src/main/res/values-ru/strings.xml | 3 ++ .../app/src/main/res/values/strings.xml | 3 ++ .../call-service-tests/build.gradle.kts | 1 + .../com/utun/chat/data/CallServiceHost.kt | 2 +- .../utun/chat/data/RouterHostEnvironment.kt | 2 + .../data/ProximityManagerLifecycleTest.kt | 43 +++++++++++++++++++ 12 files changed, 151 insertions(+), 18 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt index 0ee738ed..266373ee 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallController.kt @@ -96,6 +96,8 @@ object CallController : TelecomCallManager.Listener { val earpiece: StateFlow = _earpiece private val proximity = ProximityManager(ChatApplication.instance) val proximityNear: StateFlow = proximity.near + private var proximityEnabled = false // До чтения сохранённой настройки датчик не запускаем. + private var proximityThresholdCm = 5f private var service: CallService? = null @@ -115,6 +117,14 @@ object CallController : TelecomCallManager.Listener { if (initialized) return initialized = true TelecomCallManager.setListener(this) + scope.launch { + val provider = ChatApplication.instance.configProvider + provider.changes.observe("call proximity settings", provider.proximitySettings).collect { (enabled, threshold) -> + proximityEnabled = enabled; proximityThresholdCm = threshold + LogManager.addLog("INFO", "Proximity", "Settings applied enabled=$enabled threshold=$threshold cm") + syncProximity(); applyVideoSpeakerIfReady() + } + } scope.launch { proximity.near.collect { applyVideoSpeakerIfReady() } } scope.launch { while (true) { @@ -467,12 +477,13 @@ object CallController : TelecomCallManager.Listener { } private fun syncProximity() { - if (_state.value?.phase == CallPhase.ACTIVE && _route.value == AudioRoute.EARPIECE - && (!_videoActive.value || videoSpeakerPending)) proximity.start() else proximity.stop() + if (proximityEnabled && _state.value?.phase == CallPhase.ACTIVE && _route.value == AudioRoute.EARPIECE + && (!_videoActive.value || videoSpeakerPending)) proximity.start(proximityThresholdCm) else proximity.stop() } private fun applyVideoSpeakerIfReady() { - if (!videoSpeakerPending || !_videoActive.value || proximity.near.value != false || _route.value != AudioRoute.EARPIECE) return + if (!videoSpeakerPending || !_videoActive.value || _route.value != AudioRoute.EARPIECE) return + if (proximityEnabled && proximity.near.value != false) return val st = _state.value ?: return val audio = service?.audio ?: return if (st.phase != CallPhase.ACTIVE) return @@ -481,7 +492,7 @@ object CallController : TelecomCallManager.Listener { audio.router.fallbackRoute = AudioRoute.SPEAKER audio.router.setRoute(AudioRoute.SPEAKER, automatic = true) syncProximity() - LogManager.addLog("INFO", "call", "video speaker requested after confirmed FAR id=${st.callId}") + LogManager.addLog("INFO", "call", "video speaker requested proximityEnabled=$proximityEnabled near=${proximity.near.value} id=${st.callId}") } /** Только живые события ядра, сериализованные AppEventHandler на main thread. */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index 980f0e32..d056303b 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt @@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore @@ -138,6 +139,29 @@ class ConfigProvider(private val context: Context) { } private val mutedChannelsKey = stringSetPreferencesKey("muted_channels") private val callAudioRouteKey = stringPreferencesKey("call_audio_route") + private val proximityEnabledKey = booleanPreferencesKey("proximity_enabled") + private val proximityThresholdKey = floatPreferencesKey("proximity_threshold_cm") + /** Использование датчика и порог в сантиметрах; по умолчанию включён, 5 см. */ + val proximitySettings: Flow> = context.dataStore.data.map { prefs -> + val threshold = prefs[proximityThresholdKey] ?: 5f + val valid = threshold.isFinite() && threshold in 0.1f..10f + if (!valid) LogManager.addLog("WARN", "Proximity", "Invalid saved threshold=$threshold; using 5 cm") + (prefs[proximityEnabledKey] ?: true) to if (valid) threshold else 5f + }.distinctUntilChanged() + + suspend fun setProximityEnabled(enabled: Boolean) { + context.dataStore.edit { it[proximityEnabledKey] = enabled } + LogManager.addLog("INFO", "Proximity", "Setting saved enabled=$enabled") + } + + suspend fun setProximityThreshold(thresholdCm: Float) { + if (!thresholdCm.isFinite() || thresholdCm !in 0.1f..10f) { + LogManager.addLog("ERROR", "Proximity", "Invalid threshold=$thresholdCm cm") + throw IllegalArgumentException("Proximity threshold must be within 0.1..10 cm") + } + context.dataStore.edit { it[proximityThresholdKey] = thresholdCm } + LogManager.addLog("INFO", "Proximity", "Setting saved threshold=$thresholdCm cm") + } private val mediaAudioProfileKey = stringPreferencesKey("media_audio_profile") private val radioAutoEnabledKey = intPreferencesKey("radio_auto_enabled") private val radioAutoModeKey = intPreferencesKey("radio_auto_mode") diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ProximityManager.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ProximityManager.kt index cb14c2e3..5a9352e4 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ProximityManager.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ProximityManager.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.StateFlow * Показания приближения для блокировки UI аудиозвонка и выбора громкой связи видеозвонка. * start/stop вызываются на main; callbacks доставляются туда же. Повторные вызовы безопасны, * события остановленной регистрации игнорируются, новый start ждёт нового показания. - * near: true — расстояние меньше maximumRange, false — FAR; null — показания ещё нет или монитор остановлен. + * near: true — расстояние меньше порога и maximumRange, false — FAR; null — показания ещё нет или монитор остановлен. * Без датчика start публикует false (экран не блокируется). Ошибка доступа/регистрации оставляет null. * Runtime-разрешения не нужны; отказы ОС логируются, после отказа регистрации start можно повторить. * Failed unregister сохраняет listener/manager и повторяется на main через 250 мс..30 с; новый start ждёт его ACK. @@ -49,15 +49,22 @@ class ProximityManager(private val context: Context) { private var listener: SensorEventListener? = null private var started = false + private var thresholdCm = 5f /** Датчик обнаружен при создании объекта; успешная регистрация start этим не гарантируется. */ val available: Boolean get() = sensor != null - /** Начать наблюдение на main. При отказе сохранить UNKNOWN и разрешить следующую попытку start. */ - fun start() { + /** Начать наблюдение на main с порогом в см; смена порога ждёт нового показания. При отказе остаётся UNKNOWN. */ + fun start(thresholdCm: Float = 5f) { checkMain() - if (started) return + if (!thresholdCm.isFinite() || thresholdCm <= 0) { + LogManager.addLog("ERROR", "Proximity", "Invalid start threshold=$thresholdCm cm") + return + } + if (started && this.thresholdCm == thresholdCm) return + if (started) stop() + this.thresholdCm = thresholdCm cleanup() if (retired.isNotEmpty()) { LogManager.addLog("ERROR", "Proximity", "Sensor start blocked by previous unregister; owner retained") @@ -89,11 +96,11 @@ class ProximityManager(private val context: Context) { badReading = true; return } badReading = false - val nearNow = value < range + val nearNow = value < range && value < thresholdCm if (_near.value != nearNow) { _near.value = nearNow LogManager.addLog("INFO", "Proximity", - "${if (nearNow) "NEAR" else "FAR"} v=${event.values[0]} max=${event.sensor.maximumRange}") + "${if (nearNow) "NEAR" else "FAR"} v=$value max=$range threshold=$thresholdCm") } } @@ -112,7 +119,7 @@ class ProximityManager(private val context: Context) { LogManager.addLog("WARN", "Proximity", "sensor registration failed; proximity remains unknown") return } - LogManager.addLog("INFO", "Proximity", "started (maxRange=${s.maximumRange})") + LogManager.addLog("INFO", "Proximity", "started maxRange=${s.maximumRange} threshold=$thresholdCm cm") } /** Сначала запретить события регистрации и сбросить показание, затем снять слушатель; только main. */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/JoinChannelDialog.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/JoinChannelDialog.kt index 6f1e07df..415b1421 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/JoinChannelDialog.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/JoinChannelDialog.kt @@ -81,7 +81,7 @@ fun JoinChannelDialog(onConnect: (InviteData) -> Unit, onDismiss: () -> Unit) { ) { if (!connecting) { QrScannerPreview( - modifier = Modifier.fillMaxWidth().height(180.dp), + modifier = Modifier.fillMaxWidth().aspectRatio(3f / 4f), onResult = { link = it } ) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/QrScannerPreview.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/QrScannerPreview.kt index 87447c68..b5bc52f3 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/QrScannerPreview.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/QrScannerPreview.kt @@ -20,8 +20,10 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp @@ -78,7 +80,8 @@ fun QrScannerPreview(modifier: Modifier = Modifier, onResult: (String) -> Unit) } } - Box(modifier, contentAlignment = Alignment.Center) { + // CameraX масштабирует TextureView; ограничиваем видео областью сканера, чтобы оно не заходило под поле ссылки. + Box(modifier.clipToBounds(), contentAlignment = Alignment.Center) { if (!hasPermission) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(stringResource(R.string.camera_permission_required)) @@ -98,7 +101,10 @@ fun QrScannerPreview(modifier: Modifier = Modifier, onResult: (String) -> Unit) }) { Text(stringResource(if (permanentDenial) R.string.settings else R.string.grant)) } } } else { - val previewView = remember { PreviewView(context).apply { implementationMode = PreviewView.ImplementationMode.COMPATIBLE } } + val previewView = remember { PreviewView(context).apply { + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + scaleType = PreviewView.ScaleType.FIT_CENTER // Вписываем весь кадр с сохранением пропорций. + } } DisposableEffect(lifecycleOwner, previewView) { cameraError = null val session = QrScannerSession(context, lifecycleOwner, previewView, onScan = { values -> @@ -112,7 +118,10 @@ fun QrScannerPreview(modifier: Modifier = Modifier, onResult: (String) -> Unit) session.start() onDispose { session.close() } } - AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize()) + AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize().onSizeChanged { + LogManager.addLog("DEBUG", "QR", "Preview viewport=${it.width}x${it.height} scale=${previewView.scaleType} " + + "clipped=true mode=${previewView.implementationMode}") + }) Canvas(Modifier.fillMaxSize()) { val side = size.minDimension * 0.75f val left = (size.width - side) / 2 diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt index 0453bc9d..99b49ae1 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt @@ -1236,7 +1236,7 @@ private fun RadioSettingsTab() { /* ── Audio Settings Tab ── */ private data class AudioSettingsSnapshot(val voice: VoiceSettings, val bitrate: CallVideoBitrateRange, - val jitterMs: Int, val sound: Boolean) + val jitterMs: Int, val sound: Boolean, val proximity: Pair) @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -1249,7 +1249,8 @@ private fun AudioSettingsTab(vm: ChatViewModel) { loadFailed = false try { snapshot = withContext(Dispatchers.IO) { AudioSettingsSnapshot(provider.getVoiceSettings(), provider.getCallVideoBitrateRange(), - provider.getInt("chat.call_jitter_max_reserve_ms").coerceIn(500, 2000), provider.getSoundEnabled()) } + provider.getInt("chat.call_jitter_max_reserve_ms").coerceIn(500, 2000), provider.getSoundEnabled(), + provider.proximitySettings.first()) } } catch (e: CancellationException) { throw e } catch (e: Exception) { loadFailed = true; provider.changes.reportFailure("load audio settings", e) } } @@ -1275,6 +1276,10 @@ private fun AudioSettingsTab(vm: ChatViewModel) { /* Notification sound */ var soundEnabled by remember { mutableStateOf(settingsSnapshot.sound) } + val proximitySettings by remember { provider.changes.observe("proximity settings", provider.proximitySettings) } + .collectAsState(initial = settingsSnapshot.proximity) + var proximityThreshold by remember { mutableFloatStateOf(proximitySettings.second) } + LaunchedEffect(proximitySettings.second) { proximityThreshold = proximitySettings.second } LaunchedEffect(micTesting) { try { while (micTesting && vm.audioRecorder.isTesting) { micLevel = NativeLib.voiceGetPeakLevel(); delay(25) } } @@ -1338,6 +1343,31 @@ private fun AudioSettingsTab(vm: ChatViewModel) { HorizontalDivider(Modifier.padding(vertical = 4.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text(stringResource(R.string.use_proximity_sensor), modifier = Modifier.weight(1f)) + Switch(checked = proximitySettings.first, onCheckedChange = { enabled -> + provider.changes.submit("proximity enabled") { provider.setProximityEnabled(enabled) } + }) + } + Text(stringResource(R.string.proximity_threshold, proximityThreshold)) + Slider( + value = proximityThreshold, + onValueChange = { proximityThreshold = (it * 10).roundToInt() / 10f }, + valueRange = 0.1f..10f, + steps = 98, + enabled = proximitySettings.first, + onValueChangeFinished = { + val threshold = proximityThreshold + provider.changes.submit("proximity threshold") { + try { provider.setProximityThreshold(threshold) } + finally { proximityThreshold = proximitySettings.second } + } + } + ) + Text(stringResource(R.string.proximity_sensor_hint), style = MaterialTheme.typography.bodySmall) + + HorizontalDivider(Modifier.padding(vertical = 4.dp)) + /* ── Входящие звонки поверх блокировки ── */ Text(stringResource(R.string.incoming_calls), style = MaterialTheme.typography.titleMedium) val phoneOptions by remember { provider.changes.observe("incoming call preferences", provider.incomingCallOptions) } diff --git a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml index 9f8b666d..03bf8071 100644 --- a/tools/chatgui-android/app/src/main/res/values-ru/strings.xml +++ b/tools/chatgui-android/app/src/main/res/values-ru/strings.xml @@ -355,6 +355,9 @@ "Высокое (64 кбит/с)" "Звук уведомлений" "Звук при получении сообщения" + Использовать датчик приближения + Порог приближения: %1$.1f см + Блокирует экран звонка у уха. Порог действует только для датчиков, выдающих расстояние; для датчиков «близко/далеко» расстояние срабатывания задаёт устройство. "Показывать поверх экрана блокировки" "Всегда разрешено" "Разрешено" diff --git a/tools/chatgui-android/app/src/main/res/values/strings.xml b/tools/chatgui-android/app/src/main/res/values/strings.xml index 24a7b60b..586d1e71 100644 --- a/tools/chatgui-android/app/src/main/res/values/strings.xml +++ b/tools/chatgui-android/app/src/main/res/values/strings.xml @@ -356,6 +356,9 @@ "High (64 kbps)" "Notification sound" "Play sound on incoming message" + Use proximity sensor + Proximity threshold: %1$.1f cm + Locks the call screen near your ear. The threshold only affects sensors that report distance; for sensors reporting only near/far, the detection distance is set by the device. "Show over lock screen" "Always allowed" "Allowed" diff --git a/tools/chatgui-android/call-service-tests/build.gradle.kts b/tools/chatgui-android/call-service-tests/build.gradle.kts index 4edc2af7..8448195c 100644 --- a/tools/chatgui-android/call-service-tests/build.gradle.kts +++ b/tools/chatgui-android/call-service-tests/build.gradle.kts @@ -9,6 +9,7 @@ kotlin { "com/utun/chat/data/RouterHostEnvironment.kt", "com/utun/chat/data/RadioHostEnvironment.kt", "com/utun/chat/data/CallServiceHost.kt", "com/utun/chat/data/CallService.kt", "com/utun/chat/data/CallController.kt", "com/utun/chat/data/CallState.kt", "com/utun/chat/data/CallAudioRouter.kt", "com/utun/chat/data/CoreActivity.kt", + "com/utun/chat/data/SettingsOperations.kt", "com/utun/chat/data/RadioAutoSettings.kt", "com/utun/chat/data/StreamServiceHost.kt", "com/utun/chat/data/StreamService.kt", "com/utun/chat/data/StreamController.kt", "com/utun/chat/data/StreamTxStats.kt") } diff --git a/tools/chatgui-android/call-service-tests/src/main/kotlin/com/utun/chat/data/CallServiceHost.kt b/tools/chatgui-android/call-service-tests/src/main/kotlin/com/utun/chat/data/CallServiceHost.kt index cacfdaf8..60f96925 100644 --- a/tools/chatgui-android/call-service-tests/src/main/kotlin/com/utun/chat/data/CallServiceHost.kt +++ b/tools/chatgui-android/call-service-tests/src/main/kotlin/com/utun/chat/data/CallServiceHost.kt @@ -88,7 +88,7 @@ object TelecomCallManager { class ProximityManager(context: Context) { val near = MutableStateFlow(null) - fun start() {} + fun start(thresholdCm: Float = 5f) {} fun stop() { near.value = null } } object RadioCallController { fun busy() = false } diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt b/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt index d433f096..3ee49f84 100644 --- a/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt +++ b/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt @@ -197,6 +197,8 @@ object NativeLib { /** Управляемая подготовка конфига: тест удерживает реальный worker, считает перекрытия и затем дожидается его завершения. */ class HostConfigProvider { + val changes = SettingsOperations() + val proximitySettings = MutableStateFlow(true to 5f) val radioCompressorSettings = MutableStateFlow(Any()) var failStreamSave = false fun saveStreamCaptureSettings(config: Any?, audio: Boolean?): Deferred = CompletableDeferred().also { diff --git a/tools/chatgui-android/router-tests/src/test/kotlin/com/utun/chat/data/ProximityManagerLifecycleTest.kt b/tools/chatgui-android/router-tests/src/test/kotlin/com/utun/chat/data/ProximityManagerLifecycleTest.kt index 7187358c..63251e15 100644 --- a/tools/chatgui-android/router-tests/src/test/kotlin/com/utun/chat/data/ProximityManagerLifecycleTest.kt +++ b/tools/chatgui-android/router-tests/src/test/kotlin/com/utun/chat/data/ProximityManagerLifecycleTest.kt @@ -69,6 +69,49 @@ class ProximityManagerLifecycleTest { assertNull(proximity.near.value) } + @Test fun thresholdDeterminesNearBelowSensorMaximum() { + proximity.start(2f) + for ((distance, near) in listOf(1.9f to true, 2f to false, 3f to false, 0f to true, 5f to false)) { + manager.deliver(0, distance) + Looper.getMainLooper().dispatchAll() + assertEquals(near, proximity.near.value) + } + } + + @Test fun thresholdChangeWaitsForFreshSampleAndRejectsOldCallback() { + proximity.start(5f) + manager.deliver(0, 3f); Looper.getMainLooper().dispatchAll() + assertEquals(true, proximity.near.value) + proximity.start(2f) + assertNull(proximity.near.value) + assertEquals(2, manager.registerAttempts) + assertEquals(1, manager.active.size) + manager.deliver(0, 0f); Looper.getMainLooper().dispatchAll() + assertNull(proximity.near.value) + manager.deliver(1, 3f); Looper.getMainLooper().dispatchAll() + assertEquals(false, proximity.near.value) + } + + @Test fun binarySensorKeepsFarAtMaximumEvenWithLargerThreshold() { + manager.sensor = android.hardware.Sensor(1f) + proximity = ProximityManager(context) + for (threshold in listOf(0.1f, 5f, 10f)) { + proximity.start(threshold) + val index = manager.registrations.lastIndex + manager.deliver(index, 0f); Looper.getMainLooper().dispatchAll() + assertEquals(true, proximity.near.value) + manager.deliver(index, 1f); Looper.getMainLooper().dispatchAll() + assertEquals(false, proximity.near.value) + } + } + + @Test fun invalidThresholdIsLoggedWithoutRegistering() { + for (threshold in listOf(0f, -1f, Float.NaN, Float.POSITIVE_INFINITY)) proximity.start(threshold) + assertEquals(0, manager.registerAttempts) + assertNull(proximity.near.value) + assertEquals(4, LogManager.entries.count { it.level == "ERROR" && "Invalid start threshold" in it.message }) + } + @Test fun missingSensorServiceRemainsUnknown() { context.sensorManager = null proximity = ProximityManager(context)