onboarding

This commit is contained in:
callebtc
2025-07-10 13:15:28 +02:00
parent 85acd03855
commit f9f56794d3
5 changed files with 1089 additions and 58 deletions
@@ -17,6 +17,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.lifecycleScope
import com.bitchat.android.onboarding.*
import com.bitchat.android.ui.ChatScreen
import com.bitchat.android.ui.ChatViewModel
import com.bitchat.android.ui.theme.BitchatTheme
@@ -25,28 +26,34 @@ import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private lateinit var permissionManager: PermissionManager
private lateinit var onboardingCoordinator: OnboardingCoordinator
private val chatViewModel: ChatViewModel by viewModels()
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allPermissionsGranted = permissions.values.all { it }
if (allPermissionsGranted) {
// Permissions granted, the mesh service should start automatically
} else {
// Handle permission denial
finish()
}
// UI state for onboarding flow
private var onboardingState by mutableStateOf(OnboardingState.CHECKING)
private var errorMessage by mutableStateOf("")
enum class OnboardingState {
CHECKING,
PERMISSION_EXPLANATION,
PERMISSION_REQUESTING,
INITIALIZING,
COMPLETE,
ERROR
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Request necessary permissions
requestPermissions()
// Handle notification intent if app was opened from a notification
handleNotificationIntent(intent)
// Initialize permission management
permissionManager = PermissionManager(this)
onboardingCoordinator = OnboardingCoordinator(
activity = this,
permissionManager = permissionManager,
onOnboardingComplete = ::handleOnboardingComplete,
onOnboardingFailed = ::handleOnboardingFailed
)
setContent {
BitchatTheme {
@@ -54,28 +61,155 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ChatScreen(viewModel = chatViewModel)
OnboardingFlowScreen()
}
}
}
// Start the onboarding process
checkOnboardingStatus()
}
@Composable
private fun OnboardingFlowScreen() {
when (onboardingState) {
OnboardingState.CHECKING -> {
InitializingScreen()
}
OnboardingState.PERMISSION_EXPLANATION -> {
PermissionExplanationScreen(
permissionCategories = permissionManager.getCategorizedPermissions(),
onContinue = {
onboardingState = OnboardingState.PERMISSION_REQUESTING
onboardingCoordinator.requestPermissions()
},
onCancel = {
finish()
}
)
}
OnboardingState.PERMISSION_REQUESTING -> {
InitializingScreen()
}
OnboardingState.INITIALIZING -> {
InitializingScreen()
}
OnboardingState.COMPLETE -> {
ChatScreen(viewModel = chatViewModel)
}
OnboardingState.ERROR -> {
InitializationErrorScreen(
errorMessage = errorMessage,
onRetry = {
onboardingState = OnboardingState.CHECKING
checkOnboardingStatus()
},
onOpenSettings = {
onboardingCoordinator.openAppSettings()
}
)
}
}
}
private fun checkOnboardingStatus() {
android.util.Log.d("MainActivity", "Checking onboarding status")
lifecycleScope.launch {
// Small delay to show the checking state
delay(500)
if (permissionManager.isFirstTimeLaunch()) {
android.util.Log.d("MainActivity", "First time launch, showing permission explanation")
onboardingState = OnboardingState.PERMISSION_EXPLANATION
} else if (permissionManager.areAllPermissionsGranted()) {
android.util.Log.d("MainActivity", "Existing user with permissions, initializing app")
onboardingState = OnboardingState.INITIALIZING
initializeApp()
} else {
android.util.Log.d("MainActivity", "Existing user missing permissions, showing explanation")
onboardingState = OnboardingState.PERMISSION_EXPLANATION
}
}
}
private fun handleOnboardingComplete() {
android.util.Log.d("MainActivity", "Onboarding completed, initializing app")
onboardingState = OnboardingState.INITIALIZING
initializeApp()
}
private fun handleOnboardingFailed(message: String) {
android.util.Log.e("MainActivity", "Onboarding failed: $message")
errorMessage = message
onboardingState = OnboardingState.ERROR
}
private fun initializeApp() {
android.util.Log.d("MainActivity", "Starting app initialization")
lifecycleScope.launch {
try {
// Initialize the app with a proper delay to ensure Bluetooth stack is ready
// This solves the issue where app needs restart to work on first install
delay(1000) // Give the system time to process permission grants
android.util.Log.d("MainActivity", "Permissions verified, starting mesh service")
// Ensure all permissions are still granted (user might have revoked in settings)
if (!permissionManager.areAllPermissionsGranted()) {
val missing = permissionManager.getMissingPermissions()
android.util.Log.w("MainActivity", "Permissions revoked during initialization: $missing")
handleOnboardingFailed("Some permissions were revoked. Please grant all permissions to continue.")
return@launch
}
// Initialize chat view model - this will start the mesh service
chatViewModel.meshService.startServices()
// Handle any notification intent
handleNotificationIntent(intent)
// Small delay to ensure mesh service is fully initialized
delay(500)
android.util.Log.d("MainActivity", "App initialization complete")
onboardingState = OnboardingState.COMPLETE
} catch (e: Exception) {
android.util.Log.e("MainActivity", "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
intent?.let { handleNotificationIntent(it) }
if (onboardingState == OnboardingState.COMPLETE) {
intent?.let { handleNotificationIntent(it) }
}
}
override fun onResume() {
super.onResume()
// Notify that app is in foreground for power optimization and notifications
chatViewModel.setAppBackgroundState(false)
// Only set background state if app is fully initialized
if (onboardingState == OnboardingState.COMPLETE) {
chatViewModel.setAppBackgroundState(false)
}
}
override fun onPause() {
super.onPause()
// Notify that app is in background for power optimization and notifications
chatViewModel.setAppBackgroundState(true)
// Only set background state if app is fully initialized
if (onboardingState == OnboardingState.COMPLETE) {
chatViewModel.setAppBackgroundState(true)
}
}
/**
@@ -103,45 +237,15 @@ class MainActivity : ComponentActivity() {
}
}
private fun requestPermissions() {
val permissions = mutableListOf<String>()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
permissions.addAll(listOf(
Manifest.permission.BLUETOOTH_ADVERTISE,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
))
} else {
permissions.addAll(listOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
))
}
permissions.addAll(listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
))
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
}
permissionLauncher.launch(permissions.toTypedArray())
}
override fun onDestroy() {
super.onDestroy()
// FIXED: Ensure Bluetooth resources are cleaned up when the activity is destroyed
// This addresses the issue where stale Bluetooth advertisements interfere with
// restart discovery times. This is more reliable than relying solely on
// ChatViewModel.onCleared() which may not be called when the app is swiped away.
try {
chatViewModel.meshService.stopServices()
} catch (e: Exception) {
// Log error, but don't crash the app on exit
android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}")
// Only stop mesh services if they were started
if (onboardingState == OnboardingState.COMPLETE) {
try {
chatViewModel.meshService.stopServices()
} catch (e: Exception) {
android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}")
}
}
}
}