diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 1ed97a92..44318781 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -1,27 +1,35 @@ package com.bitchat.android -import android.Manifest import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.viewModels -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.ViewModelProvider import androidx.activity.OnBackPressedCallback -import androidx.activity.addCallback +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.Lifecycle import com.bitchat.android.mesh.BluetoothMeshService -import com.bitchat.android.onboarding.* +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 +import com.bitchat.android.onboarding.LocationStatus +import com.bitchat.android.onboarding.LocationStatusManager +import com.bitchat.android.onboarding.OnboardingCoordinator +import com.bitchat.android.onboarding.OnboardingState +import com.bitchat.android.onboarding.PermissionExplanationScreen +import com.bitchat.android.onboarding.PermissionManager import com.bitchat.android.ui.ChatScreen import com.bitchat.android.ui.ChatViewModel import com.bitchat.android.ui.theme.BitchatTheme @@ -37,6 +45,7 @@ class MainActivity : ComponentActivity() { // Core mesh service - managed at app level private lateinit var meshService: BluetoothMeshService + private val mainViewModel: MainViewModel by viewModels() private val chatViewModel: ChatViewModel by viewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -46,25 +55,6 @@ class MainActivity : ComponentActivity() { } } - // UI state for onboarding flow - private var onboardingState by mutableStateOf(OnboardingState.CHECKING) - private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED) - private var locationStatus by mutableStateOf(LocationStatus.ENABLED) - private var errorMessage by mutableStateOf("") - private var isBluetoothLoading by mutableStateOf(false) - private var isLocationLoading by mutableStateOf(false) - - enum class OnboardingState { - CHECKING, - BLUETOOTH_CHECK, - LOCATION_CHECK, - PERMISSION_EXPLANATION, - PERMISSION_REQUESTING, - INITIALIZING, - COMPLETE, - ERROR - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -103,12 +93,31 @@ class MainActivity : ComponentActivity() { } } - // Start the onboarding process - checkOnboardingStatus() + // Collect state changes in a lifecycle-aware manner + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + mainViewModel.onboardingState.collect { state -> + handleOnboardingStateChange(state) + } + } + } + + // 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() { + val onboardingState by mainViewModel.onboardingState.collectAsState() + val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState() + val locationStatus by mainViewModel.locationStatus.collectAsState() + val errorMessage by mainViewModel.errorMessage.collectAsState() + val isBluetoothLoading by mainViewModel.isBluetoothLoading.collectAsState() + val isLocationLoading by mainViewModel.isLocationLoading.collectAsState() + when (onboardingState) { OnboardingState.CHECKING -> { InitializingScreen() @@ -118,7 +127,7 @@ class MainActivity : ComponentActivity() { BluetoothCheckScreen( status = bluetoothStatus, onEnableBluetooth = { - isBluetoothLoading = true + mainViewModel.updateBluetoothLoading(true) bluetoothStatusManager.requestEnableBluetooth() }, onRetry = { @@ -132,7 +141,7 @@ class MainActivity : ComponentActivity() { LocationCheckScreen( status = locationStatus, onEnableLocation = { - isLocationLoading = true + mainViewModel.updateLocationLoading(true) locationStatusManager.requestEnableLocation() }, onRetry = { @@ -146,7 +155,7 @@ class MainActivity : ComponentActivity() { PermissionExplanationScreen( permissionCategories = permissionManager.getCategorizedPermissions(), onContinue = { - onboardingState = OnboardingState.PERMISSION_REQUESTING + mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_REQUESTING) onboardingCoordinator.requestPermissions() } ) @@ -186,7 +195,7 @@ class MainActivity : ComponentActivity() { InitializationErrorScreen( errorMessage = errorMessage, onRetry = { - onboardingState = OnboardingState.CHECKING + mainViewModel.updateOnboardingState(OnboardingState.CHECKING) checkOnboardingStatus() }, onOpenSettings = { @@ -197,6 +206,20 @@ 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") + } + OnboardingState.ERROR -> { + android.util.Log.e("MainActivity", "Onboarding error state reached") + } + else -> {} + } + } + private fun checkOnboardingStatus() { android.util.Log.d("MainActivity", "Checking onboarding status") @@ -225,9 +248,9 @@ class MainActivity : ComponentActivity() { // For existing users, check Bluetooth status first bluetoothStatusManager.logBluetoothStatus() - bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() + mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus()) - when (bluetoothStatus) { + when (mainViewModel.bluetoothStatus.value) { BluetoothStatus.ENABLED -> { // Bluetooth is enabled, check location services next checkLocationAndProceed() @@ -235,14 +258,14 @@ class MainActivity : ComponentActivity() { BluetoothStatus.DISABLED -> { // Show Bluetooth enable screen (should have permissions as existing user) android.util.Log.d("MainActivity", "Bluetooth disabled, showing enable screen") - onboardingState = OnboardingState.BLUETOOTH_CHECK - isBluetoothLoading = false + mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) + mainViewModel.updateBluetoothLoading(false) } BluetoothStatus.NOT_SUPPORTED -> { // Device doesn't support Bluetooth android.util.Log.e("MainActivity", "Bluetooth not supported") - onboardingState = OnboardingState.BLUETOOTH_CHECK - isBluetoothLoading = false + mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) + mainViewModel.updateBluetoothLoading(false) } } } @@ -258,14 +281,14 @@ class MainActivity : ComponentActivity() { if (permissionManager.isFirstTimeLaunch()) { android.util.Log.d("MainActivity", "First time launch, showing permission explanation") - onboardingState = OnboardingState.PERMISSION_EXPLANATION + mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION) } else if (permissionManager.areAllPermissionsGranted()) { android.util.Log.d("MainActivity", "Existing user with permissions, initializing app") - onboardingState = OnboardingState.INITIALIZING + mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING) initializeApp() } else { android.util.Log.d("MainActivity", "Existing user missing permissions, showing explanation") - onboardingState = OnboardingState.PERMISSION_EXPLANATION + mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION) } } } @@ -275,8 +298,8 @@ class MainActivity : ComponentActivity() { */ private fun handleBluetoothEnabled() { android.util.Log.d("MainActivity", "Bluetooth enabled by user") - isBluetoothLoading = false - bluetoothStatus = BluetoothStatus.ENABLED + mainViewModel.updateBluetoothLoading(false) + mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED) checkLocationAndProceed() } @@ -296,9 +319,9 @@ class MainActivity : ComponentActivity() { // For existing users, check location status locationStatusManager.logLocationStatus() - locationStatus = locationStatusManager.checkLocationStatus() + mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus()) - when (locationStatus) { + when (mainViewModel.locationStatus.value) { LocationStatus.ENABLED -> { // Location services enabled, proceed with permission/onboarding check proceedWithPermissionCheck() @@ -306,14 +329,14 @@ class MainActivity : ComponentActivity() { LocationStatus.DISABLED -> { // Show location enable screen (should have permissions as existing user) android.util.Log.d("MainActivity", "Location services disabled, showing enable screen") - onboardingState = OnboardingState.LOCATION_CHECK - isLocationLoading = false + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) + mainViewModel.updateLocationLoading(false) } LocationStatus.NOT_AVAILABLE -> { // Device doesn't support location services (very unusual) android.util.Log.e("MainActivity", "Location services not available") - onboardingState = OnboardingState.LOCATION_CHECK - isLocationLoading = false + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) + mainViewModel.updateLocationLoading(false) } } } @@ -323,8 +346,8 @@ class MainActivity : ComponentActivity() { */ private fun handleLocationEnabled() { android.util.Log.d("MainActivity", "Location services enabled by user") - isLocationLoading = false - locationStatus = LocationStatus.ENABLED + mainViewModel.updateLocationLoading(false) + mainViewModel.updateLocationStatus(LocationStatus.ENABLED) proceedWithPermissionCheck() } @@ -333,18 +356,18 @@ class MainActivity : ComponentActivity() { */ private fun handleLocationDisabled(message: String) { android.util.Log.w("MainActivity", "Location services disabled or failed: $message") - isLocationLoading = false - locationStatus = locationStatusManager.checkLocationStatus() + mainViewModel.updateLocationLoading(false) + mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus()) when { - locationStatus == LocationStatus.NOT_AVAILABLE -> { + mainViewModel.locationStatus.value == LocationStatus.NOT_AVAILABLE -> { // Show permanent error for devices without location services - errorMessage = message - onboardingState = OnboardingState.ERROR + mainViewModel.updateErrorMessage(message) + mainViewModel.updateOnboardingState(OnboardingState.ERROR) } else -> { // Stay on location check screen for retry - onboardingState = OnboardingState.LOCATION_CHECK + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) } } } @@ -354,14 +377,14 @@ class MainActivity : ComponentActivity() { */ private fun handleBluetoothDisabled(message: String) { android.util.Log.w("MainActivity", "Bluetooth disabled or failed: $message") - isBluetoothLoading = false - bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() + mainViewModel.updateBluetoothLoading(false) + mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus()) when { - bluetoothStatus == BluetoothStatus.NOT_SUPPORTED -> { + mainViewModel.bluetoothStatus.value == BluetoothStatus.NOT_SUPPORTED -> { // Show permanent error for unsupported devices - errorMessage = message - onboardingState = OnboardingState.ERROR + mainViewModel.updateErrorMessage(message) + mainViewModel.updateOnboardingState(OnboardingState.ERROR) } message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> { // During first-time onboarding, if Bluetooth enable fails due to permissions, @@ -372,11 +395,11 @@ class MainActivity : ComponentActivity() { message.contains("Permission") -> { // For existing users, redirect to permission explanation to grant missing permissions android.util.Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation") - onboardingState = OnboardingState.PERMISSION_EXPLANATION + mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION) } else -> { // Stay on Bluetooth check screen for retry - onboardingState = OnboardingState.BLUETOOTH_CHECK + mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) } } } @@ -392,21 +415,21 @@ class MainActivity : ComponentActivity() { currentBluetoothStatus != BluetoothStatus.ENABLED -> { // Bluetooth still disabled, but now we have permissions to enable it android.util.Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.") - bluetoothStatus = currentBluetoothStatus - onboardingState = OnboardingState.BLUETOOTH_CHECK - isBluetoothLoading = false + 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 android.util.Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.") - locationStatus = currentLocationStatus - onboardingState = OnboardingState.LOCATION_CHECK - isLocationLoading = false + mainViewModel.updateLocationStatus(currentLocationStatus) + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) + mainViewModel.updateLocationLoading(false) } else -> { // Both are enabled, proceed to app initialization android.util.Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization") - onboardingState = OnboardingState.INITIALIZING + mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING) initializeApp() } } @@ -414,8 +437,8 @@ class MainActivity : ComponentActivity() { private fun handleOnboardingFailed(message: String) { android.util.Log.e("MainActivity", "Onboarding failed: $message") - errorMessage = message - onboardingState = OnboardingState.ERROR + mainViewModel.updateErrorMessage(message) + mainViewModel.updateOnboardingState(OnboardingState.ERROR) } private fun initializeApp() { @@ -450,7 +473,7 @@ class MainActivity : ComponentActivity() { delay(500) android.util.Log.d("MainActivity", "App initialization complete") - onboardingState = OnboardingState.COMPLETE + mainViewModel.updateOnboardingState(OnboardingState.COMPLETE) } catch (e: Exception) { android.util.Log.e("MainActivity", "Failed to initialize app", e) @@ -462,7 +485,7 @@ class MainActivity : ComponentActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // Handle notification intents when app is already running - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { handleNotificationIntent(intent) } } @@ -470,7 +493,7 @@ class MainActivity : ComponentActivity() { override fun onResume() { super.onResume() // Check Bluetooth and Location status on resume and handle accordingly - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { // Set app foreground state meshService.connectionManager.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false) @@ -479,9 +502,9 @@ class MainActivity : ComponentActivity() { val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() if (currentBluetoothStatus != BluetoothStatus.ENABLED) { android.util.Log.w("MainActivity", "Bluetooth disabled while app was backgrounded") - bluetoothStatus = currentBluetoothStatus - onboardingState = OnboardingState.BLUETOOTH_CHECK - isBluetoothLoading = false + mainViewModel.updateBluetoothStatus(currentBluetoothStatus) + mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK) + mainViewModel.updateBluetoothLoading(false) return } @@ -489,9 +512,9 @@ class MainActivity : ComponentActivity() { val currentLocationStatus = locationStatusManager.checkLocationStatus() if (currentLocationStatus != LocationStatus.ENABLED) { android.util.Log.w("MainActivity", "Location services disabled while app was backgrounded") - locationStatus = currentLocationStatus - onboardingState = OnboardingState.LOCATION_CHECK - isLocationLoading = false + mainViewModel.updateLocationStatus(currentLocationStatus) + mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) + mainViewModel.updateLocationLoading(false) } } } @@ -499,7 +522,7 @@ class MainActivity : ComponentActivity() { override fun onPause() { super.onPause() // Only set background state if app is fully initialized - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { // Set app background state meshService.connectionManager.setAppBackgroundState(true) chatViewModel.setAppBackgroundState(true) @@ -535,7 +558,7 @@ class MainActivity : ComponentActivity() { * Restart mesh services (for debugging/troubleshooting) */ fun restartMeshServices() { - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { lifecycleScope.launch { try { android.util.Log.d("MainActivity", "Restarting mesh services") @@ -562,7 +585,7 @@ class MainActivity : ComponentActivity() { } // Stop mesh services if app was fully initialized - if (onboardingState == OnboardingState.COMPLETE) { + if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) { try { meshService.stopServices() android.util.Log.d("MainActivity", "Mesh services stopped successfully") diff --git a/app/src/main/java/com/bitchat/android/MainViewModel.kt b/app/src/main/java/com/bitchat/android/MainViewModel.kt new file mode 100644 index 00000000..62177c18 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/MainViewModel.kt @@ -0,0 +1,55 @@ +package com.bitchat.android + +import androidx.lifecycle.ViewModel +import com.bitchat.android.onboarding.BluetoothStatus +import com.bitchat.android.onboarding.LocationStatus +import com.bitchat.android.onboarding.OnboardingState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class MainViewModel : ViewModel() { + + private val _onboardingState = MutableStateFlow(OnboardingState.CHECKING) + val onboardingState: StateFlow = _onboardingState.asStateFlow() + + private val _bluetoothStatus = MutableStateFlow(BluetoothStatus.ENABLED) + val bluetoothStatus: StateFlow = _bluetoothStatus.asStateFlow() + + private val _locationStatus = MutableStateFlow(LocationStatus.ENABLED) + val locationStatus: StateFlow = _locationStatus.asStateFlow() + + private val _errorMessage = MutableStateFlow("") + val errorMessage: StateFlow = _errorMessage.asStateFlow() + + private val _isBluetoothLoading = MutableStateFlow(false) + val isBluetoothLoading: StateFlow = _isBluetoothLoading.asStateFlow() + + private val _isLocationLoading = MutableStateFlow(false) + val isLocationLoading: StateFlow = _isLocationLoading.asStateFlow() + + // Public update functions for MainActivity + fun updateOnboardingState(state: OnboardingState) { + _onboardingState.value = state + } + + fun updateBluetoothStatus(status: BluetoothStatus) { + _bluetoothStatus.value = status + } + + fun updateLocationStatus(status: LocationStatus) { + _locationStatus.value = status + } + + fun updateErrorMessage(message: String) { + _errorMessage.value = message + } + + fun updateBluetoothLoading(loading: Boolean) { + _isBluetoothLoading.value = loading + } + + fun updateLocationLoading(loading: Boolean) { + _isLocationLoading.value = loading + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/onboarding/OnboardingState.kt b/app/src/main/java/com/bitchat/android/onboarding/OnboardingState.kt new file mode 100644 index 00000000..cb8e73b5 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/onboarding/OnboardingState.kt @@ -0,0 +1,12 @@ +package com.bitchat.android.onboarding + +enum class OnboardingState { + CHECKING, + BLUETOOTH_CHECK, + LOCATION_CHECK, + PERMISSION_EXPLANATION, + PERMISSION_REQUESTING, + INITIALIZING, + COMPLETE, + ERROR +} \ No newline at end of file