mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:45:19 +00:00
fix: wifi aware socket binding (#533)
* wifi aware wip * wifi aware wip * starting to work * werk * dms work * wip * fix(wifi-aware): use bindSocket and scoped IPv6 instead of bindProcessToNetwork * Merge branch 'upstream/main' into fix/wifi-aware-socket-binding * Fix Wi-Fi Aware connectivity and UI integration post-merge - Replace bindProcessToNetwork with bindSocket for VPN compatibility. - Implement Scoped IPv6 address resolution (aware0) for mesh routing. - Bridge Wi-Fi Aware incoming messages to AppStateStore for UI visibility. - Fix syntax errors and variable name conflicts in Debug UI. * Enhance Wi-Fi Aware robustness and debug UI display - Clean up transport resources (sockets, server sockets, network callbacks) immediately on peer disconnection. - Implement resolveScopedAddress to show scoped IPv6 (e.g., %aware0) in Debug UI. - Fix Map type mismatch warning in ChatViewModel bridge. - Filter self-ID from peer cleanup tables to prevent recursive self-removal. * Share GossipSyncManager across transports to prevent redundant message synchronization - Registered BluetoothMeshService's GossipSyncManager as a singleton in MeshServiceHolder. - Modified WifiAwareMeshService to use the shared GossipSyncManager if available. - Added background cleanup for peer mappings on socket disconnection. - Fixed Kotlin type mismatch during nickname map merging. * Restore VPN acquisition logic and improve peer cleanup - Revert removal of NET_CAPABILITY_NOT_VPN to allow hardware handle acquisition while VPN is active. - Refactor handlePeerDisconnection to more reliably cleanup initial and routed IDs. - Switch cleanup logging to debug level to reduce log noise. --------- Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> Co-authored-by: aidenvalue <>
This commit is contained in:
co-authored by
callebtc
aidenvalue <>
parent
903a4584a8
commit
0c7505b588
@@ -5,6 +5,7 @@
|
|||||||
<!-- Internet permissions for Nostr relay connections -->
|
<!-- Internet permissions for Nostr relay connections -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
|
||||||
<!-- Bluetooth permissions -->
|
<!-- Bluetooth permissions -->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
@@ -19,6 +20,12 @@
|
|||||||
|
|
||||||
<!-- Notification permissions -->
|
<!-- Notification permissions -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<!-- Wi‑Fi / Wi‑Fi 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 Wi‑Fi operations (including Aware) -->
|
||||||
|
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
|
||||||
<!-- Signature permission for internal UI shutdown broadcasts -->
|
<!-- Signature permission for internal UI shutdown broadcasts -->
|
||||||
<uses-permission android:name="com.bitchat.android.permission.FORCE_FINISH" />
|
<uses-permission android:name="com.bitchat.android.permission.FORCE_FINISH" />
|
||||||
<!-- Foreground service and boot permissions for long-running background mesh -->
|
<!-- Foreground service and boot permissions for long-running background mesh -->
|
||||||
@@ -47,6 +54,8 @@
|
|||||||
<!-- Hardware features -->
|
<!-- Hardware features -->
|
||||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
||||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
||||||
|
<!-- Device support hint for Wi‑Fi Aware (optional) -->
|
||||||
|
<uses-feature android:name="android.hardware.wifi.aware" android:required="false" />
|
||||||
|
|
||||||
<permission
|
<permission
|
||||||
android:name="com.bitchat.android.permission.FORCE_FINISH"
|
android:name="com.bitchat.android.permission.FORCE_FINISH"
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ class BitchatApplication : Application() {
|
|||||||
// Initialize debug preference manager (persists debug toggles)
|
// Initialize debug preference manager (persists debug toggles)
|
||||||
try { com.bitchat.android.ui.debug.DebugPreferenceManager.init(this) } catch (_: Exception) { }
|
try { com.bitchat.android.ui.debug.DebugPreferenceManager.init(this) } catch (_: Exception) { }
|
||||||
|
|
||||||
|
// Initialize Wi‑Fi 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) { }
|
||||||
// Initialize mesh service preferences
|
// Initialize mesh service preferences
|
||||||
try { com.bitchat.android.service.MeshServicePreferences.init(this) } catch (_: Exception) { }
|
try { com.bitchat.android.service.MeshServicePreferences.init(this) } catch (_: Exception) { }
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
// Ensure foreground service is running and get mesh instance from holder
|
// Ensure foreground service is running and get mesh instance from holder
|
||||||
try { com.bitchat.android.service.MeshForegroundService.start(applicationContext) } catch (_: Exception) { }
|
try { com.bitchat.android.service.MeshForegroundService.start(applicationContext) } catch (_: Exception) { }
|
||||||
meshService = com.bitchat.android.service.MeshServiceHolder.getOrCreate(applicationContext)
|
meshService = com.bitchat.android.service.MeshServiceHolder.getOrCreate(applicationContext)
|
||||||
|
// Expose BLE mesh to Wi‑Fi Aware controller for cross-transport relays
|
||||||
|
try { com.bitchat.android.wifiaware.WifiAwareController.setBleMeshService(meshService) } catch (_: Exception) { }
|
||||||
bluetoothStatusManager = BluetoothStatusManager(
|
bluetoothStatusManager = BluetoothStatusManager(
|
||||||
activity = this,
|
activity = this,
|
||||||
context = this,
|
context = this,
|
||||||
@@ -159,6 +161,52 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bridge Wi‑Fi Aware callbacks into ChatViewModel (reusing BLE delegate methods)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
com.bitchat.android.wifiaware.WifiAwareController.running.collect { running ->
|
||||||
|
val svc = com.bitchat.android.wifiaware.WifiAwareController.getService()
|
||||||
|
if (running && svc != null) {
|
||||||
|
svc.delegate = object : com.bitchat.android.wifiaware.WifiAwareMeshDelegate {
|
||||||
|
override fun didReceiveMessage(message: com.bitchat.android.model.BitchatMessage) {
|
||||||
|
if (message.isPrivate) {
|
||||||
|
message.senderPeerID?.let { pid -> com.bitchat.android.services.AppStateStore.addPrivateMessage(pid, message) }
|
||||||
|
} else if (message.channel != null) {
|
||||||
|
com.bitchat.android.services.AppStateStore.addChannelMessage(message.channel, message)
|
||||||
|
} else {
|
||||||
|
com.bitchat.android.services.AppStateStore.addPublicMessage(message)
|
||||||
|
}
|
||||||
|
chatViewModel.didReceiveMessage(message)
|
||||||
|
}
|
||||||
|
override fun didUpdatePeerList(peers: List<String>) {
|
||||||
|
chatViewModel.didUpdatePeerList(peers)
|
||||||
|
}
|
||||||
|
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
||||||
|
chatViewModel.didReceiveChannelLeave(channel, fromPeer)
|
||||||
|
}
|
||||||
|
override fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) {
|
||||||
|
chatViewModel.didReceiveDeliveryAck(messageID, recipientPeerID)
|
||||||
|
}
|
||||||
|
override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) {
|
||||||
|
chatViewModel.didReceiveReadReceipt(messageID, recipientPeerID)
|
||||||
|
}
|
||||||
|
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
||||||
|
return chatViewModel.decryptChannelMessage(encryptedContent, channel)
|
||||||
|
}
|
||||||
|
override fun getNickname(): String? {
|
||||||
|
return chatViewModel.getNickname()
|
||||||
|
}
|
||||||
|
override fun isFavorite(peerID: String): Boolean {
|
||||||
|
return try {
|
||||||
|
com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(peerID)?.isMutual == true
|
||||||
|
} catch (_: Exception) { false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only start onboarding process if we're in the initial CHECKING state
|
// Only start onboarding process if we're in the initial CHECKING state
|
||||||
// This prevents restarting onboarding on configuration changes
|
// This prevents restarting onboarding on configuration changes
|
||||||
@@ -347,6 +395,12 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
bluetoothStatusManager.logBluetoothStatus()
|
bluetoothStatusManager.logBluetoothStatus()
|
||||||
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
|
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
|
||||||
|
|
||||||
|
val bleRequired = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||||||
|
if (!bleRequired) {
|
||||||
|
// Skip BLE checks entirely when BLE is disabled in debug settings
|
||||||
|
checkLocationAndProceed()
|
||||||
|
return
|
||||||
|
}
|
||||||
when (mainViewModel.bluetoothStatus.value) {
|
when (mainViewModel.bluetoothStatus.value) {
|
||||||
BluetoothStatus.ENABLED -> {
|
BluetoothStatus.ENABLED -> {
|
||||||
// Bluetooth is enabled, check location services next
|
// Bluetooth is enabled, check location services next
|
||||||
@@ -513,8 +567,9 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
else -> BatteryOptimizationStatus.ENABLED
|
else -> BatteryOptimizationStatus.ENABLED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bleRequired2 = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||||||
when {
|
when {
|
||||||
currentBluetoothStatus != BluetoothStatus.ENABLED -> {
|
bleRequired2 && currentBluetoothStatus != BluetoothStatus.ENABLED -> {
|
||||||
// Bluetooth still disabled, but now we have permissions to enable it
|
// Bluetooth still disabled, but now we have permissions to enable it
|
||||||
Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.")
|
Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.")
|
||||||
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
|
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
} catch (_: Exception) { 0.01 }
|
} catch (_: Exception) { 0.01 }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Register as shared instance for Wi-Fi Aware transport
|
||||||
|
com.bitchat.android.service.MeshServiceHolder.setGossipManager(gossipSyncManager)
|
||||||
|
|
||||||
// Wire sync manager delegate
|
// Wire sync manager delegate
|
||||||
gossipSyncManager.delegate = object : GossipSyncManager.Delegate {
|
gossipSyncManager.delegate = object : GossipSyncManager.Delegate {
|
||||||
@@ -268,7 +271,8 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
override fun sendPacket(packet: BitchatPacket) {
|
override fun sendPacket(packet: BitchatPacket) {
|
||||||
// Sign the packet before broadcasting
|
// Sign the packet before broadcasting
|
||||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
val routed = RoutedPacket(signedPacket)
|
||||||
|
connectionManager.broadcastPacket(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun relayPacket(routed: RoutedPacket) {
|
override fun relayPacket(routed: RoutedPacket) {
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ class OnboardingCoordinator(
|
|||||||
return when {
|
return when {
|
||||||
permission.contains("BLUETOOTH") -> "Bluetooth/Nearby Devices"
|
permission.contains("BLUETOOTH") -> "Bluetooth/Nearby Devices"
|
||||||
permission.contains("LOCATION") -> "Location (for Bluetooth scanning)"
|
permission.contains("LOCATION") -> "Location (for Bluetooth scanning)"
|
||||||
|
permission.contains("NEARBY_WIFI") -> "Nearby Wi‑Fi Devices (for Wi‑Fi Aware)"
|
||||||
permission.contains("NOTIFICATION") -> "Notifications"
|
permission.contains("NOTIFICATION") -> "Notifications"
|
||||||
else -> permission.substringAfterLast(".")
|
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.Power
|
||||||
import androidx.compose.material.icons.filled.Mic
|
import androidx.compose.material.icons.filled.Mic
|
||||||
import androidx.compose.material.icons.filled.Security
|
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.Settings
|
||||||
import androidx.compose.material.icons.filled.Warning
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@@ -242,6 +243,7 @@ private fun getPermissionIcon(permissionType: PermissionType): ImageVector {
|
|||||||
PermissionType.PRECISE_LOCATION -> Icons.Filled.LocationOn
|
PermissionType.PRECISE_LOCATION -> Icons.Filled.LocationOn
|
||||||
PermissionType.MICROPHONE -> Icons.Filled.Mic
|
PermissionType.MICROPHONE -> Icons.Filled.Mic
|
||||||
PermissionType.NOTIFICATIONS -> Icons.Filled.Notifications
|
PermissionType.NOTIFICATIONS -> Icons.Filled.Notifications
|
||||||
|
PermissionType.WIFI_AWARE -> Icons.Filled.Wifi
|
||||||
PermissionType.BATTERY_OPTIMIZATION -> Icons.Filled.Power
|
PermissionType.BATTERY_OPTIMIZATION -> Icons.Filled.Power
|
||||||
PermissionType.OTHER -> Icons.Filled.Settings
|
PermissionType.OTHER -> Icons.Filled.Settings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ class PermissionManager(private val context: Context) {
|
|||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
))
|
))
|
||||||
|
|
||||||
|
// Wi‑Fi 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
|
// Notification permission intentionally excluded to keep it optional
|
||||||
|
|
||||||
return permissions
|
return permissions
|
||||||
@@ -177,6 +182,20 @@ class PermissionManager(private val context: Context) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wi‑Fi 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 Wi‑Fi Aware to discover and connect to nearby bitchat users over Wi‑Fi.",
|
||||||
|
permissions = wifiAwarePermissions,
|
||||||
|
isGranted = wifiAwarePermissions.all { isPermissionGranted(it) },
|
||||||
|
systemDescription = "Allow bitchat to discover nearby Wi‑Fi devices"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Notifications category (if applicable)
|
// Notifications category (if applicable)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
categories.add(
|
categories.add(
|
||||||
@@ -262,6 +281,7 @@ enum class PermissionType(val nameValue: String) {
|
|||||||
PRECISE_LOCATION("Precise Location"),
|
PRECISE_LOCATION("Precise Location"),
|
||||||
MICROPHONE("Microphone"),
|
MICROPHONE("Microphone"),
|
||||||
NOTIFICATIONS("Notifications"),
|
NOTIFICATIONS("Notifications"),
|
||||||
|
WIFI_AWARE("Wi‑Fi Aware"),
|
||||||
BATTERY_OPTIMIZATION("Battery Optimization"),
|
BATTERY_OPTIMIZATION("Battery Optimization"),
|
||||||
OTHER("Other")
|
OTHER("Other")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import com.bitchat.android.mesh.BluetoothMeshService
|
|||||||
*/
|
*/
|
||||||
object MeshServiceHolder {
|
object MeshServiceHolder {
|
||||||
private const val TAG = "MeshServiceHolder"
|
private const val TAG = "MeshServiceHolder"
|
||||||
|
@Volatile
|
||||||
|
var sharedGossipSyncManager: com.bitchat.android.sync.GossipSyncManager? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setGossipManager(mgr: com.bitchat.android.sync.GossipSyncManager) { sharedGossipSyncManager = mgr }
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var meshService: BluetoothMeshService? = null
|
var meshService: BluetoothMeshService? = null
|
||||||
private set
|
private set
|
||||||
|
|||||||
@@ -72,9 +72,15 @@ class MessageRouter private constructor(
|
|||||||
|
|
||||||
val hasMesh = mesh.getPeerInfo(toPeerID)?.isConnected == true
|
val hasMesh = mesh.getPeerInfo(toPeerID)?.isConnected == true
|
||||||
val hasEstablished = mesh.hasEstablishedSession(toPeerID)
|
val hasEstablished = mesh.hasEstablishedSession(toPeerID)
|
||||||
|
// Check Wi‑Fi Aware availability as a secondary transport
|
||||||
|
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
||||||
|
val hasAware = try { aware?.getPeerInfo(toPeerID)?.isConnected == true && aware.hasEstablishedSession(toPeerID) } catch (_: Exception) { false }
|
||||||
if (hasMesh && hasEstablished) {
|
if (hasMesh && hasEstablished) {
|
||||||
Log.d(TAG, "Routing PM via mesh to ${toPeerID} msg_id=${messageID.take(8)}…")
|
Log.d(TAG, "Routing PM via mesh to ${toPeerID} msg_id=${messageID.take(8)}…")
|
||||||
mesh.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
|
mesh.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
|
||||||
|
} else if (hasAware) {
|
||||||
|
Log.d(TAG, "Routing PM via Wi‑Fi Aware to ${toPeerID} msg_id=${messageID.take(8)}…")
|
||||||
|
aware?.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
|
||||||
} else if (canSendViaNostr(toPeerID)) {
|
} else if (canSendViaNostr(toPeerID)) {
|
||||||
Log.d(TAG, "Routing PM via Nostr to ${toPeerID.take(32)}… msg_id=${messageID.take(8)}…")
|
Log.d(TAG, "Routing PM via Nostr to ${toPeerID.take(32)}… msg_id=${messageID.take(8)}…")
|
||||||
nostr.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
|
nostr.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
|
||||||
@@ -83,14 +89,21 @@ class MessageRouter private constructor(
|
|||||||
val q = outbox.getOrPut(toPeerID) { mutableListOf() }
|
val q = outbox.getOrPut(toPeerID) { mutableListOf() }
|
||||||
q.add(Triple(content, recipientNickname, messageID))
|
q.add(Triple(content, recipientNickname, messageID))
|
||||||
Log.d(TAG, "Initiating noise handshake after queueing PM for ${toPeerID.take(8)}…")
|
Log.d(TAG, "Initiating noise handshake after queueing PM for ${toPeerID.take(8)}…")
|
||||||
mesh.initiateNoiseHandshake(toPeerID)
|
if (hasMesh) mesh.initiateNoiseHandshake(toPeerID) else aware?.initiateNoiseHandshake(toPeerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendReadReceipt(receipt: ReadReceipt, toPeerID: String) {
|
fun sendReadReceipt(receipt: ReadReceipt, toPeerID: String) {
|
||||||
if ((mesh.getPeerInfo(toPeerID)?.isConnected == true) && mesh.hasEstablishedSession(toPeerID)) {
|
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
||||||
|
val viaMesh = (mesh.getPeerInfo(toPeerID)?.isConnected == true) && mesh.hasEstablishedSession(toPeerID)
|
||||||
|
val viaAware = try { aware?.getPeerInfo(toPeerID)?.isConnected == true && aware.hasEstablishedSession(toPeerID) } catch (_: Exception) { false }
|
||||||
|
if (viaMesh) {
|
||||||
Log.d(TAG, "Routing READ via mesh to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
|
Log.d(TAG, "Routing READ via mesh to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
|
||||||
mesh.sendReadReceipt(receipt.originalMessageID, toPeerID, mesh.getPeerNicknames()[toPeerID] ?: mesh.myPeerID)
|
mesh.sendReadReceipt(receipt.originalMessageID, toPeerID, mesh.getPeerNicknames()[toPeerID] ?: mesh.myPeerID)
|
||||||
|
} else if (viaAware) {
|
||||||
|
Log.d(TAG, "Routing READ via Wi‑Fi Aware to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
|
||||||
|
val me = try { aware?.myPeerID } catch (_: Exception) { null }
|
||||||
|
aware?.sendReadReceipt(receipt.originalMessageID, toPeerID, me ?: "")
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Routing READ via Nostr to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
|
Log.d(TAG, "Routing READ via Nostr to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
|
||||||
nostr.sendReadReceipt(receipt, toPeerID)
|
nostr.sendReadReceipt(receipt, toPeerID)
|
||||||
|
|||||||
@@ -441,8 +441,9 @@ class ChatViewModel(
|
|||||||
state.getNicknameValue()
|
state.getNicknameValue()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Default: route via mesh
|
// Default: route via mesh + Wi‑Fi Aware
|
||||||
meshService.sendMessage(messageContent, mentions, channel)
|
meshService.sendMessage(messageContent, mentions, channel)
|
||||||
|
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(messageContent, mentions, channel) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -512,19 +513,23 @@ class ChatViewModel(
|
|||||||
state.getNicknameValue(),
|
state.getNicknameValue(),
|
||||||
meshService.myPeerID,
|
meshService.myPeerID,
|
||||||
onEncryptedPayload = { encryptedData ->
|
onEncryptedPayload = { encryptedData ->
|
||||||
// This would need proper mesh service integration
|
// Send encrypted payload announcement over both transports for reachability
|
||||||
meshService.sendMessage(content, mentions, currentChannelValue)
|
meshService.sendMessage(content, mentions, currentChannelValue)
|
||||||
|
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
|
||||||
},
|
},
|
||||||
onFallback = {
|
onFallback = {
|
||||||
meshService.sendMessage(content, mentions, currentChannelValue)
|
meshService.sendMessage(content, mentions, currentChannelValue)
|
||||||
|
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
meshService.sendMessage(content, mentions, currentChannelValue)
|
meshService.sendMessage(content, mentions, currentChannelValue)
|
||||||
|
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
messageManager.addMessage(message)
|
messageManager.addMessage(message)
|
||||||
meshService.sendMessage(content, mentions, null)
|
meshService.sendMessage(content, mentions, null)
|
||||||
|
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, null) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -647,16 +652,23 @@ class ChatViewModel(
|
|||||||
val fingerprints = privateChatManager.getAllPeerFingerprints()
|
val fingerprints = privateChatManager.getAllPeerFingerprints()
|
||||||
state.setPeerFingerprints(fingerprints)
|
state.setPeerFingerprints(fingerprints)
|
||||||
|
|
||||||
val nicknames = meshService.getPeerNicknames()
|
// Merge nicknames from BLE and Wi‑Fi Aware to display names for all peers
|
||||||
state.setPeerNicknames(nicknames)
|
val bleNick = meshService.getPeerNicknames()
|
||||||
|
val awareNickRaw = try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerNicknamesMap() } catch (_: Exception) { null }
|
||||||
|
val mergedNick = if (awareNickRaw != null) bleNick + awareNickRaw.filter { it.value != null }.mapValues { it.value!! }.filterKeys { it !in bleNick || bleNick[it].isNullOrBlank() } else bleNick
|
||||||
|
state.setPeerNicknames(mergedNick)
|
||||||
|
|
||||||
val rssiValues = meshService.getPeerRSSI()
|
val rssiValues = meshService.getPeerRSSI()
|
||||||
state.setPeerRSSI(rssiValues)
|
val awareRssi = try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerRSSI() } catch (_: Exception) { null }
|
||||||
|
val mergedRssi = if (awareRssi != null) rssiValues + awareRssi.filterKeys { it !in rssiValues } else rssiValues
|
||||||
|
state.setPeerRSSI(mergedRssi)
|
||||||
|
|
||||||
// Update directness per peer (driven by PeerManager state)
|
// Update directness per peer (driven by PeerManager state)
|
||||||
try {
|
try {
|
||||||
val directMap = state.getConnectedPeersValue().associateWith { pid ->
|
val directMap = state.getConnectedPeersValue().associateWith { pid ->
|
||||||
meshService.getPeerInfo(pid)?.isDirectConnection == true
|
val ble = meshService.getPeerInfo(pid)?.isDirectConnection == true
|
||||||
|
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerInfo(pid)?.isDirectConnection == true } catch (_: Exception) { false }
|
||||||
|
ble || aware
|
||||||
}
|
}
|
||||||
state.setPeerDirect(directMap)
|
state.setPeerDirect(directMap)
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ class MediaSendingManager(
|
|||||||
|
|
||||||
Log.d(TAG, "📤 Calling meshService.sendFilePrivate to $toPeerID")
|
Log.d(TAG, "📤 Calling meshService.sendFilePrivate to $toPeerID")
|
||||||
meshService.sendFilePrivate(toPeerID, filePacket)
|
meshService.sendFilePrivate(toPeerID, filePacket)
|
||||||
|
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendFilePrivate(toPeerID, filePacket) } catch (_: Exception) {}
|
||||||
Log.d(TAG, "✅ File send completed successfully")
|
Log.d(TAG, "✅ File send completed successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +265,7 @@ class MediaSendingManager(
|
|||||||
|
|
||||||
Log.d(TAG, "📤 Calling meshService.sendFileBroadcast")
|
Log.d(TAG, "📤 Calling meshService.sendFileBroadcast")
|
||||||
meshService.sendFileBroadcast(filePacket)
|
meshService.sendFileBroadcast(filePacket)
|
||||||
|
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendFileBroadcast(filePacket) } catch (_: Exception) {}
|
||||||
Log.d(TAG, "✅ File broadcast completed successfully")
|
Log.d(TAG, "✅ File broadcast completed successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,10 @@ class MeshDelegateHandler(
|
|||||||
|
|
||||||
override fun didUpdatePeerList(peers: List<String>) {
|
override fun didUpdatePeerList(peers: List<String>) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
state.setConnectedPeers(peers)
|
// Merge peers from multiple transports to avoid flapping
|
||||||
|
val current = state.getConnectedPeersValue().toMutableSet()
|
||||||
|
current.addAll(peers)
|
||||||
|
state.setConnectedPeers(current.toList())
|
||||||
state.setIsConnected(peers.isNotEmpty())
|
state.setIsConnected(peers.isNotEmpty())
|
||||||
notificationManager.showActiveUserNotification(peers)
|
notificationManager.showActiveUserNotification(peers)
|
||||||
// Flush router outbox for any peers that just connected (and their noiseHex aliases)
|
// Flush router outbox for any peers that just connected (and their noiseHex aliases)
|
||||||
|
|||||||
@@ -576,9 +576,26 @@ private fun PeerItem(
|
|||||||
tint = Color.Gray
|
tint = Color.Gray
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
val awareConnected by com.bitchat.android.wifiaware.WifiAwareController.connectedPeers.collectAsState()
|
||||||
|
val awareDiscovered by com.bitchat.android.wifiaware.WifiAwareController.discoveredPeers.collectAsState()
|
||||||
|
val isWifiDirect = awareConnected.containsKey(peerID)
|
||||||
|
val isBleDirect = isDirect
|
||||||
|
val icon = when {
|
||||||
|
isWifiDirect -> Icons.Filled.Wifi
|
||||||
|
isBleDirect -> Icons.Outlined.SettingsInputAntenna
|
||||||
|
// Routed: show Route icon; optionally prefer Wi‑Fi Aware if discovered there
|
||||||
|
awareDiscovered.contains(peerID) -> Icons.Filled.WifiTethering
|
||||||
|
else -> Icons.Filled.Route
|
||||||
|
}
|
||||||
|
val cd = when {
|
||||||
|
isWifiDirect -> "Direct Wi‑Fi Aware"
|
||||||
|
isBleDirect -> "Direct Bluetooth"
|
||||||
|
awareDiscovered.contains(peerID) -> "Routed over Wi‑Fi"
|
||||||
|
else -> "Routed"
|
||||||
|
}
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isDirect) Icons.Outlined.SettingsInputAntenna else Icons.Filled.Route,
|
imageVector = icon,
|
||||||
contentDescription = if (isDirect) "Direct Bluetooth" else "Routed",
|
contentDescription = cd,
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
tint = colorScheme.onSurface.copy(alpha = 0.8f)
|
tint = colorScheme.onSurface.copy(alpha = 0.8f)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ object DebugPreferenceManager {
|
|||||||
// GCS keys (no migration/back-compat)
|
// GCS keys (no migration/back-compat)
|
||||||
private const val KEY_GCS_MAX_BYTES = "gcs_max_filter_bytes"
|
private const val KEY_GCS_MAX_BYTES = "gcs_max_filter_bytes"
|
||||||
private const val KEY_GCS_FPR = "gcs_filter_fpr_percent"
|
private const val KEY_GCS_FPR = "gcs_filter_fpr_percent"
|
||||||
// Removed: persistent notification toggle is now governed by MeshServicePreferences.isBackgroundEnabled
|
// 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
|
private lateinit var prefs: SharedPreferences
|
||||||
|
|
||||||
@@ -102,5 +105,25 @@ object DebugPreferenceManager {
|
|||||||
if (ready()) prefs.edit().putLong(KEY_GCS_FPR, java.lang.Double.doubleToRawLongBits(value)).apply()
|
if (ready()) prefs.edit().putLong(KEY_GCS_FPR, java.lang.Double.doubleToRawLongBits(value)).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
// No longer storing persistent notification in debug prefs.
|
// 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,17 @@ class DebugSettingsManager private constructor() {
|
|||||||
private val _packetRelayEnabled = MutableStateFlow(true)
|
private val _packetRelayEnabled = MutableStateFlow(true)
|
||||||
val packetRelayEnabled: StateFlow<Boolean> = _packetRelayEnabled.asStateFlow()
|
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()
|
||||||
|
|
||||||
|
// Master transport toggles
|
||||||
|
private val _wifiAwareVerbose = MutableStateFlow(false)
|
||||||
|
val wifiAwareVerbose: StateFlow<Boolean> = _wifiAwareVerbose.asStateFlow()
|
||||||
|
|
||||||
// Visibility of the debug sheet; gates heavy work
|
// Visibility of the debug sheet; gates heavy work
|
||||||
private val _debugSheetVisible = MutableStateFlow(false)
|
private val _debugSheetVisible = MutableStateFlow(false)
|
||||||
val debugSheetVisible: StateFlow<Boolean> = _debugSheetVisible.asStateFlow()
|
val debugSheetVisible: StateFlow<Boolean> = _debugSheetVisible.asStateFlow()
|
||||||
@@ -59,6 +70,10 @@ class DebugSettingsManager private constructor() {
|
|||||||
_maxConnectionsOverall.value = DebugPreferenceManager.getMaxConnectionsOverall(8)
|
_maxConnectionsOverall.value = DebugPreferenceManager.getMaxConnectionsOverall(8)
|
||||||
_maxServerConnections.value = DebugPreferenceManager.getMaxConnectionsServer(8)
|
_maxServerConnections.value = DebugPreferenceManager.getMaxConnectionsServer(8)
|
||||||
_maxClientConnections.value = DebugPreferenceManager.getMaxConnectionsClient(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) {
|
} catch (_: Exception) {
|
||||||
// Preferences not ready yet; keep defaults. They will be applied on first change.
|
// Preferences not ready yet; keep defaults. They will be applied on first change.
|
||||||
}
|
}
|
||||||
@@ -262,6 +277,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) "🟢 Wi‑Fi Aware enabled" else "🔴 Wi‑Fi 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) "🔊 Wi‑Fi Aware verbose logging enabled" else "🔇 Wi‑Fi Aware verbose logging disabled"))
|
||||||
|
}
|
||||||
|
|
||||||
fun setMaxConnectionsOverall(value: Int) {
|
fun setMaxConnectionsOverall(value: Int) {
|
||||||
val clamped = value.coerceIn(1, 32)
|
val clamped = value.coerceIn(1, 32)
|
||||||
DebugPreferenceManager.setMaxConnectionsOverall(clamped)
|
DebugPreferenceManager.setMaxConnectionsOverall(clamped)
|
||||||
@@ -319,6 +355,16 @@ class DebugSettingsManager private constructor() {
|
|||||||
fun updateConnectedDevices(devices: List<ConnectedDevice>) {
|
fun updateConnectedDevices(devices: List<ConnectedDevice>) {
|
||||||
_connectedDevices.value = devices
|
_connectedDevices.value = devices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wi‑Fi 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) {
|
fun updateRelayStats(stats: PacketRelayStats) {
|
||||||
_relayStats.value = stats
|
_relayStats.value = stats
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Bluetooth
|
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.BugReport
|
||||||
import androidx.compose.material.icons.filled.Cancel
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
import androidx.compose.material.icons.filled.Devices
|
import androidx.compose.material.icons.filled.Devices
|
||||||
@@ -50,7 +52,7 @@ fun DebugSettingsSheet(
|
|||||||
val verboseLogging by manager.verboseLoggingEnabled.collectAsState()
|
val verboseLogging by manager.verboseLoggingEnabled.collectAsState()
|
||||||
val gattServerEnabled by manager.gattServerEnabled.collectAsState()
|
val gattServerEnabled by manager.gattServerEnabled.collectAsState()
|
||||||
val gattClientEnabled by manager.gattClientEnabled.collectAsState()
|
val gattClientEnabled by manager.gattClientEnabled.collectAsState()
|
||||||
val packetRelayEnabled by manager.packetRelayEnabled.collectAsState()
|
val packetRelayed by manager.packetRelayEnabled.collectAsState()
|
||||||
val maxOverall by manager.maxConnectionsOverall.collectAsState()
|
val maxOverall by manager.maxConnectionsOverall.collectAsState()
|
||||||
val maxServer by manager.maxServerConnections.collectAsState()
|
val maxServer by manager.maxServerConnections.collectAsState()
|
||||||
val maxClient by manager.maxClientConnections.collectAsState()
|
val maxClient by manager.maxClientConnections.collectAsState()
|
||||||
@@ -62,6 +64,12 @@ fun DebugSettingsSheet(
|
|||||||
val gcsMaxBytes by manager.gcsMaxBytes.collectAsState()
|
val gcsMaxBytes by manager.gcsMaxBytes.collectAsState()
|
||||||
val gcsFpr by manager.gcsFprPercent.collectAsState()
|
val gcsFpr by manager.gcsFprPercent.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
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()
|
||||||
// Persistent notification is now controlled solely by MeshServicePreferences.isBackgroundEnabled
|
// Persistent notification is now controlled solely by MeshServicePreferences.isBackgroundEnabled
|
||||||
|
|
||||||
// Push live connected devices from mesh service whenever sheet is visible
|
// Push live connected devices from mesh service whenever sheet is visible
|
||||||
@@ -86,6 +94,15 @@ fun DebugSettingsSheet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
manager.updateConnectedDevices(devices)
|
manager.updateConnectedDevices(devices)
|
||||||
|
// Also surface Wi‑Fi 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)
|
kotlinx.coroutines.delay(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,6 +230,46 @@ fun DebugSettingsSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transport toggles (BLE + Wi‑Fi 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("Wi‑Fi 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("Wi‑Fi Aware verbose", fontFamily = FontFamily.Monospace, modifier = Modifier.weight(1f))
|
||||||
|
Switch(checked = wifiAwareVerbose, onCheckedChange = { manager.setWifiAwareVerbose(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Packet relay controls and stats
|
// Packet relay controls and stats
|
||||||
item {
|
item {
|
||||||
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
||||||
@@ -223,7 +280,7 @@ fun DebugSettingsSheet(
|
|||||||
Icon(Icons.Filled.PowerSettingsNew, contentDescription = null, tint = Color(0xFFFF9500))
|
Icon(Icons.Filled.PowerSettingsNew, contentDescription = null, tint = Color(0xFFFF9500))
|
||||||
Text(stringResource(R.string.debug_packet_relay), fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
Text(stringResource(R.string.debug_packet_relay), fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
Switch(checked = packetRelayEnabled, onCheckedChange = { manager.setPacketRelayEnabled(it) })
|
Switch(checked = packetRelayed, onCheckedChange = { manager.setPacketRelayEnabled(it) })
|
||||||
}
|
}
|
||||||
// Removed aggregate labels; we will show per-direction compact labels below titles
|
// Removed aggregate labels; we will show per-direction compact labels below titles
|
||||||
// Toggle: overall vs per-connection vs per-peer
|
// Toggle: overall vs per-connection vs per-peer
|
||||||
@@ -472,6 +529,43 @@ fun DebugSettingsSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wi‑Fi 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("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))
|
||||||
|
}
|
||||||
|
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
|
// Connected devices
|
||||||
item {
|
item {
|
||||||
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
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() // 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() {
|
||||||
|
if (_running.value) return
|
||||||
|
val ctx = appContext ?: 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) {}
|
||||||
|
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) {}
|
||||||
|
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("Wi‑Fi 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("Wi‑Fi 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("Wi‑Fi Aware stopped")) } catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getService(): WifiAwareMeshService? = service
|
||||||
|
|
||||||
|
// Optional bridge to BLE mesh for cross-transport relaying
|
||||||
|
@Volatile private var bleMesh: com.bitchat.android.mesh.BluetoothMeshService? = null
|
||||||
|
fun setBleMeshService(svc: com.bitchat.android.mesh.BluetoothMeshService) { bleMesh = svc }
|
||||||
|
fun getBleMeshService(): com.bitchat.android.mesh.BluetoothMeshService? = bleMesh
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user