diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 30ecc6cc1..7ee18f932 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -3,6 +3,8 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.serialization)
+
}
android {
@@ -62,6 +64,12 @@ android {
}
dependencies {
+ implementation(libs.androidx.datastore.preferences)
+ // optional - RxJava2 support
+ implementation(libs.androidx.datastore.preferences.rxjava2)
+ // optional - RxJava3 support
+ implementation(libs.androidx.datastore.preferences.rxjava3)
+ implementation(libs.kotlinx.serialization.json)
// Core Android dependencies
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 134dcdfa0..71b2a876e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -40,7 +40,7 @@
create(modelClass: Class): T {
- @Suppress("UNCHECKED_CAST")
- return ChatViewModel(application, meshService) as T
- }
- }
- }
-
+ private val mainViewModel: MainViewModel by viewModels { AppVMProvider.Factory }
+ private val chatViewModel: ChatViewModel by viewModels { AppVMProvider.Factory }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
- // Enable edge-to-edge display for modern Android look
+
+ val themePreferenceRepo: ThemePreferenceRepo =
+ ThemePrefRepoImpl(this.applicationContext, lifecycleScope)
enableEdgeToEdge()
// Initialize permission management
permissionManager = PermissionManager(this)
- // Initialize core mesh service first
- meshService = BluetoothMeshService(this)
bluetoothStatusManager = BluetoothStatusManager(
activity = this,
context = this,
@@ -98,21 +91,26 @@ class MainActivity : ComponentActivity() {
onOnboardingComplete = ::handleOnboardingComplete,
onOnboardingFailed = ::handleOnboardingFailed
)
-
setContent {
- BitchatTheme {
+ val themePref by themePreferenceRepo.theme.collectAsStateWithLifecycle()
+ val coroutineScope = rememberCoroutineScope()
+
+ BitchatTheme(
+ themePref = themePref
+ ) {
Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = MaterialTheme.colorScheme.background
) { innerPadding ->
- OnboardingFlowScreen(modifier = Modifier
- .fillMaxSize()
- .padding(innerPadding)
- )
+ OnboardingFlowScreen(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding), themePref
+ ) { coroutineScope.launch { themePreferenceRepo.updateTheme(it) } }
}
}
}
-
+
// Collect state changes in a lifecycle-aware manner
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -121,16 +119,19 @@ class MainActivity : ComponentActivity() {
}
}
}
-
+
// Only start onboarding process if we're in the initial CHECKING state
// This prevents restarting onboarding on configuration changes
if (mainViewModel.onboardingState.value == OnboardingState.CHECKING) {
checkOnboardingStatus()
}
}
-
+
@Composable
- private fun OnboardingFlowScreen(modifier: Modifier = Modifier) {
+ private fun OnboardingFlowScreen(
+ modifier: Modifier = Modifier, themePref: ThemePreference,
+ onChangeTheme: (ThemePreference) -> Unit
+ ) {
val context = LocalContext.current
val onboardingState by mainViewModel.onboardingState.collectAsState()
val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState()
@@ -158,7 +159,7 @@ class MainActivity : ComponentActivity() {
context.unregisterReceiver(receiver)
Log.d("BluetoothStatusUI", "BroadcastReceiver unregistered")
} catch (e: IllegalStateException) {
- Log.w("BluetoothStatusUI", "Receiver was not registered")
+ Log.w("BluetoothStatusUI", "Receiver was not registered ${e.printStackTrace()}")
}
}
}
@@ -167,7 +168,7 @@ class MainActivity : ComponentActivity() {
OnboardingState.PERMISSION_REQUESTING -> {
InitializingScreen(modifier)
}
-
+
OnboardingState.BLUETOOTH_CHECK -> {
BluetoothCheckScreen(
modifier = modifier,
@@ -182,7 +183,7 @@ class MainActivity : ComponentActivity() {
isLoading = isBluetoothLoading
)
}
-
+
OnboardingState.LOCATION_CHECK -> {
LocationCheckScreen(
modifier = modifier,
@@ -197,7 +198,7 @@ class MainActivity : ComponentActivity() {
isLoading = isLocationLoading
)
}
-
+
OnboardingState.BATTERY_OPTIMIZATION_CHECK -> {
BatteryOptimizationScreen(
modifier = modifier,
@@ -216,7 +217,7 @@ class MainActivity : ComponentActivity() {
isLoading = isBatteryOptimizationLoading
)
}
-
+
OnboardingState.PERMISSION_EXPLANATION -> {
PermissionExplanationScreen(
modifier = modifier,
@@ -246,9 +247,11 @@ class MainActivity : ComponentActivity() {
// Add the callback - this will be automatically removed when the activity is destroyed
onBackPressedDispatcher.addCallback(this, backCallback)
- ChatScreen(viewModel = chatViewModel)
+ ChatScreen(
+ viewModel = chatViewModel, themePref = themePref, onChangeTheme = onChangeTheme
+ )
}
-
+
OnboardingState.ERROR -> {
InitializationErrorScreen(
modifier = modifier,
@@ -264,80 +267,87 @@ class MainActivity : ComponentActivity() {
}
}
}
-
+
private fun handleOnboardingStateChange(state: OnboardingState) {
when (state) {
OnboardingState.COMPLETE -> {
// App is fully initialized, mesh service is running
- android.util.Log.d("MainActivity", "Onboarding completed - app ready")
+ Log.d("MainActivity", "Onboarding completed - app ready")
}
+
OnboardingState.ERROR -> {
- android.util.Log.e("MainActivity", "Onboarding error state reached")
+ Log.e("MainActivity", "Onboarding error state reached")
}
+
else -> {}
}
}
-
+
private fun checkOnboardingStatus() {
Log.d("MainActivity", "Checking onboarding status")
-
+
lifecycleScope.launch {
// Small delay to show the checking state
delay(500)
-
+
// First check Bluetooth status (always required)
checkBluetoothAndProceed()
}
}
-
+
/**
* Check Bluetooth status and proceed with onboarding flow
*/
private fun checkBluetoothAndProceed() {
// Log.d("MainActivity", "Checking Bluetooth status")
-
+
// For first-time users, skip Bluetooth check and go straight to permissions
// We'll check Bluetooth after permissions are granted
if (permissionManager.isFirstTimeLaunch()) {
- Log.d("MainActivity", "First-time launch, skipping Bluetooth check - will check after permissions")
+ Log.d(
+ "MainActivity",
+ "First-time launch, skipping Bluetooth check - will check after permissions"
+ )
proceedWithPermissionCheck()
return
}
-
+
// For existing users, check Bluetooth status first
bluetoothStatusManager.logBluetoothStatus()
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
-
+
when (mainViewModel.bluetoothStatus.value) {
BluetoothStatus.ENABLED -> {
// Bluetooth is enabled, check location services next
checkLocationAndProceed()
}
+
BluetoothStatus.DISABLED -> {
// Show Bluetooth enable screen (should have permissions as existing user)
Log.d("MainActivity", "Bluetooth disabled, showing enable screen")
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
mainViewModel.updateBluetoothLoading(false)
}
+
BluetoothStatus.NOT_SUPPORTED -> {
// Device doesn't support Bluetooth
- android.util.Log.e("MainActivity", "Bluetooth not supported")
+ Log.e("MainActivity", "Bluetooth not supported")
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
mainViewModel.updateBluetoothLoading(false)
}
}
}
-
+
/**
- * Proceed with permission checking
+ * Proceed with permission checking
*/
private fun proceedWithPermissionCheck() {
Log.d("MainActivity", "Proceeding with permission check")
-
+
lifecycleScope.launch {
delay(200) // Small delay for smooth transition
-
+
if (permissionManager.isFirstTimeLaunch()) {
Log.d("MainActivity", "First time launch, showing permission explanation")
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
@@ -351,7 +361,7 @@ class MainActivity : ComponentActivity() {
}
}
}
-
+
/**
* Handle Bluetooth enabled callback
*/
@@ -367,30 +377,35 @@ class MainActivity : ComponentActivity() {
*/
private fun checkLocationAndProceed() {
Log.d("MainActivity", "Checking location services status")
-
+
// For first-time users, skip location check and go straight to permissions
// We'll check location after permissions are granted
if (permissionManager.isFirstTimeLaunch()) {
- Log.d("MainActivity", "First-time launch, skipping location check - will check after permissions")
+ Log.d(
+ "MainActivity",
+ "First-time launch, skipping location check - will check after permissions"
+ )
proceedWithPermissionCheck()
return
}
-
+
// For existing users, check location status
locationStatusManager.logLocationStatus()
mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
-
+
when (mainViewModel.locationStatus.value) {
LocationStatus.ENABLED -> {
// Location services enabled, check battery optimization next
checkBatteryOptimizationAndProceed()
}
+
LocationStatus.DISABLED -> {
// Show location enable screen (should have permissions as existing user)
Log.d("MainActivity", "Location services disabled, showing enable screen")
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
mainViewModel.updateLocationLoading(false)
}
+
LocationStatus.NOT_AVAILABLE -> {
// Device doesn't support location services (very unusual)
Log.e("MainActivity", "Location services not available")
@@ -424,13 +439,14 @@ class MainActivity : ComponentActivity() {
mainViewModel.updateErrorMessage(message)
mainViewModel.updateOnboardingState(OnboardingState.ERROR)
}
+
else -> {
// Stay on location check screen for retry
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
}
}
}
-
+
/**
* Handle Bluetooth disabled callback
*/
@@ -438,34 +454,46 @@ class MainActivity : ComponentActivity() {
Log.w("MainActivity", "Bluetooth disabled or failed: $message")
mainViewModel.updateBluetoothLoading(false)
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
-
+
when {
mainViewModel.bluetoothStatus.value == BluetoothStatus.NOT_SUPPORTED -> {
// Show permanent error for unsupported devices
mainViewModel.updateErrorMessage(message)
mainViewModel.updateOnboardingState(OnboardingState.ERROR)
}
+
message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> {
// During first-time onboarding, if Bluetooth enable fails due to permissions,
// proceed to permission explanation screen where user will grant permissions first
- Log.d("MainActivity", "Bluetooth enable requires permissions, proceeding to permission explanation")
+ Log.d(
+ "MainActivity",
+ "Bluetooth enable requires permissions, proceeding to permission explanation"
+ )
proceedWithPermissionCheck()
}
+
message.contains("Permission") -> {
// For existing users, redirect to permission explanation to grant missing permissions
- Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation")
+ Log.d(
+ "MainActivity",
+ "Bluetooth enable requires permissions, showing permission explanation"
+ )
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
}
+
else -> {
// Stay on Bluetooth check screen for retry
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
}
}
}
-
+
private fun handleOnboardingComplete() {
- Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app")
-
+ Log.d(
+ "MainActivity",
+ "Onboarding completed, checking Bluetooth and Location before initializing app"
+ )
+
// After permissions are granted, re-check Bluetooth, Location, and Battery Optimization status
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
val currentLocationStatus = locationStatusManager.checkLocationStatus()
@@ -474,65 +502,86 @@ class MainActivity : ComponentActivity() {
batteryOptimizationManager.isBatteryOptimizationDisabled() -> BatteryOptimizationStatus.DISABLED
else -> BatteryOptimizationStatus.ENABLED
}
-
+
when {
currentBluetoothStatus != BluetoothStatus.ENABLED -> {
// Bluetooth still disabled, but now we have permissions to enable it
- Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.")
+ Log.d(
+ "MainActivity",
+ "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen."
+ )
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
mainViewModel.updateBluetoothLoading(false)
}
+
currentLocationStatus != LocationStatus.ENABLED -> {
// Location services still disabled, but now we have permissions to enable it
- Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.")
+ Log.d(
+ "MainActivity",
+ "Permissions granted, but Location services still disabled. Showing Location enable screen."
+ )
mainViewModel.updateLocationStatus(currentLocationStatus)
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
mainViewModel.updateLocationLoading(false)
}
+
currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> {
// Battery optimization still enabled, show battery optimization screen
- android.util.Log.d("MainActivity", "Permissions granted, but battery optimization still enabled. Showing battery optimization screen.")
+ Log.d(
+ "MainActivity",
+ "Permissions granted, but battery optimization still enabled. Showing battery optimization screen."
+ )
mainViewModel.updateBatteryOptimizationStatus(currentBatteryOptimizationStatus)
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
mainViewModel.updateBatteryOptimizationLoading(false)
}
+
else -> {
// Both are enabled, proceed to app initialization
- Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization")
+ Log.d(
+ "MainActivity",
+ "Both Bluetooth and Location services are enabled, proceeding to initialization"
+ )
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
initializeApp()
}
}
}
-
+
private fun handleOnboardingFailed(message: String) {
Log.e("MainActivity", "Onboarding failed: $message")
mainViewModel.updateErrorMessage(message)
mainViewModel.updateOnboardingState(OnboardingState.ERROR)
}
-
+
/**
* Check Battery Optimization status and proceed with onboarding flow
*/
private fun checkBatteryOptimizationAndProceed() {
- android.util.Log.d("MainActivity", "Checking battery optimization status")
-
+ Log.d("MainActivity", "Checking battery optimization status")
+
// For first-time users, skip battery optimization check and go straight to permissions
// We'll check battery optimization after permissions are granted
if (permissionManager.isFirstTimeLaunch()) {
- android.util.Log.d("MainActivity", "First-time launch, skipping battery optimization check - will check after permissions")
+ Log.d(
+ "MainActivity",
+ "First-time launch, skipping battery optimization check - will check after permissions"
+ )
proceedWithPermissionCheck()
return
}
-
+
// Check if user has previously skipped battery optimization
if (BatteryOptimizationPreferenceManager.isSkipped(this)) {
- android.util.Log.d("MainActivity", "User previously skipped battery optimization, proceeding to permissions")
+ Log.d(
+ "MainActivity",
+ "User previously skipped battery optimization, proceeding to permissions"
+ )
proceedWithPermissionCheck()
return
}
-
+
// For existing users, check battery optimization status
batteryOptimizationManager.logBatteryOptimizationStatus()
val currentBatteryOptimizationStatus = when {
@@ -541,36 +590,40 @@ class MainActivity : ComponentActivity() {
else -> BatteryOptimizationStatus.ENABLED
}
mainViewModel.updateBatteryOptimizationStatus(currentBatteryOptimizationStatus)
-
+
when (currentBatteryOptimizationStatus) {
BatteryOptimizationStatus.DISABLED, BatteryOptimizationStatus.NOT_SUPPORTED -> {
// Battery optimization is disabled or not supported, proceed with permission check
proceedWithPermissionCheck()
}
+
BatteryOptimizationStatus.ENABLED -> {
// Show battery optimization disable screen
- android.util.Log.d("MainActivity", "Battery optimization enabled, showing disable screen")
+ Log.d(
+ "MainActivity",
+ "Battery optimization enabled, showing disable screen"
+ )
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
mainViewModel.updateBatteryOptimizationLoading(false)
}
}
}
-
+
/**
* Handle Battery Optimization disabled callback
*/
private fun handleBatteryOptimizationDisabled() {
- android.util.Log.d("MainActivity", "Battery optimization disabled by user")
+ Log.d("MainActivity", "Battery optimization disabled by user")
mainViewModel.updateBatteryOptimizationLoading(false)
mainViewModel.updateBatteryOptimizationStatus(BatteryOptimizationStatus.DISABLED)
proceedWithPermissionCheck()
}
-
+
/**
* Handle Battery Optimization failed callback
*/
private fun handleBatteryOptimizationFailed(message: String) {
- android.util.Log.w("MainActivity", "Battery optimization disable failed: $message")
+ Log.w("MainActivity", "Battery optimization disable failed: $message")
mainViewModel.updateBatteryOptimizationLoading(false)
val currentStatus = when {
!batteryOptimizationManager.isBatteryOptimizationSupported() -> BatteryOptimizationStatus.NOT_SUPPORTED
@@ -578,26 +631,26 @@ class MainActivity : ComponentActivity() {
else -> BatteryOptimizationStatus.ENABLED
}
mainViewModel.updateBatteryOptimizationStatus(currentStatus)
-
+
// Stay on battery optimization check screen for retry
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
}
-
+
private fun initializeApp() {
Log.d("MainActivity", "Starting app initialization")
-
+
lifecycleScope.launch {
try {
// Initialize the app with a proper delay to ensure Bluetooth stack is ready
// This solves the issue where app needs restart to work on first install
delay(1000) // Give the system time to process permission grants
-
+
Log.d("MainActivity", "Permissions verified, initializing chat system")
-
+
// Initialize PoW preferences early in the initialization process
PoWPreferenceManager.init(this@MainActivity)
Log.d("MainActivity", "PoW preferences initialized")
-
+
// Ensure all permissions are still granted (user might have revoked in settings)
if (!permissionManager.areAllPermissionsGranted()) {
val missing = permissionManager.getMissingPermissions()
@@ -607,14 +660,15 @@ class MainActivity : ComponentActivity() {
}
// Set up mesh service delegate and start services
- meshService.delegate = chatViewModel
- meshService.startServices()
-
+
+ chatViewModel.startMeshServices()
+ chatViewModel.setUpDelegate()
+
Log.d("MainActivity", "Mesh service started successfully")
-
+
// Handle any notification intent
handleNotificationIntent(intent)
-
+
// Small delay to ensure mesh service is fully initialized
delay(500)
Log.d("MainActivity", "App initialization complete")
@@ -625,7 +679,7 @@ class MainActivity : ComponentActivity() {
}
}
}
-
+
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// Handle notification intents when app is already running
@@ -633,13 +687,14 @@ class MainActivity : ComponentActivity() {
handleNotificationIntent(intent)
}
}
-
+
override fun onResume() {
super.onResume()
// Check Bluetooth and Location status on resume and handle accordingly
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Set app foreground state
- meshService.connectionManager.setAppBackgroundState(false)
+ chatViewModel.changeMeshServiceBGState(false)
+
chatViewModel.setAppBackgroundState(false)
// Check if Bluetooth was disabled while app was backgrounded
@@ -651,7 +706,7 @@ class MainActivity : ComponentActivity() {
mainViewModel.updateBluetoothLoading(false)
return
}
-
+
// Check if location services were disabled while app was backgrounded
val currentLocationStatus = locationStatusManager.checkLocationStatus()
if (currentLocationStatus != LocationStatus.ENABLED) {
@@ -662,53 +717,59 @@ class MainActivity : ComponentActivity() {
}
}
}
-
- override fun onPause() {
+
+ override fun onPause() {
super.onPause()
// Only set background state if app is fully initialized
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Set app background state
- meshService.connectionManager.setAppBackgroundState(true)
+ chatViewModel.changeMeshServiceBGState(true)
chatViewModel.setAppBackgroundState(true)
}
}
-
+
/**
* Handle intents from notification clicks - open specific private chat or geohash chat
*/
private fun handleNotificationIntent(intent: Intent) {
val shouldOpenPrivateChat = intent.getBooleanExtra(
- com.bitchat.android.ui.NotificationManager.EXTRA_OPEN_PRIVATE_CHAT,
+ com.bitchat.android.ui.NotificationManager.EXTRA_OPEN_PRIVATE_CHAT,
false
)
-
+
val shouldOpenGeohashChat = intent.getBooleanExtra(
com.bitchat.android.ui.NotificationManager.EXTRA_OPEN_GEOHASH_CHAT,
false
)
-
+
when {
shouldOpenPrivateChat -> {
- val peerID = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_PEER_ID)
- val senderNickname = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_SENDER_NICKNAME)
-
+ val peerID =
+ intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_PEER_ID)
+ val senderNickname =
+ intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_SENDER_NICKNAME)
+
if (peerID != null) {
- Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification")
-
+ Log.d(
+ "MainActivity",
+ "Opening private chat with $senderNickname (peerID: $peerID) from notification"
+ )
+
// Open the private chat with this peer
chatViewModel.startPrivateChat(peerID)
-
+
// Clear notifications for this sender since user is now viewing the chat
chatViewModel.clearNotificationsForSender(peerID)
}
}
-
+
shouldOpenGeohashChat -> {
- val geohash = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_GEOHASH)
-
+ val geohash =
+ intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_GEOHASH)
+
if (geohash != null) {
Log.d("MainActivity", "Opening geohash chat #$geohash from notification")
-
+
// Switch to the geohash channel - create appropriate geohash channel level
val level = when (geohash.length) {
7 -> com.bitchat.android.geohash.GeohashChannelLevel.BLOCK
@@ -721,10 +782,10 @@ class MainActivity : ComponentActivity() {
val geohashChannel = com.bitchat.android.geohash.GeohashChannel(level, geohash)
val channelId = com.bitchat.android.geohash.ChannelID.Location(geohashChannel)
chatViewModel.selectLocationChannel(channelId)
-
+
// Update current geohash state for notifications
chatViewModel.setCurrentGeohash(geohash)
-
+
// Clear notifications for this geohash since user is now viewing it
chatViewModel.clearNotificationsForGeohash(geohash)
}
@@ -732,10 +793,10 @@ class MainActivity : ComponentActivity() {
}
}
-
+
override fun onDestroy() {
super.onDestroy()
-
+
// Cleanup location status manager
try {
locationStatusManager.cleanup()
@@ -743,11 +804,11 @@ class MainActivity : ComponentActivity() {
} catch (e: Exception) {
Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}")
}
-
+
// Stop mesh services if app was fully initialized
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
try {
- meshService.stopServices()
+ chatViewModel.stopMeshServices()
Log.d("MainActivity", "Mesh services stopped successfully")
} catch (e: Exception) {
Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}")
diff --git a/app/src/main/java/com/bitchat/android/MainViewModel.kt b/app/src/main/java/com/bitchat/android/MainViewModel.kt
index 35125d855..3d45983e6 100644
--- a/app/src/main/java/com/bitchat/android/MainViewModel.kt
+++ b/app/src/main/java/com/bitchat/android/MainViewModel.kt
@@ -67,4 +67,6 @@ class MainViewModel : ViewModel() {
fun updateBatteryOptimizationLoading(loading: Boolean) {
_isBatteryOptimizationLoading.value = loading
}
+
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/bitchat/android/application/AppContainer.kt b/app/src/main/java/com/bitchat/android/application/AppContainer.kt
new file mode 100644
index 000000000..9ebccf52f
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/application/AppContainer.kt
@@ -0,0 +1,22 @@
+package com.bitchat.android.application
+
+import android.app.Application
+import android.content.Context
+import com.bitchat.android.mesh.BluetoothMeshDelegate
+import com.bitchat.android.mesh.BluetoothMeshDelegateImpl
+import com.bitchat.android.mesh.BluetoothMeshService
+import kotlinx.coroutines.CoroutineScope
+
+interface AppContainer {
+ val meshService : BluetoothMeshService
+ val bmd : BluetoothMeshDelegate
+}
+
+class AppDataContainer(
+ private val scope: CoroutineScope, private val context: Context,
+ override val meshService: BluetoothMeshService, private val app: Application
+) : AppContainer {
+ override val bmd by lazy {
+ BluetoothMeshDelegateImpl(scope, context, meshService, app)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bitchat/android/application/AppVMProvider.kt b/app/src/main/java/com/bitchat/android/application/AppVMProvider.kt
new file mode 100644
index 000000000..63587d832
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/application/AppVMProvider.kt
@@ -0,0 +1,21 @@
+package com.bitchat.android.application
+
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
+import com.bitchat.android.MainViewModel
+import com.bitchat.android.ui.ChatViewModel
+
+object AppVMProvider {
+ val Factory = viewModelFactory {
+ initializer {
+ MainViewModel()
+ }
+ initializer {
+ ChatViewModel(
+ bitchatApplication(),
+ bitchatApplication().container.meshService,
+ bitchatApplication().container.bmd
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bitchat/android/application/BitchatApplication.kt b/app/src/main/java/com/bitchat/android/application/BitchatApplication.kt
new file mode 100644
index 000000000..7896b7dd4
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/application/BitchatApplication.kt
@@ -0,0 +1,63 @@
+package com.bitchat.android.application
+
+import android.app.Application
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.CreationExtras
+import com.bitchat.android.favorites.FavoritesPersistenceService
+import com.bitchat.android.mesh.BluetoothMeshService
+import com.bitchat.android.nostr.RelayDirectory
+import com.bitchat.android.net.TorManager
+import com.bitchat.android.nostr.NostrIdentityBridge
+import com.bitchat.android.ui.debug.DebugPreferenceManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+
+/**
+ * Main application class for bitchat Android
+ */
+class BitchatApplication : Application() {
+
+ lateinit var container: AppContainer
+ private val applicationScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+ override fun onCreate() {
+ super.onCreate()
+
+ // Initialize Tor first so any early network goes over Tor
+ try {
+ TorManager.init(this)
+ } catch (_: Exception) {
+ }
+
+ // Initialize relay directory (loads assets/nostr_relays.csv)
+ RelayDirectory.initialize(this)
+
+ // Initialize favorites persistence early so MessageRouter/NostrTransport can use it on startup
+ try {
+ FavoritesPersistenceService.initialize(this)
+ } catch (_: Exception) {
+ }
+
+ // Warm up Nostr identity to ensure npub is available for favorite notifications
+ try {
+ NostrIdentityBridge.getCurrentNostrIdentity(this)
+ } catch (_: Exception) {
+ }
+
+ // Initialize debug preference manager (persists debug toggles)
+ try {
+ DebugPreferenceManager.init(this)
+ } catch (_: Exception) {
+ }
+
+ // TorManager already initialized above
+
+ container = AppDataContainer(
+ applicationScope, this.applicationContext,
+ BluetoothMeshService(this), this
+ )
+ }
+}
+
+fun CreationExtras.bitchatApplication(): BitchatApplication =
+ (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as BitchatApplication)
diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshDelegate.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshDelegate.kt
new file mode 100644
index 000000000..9a51e590f
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshDelegate.kt
@@ -0,0 +1,76 @@
+package com.bitchat.android.mesh
+
+import com.bitchat.android.geohash.GeohashChannel
+import com.bitchat.android.model.BitchatMessage
+import com.bitchat.android.ui.ChannelManager
+import com.bitchat.android.ui.ChatState
+import com.bitchat.android.ui.CommandSuggestion
+import com.bitchat.android.ui.DataManager
+import com.bitchat.android.ui.MeshDelegateHandler
+import com.bitchat.android.ui.MessageManager
+import com.bitchat.android.ui.NotificationManager
+import com.bitchat.android.ui.PrivateChatManager
+
+
+/**
+ * Delegate interface for mesh service callbacks (maintains exact same interface)
+ */
+interface BluetoothMeshDelegate {
+ val dataManager: DataManager
+ val privateChatManager: PrivateChatManager
+ val meshDelegateHandler: MeshDelegateHandler
+ val messageManager: MessageManager
+ val channelManager: ChannelManager
+ val notificationManager: NotificationManager
+ val state: ChatState
+ fun setNickname(newNickname: String)
+ fun didReceiveMessage(message: BitchatMessage)
+ fun didUpdatePeerList(peers: List)
+ fun didReceiveChannelLeave(channel: String, fromPeer: String)
+ fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
+ fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
+ fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
+ fun getNickname(): String?
+ fun isFavorite(peerID: String): Boolean
+ fun cancelMediaSend(messageId: String)
+ fun joinChannel(channel: String, password: String? = null): Boolean
+
+ fun switchToChannel(channel: String?)
+
+ fun leaveChannel(channel: String)
+
+ fun setAppBackgroundState(inBackground: Boolean)
+
+ fun setCurrentPrivateChatPeer(peerID: String?)
+
+ fun setCurrentGeohash(geohash: String?)
+
+ fun clearNotificationsForSender(peerID: String)
+
+ fun clearNotificationsForGeohash(geohash: String)
+ fun changeMeshServiceBGState(b: Boolean)
+ fun startMeshServices()
+ fun stopMeshServices()
+
+ fun clearMeshMentionNotifications()
+ fun updateCommandSuggestions(input: String)
+
+ fun selectCommandSuggestion(suggestion: CommandSuggestion): String
+
+ fun updateMentionSuggestions(input: String)
+ fun selectMentionSuggestion(nickname: String, currentText: String): String
+ fun panicClearAllData(onPanicReset: () -> Unit)
+ fun openLatestUnreadPrivateChat(onEnsureGeohashDMSubscription: (String) -> Unit)
+ fun startPrivateChat(peerID: String, onEnsureGeohashDMSubscription: () -> Unit)
+ fun endPrivateChat()
+ fun loadAndInitialize(
+ logCurrentFavoriteState: () -> Unit, initializeSessionStateMonitoring: () -> Unit,
+ initializeGeoHashVM: () -> Unit
+ )
+ fun updateReactiveStates()
+
+ fun toggleFavorite(peerID: String, logCurrentFavoriteState: () -> Unit)
+ fun sendMessage(content: String, onSendGeohashMessage: (String, GeohashChannel) -> Unit)
+
+// registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshDelegateImpl.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshDelegateImpl.kt
new file mode 100644
index 000000000..4cbf985d6
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshDelegateImpl.kt
@@ -0,0 +1,729 @@
+package com.bitchat.android.mesh
+
+import android.app.Application
+import android.content.Context
+import android.util.Log
+import androidx.core.app.NotificationManagerCompat
+import androidx.lifecycle.LiveData
+import com.bitchat.android.geohash.ChannelID
+import com.bitchat.android.geohash.GeohashChannel
+import com.bitchat.android.model.BitchatMessage
+import com.bitchat.android.model.logWarn
+import com.bitchat.android.ui.ChannelManager
+import com.bitchat.android.ui.ChatState
+import com.bitchat.android.ui.ChatViewModelUtils
+import com.bitchat.android.ui.CommandProcessor
+import com.bitchat.android.ui.CommandSuggestion
+import com.bitchat.android.ui.DataManager
+import com.bitchat.android.ui.GeoPerson
+import com.bitchat.android.ui.MeshDelegateHandler
+import com.bitchat.android.ui.MessageManager
+import com.bitchat.android.ui.NoiseSessionDelegate
+import com.bitchat.android.ui.NotificationManager
+import com.bitchat.android.ui.PrivateChatManager
+import com.bitchat.android.util.NotificationIntervalManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.util.Date
+import kotlin.collections.forEach
+import kotlin.random.Random
+
+
+class BluetoothMeshDelegateImpl(
+ private val scope: CoroutineScope,
+ context: Context,
+ private val meshService: BluetoothMeshService,
+ private val application: Application
+) : BluetoothMeshDelegate {
+ companion object {
+ private const val TAG = "BluetoothMeshDelegate"
+
+ }
+ override val dataManager = DataManager(context)
+ override val state = ChatState()
+ override val messageManager = MessageManager(state)
+ private val noiseSessionDelegate = object : NoiseSessionDelegate {
+ override fun hasEstablishedSession(peerID: String) =
+ meshService.hasEstablishedSession(peerID)
+
+ override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID)
+ override fun getMyPeerID(): String = meshService.myPeerID
+ }
+ override val channelManager = ChannelManager(state, messageManager, dataManager, scope)
+ override val privateChatManager =
+ PrivateChatManager(state, messageManager, dataManager, noiseSessionDelegate)
+ override val notificationManager = NotificationManager(
+ context, NotificationManagerCompat.from(context), NotificationIntervalManager()
+ )
+ private val commandProcessor =
+ CommandProcessor(state, messageManager, channelManager, privateChatManager)
+
+ override val meshDelegateHandler = MeshDelegateHandler(
+ state = state,
+ messageManager = messageManager,
+ channelManager = channelManager,
+ privateChatManager = privateChatManager,
+ notificationManager = notificationManager,
+ coroutineScope = scope,
+ onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(context) },
+ getMyPeerID = { meshService.myPeerID },
+ getMeshService = { meshService }
+ )
+ private val transferMessageMap = mutableMapOf()
+ private val messageTransferMap = mutableMapOf()
+ private val geohashPeople: LiveData> = state.geohashPeople
+ val selectedLocationChannel: LiveData = state.selectedLocationChannel
+
+ override fun setNickname(newNickname: String) {
+ state.setNickname(newNickname)
+ dataManager.saveNickname(newNickname)
+ meshService.sendBroadcastAnnounce()
+ }
+
+ override fun didReceiveMessage(message: BitchatMessage) =
+ meshDelegateHandler.didReceiveMessage(message)
+
+ override fun didUpdatePeerList(peers: List) =
+ meshDelegateHandler.didUpdatePeerList(peers)
+
+ override fun didReceiveChannelLeave(channel: String, fromPeer: String) =
+ meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
+
+
+ override fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) =
+ meshDelegateHandler.didReceiveDeliveryAck(messageID, recipientPeerID)
+
+
+ override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) =
+ meshDelegateHandler.didReceiveReadReceipt(messageID, recipientPeerID)
+
+
+ override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String) =
+ meshDelegateHandler.decryptChannelMessage(encryptedContent, channel)
+
+ override fun getNickname() = meshDelegateHandler.getNickname()
+
+
+ override fun isFavorite(peerID: String) = meshDelegateHandler.isFavorite(peerID)
+
+ override fun openLatestUnreadPrivateChat(onEnsureGeohashDMSubscription: (String) -> Unit) {
+ try {
+ val unreadKeys = state.getUnreadPrivateMessagesValue()
+ if (unreadKeys.isEmpty()) return
+
+ val me = state.getNicknameValue() ?: meshService.myPeerID
+ val chats = state.getPrivateChatsValue()
+
+ // Pick the latest incoming message among unread conversations
+ var bestKey: String? = null
+ var bestTime: Long = Long.MIN_VALUE
+
+ unreadKeys.forEach { key ->
+ val list = chats[key]
+ if (!list.isNullOrEmpty()) {
+ // Prefer the latest incoming message (sender != me), fallback to last message
+ val latestIncoming = list.lastOrNull { it.sender != me }
+ val candidateTime = (latestIncoming ?: list.last()).timestamp.time
+ if (candidateTime > bestTime) {
+ bestTime = candidateTime
+ bestKey = key
+ }
+ }
+ }
+
+ val targetKey = bestKey ?: unreadKeys.firstOrNull() ?: return
+
+ val openPeer: String = if (targetKey.startsWith("nostr_")) {
+ // Use the exact conversation key for geohash DMs and ensure DM subscription
+ onEnsureGeohashDMSubscription(targetKey)
+ targetKey
+ } else {
+ // Resolve to a canonical mesh peer if needed
+ val canonical =
+ com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
+ selectedPeerID = targetKey,
+ connectedPeers = state.getConnectedPeersValue(),
+ meshNoiseKeyForPeer = { pid -> meshService.getPeerInfo(pid)?.noisePublicKey },
+ meshHasPeer = { pid -> meshService.getPeerInfo(pid)?.isConnected == true },
+ nostrPubHexForAlias = { alias ->
+ com.bitchat.android.nostr.GeohashAliasRegistry.get(
+ alias
+ )
+ },
+ findNoiseKeyForNostr = { key ->
+ com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(
+ key
+ )
+ }
+ )
+ canonical ?: targetKey
+ }
+
+ startPrivateChat(openPeer) { onEnsureGeohashDMSubscription(openPeer) }
+ // If sidebar visible, hide it to focus on the private chat
+ if (state.getShowSidebarValue()) {
+ state.setShowSidebar(false)
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "openLatestUnreadPrivateChat failed: ${e.message}")
+ }
+ }
+
+ override fun startPrivateChat(peerID: String, onEnsureGeohashDMSubscription: () -> Unit) {
+ // For geohash conversation keys, ensure DM subscription is active
+ if (peerID.startsWith("nostr_")) {
+ onEnsureGeohashDMSubscription()
+ }
+
+ val success = privateChatManager.startPrivateChat(peerID, meshService)
+ if (success) {
+ // Notify notification manager about current private chat
+ setCurrentPrivateChatPeer(peerID)
+ // Clear notifications for this sender since user is now viewing the chat
+ clearNotificationsForSender(peerID)
+
+ // Persistently mark all messages in this conversation as read so Nostr fetches
+ // after app restarts won't re-mark them as unread.
+ try {
+ val seen = com.bitchat.android.services.SeenMessageStore.getInstance(application)
+ val chats = state.getPrivateChatsValue()
+ val messages = chats[peerID] ?: emptyList()
+ messages.forEach { msg ->
+ try {
+ seen.markRead(msg.id)
+ } catch (_: Exception) {
+ }
+ }
+ } catch (_: Exception) {
+ }
+ }
+ }
+
+ override fun endPrivateChat() {
+ privateChatManager.endPrivateChat()
+ // Notify notification manager that no private chat is active
+ setCurrentPrivateChatPeer(null)
+ // Clear mesh mention notifications since user is now back in mesh chat
+ clearMeshMentionNotifications()
+ }
+
+ override fun cancelMediaSend(messageId: String) {
+ val transferId = synchronized(transferMessageMap) { messageTransferMap[messageId] }
+ if (transferId != null) {
+ val cancelled = meshService.cancelFileTransfer(transferId)
+ if (cancelled) {
+ // Remove the message from chat upon explicit cancel
+ messageManager.removeMessageById(messageId)
+ synchronized(transferMessageMap) {
+ transferMessageMap.remove(transferId)
+ messageTransferMap.remove(messageId)
+ }
+ }
+ }
+ }
+
+ override fun updateReactiveStates() {
+ val currentPeers = state.getConnectedPeersValue()
+
+ // Update session states
+ val prevStates = state.getPeerSessionStatesValue()
+ val sessionStates = currentPeers.associateWith { peerID ->
+ meshService.getSessionState(peerID).toString()
+ }
+ state.setPeerSessionStates(sessionStates)
+ // Detect new established sessions and flush router outbox for them and their noiseHex aliases
+ sessionStates.forEach { (peerID, newState) ->
+ val old = prevStates[peerID]
+ if (old != "established" && newState == "established") {
+ com.bitchat.android.services.MessageRouter
+ .getInstance(application, meshService)
+ .onSessionEstablished(peerID)
+ }
+ }
+ // Update fingerprint mappings from centralized manager
+ val fingerprints = privateChatManager.getAllPeerFingerprints()
+ state.setPeerFingerprints(fingerprints)
+
+ val nicknames = meshService.getPeerNicknames()
+ state.setPeerNicknames(nicknames)
+
+ val rssiValues = meshService.getPeerRSSI()
+ state.setPeerRSSI(rssiValues)
+
+ // Update directness per peer (driven by PeerManager state)
+ try {
+ val directMap = state.getConnectedPeersValue().associateWith { pid ->
+ meshService.getPeerInfo(pid)?.isDirectConnection == true
+ }
+ state.setPeerDirect(directMap)
+ } catch (_: Exception) {
+ }
+ }
+
+ override fun joinChannel(channel: String, password: String?): Boolean {
+ return channelManager.joinChannel(channel, password, meshService.myPeerID)
+ }
+
+ override fun switchToChannel(channel: String?) = channelManager.switchToChannel(channel)
+
+
+ override fun leaveChannel(channel: String) {
+ channelManager.leaveChannel(channel)
+ meshService.sendMessage("left $channel")
+ }
+
+ /** Forward to notification manager for notification logic */
+ override fun setAppBackgroundState(inBackground: Boolean) =
+ notificationManager.setAppBackgroundState(inBackground)
+
+
+ override fun setCurrentPrivateChatPeer(peerID: String?) {
+ // Update notification manager with current private chat peer
+ notificationManager.setCurrentPrivateChatPeer(peerID)
+ }
+
+ override fun setCurrentGeohash(geohash: String?) {
+ // Update notification manager with current geohash for notification logic
+ notificationManager.setCurrentGeohash(geohash)
+ }
+
+ override fun clearNotificationsForSender(peerID: String) {
+ // Clear notifications when user opens a chat
+ notificationManager.clearNotificationsForSender(peerID)
+ }
+
+ override fun clearNotificationsForGeohash(geohash: String) {
+ // Clear notifications when user opens a geohash chat
+ notificationManager.clearNotificationsForGeohash(geohash)
+ }
+
+ /**
+ * Clear mesh mention notifications when user opens mesh chat
+ */
+ override fun clearMeshMentionNotifications() {
+ notificationManager.clearMeshMentionNotifications()
+ }
+
+ // MARK: - Command Autocomplete (delegated)
+
+ override fun updateCommandSuggestions(input: String) {
+ commandProcessor.updateCommandSuggestions(input)
+ }
+
+ override fun selectCommandSuggestion(suggestion: CommandSuggestion): String {
+ return commandProcessor.selectCommandSuggestion(suggestion)
+ }
+
+ // MARK: - Mention Autocomplete
+
+ override fun updateMentionSuggestions(input: String) {
+ commandProcessor.updateMentionSuggestions(
+ input, meshService, Pair(geohashPeople.value, selectedLocationChannel.value)
+ )
+ }
+
+ override fun selectMentionSuggestion(nickname: String, currentText: String) =
+ commandProcessor.selectMentionSuggestion(nickname, currentText)
+
+ override fun changeMeshServiceBGState(b: Boolean) =
+ meshService.connectionManager.setAppBackgroundState(b)
+
+ override fun startMeshServices() = meshService.startServices()
+ override fun stopMeshServices() = meshService.startServices()
+ // registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager
+
+ // MARK: - Emergency Clear
+
+ override fun panicClearAllData(onPanicReset: () -> Unit) {
+ Log.w(TAG, "🚨 PANIC MODE ACTIVATED - Clearing all sensitive data")
+
+ // Clear all UI managers
+ messageManager.clearAllMessages()
+ channelManager.clearAllChannels()
+ privateChatManager.clearAllPrivateChats()
+ dataManager.clearAllData()
+
+ // Clear all mesh service data
+ clearAllMeshServiceData()
+
+ // Clear all cryptographic data
+ clearAllCryptographicData()
+
+ // Clear all notifications
+ notificationManager.clearAllNotifications()
+
+ // Clear Nostr/geohash state, keys, connections, bookmarks, and reinitialize from scratch
+ try {
+ // Clear geohash bookmarks too (panic should remove everything)
+ try {
+ val store =
+ com.bitchat.android.geohash.GeohashBookmarksStore.getInstance(application)
+ store.clearAll()
+ } catch (_: Exception) {
+ }
+
+ onPanicReset()
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to reset Nostr/geohash: ${e.message}")
+ }
+
+ // Reset nickname
+ val newNickname = "anon${Random.nextInt(1000, 9999)}"
+ state.setNickname(newNickname)
+ dataManager.saveNickname(newNickname)
+
+ Log.w(TAG, "🚨 PANIC MODE COMPLETED - All sensitive data cleared")
+
+ // Note: Mesh service restart is now handled by MainActivity
+ // This method now only clears data, not mesh service lifecycle
+ }
+
+ /**
+ * Clear all mesh service related data
+ */
+ private fun clearAllMeshServiceData() {
+ try {
+ // Request mesh service to clear all its internal data
+ meshService.clearAllInternalData()
+
+ Log.d(TAG, "✅ Cleared all mesh service data")
+ } catch (e: Exception) {
+ Log.e(TAG, "❌ Error clearing mesh service data: ${e.message}")
+ }
+ }
+
+ /**
+ * Clear all cryptographic data including persistent identity
+ */
+ private fun clearAllCryptographicData() {
+ try {
+ // Clear encryption service persistent identity (Ed25519 signing keys)
+ meshService.clearAllEncryptionData()
+
+ // Clear secure identity state (if used)
+ try {
+ val identityManager =
+ com.bitchat.android.identity.SecureIdentityStateManager(application)
+ identityManager.clearIdentityData()
+ // Also clear secure values used by FavoritesPersistenceService (favorites + peerID index)
+ try {
+ identityManager.clearSecureValues(
+ "favorite_relationships",
+ "favorite_peerid_index"
+ )
+ } catch (_: Exception) {
+ }
+ Log.d(TAG, "✅ Cleared secure identity state and secure favorites store")
+ } catch (e: Exception) {
+ Log.d(
+ TAG,
+ "SecureIdentityStateManager not available or already cleared: ${e.message}"
+ )
+ }
+
+ // Clear FavoritesPersistenceService persistent relationships
+ try {
+ com.bitchat.android.favorites.FavoritesPersistenceService.shared.clearAllFavorites()
+ Log.d(TAG, "✅ Cleared FavoritesPersistenceService relationships")
+ } catch (_: Exception) {
+ }
+
+ Log.d(TAG, "✅ Cleared all cryptographic data")
+ } catch (e: Exception) {
+ Log.e(TAG, "❌ Error clearing cryptographic data: ${e.message}")
+ }
+ }
+
+ override fun loadAndInitialize(
+ logCurrentFavoriteState: () -> Unit, initializeSessionStateMonitoring: () -> Unit,
+ initializeGeoHashVM: () -> Unit
+ ) {
+ // Load nickname
+ val nickname = dataManager.loadNickname()
+ state.setNickname(nickname)
+
+ // Load data
+ val (joinedChannels, protectedChannels) = channelManager.loadChannelData()
+ state.setJoinedChannels(joinedChannels)
+ state.setPasswordProtectedChannels(protectedChannels)
+
+ // Initialize channel messages
+ joinedChannels.forEach { channel ->
+ if (!state.getChannelMessagesValue().containsKey(channel)) {
+ val updatedChannelMessages = state.getChannelMessagesValue().toMutableMap()
+ updatedChannelMessages[channel] = emptyList()
+ state.setChannelMessages(updatedChannelMessages)
+ }
+ }
+
+ // Load other data
+ dataManager.loadFavorites()
+ state.setFavoritePeers(dataManager.favoritePeers.toSet())
+ dataManager.loadBlockedUsers()
+ dataManager.loadGeohashBlockedUsers()
+
+ // Log all favorites at startup
+ dataManager.logAllFavorites()
+ logCurrentFavoriteState()
+
+ // Initialize session state monitoring
+ initializeSessionStateMonitoring()
+
+ // Bridge DebugSettingsManager -> Chat messages when verbose logging is on
+ scope.launch {
+ com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().debugMessages.collect { msgs ->
+ if (com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().verboseLoggingEnabled.value) {
+ // Only show debug logs in the Mesh chat timeline to avoid leaking into geohash chats
+ val selectedLocation = state.selectedLocationChannel.value
+ if (selectedLocation is ChannelID.Mesh) {
+ // Append only latest debug message as system message to avoid flooding
+ msgs.lastOrNull()?.let { dm ->
+ messageManager.addSystemMessage(dm.content)
+ }
+ }
+ }
+ }
+ }
+
+ // Initialize new geohash architecture
+ initializeGeoHashVM()
+
+ // Initialize favorites persistence service
+ com.bitchat.android.favorites.FavoritesPersistenceService.initialize(application)
+
+
+ // Ensure NostrTransport knows our mesh peer ID for embedded packets
+ try {
+ val nostrTransport =
+ com.bitchat.android.nostr.NostrTransport.getInstance(application)
+ nostrTransport.senderPeerID = meshService.myPeerID
+ } catch (_: Exception) {
+ }
+
+ // Note: Mesh service is now started by MainActivity
+
+ // Show welcome message if no peers after delay
+ scope.launch(Dispatchers.Main) {
+ delay(10000)
+ if (state.getConnectedPeersValue().isEmpty() && state.getMessagesValue().isEmpty()) {
+ try {
+ val welcomeMessage = BitchatMessage(
+ sender = "system",
+ content = "get people around you to download bitchat and chat with them here!",
+ timestamp = Date(),
+ isRelay = false
+ )
+ messageManager.addMessage(welcomeMessage)
+ } catch (e: Exception) {
+ logWarn("${e.printStackTrace()}")
+ }
+ }
+ }
+
+ // BLE receives are inserted by MessageHandler path; no VoiceNoteBus for Tor in this branch.
+ }
+
+ override fun toggleFavorite(peerID: String, logCurrentFavoriteState: () -> Unit) {
+ Log.d("ChatViewModel", "toggleFavorite called for peerID: $peerID")
+ privateChatManager.toggleFavorite(peerID)
+
+ // Persist relationship in FavoritesPersistenceService
+ try {
+ var noiseKey: ByteArray? = null
+ var nickname: String = meshService.getPeerNicknames()[peerID] ?: peerID
+
+ // Case 1: Live mesh peer with known info
+ val peerInfo = meshService.getPeerInfo(peerID)
+ if (peerInfo?.noisePublicKey != null) {
+ noiseKey = peerInfo.noisePublicKey
+ nickname = peerInfo.nickname
+ } else {
+ // Case 2: Offline favorite entry using 64-hex noise public key as peerID
+ if (peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
+ try {
+ noiseKey = peerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
+ // Prefer nickname from favorites store if available
+ val rel =
+ com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(
+ noiseKey
+ )
+ if (rel != null) nickname = rel.peerNickname
+ } catch (_: Exception) {
+ }
+ }
+ }
+
+ if (noiseKey != null) {
+ // Determine current favorite state from DataManager using fingerprint
+ val identityManager =
+ com.bitchat.android.identity.SecureIdentityStateManager(application)
+ val fingerprint = identityManager.generateFingerprint(noiseKey)
+ val isNowFavorite = dataManager.favoritePeers.contains(fingerprint)
+
+ com.bitchat.android.favorites.FavoritesPersistenceService.shared.updateFavoriteStatus(
+ noisePublicKey = noiseKey,
+ nickname = nickname,
+ isFavorite = isNowFavorite
+ )
+
+ // Send favorite notification via mesh or Nostr with our npub if available
+ try {
+ val myNostr =
+ com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(
+ application
+ )
+ val announcementContent =
+ if (isNowFavorite) "[FAVORITED]:${myNostr?.npub ?: ""}" else "[UNFAVORITED]:${myNostr?.npub ?: ""}"
+ // Prefer mesh if session established, else try Nostr
+ 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(application)
+ nostrTransport.senderPeerID = meshService.myPeerID
+ nostrTransport.sendFavoriteNotification(peerID, isNowFavorite)
+ }
+ } catch (_: Exception) {
+ }
+ }
+ } catch (_: Exception) {
+ }
+
+ // Log current state after toggle
+ logCurrentFavoriteState()
+ }
+
+ override fun sendMessage(
+ content: String,
+ onSendGeohashMessage: (String, GeohashChannel) -> Unit
+ ) {
+ if (content.isEmpty()) return
+
+ // Check for commands
+ if (content.startsWith("/")) {
+ val selectedLocationForCommand = state.selectedLocationChannel.value
+ commandProcessor.processCommand(
+ content, meshService, meshService.myPeerID,
+ onSendMessage = { messageContent, mentions, channel ->
+ if (selectedLocationForCommand is ChannelID.Location) {
+ // Route command-generated public messages via Nostr in geohash channels
+ onSendGeohashMessage(messageContent, selectedLocationForCommand.channel)
+
+ } else {
+ // Default: route via mesh
+ meshService.sendMessage(messageContent, mentions, channel)
+ }
+ })
+ return
+ }
+
+ val mentions = messageManager.parseMentions(
+ content,
+ meshService.getPeerNicknames().values.toSet(),
+ state.getNicknameValue()
+ )
+ // 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"
+
+ var selectedPeer = state.getSelectedPrivateChatPeerValue()
+ val currentChannelValue = state.getCurrentChannelValue()
+
+ if (selectedPeer != null) {
+ // If the selected peer is a temporary Nostr alias or a noise-hex identity, resolve to a canonical target
+ selectedPeer =
+ com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
+ selectedPeerID = selectedPeer,
+ connectedPeers = state.getConnectedPeersValue(),
+ meshNoiseKeyForPeer = { pid -> meshService.getPeerInfo(pid)?.noisePublicKey },
+ meshHasPeer = { pid -> meshService.getPeerInfo(pid)?.isConnected == true },
+ nostrPubHexForAlias = { alias ->
+ com.bitchat.android.nostr.GeohashAliasRegistry.get(
+ alias
+ )
+ },
+ findNoiseKeyForNostr = { key ->
+ com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(
+ key
+ )
+ }
+ ).also { canonical ->
+ if (canonical != state.getSelectedPrivateChatPeerValue()) {
+ privateChatManager.startPrivateChat(canonical, meshService)
+ }
+ }
+ // Send private message
+ val recipientNickname = meshService.getPeerNicknames()[selectedPeer]
+ privateChatManager.sendPrivateMessage(
+ content,
+ selectedPeer,
+ recipientNickname,
+ state.getNicknameValue(),
+ meshService.myPeerID
+ ) { messageContent, peerID, recipientNicknameParam, messageId ->
+ // Route via MessageRouter (mesh when connected+established, else Nostr)
+ val router = com.bitchat.android.services.MessageRouter.getInstance(
+ application,
+ meshService
+ )
+ router.sendPrivate(messageContent, peerID, recipientNicknameParam, messageId)
+ }
+ } else {
+ // Check if we're in a location channel
+ val selectedLocationChannel = state.selectedLocationChannel.value
+ if (selectedLocationChannel is ChannelID.Location) {
+ // Send to geohash channel via Nostr ephemeral event
+ onSendGeohashMessage(content, selectedLocationChannel.channel)
+ } else {
+ // Send public/channel message via mesh
+ val message = BitchatMessage(
+ sender = state.getNicknameValue() ?: meshService.myPeerID,
+ content = content,
+ timestamp = Date(),
+ isRelay = false,
+ senderPeerID = meshService.myPeerID,
+ mentions = mentions.ifEmpty { null },
+ channel = currentChannelValue
+ )
+
+ if (currentChannelValue != null) {
+ channelManager.addChannelMessage(
+ currentChannelValue,
+ message,
+ meshService.myPeerID
+ )
+
+ // Check if encrypted channel
+ if (channelManager.hasChannelKey(currentChannelValue)) {
+ channelManager.sendEncryptedChannelMessage(
+ content,
+ mentions,
+ currentChannelValue,
+ state.getNicknameValue(),
+ meshService.myPeerID,
+ onEncryptedPayload = { encryptedData ->
+ // This would need proper mesh service integration
+ meshService.sendMessage(content, mentions, currentChannelValue)
+ },
+ onFallback = {
+ meshService.sendMessage(content, mentions, currentChannelValue)
+ }
+ )
+ } else {
+ meshService.sendMessage(content, mentions, currentChannelValue)
+ }
+ } else {
+ messageManager.addMessage(message)
+ meshService.sendMessage(content, mentions, null)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt
index 98d7a6227..1741a0151 100644
--- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt
+++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt
@@ -1,10 +1,13 @@
package com.bitchat.android.mesh
+import android.app.Application
import android.content.Context
import android.util.Log
+import androidx.core.app.NotificationManagerCompat
+import androidx.lifecycle.LiveData
import com.bitchat.android.crypto.EncryptionService
+import com.bitchat.android.geohash.ChannelID
import com.bitchat.android.model.BitchatMessage
-import com.bitchat.android.protocol.MessagePadding
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.model.IdentityAnnouncement
import com.bitchat.android.protocol.BitchatPacket
@@ -12,6 +15,19 @@ import com.bitchat.android.protocol.MessageType
import com.bitchat.android.protocol.SpecialRecipients
import com.bitchat.android.model.RequestSyncPacket
import com.bitchat.android.sync.GossipSyncManager
+import com.bitchat.android.ui.ChannelManager
+import com.bitchat.android.ui.ChatState
+import com.bitchat.android.ui.ChatViewModelUtils
+import com.bitchat.android.ui.CommandProcessor
+import com.bitchat.android.ui.CommandSuggestion
+import com.bitchat.android.ui.DataManager
+import com.bitchat.android.ui.GeoPerson
+import com.bitchat.android.ui.MeshDelegateHandler
+import com.bitchat.android.ui.MessageManager
+import com.bitchat.android.ui.NoiseSessionDelegate
+import com.bitchat.android.ui.NotificationManager
+import com.bitchat.android.ui.PrivateChatManager
+import com.bitchat.android.util.NotificationIntervalManager
import com.bitchat.android.util.toHexString
import kotlinx.coroutines.*
import java.util.*
@@ -21,10 +37,10 @@ import kotlin.random.Random
/**
* Bluetooth mesh service - REFACTORED to use component-based architecture
* 100% compatible with iOS version and maintains exact same UUIDs, packet format, and protocol logic
- *
+ *
* This is now a coordinator that orchestrates the following components:
* - PeerManager: Peer lifecycle management
- * - FragmentManager: Message fragmentation and reassembly
+ * - FragmentManager: Message fragmentation and reassembly
* - SecurityManager: Security, duplicate detection, encryption
* - StoreForwardManager: Offline message caching
* - MessageHandler: Message type processing and relay logic
@@ -32,13 +48,19 @@ import kotlin.random.Random
* - PacketProcessor: Incoming packet routing
*/
class BluetoothMeshService(private val context: Context) {
- 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 {
private const val TAG = "BluetoothMeshService"
private const val MAX_TTL: UByte = 7u
}
-
+
// Core components - each handling specific responsibilities
private val encryptionService = EncryptionService(context)
@@ -49,19 +71,20 @@ class BluetoothMeshService(private val context: Context) {
private val securityManager = SecurityManager(encryptionService, myPeerID)
private val storeForwardManager = StoreForwardManager()
private val messageHandler = MessageHandler(myPeerID, context.applicationContext)
- internal val connectionManager = BluetoothConnectionManager(context, myPeerID, fragmentManager) // Made internal for access
+ internal val connectionManager =
+ BluetoothConnectionManager(context, myPeerID, fragmentManager) // Made internal for access
private val packetProcessor = PacketProcessor(myPeerID)
private lateinit var gossipSyncManager: GossipSyncManager
-
+
// Service state management
private var isActive = false
-
+
// Delegate for message callbacks (maintains same interface)
var delegate: BluetoothMeshDelegate? = null
-
+
// Coroutines
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
+
init {
setupDelegates()
messageHandler.packetProcessor = packetProcessor
@@ -74,15 +97,21 @@ class BluetoothMeshService(private val context: Context) {
configProvider = object : GossipSyncManager.ConfigProvider {
override fun seenCapacity(): Int = try {
com.bitchat.android.ui.debug.DebugPreferenceManager.getSeenPacketCapacity(500)
- } catch (_: Exception) { 500 }
+ } catch (_: Exception) {
+ 500
+ }
override fun gcsMaxBytes(): Int = try {
com.bitchat.android.ui.debug.DebugPreferenceManager.getGcsMaxFilterBytes(400)
- } catch (_: Exception) { 400 }
+ } catch (_: Exception) {
+ 400
+ }
override fun gcsTargetFpr(): Double = try {
com.bitchat.android.ui.debug.DebugPreferenceManager.getGcsFprPercent(1.0) / 100.0
- } catch (_: Exception) { 0.01 }
+ } catch (_: Exception) {
+ 0.01
+ }
}
)
@@ -91,15 +120,17 @@ class BluetoothMeshService(private val context: Context) {
override fun sendPacket(packet: BitchatPacket) {
connectionManager.broadcastPacket(RoutedPacket(packet))
}
+
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
connectionManager.sendPacketToPeer(peerID, packet)
}
+
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
return signPacketBeforeBroadcast(packet)
}
}
}
-
+
/**
* Start periodic debug logging every 10 seconds
*/
@@ -110,7 +141,10 @@ class BluetoothMeshService(private val context: Context) {
delay(10000) // 10 seconds
if (isActive) { // Double-check before logging
val debugInfo = getDebugStatus()
- Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===")
+ Log.d(
+ TAG,
+ "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ==="
+ )
}
} catch (e: Exception) {
Log.e(TAG, "Error in periodic debug logging: ${e.message}")
@@ -134,7 +168,7 @@ class BluetoothMeshService(private val context: Context) {
}
}
}
-
+
/**
* Setup delegate connections between components
*/
@@ -142,14 +176,19 @@ class BluetoothMeshService(private val context: Context) {
// Provide nickname resolver to BLE broadcaster for detailed logs
try {
connectionManager.setNicknameResolver { pid -> peerManager.getPeerNickname(pid) }
- } catch (_: Exception) { }
+ } catch (_: Exception) {
+ }
// PeerManager delegates to main mesh service delegate
peerManager.delegate = object : PeerManagerDelegate {
override fun onPeerListUpdated(peerIDs: List) {
delegate?.didUpdatePeerList(peerIDs)
}
+
override fun onPeerRemoved(peerID: String) {
- try { gossipSyncManager.removeAnnouncementForPeer(peerID) } catch (_: Exception) { }
+ try {
+ gossipSyncManager.removeAnnouncementForPeer(peerID)
+ } catch (_: Exception) {
+ }
// Also drop any Noise session state for this peer when they go offline
try {
encryptionService.removePeer(peerID)
@@ -159,7 +198,7 @@ class BluetoothMeshService(private val context: Context) {
}
}
}
-
+
// SecurityManager delegate for key exchange notifications
securityManager.delegate = object : SecurityManagerDelegate {
override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) {
@@ -167,12 +206,12 @@ class BluetoothMeshService(private val context: Context) {
serviceScope.launch {
delay(100)
sendAnnouncementToPeer(peerID)
-
+
delay(1000)
storeForwardManager.sendCachedMessages(peerID)
}
}
-
+
override fun sendHandshakeResponse(peerID: String, response: ByteArray) {
// Send Noise handshake response
val responsePacket = BitchatPacket(
@@ -189,99 +228,118 @@ class BluetoothMeshService(private val context: Context) {
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)")
}
-
+
override fun getPeerInfo(peerID: String): PeerInfo? {
return peerManager.getPeerInfo(peerID)
}
}
-
+
// StoreForwardManager delegates
storeForwardManager.delegate = object : StoreForwardManagerDelegate {
override fun isFavorite(peerID: String): Boolean {
return delegate?.isFavorite(peerID) ?: false
}
-
+
override fun isPeerOnline(peerID: String): Boolean {
return peerManager.isPeerActive(peerID)
}
-
+
override fun sendPacket(packet: BitchatPacket) {
connectionManager.broadcastPacket(RoutedPacket(packet))
}
}
-
+
// MessageHandler delegates
messageHandler.delegate = object : MessageHandlerDelegate {
// Peer management
override fun addOrUpdatePeer(peerID: String, nickname: String): Boolean {
return peerManager.addOrUpdatePeer(peerID, nickname)
}
-
+
override fun removePeer(peerID: String) {
peerManager.removePeer(peerID)
}
-
+
override fun updatePeerNickname(peerID: String, nickname: String) {
peerManager.addOrUpdatePeer(peerID, nickname)
}
-
+
override fun getPeerNickname(peerID: String): String? {
return peerManager.getPeerNickname(peerID)
}
-
+
override fun getNetworkSize(): Int {
return peerManager.getActivePeerCount()
}
-
+
override fun getMyNickname(): String? {
return delegate?.getNickname()
}
-
+
override fun getPeerInfo(peerID: String): PeerInfo? {
return peerManager.getPeerInfo(peerID)
}
-
- override fun updatePeerInfo(peerID: String, nickname: String, noisePublicKey: ByteArray, signingPublicKey: ByteArray, isVerified: Boolean): Boolean {
- return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
+
+ override fun updatePeerInfo(
+ peerID: String,
+ nickname: String,
+ noisePublicKey: ByteArray,
+ signingPublicKey: ByteArray,
+ isVerified: Boolean
+ ): Boolean {
+ return peerManager.updatePeerInfo(
+ peerID,
+ nickname,
+ noisePublicKey,
+ signingPublicKey,
+ isVerified
+ )
}
-
+
// Packet operations
override fun sendPacket(packet: BitchatPacket) {
// Sign the packet before broadcasting
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
}
-
+
override fun relayPacket(routed: RoutedPacket) {
connectionManager.broadcastPacket(routed)
}
-
+
override fun getBroadcastRecipient(): ByteArray {
return SpecialRecipients.BROADCAST
}
-
+
// Cryptographic operations
override fun verifySignature(packet: BitchatPacket, peerID: String): Boolean {
return securityManager.verifySignature(packet, peerID)
}
-
+
override fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? {
return securityManager.encryptForPeer(data, recipientPeerID)
}
-
- override fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? {
+
+ override fun decryptFromPeer(
+ encryptedData: ByteArray,
+ senderPeerID: String
+ ): ByteArray? {
return securityManager.decryptFromPeer(encryptedData, senderPeerID)
}
-
- override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean {
+
+ override fun verifyEd25519Signature(
+ signature: ByteArray,
+ data: ByteArray,
+ publicKey: ByteArray
+ ): Boolean {
return encryptionService.verifyEd25519Signature(signature, data, publicKey)
}
-
+
// Noise protocol operations
override fun hasNoiseSession(peerID: String): Boolean {
return encryptionService.hasEstablishedSession(peerID)
}
-
+
override fun initiateNoiseHandshake(peerID: String) {
try {
// Initiate proper Noise handshake with specific peer
@@ -301,17 +359,23 @@ class BluetoothMeshService(private val context: Context) {
// Sign the handshake packet before broadcasting
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
- Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)")
+ Log.d(
+ TAG,
+ "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)"
+ )
} else {
Log.w(TAG, "Failed to generate Noise handshake data for $peerID")
}
-
+
} catch (e: Exception) {
Log.e(TAG, "Failed to initiate Noise handshake with $peerID: ${e.message}")
}
}
-
- override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? {
+
+ override fun processNoiseHandshakeMessage(
+ payload: ByteArray,
+ peerID: String
+ ): ByteArray? {
return try {
encryptionService.processHandshakeMessage(payload, peerID)
} catch (e: Exception) {
@@ -319,86 +383,107 @@ class BluetoothMeshService(private val context: Context) {
null
}
}
-
- override fun updatePeerIDBinding(newPeerID: String, nickname: String,
- publicKey: ByteArray, previousPeerID: String?) {
- Log.d(TAG, "Updating peer ID binding: $newPeerID (was: $previousPeerID) with nickname: $nickname and public key: ${publicKey.toHexString().take(16)}...")
+ override fun updatePeerIDBinding(
+ newPeerID: String, nickname: String,
+ publicKey: ByteArray, previousPeerID: String?
+ ) {
+
+ Log.d(
+ TAG,
+ "Updating peer ID binding: $newPeerID (was: $previousPeerID) with nickname: $nickname and public key: ${
+ publicKey.toHexString().take(16)
+ }..."
+ )
// Update peer mapping in the PeerManager for peer ID rotation support
peerManager.addOrUpdatePeer(newPeerID, nickname)
-
+
// Store fingerprint for the peer via centralized fingerprint manager
val fingerprint = peerManager.storeFingerprintForPeer(newPeerID, publicKey)
// Index existing Nostr mapping by the new peerID if we have it
try {
- com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNostrPubkey(publicKey)?.let { npub ->
- com.bitchat.android.favorites.FavoritesPersistenceService.shared.updateNostrPublicKeyForPeerID(newPeerID, npub)
+ com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNostrPubkey(
+ publicKey
+ )?.let { npub ->
+ com.bitchat.android.favorites.FavoritesPersistenceService.shared.updateNostrPublicKeyForPeerID(
+ newPeerID,
+ npub
+ )
}
- } catch (_: Exception) { }
-
+ } catch (_: Exception) {
+ }
+
// If there was a previous peer ID, remove it to avoid duplicates
previousPeerID?.let { oldPeerID ->
peerManager.removePeer(oldPeerID)
}
-
- Log.d(TAG, "Updated peer ID binding: $newPeerID (was: $previousPeerID), fingerprint: ${fingerprint.take(16)}...")
+
+ Log.d(
+ TAG,
+ "Updated peer ID binding: $newPeerID (was: $previousPeerID), fingerprint: ${
+ fingerprint.take(16)
+ }..."
+ )
}
-
+
// Message operations
- override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
+ override fun decryptChannelMessage(
+ encryptedContent: ByteArray,
+ channel: String
+ ): String? {
return delegate?.decryptChannelMessage(encryptedContent, channel)
}
-
+
// Callbacks
override fun onMessageReceived(message: BitchatMessage) {
delegate?.didReceiveMessage(message)
}
-
+
override fun onChannelLeave(channel: String, fromPeer: String) {
delegate?.didReceiveChannelLeave(channel, fromPeer)
}
-
+
override fun onDeliveryAckReceived(messageID: String, peerID: String) {
delegate?.didReceiveDeliveryAck(messageID, peerID)
}
-
+
override fun onReadReceiptReceived(messageID: String, peerID: String) {
delegate?.didReceiveReadReceipt(messageID, peerID)
}
}
-
+
// PacketProcessor delegates
packetProcessor.delegate = object : PacketProcessorDelegate {
override fun validatePacketSecurity(packet: BitchatPacket, peerID: String): Boolean {
return securityManager.validatePacket(packet, peerID)
}
-
+
override fun updatePeerLastSeen(peerID: String) {
peerManager.updatePeerLastSeen(peerID)
}
-
+
override fun getPeerNickname(peerID: String): String? {
return peerManager.getPeerNickname(peerID)
}
-
+
// Network information for relay manager
override fun getNetworkSize(): Int {
return peerManager.getActivePeerCount()
}
-
+
override fun getBroadcastRecipient(): ByteArray {
return SpecialRecipients.BROADCAST
}
-
+
override fun handleNoiseHandshake(routed: RoutedPacket): Boolean {
return runBlocking { securityManager.handleNoiseHandshake(routed) }
}
-
+
override fun handleNoiseEncrypted(routed: RoutedPacket) {
serviceScope.launch { messageHandler.handleNoiseEncrypted(routed) }
}
-
+
override fun handleAnnounce(routed: RoutedPacket) {
serviceScope.launch {
// Process the announce
@@ -422,54 +507,68 @@ class BluetoothMeshService(private val context: Context) {
// Also push reactive directness state to UI (best-effort)
try {
// Note: UI observes via didUpdatePeerList, but we can also update ChatState on a timer
- } catch (_: Exception) { }
+ } catch (_: Exception) {
+ }
}
- } catch (_: Exception) { }
+ } catch (_: Exception) {
+ }
// Schedule initial sync for this new directly connected peer only
- try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { }
+ try {
+ gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000)
+ } catch (_: Exception) {
+ }
}
}
// Track for sync
- try { gossipSyncManager.onPublicPacketSeen(routed.packet) } catch (_: Exception) { }
+ try {
+ gossipSyncManager.onPublicPacketSeen(routed.packet)
+ } catch (_: Exception) {
+ }
}
}
-
+
override fun handleMessage(routed: RoutedPacket) {
serviceScope.launch { messageHandler.handleMessage(routed) }
// Track broadcast messages for sync
try {
val pkt = routed.packet
- val isBroadcast = (pkt.recipientID == null || pkt.recipientID.contentEquals(SpecialRecipients.BROADCAST))
+ val isBroadcast =
+ (pkt.recipientID == null || pkt.recipientID.contentEquals(SpecialRecipients.BROADCAST))
if (isBroadcast && pkt.type == MessageType.MESSAGE.value) {
gossipSyncManager.onPublicPacketSeen(pkt)
}
- } catch (_: Exception) { }
+ } catch (_: Exception) {
+ }
}
-
+
override fun handleLeave(routed: RoutedPacket) {
serviceScope.launch { messageHandler.handleLeave(routed) }
}
-
+
override fun handleFragment(packet: BitchatPacket): BitchatPacket? {
// Track broadcast fragments for gossip sync
try {
- val isBroadcast = (packet.recipientID == null || packet.recipientID.contentEquals(SpecialRecipients.BROADCAST))
+ val isBroadcast =
+ (packet.recipientID == null || packet.recipientID.contentEquals(
+ SpecialRecipients.BROADCAST
+ ))
if (isBroadcast && packet.type == MessageType.FRAGMENT.value) {
gossipSyncManager.onPublicPacketSeen(packet)
}
- } catch (_: Exception) { }
+ } catch (_: Exception) {
+ }
return fragmentManager.handleFragment(packet)
}
-
+
override fun sendAnnouncementToPeer(peerID: String) {
this@BluetoothMeshService.sendAnnouncementToPeer(peerID)
}
-
+
override fun sendCachedMessages(peerID: String) {
storeForwardManager.sendCachedMessages(peerID)
}
-
+
override fun relayPacket(routed: RoutedPacket) {
connectionManager.broadcastPacket(routed)
}
@@ -481,13 +580,17 @@ class BluetoothMeshService(private val context: Context) {
gossipSyncManager.handleRequestSync(fromPeer, req)
}
}
-
+
// BluetoothConnectionManager delegates
connectionManager.delegate = object : BluetoothConnectionManagerDelegate {
- override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: android.bluetooth.BluetoothDevice?) {
+ override fun onPacketReceived(
+ packet: BitchatPacket,
+ peerID: String,
+ device: android.bluetooth.BluetoothDevice?
+ ) {
packetProcessor.processPacket(RoutedPacket(packet, peerID, device?.address))
}
-
+
override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) {
// Send initial announcements after services are ready
serviceScope.launch {
@@ -500,8 +603,14 @@ class BluetoothMeshService(private val context: Context) {
val peer = connectionManager.addressPeerMap[addr]
val nick = peer?.let { peerManager.getPeerNickname(it) } ?: "unknown"
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
- .logPeerConnection(peer ?: "unknown", nick, addr, isInbound = !connectionManager.isClientConnection(addr)!!)
- } catch (_: Exception) { }
+ .logPeerConnection(
+ peer ?: "unknown",
+ nick,
+ addr,
+ isInbound = !connectionManager.isClientConnection(addr)!!
+ )
+ } catch (_: Exception) {
+ }
}
override fun onDeviceDisconnected(device: android.bluetooth.BluetoothDevice) {
@@ -514,17 +623,21 @@ class BluetoothMeshService(private val context: Context) {
val stillMapped = connectionManager.addressPeerMap.values.any { it == peer }
if (!stillMapped) {
// Peer might still be reachable indirectly; mark as not-direct
- try { peerManager.setDirectConnection(peer, false) } catch (_: Exception) { }
+ try {
+ peerManager.setDirectConnection(peer, false)
+ } catch (_: Exception) {
+ }
}
// Verbose debug: device disconnected
try {
val nick = peerManager.getPeerNickname(peer) ?: "unknown"
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
.logPeerDisconnection(peer, nick, addr)
- } catch (_: Exception) { }
+ } catch (_: Exception) {
+ }
}
}
-
+
override fun onRSSIUpdated(deviceAddress: String, rssi: Int) {
// Find the peer ID for this device address and update RSSI in PeerManager
connectionManager.addressPeerMap[deviceAddress]?.let { peerID ->
@@ -533,7 +646,7 @@ class BluetoothMeshService(private val context: Context) {
}
}
}
-
+
/**
* Start the mesh service
*/
@@ -543,12 +656,12 @@ class BluetoothMeshService(private val context: Context) {
Log.w(TAG, "Mesh service already active, ignoring duplicate start request")
return
}
-
+
Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID")
-
+
if (connectionManager.startServices()) {
isActive = true
-
+
// Start periodic announcements for peer discovery and connectivity
sendPeriodicBroadcastAnnounce()
Log.d(TAG, "Started periodic broadcast announcements (every 30 seconds)")
@@ -558,7 +671,7 @@ class BluetoothMeshService(private val context: Context) {
Log.e(TAG, "Failed to start Bluetooth services")
}
}
-
+
/**
* Stop all mesh services
*/
@@ -567,16 +680,16 @@ class BluetoothMeshService(private val context: Context) {
Log.w(TAG, "Mesh service not active, ignoring stop request")
return
}
-
+
Log.i(TAG, "Stopping Bluetooth mesh service")
isActive = false
-
+
// Send leave announcement
sendLeaveAnnouncement()
-
+
serviceScope.launch {
delay(200) // Give leave message time to send
-
+
// Stop all components
gossipSyncManager.stop()
connectionManager.stopServices()
@@ -586,17 +699,21 @@ class BluetoothMeshService(private val context: Context) {
storeForwardManager.shutdown()
messageHandler.shutdown()
packetProcessor.shutdown()
-
+
serviceScope.cancel()
}
}
-
+
/**
* Send public message
*/
- fun sendMessage(content: String, mentions: List = emptyList(), channel: String? = null) {
+ fun sendMessage(
+ content: String,
+ mentions: List = emptyList(),
+ channel: String? = null
+ ) {
if (content.isEmpty()) return
-
+
serviceScope.launch {
val packet = BitchatPacket(
version = 1u,
@@ -613,7 +730,10 @@ class BluetoothMeshService(private val context: Context) {
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
// Track our own broadcast message for sync
- try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
+ try {
+ gossipSyncManager.onPublicPacketSeen(signedPacket)
+ } catch (_: Exception) {
+ }
}
}
@@ -629,24 +749,27 @@ class BluetoothMeshService(private val context: Context) {
return
}
Log.d(TAG, "📦 Encoded payload: ${payload.size} bytes")
- serviceScope.launch {
- val packet = BitchatPacket(
- version = 2u, // FILE_TRANSFER uses v2 for 4-byte payload length to support large files
- type = MessageType.FILE_TRANSFER.value,
- senderID = hexStringToByteArray(myPeerID),
- recipientID = SpecialRecipients.BROADCAST,
- timestamp = System.currentTimeMillis().toULong(),
- payload = payload,
- signature = null,
- ttl = MAX_TTL
- )
- val signed = signPacketBeforeBroadcast(packet)
- // Use a stable transferId based on the file TLV payload for progress tracking
- val transferId = sha256Hex(payload)
- connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
- try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
- }
- } catch (e: Exception) {
+ serviceScope.launch {
+ val packet = BitchatPacket(
+ version = 2u, // FILE_TRANSFER uses v2 for 4-byte payload length to support large files
+ type = MessageType.FILE_TRANSFER.value,
+ senderID = hexStringToByteArray(myPeerID),
+ recipientID = SpecialRecipients.BROADCAST,
+ timestamp = System.currentTimeMillis().toULong(),
+ payload = payload,
+ signature = null,
+ ttl = MAX_TTL
+ )
+ val signed = signPacketBeforeBroadcast(packet)
+ // Use a stable transferId based on the file TLV payload for progress tracking
+ val transferId = sha256Hex(payload)
+ connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
+ try {
+ gossipSyncManager.onPublicPacketSeen(signed)
+ } catch (_: Exception) {
+ }
+ }
+ } catch (e: Exception) {
Log.e(TAG, "❌ sendFileBroadcast failed: ${e.message}", e)
Log.e(TAG, "❌ File: name=${file.fileName}, size=${file.fileSize}")
}
@@ -655,10 +778,16 @@ class BluetoothMeshService(private val context: Context) {
/**
* Send a file as an encrypted private message using Noise protocol
*/
- fun sendFilePrivate(recipientPeerID: String, file: com.bitchat.android.model.BitchatFilePacket) {
+ fun sendFilePrivate(
+ recipientPeerID: String,
+ file: com.bitchat.android.model.BitchatFilePacket
+ ) {
try {
- Log.d(TAG, "📤 sendFilePrivate (ENCRYPTED): to=$recipientPeerID, name=${file.fileName}, size=${file.fileSize}")
-
+ Log.d(
+ TAG,
+ "📤 sendFilePrivate (ENCRYPTED): to=$recipientPeerID, name=${file.fileName}, size=${file.fileSize}"
+ )
+
serviceScope.launch {
// Check if we have an established Noise session
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
@@ -670,21 +799,22 @@ class BluetoothMeshService(private val context: Context) {
return@launch
}
Log.d(TAG, "📦 Encoded file TLV: ${filePayload.size} bytes")
-
+
// Create NoisePayload wrapper (type byte + file TLV data) - same as iOS
val noisePayload = com.bitchat.android.model.NoisePayload(
type = com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER,
data = filePayload
)
-
+
// Encrypt the payload using Noise
- val encrypted = encryptionService.encrypt(noisePayload.encode(), recipientPeerID)
+ val encrypted =
+ encryptionService.encrypt(noisePayload.encode(), recipientPeerID)
if (encrypted == null) {
Log.e(TAG, "❌ Failed to encrypt file for $recipientPeerID")
return@launch
}
Log.d(TAG, "🔐 Encrypted file payload: ${encrypted.size} bytes")
-
+
// Create NOISE_ENCRYPTED packet (not FILE_TRANSFER!)
val packet = BitchatPacket(
version = 1u,
@@ -696,20 +826,28 @@ class BluetoothMeshService(private val context: Context) {
signature = null,
ttl = 7u
)
-
+
// Sign and send the encrypted packet
val signed = signPacketBeforeBroadcast(packet)
// Use a stable transferId based on the unencrypted file TLV payload for progress tracking
val transferId = sha256Hex(filePayload)
- connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
+ connectionManager.broadcastPacket(
+ RoutedPacket(
+ signed,
+ transferId = transferId
+ )
+ )
Log.d(TAG, "✅ Sent encrypted file to $recipientPeerID")
-
+
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to encrypt file for $recipientPeerID: ${e.message}", e)
}
} else {
// No session - initiate handshake but don't queue file
- Log.w(TAG, "⚠️ No Noise session with $recipientPeerID for file transfer, initiating handshake")
+ Log.w(
+ TAG,
+ "⚠️ No Noise session with $recipientPeerID for file transfer, initiating handshake"
+ )
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
}
}
@@ -728,21 +866,28 @@ class BluetoothMeshService(private val context: Context) {
val md = java.security.MessageDigest.getInstance("SHA-256")
md.update(bytes)
md.digest().joinToString("") { "%02x".format(it) }
- } catch (_: Exception) { bytes.size.toString(16) }
-
+ } catch (_: Exception) {
+ bytes.size.toString(16)
+ }
+
/**
- * Send private message - SIMPLIFIED iOS-compatible version
+ * Send private message - SIMPLIFIED iOS-compatible version
* Uses NoisePayloadType system exactly like iOS SimplifiedBluetoothService
*/
- fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) {
+ fun sendPrivateMessage(
+ content: String,
+ recipientPeerID: String,
+ recipientNickname: String,
+ messageID: String? = null
+ ) {
if (content.isEmpty() || recipientPeerID.isEmpty()) return
if (recipientNickname.isEmpty()) return
-
+
serviceScope.launch {
val finalMessageID = messageID ?: java.util.UUID.randomUUID().toString()
-
+
Log.d(TAG, "📨 Sending PM to $recipientPeerID: ${content.take(30)}...")
-
+
// Check if we have an established Noise session
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
try {
@@ -751,22 +896,23 @@ class BluetoothMeshService(private val context: Context) {
messageID = finalMessageID,
content = content
)
-
+
val tlvData = privateMessage.encode()
if (tlvData == null) {
Log.e(TAG, "Failed to encode private message with TLV")
return@launch
}
-
+
// Create message payload with NoisePayloadType prefix: [type byte] + [TLV data]
val messagePayload = com.bitchat.android.model.NoisePayload(
type = com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE,
data = tlvData
)
-
+
// Encrypt the payload
- val encrypted = encryptionService.encrypt(messagePayload.encode(), recipientPeerID)
-
+ val encrypted =
+ encryptionService.encrypt(messagePayload.encode(), recipientPeerID)
+
// Create NOISE_ENCRYPTED packet exactly like iOS
val packet = BitchatPacket(
version = 1u,
@@ -778,30 +924,36 @@ class BluetoothMeshService(private val context: Context) {
signature = null,
ttl = MAX_TTL
)
-
+
// Sign the packet before broadcasting
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
- Log.d(TAG, "📤 Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)")
-
+ Log.d(
+ TAG,
+ "📤 Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)"
+ )
+
// FIXED: Don't send didReceiveMessage for our own sent messages
// This was causing self-notifications - iOS doesn't do this
// The UI handles showing sent messages through its own message sending logic
-
+
} catch (e: Exception) {
- Log.e(TAG, "Failed to encrypt private message for $recipientPeerID: ${e.message}")
+ Log.e(
+ TAG,
+ "Failed to encrypt private message for $recipientPeerID: ${e.message}"
+ )
}
} else {
// Fire and forget - initiate handshake but don't queue exactly like iOS
Log.d(TAG, "🤝 No session with $recipientPeerID, initiating handshake")
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
-
+
// FIXED: Don't send didReceiveMessage for our own sent messages
// The UI will handle showing the message in the chat interface
}
}
}
-
+
/**
* Send read receipt for a received private message - NEW NoisePayloadType implementation
* Uses same encryption approach as iOS SimplifiedBluetoothService
@@ -809,28 +961,35 @@ class BluetoothMeshService(private val context: Context) {
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
serviceScope.launch {
Log.d(TAG, "📖 Sending read receipt for message $messageID to $recipientPeerID")
-
+
// Route geohash read receipts via MessageRouter instead of here
- val geo = runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance() }.getOrNull()
+ val geo =
+ runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance() }.getOrNull()
val isGeoAlias = try {
val map = com.bitchat.android.nostr.GeohashAliasRegistry.snapshot()
map.containsKey(recipientPeerID)
- } catch (_: Exception) { false }
+ } catch (_: Exception) {
+ false
+ }
if (isGeoAlias && geo != null) {
- geo.sendReadReceipt(com.bitchat.android.model.ReadReceipt(messageID), recipientPeerID)
+ geo.sendReadReceipt(
+ com.bitchat.android.model.ReadReceipt(messageID),
+ recipientPeerID
+ )
return@launch
}
-
+
try {
// Create read receipt payload using NoisePayloadType exactly like iOS
val readReceiptPayload = com.bitchat.android.model.NoisePayload(
type = com.bitchat.android.model.NoisePayloadType.READ_RECEIPT,
data = messageID.toByteArray(Charsets.UTF_8)
)
-
+
// Encrypt the payload
- val encrypted = encryptionService.encrypt(readReceiptPayload.encode(), recipientPeerID)
-
+ val encrypted =
+ encryptionService.encrypt(readReceiptPayload.encode(), recipientPeerID)
+
// Create NOISE_ENCRYPTED packet exactly like iOS
val packet = BitchatPacket(
version = 1u,
@@ -842,18 +1001,18 @@ class BluetoothMeshService(private val context: Context) {
signature = null,
ttl = 7u // Same TTL as iOS messageTTL
)
-
+
// Sign the packet before broadcasting
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
Log.d(TAG, "📤 Sent read receipt to $recipientPeerID for message $messageID")
-
+
} catch (e: Exception) {
Log.e(TAG, "Failed to send read receipt to $recipientPeerID: ${e.message}")
}
}
}
-
+
/**
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
*/
@@ -861,21 +1020,21 @@ class BluetoothMeshService(private val context: Context) {
Log.d(TAG, "Sending broadcast announce")
serviceScope.launch {
val nickname = delegate?.getNickname() ?: myPeerID
-
+
// Get the static public key for the announcement
val staticKey = encryptionService.getStaticPublicKey()
if (staticKey == null) {
Log.e(TAG, "No static public key available for announcement")
return@launch
}
-
+
// Get the signing public key for the announcement
val signingKey = encryptionService.getSigningPublicKey()
if (signingKey == null) {
Log.e(TAG, "No signing public key available for announcement")
return@launch
}
-
+
// Create iOS-compatible IdentityAnnouncement with TLV encoding
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
val tlvPayload = announcement.encode()
@@ -883,48 +1042,52 @@ class BluetoothMeshService(private val context: Context) {
Log.e(TAG, "Failed to encode announcement as TLV")
return@launch
}
-
+
val announcePacket = BitchatPacket(
type = MessageType.ANNOUNCE.value,
ttl = MAX_TTL,
senderID = myPeerID,
payload = tlvPayload
)
-
+
// Sign the packet using our signing key (exactly like iOS)
- val signedPacket = encryptionService.signData(announcePacket.toBinaryDataForSigning()!!)?.let { signature ->
- announcePacket.copy(signature = signature)
- } ?: announcePacket
-
+ val signedPacket = encryptionService.signData(announcePacket.toBinaryDataForSigning()!!)
+ ?.let { signature ->
+ announcePacket.copy(signature = signature)
+ } ?: announcePacket
+
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)")
// Track announce for sync
- try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
+ try {
+ gossipSyncManager.onPublicPacketSeen(signedPacket)
+ } catch (_: Exception) {
+ }
}
}
-
+
/**
* Send announcement to specific peer with TLV-encoded identity announcement - exactly like iOS
*/
fun sendAnnouncementToPeer(peerID: String) {
if (peerManager.hasAnnouncedToPeer(peerID)) return
-
+
val nickname = delegate?.getNickname() ?: myPeerID
-
+
// Get the static public key for the announcement
val staticKey = encryptionService.getStaticPublicKey()
if (staticKey == null) {
Log.e(TAG, "No static public key available for peer announcement")
return
}
-
+
// Get the signing public key for the announcement
val signingKey = encryptionService.getSigningPublicKey()
if (signingKey == null) {
Log.e(TAG, "No signing public key available for peer announcement")
return
}
-
+
// Create iOS-compatible IdentityAnnouncement with TLV encoding
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
val tlvPayload = announcement.encode()
@@ -932,25 +1095,32 @@ class BluetoothMeshService(private val context: Context) {
Log.e(TAG, "Failed to encode peer announcement as TLV")
return
}
-
+
val packet = BitchatPacket(
type = MessageType.ANNOUNCE.value,
ttl = MAX_TTL,
senderID = myPeerID,
payload = tlvPayload
)
-
+
// Sign the packet using our signing key (exactly like iOS)
- val signedPacket = encryptionService.signData(packet.toBinaryDataForSigning()!!)?.let { signature ->
- packet.copy(signature = signature)
- } ?: packet
-
+ val signedPacket =
+ encryptionService.signData(packet.toBinaryDataForSigning()!!)?.let { signature ->
+ packet.copy(signature = signature)
+ } ?: packet
+
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
peerManager.markPeerAsAnnouncedTo(peerID)
- Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)")
+ Log.d(
+ TAG,
+ "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)"
+ )
// Track announce for sync
- try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
+ try {
+ gossipSyncManager.onPublicPacketSeen(signedPacket)
+ } catch (_: Exception) {
+ }
}
/**
@@ -964,36 +1134,36 @@ class BluetoothMeshService(private val context: Context) {
senderID = myPeerID,
payload = nickname.toByteArray()
)
-
+
// Sign the packet before broadcasting
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
}
-
+
/**
* Get peer nicknames
*/
fun getPeerNicknames(): Map = peerManager.getAllPeerNicknames()
-
+
/**
- * Get peer RSSI values
+ * Get peer RSSI values
*/
fun getPeerRSSI(): Map = peerManager.getAllPeerRSSI()
-
+
/**
- * Check if we have an established Noise session with a peer
+ * Check if we have an established Noise session with a peer
*/
fun hasEstablishedSession(peerID: String): Boolean {
return encryptionService.hasEstablishedSession(peerID)
}
-
+
/**
* Get session state for a peer (for UI state display)
*/
fun getSessionState(peerID: String): com.bitchat.android.noise.NoiseSession.NoiseSessionState {
return encryptionService.getSessionState(peerID)
}
-
+
/**
* Initiate Noise handshake with a specific peer (public API)
*/
@@ -1001,7 +1171,7 @@ class BluetoothMeshService(private val context: Context) {
// Delegate to the existing implementation in the MessageHandler delegate
messageHandler.delegate?.initiateNoiseHandshake(peerID)
}
-
+
/**
* Get peer fingerprint for identity management
*/
@@ -1026,23 +1196,29 @@ class BluetoothMeshService(private val context: Context) {
signingPublicKey: ByteArray,
isVerified: Boolean
): Boolean {
- return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
+ return peerManager.updatePeerInfo(
+ peerID,
+ nickname,
+ noisePublicKey,
+ signingPublicKey,
+ isVerified
+ )
}
-
+
/**
* Get our identity fingerprint
*/
fun getIdentityFingerprint(): String {
return encryptionService.getIdentityFingerprint()
}
-
+
/**
* Check if encryption icon should be shown for a peer
*/
fun shouldShowEncryptionIcon(peerID: String): Boolean {
return encryptionService.hasEstablishedSession(peerID)
}
-
+
/**
* Get all peers with established encrypted sessions
*/
@@ -1051,21 +1227,21 @@ class BluetoothMeshService(private val context: Context) {
// This method is not critical for the session retention fix
return emptyList()
}
-
+
/**
* Get device address for a specific peer ID
*/
fun getDeviceAddressForPeer(peerID: String): String? {
return connectionManager.addressPeerMap.entries.find { it.value == peerID }?.key
}
-
+
/**
* Get all device addresses mapped to their peer IDs
*/
fun getDeviceAddressToPeerMapping(): Map {
return connectionManager.addressPeerMap.toMap()
}
-
+
/**
* Print device addresses for all connected peers
*/
@@ -1098,7 +1274,7 @@ class BluetoothMeshService(private val context: Context) {
appendLine(packetProcessor.getDebugInfo())
}
}
-
+
/**
* Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS
*/
@@ -1106,7 +1282,7 @@ class BluetoothMeshService(private val context: Context) {
val result = ByteArray(8) { 0 } // Initialize with zeros, exactly 8 bytes
var tempID = hexString
var index = 0
-
+
while (tempID.length >= 2 && index < 8) {
val hexByte = tempID.substring(0, 2)
val byte = hexByte.toIntOrNull(16)?.toByte()
@@ -1116,10 +1292,10 @@ class BluetoothMeshService(private val context: Context) {
tempID = tempID.substring(2)
index++
}
-
+
return result
}
-
+
/**
* Sign packet before broadcasting using our signing private key
*/
@@ -1128,14 +1304,20 @@ class BluetoothMeshService(private val context: Context) {
// Get the canonical packet data for signing (without signature)
val packetDataForSigning = packet.toBinaryDataForSigning()
if (packetDataForSigning == null) {
- Log.w(TAG, "Failed to encode packet type ${packet.type} for signing, sending unsigned")
+ Log.w(
+ TAG,
+ "Failed to encode packet type ${packet.type} for signing, sending unsigned"
+ )
return packet
}
-
+
// Sign the packet data using our signing key
val signature = encryptionService.signData(packetDataForSigning)
if (signature != null) {
- Log.d(TAG, "✅ Signed packet type ${packet.type} (signature ${signature.size} bytes)")
+ Log.d(
+ TAG,
+ "✅ Signed packet type ${packet.type} (signature ${signature.size} bytes)"
+ )
packet.copy(signature = signature)
} else {
Log.w(TAG, "Failed to sign packet type ${packet.type}, sending unsigned")
@@ -1146,9 +1328,9 @@ class BluetoothMeshService(private val context: Context) {
packet
}
}
-
+
// MARK: - Panic Mode Support
-
+
/**
* Clear all internal mesh service data (for panic mode)
*/
@@ -1166,7 +1348,7 @@ class BluetoothMeshService(private val context: Context) {
Log.e(TAG, "❌ Error clearing mesh service internal data: ${e.message}")
}
}
-
+
/**
* Clear all encryption and cryptographic data (for panic mode)
*/
@@ -1181,18 +1363,3 @@ class BluetoothMeshService(private val context: Context) {
}
}
}
-
-/**
- * Delegate interface for mesh service callbacks (maintains exact same interface)
- */
-interface BluetoothMeshDelegate {
- fun didReceiveMessage(message: BitchatMessage)
- fun didUpdatePeerList(peers: List)
- fun didReceiveChannelLeave(channel: String, fromPeer: String)
- fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
- fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
- fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
- fun getNickname(): String?
- fun isFavorite(peerID: String): Boolean
- // registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager
-}
diff --git a/app/src/main/java/com/bitchat/android/model/Misc.kt b/app/src/main/java/com/bitchat/android/model/Misc.kt
new file mode 100644
index 000000000..390263d02
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/model/Misc.kt
@@ -0,0 +1,7 @@
+package com.bitchat.android.model
+
+import android.util.Log
+
+fun logWarn(msg: String) {
+ Log.w("Bitchat", msg)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt
index bce3ef1c2..0c2faba65 100644
--- a/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt
+++ b/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt
@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.sp
import com.bitchat.android.nostr.NostrProofOfWork
import com.bitchat.android.nostr.PoWPreferenceManager
import com.bitchat.android.ui.debug.DebugSettingsSheet
+import com.bitchat.android.ui.theme.ThemePreference
/**
* About Sheet for bitchat app information
@@ -38,8 +39,8 @@ import com.bitchat.android.ui.debug.DebugSettingsSheet
@Composable
fun AboutSheet(
isPresented: Boolean,
- onDismiss: () -> Unit,
- onShowDebug: (() -> Unit)? = null,
+ onDismiss: () -> Unit, onShowDebug: (() -> Unit)? = null,
+ themePref: ThemePreference, onChangeTheme: (ThemePreference) -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
@@ -70,6 +71,7 @@ fun AboutSheet(
label = "topBarAlpha"
)
+
// Color scheme matching LocationChannelsSheet
val colorScheme = MaterialTheme.colorScheme
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
@@ -239,24 +241,23 @@ fun AboutSheet(
.padding(horizontal = 24.dp)
.padding(top = 24.dp, bottom = 8.dp)
)
- val themePref by com.bitchat.android.ui.theme.ThemePreferenceManager.themeFlow.collectAsState()
Row(
modifier = Modifier.padding(horizontal = 24.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
selected = themePref.isSystem,
- onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.System) },
+ onClick = { onChangeTheme(ThemePreference.System) },
label = { Text("system", fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = themePref.isLight,
- onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Light) },
+ onClick = { onChangeTheme(ThemePreference.Light) },
label = { Text("light", fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = themePref.isDark,
- onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Dark) },
+ onClick = { onChangeTheme(ThemePreference.Dark) },
label = { Text("dark", fontFamily = FontFamily.Monospace) }
)
}
diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
index 72a9e1e44..7c1daec34 100644
--- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
+++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
@@ -26,6 +26,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.zIndex
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.ui.media.FullScreenImageViewer
+import com.bitchat.android.ui.theme.ThemePreference
/**
* Main ChatScreen - REFACTORED to use component-based architecture
@@ -38,7 +39,9 @@ import com.bitchat.android.ui.media.FullScreenImageViewer
* - ChatUIUtils: Utility functions for formatting and colors
*/
@Composable
-fun ChatScreen(viewModel: ChatViewModel) {
+fun ChatScreen(
+ viewModel: ChatViewModel, themePref: ThemePreference, onChangeTheme: (ThemePreference) -> Unit
+) {
val colorScheme = MaterialTheme.colorScheme
val messages by viewModel.messages.observeAsState(emptyList())
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
@@ -104,7 +107,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
.background(colorScheme.background) // Extend background to fill entire screen including status bar
) {
val headerHeight = 42.dp
-
+
// Main content area that responds to keyboard/window insets
Column(
modifier = Modifier
@@ -130,26 +133,27 @@ fun ChatScreen(viewModel: ChatViewModel) {
onNicknameClick = { fullSenderName ->
// Single click - mention user in text input
val currentText = messageText.text
-
+
// Extract base nickname and hash suffix from full sender name
val (baseName, hashSuffix) = splitSuffix(fullSenderName)
-
+
// Check if we're in a geohash channel to include hash suffix
val selectedLocationChannel = viewModel.selectedLocationChannel.value
- val mentionText = if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location && hashSuffix.isNotEmpty()) {
- // In geohash chat - include the hash suffix from the full display name
- "@$baseName$hashSuffix"
- } else {
- // Regular chat - just the base nickname
- "@$baseName"
- }
-
+ val mentionText =
+ if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location && hashSuffix.isNotEmpty()) {
+ // In geohash chat - include the hash suffix from the full display name
+ "@$baseName$hashSuffix"
+ } else {
+ // Regular chat - just the base nickname
+ "@$baseName"
+ }
+
val newText = when {
currentText.isEmpty() -> "$mentionText "
currentText.endsWith(" ") -> "$currentText$mentionText "
else -> "$currentText $mentionText "
}
-
+
messageText = TextFieldValue(
text = newText,
selection = TextRange(newText.length)
@@ -173,42 +177,42 @@ fun ChatScreen(viewModel: ChatViewModel) {
}
)
// Input area - stays at bottom
- // Bridge file share from lower-level input to ViewModel
- androidx.compose.runtime.LaunchedEffect(Unit) {
- com.bitchat.android.ui.events.FileShareDispatcher.setHandler { peer, channel, path ->
- viewModel.sendFileNote(peer, channel, path)
- }
- }
-
- ChatInputSection(
- messageText = messageText,
- onMessageTextChange = { newText: TextFieldValue ->
- messageText = newText
- viewModel.updateCommandSuggestions(newText.text)
- viewModel.updateMentionSuggestions(newText.text)
- },
- onSend = {
- if (messageText.text.trim().isNotEmpty()) {
- viewModel.sendMessage(messageText.text.trim())
- messageText = TextFieldValue("")
- forceScrollToBottom = !forceScrollToBottom // Toggle to trigger scroll
+ // Bridge file share from lower-level input to ViewModel
+ androidx.compose.runtime.LaunchedEffect(Unit) {
+ com.bitchat.android.ui.events.FileShareDispatcher.setHandler { peer, channel, path ->
+ viewModel.sendFileNote(peer, channel, path)
+ }
}
- },
- onSendVoiceNote = { peer, onionOrChannel, path ->
- viewModel.sendVoiceNote(peer, onionOrChannel, path)
- },
- onSendImageNote = { peer, onionOrChannel, path ->
- viewModel.sendImageNote(peer, onionOrChannel, path)
- },
- onSendFileNote = { peer, onionOrChannel, path ->
- viewModel.sendFileNote(peer, onionOrChannel, path)
- },
-
- showCommandSuggestions = showCommandSuggestions,
- commandSuggestions = commandSuggestions,
- showMentionSuggestions = showMentionSuggestions,
- mentionSuggestions = mentionSuggestions,
- onCommandSuggestionClick = { suggestion: CommandSuggestion ->
+
+ ChatInputSection(
+ messageText = messageText,
+ onMessageTextChange = { newText: TextFieldValue ->
+ messageText = newText
+ viewModel.updateCommandSuggestions(newText.text)
+ viewModel.updateMentionSuggestions(newText.text)
+ },
+ onSend = {
+ if (messageText.text.trim().isNotEmpty()) {
+ viewModel.sendMessage(messageText.text.trim())
+ messageText = TextFieldValue("")
+ forceScrollToBottom = !forceScrollToBottom // Toggle to trigger scroll
+ }
+ },
+ onSendVoiceNote = { peer, onionOrChannel, path ->
+ viewModel.sendVoiceNote(peer, onionOrChannel, path)
+ },
+ onSendImageNote = { peer, onionOrChannel, path ->
+ viewModel.sendImageNote(peer, onionOrChannel, path)
+ },
+ onSendFileNote = { peer, onionOrChannel, path ->
+ viewModel.sendFileNote(peer, onionOrChannel, path)
+ },
+
+ showCommandSuggestions = showCommandSuggestions,
+ commandSuggestions = commandSuggestions,
+ showMentionSuggestions = showMentionSuggestions,
+ mentionSuggestions = mentionSuggestions,
+ onCommandSuggestionClick = { suggestion: CommandSuggestion ->
val commandText = viewModel.selectCommandSuggestion(suggestion)
messageText = TextFieldValue(
text = commandText,
@@ -349,12 +353,13 @@ fun ChatScreen(viewModel: ChatViewModel) {
showPasswordDialog = false
passwordInput = ""
},
+ onChangeTheme = onChangeTheme, themePref = themePref,
showAppInfo = showAppInfo,
onAppInfoDismiss = { viewModel.hideAppInfo() },
showLocationChannelsSheet = showLocationChannelsSheet,
onLocationChannelsSheetDismiss = { showLocationChannelsSheet = false },
showUserSheet = showUserSheet,
- onUserSheetDismiss = {
+ onUserSheetDismiss = {
showUserSheet = false
selectedMessageForSheet = null // Reset message when dismissing
},
@@ -422,6 +427,7 @@ private fun ChatInputSection(
}
}
}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ChatFloatingHeader(
@@ -479,6 +485,8 @@ private fun ChatDialogs(
onPasswordConfirm: () -> Unit,
onPasswordDismiss: () -> Unit,
showAppInfo: Boolean,
+ onChangeTheme: (ThemePreference) -> Unit,
+ themePref: ThemePreference,
onAppInfoDismiss: () -> Unit,
showLocationChannelsSheet: Boolean,
onLocationChannelsSheetDismiss: () -> Unit,
@@ -503,7 +511,8 @@ private fun ChatDialogs(
AboutSheet(
isPresented = showAppInfo,
onDismiss = onAppInfoDismiss,
- onShowDebug = { showDebugSheet = true }
+ onShowDebug = { showDebugSheet = true },
+ onChangeTheme = onChangeTheme, themePref = themePref
)
if (showDebugSheet) {
com.bitchat.android.ui.debug.DebugSettingsSheet(
@@ -512,7 +521,7 @@ private fun ChatDialogs(
meshService = viewModel.meshService
)
}
-
+
// Location channels sheet
if (showLocationChannelsSheet) {
LocationChannelsSheet(
@@ -521,7 +530,7 @@ private fun ChatDialogs(
viewModel = viewModel
)
}
-
+
// User action sheet
if (showUserSheet) {
ChatUserSheet(
diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
index 4224d9c1f..cdafdda3d 100644
--- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
+++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
@@ -2,23 +2,14 @@ package com.bitchat.android.ui
import android.app.Application
import android.util.Log
-import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.bitchat.android.mesh.BluetoothMeshDelegate
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.model.BitchatMessage
-import com.bitchat.android.model.BitchatMessageType
-import com.bitchat.android.protocol.BitchatPacket
-
-
import kotlinx.coroutines.launch
-import com.bitchat.android.util.NotificationIntervalManager
import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import java.util.Date
-import kotlin.random.Random
/**
* Refactored ChatViewModel - Main coordinator for bitchat functionality
@@ -26,9 +17,15 @@ import kotlin.random.Random
*/
class ChatViewModel(
application: Application,
- val meshService: BluetoothMeshService
-) : AndroidViewModel(application), BluetoothMeshDelegate {
- private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
+ val meshService: BluetoothMeshService, private val bmd: BluetoothMeshDelegate
+) : AndroidViewModel(application) {
+ private val debugManager by lazy {
+ try {
+ com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
+ } catch (e: Exception) {
+ null
+ }
+ }
companion object {
private const val TAG = "ChatViewModel"
@@ -46,97 +43,59 @@ class ChatViewModel(
mediaSendingManager.sendImageNote(toPeerIDOrNull, channelOrNull, filePath)
}
- // MARK: - State management
- private val state = ChatState()
-
- // Transfer progress tracking
- private val transferMessageMap = mutableMapOf()
- private val messageTransferMap = mutableMapOf()
-
- // Specialized managers
- private val dataManager = DataManager(application.applicationContext)
- private val messageManager = MessageManager(state)
- private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope)
-
- // Create Noise session delegate for clean dependency injection
- private val noiseSessionDelegate = object : NoiseSessionDelegate {
- override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID)
- override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID)
- override fun getMyPeerID(): String = meshService.myPeerID
- }
-
- val privateChatManager = PrivateChatManager(state, messageManager, dataManager, noiseSessionDelegate)
- private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager)
- private val notificationManager = NotificationManager(
- application.applicationContext,
- NotificationManagerCompat.from(application.applicationContext),
- NotificationIntervalManager()
- )
-
// Media file sending manager
- private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager, meshService)
-
+ private val mediaSendingManager =
+ MediaSendingManager(bmd.state, bmd.messageManager, bmd.channelManager, meshService)
+
// Delegate handler for mesh callbacks
- private val meshDelegateHandler = MeshDelegateHandler(
- state = state,
- messageManager = messageManager,
- channelManager = channelManager,
- privateChatManager = privateChatManager,
- notificationManager = notificationManager,
- coroutineScope = viewModelScope,
- onHapticFeedback = { ChatViewModelUtils.triggerHapticFeedback(application.applicationContext) },
- getMyPeerID = { meshService.myPeerID },
- getMeshService = { meshService }
- )
-
+
// New Geohash architecture ViewModel (replaces God object service usage in UI path)
val geohashViewModel = GeohashViewModel(
application = application,
- state = state,
- messageManager = messageManager,
- privateChatManager = privateChatManager,
- meshDelegateHandler = meshDelegateHandler,
- dataManager = dataManager,
- notificationManager = notificationManager
+ state = bmd.state,
+ messageManager = bmd.messageManager,
+ privateChatManager = bmd.privateChatManager,
+ meshDelegateHandler = bmd.meshDelegateHandler,
+ dataManager = bmd.dataManager,
+ notificationManager = bmd.notificationManager
)
-
-
// Expose state through LiveData (maintaining the same interface)
- val messages: LiveData> = state.messages
- val connectedPeers: LiveData> = state.connectedPeers
- val nickname: LiveData = state.nickname
- val isConnected: LiveData = state.isConnected
- val privateChats: LiveData