wifi aware wip

This commit is contained in:
callebtc
2025-10-21 22:24:55 +02:00
parent 9535ca8940
commit f4341ae1c1
11 changed files with 1490 additions and 2 deletions
+9
View File
@@ -5,6 +5,7 @@
<!-- Internet permissions for Nostr relay connections -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<!-- Bluetooth permissions -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
@@ -20,6 +21,12 @@
<!-- Notification permissions -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- WiFi / WiFi Aware permissions -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- Android 13+ runtime permission for WiFi operations (including Aware) -->
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<!-- Microphone for voice notes -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
@@ -38,6 +45,8 @@
<!-- Hardware features -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<!-- Device support hint for WiFi Aware (optional) -->
<uses-feature android:name="android.hardware.wifi.aware" android:required="false" />
<application
android:name=".BitchatApplication"
@@ -38,6 +38,12 @@ class BitchatApplication : Application() {
// Initialize debug preference manager (persists debug toggles)
try { com.bitchat.android.ui.debug.DebugPreferenceManager.init(this) } catch (_: Exception) { }
// Initialize WiFi Aware controller with persisted default
try {
val enabled = com.bitchat.android.ui.debug.DebugPreferenceManager.getWifiAwareEnabled(false)
com.bitchat.android.wifiaware.WifiAwareController.initialize(this, enabled)
} catch (_: Exception) { }
// TorManager already initialized above
}
}
@@ -209,6 +209,7 @@ class OnboardingCoordinator(
return when {
permission.contains("BLUETOOTH") -> "Bluetooth/Nearby Devices"
permission.contains("LOCATION") -> "Location (for Bluetooth scanning)"
permission.contains("NEARBY_WIFI") -> "Nearby WiFi Devices (for WiFi Aware)"
permission.contains("NOTIFICATION") -> "Notifications"
else -> permission.substringAfterLast(".")
}
@@ -11,6 +11,7 @@ import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
@@ -242,6 +243,7 @@ private fun getPermissionIcon(permissionType: PermissionType): ImageVector {
PermissionType.PRECISE_LOCATION -> Icons.Filled.LocationOn
PermissionType.MICROPHONE -> Icons.Filled.Mic
PermissionType.NOTIFICATIONS -> Icons.Filled.Notifications
PermissionType.WIFI_AWARE -> Icons.Filled.Wifi
PermissionType.BATTERY_OPTIMIZATION -> Icons.Filled.Power
PermissionType.OTHER -> Icons.Filled.Settings
}
@@ -67,6 +67,11 @@ class PermissionManager(private val context: Context) {
Manifest.permission.ACCESS_FINE_LOCATION
))
// WiFi Aware: Android 13+ requires NEARBY_WIFI_DEVICES runtime permission
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissions.add(Manifest.permission.NEARBY_WIFI_DEVICES)
}
// Notification permission intentionally excluded to keep it optional
return permissions
@@ -177,6 +182,20 @@ class PermissionManager(private val context: Context) {
)
)
// WiFi Aware category (Android 13+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val wifiAwarePermissions = listOf(Manifest.permission.NEARBY_WIFI_DEVICES)
categories.add(
PermissionCategory(
type = PermissionType.WIFI_AWARE,
description = "Enable WiFi Aware to discover and connect to nearby bitchat users over WiFi.",
permissions = wifiAwarePermissions,
isGranted = wifiAwarePermissions.all { isPermissionGranted(it) },
systemDescription = "Allow bitchat to discover nearby WiFi devices"
)
)
}
// Notifications category (if applicable)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
categories.add(
@@ -262,6 +281,7 @@ enum class PermissionType(val nameValue: String) {
PRECISE_LOCATION("Precise Location"),
MICROPHONE("Microphone"),
NOTIFICATIONS("Notifications"),
WIFI_AWARE("WiFi Aware"),
BATTERY_OPTIMIZATION("Battery Optimization"),
OTHER("Other")
}
@@ -576,9 +576,21 @@ private fun PeerItem(
tint = Color.Gray
)
} else {
val wifiAwarePeers by com.bitchat.android.wifiaware.WifiAwareController.connectedPeers.collectAsState()
val isWifiAware = wifiAwarePeers.containsKey(peerID)
val icon = when {
isWifiAware -> Icons.Filled.Wifi
isDirect -> Icons.Outlined.SettingsInputAntenna
else -> Icons.Filled.Route
}
val cd = when {
isWifiAware -> "Direct WiFi Aware"
isDirect -> "Direct Bluetooth"
else -> "Routed"
}
Icon(
imageVector = if (isDirect) Icons.Outlined.SettingsInputAntenna else Icons.Filled.Route,
contentDescription = if (isDirect) "Direct Bluetooth" else "Routed",
imageVector = icon,
contentDescription = cd,
modifier = Modifier.size(16.dp),
tint = colorScheme.onSurface.copy(alpha = 0.8f)
)
@@ -20,6 +20,10 @@ object DebugPreferenceManager {
// GCS keys (no migration/back-compat)
private const val KEY_GCS_MAX_BYTES = "gcs_max_filter_bytes"
private const val KEY_GCS_FPR = "gcs_filter_fpr_percent"
// Transport master toggles
private const val KEY_BLE_ENABLED = "ble_enabled"
private const val KEY_WIFI_AWARE_ENABLED = "wifi_aware_enabled"
private const val KEY_WIFI_AWARE_VERBOSE = "wifi_aware_verbose"
private lateinit var prefs: SharedPreferences
@@ -100,4 +104,26 @@ object DebugPreferenceManager {
fun setGcsFprPercent(value: Double) {
if (ready()) prefs.edit().putLong(KEY_GCS_FPR, java.lang.Double.doubleToRawLongBits(value)).apply()
}
// Transport toggles
fun getBleEnabled(default: Boolean = true): Boolean =
if (ready()) prefs.getBoolean(KEY_BLE_ENABLED, default) else default
fun setBleEnabled(value: Boolean) {
if (ready()) prefs.edit().putBoolean(KEY_BLE_ENABLED, value).apply()
}
fun getWifiAwareEnabled(default: Boolean = false): Boolean =
if (ready()) prefs.getBoolean(KEY_WIFI_AWARE_ENABLED, default) else default
fun setWifiAwareEnabled(value: Boolean) {
if (ready()) prefs.edit().putBoolean(KEY_WIFI_AWARE_ENABLED, value).apply()
}
fun getWifiAwareVerbose(default: Boolean = false): Boolean =
if (ready()) prefs.getBoolean(KEY_WIFI_AWARE_VERBOSE, default) else default
fun setWifiAwareVerbose(value: Boolean) {
if (ready()) prefs.edit().putBoolean(KEY_WIFI_AWARE_VERBOSE, value).apply()
}
}
@@ -36,6 +36,16 @@ class DebugSettingsManager private constructor() {
private val _packetRelayEnabled = MutableStateFlow(true)
val packetRelayEnabled: StateFlow<Boolean> = _packetRelayEnabled.asStateFlow()
// Master transport toggles
private val _bleEnabled = MutableStateFlow(true)
val bleEnabled: StateFlow<Boolean> = _bleEnabled.asStateFlow()
private val _wifiAwareEnabled = MutableStateFlow(false)
val wifiAwareEnabled: StateFlow<Boolean> = _wifiAwareEnabled.asStateFlow()
private val _wifiAwareVerbose = MutableStateFlow(false)
val wifiAwareVerbose: StateFlow<Boolean> = _wifiAwareVerbose.asStateFlow()
// Connection limit overrides (debug)
private val _maxConnectionsOverall = MutableStateFlow(8)
val maxConnectionsOverall: StateFlow<Int> = _maxConnectionsOverall.asStateFlow()
@@ -54,6 +64,10 @@ class DebugSettingsManager private constructor() {
_maxConnectionsOverall.value = DebugPreferenceManager.getMaxConnectionsOverall(8)
_maxServerConnections.value = DebugPreferenceManager.getMaxConnectionsServer(8)
_maxClientConnections.value = DebugPreferenceManager.getMaxConnectionsClient(8)
// Transport toggles
_bleEnabled.value = DebugPreferenceManager.getBleEnabled(true)
_wifiAwareEnabled.value = DebugPreferenceManager.getWifiAwareEnabled(false)
_wifiAwareVerbose.value = DebugPreferenceManager.getWifiAwareVerbose(false)
} catch (_: Exception) {
// Preferences not ready yet; keep defaults. They will be applied on first change.
}
@@ -140,6 +154,27 @@ class DebugSettingsManager private constructor() {
))
}
fun setBleEnabled(enabled: Boolean) {
DebugPreferenceManager.setBleEnabled(enabled)
_bleEnabled.value = enabled
addDebugMessage(DebugMessage.SystemMessage(if (enabled) "🟢 BLE enabled" else "🔴 BLE disabled"))
}
fun setWifiAwareEnabled(enabled: Boolean) {
DebugPreferenceManager.setWifiAwareEnabled(enabled)
_wifiAwareEnabled.value = enabled
addDebugMessage(DebugMessage.SystemMessage(if (enabled) "🟢 WiFi Aware enabled" else "🔴 WiFi Aware disabled"))
try {
com.bitchat.android.wifiaware.WifiAwareController.setEnabled(enabled)
} catch (_: Exception) { }
}
fun setWifiAwareVerbose(enabled: Boolean) {
DebugPreferenceManager.setWifiAwareVerbose(enabled)
_wifiAwareVerbose.value = enabled
addDebugMessage(DebugMessage.SystemMessage(if (enabled) "🔊 WiFi Aware verbose logging enabled" else "🔇 WiFi Aware verbose logging disabled"))
}
fun setMaxConnectionsOverall(value: Int) {
val clamped = value.coerceIn(1, 32)
DebugPreferenceManager.setMaxConnectionsOverall(clamped)
@@ -198,6 +233,16 @@ class DebugSettingsManager private constructor() {
_connectedDevices.value = devices
}
// WiFi Aware debug collections
private val _wifiAwareDiscovered = MutableStateFlow<Map<String, String>>(emptyMap()) // peerID->nickname
val wifiAwareDiscovered: StateFlow<Map<String, String>> = _wifiAwareDiscovered.asStateFlow()
private val _wifiAwareConnected = MutableStateFlow<Map<String, String>>(emptyMap()) // peerID->ip
val wifiAwareConnected: StateFlow<Map<String, String>> = _wifiAwareConnected.asStateFlow()
fun updateWifiAwareDiscovered(map: Map<String, String>) { _wifiAwareDiscovered.value = map }
fun updateWifiAwareConnected(map: Map<String, String>) { _wifiAwareConnected.value = map }
fun updateRelayStats(stats: PacketRelayStats) {
_relayStats.value = stats
}
@@ -8,6 +8,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiTethering
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.Devices
@@ -53,6 +55,11 @@ fun DebugSettingsSheet(
val seenCapacity by manager.seenPacketCapacity.collectAsState()
val gcsMaxBytes by manager.gcsMaxBytes.collectAsState()
val gcsFpr by manager.gcsFprPercent.collectAsState()
val bleEnabled by manager.bleEnabled.collectAsState()
val wifiAwareEnabled by manager.wifiAwareEnabled.collectAsState()
val wifiAwareVerbose by manager.wifiAwareVerbose.collectAsState()
val wifiAwareDiscovered by manager.wifiAwareDiscovered.collectAsState()
val wifiAwareConnected by manager.wifiAwareConnected.collectAsState()
// Push live connected devices from mesh service whenever sheet is visible
LaunchedEffect(isPresented) {
@@ -76,6 +83,15 @@ fun DebugSettingsSheet(
)
}
manager.updateConnectedDevices(devices)
// Also surface WiFi Aware status
try {
val ctrl = com.bitchat.android.wifiaware.WifiAwareController
val known = ctrl.knownPeers.value
val discovered = ctrl.discoveredPeers.value
val discoveredMap = discovered.associateWith { pid -> known[pid] ?: "" }
manager.updateWifiAwareDiscovered(discoveredMap)
manager.updateWifiAwareConnected(ctrl.connectedPeers.value)
} catch (_: Exception) { }
kotlinx.coroutines.delay(1000)
}
}
@@ -198,6 +214,46 @@ fun DebugSettingsSheet(
}
}
// Transport toggles (BLE + WiFi Aware)
item {
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.Devices, contentDescription = null, tint = Color(0xFF4CAF50))
Text("Transports", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
}
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Bluetooth, contentDescription = null, tint = Color(0xFF007AFF))
Spacer(Modifier.width(8.dp))
Text("BLE", fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f))
Switch(checked = bleEnabled, onCheckedChange = {
manager.setBleEnabled(it)
scope.launch {
if (it) {
if (gattServerEnabled) meshService.connectionManager.startServer()
if (gattClientEnabled) meshService.connectionManager.startClient()
} else {
meshService.connectionManager.stopServer()
meshService.connectionManager.stopClient()
}
}
})
}
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Wifi, contentDescription = null, tint = Color(0xFF9C27B0))
Spacer(Modifier.width(8.dp))
Text("WiFi Aware", fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f))
Switch(checked = wifiAwareEnabled, onCheckedChange = { manager.setWifiAwareEnabled(it) })
}
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.width(24.dp))
Text("WiFi Aware verbose", fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f))
Switch(checked = wifiAwareVerbose, onCheckedChange = { manager.setWifiAwareVerbose(it) })
}
}
}
}
// Packet relay controls and stats
item {
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
@@ -289,6 +345,43 @@ fun DebugSettingsSheet(
}
}
// WiFi Aware controls and status
item {
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Filled.WifiTethering, contentDescription = null, tint = Color(0xFF9C27B0))
Text("WiFi 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))
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
AssistChip(onClick = { com.bitchat.android.wifiaware.WifiAwareController.startIfPossible() }, label = { Text("Start") })
AssistChip(onClick = { com.bitchat.android.wifiaware.WifiAwareController.stop() }, label = { Text("Stop") })
AssistChip(onClick = { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendBroadcastAnnounce() }, label = { Text("Announce") })
}
Text("Discovered: ${wifiAwareDiscovered.size}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
if (wifiAwareDiscovered.isEmpty()) {
Text("No discoveries yet", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.6f))
} else {
wifiAwareDiscovered.entries.take(50).forEach { (peer, nick) ->
Text("${if (nick.isBlank()) peer.take(8) + "…" else nick} (${peer.take(8)}…) ", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
}
}
Divider()
Text("Connected: ${wifiAwareConnected.size}", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
if (wifiAwareConnected.isEmpty()) {
Text("No active sockets", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.6f))
} else {
wifiAwareConnected.entries.take(50).forEach { (peer, ip) ->
Text("${peer.take(8)}… @ $ip", fontFamily = FontFamily.Monospace, fontSize = 12.sp)
}
}
}
}
}
// Connected devices
item {
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
@@ -0,0 +1,111 @@
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
/**
* 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 var service: WifiAwareMeshService? = null
private var appContext: Context? = null
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val _enabled = MutableStateFlow(false)
val enabled: StateFlow<Boolean> = _enabled.asStateFlow()
private val _running = MutableStateFlow(false)
val running: StateFlow<Boolean> = _running.asStateFlow()
// Simple debug surfacing
private val _connectedPeers = MutableStateFlow<Map<String, String>>(emptyMap()) // peerID -> ip
val connectedPeers: StateFlow<Map<String, String>> = _connectedPeers.asStateFlow()
private val _knownPeers = MutableStateFlow<Map<String, String>>(emptyMap()) // peerID -> nickname
val knownPeers: StateFlow<Map<String, String>> = _knownPeers.asStateFlow()
private val _discoveredPeers = MutableStateFlow<Set<String>>(emptySet())
val discoveredPeers: StateFlow<Set<String>> = _discoveredPeers.asStateFlow()
fun initialize(context: Context, enabledByDefault: Boolean) {
appContext = context.applicationContext
setEnabled(enabledByDefault)
// Start background poller for debug surfacing
scope.launch {
while (isActive) {
try {
val s = service
if (s != null) {
_connectedPeers.value = s.getDeviceAddressToPeerMapping().entries.associate { (ip, peer) -> peer to 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() {
if (_running.value) return
val ctx = appContext ?: return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Log.w(TAG, "WiFi Aware requires Android 10 (Q)+; disabled.")
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("WiFi Aware not supported on this device (requires Android 10+)")) } catch (_: Exception) {}
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 WiFi Aware")
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Grant Nearby WiFi Devices to start WiFi Aware")) } catch (_: Exception) {}
return
}
}
try {
service = WifiAwareMeshService(ctx).also {
it.startServices()
_running.value = true
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("WiFi Aware started")) } 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("WiFi Aware failed to start: ${e.message}")) } catch (_: Exception) {}
}
}
fun stop() {
try { service?.stopServices() } catch (_: Exception) { }
service = null
_running.value = false
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("WiFi Aware stopped")) } catch (_: Exception) {}
}
fun getService(): WifiAwareMeshService? = service
}
File diff suppressed because it is too large Load Diff