diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d37638f06..96bdcf40c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -48,6 +48,7 @@ android {
}
buildFeatures {
compose = true
+ buildConfig = true
}
packaging {
resources {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3cc22c28d..0e4cd04aa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,20 +8,24 @@
-
+
-
+
+
+
+
+
-
+
-
+
-
+
@@ -42,7 +46,6 @@
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.BitchatAndroid"
- android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTop">
@@ -50,5 +53,13 @@
+
+
+
+
diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt
index 2e428a285..ff3c344e1 100644
--- a/app/src/main/java/com/bitchat/android/MainActivity.kt
+++ b/app/src/main/java/com/bitchat/android/MainActivity.kt
@@ -1,10 +1,12 @@
package com.bitchat.android
+import android.content.ComponentName
import android.content.Intent
+import android.content.ServiceConnection
import android.os.Bundle
+import android.os.IBinder
import android.util.Log
import androidx.activity.ComponentActivity
-import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
@@ -15,18 +17,20 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
+import androidx.lifecycle.Lifecycle
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.repeatOnLifecycle
-import androidx.lifecycle.Lifecycle
-import com.bitchat.android.mesh.BluetoothMeshService
-import com.bitchat.android.onboarding.BluetoothCheckScreen
-import com.bitchat.android.onboarding.BluetoothStatus
-import com.bitchat.android.onboarding.BluetoothStatusManager
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
+import com.bitchat.android.mesh.ForegroundService
import com.bitchat.android.onboarding.BatteryOptimizationManager
import com.bitchat.android.onboarding.BatteryOptimizationScreen
import com.bitchat.android.onboarding.BatteryOptimizationStatus
+import com.bitchat.android.onboarding.BluetoothCheckScreen
+import com.bitchat.android.onboarding.BluetoothStatus
+import com.bitchat.android.onboarding.BluetoothStatusManager
import com.bitchat.android.onboarding.InitializationErrorScreen
import com.bitchat.android.onboarding.InitializingScreen
import com.bitchat.android.onboarding.LocationCheckScreen
@@ -43,32 +47,93 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
-
+
+ companion object {
+ private val TAG = MainActivity::class.simpleName
+ }
+
private lateinit var permissionManager: PermissionManager
private lateinit var onboardingCoordinator: OnboardingCoordinator
private lateinit var bluetoothStatusManager: BluetoothStatusManager
private lateinit var locationStatusManager: LocationStatusManager
private lateinit var batteryOptimizationManager: BatteryOptimizationManager
-
- // Core mesh service - managed at app level
- private lateinit var meshService: BluetoothMeshService
+
+ // Core mesh service - now managed by a foreground service
+ @Volatile private var foregroundService: ForegroundService? = null
+ @Volatile private var isServiceBound = false
+
private val mainViewModel: MainViewModel by viewModels()
- private val chatViewModel: ChatViewModel by viewModels {
- object : ViewModelProvider.Factory {
- override fun create(modelClass: Class): T {
- @Suppress("UNCHECKED_CAST")
- return ChatViewModel(application, meshService) as T
+ private val chatViewModel: ChatViewModel by viewModels {
+ viewModelFactory {
+ initializer {
+ ChatViewModel(application)
}
}
}
-
+
+ private val serviceConnection = object : ServiceConnection, ForegroundService.ServiceListener {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ val binder = service as? ForegroundService.LocalBinder
+ if (binder != null) {
+ foregroundService = binder.getService()
+ binder.setServiceListener(this)
+ isServiceBound = true
+ Log.d(TAG, "ForegroundService connected.")
+ initializeApp()
+ } else {
+ Log.e(TAG, "Failed to cast binder. Service might not be the expected type.")
+ finish() // Can't work without the service
+ }
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ // This is called for UNEXPECTED disconnection (e.g., service crashes)
+ Log.w(TAG, "ForegroundService unexpectedly disconnected.")
+ foregroundService = null
+ isServiceBound = false
+ finish()
+ }
+
+ override fun onServiceStopping() {
+ Log.w(TAG, "ForegroundService stopping")
+ finish()
+ }
+ }
+
+ /**
+ * Starts the [ForegroundService] if it's not already running and then binds to it.
+ * This ensures that the service is running as a foreground service, which is crucial
+ * for its continuous operation, especially for tasks like Bluetooth mesh networking.
+ * Binding to the service allows the Activity to interact with it, for example,
+ * to get a reference to the MeshService instance or to listen for service events.
+ *
+ * The service is started first using `startForegroundService` to guarantee it transitions
+ * to a foreground state. Then, `bindService` is called to establish a connection.
+ * The `BIND_AUTO_CREATE` flag ensures that the service is created if it's not already running,
+ * though in this flow, `startForegroundService` typically handles the creation.
+ */
+ private fun startAndBindService() {
+ // Always start the service first to ensure it's running as a foreground service.
+ val serviceIntent = Intent(this, ForegroundService::class.java)
+ if (!ForegroundService.isServiceRunning) {
+ Log.d(TAG, "Starting foreground service")
+ startForegroundService(serviceIntent)
+ } else {
+ Log.d(TAG, "Foreground service already running!")
+ }
+ // Bind to the service to get a reference to it.
+ if (!isServiceBound) {
+ Log.d(TAG, "Binding to foreground service")
+ bindService(serviceIntent, serviceConnection, BIND_AUTO_CREATE)
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
+
// Initialize permission management
permissionManager = PermissionManager(this)
- // Initialize core mesh service first
- meshService = BluetoothMeshService(this)
+
bluetoothStatusManager = BluetoothStatusManager(
activity = this,
context = this,
@@ -113,12 +178,27 @@ class MainActivity : ComponentActivity() {
}
}
}
-
+
+ // Listen for background requests from the ViewModel
+ chatViewModel.backgroundRequest.observe(this) { shouldBackground ->
+ if (shouldBackground == true) {
+ finish()
+ }
+ }
+
+ // Listen for shutdown requests from the ViewModel
+ chatViewModel.shutdownRequest.observe(this) { shouldShutdown ->
+ if (shouldShutdown == true) {
+ stopServiceAndExit()
+ }
+ }
+
// 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
@@ -222,26 +302,10 @@ class MainActivity : ComponentActivity() {
OnboardingState.INITIALIZING -> {
InitializingScreen()
+ startAndBindService()
}
-
- OnboardingState.COMPLETE -> {
- // Set up back navigation handling for the chat screen
- val backCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- // Let ChatViewModel handle navigation state
- val handled = chatViewModel.handleBackPressed()
- if (!handled) {
- // If ChatViewModel doesn't handle it, disable this callback
- // and let the system handle it (which will exit the app)
- this.isEnabled = false
- onBackPressedDispatcher.onBackPressed()
- this.isEnabled = true
- }
- }
- }
- // Add the callback - this will be automatically removed when the activity is destroyed
- onBackPressedDispatcher.addCallback(this, backCallback)
+ OnboardingState.COMPLETE -> {
ChatScreen(viewModel = chatViewModel)
}
@@ -265,45 +329,45 @@ class MainActivity : ComponentActivity() {
when (state) {
OnboardingState.COMPLETE -> {
// App is fully initialized, mesh service is running
- android.util.Log.d("MainActivity", "Onboarding completed - app ready")
+ android.util.Log.d(TAG, "Onboarding completed - app ready")
}
OnboardingState.ERROR -> {
- android.util.Log.e("MainActivity", "Onboarding error state reached")
+ android.util.Log.e(TAG, "Onboarding error state reached")
}
else -> {}
}
}
-
+
private fun checkOnboardingStatus() {
- Log.d("MainActivity", "Checking onboarding status")
-
+ Log.d(TAG, "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")
-
+ // Log.d(TAG, "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(TAG, "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
@@ -311,47 +375,47 @@ class MainActivity : ComponentActivity() {
}
BluetoothStatus.DISABLED -> {
// Show Bluetooth enable screen (should have permissions as existing user)
- Log.d("MainActivity", "Bluetooth disabled, showing enable screen")
+ Log.d(TAG, "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")
+ android.util.Log.e(TAG, "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")
-
+ Log.d(TAG, "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")
+ Log.d(TAG, "First time launch, showing permission explanation")
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
} else if (permissionManager.areAllPermissionsGranted()) {
- Log.d("MainActivity", "Existing user with permissions, initializing app")
+ Log.d(TAG, "Existing user with permissions, initializing app")
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
initializeApp()
} else {
- Log.d("MainActivity", "Existing user missing permissions, showing explanation")
+ Log.d(TAG, "Existing user missing permissions, showing explanation")
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
}
}
}
-
+
/**
* Handle Bluetooth enabled callback
*/
private fun handleBluetoothEnabled() {
- Log.d("MainActivity", "Bluetooth enabled by user")
+ Log.d(TAG, "Bluetooth enabled by user")
mainViewModel.updateBluetoothLoading(false)
mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED)
checkLocationAndProceed()
@@ -361,20 +425,20 @@ class MainActivity : ComponentActivity() {
* Check Location services status and proceed with onboarding flow
*/
private fun checkLocationAndProceed() {
- Log.d("MainActivity", "Checking location services status")
-
+ Log.d(TAG, "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(TAG, "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
@@ -382,13 +446,13 @@ class MainActivity : ComponentActivity() {
}
LocationStatus.DISABLED -> {
// Show location enable screen (should have permissions as existing user)
- Log.d("MainActivity", "Location services disabled, showing enable screen")
+ Log.d(TAG, "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")
+ Log.e(TAG, "Location services not available")
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
mainViewModel.updateLocationLoading(false)
}
@@ -399,7 +463,7 @@ class MainActivity : ComponentActivity() {
* Handle Location enabled callback
*/
private fun handleLocationEnabled() {
- Log.d("MainActivity", "Location services enabled by user")
+ Log.d(TAG, "Location services enabled by user")
mainViewModel.updateLocationLoading(false)
mainViewModel.updateLocationStatus(LocationStatus.ENABLED)
checkBatteryOptimizationAndProceed()
@@ -409,7 +473,7 @@ class MainActivity : ComponentActivity() {
* Handle Location disabled callback
*/
private fun handleLocationDisabled(message: String) {
- Log.w("MainActivity", "Location services disabled or failed: $message")
+ Log.w(TAG, "Location services disabled or failed: $message")
mainViewModel.updateLocationLoading(false)
mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
@@ -425,15 +489,15 @@ class MainActivity : ComponentActivity() {
}
}
}
-
+
/**
* Handle Bluetooth disabled callback
*/
private fun handleBluetoothDisabled(message: String) {
- Log.w("MainActivity", "Bluetooth disabled or failed: $message")
+ Log.w(TAG, "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
@@ -443,12 +507,12 @@ class MainActivity : ComponentActivity() {
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(TAG, "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(TAG, "Bluetooth enable requires permissions, showing permission explanation")
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
}
else -> {
@@ -457,10 +521,10 @@ class MainActivity : ComponentActivity() {
}
}
}
-
+
private fun handleOnboardingComplete() {
- Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app")
-
+ Log.d(TAG, "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()
@@ -469,58 +533,58 @@ 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(TAG, "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(TAG, "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.")
+ android.util.Log.d(TAG, "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(TAG, "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")
+ Log.e(TAG, "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")
-
+ android.util.Log.d(TAG, "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")
+ android.util.Log.d(TAG, "First-time launch, skipping battery optimization check - will check after permissions")
proceedWithPermissionCheck()
return
}
-
+
// For existing users, check battery optimization status
batteryOptimizationManager.logBatteryOptimizationStatus()
val currentBatteryOptimizationStatus = when {
@@ -529,7 +593,7 @@ 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
@@ -537,28 +601,28 @@ class MainActivity : ComponentActivity() {
}
BatteryOptimizationStatus.ENABLED -> {
// Show battery optimization disable screen
- android.util.Log.d("MainActivity", "Battery optimization enabled, showing disable screen")
+ Log.d(TAG, "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(TAG, "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")
+ android.util.Log.w(TAG, "Battery optimization disable failed: $message")
mainViewModel.updateBatteryOptimizationLoading(false)
val currentStatus = when {
!batteryOptimizationManager.isBatteryOptimizationSupported() -> BatteryOptimizationStatus.NOT_SUPPORTED
@@ -566,50 +630,49 @@ 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")
-
+ Log.d(TAG, "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")
-
+
+ Log.d(TAG, "Permissions verified, initializing chat system")
+
// Ensure all permissions are still granted (user might have revoked in settings)
if (!permissionManager.areAllPermissionsGranted()) {
val missing = permissionManager.getMissingPermissions()
- Log.w("MainActivity", "Permissions revoked during initialization: $missing")
+ Log.w(TAG, "Permissions revoked during initialization: $missing")
handleOnboardingFailed("Some permissions were revoked. Please grant all permissions to continue.")
return@launch
}
// Set up mesh service delegate and start services
- meshService.delegate = chatViewModel
- meshService.startServices()
-
- Log.d("MainActivity", "Mesh service started successfully")
-
+ foregroundService?.getMeshService()?.let { chatViewModel.initialize(it) }
+
+ Log.d(TAG, "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")
+ Log.d(TAG, "App initialization complete")
mainViewModel.updateOnboardingState(OnboardingState.COMPLETE)
} catch (e: Exception) {
- Log.e("MainActivity", "Failed to initialize app", e)
+ Log.e(TAG, "Failed to initialize app", e)
handleOnboardingFailed("Failed to initialize the app: ${e.message}")
}
}
}
-
+
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// Handle notification intents when app is already running
@@ -617,91 +680,97 @@ class MainActivity : ComponentActivity() {
handleNotificationIntent(intent)
}
}
-
+
override fun onResume() {
super.onResume()
+ chatViewModel.setAppBackgroundState(inBackground = false)
// 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.setAppBackgroundState(false)
-
// Check if Bluetooth was disabled while app was backgrounded
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
if (currentBluetoothStatus != BluetoothStatus.ENABLED) {
- Log.w("MainActivity", "Bluetooth disabled while app was backgrounded")
+ Log.w(TAG, "Bluetooth disabled while app was backgrounded")
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
mainViewModel.updateBluetoothLoading(false)
return
}
-
+
// Check if location services were disabled while app was backgrounded
val currentLocationStatus = locationStatusManager.checkLocationStatus()
if (currentLocationStatus != LocationStatus.ENABLED) {
- Log.w("MainActivity", "Location services disabled while app was backgrounded")
+ Log.w(TAG, "Location services disabled while app was backgrounded")
mainViewModel.updateLocationStatus(currentLocationStatus)
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
mainViewModel.updateLocationLoading(false)
}
+
+ startAndBindService()
}
}
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.setAppBackgroundState(true)
+ chatViewModel.setAppBackgroundState(inBackground = true)
+ // Only unbind if the service is actually bound
+ if (isServiceBound) {
+ unbindService(serviceConnection)
+ isServiceBound = false
+ Log.d(TAG, "Service unbound in onPause")
}
}
-
+
/**
* Handle intents from notification clicks - open specific private 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
)
-
+
if (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)
-
+
if (peerID != null) {
- Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification")
-
+ Log.d(TAG, "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)
}
}
}
+ /**
+ * Triggers the foreground service to stop itself. The service will then
+ * call onServiceStopping(), which will finish the activity.
+ */
+ private fun stopServiceAndExit() {
+ Log.d(TAG, "User requested shutdown. Stopping service and exiting.")
+ foregroundService?.shutdownService()
+ finish()
+ }
override fun onDestroy() {
super.onDestroy()
-
// Cleanup location status manager
try {
locationStatusManager.cleanup()
- Log.d("MainActivity", "Location status manager cleaned up successfully")
+ Log.d(TAG, "Location status manager cleaned up successfully")
} catch (e: Exception) {
- Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}")
+ Log.w(TAG, "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()
- Log.d("MainActivity", "Mesh services stopped successfully")
- } catch (e: Exception) {
- Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}")
- }
+ }
+
+ @Deprecated("Deprecated")
+ override fun onBackPressed() {
+ val handled = chatViewModel.handleBackPressed()
+ if (!handled) {
+ super.onBackPressed()
}
}
}
diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshServiceDelegate.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshServiceDelegate.kt
new file mode 100644
index 000000000..ddc503acf
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshServiceDelegate.kt
@@ -0,0 +1,15 @@
+package com.bitchat.android.mesh
+
+import com.bitchat.android.model.BitchatMessage
+
+/**
+ * A dedicated delegate for providing real-time state updates from the
+ * BluetoothMeshService to the ForegroundService notification.
+ */
+interface MeshServiceStateDelegate {
+ fun onMeshStateUpdated(
+ peerCount: Int,
+ unreadCount: Int,
+ recentMessages: List
+ )
+}
diff --git a/app/src/main/java/com/bitchat/android/mesh/ForegroundService.kt b/app/src/main/java/com/bitchat/android/mesh/ForegroundService.kt
new file mode 100644
index 000000000..62db01618
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/mesh/ForegroundService.kt
@@ -0,0 +1,313 @@
+package com.bitchat.android.mesh
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Configuration
+import android.os.Binder
+import android.os.IBinder
+import android.util.Log
+import androidx.compose.ui.graphics.toArgb
+import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
+import com.bitchat.android.R
+import com.bitchat.android.model.BitchatMessage
+import com.bitchat.android.model.DeliveryAck
+import com.bitchat.android.model.ReadReceipt
+import com.bitchat.android.ui.theme.DarkColorScheme
+import com.bitchat.android.ui.theme.LightColorScheme
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.TimeUnit
+import kotlin.random.Random
+
+/**
+ * A foreground service that provides a standard, live-updating Android notification
+ * with peer and message counts.
+ */
+class ForegroundService : Service(), BluetoothMeshDelegate {
+
+ private val binder = LocalBinder()
+ private var meshService: BluetoothMeshService? = null
+ private lateinit var notificationManager: NotificationManager
+ private var serviceListener: ServiceListener? = null
+
+ // --- Live State for Notification UI ---
+ private var activePeers = listOf()
+ private var unreadMessageCount = 0
+ private val knownPeerIds = HashSet() // Used to detect new peers
+
+ // Scheduler for periodic UI refreshes
+ private lateinit var uiUpdateScheduler: ScheduledExecutorService
+
+ companion object {
+ private const val TAG = "MeshForegroundService"
+ private const val NOTIFICATION_ID = 1
+ private const val FOREGROUND_CHANNEL_ID = "com.bitchat.android.FOREGROUND_SERVICE"
+ private const val MOCK_PEERS_ENABLED = false
+
+ const val ACTION_RESET_UNREAD_COUNT = "com.bitchat.android.ACTION_RESET_UNREAD_COUNT"
+ const val ACTION_SHUTDOWN = "com.bitchat.android.ACTION_SHUTDOWN"
+
+ @Volatile
+ var isServiceRunning = false
+ private set
+ }
+
+ // --- Service Lifecycle & Setup ---
+
+ override fun onCreate() {
+ super.onCreate()
+ Log.d(TAG, "Foreground Service onCreate")
+ isServiceRunning = true
+ notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+
+ if (meshService == null) {
+ meshService = BluetoothMeshService(this).apply {
+ delegate = this@ForegroundService
+ }
+ }
+
+ val intentFilter = IntentFilter().apply {
+ addAction(ACTION_SHUTDOWN)
+ }
+ ContextCompat.registerReceiver(this, notificationActionReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ // Reset unread count if the user tapped the notification to open the app
+ if (intent?.action == ACTION_RESET_UNREAD_COUNT) {
+ unreadMessageCount = 0
+ }
+ createNotificationChannel()
+ startForeground(NOTIFICATION_ID, buildNotification(false))
+ startUiUpdater()
+ meshService?.startServices()
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ isServiceRunning = false
+ uiUpdateScheduler.shutdown()
+ unregisterReceiver(notificationActionReceiver)
+ meshService?.stopServices()
+ meshService = null
+ }
+
+ override fun onBind(intent: Intent): IBinder = binder
+
+ fun getMeshService(): BluetoothMeshService? = meshService
+
+ // --- BluetoothMeshDelegate Implementation ---
+
+ override fun didReceiveMessage(message: BitchatMessage) {
+ Log.d(TAG, "didReceiveMessage: '${message.content}' from ${message.sender}")
+ unreadMessageCount++
+ updateNotification(false)
+ }
+
+ override fun didUpdatePeerList(peers: List) {
+ updateNotification(false)
+ }
+
+ override fun didConnectToPeer(peerID: String) {
+ if (knownPeerIds.add(peerID)) {
+ Log.i(TAG, "New peer connected: $peerID. Triggering alert.")
+ updateNotification(true)
+ } else {
+ updateNotification(false)
+ }
+ }
+
+ override fun didDisconnectFromPeer(peerID: String) {
+ knownPeerIds.remove(peerID)
+ updateNotification(false)
+ }
+
+ override fun didReceiveDeliveryAck(ack: DeliveryAck) {}
+ override fun didReceiveReadReceipt(receipt: ReadReceipt) {}
+ override fun didReceiveChannelLeave(channel: String, fromPeer: String) {}
+ override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? = null
+ override fun getNickname(): String? = "bitchat_user"
+ override fun isFavorite(peerID: String): Boolean = false
+
+ // --- Notification Building & Logic ---
+
+ /**
+ * Creates a list of mock peers for debugging purposes.
+ * Proximity is randomized on each call to simulate changing conditions.
+ */
+ private fun getMockPeers(): List {
+ val allMockPeers = listOf(
+ PeerInfo(id = "mock_1", nickname = "debugger_dan", proximity = Random.nextInt(0, 5)),
+ PeerInfo(id = "mock_2", nickname = "test_tanya", proximity = Random.nextInt(0, 5)),
+ PeerInfo(id = "mock_3", nickname = "fake_fred", proximity = Random.nextInt(0, 5)),
+ PeerInfo(id = "mock_4", nickname = "staging_sue", proximity = Random.nextInt(0, 5)),
+ PeerInfo(id = "mock_5", nickname = "dev_dave", proximity = Random.nextInt(0, 5))
+ )
+
+ // Determine a random number of users to show (between 3 and 5)
+ val numToShow = Random.nextInt(3, 6) // Generates a number from 3 to 5
+
+ // Shuffle the list and take a random number of peers
+ return allMockPeers.shuffled().take(numToShow).sortedByDescending { it.proximity }
+ }
+
+
+ private fun updateNotification(alert: Boolean) {
+ // When MOCK_PEERS_ENABLED, override real data with a mock user list.
+ // This allows for easy UI testing without requiring physical peer devices.
+ if (MOCK_PEERS_ENABLED) {
+ activePeers = getMockPeers()
+ // Set a mock message count for a more realistic debug notification.
+ unreadMessageCount = 7
+ } else {
+ val nicknames = meshService?.getPeerNicknames() ?: emptyMap()
+ val rssiValues = meshService?.getPeerRSSI() ?: emptyMap()
+
+ activePeers = nicknames.map { (peerId, nickname) ->
+ val rssi = rssiValues[peerId] ?: -100
+ PeerInfo(id = peerId, nickname = nickname, proximity = getProximityFromRssi(rssi))
+ }.sortedByDescending { it.proximity }
+ }
+ notificationManager.notify(NOTIFICATION_ID, buildNotification(alert))
+ }
+
+
+ /**
+ * Builds a standard, live-updating Android notification.
+ * Collapsed: Shows peer and unread message counts.
+ * Expanded: Shows a list of nearby peers and their proximity.
+ */
+ private fun buildNotification(alert: Boolean): Notification {
+ val isDarkTheme = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
+ val colors = if (isDarkTheme) DarkColorScheme else LightColorScheme
+
+ val peerCount = activePeers.size
+ val contentText = getString(R.string.notification_scanning)
+ val contentTitle = resources.getQuantityString(R.plurals.peers_nearby, peerCount, peerCount)
+ val summaryText = resources.getQuantityString(R.plurals.unread_messages, unreadMessageCount, unreadMessageCount)
+
+ // Expanded view style using InboxStyle
+ val inboxStyle = NotificationCompat.InboxStyle()
+ .setBigContentTitle(contentTitle)
+ .setSummaryText(summaryText)
+
+ // Add each peer to the expanded view
+ if (activePeers.isNotEmpty()) {
+ activePeers.forEach { peer ->
+ val proximityBars = "◼".repeat(peer.proximity) + "◻".repeat(4 - peer.proximity)
+ inboxStyle.addLine("${peer.nickname} $proximityBars")
+ }
+ } else {
+ inboxStyle.addLine(contentText)
+ }
+
+ val builder = NotificationCompat.Builder(this, FOREGROUND_CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setColor(colors.primary.toArgb())
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setNumber(unreadMessageCount)
+ .setStyle(inboxStyle)
+ .setContentIntent(createMainPendingIntent())
+ .setOngoing(true)
+ .addAction(
+ 0,
+ getString(
+ R.string.notification_action_shutdown),
+ createActionPendingIntent(ACTION_SHUTDOWN))
+
+ if (alert) {
+ builder.setOnlyAlertOnce(false)
+ builder.setDefaults(Notification.DEFAULT_ALL)
+ } else {
+ builder.setOnlyAlertOnce(true)
+ }
+
+ return builder.build()
+ }
+
+ // --- Helper Functions ---
+
+ private fun getProximityFromRssi(rssi: Int): Int {
+ return when {
+ rssi > -60 -> 4 // Excellent
+ rssi > -70 -> 3 // Good
+ rssi > -80 -> 2 // Fair
+ rssi > -95 -> 1 // Weak
+ else -> 0 // Very weak
+ }
+ }
+
+ private fun startUiUpdater() {
+ if (::uiUpdateScheduler.isInitialized && !uiUpdateScheduler.isShutdown) return
+ uiUpdateScheduler = Executors.newSingleThreadScheduledExecutor()
+ uiUpdateScheduler.scheduleWithFixedDelay({
+ updateNotification(false)
+ }, 0, 5000L, TimeUnit.MILLISECONDS) // Update every 5 seconds
+ }
+
+ // --- Boilerplate ---
+
+ inner class LocalBinder : Binder() {
+ fun getService(): ForegroundService = this@ForegroundService
+ fun setServiceListener(listener: ServiceListener?) {
+ this@ForegroundService.serviceListener = listener
+ }
+ }
+
+ interface ServiceListener {
+ fun onServiceStopping()
+ }
+
+ private val notificationActionReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ when (intent?.action) {
+ ACTION_SHUTDOWN -> shutdownService()
+ }
+ }
+ }
+
+ internal fun shutdownService() {
+ Log.i(TAG, "Shutdown action triggered. Stopping service.")
+ serviceListener?.onServiceStopping()
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ }
+
+
+
+ private fun createNotificationChannel() {
+ val channelName = getString(R.string.notification_channel_name)
+ val channelDescription = getString(R.string.notification_channel_description)
+ val serviceChannel = NotificationChannel(
+ FOREGROUND_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_DEFAULT
+ ).apply {
+ description = channelDescription
+ setShowBadge(false)
+ enableVibration(false)
+ setSound(null, null)
+ }
+ notificationManager.createNotificationChannel(serviceChannel)
+ }
+
+ private fun createMainPendingIntent(): PendingIntent {
+ val intent = packageManager.getLaunchIntentForPackage(packageName)
+ // Add a way to reset unread count when user opens the app
+ intent?.action = ACTION_RESET_UNREAD_COUNT
+ return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ }
+
+ private fun createActionPendingIntent(action: String): PendingIntent {
+ val intent = Intent(action).apply { `package` = packageName }
+ return PendingIntent.getBroadcast(this, action.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ }
+}
diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerInfo.kt b/app/src/main/java/com/bitchat/android/mesh/PeerInfo.kt
new file mode 100644
index 000000000..8a4bd5def
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/mesh/PeerInfo.kt
@@ -0,0 +1,4 @@
+package com.bitchat.android.mesh
+
+// Data class to hold combined peer information for the UI
+data class PeerInfo(val id: String, val nickname: String, val proximity: Int)
\ No newline at end of file
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 1b0ceaa34..0496d0236 100644
--- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
+++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
@@ -1,5 +1,7 @@
package com.bitchat.android.ui
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
@@ -11,8 +13,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
/**
@@ -44,7 +46,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
val showMentionSuggestions by viewModel.showMentionSuggestions.observeAsState(false)
val mentionSuggestions by viewModel.mentionSuggestions.observeAsState(emptyList())
val showAppInfo by viewModel.showAppInfo.observeAsState(false)
-
+ val showExitDialog by viewModel.showExitDialog.observeAsState(false)
var messageText by remember { mutableStateOf(TextFieldValue("")) }
var showPasswordPrompt by remember { mutableStateOf(false) }
var showPasswordDialog by remember { mutableStateOf(false) }
@@ -199,6 +201,14 @@ fun ChatScreen(viewModel: ChatViewModel) {
showAppInfo = showAppInfo,
onAppInfoDismiss = { viewModel.hideAppInfo() }
)
+
+ // Exit confirmation dialog
+ ExitConfirmationDialog(
+ show = showExitDialog,
+ onDismiss = { viewModel.dismissExitConfirmation() },
+ onConfirmExit = { viewModel.requestShutdown() },
+ onConfirmBackground = { viewModel.requestBackground() }
+ )
}
@Composable
diff --git a/app/src/main/java/com/bitchat/android/ui/ChatState.kt b/app/src/main/java/com/bitchat/android/ui/ChatState.kt
index bbc55dfe8..9fd8cac2b 100644
--- a/app/src/main/java/com/bitchat/android/ui/ChatState.kt
+++ b/app/src/main/java/com/bitchat/android/ui/ChatState.kt
@@ -5,6 +5,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import com.bitchat.android.model.BitchatMessage
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
/**
* Centralized state definitions and data classes for the chat system
@@ -109,7 +111,19 @@ class ChatState {
// Navigation state
private val _showAppInfo = MutableLiveData(false)
val showAppInfo: LiveData = _showAppInfo
-
+
+ // New LiveData to control the exit confirmation dialog
+ private val _showExitDialog = MutableLiveData(false)
+ val showExitDialog: LiveData = _showExitDialog
+
+ // New LiveData for a shutdown request to the Activity
+ private val _shutdownRequest = MutableLiveData(false)
+ val shutdownRequest = _shutdownRequest
+
+ // New LiveData for a background request to the Activity
+ private val _backgroundRequest = MutableLiveData(false)
+ val backgroundRequest = _backgroundRequest
+
// Unread state computed properties
val hasUnreadChannels: MediatorLiveData = MediatorLiveData()
val hasUnreadPrivateMessages: MediatorLiveData = MediatorLiveData()
@@ -148,7 +162,9 @@ class ChatState {
fun getPeerSessionStatesValue() = _peerSessionStates.value ?: emptyMap()
fun getPeerFingerprintsValue() = _peerFingerprints.value ?: emptyMap()
fun getShowAppInfoValue() = _showAppInfo.value ?: false
-
+ fun getShowExitDialogValue() = _showExitDialog.value ?: false
+ fun getShutdownRequestValue() = _shutdownRequest.value ?: false
+
// Setters for state updates
fun setMessages(messages: List) {
_messages.value = messages
@@ -260,4 +276,16 @@ class ChatState {
_showAppInfo.value = show
}
+ fun setShowExitDialog(show: Boolean) {
+ _showExitDialog.value = show
+ }
+
+ fun setShutdownRequest() {
+ _shutdownRequest.value = true
+ }
+
+ fun setBackgroundRequest() {
+ _backgroundRequest.value = true
+ }
+
}
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 8b941bbb5..eb533d974 100644
--- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
+++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
@@ -1,7 +1,6 @@
package com.bitchat.android.ui
import android.app.Application
-import android.content.Context
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
@@ -11,9 +10,9 @@ import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryAck
import com.bitchat.android.model.ReadReceipt
-import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
-import java.util.*
+import kotlinx.coroutines.launch
+import java.util.Date
import kotlin.random.Random
/**
@@ -21,14 +20,16 @@ import kotlin.random.Random
* Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility
*/
class ChatViewModel(
- application: Application,
- val meshService: BluetoothMeshService
-) : AndroidViewModel(application), BluetoothMeshDelegate {
+ application: Application
+) : AndroidViewModel(application) {
companion object {
private const val TAG = "ChatViewModel"
}
+ lateinit var meshService: BluetoothMeshService
+ private set
+
// State management
private val state = ChatState()
@@ -49,20 +50,7 @@ class ChatViewModel(
val privateChatManager = PrivateChatManager(state, messageManager, dataManager, noiseSessionDelegate)
private val commandProcessor = CommandProcessor(state, messageManager, channelManager, privateChatManager)
private val notificationManager = NotificationManager(application.applicationContext)
-
- // 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 }
- )
-
+
// Expose state through LiveData (maintaining the same interface)
val messages: LiveData> = state.messages
val connectedPeers: LiveData> = state.connectedPeers
@@ -91,12 +79,35 @@ class ChatViewModel(
val peerNicknames: LiveData