Dev e19ed03755 Merge remote-tracking branch 'origin/main' into codex/nostr-double-ratchet
# Conflicts:
#	app/src/main/java/com/bitchat/android/BitchatApplication.kt
#	app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt
#	app/src/main/java/com/bitchat/android/nostr/NostrDirectMessageHandler.kt
#	app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt
#	app/src/main/java/com/bitchat/android/nostr/NostrSubscriptionManager.kt
#	app/src/main/java/com/bitchat/android/service/AppShutdownCoordinator.kt
#	app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
#	app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt
2026-07-28 02:29:44 +03:00

533 lines
21 KiB
Kotlin

package com.bitchat.android.mesh
import android.content.Context
import android.util.Log
import com.bitchat.android.favorites.FavoriteControlMessage
import com.bitchat.android.model.BitchatFilePacket
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.NdrFeatureGate
import com.bitchat.android.noise.NoiseSession
import com.bitchat.android.wifiaware.WifiAwareController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/**
* Feature-facing mesh service that hides local transport selection from the rest of the app.
*
* BLE remains the canonical origin for broadcast packets when it is enabled so existing BLE mesh
* behavior and bridge semantics stay intact. Addressed Noise traffic is routed over whichever
* local transport already has the peer/session, falling back to a connected transport handshake.
*/
class UnifiedMeshService(
private val context: Context,
private val bluetooth: BluetoothMeshService
) : MeshService, BluetoothMeshDelegate {
companion object {
private const val TAG = "UnifiedMeshService"
private const val BLE_NDR_TRANSPORT_ID = "BLE"
}
private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val powerManager = PowerManager.getInstance(context.applicationContext)
private var announcementJob: Job? = null
override val myPeerID: String
get() = bluetooth.myPeerID
override var delegate: MeshDelegate? = null
set(value) {
field = value
refreshDelegates()
}
fun refreshDelegates() {
try { bluetooth.delegate = if (delegate != null) this else null } catch (_: Exception) { }
try { wifiService()?.delegate = if (delegate != null) this else null } catch (_: Exception) { }
}
override fun startServices() {
if (isBleEnabled()) {
try { bluetooth.startServices() } catch (e: Exception) {
Log.w(TAG, "Failed to start BLE transport: ${e.message}")
}
} else {
try { bluetooth.setBleTransportEnabled(false) } catch (_: Exception) { }
}
try { WifiAwareController.startIfPossible() } catch (e: Exception) {
Log.w(TAG, "Failed to start Wi-Fi Aware transport: ${e.message}")
}
startAnnouncementScheduler()
refreshDelegates()
}
override fun stopServices() {
announcementJob?.cancel()
announcementJob = null
try { bluetooth.stopServices() } catch (_: Exception) { }
try { WifiAwareController.stop() } catch (_: Exception) { }
}
private fun startAnnouncementScheduler() {
if (announcementJob?.isActive == true) return
announcementJob = serviceScope.launch {
powerManager.profile
.map { profile ->
profile.meshAnnouncementIntervalMs to profile.hasDirectPeers
}
.distinctUntilChanged()
.collectLatest { (intervalMs, hasRecipients) ->
if (!hasRecipients) return@collectLatest
// Connection-specific paths already send an immediate announce. Begin the
// periodic cadence after the configured interval to avoid a transition burst.
while (isActive) {
delay(intervalMs)
if (powerManager.profile.value.hasDirectPeers) sendBroadcastAnnounce()
}
}
}
}
override fun sendMessage(content: String, mentions: List<String>, channel: String?) {
when {
isBleEnabled() -> bluetooth.sendMessage(content, mentions, channel)
else -> wifiService()?.sendMessage(content, mentions, channel)
}
}
override fun sendPrivateMessage(
content: String,
recipientPeerID: String,
recipientNickname: String,
messageID: String?
) {
when {
isBleReady(recipientPeerID) -> bluetooth.sendPrivateMessage(content, recipientPeerID, recipientNickname, messageID)
isWifiReady(recipientPeerID) -> wifiService()?.sendPrivateMessage(content, recipientPeerID, recipientNickname, messageID)
isBleConnected(recipientPeerID) || (isBleEnabled() && !isWifiConnected(recipientPeerID)) ->
bluetooth.sendPrivateMessage(content, recipientPeerID, recipientNickname, messageID)
else -> wifiService()?.sendPrivateMessage(content, recipientPeerID, recipientNickname, messageID)
}
}
override fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
when {
isBleReady(recipientPeerID) -> bluetooth.sendReadReceipt(messageID, recipientPeerID, readerNickname)
isWifiReady(recipientPeerID) -> wifiService()?.sendReadReceipt(messageID, recipientPeerID, readerNickname)
}
}
override fun sendFavoriteNotification(peerID: String, isFavorite: Boolean) {
val myNpub = try {
com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(context)?.npub
} catch (_: Exception) {
null
}
val content = FavoriteControlMessage.encode(isFavorite, myNpub)
val nickname = getPeerNicknames()[peerID] ?: peerID
if (hasEstablishedSession(peerID)) {
sendPrivateMessage(content, peerID, nickname, java.util.UUID.randomUUID().toString())
}
}
override fun sendVerifyChallenge(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
when {
isBleReady(peerID) -> bluetooth.sendVerifyChallenge(peerID, noiseKeyHex, nonceA)
isWifiReady(peerID) -> wifiService()?.sendVerifyChallenge(peerID, noiseKeyHex, nonceA)
}
}
override fun sendVerifyResponse(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
when {
isBleReady(peerID) -> bluetooth.sendVerifyResponse(peerID, noiseKeyHex, nonceA)
isWifiReady(peerID) -> wifiService()?.sendVerifyResponse(peerID, noiseKeyHex, nonceA)
}
}
override fun currentNdrRoute(peerID: String, transportId: String?): NdrMeshRoute? {
if (!NdrFeatureGate.isEnabled()) return null
return when (transportId) {
null -> bluetooth.currentNdrRoute(peerID)
?: wifiService()?.currentNdrRoute(peerID)
BLE_NDR_TRANSPORT_ID ->
bluetooth.currentNdrRoute(peerID, transportId)
else -> wifiService()?.currentNdrRoute(peerID, transportId)
}
}
override fun sendNdrEvent(
route: NdrMeshRoute,
payload: String,
isStillAuthorized: () -> Boolean,
completion: (admitted: Boolean) -> Unit
) {
if (!NdrFeatureGate.isEnabled()) {
completion(false)
return
}
if (route.transportId == BLE_NDR_TRANSPORT_ID) {
bluetooth.sendNdrEvent(route, payload, isStillAuthorized, completion)
return
}
val wifi = wifiService()
if (wifi == null) {
completion(false)
return
}
wifi.sendNdrEvent(route, payload, isStillAuthorized, completion)
}
override fun sendFileBroadcast(file: BitchatFilePacket) {
when {
isBleEnabled() -> bluetooth.sendFileBroadcast(file)
else -> wifiService()?.sendFileBroadcast(file)
}
}
override fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) {
when {
isBleReady(recipientPeerID) -> bluetooth.sendFilePrivate(recipientPeerID, file)
isWifiReady(recipientPeerID) -> wifiService()?.sendFilePrivate(recipientPeerID, file)
isBleConnected(recipientPeerID) || (isBleEnabled() && !isWifiConnected(recipientPeerID)) ->
bluetooth.sendFilePrivate(recipientPeerID, file)
else -> wifiService()?.sendFilePrivate(recipientPeerID, file)
}
}
override fun prepareFilePrivate(
recipientPeerID: String,
file: BitchatFilePacket,
transferId: String,
allowLegacyFallback: Boolean
): PrivateMediaPreparation {
return when {
isBleReady(recipientPeerID) -> bluetooth.prepareFilePrivate(
recipientPeerID,
file,
transferId,
allowLegacyFallback
)
isWifiReady(recipientPeerID) -> wifiService()?.prepareFilePrivate(
recipientPeerID,
file,
transferId,
allowLegacyFallback
) ?: PrivateMediaPreparation.Rejected("Wi-Fi Aware transport is unavailable")
isBleConnected(recipientPeerID) || (isBleEnabled() && !isWifiConnected(recipientPeerID)) ->
bluetooth.prepareFilePrivate(
recipientPeerID,
file,
transferId,
allowLegacyFallback
)
else -> wifiService()?.prepareFilePrivate(
recipientPeerID,
file,
transferId,
allowLegacyFallback
) ?: PrivateMediaPreparation.Rejected("No local transport is available for this peer")
}
}
override fun cancelFileTransfer(transferId: String): Boolean {
val bleCancelled = try { bluetooth.cancelFileTransfer(transferId) } catch (_: Exception) { false }
val wifiCancelled = try { wifiService()?.cancelFileTransfer(transferId) == true } catch (_: Exception) { false }
return bleCancelled || wifiCancelled
}
override fun sendBroadcastAnnounce() {
if (isBleEnabled()) {
try { bluetooth.sendBroadcastAnnounce() } catch (_: Exception) { }
}
try { wifiService()?.sendBroadcastAnnounce() } catch (_: Exception) { }
}
override fun sendAnnouncementToPeer(peerID: String) {
when {
isBleConnected(peerID) || (isBleEnabled() && !isWifiConnected(peerID)) -> bluetooth.sendAnnouncementToPeer(peerID)
else -> wifiService()?.sendAnnouncementToPeer(peerID)
}
}
override fun getPeerNicknames(): Map<String, String> {
val merged = linkedMapOf<String, String>()
try { merged.putAll(wifiService()?.getPeerNicknames().orEmpty()) } catch (_: Exception) { }
try { merged.putAll(bluetooth.getPeerNicknames()) } catch (_: Exception) { }
return merged
}
override fun getPeerRSSI(): Map<String, Int> {
val merged = linkedMapOf<String, Int>()
try { merged.putAll(wifiService()?.getPeerRSSI().orEmpty()) } catch (_: Exception) { }
try { merged.putAll(bluetooth.getPeerRSSI()) } catch (_: Exception) { }
return merged
}
override fun getActivePeerCount(): Int {
return mergedPeerIDs().filter { it != myPeerID }.distinct().size
}
override fun hasEstablishedSession(peerID: String): Boolean {
return isBleReady(peerID) || isWifiReady(peerID)
}
override fun getSessionState(peerID: String): NoiseSession.NoiseSessionState {
val bleState = try { bluetooth.getSessionState(peerID) } catch (_: Exception) { NoiseSession.NoiseSessionState.Uninitialized }
val wifiState = try { wifiService()?.getSessionState(peerID) } catch (_: Exception) { null }
return when {
bleState is NoiseSession.NoiseSessionState.Established -> bleState
wifiState is NoiseSession.NoiseSessionState.Established -> wifiState
bleState is NoiseSession.NoiseSessionState.Handshaking -> bleState
wifiState is NoiseSession.NoiseSessionState.Handshaking -> wifiState
bleState !is NoiseSession.NoiseSessionState.Uninitialized -> bleState
wifiState != null -> wifiState
else -> bleState
}
}
override fun initiateNoiseHandshake(peerID: String) {
when {
isBleConnected(peerID) -> bluetooth.initiateNoiseHandshake(peerID)
isWifiConnected(peerID) -> wifiService()?.initiateNoiseHandshake(peerID)
isBleEnabled() -> bluetooth.initiateNoiseHandshake(peerID)
else -> wifiService()?.initiateNoiseHandshake(peerID)
}
}
override fun getPeerFingerprint(peerID: String): String? {
return try { bluetooth.getPeerFingerprint(peerID) } catch (_: Exception) { null }
?: try { wifiService()?.getPeerFingerprint(peerID) } catch (_: Exception) { null }
}
override fun getPeerInfo(peerID: String): PeerInfo? {
val ble = try { bluetooth.getPeerInfo(peerID) } catch (_: Exception) { null }
val wifi = try { wifiService()?.getPeerInfo(peerID) } catch (_: Exception) { null }
return when {
ble?.isConnected == true && hasEstablishedSessionOnBluetooth(peerID) -> ble
wifi?.isConnected == true && wifiService()?.hasEstablishedSession(peerID) == true -> wifi
ble?.isConnected == true -> ble
wifi?.isConnected == true -> wifi
else -> ble ?: wifi
}
}
override fun peerSupportsAuthenticatedCapability(
peerID: String,
capability: com.bitchat.android.model.PeerCapabilities
): Boolean =
bleSupportsAuthenticatedCapability(peerID, capability) ||
wifiSupportsAuthenticatedCapability(peerID, capability)
private fun bleSupportsAuthenticatedCapability(
peerID: String,
capability: com.bitchat.android.model.PeerCapabilities
): Boolean {
return try {
isBleReady(peerID) && bluetooth.peerSupportsAuthenticatedCapability(peerID, capability)
} catch (_: Exception) {
false
}
}
private fun wifiSupportsAuthenticatedCapability(
peerID: String,
capability: com.bitchat.android.model.PeerCapabilities
): Boolean {
return try {
isWifiReady(peerID) &&
wifiService()?.peerSupportsAuthenticatedCapability(peerID, capability) == true
} catch (_: Exception) {
false
}
}
override fun updatePeerInfo(
peerID: String,
nickname: String,
noisePublicKey: ByteArray,
signingPublicKey: ByteArray,
isVerified: Boolean
): Boolean {
val bleUpdated = try {
bluetooth.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
} catch (_: Exception) {
false
}
val wifiUpdated = try {
wifiService()?.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified) == true
} catch (_: Exception) {
false
}
return bleUpdated || wifiUpdated
}
override fun getIdentityFingerprint(): String = bluetooth.getIdentityFingerprint()
override fun getStaticNoisePublicKey(): ByteArray? {
return bluetooth.getStaticNoisePublicKey() ?: wifiService()?.getStaticNoisePublicKey()
}
override fun shouldShowEncryptionIcon(peerID: String): Boolean {
return hasEstablishedSession(peerID)
}
override fun getEncryptedPeers(): List<String> {
val encrypted = linkedSetOf<String>()
try { encrypted.addAll(bluetooth.getEncryptedPeers()) } catch (_: Exception) { }
try { encrypted.addAll(wifiService()?.getEncryptedPeers().orEmpty()) } catch (_: Exception) { }
mergedPeerIDs().filterTo(encrypted) { hasEstablishedSession(it) }
return encrypted.toList()
}
override fun getDeviceAddressForPeer(peerID: String): String? {
return try { bluetooth.getDeviceAddressForPeer(peerID) } catch (_: Exception) { null }
?: try { wifiService()?.getDeviceAddressForPeer(peerID) } catch (_: Exception) { null }
}
override fun getDeviceAddressToPeerMapping(): Map<String, String> {
val merged = linkedMapOf<String, String>()
try { merged.putAll(wifiService()?.getDeviceAddressToPeerMapping().orEmpty()) } catch (_: Exception) { }
try { merged.putAll(bluetooth.getDeviceAddressToPeerMapping()) } catch (_: Exception) { }
return merged
}
override fun printDeviceAddressesForPeers(): String {
return buildString {
appendLine(bluetooth.printDeviceAddressesForPeers())
wifiService()?.let {
appendLine()
appendLine(it.printDeviceAddressesForPeers())
}
}
}
override fun getDebugStatus(): String {
return buildString {
appendLine("=== Unified Mesh Service Debug Status ===")
appendLine("My Peer ID: $myPeerID")
appendLine("Merged Peers: ${mergedPeerIDs().joinToString(", ")}")
appendLine()
appendLine(bluetooth.getDebugStatus())
wifiService()?.let {
appendLine()
appendLine(it.getDebugStatus())
}
}
}
override fun clearAllInternalData() {
try { bluetooth.clearAllInternalData() } catch (_: Exception) { }
try { wifiService()?.clearAllInternalData() } catch (_: Exception) { }
}
override fun clearAllEncryptionData() {
try { bluetooth.clearAllEncryptionData() } catch (_: Exception) { }
try { wifiService()?.clearAllEncryptionData() } catch (_: Exception) { }
}
override fun didReceiveMessage(message: BitchatMessage) {
delegate?.didReceiveMessage(message)
}
override fun didUpdatePeerList(peers: List<String>) {
delegate?.didUpdatePeerList(mergedPeerIDs().ifEmpty { peers.distinct() })
}
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
delegate?.didReceiveChannelLeave(channel, fromPeer)
}
override fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) {
delegate?.didReceiveDeliveryAck(messageID, recipientPeerID)
}
override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) {
delegate?.didReceiveReadReceipt(messageID, recipientPeerID)
}
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
delegate?.didReceiveVerifyChallenge(peerID, payload, timestampMs)
}
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
delegate?.didReceiveVerifyResponse(peerID, payload, timestampMs)
}
override fun didReceiveNdrEvent(route: NdrMeshRoute, payload: ByteArray, timestampMs: Long) {
delegate?.didReceiveNdrEvent(route, payload, timestampMs)
}
override fun didResolvePrivateMediaPolicy(peerID: String) {
delegate?.didResolvePrivateMediaPolicy(peerID)
}
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
return delegate?.decryptChannelMessage(encryptedContent, channel)
}
override fun getNickname(): String? = delegate?.getNickname()
override fun isFavorite(peerID: String): Boolean = delegate?.isFavorite(peerID) ?: false
private fun mergedPeerIDs(): List<String> {
val ids = linkedSetOf<String>()
try { ids.addAll(com.bitchat.android.services.AppStateStore.peers.value) } catch (_: Exception) { }
try { ids.addAll(bluetooth.getPeerNicknames().keys) } catch (_: Exception) { }
try { ids.addAll(wifiService()?.getPeerNicknames()?.keys.orEmpty()) } catch (_: Exception) { }
return ids.toList()
}
private fun wifiService(): MeshService? {
return try {
WifiAwareController.getService()?.also { service ->
if (delegate != null && service.delegate !== this) {
service.delegate = this
}
}
} catch (_: Exception) {
null
}
}
private fun isBleEnabled(): Boolean {
return try {
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().bleEnabled.value
} catch (_: Exception) {
try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
}
}
private fun isBleConnected(peerID: String): Boolean {
return try { bluetooth.getPeerInfo(peerID)?.isConnected == true } catch (_: Exception) { false }
}
private fun isWifiConnected(peerID: String): Boolean {
return try { wifiService()?.getPeerInfo(peerID)?.isConnected == true } catch (_: Exception) { false }
}
private fun isBleReady(peerID: String): Boolean {
return isBleConnected(peerID) && hasEstablishedSessionOnBluetooth(peerID)
}
private fun isWifiReady(peerID: String): Boolean {
return try {
val wifi = wifiService()
wifi?.getPeerInfo(peerID)?.isConnected == true && wifi.hasEstablishedSession(peerID)
} catch (_: Exception) {
false
}
}
private fun hasEstablishedSessionOnBluetooth(peerID: String): Boolean {
return try { bluetooth.hasEstablishedSession(peerID) } catch (_: Exception) { false }
}
}