mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 04:05:21 +00:00
onboarding
This commit is contained in:
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user