diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 6f80052a..7832a5dd 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -4,17 +4,9 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log -import androidx.activity.OnBackPressedCallback import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels -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.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.repeatOnLifecycle @@ -22,27 +14,19 @@ import androidx.lifecycle.Lifecycle import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.mesh.MeshService import com.bitchat.android.geohash.LocationChannelManager -import com.bitchat.android.onboarding.BluetoothCheckScreen import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.BluetoothStatusManager import com.bitchat.android.onboarding.BatteryOptimizationManager import com.bitchat.android.onboarding.BatteryOptimizationPreferenceManager -import com.bitchat.android.onboarding.BatteryOptimizationScreen import com.bitchat.android.onboarding.BatteryOptimizationStatus -import com.bitchat.android.onboarding.BackgroundLocationPermissionScreen -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.BitchatApp import com.bitchat.android.ui.ChatViewModel import com.bitchat.android.ui.OrientationAwareActivity -import com.bitchat.android.ui.theme.BitchatTheme import com.bitchat.android.wifiaware.WifiAwareController import com.bitchat.android.nostr.PoWPreferenceManager import com.bitchat.android.services.VerificationService @@ -155,17 +139,20 @@ class MainActivity : OrientationAwareActivity() { ) setContent { - BitchatTheme { - Scaffold( - modifier = Modifier.fillMaxSize(), - containerColor = MaterialTheme.colorScheme.background - ) { innerPadding -> - OnboardingFlowScreen(modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) - } - } + BitchatApp( + mainViewModel = mainViewModel, + chatViewModel = chatViewModel, + permissionManager = permissionManager, + onboardingCoordinator = onboardingCoordinator, + bluetoothStatusManager = bluetoothStatusManager, + locationStatusManager = locationStatusManager, + batteryOptimizationManager = batteryOptimizationManager, + onCheckBluetoothAndProceed = ::checkBluetoothAndProceed, + onCheckLocationAndProceed = ::checkLocationAndProceed, + onCheckBatteryOptimizationAndProceed = ::checkBatteryOptimizationAndProceed, + onProceedWithPermissionCheck = ::proceedWithPermissionCheck, + onCheckOnboardingStatus = ::checkOnboardingStatus + ) } // Collect state changes in a lifecycle-aware manner @@ -195,159 +182,7 @@ class MainActivity : OrientationAwareActivity() { } } - @Composable - private fun OnboardingFlowScreen(modifier: Modifier = Modifier) { - val context = LocalContext.current - val onboardingState by mainViewModel.onboardingState.collectAsState() - val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState() - val locationStatus by mainViewModel.locationStatus.collectAsState() - val batteryOptimizationStatus by mainViewModel.batteryOptimizationStatus.collectAsState() - val errorMessage by mainViewModel.errorMessage.collectAsState() - val isBluetoothLoading by mainViewModel.isBluetoothLoading.collectAsState() - val isLocationLoading by mainViewModel.isLocationLoading.collectAsState() - val isBatteryOptimizationLoading by mainViewModel.isBatteryOptimizationLoading.collectAsState() - DisposableEffect(context, bluetoothStatusManager) { - - val receiver = bluetoothStatusManager.monitorBluetoothState( - context = context, - bluetoothStatusManager = bluetoothStatusManager, - onBluetoothStateChanged = { status -> - if (status == BluetoothStatus.ENABLED && onboardingState == OnboardingState.BLUETOOTH_CHECK) { - checkBluetoothAndProceed() - } - } - ) - - onDispose { - try { - context.unregisterReceiver(receiver) - } catch (e: IllegalStateException) { - Log.w("BluetoothStatusUI", "Receiver was not registered") - } - } - } - - when (onboardingState) { - OnboardingState.PERMISSION_REQUESTING -> { - InitializingScreen(modifier) - } - - OnboardingState.BLUETOOTH_CHECK -> { - BluetoothCheckScreen( - modifier = modifier, - status = bluetoothStatus, - onEnableBluetooth = { - mainViewModel.updateBluetoothLoading(true) - bluetoothStatusManager.requestEnableBluetooth() - }, - onRetry = { - checkBluetoothAndProceed() - }, - onSkip = { - mainViewModel.skipBluetoothCheck() - checkLocationAndProceed() - }, - isLoading = isBluetoothLoading - ) - } - - OnboardingState.LOCATION_CHECK -> { - LocationCheckScreen( - modifier = modifier, - status = locationStatus, - onEnableLocation = { - mainViewModel.updateLocationLoading(true) - locationStatusManager.requestEnableLocation() - }, - onRetry = { - checkLocationAndProceed() - }, - isLoading = isLocationLoading - ) - } - - OnboardingState.BATTERY_OPTIMIZATION_CHECK -> { - BatteryOptimizationScreen( - modifier = modifier, - status = batteryOptimizationStatus, - onDisableBatteryOptimization = { - mainViewModel.updateBatteryOptimizationLoading(true) - batteryOptimizationManager.requestDisableBatteryOptimization() - }, - onRetry = { - checkBatteryOptimizationAndProceed() - }, - onSkip = { - // Skip battery optimization and proceed - proceedWithPermissionCheck() - }, - isLoading = isBatteryOptimizationLoading - ) - } - - OnboardingState.PERMISSION_EXPLANATION -> { - PermissionExplanationScreen( - modifier = modifier, - permissionCategories = permissionManager.getCategorizedPermissions(), - onContinue = { - mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_REQUESTING) - onboardingCoordinator.requestPermissions() - } - ) - } - - OnboardingState.BACKGROUND_LOCATION_EXPLANATION -> { - BackgroundLocationPermissionScreen( - modifier = modifier, - onContinue = { - onboardingCoordinator.requestBackgroundLocation() - }, - onRetry = { - onboardingCoordinator.checkBackgroundLocationAndProceed() - }, - onSkip = { - onboardingCoordinator.skipBackgroundLocation() - } - ) - } - - OnboardingState.CHECKING, OnboardingState.INITIALIZING, 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) - ChatScreen(viewModel = chatViewModel) - } - - OnboardingState.ERROR -> { - InitializationErrorScreen( - modifier = modifier, - errorMessage = errorMessage, - onRetry = { - mainViewModel.updateOnboardingState(OnboardingState.CHECKING) - checkOnboardingStatus() - }, - onOpenSettings = { - onboardingCoordinator.openAppSettings() - } - ) - } - } - } private fun handleOnboardingStateChange(state: OnboardingState) { diff --git a/app/src/main/java/com/bitchat/android/ui/BitchatApp.kt b/app/src/main/java/com/bitchat/android/ui/BitchatApp.kt new file mode 100644 index 00000000..acf060ac --- /dev/null +++ b/app/src/main/java/com/bitchat/android/ui/BitchatApp.kt @@ -0,0 +1,234 @@ +package com.bitchat.android.ui + +import android.util.Log +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +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.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.bitchat.android.MainViewModel +import com.bitchat.android.onboarding.* +import com.bitchat.android.ui.theme.BitchatTheme + +@Composable +internal fun BitchatApp( + mainViewModel: MainViewModel, + chatViewModel: ChatViewModel, + permissionManager: PermissionManager, + onboardingCoordinator: OnboardingCoordinator, + bluetoothStatusManager: BluetoothStatusManager, + locationStatusManager: LocationStatusManager, + batteryOptimizationManager: BatteryOptimizationManager, + onCheckBluetoothAndProceed: () -> Unit, + onCheckLocationAndProceed: () -> Unit, + onCheckBatteryOptimizationAndProceed: () -> Unit, + onProceedWithPermissionCheck: () -> Unit, + onCheckOnboardingStatus: () -> Unit +) { + BitchatTheme { + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background + ) { innerPadding -> + OnboardingFlowScreen( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + mainViewModel = mainViewModel, + chatViewModel = chatViewModel, + permissionManager = permissionManager, + onboardingCoordinator = onboardingCoordinator, + bluetoothStatusManager = bluetoothStatusManager, + locationStatusManager = locationStatusManager, + batteryOptimizationManager = batteryOptimizationManager, + onCheckBluetoothAndProceed = onCheckBluetoothAndProceed, + onCheckLocationAndProceed = onCheckLocationAndProceed, + onCheckBatteryOptimizationAndProceed = onCheckBatteryOptimizationAndProceed, + onProceedWithPermissionCheck = onProceedWithPermissionCheck, + onCheckOnboardingStatus = onCheckOnboardingStatus + ) + } + } +} + +@Composable +private fun OnboardingFlowScreen( + modifier: Modifier = Modifier, + mainViewModel: MainViewModel, + chatViewModel: ChatViewModel, + permissionManager: PermissionManager, + onboardingCoordinator: OnboardingCoordinator, + bluetoothStatusManager: BluetoothStatusManager, + locationStatusManager: LocationStatusManager, + batteryOptimizationManager: BatteryOptimizationManager, + onCheckBluetoothAndProceed: () -> Unit, + onCheckLocationAndProceed: () -> Unit, + onCheckBatteryOptimizationAndProceed: () -> Unit, + onProceedWithPermissionCheck: () -> Unit, + onCheckOnboardingStatus: () -> Unit +) { + val context = LocalContext.current + val onboardingState by mainViewModel.onboardingState.collectAsState() + val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState() + val locationStatus by mainViewModel.locationStatus.collectAsState() + val batteryOptimizationStatus by mainViewModel.batteryOptimizationStatus.collectAsState() + val errorMessage by mainViewModel.errorMessage.collectAsState() + val isBluetoothLoading by mainViewModel.isBluetoothLoading.collectAsState() + val isLocationLoading by mainViewModel.isLocationLoading.collectAsState() + val isBatteryOptimizationLoading by mainViewModel.isBatteryOptimizationLoading.collectAsState() + + DisposableEffect(context, bluetoothStatusManager) { + val receiver = bluetoothStatusManager.monitorBluetoothState( + context = context, + bluetoothStatusManager = bluetoothStatusManager, + onBluetoothStateChanged = { status -> + if (status == BluetoothStatus.ENABLED && onboardingState == OnboardingState.BLUETOOTH_CHECK) { + onCheckBluetoothAndProceed() + } + } + ) + + onDispose { + try { + context.unregisterReceiver(receiver) + } catch (e: IllegalStateException) { + Log.w("BluetoothStatusUI", "Receiver was not registered") + } + } + } + + val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + val lifecycleOwner = LocalLifecycleOwner.current + + when (onboardingState) { + OnboardingState.PERMISSION_REQUESTING -> { + InitializingScreen(modifier) + } + + OnboardingState.BLUETOOTH_CHECK -> { + BluetoothCheckScreen( + modifier = modifier, + status = bluetoothStatus, + onEnableBluetooth = { + mainViewModel.updateBluetoothLoading(true) + bluetoothStatusManager.requestEnableBluetooth() + }, + onRetry = { + onCheckBluetoothAndProceed() + }, + onSkip = { + mainViewModel.skipBluetoothCheck() + onCheckLocationAndProceed() + }, + isLoading = isBluetoothLoading + ) + } + + OnboardingState.LOCATION_CHECK -> { + LocationCheckScreen( + modifier = modifier, + status = locationStatus, + onEnableLocation = { + mainViewModel.updateLocationLoading(true) + locationStatusManager.requestEnableLocation() + }, + onRetry = { + onCheckLocationAndProceed() + }, + isLoading = isLocationLoading + ) + } + + OnboardingState.BATTERY_OPTIMIZATION_CHECK -> { + BatteryOptimizationScreen( + modifier = modifier, + status = batteryOptimizationStatus, + onDisableBatteryOptimization = { + mainViewModel.updateBatteryOptimizationLoading(true) + batteryOptimizationManager.requestDisableBatteryOptimization() + }, + onRetry = { + onCheckBatteryOptimizationAndProceed() + }, + onSkip = { + // Skip battery optimization and proceed + onProceedWithPermissionCheck() + }, + isLoading = isBatteryOptimizationLoading + ) + } + + OnboardingState.PERMISSION_EXPLANATION -> { + PermissionExplanationScreen( + modifier = modifier, + permissionCategories = permissionManager.getCategorizedPermissions(), + onContinue = { + mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_REQUESTING) + onboardingCoordinator.requestPermissions() + } + ) + } + + OnboardingState.BACKGROUND_LOCATION_EXPLANATION -> { + BackgroundLocationPermissionScreen( + modifier = modifier, + onContinue = { + onboardingCoordinator.requestBackgroundLocation() + }, + onRetry = { + onboardingCoordinator.checkBackgroundLocationAndProceed() + }, + onSkip = { + onboardingCoordinator.skipBackgroundLocation() + } + ) + } + + OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> { + // Set up back navigation handling for the chat screen + if (onBackPressedDispatcherOwner != null) { + DisposableEffect(onBackPressedDispatcherOwner, lifecycleOwner) { + 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 + onBackPressedDispatcherOwner.onBackPressedDispatcher.onBackPressed() + this.isEnabled = true + } + } + } + + onBackPressedDispatcherOwner.onBackPressedDispatcher.addCallback(lifecycleOwner, backCallback) + + onDispose { + backCallback.remove() + } + } + } + ChatScreen(viewModel = chatViewModel) + } + + OnboardingState.ERROR -> { + InitializationErrorScreen( + modifier = modifier, + errorMessage = errorMessage, + onRetry = { + mainViewModel.updateOnboardingState(OnboardingState.CHECKING) + onCheckOnboardingStatus() + }, + onOpenSettings = { + onboardingCoordinator.openAppSettings() + } + ) + } + } +}