Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand All @@ -19,6 +20,12 @@

<!-- Notification permissions -->
<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" />

<!-- Microphone for voice notes -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
Expand All @@ -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 Wi‑Fi Aware (optional) -->
<uses-feature android:name="android.hardware.wifi.aware" android:required="false" />

<application
android:name=".BitchatApplication"
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/bitchat/android/BitchatApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class BitchatApplication : Application() {
// Initialize debug preference manager (persists debug toggles)
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) { }

// TorManager already initialized above
}
}
50 changes: 49 additions & 1 deletion app/src/main/java/com/bitchat/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ class MainActivity : OrientationAwareActivity() {
permissionManager = PermissionManager(this)
// Initialize core mesh service first
meshService = BluetoothMeshService(this)
// Expose BLE mesh to Wi‑Fi Aware controller for cross-transport relays
try { com.bitchat.android.wifiaware.WifiAwareController.setBleMeshService(meshService) } catch (_: Exception) { }
bluetoothStatusManager = BluetoothStatusManager(
activity = this,
context = this,
Expand Down Expand Up @@ -120,6 +122,45 @@ 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) {
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
// This prevents restarting onboarding on configuration changes
Expand Down Expand Up @@ -308,6 +349,12 @@ class MainActivity : OrientationAwareActivity() {
bluetoothStatusManager.logBluetoothStatus()
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) {
BluetoothStatus.ENABLED -> {
// Bluetooth is enabled, check location services next
Expand Down Expand Up @@ -474,8 +521,9 @@ class MainActivity : OrientationAwareActivity() {
else -> BatteryOptimizationStatus.ENABLED
}

val bleRequired2 = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
when {
currentBluetoothStatus != BluetoothStatus.ENABLED -> {
bleRequired2 && currentBluetoothStatus != BluetoothStatus.ENABLED -> {
// Bluetooth still disabled, but now we have permissions to enable it
Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.")
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ class BluetoothMeshService(private val context: Context) {
override fun sendPacket(packet: BitchatPacket) {
// Sign the packet before broadcasting
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
val routed = RoutedPacket(signedPacket)
connectionManager.broadcastPacket(routed)
}

override fun relayPacket(routed: RoutedPacket) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 Wi‑Fi Devices (for Wi‑Fi Aware)"
permission.contains("NOTIFICATION") -> "Notifications"
else -> permission.substringAfterLast(".")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ class PermissionManager(private val context: Context) {
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

return permissions
Expand Down Expand Up @@ -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)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
categories.add(
Expand Down Expand Up @@ -262,6 +281,7 @@ enum class PermissionType(val nameValue: String) {
PRECISE_LOCATION("Precise Location"),
MICROPHONE("Microphone"),
NOTIFICATIONS("Notifications"),
WIFI_AWARE("Wi‑Fi Aware"),
BATTERY_OPTIMIZATION("Battery Optimization"),
OTHER("Other")
}
17 changes: 15 additions & 2 deletions app/src/main/java/com/bitchat/android/services/MessageRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,15 @@ class MessageRouter private constructor(

val hasMesh = mesh.getPeerInfo(toPeerID)?.isConnected == true
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) {
Log.d(TAG, "Routing PM via mesh to ${toPeerID} msg_id=${messageID.take(8)}…")
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)) {
Log.d(TAG, "Routing PM via Nostr to ${toPeerID.take(32)}… msg_id=${messageID.take(8)}…")
nostr.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
Expand All @@ -83,14 +89,21 @@ class MessageRouter private constructor(
val q = outbox.getOrPut(toPeerID) { mutableListOf() }
q.add(Triple(content, recipientNickname, messageID))
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) {
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)}…")
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 {
Log.d(TAG, "Routing READ via Nostr to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
nostr.sendReadReceipt(receipt, toPeerID)
Expand Down
24 changes: 18 additions & 6 deletions app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,9 @@ class ChatViewModel(
state.getNicknameValue()
)
} else {
// Default: route via mesh
// Default: route via mesh + Wi‑Fi Aware
meshService.sendMessage(messageContent, mentions, channel)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(messageContent, mentions, channel) } catch (_: Exception) {}
}
})
return
Expand Down Expand Up @@ -477,19 +478,23 @@ class ChatViewModel(
state.getNicknameValue(),
meshService.myPeerID,
onEncryptedPayload = { encryptedData ->
// This would need proper mesh service integration
// Send encrypted payload announcement over both transports for reachability
meshService.sendMessage(content, mentions, currentChannelValue)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
},
onFallback = {
meshService.sendMessage(content, mentions, currentChannelValue)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
}
)
} else {
meshService.sendMessage(content, mentions, currentChannelValue)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
}
} else {
messageManager.addMessage(message)
meshService.sendMessage(content, mentions, null)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, null) } catch (_: Exception) {}
}
}
}
Expand Down Expand Up @@ -612,16 +617,23 @@ class ChatViewModel(
val fingerprints = privateChatManager.getAllPeerFingerprints()
state.setPeerFingerprints(fingerprints)

val nicknames = meshService.getPeerNicknames()
state.setPeerNicknames(nicknames)
// Merge nicknames from BLE and Wi‑Fi Aware to display names for all peers
val bleNick = meshService.getPeerNicknames()
val awareNick = try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerNicknames() } catch (_: Exception) { null }
val mergedNick = if (awareNick != null) bleNick + awareNick.filterKeys { it !in bleNick || bleNick[it].isNullOrBlank() } else bleNick
state.setPeerNicknames(mergedNick)

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)
try {
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)
} catch (_: Exception) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ class MediaSendingManager(

Log.d(TAG, "📤 Calling meshService.sendFilePrivate to $toPeerID")
meshService.sendFilePrivate(toPeerID, filePacket)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendFilePrivate(toPeerID, filePacket) } catch (_: Exception) {}
Log.d(TAG, "✅ File send completed successfully")
}

Expand Down Expand Up @@ -264,6 +265,7 @@ class MediaSendingManager(

Log.d(TAG, "📤 Calling meshService.sendFileBroadcast")
meshService.sendFileBroadcast(filePacket)
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendFileBroadcast(filePacket) } catch (_: Exception) {}
Log.d(TAG, "✅ File broadcast completed successfully")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ class MeshDelegateHandler(

override fun didUpdatePeerList(peers: List<String>) {
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())
notificationManager.showActiveUserNotification(peers)
// Flush router outbox for any peers that just connected (and their noiseHex aliases)
Expand Down
Loading
Loading