Refactored MainActivity by extracting the Compose application root and onboarding flow into a new BitchatApp composable. This reduces the activity's responsibilities to primarily handling Android lifecycle, permissions, and service coordination, while delegating the UI tree to a dedicated top-level composable.

This commit is contained in:
yet300 2026-08-26 16:09:22 +04:00
parent e0059b5980
commit 27a13094de
2 changed files with 249 additions and 180 deletions

View File

@ -4,17 +4,9 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels 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.lifecycleScope
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
@ -22,27 +14,19 @@ import androidx.lifecycle.Lifecycle
import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.mesh.MeshService import com.bitchat.android.mesh.MeshService
import com.bitchat.android.geohash.LocationChannelManager import com.bitchat.android.geohash.LocationChannelManager
import com.bitchat.android.onboarding.BluetoothCheckScreen
import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.BluetoothStatus
import com.bitchat.android.onboarding.BluetoothStatusManager import com.bitchat.android.onboarding.BluetoothStatusManager
import com.bitchat.android.onboarding.BatteryOptimizationManager import com.bitchat.android.onboarding.BatteryOptimizationManager
import com.bitchat.android.onboarding.BatteryOptimizationPreferenceManager import com.bitchat.android.onboarding.BatteryOptimizationPreferenceManager
import com.bitchat.android.onboarding.BatteryOptimizationScreen
import com.bitchat.android.onboarding.BatteryOptimizationStatus 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.LocationStatus
import com.bitchat.android.onboarding.LocationStatusManager import com.bitchat.android.onboarding.LocationStatusManager
import com.bitchat.android.onboarding.OnboardingCoordinator import com.bitchat.android.onboarding.OnboardingCoordinator
import com.bitchat.android.onboarding.OnboardingState import com.bitchat.android.onboarding.OnboardingState
import com.bitchat.android.onboarding.PermissionExplanationScreen
import com.bitchat.android.onboarding.PermissionManager 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.ChatViewModel
import com.bitchat.android.ui.OrientationAwareActivity import com.bitchat.android.ui.OrientationAwareActivity
import com.bitchat.android.ui.theme.BitchatTheme
import com.bitchat.android.wifiaware.WifiAwareController import com.bitchat.android.wifiaware.WifiAwareController
import com.bitchat.android.nostr.PoWPreferenceManager import com.bitchat.android.nostr.PoWPreferenceManager
import com.bitchat.android.services.VerificationService import com.bitchat.android.services.VerificationService
@ -155,17 +139,20 @@ class MainActivity : OrientationAwareActivity() {
) )
setContent { setContent {
BitchatTheme { BitchatApp(
Scaffold( mainViewModel = mainViewModel,
modifier = Modifier.fillMaxSize(), chatViewModel = chatViewModel,
containerColor = MaterialTheme.colorScheme.background permissionManager = permissionManager,
) { innerPadding -> onboardingCoordinator = onboardingCoordinator,
OnboardingFlowScreen(modifier = Modifier bluetoothStatusManager = bluetoothStatusManager,
.fillMaxSize() locationStatusManager = locationStatusManager,
.padding(innerPadding) batteryOptimizationManager = batteryOptimizationManager,
) onCheckBluetoothAndProceed = ::checkBluetoothAndProceed,
} onCheckLocationAndProceed = ::checkLocationAndProceed,
} onCheckBatteryOptimizationAndProceed = ::checkBatteryOptimizationAndProceed,
onProceedWithPermissionCheck = ::proceedWithPermissionCheck,
onCheckOnboardingStatus = ::checkOnboardingStatus
)
} }
// Collect state changes in a lifecycle-aware manner // 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) { private fun handleOnboardingStateChange(state: OnboardingState) {

View File

@ -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()
}
)
}
}
}