mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 02:25:21 +00:00
improve
This commit is contained in:
@@ -19,6 +19,7 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.onboarding.BluetoothCheckScreen
|
import com.bitchat.android.onboarding.BluetoothCheckScreen
|
||||||
import com.bitchat.android.onboarding.BluetoothStatus
|
import com.bitchat.android.onboarding.BluetoothStatus
|
||||||
import com.bitchat.android.onboarding.BluetoothStatusManager
|
import com.bitchat.android.onboarding.BluetoothStatusManager
|
||||||
@@ -41,7 +42,6 @@ import com.bitchat.android.ui.ChatViewModel
|
|||||||
import com.bitchat.android.ui.OrientationAwareActivity
|
import com.bitchat.android.ui.OrientationAwareActivity
|
||||||
import com.bitchat.android.ui.theme.BitchatTheme
|
import com.bitchat.android.ui.theme.BitchatTheme
|
||||||
import com.bitchat.android.wifiaware.WifiAwareController
|
import com.bitchat.android.wifiaware.WifiAwareController
|
||||||
import com.bitchat.android.wifiaware.WifiAwareMeshDelegate
|
|
||||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||||
import com.bitchat.android.services.VerificationService
|
import com.bitchat.android.services.VerificationService
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -57,43 +57,13 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
|
|
||||||
// Core mesh service - provided by the foreground service holder
|
// Core mesh service - provided by the foreground service holder
|
||||||
private lateinit var meshService: BluetoothMeshService
|
private lateinit var meshService: BluetoothMeshService
|
||||||
|
private lateinit var unifiedMeshService: MeshService
|
||||||
private val mainViewModel: MainViewModel by viewModels()
|
private val mainViewModel: MainViewModel by viewModels()
|
||||||
private val chatViewModel: ChatViewModel by viewModels {
|
private val chatViewModel: ChatViewModel by viewModels {
|
||||||
object : ViewModelProvider.Factory {
|
object : ViewModelProvider.Factory {
|
||||||
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return ChatViewModel(application, meshService) as T
|
return ChatViewModel(application, meshService, unifiedMeshService) as T
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val wifiAwareDelegate by lazy {
|
|
||||||
object : WifiAwareMeshDelegate {
|
|
||||||
override fun didReceiveMessage(message: com.bitchat.android.model.BitchatMessage) {
|
|
||||||
chatViewModel.didReceiveMessage(message)
|
|
||||||
}
|
|
||||||
override fun didUpdatePeerList(peers: List<String>) {
|
|
||||||
chatViewModel.onWifiPeersUpdated(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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +117,7 @@ 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)
|
||||||
|
unifiedMeshService = com.bitchat.android.service.MeshServiceHolder.getUnifiedOrCreate(applicationContext)
|
||||||
// Expose BLE mesh to Wi‑Fi Aware controller for cross-transport relays - DEPRECATED
|
// Expose BLE mesh to Wi‑Fi Aware controller for cross-transport relays - DEPRECATED
|
||||||
// Bridging is now handled by TransportBridgeService automatically
|
// Bridging is now handled by TransportBridgeService automatically
|
||||||
|
|
||||||
@@ -201,13 +172,12 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bridge Wi‑Fi Aware callbacks into ChatViewModel (reusing BLE delegate methods)
|
// Keep the unified mesh delegate attached when Wi-Fi Aware starts after the UI.
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
WifiAwareController.running.collect { running ->
|
WifiAwareController.running.collect { running ->
|
||||||
val svc = WifiAwareController.getService()
|
if (running && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||||
if (running && svc != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
unifiedMeshService.delegate = chatViewModel
|
||||||
svc.delegate = wifiAwareDelegate
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -742,9 +712,9 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up mesh service delegate and start services
|
// Set up unified mesh delegate and start enabled transports
|
||||||
meshService.delegate = chatViewModel
|
unifiedMeshService.delegate = chatViewModel
|
||||||
meshService.startServices()
|
unifiedMeshService.startServices()
|
||||||
|
|
||||||
Log.d("MainActivity", "Mesh service started successfully")
|
Log.d("MainActivity", "Mesh service started successfully")
|
||||||
|
|
||||||
@@ -788,8 +758,7 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
// Check Bluetooth and Location status on resume and handle accordingly
|
// Check Bluetooth and Location status on resume and handle accordingly
|
||||||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||||||
// Reattach mesh delegate to new ChatViewModel instance after Activity recreation
|
// Reattach mesh delegate to new ChatViewModel instance after Activity recreation
|
||||||
try { meshService.delegate = chatViewModel } catch (_: Exception) { }
|
try { unifiedMeshService.delegate = chatViewModel } catch (_: Exception) { }
|
||||||
try { WifiAwareController.getService()?.delegate = wifiAwareDelegate } catch (_: Exception) { }
|
|
||||||
|
|
||||||
// Check if Bluetooth was disabled while app was backgrounded
|
// Check if Bluetooth was disabled while app was backgrounded
|
||||||
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
||||||
@@ -821,8 +790,7 @@ class MainActivity : OrientationAwareActivity() {
|
|||||||
// Only set background state if app is fully initialized
|
// Only set background state if app is fully initialized
|
||||||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||||||
// Detach UI delegate so the foreground service can own DM notifications while UI is closed
|
// Detach UI delegate so the foreground service can own DM notifications while UI is closed
|
||||||
try { meshService.delegate = null } catch (_: Exception) { }
|
try { unifiedMeshService.delegate = null } catch (_: Exception) { }
|
||||||
try { WifiAwareController.getService()?.delegate = null } catch (_: Exception) { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ class BluetoothGattClientManager(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "BluetoothGattClientManager"
|
private const val TAG = "BluetoothGattClientManager"
|
||||||
|
// Self-healing scan recovery tuning
|
||||||
|
private const val SCAN_RETRY_BASE_MS = 3_000L // base backoff for transient scan failures
|
||||||
|
private const val SCAN_MAX_RETRY_DELAY_MS = 30_000L // cap on backoff delay
|
||||||
|
private const val SCAN_WATCHDOG_INTERVAL_MS = 30_000L // how often to verify the scanner is alive
|
||||||
|
private const val SCAN_STALE_RESULT_MS = 120_000L // force a scan restart if no results for this long
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core Bluetooth components
|
// Core Bluetooth components
|
||||||
@@ -78,9 +83,17 @@ class BluetoothGattClientManager(
|
|||||||
// Scan rate limiting to prevent "scanning too frequently" errors
|
// Scan rate limiting to prevent "scanning too frequently" errors
|
||||||
private var lastScanStartTime = 0L
|
private var lastScanStartTime = 0L
|
||||||
private var lastScanStopTime = 0L
|
private var lastScanStopTime = 0L
|
||||||
private var isCurrentlyScanning = false
|
@Volatile private var isCurrentlyScanning = false
|
||||||
private val scanRateLimit = 5000L // Minimum 5 seconds between scan start attempts
|
private val scanRateLimit = 5000L // Minimum 5 seconds between scan start attempts
|
||||||
|
|
||||||
|
// Self-healing scan state.
|
||||||
|
// scanningDesired distinguishes "we want to be scanning but it isn't running" (a fault to recover
|
||||||
|
// from) from "scanning is intentionally off" (e.g. duty-cycle OFF window or client disabled).
|
||||||
|
@Volatile private var scanningDesired = false
|
||||||
|
@Volatile private var lastScanResultTime = 0L
|
||||||
|
private var scanRetryCount = 0
|
||||||
|
private var scanWatchdogJob: Job? = null
|
||||||
|
|
||||||
// RSSI monitoring state
|
// RSSI monitoring state
|
||||||
private var rssiMonitoringJob: Job? = null
|
private var rssiMonitoringJob: Job? = null
|
||||||
|
|
||||||
@@ -121,12 +134,16 @@ class BluetoothGattClientManager(
|
|||||||
connectionScope.launch {
|
connectionScope.launch {
|
||||||
if (powerManager.shouldUseDutyCycle()) {
|
if (powerManager.shouldUseDutyCycle()) {
|
||||||
Log.i(TAG, "Using power-aware duty cycling")
|
Log.i(TAG, "Using power-aware duty cycling")
|
||||||
|
// Duty cycle drives onScanStateChanged(true/false); scanningDesired follows that.
|
||||||
} else {
|
} else {
|
||||||
|
scanningDesired = true
|
||||||
startScanning()
|
startScanning()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start RSSI monitoring
|
// Start RSSI monitoring
|
||||||
startRSSIMonitoring()
|
startRSSIMonitoring()
|
||||||
|
// Start the scan watchdog so a silently-dead or wedged scanner self-heals.
|
||||||
|
startScanWatchdog()
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -136,6 +153,8 @@ class BluetoothGattClientManager(
|
|||||||
* Stop client manager
|
* Stop client manager
|
||||||
*/
|
*/
|
||||||
fun stop() {
|
fun stop() {
|
||||||
|
scanningDesired = false
|
||||||
|
stopScanWatchdog()
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
// Idempotent stop
|
// Idempotent stop
|
||||||
stopScanning()
|
stopScanning()
|
||||||
@@ -166,6 +185,7 @@ class BluetoothGattClientManager(
|
|||||||
*/
|
*/
|
||||||
fun onScanStateChanged(shouldScan: Boolean) {
|
fun onScanStateChanged(shouldScan: Boolean) {
|
||||||
val enabled = isClientRoleEnabled()
|
val enabled = isClientRoleEnabled()
|
||||||
|
scanningDesired = shouldScan && enabled
|
||||||
if (shouldScan && enabled) {
|
if (shouldScan && enabled) {
|
||||||
startScanning()
|
startScanning()
|
||||||
} else {
|
} else {
|
||||||
@@ -266,23 +286,40 @@ class BluetoothGattClientManager(
|
|||||||
lastScanStopTime = System.currentTimeMillis()
|
lastScanStopTime = System.currentTimeMillis()
|
||||||
|
|
||||||
when (errorCode) {
|
when (errorCode) {
|
||||||
1 -> Log.e(TAG, "SCAN_FAILED_ALREADY_STARTED")
|
1 -> {
|
||||||
2 -> Log.e(TAG, "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED")
|
// Already started: the stack thinks a scan is running. Re-arm from a clean
|
||||||
3 -> Log.e(TAG, "SCAN_FAILED_INTERNAL_ERROR")
|
// state so we don't stay wedged (stop then restart with backoff).
|
||||||
4 -> Log.e(TAG, "SCAN_FAILED_FEATURE_UNSUPPORTED")
|
Log.e(TAG, "SCAN_FAILED_ALREADY_STARTED")
|
||||||
5 -> Log.e(TAG, "SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES")
|
stopScanning()
|
||||||
|
scheduleScanRestart("already-started", SCAN_RETRY_BASE_MS)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
// App registration failed: common transient stack fault. Previously had NO
|
||||||
|
// retry, which left discovery dead until a manual BLE toggle.
|
||||||
|
Log.e(TAG, "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED")
|
||||||
|
scheduleScanRestart("registration-failed", SCAN_RETRY_BASE_MS)
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
Log.e(TAG, "SCAN_FAILED_INTERNAL_ERROR")
|
||||||
|
scheduleScanRestart("internal-error", SCAN_RETRY_BASE_MS)
|
||||||
|
}
|
||||||
|
4 -> Log.e(TAG, "SCAN_FAILED_FEATURE_UNSUPPORTED") // permanent: don't retry
|
||||||
|
5 -> {
|
||||||
|
// Out of hardware resources: back off longer so other scanners/connections
|
||||||
|
// can free up before we try again.
|
||||||
|
Log.e(TAG, "SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES")
|
||||||
|
scheduleScanRestart("out-of-resources", SCAN_RETRY_BASE_MS * 3)
|
||||||
|
}
|
||||||
6 -> {
|
6 -> {
|
||||||
Log.e(TAG, "SCAN_FAILED_SCANNING_TOO_FREQUENTLY")
|
Log.e(TAG, "SCAN_FAILED_SCANNING_TOO_FREQUENTLY")
|
||||||
Log.w(TAG, "Scan failed due to rate limiting - will retry after delay")
|
Log.w(TAG, "Scan failed due to rate limiting - will retry after delay")
|
||||||
connectionScope.launch {
|
scheduleScanRestart("too-frequently", 10_000L)
|
||||||
delay(10000) // Wait 10 seconds before retrying
|
|
||||||
if (isActive) {
|
|
||||||
startScanning()
|
|
||||||
}
|
}
|
||||||
|
else -> {
|
||||||
|
Log.e(TAG, "Unknown scan failure code: $errorCode")
|
||||||
|
scheduleScanRestart("unknown-$errorCode", SCAN_RETRY_BASE_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> Log.e(TAG, "Unknown scan failure code: $errorCode")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,6 +357,76 @@ class BluetoothGattClientManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a scan restart with incremental backoff. Used to recover from transient scan
|
||||||
|
* failures that previously had no retry path (codes 2/3/5), leaving discovery dead until a
|
||||||
|
* manual BLE toggle.
|
||||||
|
*/
|
||||||
|
private fun scheduleScanRestart(reason: String, baseDelayMs: Long) {
|
||||||
|
scanRetryCount++
|
||||||
|
val delayMs = (baseDelayMs * scanRetryCount).coerceAtMost(SCAN_MAX_RETRY_DELAY_MS)
|
||||||
|
Log.w(TAG, "Scheduling scan restart in ${delayMs}ms (attempt $scanRetryCount, reason=$reason)")
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(delayMs)
|
||||||
|
if (isActive && scanningDesired && isClientRoleEnabled() && !isCurrentlyScanning) {
|
||||||
|
startScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodic watchdog that self-heals the scanner. Android can stop a scan without ever invoking
|
||||||
|
* onScanFailed (internal stack reset, Doze, background throttling), which leaves the app
|
||||||
|
* believing it is scanning while it is not. This re-arms the scanner in those cases.
|
||||||
|
*/
|
||||||
|
private fun startScanWatchdog() {
|
||||||
|
scanWatchdogJob?.cancel()
|
||||||
|
scanWatchdogJob = connectionScope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
delay(SCAN_WATCHDOG_INTERVAL_MS)
|
||||||
|
try {
|
||||||
|
// Only act when we are supposed to be scanning. Honors duty-cycle OFF windows
|
||||||
|
// and the client-disabled state via scanningDesired.
|
||||||
|
if (!isActive || !scanningDesired || !isClientRoleEnabled()) continue
|
||||||
|
if (!permissionManager.hasBluetoothPermissions() || bluetoothAdapter?.isEnabled != true) continue
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (!isCurrentlyScanning) {
|
||||||
|
Log.w(TAG, "Watchdog: scan desired but not running -> restarting scan")
|
||||||
|
startScanning()
|
||||||
|
} else if (lastScanResultTime > 0L &&
|
||||||
|
now - lastScanResultTime > SCAN_STALE_RESULT_MS &&
|
||||||
|
now - lastScanStartTime > SCAN_STALE_RESULT_MS) {
|
||||||
|
// We think we're scanning but haven't seen anything for a long time. The scan
|
||||||
|
// may have silently died (flag wedged true). Force a clean re-arm.
|
||||||
|
Log.w(TAG, "Watchdog: no scan results for ${(now - lastScanResultTime) / 1000}s -> forcing scan restart")
|
||||||
|
forceRestartScan()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Scan watchdog error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopScanWatchdog() {
|
||||||
|
scanWatchdogJob?.cancel()
|
||||||
|
scanWatchdogJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force a clean scan restart, clearing a possibly-wedged isCurrentlyScanning flag.
|
||||||
|
*/
|
||||||
|
private fun forceRestartScan() {
|
||||||
|
stopScanning()
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(500)
|
||||||
|
if (isActive && scanningDesired && isClientRoleEnabled() && !isCurrentlyScanning) {
|
||||||
|
startScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle scan result and initiate connection if appropriate
|
* Handle scan result and initiate connection if appropriate
|
||||||
*/
|
*/
|
||||||
@@ -335,6 +442,10 @@ class BluetoothGattClientManager(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Proof the scanner is alive and finding our network: refresh liveness and clear backoff.
|
||||||
|
lastScanResultTime = System.currentTimeMillis()
|
||||||
|
scanRetryCount = 0
|
||||||
|
|
||||||
// Try to extract peerID from Service Data (if available) for stable identity
|
// Try to extract peerID from Service Data (if available) for stable identity
|
||||||
val serviceData = scanRecord?.getServiceData(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID))
|
val serviceData = scanRecord?.getServiceData(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID))
|
||||||
val peerID = if (serviceData != null && serviceData.size >= 8) {
|
val peerID = if (serviceData != null && serviceData.size >= 8) {
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ class BluetoothGattServerManager(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "BluetoothGattServerManager"
|
private const val TAG = "BluetoothGattServerManager"
|
||||||
|
// Self-healing advertising recovery tuning
|
||||||
|
private const val ADVERTISE_RETRY_BASE_MS = 3_000L // base backoff for transient advertise failures
|
||||||
|
private const val ADVERTISE_MAX_RETRY_DELAY_MS = 30_000L // cap on backoff delay
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core Bluetooth components
|
// Core Bluetooth components
|
||||||
@@ -42,6 +45,7 @@ class BluetoothGattServerManager(
|
|||||||
private var gattServer: BluetoothGattServer? = null
|
private var gattServer: BluetoothGattServer? = null
|
||||||
private var characteristic: BluetoothGattCharacteristic? = null
|
private var characteristic: BluetoothGattCharacteristic? = null
|
||||||
private var advertiseCallback: AdvertiseCallback? = null
|
private var advertiseCallback: AdvertiseCallback? = null
|
||||||
|
private var advertiseRetryCount = 0
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
private var isActive = false
|
private var isActive = false
|
||||||
@@ -385,6 +389,7 @@ class BluetoothGattServerManager(
|
|||||||
|
|
||||||
advertiseCallback = object : AdvertiseCallback() {
|
advertiseCallback = object : AdvertiseCallback() {
|
||||||
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
||||||
|
advertiseRetryCount = 0
|
||||||
val mode = try {
|
val mode = try {
|
||||||
powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]
|
powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]
|
||||||
} catch (_: Exception) { "unknown" }
|
} catch (_: Exception) { "unknown" }
|
||||||
@@ -393,6 +398,28 @@ class BluetoothGattServerManager(
|
|||||||
|
|
||||||
override fun onStartFailure(errorCode: Int) {
|
override fun onStartFailure(errorCode: Int) {
|
||||||
Log.e(TAG, "Advertising failed: $errorCode")
|
Log.e(TAG, "Advertising failed: $errorCode")
|
||||||
|
// Previously this only logged, so if advertising failed this device became
|
||||||
|
// undiscoverable until a manual BLE toggle. Retry transient failures with backoff.
|
||||||
|
when (errorCode) {
|
||||||
|
ADVERTISE_FAILED_ALREADY_STARTED ->
|
||||||
|
Log.w(TAG, "ADVERTISE_FAILED_ALREADY_STARTED - already advertising, no retry")
|
||||||
|
ADVERTISE_FAILED_DATA_TOO_LARGE ->
|
||||||
|
Log.e(TAG, "ADVERTISE_FAILED_DATA_TOO_LARGE - config issue, not retrying")
|
||||||
|
ADVERTISE_FAILED_FEATURE_UNSUPPORTED ->
|
||||||
|
Log.e(TAG, "ADVERTISE_FAILED_FEATURE_UNSUPPORTED - unsupported, not retrying")
|
||||||
|
ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> {
|
||||||
|
Log.w(TAG, "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS - will retry after backoff")
|
||||||
|
scheduleAdvertiseRestart("too-many-advertisers")
|
||||||
|
}
|
||||||
|
ADVERTISE_FAILED_INTERNAL_ERROR -> {
|
||||||
|
Log.w(TAG, "ADVERTISE_FAILED_INTERNAL_ERROR - will retry after backoff")
|
||||||
|
scheduleAdvertiseRestart("internal-error")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Unknown advertise failure $errorCode - will retry after backoff")
|
||||||
|
scheduleAdvertiseRestart("unknown-$errorCode")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,6 +445,23 @@ class BluetoothGattServerManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule an advertising restart with incremental backoff after a transient failure.
|
||||||
|
*/
|
||||||
|
private fun scheduleAdvertiseRestart(reason: String) {
|
||||||
|
advertiseRetryCount++
|
||||||
|
val delayMs = (ADVERTISE_RETRY_BASE_MS * advertiseRetryCount).coerceAtMost(ADVERTISE_MAX_RETRY_DELAY_MS)
|
||||||
|
Log.w(TAG, "Scheduling advertising restart in ${delayMs}ms (attempt $advertiseRetryCount, reason=$reason)")
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(delayMs)
|
||||||
|
if (isActive && isServerRoleEnabled()) {
|
||||||
|
stopAdvertising()
|
||||||
|
delay(100)
|
||||||
|
startAdvertising()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restart advertising (for power mode changes)
|
* Restart advertising (for power mode changes)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1500,18 +1500,10 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegate interface for mesh service callbacks (maintains exact same interface)
|
* Delegate interface for BLE mesh callbacks. Extends the shared mesh delegate so
|
||||||
|
* transport-agnostic facades can receive the same callback stream.
|
||||||
*/
|
*/
|
||||||
interface BluetoothMeshDelegate {
|
interface BluetoothMeshDelegate : MeshDelegate {
|
||||||
fun didReceiveMessage(message: BitchatMessage)
|
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||||
fun didUpdatePeerList(peers: List<String>)
|
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long)
|
||||||
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
|
||||||
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
|
||||||
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
|
||||||
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long)
|
|
||||||
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long)
|
|
||||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
|
||||||
fun getNickname(): String?
|
|
||||||
fun isFavorite(peerID: String): Boolean
|
|
||||||
// registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,11 +315,11 @@ class MeshCore(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
|
override fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||||
// MeshDelegate intentionally does not expose QR verification yet.
|
delegate?.didReceiveVerifyChallenge(peerID, payload, timestampMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
|
override fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
|
||||||
// MeshDelegate intentionally does not expose QR verification yet.
|
delegate?.didReceiveVerifyResponse(peerID, payload, timestampMs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,6 +549,44 @@ class MeshCore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendVerifyChallenge(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
|
||||||
|
val payload = NoisePayload(
|
||||||
|
type = NoisePayloadType.VERIFY_CHALLENGE,
|
||||||
|
data = com.bitchat.android.services.VerificationService.buildVerifyChallenge(noiseKeyHex, nonceA)
|
||||||
|
)
|
||||||
|
sendNoisePayloadToPeer(payload, peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendVerifyResponse(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
|
||||||
|
val tlv = com.bitchat.android.services.VerificationService.buildVerifyResponse(noiseKeyHex, nonceA) ?: return
|
||||||
|
val payload = NoisePayload(
|
||||||
|
type = NoisePayloadType.VERIFY_RESPONSE,
|
||||||
|
data = tlv
|
||||||
|
)
|
||||||
|
sendNoisePayloadToPeer(payload, peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendNoisePayloadToPeer(payload: NoisePayload, recipientPeerID: String) {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val encrypted = encryptionService.encrypt(payload.encode(), recipientPeerID)
|
||||||
|
val packet = BitchatPacket(
|
||||||
|
version = 1u,
|
||||||
|
type = MessageType.NOISE_ENCRYPTED.value,
|
||||||
|
senderID = MeshPacketUtils.hexStringToByteArray(myPeerID),
|
||||||
|
recipientID = MeshPacketUtils.hexStringToByteArray(recipientPeerID),
|
||||||
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
|
payload = encrypted,
|
||||||
|
signature = null,
|
||||||
|
ttl = maxTtl
|
||||||
|
)
|
||||||
|
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("MeshCore", "Failed to send Noise payload to $recipientPeerID: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun sendBroadcastAnnounce() {
|
fun sendBroadcastAnnounce() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val nickname = hooks.announcementNicknameProvider?.invoke()
|
val nickname = hooks.announcementNicknameProvider?.invoke()
|
||||||
@@ -718,6 +756,8 @@ class MeshCore(
|
|||||||
|
|
||||||
fun getIdentityFingerprint(): String = encryptionService.getIdentityFingerprint()
|
fun getIdentityFingerprint(): String = encryptionService.getIdentityFingerprint()
|
||||||
|
|
||||||
|
fun getStaticNoisePublicKey(): ByteArray? = encryptionService.getStaticPublicKey()
|
||||||
|
|
||||||
fun shouldShowEncryptionIcon(peerID: String): Boolean = encryptionService.hasEstablishedSession(peerID)
|
fun shouldShowEncryptionIcon(peerID: String): Boolean = encryptionService.hasEstablishedSession(peerID)
|
||||||
|
|
||||||
fun getEncryptedPeers(): List<String> = emptyList()
|
fun getEncryptedPeers(): List<String> = emptyList()
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ interface MeshDelegate {
|
|||||||
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
||||||
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
||||||
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
||||||
|
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {}
|
||||||
|
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {}
|
||||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
||||||
fun getNickname(): String?
|
fun getNickname(): String?
|
||||||
fun isFavorite(peerID: String): Boolean
|
fun isFavorite(peerID: String): Boolean
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ interface MeshService {
|
|||||||
fun sendMessage(content: String, mentions: List<String> = emptyList(), channel: String? = null)
|
fun sendMessage(content: String, mentions: List<String> = emptyList(), channel: String? = null)
|
||||||
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null)
|
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null)
|
||||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String)
|
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String)
|
||||||
|
fun sendDeliveryAck(messageID: String, recipientPeerID: String) {}
|
||||||
|
fun sendFavoriteNotification(peerID: String, isFavorite: Boolean) {}
|
||||||
|
fun sendVerifyChallenge(peerID: String, noiseKeyHex: String, nonceA: ByteArray)
|
||||||
|
fun sendVerifyResponse(peerID: String, noiseKeyHex: String, nonceA: ByteArray)
|
||||||
fun sendFileBroadcast(file: BitchatFilePacket)
|
fun sendFileBroadcast(file: BitchatFilePacket)
|
||||||
fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket)
|
fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket)
|
||||||
fun cancelFileTransfer(transferId: String): Boolean
|
fun cancelFileTransfer(transferId: String): Boolean
|
||||||
@@ -38,6 +42,7 @@ interface MeshService {
|
|||||||
isVerified: Boolean
|
isVerified: Boolean
|
||||||
): Boolean
|
): Boolean
|
||||||
fun getIdentityFingerprint(): String
|
fun getIdentityFingerprint(): String
|
||||||
|
fun getStaticNoisePublicKey(): ByteArray?
|
||||||
fun shouldShowEncryptionIcon(peerID: String): Boolean
|
fun shouldShowEncryptionIcon(peerID: String): Boolean
|
||||||
fun getEncryptedPeers(): List<String>
|
fun getEncryptedPeers(): List<String>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.bitchat.android.model.BitchatFilePacket
|
||||||
|
import com.bitchat.android.model.BitchatMessage
|
||||||
|
import com.bitchat.android.noise.NoiseSession
|
||||||
|
import com.bitchat.android.wifiaware.WifiAwareController
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
refreshDelegates()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopServices() {
|
||||||
|
try { bluetooth.stopServices() } catch (_: Exception) { }
|
||||||
|
try { WifiAwareController.stop() } catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = if (isFavorite) "[FAVORITED]:${myNpub ?: ""}" else "[UNFAVORITED]:${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 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 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 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 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package com.bitchat.android.service
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.net.ArtiTorManager
|
import com.bitchat.android.net.ArtiTorManager
|
||||||
import com.bitchat.android.net.TorMode
|
import com.bitchat.android.net.TorMode
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -39,7 +39,7 @@ object AppShutdownCoordinator {
|
|||||||
|
|
||||||
fun requestFullShutdownAndKill(
|
fun requestFullShutdownAndKill(
|
||||||
app: Application,
|
app: Application,
|
||||||
mesh: BluetoothMeshService?,
|
mesh: MeshService?,
|
||||||
notificationManager: NotificationManagerCompat,
|
notificationManager: NotificationManagerCompat,
|
||||||
stopForeground: () -> Unit,
|
stopForeground: () -> Unit,
|
||||||
stopService: () -> Unit
|
stopService: () -> Unit
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ class MeshForegroundService : Service() {
|
|||||||
private var updateJob: Job? = null
|
private var updateJob: Job? = null
|
||||||
private val meshService: BluetoothMeshService?
|
private val meshService: BluetoothMeshService?
|
||||||
get() = MeshServiceHolder.meshService
|
get() = MeshServiceHolder.meshService
|
||||||
|
private val unifiedMeshService: com.bitchat.android.mesh.MeshService?
|
||||||
|
get() = MeshServiceHolder.unifiedMeshService
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + serviceJob)
|
private val scope = CoroutineScope(Dispatchers.Default + serviceJob)
|
||||||
private var isInForeground: Boolean = false
|
private var isInForeground: Boolean = false
|
||||||
@@ -134,6 +136,7 @@ class MeshForegroundService : Service() {
|
|||||||
Log.i("MeshForegroundService", "Created new BluetoothMeshService via holder")
|
Log.i("MeshForegroundService", "Created new BluetoothMeshService via holder")
|
||||||
MeshServiceHolder.attach(created)
|
MeshServiceHolder.attach(created)
|
||||||
}
|
}
|
||||||
|
MeshServiceHolder.getUnifiedOrCreate(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
@@ -147,7 +150,7 @@ class MeshForegroundService : Service() {
|
|||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
ACTION_STOP -> {
|
ACTION_STOP -> {
|
||||||
// Stop FGS and mesh cleanly
|
// Stop FGS and mesh cleanly
|
||||||
try { meshService?.stopServices() } catch (_: Exception) { }
|
try { unifiedMeshService?.stopServices() ?: meshService?.stopServices() } catch (_: Exception) { }
|
||||||
try { MeshServiceHolder.clear() } catch (_: Exception) { }
|
try { MeshServiceHolder.clear() } catch (_: Exception) { }
|
||||||
try { stopForeground(true) } catch (_: Exception) { }
|
try { stopForeground(true) } catch (_: Exception) { }
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
@@ -165,7 +168,7 @@ class MeshForegroundService : Service() {
|
|||||||
// Fully stop all background activity, stop Tor (without changing setting), then kill the app
|
// Fully stop all background activity, stop Tor (without changing setting), then kill the app
|
||||||
AppShutdownCoordinator.requestFullShutdownAndKill(
|
AppShutdownCoordinator.requestFullShutdownAndKill(
|
||||||
app = application,
|
app = application,
|
||||||
mesh = meshService,
|
mesh = unifiedMeshService,
|
||||||
notificationManager = notificationManager,
|
notificationManager = notificationManager,
|
||||||
stopForeground = {
|
stopForeground = {
|
||||||
try { stopForeground(true) } catch (_: Exception) { }
|
try { stopForeground(true) } catch (_: Exception) { }
|
||||||
@@ -178,7 +181,7 @@ class MeshForegroundService : Service() {
|
|||||||
ACTION_UPDATE_NOTIFICATION -> {
|
ACTION_UPDATE_NOTIFICATION -> {
|
||||||
// If we became eligible and are not in foreground yet, promote once
|
// If we became eligible and are not in foreground yet, promote once
|
||||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
|
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
|
||||||
val n = buildNotification(meshService?.getActivePeerCount() ?: 0)
|
val n = buildNotification(getUnifiedActivePeerCount())
|
||||||
startForegroundCompat(n)
|
startForegroundCompat(n)
|
||||||
isInForeground = true
|
isInForeground = true
|
||||||
} else {
|
} else {
|
||||||
@@ -193,7 +196,7 @@ class MeshForegroundService : Service() {
|
|||||||
|
|
||||||
// Promote exactly once when eligible, otherwise stay background (or stop)
|
// Promote exactly once when eligible, otherwise stay background (or stop)
|
||||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
|
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) {
|
||||||
val notification = buildNotification(meshService?.getActivePeerCount() ?: 0)
|
val notification = buildNotification(getUnifiedActivePeerCount())
|
||||||
startForegroundCompat(notification)
|
startForegroundCompat(notification)
|
||||||
isInForeground = true
|
isInForeground = true
|
||||||
}
|
}
|
||||||
@@ -256,7 +259,7 @@ class MeshForegroundService : Service() {
|
|||||||
notificationManager.cancel(NOTIFICATION_ID)
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val count = meshService?.getActivePeerCount() ?: 0
|
val count = getUnifiedActivePeerCount()
|
||||||
val notification = buildNotification(count)
|
val notification = buildNotification(count)
|
||||||
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions()) {
|
if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions()) {
|
||||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
@@ -276,6 +279,14 @@ class MeshForegroundService : Service() {
|
|||||||
return hasBluetoothPermissions() && hasNotificationPermission()
|
return hasBluetoothPermissions() && hasNotificationPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUnifiedActivePeerCount(): Int {
|
||||||
|
return try {
|
||||||
|
unifiedMeshService?.getActivePeerCount() ?: meshService?.getActivePeerCount() ?: 0
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun hasBluetoothPermissions(): Boolean {
|
private fun hasBluetoothPermissions(): Boolean {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
androidx.core.content.ContextCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_ADVERTISE) == android.content.pm.PackageManager.PERMISSION_GRANTED &&
|
androidx.core.content.ContextCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_ADVERTISE) == android.content.pm.PackageManager.PERMISSION_GRANTED &&
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.bitchat.android.service
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
|
import com.bitchat.android.mesh.UnifiedMeshService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process-wide holder to share a single BluetoothMeshService instance
|
* Process-wide holder to share a single BluetoothMeshService instance
|
||||||
@@ -19,6 +20,10 @@ object MeshServiceHolder {
|
|||||||
var meshService: BluetoothMeshService? = null
|
var meshService: BluetoothMeshService? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var unifiedMeshService: UnifiedMeshService? = null
|
||||||
|
private set
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun getOrCreate(context: Context): BluetoothMeshService {
|
fun getOrCreate(context: Context): BluetoothMeshService {
|
||||||
val existing = meshService
|
val existing = meshService
|
||||||
@@ -37,18 +42,35 @@ object MeshServiceHolder {
|
|||||||
val created = BluetoothMeshService(context.applicationContext)
|
val created = BluetoothMeshService(context.applicationContext)
|
||||||
android.util.Log.i(TAG, "Created new BluetoothMeshService (replacement)")
|
android.util.Log.i(TAG, "Created new BluetoothMeshService (replacement)")
|
||||||
meshService = created
|
meshService = created
|
||||||
|
unifiedMeshService = null
|
||||||
created
|
created
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e(TAG, "Error checking service reusability; creating new instance: ${e.message}")
|
android.util.Log.e(TAG, "Error checking service reusability; creating new instance: ${e.message}")
|
||||||
val created = BluetoothMeshService(context.applicationContext)
|
val created = BluetoothMeshService(context.applicationContext)
|
||||||
meshService = created
|
meshService = created
|
||||||
|
unifiedMeshService = null
|
||||||
created
|
created
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val created = BluetoothMeshService(context.applicationContext)
|
val created = BluetoothMeshService(context.applicationContext)
|
||||||
android.util.Log.i(TAG, "Created new BluetoothMeshService (no existing instance)")
|
android.util.Log.i(TAG, "Created new BluetoothMeshService (no existing instance)")
|
||||||
meshService = created
|
meshService = created
|
||||||
|
unifiedMeshService = null
|
||||||
|
return created
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun getUnifiedOrCreate(context: Context): UnifiedMeshService {
|
||||||
|
val bluetooth = getOrCreate(context)
|
||||||
|
val existing = unifiedMeshService
|
||||||
|
if (existing != null) {
|
||||||
|
existing.refreshDelegates()
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
val created = UnifiedMeshService(context.applicationContext, bluetooth)
|
||||||
|
unifiedMeshService = created
|
||||||
|
android.util.Log.i(TAG, "Created new UnifiedMeshService")
|
||||||
return created
|
return created
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,11 +78,13 @@ object MeshServiceHolder {
|
|||||||
fun attach(service: BluetoothMeshService) {
|
fun attach(service: BluetoothMeshService) {
|
||||||
android.util.Log.d(TAG, "Attaching BluetoothMeshService to holder")
|
android.util.Log.d(TAG, "Attaching BluetoothMeshService to holder")
|
||||||
meshService = service
|
meshService = service
|
||||||
|
unifiedMeshService = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun clear() {
|
fun clear() {
|
||||||
android.util.Log.d(TAG, "Clearing BluetoothMeshService from holder")
|
android.util.Log.d(TAG, "Clearing BluetoothMeshService from holder")
|
||||||
meshService = null
|
meshService = null
|
||||||
|
unifiedMeshService = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.bitchat.android.services
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
|
||||||
import com.bitchat.android.mesh.MeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.model.ReadReceipt
|
import com.bitchat.android.model.ReadReceipt
|
||||||
import com.bitchat.android.nostr.NostrTransport
|
import com.bitchat.android.nostr.NostrTransport
|
||||||
@@ -12,14 +11,14 @@ import com.bitchat.android.nostr.NostrTransport
|
|||||||
*/
|
*/
|
||||||
class MessageRouter private constructor(
|
class MessageRouter private constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private var mesh: BluetoothMeshService,
|
private var mesh: MeshService,
|
||||||
private val nostr: NostrTransport
|
private val nostr: NostrTransport
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MessageRouter"
|
private const val TAG = "MessageRouter"
|
||||||
@Volatile private var INSTANCE: MessageRouter? = null
|
@Volatile private var INSTANCE: MessageRouter? = null
|
||||||
fun tryGetInstance(): MessageRouter? = INSTANCE
|
fun tryGetInstance(): MessageRouter? = INSTANCE
|
||||||
fun getInstance(context: Context, mesh: BluetoothMeshService): MessageRouter {
|
fun getInstance(context: Context, mesh: MeshService): MessageRouter {
|
||||||
val instance = INSTANCE ?: synchronized(this) {
|
val instance = INSTANCE ?: synchronized(this) {
|
||||||
INSTANCE ?: run {
|
INSTANCE ?: run {
|
||||||
val nostr = NostrTransport.getInstance(context)
|
val nostr = NostrTransport.getInstance(context)
|
||||||
@@ -71,15 +70,10 @@ class MessageRouter private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val aware = getAwareService()
|
|
||||||
val hasMesh = isConnected(mesh, toPeerID)
|
val hasMesh = isConnected(mesh, toPeerID)
|
||||||
val hasAwareConnection = isConnected(aware, toPeerID)
|
|
||||||
if (isReady(mesh, toPeerID)) {
|
if (isReady(mesh, toPeerID)) {
|
||||||
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 (isReady(aware, toPeerID)) {
|
|
||||||
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)
|
||||||
@@ -88,23 +82,14 @@ 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)}…")
|
||||||
when {
|
if (hasMesh) mesh.initiateNoiseHandshake(toPeerID)
|
||||||
hasMesh -> mesh.initiateNoiseHandshake(toPeerID)
|
|
||||||
hasAwareConnection -> aware?.initiateNoiseHandshake(toPeerID)
|
|
||||||
else -> aware?.initiateNoiseHandshake(toPeerID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendReadReceipt(receipt: ReadReceipt, toPeerID: String) {
|
fun sendReadReceipt(receipt: ReadReceipt, toPeerID: String) {
|
||||||
val aware = getAwareService()
|
|
||||||
if (isReady(mesh, toPeerID)) {
|
if (isReady(mesh, toPeerID)) {
|
||||||
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 (isReady(aware, toPeerID)) {
|
|
||||||
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)
|
||||||
@@ -127,7 +112,7 @@ class MessageRouter private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendFavoriteNotification(toPeerID: String, isFavorite: Boolean) {
|
fun sendFavoriteNotification(toPeerID: String, isFavorite: Boolean) {
|
||||||
if (mesh.getPeerInfo(toPeerID)?.isConnected == true) {
|
if (mesh.getPeerInfo(toPeerID)?.isConnected == true && mesh.hasEstablishedSession(toPeerID)) {
|
||||||
val myNpub = try { com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(context)?.npub } catch (_: Exception) { null }
|
val myNpub = try { com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(context)?.npub } catch (_: Exception) { null }
|
||||||
val content = if (isFavorite) "[FAVORITED]:${myNpub ?: ""}" else "[UNFAVORITED]:${myNpub ?: ""}"
|
val content = if (isFavorite) "[FAVORITED]:${myNpub ?: ""}" else "[UNFAVORITED]:${myNpub ?: ""}"
|
||||||
val nickname = mesh.getPeerNicknames()[toPeerID] ?: toPeerID
|
val nickname = mesh.getPeerNicknames()[toPeerID] ?: toPeerID
|
||||||
@@ -142,34 +127,23 @@ class MessageRouter private constructor(
|
|||||||
val queued = outbox[peerID] ?: return
|
val queued = outbox[peerID] ?: return
|
||||||
if (queued.isEmpty()) return
|
if (queued.isEmpty()) return
|
||||||
Log.d(TAG, "Flushing outbox for ${peerID.take(8)}… count=${queued.size}")
|
Log.d(TAG, "Flushing outbox for ${peerID.take(8)}… count=${queued.size}")
|
||||||
val aware = getAwareService()
|
|
||||||
val iterator = queued.iterator()
|
val iterator = queued.iterator()
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val (content, nickname, messageID) = iterator.next()
|
val (content, nickname, messageID) = iterator.next()
|
||||||
val hasMesh = isReady(mesh, peerID)
|
val hasMesh = isReady(mesh, peerID)
|
||||||
val hasAware = isReady(aware, peerID)
|
|
||||||
// If this is a noiseHex key, see if there is a connected mesh peer for this identity
|
// If this is a noiseHex key, see if there is a connected mesh peer for this identity
|
||||||
if (!hasMesh && !hasAware && peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
if (!hasMesh && peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
||||||
val meshPeer = resolvePeerForNoiseHex(peerID, mesh)
|
val meshPeer = resolvePeerForNoiseHex(peerID, mesh)
|
||||||
if (meshPeer != null && isReady(mesh, meshPeer)) {
|
if (meshPeer != null && isReady(mesh, meshPeer)) {
|
||||||
mesh.sendPrivateMessage(content, meshPeer, nickname, messageID)
|
mesh.sendPrivateMessage(content, meshPeer, nickname, messageID)
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val awarePeer = resolvePeerForNoiseHex(peerID, aware)
|
|
||||||
if (awarePeer != null && isReady(aware, awarePeer)) {
|
|
||||||
aware?.sendPrivateMessage(content, awarePeer, nickname, messageID)
|
|
||||||
iterator.remove()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val canNostr = canSendViaNostr(peerID)
|
val canNostr = canSendViaNostr(peerID)
|
||||||
if (hasMesh) {
|
if (hasMesh) {
|
||||||
mesh.sendPrivateMessage(content, peerID, nickname, messageID)
|
mesh.sendPrivateMessage(content, peerID, nickname, messageID)
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
} else if (hasAware) {
|
|
||||||
aware?.sendPrivateMessage(content, peerID, nickname, messageID)
|
|
||||||
iterator.remove()
|
|
||||||
} else if (canNostr) {
|
} else if (canNostr) {
|
||||||
nostr.sendPrivateMessage(content, peerID, nickname, messageID)
|
nostr.sendPrivateMessage(content, peerID, nickname, messageID)
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
@@ -207,11 +181,7 @@ class MessageRouter private constructor(
|
|||||||
return clean.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
return clean.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAwareService(): MeshService? {
|
private fun isConnected(service: MeshService, peerID: String): Boolean {
|
||||||
return try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isConnected(service: BluetoothMeshService, peerID: String): Boolean {
|
|
||||||
return try {
|
return try {
|
||||||
service.getPeerInfo(peerID)?.isConnected == true
|
service.getPeerInfo(peerID)?.isConnected == true
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
@@ -219,15 +189,7 @@ class MessageRouter private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isConnected(service: MeshService?, peerID: String): Boolean {
|
private fun isReady(service: MeshService, peerID: String): Boolean {
|
||||||
return try {
|
|
||||||
service != null && service.getPeerInfo(peerID)?.isConnected == true
|
|
||||||
} catch (_: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isReady(service: BluetoothMeshService, peerID: String): Boolean {
|
|
||||||
return try {
|
return try {
|
||||||
service.getPeerInfo(peerID)?.isConnected == true &&
|
service.getPeerInfo(peerID)?.isConnected == true &&
|
||||||
service.hasEstablishedSession(peerID)
|
service.hasEstablishedSession(peerID)
|
||||||
@@ -236,29 +198,8 @@ class MessageRouter private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isReady(service: MeshService?, peerID: String): Boolean {
|
private fun resolvePeerForNoiseHex(noiseHex: String, service: MeshService): String? {
|
||||||
return try {
|
return try {
|
||||||
service != null &&
|
|
||||||
service.getPeerInfo(peerID)?.isConnected == true &&
|
|
||||||
service.hasEstablishedSession(peerID)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolvePeerForNoiseHex(noiseHex: String, service: BluetoothMeshService): String? {
|
|
||||||
return try {
|
|
||||||
service.getPeerNicknames().keys.firstOrNull { pid ->
|
|
||||||
val info = service.getPeerInfo(pid)
|
|
||||||
val keyHex = info?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
|
||||||
keyHex != null && keyHex.equals(noiseHex, ignoreCase = true)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolvePeerForNoiseHex(noiseHex: String, service: MeshService?): String? {
|
|
||||||
return try {
|
|
||||||
if (service == null) return null
|
|
||||||
service.getPeerNicknames().keys.firstOrNull { pid ->
|
service.getPeerNicknames().keys.firstOrNull { pid ->
|
||||||
val info = service.getPeerInfo(pid)
|
val info = service.getPeerInfo(pid)
|
||||||
val keyHex = info?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
val keyHex = info?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
@@ -275,10 +216,6 @@ class MessageRouter private constructor(
|
|||||||
mesh.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
mesh.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
noiseHex?.let { flushOutboxFor(it) }
|
noiseHex?.let { flushOutboxFor(it) }
|
||||||
val awareNoiseHex = try {
|
|
||||||
getAwareService()?.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
awareNoiseHex?.let { flushOutboxFor(it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,9 +226,5 @@ class MessageRouter private constructor(
|
|||||||
mesh.getPeerInfo(peerID)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
mesh.getPeerInfo(peerID)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
noiseHex?.let { flushOutboxFor(it) }
|
noiseHex?.let { flushOutboxFor(it) }
|
||||||
val awareNoiseHex = try {
|
|
||||||
getAwareService()?.getPeerInfo(peerID)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
|
||||||
} catch (_: Exception) { null }
|
|
||||||
awareNoiseHex?.let { flushOutboxFor(it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ private fun MainHeader(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(2.dp))
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
PeerCounter(
|
PeerCounter(
|
||||||
connectedPeers = connectedPeers.filter { it != viewModel.meshService.myPeerID },
|
connectedPeers = connectedPeers.filter { it != viewModel.myPeerID },
|
||||||
joinedChannels = joinedChannels,
|
joinedChannels = joinedChannels,
|
||||||
hasUnreadChannels = hasUnreadChannels,
|
hasUnreadChannels = hasUnreadChannels,
|
||||||
isConnected = isConnected,
|
isConnected = isConnected,
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
MessagesList(
|
MessagesList(
|
||||||
messages = displayMessages,
|
messages = displayMessages,
|
||||||
currentUserNickname = nickname,
|
currentUserNickname = nickname,
|
||||||
meshService = viewModel.meshService,
|
meshService = viewModel.meshServiceFacade,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
forceScrollToBottom = forceScrollToBottom,
|
forceScrollToBottom = forceScrollToBottom,
|
||||||
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
|
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.Shield
|
import androidx.compose.material.icons.outlined.Shield
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -42,7 +42,7 @@ fun getRSSIColor(rssi: Int): Color {
|
|||||||
fun formatMessageAsAnnotatedString(
|
fun formatMessageAsAnnotatedString(
|
||||||
message: BitchatMessage,
|
message: BitchatMessage,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: MeshService,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
@@ -162,7 +162,7 @@ fun formatMessageAsAnnotatedString(
|
|||||||
fun formatMessageHeaderAnnotatedString(
|
fun formatMessageHeaderAnnotatedString(
|
||||||
message: BitchatMessage,
|
message: BitchatMessage,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: MeshService,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
timeFormatter: SimpleDateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.service.MeshServiceHolder
|
import com.bitchat.android.service.MeshServiceHolder
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.BitchatMessageType
|
import com.bitchat.android.model.BitchatMessageType
|
||||||
@@ -38,12 +39,16 @@ import java.security.MessageDigest
|
|||||||
*/
|
*/
|
||||||
class ChatViewModel(
|
class ChatViewModel(
|
||||||
application: Application,
|
application: Application,
|
||||||
initialMeshService: BluetoothMeshService
|
initialMeshService: BluetoothMeshService,
|
||||||
|
initialUnifiedMeshService: MeshService
|
||||||
) : AndroidViewModel(application), BluetoothMeshDelegate {
|
) : AndroidViewModel(application), BluetoothMeshDelegate {
|
||||||
|
|
||||||
// Made var to support mesh service replacement after panic clear
|
// Made var to support mesh service replacement after panic clear
|
||||||
var meshService: BluetoothMeshService = initialMeshService
|
var meshService: BluetoothMeshService = initialMeshService
|
||||||
private set
|
private set
|
||||||
|
private var unifiedMeshService: MeshService = initialUnifiedMeshService
|
||||||
|
private val mesh: MeshService
|
||||||
|
get() = unifiedMeshService
|
||||||
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
|
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -95,7 +100,7 @@ class ChatViewModel(
|
|||||||
private val noiseSessionDelegate = object : NoiseSessionDelegate {
|
private val noiseSessionDelegate = object : NoiseSessionDelegate {
|
||||||
override fun hasEstablishedSession(peerID: String): Boolean = hasEstablishedSessionOnAnyLocalTransport(peerID)
|
override fun hasEstablishedSession(peerID: String): Boolean = hasEstablishedSessionOnAnyLocalTransport(peerID)
|
||||||
override fun initiateHandshake(peerID: String) = initiateNoiseHandshakeOnBestLocalTransport(peerID)
|
override fun initiateHandshake(peerID: String) = initiateNoiseHandshakeOnBestLocalTransport(peerID)
|
||||||
override fun getMyPeerID(): String = meshService.myPeerID
|
override fun getMyPeerID(): String = mesh.myPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
val privateChatManager = PrivateChatManager(state, messageManager, dataManager, noiseSessionDelegate)
|
val privateChatManager = PrivateChatManager(state, messageManager, dataManager, noiseSessionDelegate)
|
||||||
@@ -109,7 +114,7 @@ class ChatViewModel(
|
|||||||
private val verificationHandler = VerificationHandler(
|
private val verificationHandler = VerificationHandler(
|
||||||
context = application.applicationContext,
|
context = application.applicationContext,
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
getMeshService = { meshService },
|
getMeshService = { mesh },
|
||||||
identityManager = identityManager,
|
identityManager = identityManager,
|
||||||
state = state,
|
state = state,
|
||||||
notificationManager = notificationManager,
|
notificationManager = notificationManager,
|
||||||
@@ -118,7 +123,7 @@ class ChatViewModel(
|
|||||||
val verifiedFingerprints = verificationHandler.verifiedFingerprints
|
val verifiedFingerprints = verificationHandler.verifiedFingerprints
|
||||||
|
|
||||||
// Media file sending manager
|
// Media file sending manager
|
||||||
private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager) { meshService }
|
private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager) { mesh }
|
||||||
|
|
||||||
// Delegate handler for mesh callbacks
|
// Delegate handler for mesh callbacks
|
||||||
private val meshDelegateHandler = MeshDelegateHandler(
|
private val meshDelegateHandler = MeshDelegateHandler(
|
||||||
@@ -129,8 +134,8 @@ class ChatViewModel(
|
|||||||
notificationManager = notificationManager,
|
notificationManager = notificationManager,
|
||||||
coroutineScope = viewModelScope,
|
coroutineScope = viewModelScope,
|
||||||
onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(application.applicationContext) },
|
onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(application.applicationContext) },
|
||||||
getMyPeerID = { meshService.myPeerID },
|
getMyPeerID = { mesh.myPeerID },
|
||||||
getMeshService = { meshService }
|
getMeshService = { mesh }
|
||||||
)
|
)
|
||||||
|
|
||||||
// New Geohash architecture ViewModel (replaces God object service usage in UI path)
|
// New Geohash architecture ViewModel (replaces God object service usage in UI path)
|
||||||
@@ -184,6 +189,18 @@ class ChatViewModel(
|
|||||||
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
|
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
|
||||||
val teleportedGeo: StateFlow<Set<String>> = state.teleportedGeo
|
val teleportedGeo: StateFlow<Set<String>> = state.teleportedGeo
|
||||||
val geohashParticipantCounts: StateFlow<Map<String, Int>> = state.geohashParticipantCounts
|
val geohashParticipantCounts: StateFlow<Map<String, Int>> = state.geohashParticipantCounts
|
||||||
|
val meshServiceFacade: MeshService
|
||||||
|
get() = mesh
|
||||||
|
val myPeerID: String
|
||||||
|
get() = mesh.myPeerID
|
||||||
|
|
||||||
|
fun getMeshPeerFingerprint(peerID: String): String? = mesh.getPeerFingerprint(peerID)
|
||||||
|
|
||||||
|
fun getMeshPeerInfo(peerID: String): com.bitchat.android.mesh.PeerInfo? = mesh.getPeerInfo(peerID)
|
||||||
|
|
||||||
|
fun initiateMeshHandshake(peerID: String) {
|
||||||
|
mesh.initiateNoiseHandshake(peerID)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Note: Mesh service delegate is now set by MainActivity
|
// Note: Mesh service delegate is now set by MainActivity
|
||||||
@@ -208,7 +225,7 @@ class ChatViewModel(
|
|||||||
// Recompute unread set using SeenMessageStore for robustness across Activity recreation
|
// Recompute unread set using SeenMessageStore for robustness across Activity recreation
|
||||||
try {
|
try {
|
||||||
val seen = com.bitchat.android.services.SeenMessageStore.getInstance(getApplication())
|
val seen = com.bitchat.android.services.SeenMessageStore.getInstance(getApplication())
|
||||||
val myNick = state.getNicknameValue() ?: meshService.myPeerID
|
val myNick = state.getNicknameValue() ?: mesh.myPeerID
|
||||||
val unread = mutableSetOf<String>()
|
val unread = mutableSetOf<String>()
|
||||||
byPeer.forEach { (peer, list) ->
|
byPeer.forEach { (peer, list) ->
|
||||||
if (list.any { msg -> msg.sender != myNick && !seen.hasRead(msg.id) }) unread.add(peer)
|
if (list.any { msg -> msg.sender != myNick && !seen.hasRead(msg.id) }) unread.add(peer)
|
||||||
@@ -299,7 +316,7 @@ class ChatViewModel(
|
|||||||
// Ensure NostrTransport knows our mesh peer ID for embedded packets
|
// Ensure NostrTransport knows our mesh peer ID for embedded packets
|
||||||
try {
|
try {
|
||||||
val nostrTransport = com.bitchat.android.nostr.NostrTransport.getInstance(getApplication())
|
val nostrTransport = com.bitchat.android.nostr.NostrTransport.getInstance(getApplication())
|
||||||
nostrTransport.senderPeerID = meshService.myPeerID
|
nostrTransport.senderPeerID = mesh.myPeerID
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
|
|
||||||
// Note: Mesh service is now started by MainActivity
|
// Note: Mesh service is now started by MainActivity
|
||||||
@@ -317,7 +334,7 @@ class ChatViewModel(
|
|||||||
fun setNickname(newNickname: String) {
|
fun setNickname(newNickname: String) {
|
||||||
state.setNickname(newNickname)
|
state.setNickname(newNickname)
|
||||||
dataManager.saveNickname(newNickname)
|
dataManager.saveNickname(newNickname)
|
||||||
meshService.sendBroadcastAnnounce()
|
mesh.sendBroadcastAnnounce()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,7 +380,7 @@ class ChatViewModel(
|
|||||||
// MARK: - Channel Management (delegated)
|
// MARK: - Channel Management (delegated)
|
||||||
|
|
||||||
fun joinChannel(channel: String, password: String? = null): Boolean {
|
fun joinChannel(channel: String, password: String? = null): Boolean {
|
||||||
return channelManager.joinChannel(channel, password, meshService.myPeerID)
|
return channelManager.joinChannel(channel, password, mesh.myPeerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchToChannel(channel: String?) {
|
fun switchToChannel(channel: String?) {
|
||||||
@@ -372,7 +389,7 @@ class ChatViewModel(
|
|||||||
|
|
||||||
fun leaveChannel(channel: String) {
|
fun leaveChannel(channel: String) {
|
||||||
channelManager.leaveChannel(channel)
|
channelManager.leaveChannel(channel)
|
||||||
meshService.sendMessage("left $channel", emptyList(), null)
|
mesh.sendMessage("left $channel", emptyList(), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Chat Management (delegated)
|
// MARK: - Private Chat Management (delegated)
|
||||||
@@ -383,7 +400,7 @@ class ChatViewModel(
|
|||||||
ensureGeohashDMSubscriptionIfNeeded(peerID)
|
ensureGeohashDMSubscriptionIfNeeded(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
val success = privateChatManager.startPrivateChat(peerID, meshService)
|
val success = privateChatManager.startPrivateChat(peerID, mesh)
|
||||||
if (success) {
|
if (success) {
|
||||||
// Notify notification manager about current private chat
|
// Notify notification manager about current private chat
|
||||||
setCurrentPrivateChatPeer(peerID)
|
setCurrentPrivateChatPeer(peerID)
|
||||||
@@ -420,7 +437,7 @@ class ChatViewModel(
|
|||||||
val unreadKeys = state.getUnreadPrivateMessagesValue()
|
val unreadKeys = state.getUnreadPrivateMessagesValue()
|
||||||
if (unreadKeys.isEmpty()) return
|
if (unreadKeys.isEmpty()) return
|
||||||
|
|
||||||
val me = state.getNicknameValue() ?: meshService.myPeerID
|
val me = state.getNicknameValue() ?: mesh.myPeerID
|
||||||
val chats = state.getPrivateChatsValue()
|
val chats = state.getPrivateChatsValue()
|
||||||
|
|
||||||
// Pick the latest incoming message among unread conversations
|
// Pick the latest incoming message among unread conversations
|
||||||
@@ -451,8 +468,8 @@ class ChatViewModel(
|
|||||||
val canonical = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
|
val canonical = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
|
||||||
selectedPeerID = targetKey,
|
selectedPeerID = targetKey,
|
||||||
connectedPeers = state.getConnectedPeersValue(),
|
connectedPeers = state.getConnectedPeersValue(),
|
||||||
meshNoiseKeyForPeer = { pid -> meshService.getPeerInfo(pid)?.noisePublicKey },
|
meshNoiseKeyForPeer = { pid -> mesh.getPeerInfo(pid)?.noisePublicKey },
|
||||||
meshHasPeer = { pid -> meshService.getPeerInfo(pid)?.isConnected == true },
|
meshHasPeer = { pid -> mesh.getPeerInfo(pid)?.isConnected == true },
|
||||||
nostrPubHexForAlias = { alias -> com.bitchat.android.nostr.GeohashAliasRegistry.get(alias) },
|
nostrPubHexForAlias = { alias -> com.bitchat.android.nostr.GeohashAliasRegistry.get(alias) },
|
||||||
findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) }
|
findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) }
|
||||||
)
|
)
|
||||||
@@ -476,25 +493,23 @@ class ChatViewModel(
|
|||||||
// Check for commands
|
// Check for commands
|
||||||
if (content.startsWith("/")) {
|
if (content.startsWith("/")) {
|
||||||
val selectedLocationForCommand = state.selectedLocationChannel.value
|
val selectedLocationForCommand = state.selectedLocationChannel.value
|
||||||
commandProcessor.processCommand(content, meshService, meshService.myPeerID, { messageContent, mentions, channel ->
|
commandProcessor.processCommand(content, mesh, mesh.myPeerID, { messageContent, mentions, channel ->
|
||||||
if (selectedLocationForCommand is com.bitchat.android.geohash.ChannelID.Location) {
|
if (selectedLocationForCommand is com.bitchat.android.geohash.ChannelID.Location) {
|
||||||
// Route command-generated public messages via Nostr in geohash channels
|
// Route command-generated public messages via Nostr in geohash channels
|
||||||
geohashViewModel.sendGeohashMessage(
|
geohashViewModel.sendGeohashMessage(
|
||||||
messageContent,
|
messageContent,
|
||||||
selectedLocationForCommand.channel,
|
selectedLocationForCommand.channel,
|
||||||
meshService.myPeerID,
|
mesh.myPeerID,
|
||||||
state.getNicknameValue()
|
state.getNicknameValue()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Default: route via mesh + Wi‑Fi Aware
|
mesh.sendMessage(messageContent, mentions, channel)
|
||||||
meshService.sendMessage(messageContent, mentions, channel)
|
|
||||||
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(messageContent, mentions, channel) } catch (_: Exception) {}
|
|
||||||
}
|
}
|
||||||
}, this)
|
}, this)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val mentions = messageManager.parseMentions(content, meshService.getPeerNicknames().values.toSet(), state.getNicknameValue())
|
val mentions = messageManager.parseMentions(content, mesh.getPeerNicknames().values.toSet(), state.getNicknameValue())
|
||||||
// REMOVED: Auto-join mentioned channels feature that was incorrectly parsing hashtags from @mentions
|
// REMOVED: Auto-join mentioned channels feature that was incorrectly parsing hashtags from @mentions
|
||||||
// This was causing messages like "test @jack#1234 test" to auto-join channel "#1234"
|
// This was causing messages like "test @jack#1234 test" to auto-join channel "#1234"
|
||||||
|
|
||||||
@@ -506,13 +521,13 @@ class ChatViewModel(
|
|||||||
selectedPeer = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
|
selectedPeer = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
|
||||||
selectedPeerID = selectedPeer,
|
selectedPeerID = selectedPeer,
|
||||||
connectedPeers = state.getConnectedPeersValue(),
|
connectedPeers = state.getConnectedPeersValue(),
|
||||||
meshNoiseKeyForPeer = { pid -> meshService.getPeerInfo(pid)?.noisePublicKey },
|
meshNoiseKeyForPeer = { pid -> mesh.getPeerInfo(pid)?.noisePublicKey },
|
||||||
meshHasPeer = { pid -> meshService.getPeerInfo(pid)?.isConnected == true },
|
meshHasPeer = { pid -> mesh.getPeerInfo(pid)?.isConnected == true },
|
||||||
nostrPubHexForAlias = { alias -> com.bitchat.android.nostr.GeohashAliasRegistry.get(alias) },
|
nostrPubHexForAlias = { alias -> com.bitchat.android.nostr.GeohashAliasRegistry.get(alias) },
|
||||||
findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) }
|
findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) }
|
||||||
).also { canonical ->
|
).also { canonical ->
|
||||||
if (canonical != state.getSelectedPrivateChatPeerValue()) {
|
if (canonical != state.getSelectedPrivateChatPeerValue()) {
|
||||||
privateChatManager.startPrivateChat(canonical, meshService)
|
privateChatManager.startPrivateChat(canonical, mesh)
|
||||||
// If we're in the private chat sheet, update its active peer too
|
// If we're in the private chat sheet, update its active peer too
|
||||||
if (state.getPrivateChatSheetPeerValue() != null) {
|
if (state.getPrivateChatSheetPeerValue() != null) {
|
||||||
showPrivateChatSheet(canonical)
|
showPrivateChatSheet(canonical)
|
||||||
@@ -526,10 +541,10 @@ class ChatViewModel(
|
|||||||
selectedPeer,
|
selectedPeer,
|
||||||
recipientNickname,
|
recipientNickname,
|
||||||
state.getNicknameValue(),
|
state.getNicknameValue(),
|
||||||
meshService.myPeerID
|
mesh.myPeerID
|
||||||
) { messageContent, peerID, recipientNicknameParam, messageId ->
|
) { messageContent, peerID, recipientNicknameParam, messageId ->
|
||||||
// Route via MessageRouter (mesh when connected+established, else Nostr)
|
// Route via MessageRouter (mesh when connected+established, else Nostr)
|
||||||
val router = com.bitchat.android.services.MessageRouter.getInstance(getApplication(), meshService)
|
val router = com.bitchat.android.services.MessageRouter.getInstance(getApplication(), mesh)
|
||||||
router.sendPrivate(messageContent, peerID, recipientNicknameParam, messageId)
|
router.sendPrivate(messageContent, peerID, recipientNicknameParam, messageId)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -537,21 +552,21 @@ class ChatViewModel(
|
|||||||
val selectedLocationChannel = state.selectedLocationChannel.value
|
val selectedLocationChannel = state.selectedLocationChannel.value
|
||||||
if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location) {
|
if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location) {
|
||||||
// Send to geohash channel via Nostr ephemeral event
|
// Send to geohash channel via Nostr ephemeral event
|
||||||
geohashViewModel.sendGeohashMessage(content, selectedLocationChannel.channel, meshService.myPeerID, state.getNicknameValue())
|
geohashViewModel.sendGeohashMessage(content, selectedLocationChannel.channel, mesh.myPeerID, state.getNicknameValue())
|
||||||
} else {
|
} else {
|
||||||
// Send public/channel message via mesh
|
// Send public/channel message via mesh
|
||||||
val message = BitchatMessage(
|
val message = BitchatMessage(
|
||||||
sender = state.getNicknameValue() ?: meshService.myPeerID,
|
sender = state.getNicknameValue() ?: mesh.myPeerID,
|
||||||
content = content,
|
content = content,
|
||||||
timestamp = Date(),
|
timestamp = Date(),
|
||||||
isRelay = false,
|
isRelay = false,
|
||||||
senderPeerID = meshService.myPeerID,
|
senderPeerID = mesh.myPeerID,
|
||||||
mentions = if (mentions.isNotEmpty()) mentions else null,
|
mentions = if (mentions.isNotEmpty()) mentions else null,
|
||||||
channel = currentChannelValue
|
channel = currentChannelValue
|
||||||
)
|
)
|
||||||
|
|
||||||
if (currentChannelValue != null) {
|
if (currentChannelValue != null) {
|
||||||
channelManager.addChannelMessage(currentChannelValue, message, meshService.myPeerID)
|
channelManager.addChannelMessage(currentChannelValue, message, mesh.myPeerID)
|
||||||
|
|
||||||
// Check if encrypted channel
|
// Check if encrypted channel
|
||||||
if (channelManager.hasChannelKey(currentChannelValue)) {
|
if (channelManager.hasChannelKey(currentChannelValue)) {
|
||||||
@@ -560,25 +575,20 @@ class ChatViewModel(
|
|||||||
mentions,
|
mentions,
|
||||||
currentChannelValue,
|
currentChannelValue,
|
||||||
state.getNicknameValue(),
|
state.getNicknameValue(),
|
||||||
meshService.myPeerID,
|
mesh.myPeerID,
|
||||||
onEncryptedPayload = { encryptedData ->
|
onEncryptedPayload = { encryptedData ->
|
||||||
// Send encrypted payload announcement over both transports for reachability
|
mesh.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)
|
mesh.sendMessage(content, mentions, currentChannelValue)
|
||||||
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, currentChannelValue) } catch (_: Exception) {}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
meshService.sendMessage(content, mentions, currentChannelValue)
|
mesh.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)
|
mesh.sendMessage(content, mentions, null)
|
||||||
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(content, mentions, null) } catch (_: Exception) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,7 +597,7 @@ class ChatViewModel(
|
|||||||
// MARK: - Utility Functions
|
// MARK: - Utility Functions
|
||||||
|
|
||||||
fun getPeerIDForNickname(nickname: String): String? {
|
fun getPeerIDForNickname(nickname: String): String? {
|
||||||
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
return mesh.getPeerNicknames().entries.find { it.value == nickname }?.key
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleFavorite(peerID: String) {
|
fun toggleFavorite(peerID: String) {
|
||||||
@@ -597,10 +607,10 @@ class ChatViewModel(
|
|||||||
// Persist relationship in FavoritesPersistenceService
|
// Persist relationship in FavoritesPersistenceService
|
||||||
try {
|
try {
|
||||||
var noiseKey: ByteArray? = null
|
var noiseKey: ByteArray? = null
|
||||||
var nickname: String = meshService.getPeerNicknames()[peerID] ?: peerID
|
var nickname: String = mesh.getPeerNicknames()[peerID] ?: peerID
|
||||||
|
|
||||||
// Case 1: Live mesh peer with known info
|
// Case 1: Live mesh peer with known info
|
||||||
val peerInfo = meshService.getPeerInfo(peerID)
|
val peerInfo = mesh.getPeerInfo(peerID)
|
||||||
if (peerInfo?.noisePublicKey != null) {
|
if (peerInfo?.noisePublicKey != null) {
|
||||||
noiseKey = peerInfo.noisePublicKey
|
noiseKey = peerInfo.noisePublicKey
|
||||||
nickname = peerInfo.nickname
|
nickname = peerInfo.nickname
|
||||||
@@ -630,22 +640,9 @@ class ChatViewModel(
|
|||||||
|
|
||||||
// Send favorite notification via mesh or Nostr with our npub if available
|
// Send favorite notification via mesh or Nostr with our npub if available
|
||||||
try {
|
try {
|
||||||
val myNostr = com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(getApplication())
|
com.bitchat.android.services.MessageRouter
|
||||||
val announcementContent = if (isNowFavorite) "[FAVORITED]:${myNostr?.npub ?: ""}" else "[UNFAVORITED]:${myNostr?.npub ?: ""}"
|
.getInstance(getApplication(), mesh)
|
||||||
// Prefer mesh if session established, else try Nostr
|
.sendFavoriteNotification(peerID, isNowFavorite)
|
||||||
if (meshService.hasEstablishedSession(peerID)) {
|
|
||||||
// Reuse existing private message path for notifications
|
|
||||||
meshService.sendPrivateMessage(
|
|
||||||
announcementContent,
|
|
||||||
peerID,
|
|
||||||
nickname,
|
|
||||||
java.util.UUID.randomUUID().toString()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val nostrTransport = com.bitchat.android.nostr.NostrTransport.getInstance(getApplication())
|
|
||||||
nostrTransport.senderPeerID = meshService.myPeerID
|
|
||||||
nostrTransport.sendFavoriteNotification(peerID, isNowFavorite)
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
@@ -662,21 +659,9 @@ class ChatViewModel(
|
|||||||
Log.i("ChatViewModel", "==============================")
|
Log.i("ChatViewModel", "==============================")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getWifiAwareService(): com.bitchat.android.mesh.MeshService? {
|
|
||||||
return try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isConnectedOnMesh(peerID: String): Boolean {
|
private fun isConnectedOnMesh(peerID: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
meshService.getPeerInfo(peerID)?.isConnected == true
|
mesh.getPeerInfo(peerID)?.isConnected == true
|
||||||
} catch (_: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isConnectedOnService(service: com.bitchat.android.mesh.MeshService?, peerID: String): Boolean {
|
|
||||||
return try {
|
|
||||||
service != null && service.getPeerInfo(peerID)?.isConnected == true
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -684,55 +669,28 @@ class ChatViewModel(
|
|||||||
|
|
||||||
private fun hasEstablishedSessionOnMesh(peerID: String): Boolean {
|
private fun hasEstablishedSessionOnMesh(peerID: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
meshService.getPeerInfo(peerID)?.isConnected == true &&
|
mesh.getPeerInfo(peerID)?.isConnected == true &&
|
||||||
meshService.hasEstablishedSession(peerID)
|
mesh.hasEstablishedSession(peerID)
|
||||||
} catch (_: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hasEstablishedSessionOnService(service: com.bitchat.android.mesh.MeshService?, peerID: String): Boolean {
|
|
||||||
return try {
|
|
||||||
service != null &&
|
|
||||||
service.getPeerInfo(peerID)?.isConnected == true &&
|
|
||||||
service.hasEstablishedSession(peerID)
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasEstablishedSessionOnAnyLocalTransport(peerID: String): Boolean {
|
private fun hasEstablishedSessionOnAnyLocalTransport(peerID: String): Boolean {
|
||||||
return hasEstablishedSessionOnMesh(peerID) ||
|
return hasEstablishedSessionOnMesh(peerID)
|
||||||
hasEstablishedSessionOnService(getWifiAwareService(), peerID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initiateNoiseHandshakeOnBestLocalTransport(peerID: String) {
|
private fun initiateNoiseHandshakeOnBestLocalTransport(peerID: String) {
|
||||||
val aware = getWifiAwareService()
|
mesh.initiateNoiseHandshake(peerID)
|
||||||
when {
|
|
||||||
isConnectedOnMesh(peerID) -> meshService.initiateNoiseHandshake(peerID)
|
|
||||||
isConnectedOnService(aware, peerID) -> aware?.initiateNoiseHandshake(peerID)
|
|
||||||
else -> meshService.initiateNoiseHandshake(peerID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun nicknameForPeer(peerID: String): String? {
|
private fun nicknameForPeer(peerID: String): String? {
|
||||||
return state.peerNicknames.value[peerID]
|
return state.peerNicknames.value[peerID]
|
||||||
?: try { meshService.getPeerNicknames()[peerID] } catch (_: Exception) { null }
|
?: try { mesh.getPeerNicknames()[peerID] } catch (_: Exception) { null }
|
||||||
?: try { getWifiAwareService()?.getPeerNicknames()?.get(peerID) } catch (_: Exception) { null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sessionStateForPeer(peerID: String): NoiseSession.NoiseSessionState {
|
private fun sessionStateForPeer(peerID: String): NoiseSession.NoiseSessionState {
|
||||||
val meshState = try { meshService.getSessionState(peerID) } catch (_: Exception) { NoiseSession.NoiseSessionState.Uninitialized }
|
return try { mesh.getSessionState(peerID) } catch (_: Exception) { NoiseSession.NoiseSessionState.Uninitialized }
|
||||||
val awareState = try { getWifiAwareService()?.getSessionState(peerID) } catch (_: Exception) { null }
|
|
||||||
return when {
|
|
||||||
meshState is NoiseSession.NoiseSessionState.Established -> meshState
|
|
||||||
awareState is NoiseSession.NoiseSessionState.Established -> awareState
|
|
||||||
meshState is NoiseSession.NoiseSessionState.Handshaking -> meshState
|
|
||||||
awareState is NoiseSession.NoiseSessionState.Handshaking -> awareState
|
|
||||||
meshState !is NoiseSession.NoiseSessionState.Uninitialized -> meshState
|
|
||||||
awareState != null -> awareState
|
|
||||||
else -> meshState
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -766,7 +724,7 @@ class ChatViewModel(
|
|||||||
val old = prevStates[peerID]
|
val old = prevStates[peerID]
|
||||||
if (old != "established" && newState == "established") {
|
if (old != "established" && newState == "established") {
|
||||||
com.bitchat.android.services.MessageRouter
|
com.bitchat.android.services.MessageRouter
|
||||||
.getInstance(getApplication(), meshService)
|
.getInstance(getApplication(), mesh)
|
||||||
.onSessionEstablished(peerID)
|
.onSessionEstablished(peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -775,7 +733,7 @@ class ChatViewModel(
|
|||||||
state.setPeerFingerprints(fingerprints)
|
state.setPeerFingerprints(fingerprints)
|
||||||
fingerprints.forEach { (peerID, fingerprint) ->
|
fingerprints.forEach { (peerID, fingerprint) ->
|
||||||
identityManager.cachePeerFingerprint(peerID, fingerprint)
|
identityManager.cachePeerFingerprint(peerID, fingerprint)
|
||||||
val info = try { meshService.getPeerInfo(peerID) } catch (_: Exception) { null }
|
val info = try { mesh.getPeerInfo(peerID) } catch (_: Exception) { null }
|
||||||
val noiseKeyHex = info?.noisePublicKey?.hexEncodedString()
|
val noiseKeyHex = info?.noisePublicKey?.hexEncodedString()
|
||||||
if (noiseKeyHex != null) {
|
if (noiseKeyHex != null) {
|
||||||
identityManager.cachePeerNoiseKey(peerID, noiseKeyHex)
|
identityManager.cachePeerNoiseKey(peerID, noiseKeyHex)
|
||||||
@@ -786,23 +744,14 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge nicknames from BLE and Wi‑Fi Aware to display names for all peers
|
state.setPeerNicknames(mesh.getPeerNicknames())
|
||||||
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()
|
state.setPeerRSSI(mesh.getPeerRSSI())
|
||||||
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 ->
|
||||||
val ble = meshService.getPeerInfo(pid)?.isDirectConnection == true
|
mesh.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) { }
|
||||||
@@ -839,7 +788,7 @@ class ChatViewModel(
|
|||||||
// MARK: - Debug and Troubleshooting
|
// MARK: - Debug and Troubleshooting
|
||||||
|
|
||||||
fun getDebugStatus(): String {
|
fun getDebugStatus(): String {
|
||||||
return meshService.getDebugStatus()
|
return mesh.getDebugStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setCurrentPrivateChatPeer(peerID: String?) {
|
fun setCurrentPrivateChatPeer(peerID: String?) {
|
||||||
@@ -936,7 +885,7 @@ class ChatViewModel(
|
|||||||
// MARK: - Mention Autocomplete
|
// MARK: - Mention Autocomplete
|
||||||
|
|
||||||
fun updateMentionSuggestions(input: String) {
|
fun updateMentionSuggestions(input: String) {
|
||||||
commandProcessor.updateMentionSuggestions(input, meshService, this)
|
commandProcessor.updateMentionSuggestions(input, mesh, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectMentionSuggestion(nickname: String, currentText: String): String {
|
fun selectMentionSuggestion(nickname: String, currentText: String): String {
|
||||||
@@ -953,10 +902,6 @@ class ChatViewModel(
|
|||||||
meshDelegateHandler.didUpdatePeerList(peers)
|
meshDelegateHandler.didUpdatePeerList(peers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onWifiPeersUpdated(peers: List<String>) {
|
|
||||||
meshDelegateHandler.onWifiPeersUpdated(peers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
||||||
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
|
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
|
||||||
}
|
}
|
||||||
@@ -1040,7 +985,7 @@ class ChatViewModel(
|
|||||||
// Recreate mesh service with fresh identity
|
// Recreate mesh service with fresh identity
|
||||||
recreateMeshServiceAfterPanic()
|
recreateMeshServiceAfterPanic()
|
||||||
|
|
||||||
Log.w(TAG, "🚨 PANIC MODE COMPLETED - New identity: ${meshService.myPeerID}")
|
Log.w(TAG, "🚨 PANIC MODE COMPLETED - New identity: ${mesh.myPeerID}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1048,25 +993,27 @@ class ChatViewModel(
|
|||||||
* This ensures the new cryptographic keys are used for a new peer ID.
|
* This ensures the new cryptographic keys are used for a new peer ID.
|
||||||
*/
|
*/
|
||||||
private fun recreateMeshServiceAfterPanic() {
|
private fun recreateMeshServiceAfterPanic() {
|
||||||
val oldPeerID = meshService.myPeerID
|
val oldPeerID = mesh.myPeerID
|
||||||
|
|
||||||
// Clear the holder so getOrCreate() returns a fresh instance
|
// Clear the holder so getOrCreate() returns a fresh instance
|
||||||
MeshServiceHolder.clear()
|
MeshServiceHolder.clear()
|
||||||
|
|
||||||
// Create fresh mesh service with new identity (keys were regenerated in clearAllCryptographicData)
|
// Create fresh mesh service with new identity (keys were regenerated in clearAllCryptographicData)
|
||||||
val freshMeshService = MeshServiceHolder.getOrCreate(getApplication())
|
val freshMeshService = MeshServiceHolder.getOrCreate(getApplication())
|
||||||
|
val freshUnifiedMeshService = MeshServiceHolder.getUnifiedOrCreate(getApplication())
|
||||||
|
|
||||||
// Replace our reference and set up the new service
|
// Replace our reference and set up the new service
|
||||||
meshService = freshMeshService
|
meshService = freshMeshService
|
||||||
meshService.delegate = this
|
unifiedMeshService = freshUnifiedMeshService
|
||||||
|
mesh.delegate = this
|
||||||
|
|
||||||
// Restart mesh operations with new identity
|
// Restart mesh operations with new identity
|
||||||
meshService.startServices()
|
mesh.startServices()
|
||||||
meshService.sendBroadcastAnnounce()
|
mesh.sendBroadcastAnnounce()
|
||||||
|
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"✅ Mesh service recreated. Old peerID: $oldPeerID, New peerID: ${meshService.myPeerID}"
|
"✅ Mesh service recreated. Old peerID: $oldPeerID, New peerID: ${mesh.myPeerID}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,7 +1023,7 @@ class ChatViewModel(
|
|||||||
private fun clearAllMeshServiceData() {
|
private fun clearAllMeshServiceData() {
|
||||||
try {
|
try {
|
||||||
// Request mesh service to clear all its internal data
|
// Request mesh service to clear all its internal data
|
||||||
meshService.clearAllInternalData()
|
mesh.clearAllInternalData()
|
||||||
|
|
||||||
Log.d(TAG, "✅ Cleared all mesh service data")
|
Log.d(TAG, "✅ Cleared all mesh service data")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -1090,7 +1037,7 @@ class ChatViewModel(
|
|||||||
private fun clearAllCryptographicData() {
|
private fun clearAllCryptographicData() {
|
||||||
try {
|
try {
|
||||||
// Clear encryption service persistent identity (Ed25519 signing keys)
|
// Clear encryption service persistent identity (Ed25519 signing keys)
|
||||||
meshService.clearAllEncryptionData()
|
mesh.clearAllEncryptionData()
|
||||||
|
|
||||||
// Clear secure identity state (if used)
|
// Clear secure identity state (if used)
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class CommandProcessor(
|
|||||||
|
|
||||||
// MARK: - Command Processing
|
// MARK: - Command Processing
|
||||||
|
|
||||||
fun processCommand(command: String, meshService: BluetoothMeshService, myPeerID: String, onSendMessage: (String, List<String>, String?) -> Unit, viewModel: ChatViewModel? = null): Boolean {
|
fun processCommand(command: String, meshService: MeshService, myPeerID: String, onSendMessage: (String, List<String>, String?) -> Unit, viewModel: ChatViewModel? = null): Boolean {
|
||||||
if (!command.startsWith("/")) return false
|
if (!command.startsWith("/")) return false
|
||||||
|
|
||||||
val parts = command.split(" ")
|
val parts = command.split(" ")
|
||||||
@@ -77,7 +77,7 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMessageCommand(parts: List<String>, meshService: BluetoothMeshService, viewModel: ChatViewModel?) {
|
private fun handleMessageCommand(parts: List<String>, meshService: MeshService, viewModel: ChatViewModel?) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
val peerID = getPeerIDForNickname(targetName, meshService)
|
||||||
@@ -128,7 +128,7 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleWhoCommand(meshService: BluetoothMeshService, viewModel: ChatViewModel? = null) {
|
private fun handleWhoCommand(meshService: MeshService, viewModel: ChatViewModel? = null) {
|
||||||
// Channel-aware who command (matches iOS behavior)
|
// Channel-aware who command (matches iOS behavior)
|
||||||
val (peerList, contextDescription) = if (viewModel != null) {
|
val (peerList, contextDescription) = if (viewModel != null) {
|
||||||
when (val selectedChannel = viewModel.selectedLocationChannel.value) {
|
when (val selectedChannel = viewModel.selectedLocationChannel.value) {
|
||||||
@@ -247,7 +247,7 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleBlockCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
private fun handleBlockCommand(parts: List<String>, meshService: MeshService) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
privateChatManager.blockPeerByNickname(targetName, meshService)
|
privateChatManager.blockPeerByNickname(targetName, meshService)
|
||||||
@@ -264,7 +264,7 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUnblockCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
private fun handleUnblockCommand(parts: List<String>, meshService: MeshService) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
privateChatManager.unblockPeerByNickname(targetName, meshService)
|
privateChatManager.unblockPeerByNickname(targetName, meshService)
|
||||||
@@ -283,7 +283,7 @@ class CommandProcessor(
|
|||||||
parts: List<String>,
|
parts: List<String>,
|
||||||
verb: String,
|
verb: String,
|
||||||
object_: String,
|
object_: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: MeshService,
|
||||||
myPeerID: String,
|
myPeerID: String,
|
||||||
onSendMessage: (String, List<String>, String?) -> Unit,
|
onSendMessage: (String, List<String>, String?) -> Unit,
|
||||||
viewModel: ChatViewModel?
|
viewModel: ChatViewModel?
|
||||||
@@ -423,7 +423,7 @@ class CommandProcessor(
|
|||||||
|
|
||||||
// MARK: - Mention Autocomplete
|
// MARK: - Mention Autocomplete
|
||||||
|
|
||||||
fun updateMentionSuggestions(input: String, meshService: BluetoothMeshService, viewModel: ChatViewModel? = null) {
|
fun updateMentionSuggestions(input: String, meshService: MeshService, viewModel: ChatViewModel? = null) {
|
||||||
// Check if input contains @ and we're at the end of a word or at the end of input
|
// Check if input contains @ and we're at the end of a word or at the end of input
|
||||||
val atIndex = input.lastIndexOf('@')
|
val atIndex = input.lastIndexOf('@')
|
||||||
if (atIndex == -1) {
|
if (atIndex == -1) {
|
||||||
@@ -503,31 +503,21 @@ class CommandProcessor(
|
|||||||
|
|
||||||
// MARK: - Utility Functions
|
// MARK: - Utility Functions
|
||||||
|
|
||||||
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
|
private fun getPeerIDForNickname(nickname: String, meshService: MeshService): String? {
|
||||||
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
||||||
?: try {
|
|
||||||
com.bitchat.android.wifiaware.WifiAwareController.getService()
|
|
||||||
?.getPeerNicknames()
|
|
||||||
?.entries
|
|
||||||
?.find { it.value == nickname }
|
|
||||||
?.key
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
private fun getPeerNickname(peerID: String, meshService: MeshService): String {
|
||||||
return meshService.getPeerNicknames()[peerID]
|
return meshService.getPeerNicknames()[peerID]
|
||||||
?: try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerNicknames()?.get(peerID) } catch (_: Exception) { null }
|
|
||||||
?: peerID
|
?: peerID
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMyPeerID(meshService: BluetoothMeshService): String {
|
private fun getMyPeerID(meshService: MeshService): String {
|
||||||
return meshService.myPeerID
|
return meshService.myPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendPrivateMessageVia(
|
private fun sendPrivateMessageVia(
|
||||||
meshService: BluetoothMeshService,
|
meshService: MeshService,
|
||||||
content: String,
|
content: String,
|
||||||
peerID: String,
|
peerID: String,
|
||||||
recipientNickname: String,
|
recipientNickname: String,
|
||||||
|
|||||||
@@ -656,7 +656,7 @@ private fun meshTitleWithCount(viewModel: ChatViewModel): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun meshCount(viewModel: ChatViewModel): Int {
|
private fun meshCount(viewModel: ChatViewModel): Int {
|
||||||
val myID = viewModel.meshService.myPeerID
|
val myID = viewModel.myPeerID
|
||||||
return viewModel.connectedPeers.value?.count { peerID ->
|
return viewModel.connectedPeers.value?.count { peerID ->
|
||||||
peerID != myID
|
peerID != myID
|
||||||
} ?: 0
|
} ?: 0
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ fun MessageWithMatrixAnimation(
|
|||||||
message: com.bitchat.android.model.BitchatMessage,
|
message: com.bitchat.android.model.BitchatMessage,
|
||||||
messages: List<com.bitchat.android.model.BitchatMessage> = emptyList(),
|
messages: List<com.bitchat.android.model.BitchatMessage> = emptyList(),
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: com.bitchat.android.mesh.BluetoothMeshService,
|
meshService: com.bitchat.android.mesh.MeshService,
|
||||||
colorScheme: androidx.compose.material3.ColorScheme,
|
colorScheme: androidx.compose.material3.ColorScheme,
|
||||||
timeFormatter: java.text.SimpleDateFormat,
|
timeFormatter: java.text.SimpleDateFormat,
|
||||||
onNicknameClick: ((String) -> Unit)?,
|
onNicknameClick: ((String) -> Unit)?,
|
||||||
@@ -124,7 +124,7 @@ fun MessageWithMatrixAnimation(
|
|||||||
private fun AnimatedMessageDisplay(
|
private fun AnimatedMessageDisplay(
|
||||||
message: com.bitchat.android.model.BitchatMessage,
|
message: com.bitchat.android.model.BitchatMessage,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: com.bitchat.android.mesh.BluetoothMeshService,
|
meshService: com.bitchat.android.mesh.MeshService,
|
||||||
colorScheme: androidx.compose.material3.ColorScheme,
|
colorScheme: androidx.compose.material3.ColorScheme,
|
||||||
timeFormatter: java.text.SimpleDateFormat,
|
timeFormatter: java.text.SimpleDateFormat,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -241,7 +241,7 @@ private fun AnimatedMessageDisplay(
|
|||||||
private fun formatMessageAsAnnotatedStringWithoutTimestamp(
|
private fun formatMessageAsAnnotatedStringWithoutTimestamp(
|
||||||
message: com.bitchat.android.model.BitchatMessage,
|
message: com.bitchat.android.model.BitchatMessage,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: com.bitchat.android.mesh.BluetoothMeshService,
|
meshService: com.bitchat.android.mesh.MeshService,
|
||||||
colorScheme: androidx.compose.material3.ColorScheme
|
colorScheme: androidx.compose.material3.ColorScheme
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
// Get the full formatted text first
|
// Get the full formatted text first
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.model.BitchatFilePacket
|
import com.bitchat.android.model.BitchatFilePacket
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.BitchatMessageType
|
import com.bitchat.android.model.BitchatMessageType
|
||||||
@@ -16,10 +16,10 @@ class MediaSendingManager(
|
|||||||
private val state: ChatState,
|
private val state: ChatState,
|
||||||
private val messageManager: MessageManager,
|
private val messageManager: MessageManager,
|
||||||
private val channelManager: ChannelManager,
|
private val channelManager: ChannelManager,
|
||||||
private val getMeshService: () -> BluetoothMeshService
|
private val getMeshService: () -> MeshService
|
||||||
) {
|
) {
|
||||||
// Helper to get current mesh service (may change after panic clear)
|
// Helper to get current mesh service (may change after panic clear)
|
||||||
private val meshService: BluetoothMeshService
|
private val meshService: MeshService
|
||||||
get() = getMeshService()
|
get() = getMeshService()
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MediaSendingManager"
|
private const val TAG = "MediaSendingManager"
|
||||||
@@ -213,7 +213,6 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +267,6 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.bitchat.android.ui
|
|||||||
|
|
||||||
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
||||||
import com.bitchat.android.ui.NotificationTextUtils
|
import com.bitchat.android.ui.NotificationTextUtils
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryStatus
|
import com.bitchat.android.model.DeliveryStatus
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -21,7 +21,7 @@ class MeshDelegateHandler(
|
|||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val onHapticFeedback: () -> Unit,
|
private val onHapticFeedback: () -> Unit,
|
||||||
private val getMyPeerID: () -> String,
|
private val getMyPeerID: () -> String,
|
||||||
private val getMeshService: () -> BluetoothMeshService
|
private val getMeshService: () -> MeshService
|
||||||
) : BluetoothMeshDelegate {
|
) : BluetoothMeshDelegate {
|
||||||
|
|
||||||
override fun didReceiveMessage(message: BitchatMessage) {
|
override fun didReceiveMessage(message: BitchatMessage) {
|
||||||
@@ -94,30 +94,13 @@ class MeshDelegateHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var blePeers: Set<String> = emptySet()
|
|
||||||
private var wifiPeers: Set<String> = emptySet()
|
|
||||||
|
|
||||||
override fun didUpdatePeerList(peers: List<String>) {
|
override fun didUpdatePeerList(peers: List<String>) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
blePeers = peers.toSet()
|
processPeerUpdate(peers.distinct())
|
||||||
try { com.bitchat.android.services.AppStateStore.setTransportPeers("BLE", peers) } catch (_: Exception) { }
|
|
||||||
processPeerUpdate()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onWifiPeersUpdated(peers: List<String>) {
|
private suspend fun processPeerUpdate(mergedPeers: List<String>) {
|
||||||
coroutineScope.launch {
|
|
||||||
wifiPeers = peers.toSet()
|
|
||||||
try { com.bitchat.android.services.AppStateStore.setTransportPeers("WIFI", peers) } catch (_: Exception) { }
|
|
||||||
processPeerUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun processPeerUpdate() {
|
|
||||||
// Merge peers from multiple transports
|
|
||||||
val mergedPeers = com.bitchat.android.services.AppStateStore.peers.value
|
|
||||||
.ifEmpty { (blePeers + wifiPeers).toList() }
|
|
||||||
|
|
||||||
state.setConnectedPeers(mergedPeers)
|
state.setConnectedPeers(mergedPeers)
|
||||||
state.setIsConnected(mergedPeers.isNotEmpty())
|
state.setIsConnected(mergedPeers.isNotEmpty())
|
||||||
notificationManager.showActiveUserNotification(mergedPeers)
|
notificationManager.showActiveUserNotification(mergedPeers)
|
||||||
@@ -322,16 +305,9 @@ class MeshDelegateHandler(
|
|||||||
if (hasMesh) {
|
if (hasMesh) {
|
||||||
mesh.sendReadReceipt(message.id, senderPeerID, nickname)
|
mesh.sendReadReceipt(message.id, senderPeerID, nickname)
|
||||||
true
|
true
|
||||||
} else {
|
|
||||||
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
|
||||||
val hasAware = try { aware?.getPeerInfo(senderPeerID)?.isConnected == true && aware.hasEstablishedSession(senderPeerID) } catch (_: Exception) { false }
|
|
||||||
if (hasAware) {
|
|
||||||
aware?.sendReadReceipt(message.id, senderPeerID, nickname)
|
|
||||||
true
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ fun PeopleSection(
|
|||||||
// Build mapping of connected peerID -> noise key hex to unify with offline favorites
|
// Build mapping of connected peerID -> noise key hex to unify with offline favorites
|
||||||
val noiseHexByPeerID: Map<String, String> = connectedPeers.associateWith { pid ->
|
val noiseHexByPeerID: Map<String, String> = connectedPeers.associateWith { pid ->
|
||||||
try {
|
try {
|
||||||
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
viewModel.getMeshPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
}.filterValues { it != null }.mapValues { it.value!! }
|
}.filterValues { it != null }.mapValues { it.value!! }
|
||||||
|
|
||||||
@@ -415,7 +415,7 @@ fun PeopleSection(
|
|||||||
val showHash = (baseNameCounts[bName] ?: 0) > 1
|
val showHash = (baseNameCounts[bName] ?: 0) > 1
|
||||||
|
|
||||||
val directMap by viewModel.peerDirect.collectAsStateWithLifecycle()
|
val directMap by viewModel.peerDirect.collectAsStateWithLifecycle()
|
||||||
val isDirectLive = directMap[peerID] ?: try { viewModel.meshService.getPeerInfo(peerID)?.isDirectConnection == true } catch (_: Exception) { false }
|
val isDirectLive = directMap[peerID] ?: try { viewModel.getMeshPeerInfo(peerID)?.isDirectConnection == true } catch (_: Exception) { false }
|
||||||
PeerItem(
|
PeerItem(
|
||||||
peerID = peerID,
|
peerID = peerID,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
@@ -787,6 +787,8 @@ fun PrivateChatSheet(
|
|||||||
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
||||||
|
val wifiAwareConnected by com.bitchat.android.wifiaware.WifiAwareController.connectedPeers.collectAsStateWithLifecycle()
|
||||||
|
val isWifiAware = peerID in wifiAwareConnected.keys
|
||||||
|
|
||||||
// Start private chat when screen opens
|
// Start private chat when screen opens
|
||||||
LaunchedEffect(peerID) {
|
LaunchedEffect(peerID) {
|
||||||
@@ -855,7 +857,7 @@ fun PrivateChatSheet(
|
|||||||
MessagesList(
|
MessagesList(
|
||||||
messages = messages,
|
messages = messages,
|
||||||
currentUserNickname = nickname,
|
currentUserNickname = nickname,
|
||||||
meshService = viewModel.meshService,
|
meshService = viewModel.meshServiceFacade,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
forceScrollToBottom = forceScrollToBottom,
|
forceScrollToBottom = forceScrollToBottom,
|
||||||
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
|
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
|
||||||
@@ -939,10 +941,26 @@ fun PrivateChatSheet(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
|
isNostrPeer -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Public,
|
||||||
|
contentDescription = stringResource(R.string.cd_nostr_reachable),
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = Color(0xFF9C27B0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isWifiAware -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Wifi,
|
||||||
|
contentDescription = "Direct Wi-Fi Aware",
|
||||||
|
modifier = Modifier.size(14.dp),
|
||||||
|
tint = colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
isDirect -> {
|
isDirect -> {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.SettingsInputAntenna,
|
imageVector = Icons.Outlined.Bluetooth,
|
||||||
contentDescription = stringResource(R.string.cd_connected_peers),
|
contentDescription = "Direct Bluetooth",
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(14.dp),
|
||||||
tint = colorScheme.onSurface.copy(alpha = 0.6f)
|
tint = colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
@@ -950,19 +968,11 @@ fun PrivateChatSheet(
|
|||||||
isConnected -> {
|
isConnected -> {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Route,
|
imageVector = Icons.Filled.Route,
|
||||||
contentDescription = stringResource(R.string.cd_ready_for_handshake),
|
contentDescription = "Routed",
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(14.dp),
|
||||||
tint = colorScheme.onSurface.copy(alpha = 0.6f)
|
tint = colorScheme.onSurface.copy(alpha = 0.6f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isNostrPeer -> {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Public,
|
|
||||||
contentDescription = stringResource(R.string.cd_nostr_reachable),
|
|
||||||
modifier = Modifier.size(14.dp),
|
|
||||||
tint = Color(0xFF9C27B0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import android.content.Intent
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryStatus
|
import com.bitchat.android.model.DeliveryStatus
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import com.bitchat.android.ui.media.VoiceNotePlayer
|
import com.bitchat.android.ui.media.VoiceNotePlayer
|
||||||
@@ -58,7 +58,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
fun MessagesList(
|
fun MessagesList(
|
||||||
messages: List<BitchatMessage>,
|
messages: List<BitchatMessage>,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: MeshService,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceScrollToBottom: Boolean = false,
|
forceScrollToBottom: Boolean = false,
|
||||||
onScrolledUpChanged: ((Boolean) -> Unit)? = null,
|
onScrolledUpChanged: ((Boolean) -> Unit)? = null,
|
||||||
@@ -137,7 +137,7 @@ fun MessagesList(
|
|||||||
fun MessageItem(
|
fun MessageItem(
|
||||||
message: BitchatMessage,
|
message: BitchatMessage,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: MeshService,
|
||||||
messages: List<BitchatMessage> = emptyList(),
|
messages: List<BitchatMessage> = emptyList(),
|
||||||
onNicknameClick: ((String) -> Unit)? = null,
|
onNicknameClick: ((String) -> Unit)? = null,
|
||||||
onMessageLongPress: ((BitchatMessage) -> Unit)? = null,
|
onMessageLongPress: ((BitchatMessage) -> Unit)? = null,
|
||||||
@@ -201,7 +201,7 @@ fun MessageItem(
|
|||||||
message: BitchatMessage,
|
message: BitchatMessage,
|
||||||
messages: List<BitchatMessage>,
|
messages: List<BitchatMessage>,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: MeshService,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
timeFormatter: SimpleDateFormat,
|
timeFormatter: SimpleDateFormat,
|
||||||
onNicknameClick: ((String) -> Unit)?,
|
onNicknameClick: ((String) -> Unit)?,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package com.bitchat.android.ui
|
|||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryStatus
|
import com.bitchat.android.model.DeliveryStatus
|
||||||
import com.bitchat.android.mesh.PeerFingerprintManager
|
import com.bitchat.android.mesh.PeerFingerprintManager
|
||||||
|
import com.bitchat.android.mesh.MeshService
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class PrivateChatManager(
|
|||||||
|
|
||||||
// MARK: - Private Chat Lifecycle
|
// MARK: - Private Chat Lifecycle
|
||||||
|
|
||||||
fun startPrivateChat(peerID: String, meshService: BluetoothMeshService): Boolean {
|
fun startPrivateChat(peerID: String, meshService: MeshService): Boolean {
|
||||||
if (isPeerBlocked(peerID)) {
|
if (isPeerBlocked(peerID)) {
|
||||||
val peerNickname = getPeerNickname(peerID, meshService)
|
val peerNickname = getPeerNickname(peerID, meshService)
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
@@ -188,7 +188,7 @@ class PrivateChatManager(
|
|||||||
|
|
||||||
// MARK: - Block/Unblock Operations
|
// MARK: - Block/Unblock Operations
|
||||||
|
|
||||||
fun blockPeer(peerID: String, meshService: BluetoothMeshService): Boolean {
|
fun blockPeer(peerID: String, meshService: MeshService): Boolean {
|
||||||
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
||||||
if (fingerprint != null) {
|
if (fingerprint != null) {
|
||||||
dataManager.addBlockedUser(fingerprint)
|
dataManager.addBlockedUser(fingerprint)
|
||||||
@@ -212,7 +212,7 @@ class PrivateChatManager(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unblockPeer(peerID: String, meshService: BluetoothMeshService): Boolean {
|
fun unblockPeer(peerID: String, meshService: MeshService): Boolean {
|
||||||
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
||||||
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
|
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
|
||||||
dataManager.removeBlockedUser(fingerprint)
|
dataManager.removeBlockedUser(fingerprint)
|
||||||
@@ -230,7 +230,7 @@ class PrivateChatManager(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun blockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean {
|
fun blockPeerByNickname(targetName: String, meshService: MeshService): Boolean {
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
val peerID = getPeerIDForNickname(targetName, meshService)
|
||||||
|
|
||||||
if (peerID != null) {
|
if (peerID != null) {
|
||||||
@@ -247,7 +247,7 @@ class PrivateChatManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unblockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean {
|
fun unblockPeerByNickname(targetName: String, meshService: MeshService): Boolean {
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
val peerID = getPeerIDForNickname(targetName, meshService)
|
||||||
|
|
||||||
if (peerID != null) {
|
if (peerID != null) {
|
||||||
@@ -333,7 +333,7 @@ class PrivateChatManager(
|
|||||||
* Send read receipts for all unread messages from a specific peer
|
* Send read receipts for all unread messages from a specific peer
|
||||||
* Called when the user focuses on a private chat
|
* Called when the user focuses on a private chat
|
||||||
*/
|
*/
|
||||||
fun sendReadReceiptsForPeer(peerID: String, meshService: BluetoothMeshService) {
|
fun sendReadReceiptsForPeer(peerID: String, meshService: MeshService) {
|
||||||
// Collect candidate messages: all incoming messages from this peer in the conversation
|
// Collect candidate messages: all incoming messages from this peer in the conversation
|
||||||
val chats = try { state.getPrivateChatsValue() } catch (_: Exception) { emptyMap<String, List<BitchatMessage>>() }
|
val chats = try { state.getPrivateChatsValue() } catch (_: Exception) { emptyMap<String, List<BitchatMessage>>() }
|
||||||
val messages = chats[peerID].orEmpty()
|
val messages = chats[peerID].orEmpty()
|
||||||
@@ -343,9 +343,7 @@ class PrivateChatManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val myNickname = state.getNicknameValue() ?: "unknown"
|
val myNickname = state.getNicknameValue() ?: "unknown"
|
||||||
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
|
||||||
val hasMesh = try { meshService.getPeerInfo(peerID)?.isConnected == true && meshService.hasEstablishedSession(peerID) } catch (_: Exception) { false }
|
val hasMesh = try { meshService.getPeerInfo(peerID)?.isConnected == true && meshService.hasEstablishedSession(peerID) } catch (_: Exception) { false }
|
||||||
val hasAware = try { aware?.getPeerInfo(peerID)?.isConnected == true && aware.hasEstablishedSession(peerID) } catch (_: Exception) { false }
|
|
||||||
var sentCount = 0
|
var sentCount = 0
|
||||||
messages.forEach { msg ->
|
messages.forEach { msg ->
|
||||||
// Only for incoming messages from this peer
|
// Only for incoming messages from this peer
|
||||||
@@ -354,9 +352,6 @@ class PrivateChatManager(
|
|||||||
if (hasMesh) {
|
if (hasMesh) {
|
||||||
meshService.sendReadReceipt(msg.id, peerID, myNickname)
|
meshService.sendReadReceipt(msg.id, peerID, myNickname)
|
||||||
sentCount += 1
|
sentCount += 1
|
||||||
} else if (hasAware) {
|
|
||||||
aware?.sendReadReceipt(msg.id, peerID, myNickname)
|
|
||||||
sentCount += 1
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to send read receipt for message ${msg.id}: ${e.message}")
|
Log.w(TAG, "Failed to send read receipt for message ${msg.id}: ${e.message}")
|
||||||
@@ -388,7 +383,7 @@ class PrivateChatManager(
|
|||||||
* Establish Noise session if needed before starting private chat
|
* Establish Noise session if needed before starting private chat
|
||||||
* Uses same lexicographical logic as MessageHandler.handleNoiseIdentityAnnouncement
|
* Uses same lexicographical logic as MessageHandler.handleNoiseIdentityAnnouncement
|
||||||
*/
|
*/
|
||||||
private fun establishNoiseSessionIfNeeded(peerID: String, meshService: BluetoothMeshService) {
|
private fun establishNoiseSessionIfNeeded(peerID: String, meshService: MeshService) {
|
||||||
if (noiseSessionDelegate.hasEstablishedSession(peerID)) {
|
if (noiseSessionDelegate.hasEstablishedSession(peerID)) {
|
||||||
Log.d(TAG, "Noise session already established with $peerID")
|
Log.d(TAG, "Noise session already established with $peerID")
|
||||||
return
|
return
|
||||||
@@ -421,11 +416,11 @@ class PrivateChatManager(
|
|||||||
|
|
||||||
// MARK: - Utility Functions
|
// MARK: - Utility Functions
|
||||||
|
|
||||||
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
|
private fun getPeerIDForNickname(nickname: String, meshService: MeshService): String? {
|
||||||
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
private fun getPeerNickname(peerID: String, meshService: MeshService): String {
|
||||||
return meshService.getPeerNicknames()[peerID] ?: peerID
|
return meshService.getPeerNicknames()[peerID] ?: peerID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ fun SecurityVerificationSheet(
|
|||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
accent = accent,
|
accent = accent,
|
||||||
canStartHandshake = fingerprint == null && selectedPeerID.matches(peerHexRegex),
|
canStartHandshake = fingerprint == null && selectedPeerID.matches(peerHexRegex),
|
||||||
onStartHandshake = { viewModel.meshService.initiateNoiseHandshake(selectedPeerID) },
|
onStartHandshake = { viewModel.initiateMeshHandshake(selectedPeerID) },
|
||||||
onVerify = { fp -> viewModel.verifyFingerprintValue(fp) },
|
onVerify = { fp -> viewModel.verifyFingerprintValue(fp) },
|
||||||
onUnverify = { fp -> viewModel.unverifyFingerprintValue(fp) }
|
onUnverify = { fp -> viewModel.unverifyFingerprintValue(fp) }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.Context
|
|||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
import com.bitchat.android.favorites.FavoritesPersistenceService
|
import com.bitchat.android.favorites.FavoritesPersistenceService
|
||||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.noise.NoiseSession
|
import com.bitchat.android.noise.NoiseSession
|
||||||
import com.bitchat.android.nostr.GeohashAliasRegistry
|
import com.bitchat.android.nostr.GeohashAliasRegistry
|
||||||
@@ -26,14 +26,14 @@ import java.util.concurrent.ConcurrentHashMap
|
|||||||
class VerificationHandler(
|
class VerificationHandler(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
private val getMeshService: () -> BluetoothMeshService,
|
private val getMeshService: () -> MeshService,
|
||||||
private val identityManager: SecureIdentityStateManager,
|
private val identityManager: SecureIdentityStateManager,
|
||||||
private val state: ChatState,
|
private val state: ChatState,
|
||||||
private val notificationManager: NotificationManager,
|
private val notificationManager: NotificationManager,
|
||||||
private val messageManager: MessageManager
|
private val messageManager: MessageManager
|
||||||
) {
|
) {
|
||||||
// Helper to get current mesh service (may change after panic clear)
|
// Helper to get current mesh service (may change after panic clear)
|
||||||
private val meshService: BluetoothMeshService
|
private val meshService: MeshService
|
||||||
get() = getMeshService()
|
get() = getMeshService()
|
||||||
|
|
||||||
private val _verifiedFingerprints = MutableStateFlow<Set<String>>(emptySet())
|
private val _verifiedFingerprints = MutableStateFlow<Set<String>>(emptySet())
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ fun VerificationSheet(
|
|||||||
val fingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
val fingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
if (peerID != null) {
|
if (peerID != null) {
|
||||||
val fingerprint = viewModel.meshService.getPeerFingerprint(peerID!!)
|
val fingerprint = viewModel.getMeshPeerFingerprint(peerID!!)
|
||||||
if (fingerprint != null && fingerprints.contains(fingerprint)) {
|
if (fingerprint != null && fingerprints.contains(fingerprint)) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(
|
Button(
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -30,7 +30,7 @@ import java.text.SimpleDateFormat
|
|||||||
fun AudioMessageItem(
|
fun AudioMessageItem(
|
||||||
message: BitchatMessage,
|
message: BitchatMessage,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: MeshService,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
timeFormatter: SimpleDateFormat,
|
timeFormatter: SimpleDateFormat,
|
||||||
onNicknameClick: ((String) -> Unit)?,
|
onNicknameClick: ((String) -> Unit)?,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import androidx.compose.ui.text.TextLayoutResult
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.BitchatMessageType
|
import com.bitchat.android.model.BitchatMessageType
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
@@ -39,7 +39,7 @@ fun ImageMessageItem(
|
|||||||
message: BitchatMessage,
|
message: BitchatMessage,
|
||||||
messages: List<BitchatMessage>,
|
messages: List<BitchatMessage>,
|
||||||
currentUserNickname: String,
|
currentUserNickname: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: MeshService,
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
timeFormatter: SimpleDateFormat,
|
timeFormatter: SimpleDateFormat,
|
||||||
onNicknameClick: ((String) -> Unit)?,
|
onNicknameClick: ((String) -> Unit)?,
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ object WifiAwareController {
|
|||||||
service = startedService
|
service = startedService
|
||||||
_running.value = true
|
_running.value = true
|
||||||
}
|
}
|
||||||
|
try { com.bitchat.android.service.MeshServiceHolder.unifiedMeshService?.refreshDelegates() } catch (_: Exception) { }
|
||||||
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware started")) } catch (_: Exception) {}
|
try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(com.bitchat.android.ui.debug.DebugMessage.SystemMessage("Wi‑Fi Aware started")) } catch (_: Exception) {}
|
||||||
} else {
|
} else {
|
||||||
if (reusableService == null) {
|
if (reusableService == null) {
|
||||||
|
|||||||
@@ -1103,6 +1103,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
meshCore.sendReadReceipt(messageID, recipientPeerID, readerNickname)
|
meshCore.sendReadReceipt(messageID, recipientPeerID, readerNickname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun sendVerifyChallenge(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
|
||||||
|
meshCore.sendVerifyChallenge(peerID, noiseKeyHex, nonceA)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sendVerifyResponse(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
|
||||||
|
meshCore.sendVerifyResponse(peerID, noiseKeyHex, nonceA)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts a file (TLV payload) to all peers. Uses protocol version 2 to support
|
* Broadcasts a file (TLV payload) to all peers. Uses protocol version 2 to support
|
||||||
* large payloads and generates a deterministic transferId (sha256 of payload) for UI/state.
|
* large payloads and generates a deterministic transferId (sha256 of payload) for UI/state.
|
||||||
@@ -1205,6 +1213,8 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
*/
|
*/
|
||||||
override fun getIdentityFingerprint(): String = meshCore.getIdentityFingerprint()
|
override fun getIdentityFingerprint(): String = meshCore.getIdentityFingerprint()
|
||||||
|
|
||||||
|
override fun getStaticNoisePublicKey(): ByteArray? = meshCore.getStaticNoisePublicKey()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return true if the UI should show an “encrypted” indicator for this peer.
|
* @return true if the UI should show an “encrypted” indicator for this peer.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user