diff --git a/app/src/main/java/com/bitchat/android/BitchatApplication.kt b/app/src/main/java/com/bitchat/android/BitchatApplication.kt index 282f3295..32f11d0b 100644 --- a/app/src/main/java/com/bitchat/android/BitchatApplication.kt +++ b/app/src/main/java/com/bitchat/android/BitchatApplication.kt @@ -43,7 +43,7 @@ class BitchatApplication : Application() { // Initialize Wi‑Fi Aware controller with persisted default try { - val enabled = com.bitchat.android.ui.debug.DebugPreferenceManager.getWifiAwareEnabled(false) + val enabled = com.bitchat.android.ui.debug.DebugPreferenceManager.getWifiAwareEnabled(true) com.bitchat.android.wifiaware.WifiAwareController.initialize(this, enabled) } catch (_: Exception) { } diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt index b8b92048..8fcf9c31 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionManager.kt @@ -26,18 +26,17 @@ class PermissionManager(private val context: Context) { private fun shouldRequireWifiAwarePermission(): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return false val enabled = try { - com.bitchat.android.ui.debug.DebugPreferenceManager.getWifiAwareEnabled(false) + com.bitchat.android.ui.debug.DebugPreferenceManager.getWifiAwareEnabled(true) } catch (_: Exception) { - false + true } if (!enabled) return false - val hasFeature = try { - context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE) + return try { + com.bitchat.android.wifiaware.WifiAwareSupport.isSupported(context) } catch (_: Exception) { false } - return hasFeature } /** diff --git a/app/src/main/java/com/bitchat/android/ui/debug/DebugPreferenceManager.kt b/app/src/main/java/com/bitchat/android/ui/debug/DebugPreferenceManager.kt index 2d734c14..f03df3da 100644 --- a/app/src/main/java/com/bitchat/android/ui/debug/DebugPreferenceManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/debug/DebugPreferenceManager.kt @@ -113,7 +113,7 @@ object DebugPreferenceManager { if (ready()) prefs.edit().putBoolean(KEY_BLE_ENABLED, value).apply() } - fun getWifiAwareEnabled(default: Boolean = false): Boolean = + fun getWifiAwareEnabled(default: Boolean = true): Boolean = if (ready()) prefs.getBoolean(KEY_WIFI_AWARE_ENABLED, default) else default fun setWifiAwareEnabled(value: Boolean) { diff --git a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsManager.kt b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsManager.kt index 80910b15..82bc7fc2 100644 --- a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsManager.kt @@ -44,7 +44,7 @@ class DebugSettingsManager private constructor() { private val _bleEnabled = MutableStateFlow(true) val bleEnabled: StateFlow = _bleEnabled.asStateFlow() - private val _wifiAwareEnabled = MutableStateFlow(false) + private val _wifiAwareEnabled = MutableStateFlow(true) val wifiAwareEnabled: StateFlow = _wifiAwareEnabled.asStateFlow() // Master transport toggles @@ -76,7 +76,7 @@ class DebugSettingsManager private constructor() { _maxClientConnections.value = DebugPreferenceManager.getMaxConnectionsClient(8) // Transport toggles _bleEnabled.value = DebugPreferenceManager.getBleEnabled(true) - _wifiAwareEnabled.value = DebugPreferenceManager.getWifiAwareEnabled(false) + _wifiAwareEnabled.value = DebugPreferenceManager.getWifiAwareEnabled(true) _wifiAwareVerbose.value = DebugPreferenceManager.getWifiAwareVerbose(false) } catch (_: Exception) { // Preferences not ready yet; keep defaults. They will be applied on first change. diff --git a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt index 4d1d6a42..65b19182 100644 --- a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt @@ -130,6 +130,9 @@ fun DebugSettingsSheet( val wifiAwareVerbose by manager.wifiAwareVerbose.collectAsState() val wifiAwareDiscovered by manager.wifiAwareDiscovered.collectAsState() val wifiAwareConnected by manager.wifiAwareConnected.collectAsState() + val wifiAwareSupported by com.bitchat.android.wifiaware.WifiAwareController.supported.collectAsState() + val wifiAwareAvailable by com.bitchat.android.wifiaware.WifiAwareController.available.collectAsState() + val wifiAwareSupportStatus by com.bitchat.android.wifiaware.WifiAwareController.supportStatus.collectAsState() // Persistent notification is now controlled solely by MeshServicePreferences.isBackgroundEnabled val listState = rememberLazyListState() val isScrolled by remember { @@ -327,7 +330,23 @@ fun DebugSettingsSheet( Icon(Icons.Filled.Wifi, contentDescription = null, tint = Color(0xFF9C27B0)) Spacer(Modifier.width(8.dp)) Text("Wi‑Fi Aware", fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f)) - Switch(checked = wifiAwareEnabled, onCheckedChange = { manager.setWifiAwareEnabled(it) }) + val wifiSwitchEnabled = wifiAwareSupported + Text( + when { + !wifiAwareSupported -> "unsupported" + wifiAwareAvailable -> "available" + else -> "unavailable" + }, + fontFamily = FontFamily.Monospace, + fontSize = 11.sp, + color = colorScheme.onSurface.copy(alpha = 0.6f) + ) + Spacer(Modifier.width(8.dp)) + Switch( + checked = wifiAwareEnabled && wifiAwareSupported, + enabled = wifiSwitchEnabled, + onCheckedChange = { manager.setWifiAwareEnabled(it) } + ) } Row(verticalAlignment = Alignment.CenterVertically) { Spacer(Modifier.width(24.dp)) @@ -601,17 +620,39 @@ fun DebugSettingsSheet( item { Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + val running by com.bitchat.android.wifiaware.WifiAwareController.running.collectAsState() Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Icon(Icons.Filled.WifiTethering, contentDescription = null, tint = Color(0xFF9C27B0)) Text("Wi‑Fi Aware", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium) Spacer(Modifier.weight(1f)) - val running by com.bitchat.android.wifiaware.WifiAwareController.running.collectAsState() - Text(if (running) "running" else "stopped", fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = colorScheme.onSurface.copy(alpha = 0.7f)) + val wifiStatusText = when { + !wifiAwareSupported -> "unsupported" + running -> "running" + !wifiAwareAvailable -> "unavailable" + else -> "stopped" + } + Text(wifiStatusText, fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = colorScheme.onSurface.copy(alpha = 0.7f)) + } + if (!wifiAwareSupported) { + Text( + wifiAwareSupportStatus?.reason ?: "Wi-Fi Aware is not supported on this device", + fontFamily = FontFamily.Monospace, + fontSize = 11.sp, + color = colorScheme.onSurface.copy(alpha = 0.6f) + ) } Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - AssistChip(onClick = { manager.setWifiAwareEnabled(true) }, label = { Text("Start") }) + AssistChip( + onClick = { manager.setWifiAwareEnabled(true) }, + enabled = wifiAwareSupported, + label = { Text("Start") } + ) AssistChip(onClick = { manager.setWifiAwareEnabled(false) }, label = { Text("Stop") }) - AssistChip(onClick = { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendBroadcastAnnounce() }, label = { Text("Announce") }) + AssistChip( + onClick = { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendBroadcastAnnounce() }, + enabled = running, + label = { Text("Announce") } + ) } Text("Discovered: ${wifiAwareDiscovered.size}", fontFamily = FontFamily.Monospace, fontSize = 12.sp) if (wifiAwareDiscovered.isEmpty()) { diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareController.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareController.kt index b89cd49e..f2ec4cfa 100644 --- a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareController.kt +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareController.kt @@ -30,12 +30,22 @@ object WifiAwareController { private var starting = false private val restartInFlight = AtomicBoolean(false) private var awareReceiverRegistered = false + private var lastBlockedReason: String? = null private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val _enabled = MutableStateFlow(false) val enabled: StateFlow = _enabled.asStateFlow() + private val _supported = MutableStateFlow(false) + val supported: StateFlow = _supported.asStateFlow() + + private val _available = MutableStateFlow(false) + val available: StateFlow = _available.asStateFlow() + + private val _supportStatus = MutableStateFlow(null) + val supportStatus: StateFlow = _supportStatus.asStateFlow() + private val _running = MutableStateFlow(false) val running: StateFlow = _running.asStateFlow() @@ -51,7 +61,12 @@ object WifiAwareController { fun initialize(context: Context, enabledByDefault: Boolean) { appContext = context.applicationContext - registerAwareStateReceiver(appContext!!) + val status = refreshSupportStatus(appContext!!) + if (status.supported) { + registerAwareStateReceiver(appContext!!) + } else { + Log.i(TAG, "Wi-Fi Aware unsupported: ${status.reason}") + } setEnabled(enabledByDefault) // Start background poller for debug surfacing scope.launch { @@ -95,16 +110,20 @@ object WifiAwareController { 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) {} + + val status = refreshSupportStatus(ctx) + if (!status.supported) { + val reason = status.reason ?: "not supported" + Log.w(TAG, "Wi‑Fi Aware unsupported; not starting ($reason)") + addBlockedDebugMessage("unsupported:$reason", "Wi-Fi Aware not supported on this device ($reason)") synchronized(lifecycleLock) { starting = false } return } - val awareManager = ctx.getSystemService(android.net.wifi.aware.WifiAwareManager::class.java) - if (awareManager == null || !awareManager.isAvailable) { + + val awareManager = WifiAwareSupport.getManager(ctx) + if (awareManager == null || !status.available) { 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) {} + addBlockedDebugMessage("unavailable", "Wi-Fi Aware is not available right now") synchronized(lifecycleLock) { starting = false } return } @@ -121,7 +140,7 @@ object WifiAwareController { 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) {} + addBlockedDebugMessage("location-disabled", "Enable Location Services to start Wi-Fi Aware") synchronized(lifecycleLock) { starting = false } return } @@ -131,7 +150,7 @@ object WifiAwareController { 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) {} + addBlockedDebugMessage("missing-nearby-wifi", "Grant Nearby Wi-Fi Devices to start Wi-Fi Aware") synchronized(lifecycleLock) { starting = false } return } @@ -152,6 +171,7 @@ object WifiAwareController { _running.value = true } try { com.bitchat.android.service.MeshServiceHolder.unifiedMeshService?.refreshDelegates() } catch (_: Exception) { } + clearBlockedDebugMessage() 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) { @@ -216,6 +236,8 @@ object WifiAwareController { if (delayMs > 0L) delay(delayMs) var attempt = 0 while (_enabled.value && !_running.value && attempt < MAX_RESTART_ATTEMPTS) { + val ctx = appContext + if (ctx != null && !refreshSupportStatus(ctx).supported) break startIfPossible() if (_running.value) break attempt++ @@ -234,17 +256,16 @@ object WifiAwareController { */ private fun registerAwareStateReceiver(ctx: Context) { if (awareReceiverRegistered) return - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return + if (!refreshSupportStatus(ctx).supported) 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) { + val status = refreshSupportStatus(ctx) + Log.i(TAG, "Wi-Fi Aware availability changed: supported=${status.supported} available=${status.available} enabled=${_enabled.value} running=${_running.value}") + if (status.available) { if (_enabled.value) restartIfStillEnabled(500) } else if (_running.value) { // Aware went away; tear down cleanly so we can re-attach when it returns. @@ -259,5 +280,26 @@ object WifiAwareController { } } + private fun refreshSupportStatus(ctx: Context): WifiAwareSupport.Status { + val status = WifiAwareSupport.evaluate(ctx) + _supported.value = status.supported + _available.value = status.available + _supportStatus.value = status + return status + } + + private fun addBlockedDebugMessage(key: String, message: String) { + if (lastBlockedReason == key) return + lastBlockedReason = key + try { + com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() + .addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage(message)) + } catch (_: Exception) { } + } + + private fun clearBlockedDebugMessage() { + lastBlockedReason = null + } + fun getService(): WifiAwareMeshService? = service } diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt index 6a8c67b8..c9824107 100644 --- a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareMeshService.kt @@ -318,6 +318,15 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor Log.i(TAG, "Wi-Fi Aware transport disabled by debug settings; not starting") return } + val supportStatus = com.bitchat.android.wifiaware.WifiAwareSupport.evaluate(context) + if (!supportStatus.supported) { + Log.i(TAG, "Wi-Fi Aware unsupported on this device; not starting (${supportStatus.reason})") + return + } + if (!supportStatus.available) { + Log.i(TAG, "Wi-Fi Aware unavailable right now; not starting (${supportStatus.reason})") + return + } if (recoveryInProgress) { Log.i(TAG, "Wi-Fi Aware recovery cleanup still in progress; deferring start") return diff --git a/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareSupport.kt b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareSupport.kt new file mode 100644 index 00000000..3e7dbc58 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/wifi-aware/WifiAwareSupport.kt @@ -0,0 +1,74 @@ +package com.bitchat.android.wifiaware + +import android.content.Context +import android.content.pm.PackageManager +import android.net.wifi.aware.WifiAwareManager +import android.os.Build + +/** + * Centralized Wi-Fi Aware capability checks. + * + * "Supported" is stable device/API capability. "Available" is runtime state and can change + * when Wi-Fi, location, airplane mode, or system radio state changes. + */ +object WifiAwareSupport { + data class Status( + val supported: Boolean, + val available: Boolean, + val reason: String? = null + ) + + fun evaluate(context: Context): Status { + val appContext = context.applicationContext + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return Status( + supported = false, + available = false, + reason = "requires Android 10+" + ) + } + + val hasFeature = try { + appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE) + } catch (_: Exception) { + false + } + if (!hasFeature) { + return Status( + supported = false, + available = false, + reason = "device does not advertise Wi-Fi Aware support" + ) + } + + val manager = getManager(appContext) + ?: return Status( + supported = false, + available = false, + reason = "WifiAwareManager unavailable" + ) + + val available = try { + manager.isAvailable + } catch (_: Exception) { + false + } + + return Status( + supported = true, + available = available, + reason = if (available) null else "Wi-Fi Aware temporarily unavailable" + ) + } + + fun isSupported(context: Context): Boolean = evaluate(context).supported + + fun getManager(context: Context): WifiAwareManager? { + return try { + context.applicationContext.getSystemService(WifiAwareManager::class.java) + } catch (_: Exception) { + null + } + } +}