package com.bitchat.android.wifiaware import android.content.Context import android.os.Build import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean /** * WifiAwareController manages lifecycle and debug surfacing for the WifiAwareMeshService. * It starts/stops the service based on debug preferences and exposes simple flows for UI. */ object WifiAwareController { private const val TAG = "WifiAwareController" private const val MAX_RESTART_ATTEMPTS = 15 private const val RESTART_RETRY_DELAY_MS = 2_000L private var service: WifiAwareMeshService? = null private var appContext: Context? = null private val lifecycleLock = Any() private var starting = false private val restartInFlight = AtomicBoolean(false) private var awareReceiverRegistered = false private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val _enabled = MutableStateFlow(false) val enabled: StateFlow = _enabled.asStateFlow() private val _running = MutableStateFlow(false) val running: StateFlow = _running.asStateFlow() // Simple debug surfacing private val _connectedPeers = MutableStateFlow>(emptyMap()) // peerID -> ip val connectedPeers: StateFlow> = _connectedPeers.asStateFlow() private val _knownPeers = MutableStateFlow>(emptyMap()) // peerID -> nickname val knownPeers: StateFlow> = _knownPeers.asStateFlow() private val _discoveredPeers = MutableStateFlow>(emptySet()) val discoveredPeers: StateFlow> = _discoveredPeers.asStateFlow() fun initialize(context: Context, enabledByDefault: Boolean) { appContext = context.applicationContext registerAwareStateReceiver(appContext!!) setEnabled(enabledByDefault) // Start background poller for debug surfacing scope.launch { while (isActive) { try { val s = service if (s != null) { _connectedPeers.value = s.getDeviceAddressToPeerMapping() // peerID -> ip _knownPeers.value = s.getPeerNicknames() _discoveredPeers.value = s.getDiscoveredPeerIds() } else { _connectedPeers.value = emptyMap() _knownPeers.value = emptyMap() _discoveredPeers.value = emptySet() } } catch (_: Exception) { } delay(1000) } } } fun setEnabled(value: Boolean) { _enabled.value = value if (value) startIfPossible() else stop() } fun startIfPossible() { val reusableService = synchronized(lifecycleLock) { if (!_enabled.value) return val existing = service if (existing?.isRunning() == true) { _running.value = true return } if (starting) return starting = true existing } val ctx = appContext ?: run { synchronized(lifecycleLock) { starting = false } return } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { Log.w(TAG, "Wi‑Fi Aware requires Android 10 (Q)+; disabled.") try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware not supported on this device (requires Android 10+)")) } catch (_: Exception) {} synchronized(lifecycleLock) { starting = false } return } val awareManager = ctx.getSystemService(android.net.wifi.aware.WifiAwareManager::class.java) if (awareManager == null || !awareManager.isAvailable) { Log.w(TAG, "Wi-Fi Aware is not currently available; not starting") try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware is not available on this device right now")) } catch (_: Exception) {} synchronized(lifecycleLock) { starting = false } return } // Check system location setting: WifiAwareManager.attach() throws SecurityException if disabled val lm = ctx.getSystemService(Context.LOCATION_SERVICE) as? android.location.LocationManager val locationEnabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { lm?.isLocationEnabled == true } else { @Suppress("DEPRECATION") lm?.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER) == true || lm?.isProviderEnabled(android.location.LocationManager.NETWORK_PROVIDER) == true } if (!locationEnabled) { Log.w(TAG, "Location services are disabled; Wi-Fi Aware cannot start.") try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Enable Location Services to start Wi-Fi Aware")) } catch (_: Exception) {} synchronized(lifecycleLock) { starting = false } return } // Android 13+: require NEARBY_WIFI_DEVICES runtime permission if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val granted = androidx.core.content.ContextCompat.checkSelfPermission(ctx, android.Manifest.permission.NEARBY_WIFI_DEVICES) == android.content.pm.PackageManager.PERMISSION_GRANTED if (!granted) { Log.w(TAG, "Missing NEARBY_WIFI_DEVICES permission; not starting Wi‑Fi Aware") try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Grant Nearby Wi‑Fi Devices to start Wi‑Fi Aware")) } catch (_: Exception) {} synchronized(lifecycleLock) { starting = false } return } } if (!_enabled.value) { synchronized(lifecycleLock) { starting = false } return } try { val startedService = reusableService ?: run { Log.i(TAG, "Instantiating WifiAwareMeshService...") WifiAwareMeshService(ctx) } startedService.startServices() if (startedService.isRunning()) { synchronized(lifecycleLock) { service = startedService _running.value = true } try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware started")) } catch (_: Exception) {} } else { if (reusableService == null) { try { startedService.stopServices() } catch (_: Exception) { } } synchronized(lifecycleLock) { if (service === startedService) service = null _running.value = false } try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware did not start")) } catch (_: Exception) {} } } catch (e: Throwable) { Log.e(TAG, "Failed to start WifiAwareMeshService", e) _running.value = false try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware failed to start: ${e.message}")) } catch (_: Exception) {} } finally { synchronized(lifecycleLock) { starting = false } } } fun stop() { val stopped = synchronized(lifecycleLock) { val current = service service = null starting = false _running.value = false current } try { stopped?.stopServices() } catch (_: Exception) { } try { com.bitchat.android.services.AppStateStore.clearTransportPeers("WIFI") } catch (_: Exception) { } _connectedPeers.value = emptyMap() _knownPeers.value = emptyMap() _discoveredPeers.value = emptySet() try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware stopped")) } catch (_: Exception) {} } internal fun onServiceStopped(stoppedService: WifiAwareMeshService) { synchronized(lifecycleLock) { if (service !== stoppedService) return service = null _running.value = false try { com.bitchat.android.services.AppStateStore.clearTransportPeers("WIFI") } catch (_: Exception) { } _connectedPeers.value = emptyMap() _knownPeers.value = emptyMap() _discoveredPeers.value = emptySet() } } /** * Schedules a restart of the Wi-Fi Aware transport. Concurrent requests are coalesced into * a single in-flight loop that retries with backoff. This is important because a single fixed * delay can land while the service is still tearing down (recoveryInProgress), in which case * startServices() defers and we must try again rather than give up. */ internal fun restartIfStillEnabled(delayMs: Long = 0L) { if (!restartInFlight.compareAndSet(false, true)) { Log.d(TAG, "Restart already in flight; coalescing request") return } scope.launch { try { if (delayMs > 0L) delay(delayMs) var attempt = 0 while (_enabled.value && !_running.value && attempt < MAX_RESTART_ATTEMPTS) { startIfPossible() if (_running.value) break attempt++ delay(RESTART_RETRY_DELAY_MS) } } finally { restartInFlight.set(false) } } } /** * Listens for system Wi-Fi Aware availability changes. Aware can flip off/on at runtime * (Wi-Fi toggling, hotspot/SoftAP, location changes); without this we would only recover on * an unrelated trigger. */ private fun registerAwareStateReceiver(ctx: Context) { if (awareReceiverRegistered) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return try { val filter = android.content.IntentFilter( android.net.wifi.aware.WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED ) ctx.registerReceiver(object : android.content.BroadcastReceiver() { override fun onReceive(c: Context?, intent: android.content.Intent?) { val mgr = ctx.getSystemService(android.net.wifi.aware.WifiAwareManager::class.java) val available = mgr?.isAvailable == true Log.i(TAG, "Wi-Fi Aware availability changed: available=$available enabled=${_enabled.value} running=${_running.value}") if (available) { if (_enabled.value) restartIfStillEnabled(500) } else if (_running.value) { // Aware went away; tear down cleanly so we can re-attach when it returns. // Note: this does not change the enabled preference. stop() } } }, filter) awareReceiverRegistered = true } catch (e: Exception) { Log.w(TAG, "Failed to register Wi-Fi Aware state receiver: ${e.message}") } } fun getService(): WifiAwareMeshService? = service }