mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 13:25:21 +00:00
885 lines
40 KiB
Kotlin
885 lines
40 KiB
Kotlin
package com.bitchat.android
|
||
|
||
import android.content.Intent
|
||
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
|
||
import androidx.lifecycle.Lifecycle
|
||
import com.bitchat.android.mesh.BluetoothMeshService
|
||
import com.bitchat.android.mesh.MeshService
|
||
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.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
|
||
import kotlinx.coroutines.delay
|
||
import kotlinx.coroutines.launch
|
||
|
||
class MainActivity : OrientationAwareActivity() {
|
||
|
||
private lateinit var permissionManager: PermissionManager
|
||
private lateinit var onboardingCoordinator: OnboardingCoordinator
|
||
private lateinit var bluetoothStatusManager: BluetoothStatusManager
|
||
private lateinit var locationStatusManager: LocationStatusManager
|
||
private lateinit var batteryOptimizationManager: BatteryOptimizationManager
|
||
|
||
// Core mesh service - provided by the foreground service holder
|
||
private lateinit var meshService: BluetoothMeshService
|
||
private lateinit var unifiedMeshService: MeshService
|
||
private val mainViewModel: MainViewModel by viewModels()
|
||
private val chatViewModel: ChatViewModel by viewModels {
|
||
object : ViewModelProvider.Factory {
|
||
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
|
||
@Suppress("UNCHECKED_CAST")
|
||
return ChatViewModel(application, meshService, unifiedMeshService) as T
|
||
}
|
||
}
|
||
}
|
||
|
||
private val forceFinishReceiver = object : android.content.BroadcastReceiver() {
|
||
override fun onReceive(context: android.content.Context, intent: android.content.Intent) {
|
||
if (intent.action == com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH) {
|
||
android.util.Log.i("MainActivity", "Received force finish broadcast, closing UI")
|
||
finishAffinity()
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onCreate(savedInstanceState: Bundle?) {
|
||
super.onCreate(savedInstanceState)
|
||
|
||
// Register receiver for force finish signal from shutdown coordinator
|
||
val filter = android.content.IntentFilter(com.bitchat.android.util.AppConstants.UI.ACTION_FORCE_FINISH)
|
||
if (android.os.Build.VERSION.SDK_INT >= 33) {
|
||
registerReceiver(
|
||
forceFinishReceiver,
|
||
filter,
|
||
com.bitchat.android.util.AppConstants.UI.PERMISSION_FORCE_FINISH,
|
||
null,
|
||
android.content.Context.RECEIVER_NOT_EXPORTED
|
||
)
|
||
} else {
|
||
@Suppress("DEPRECATION")
|
||
registerReceiver(
|
||
forceFinishReceiver,
|
||
filter,
|
||
com.bitchat.android.util.AppConstants.UI.PERMISSION_FORCE_FINISH,
|
||
null
|
||
)
|
||
}
|
||
|
||
// Check if this is a quit request from the notification
|
||
if (intent.getBooleanExtra("ACTION_QUIT_APP", false)) {
|
||
android.util.Log.d("MainActivity", "Quit request received in onCreate, finishing activity")
|
||
finish()
|
||
return
|
||
}
|
||
|
||
com.bitchat.android.service.AppShutdownCoordinator.cancelPendingShutdown()
|
||
|
||
// Enable edge-to-edge display for modern Android look
|
||
enableEdgeToEdge()
|
||
|
||
// Initialize permission management
|
||
permissionManager = PermissionManager(this)
|
||
// Ensure foreground service is running and get mesh instance from holder
|
||
try { com.bitchat.android.service.MeshForegroundService.start(applicationContext) } catch (_: Exception) { }
|
||
meshService = com.bitchat.android.service.MeshServiceHolder.getOrCreate(applicationContext)
|
||
unifiedMeshService = com.bitchat.android.service.MeshServiceHolder.getUnifiedOrCreate(applicationContext)
|
||
// Expose BLE mesh to Wi‑Fi Aware controller for cross-transport relays - DEPRECATED
|
||
// Bridging is now handled by TransportBridgeService automatically
|
||
|
||
bluetoothStatusManager = BluetoothStatusManager(
|
||
activity = this,
|
||
context = this,
|
||
onBluetoothEnabled = ::handleBluetoothEnabled,
|
||
onBluetoothDisabled = ::handleBluetoothDisabled
|
||
)
|
||
locationStatusManager = LocationStatusManager(
|
||
activity = this,
|
||
context = this,
|
||
onLocationEnabled = ::handleLocationEnabled,
|
||
onLocationDisabled = ::handleLocationDisabled
|
||
)
|
||
batteryOptimizationManager = BatteryOptimizationManager(
|
||
activity = this,
|
||
context = this,
|
||
onBatteryOptimizationDisabled = ::handleBatteryOptimizationDisabled,
|
||
onBatteryOptimizationFailed = ::handleBatteryOptimizationFailed
|
||
)
|
||
onboardingCoordinator = OnboardingCoordinator(
|
||
activity = this,
|
||
permissionManager = permissionManager,
|
||
onOnboardingComplete = ::handleOnboardingComplete,
|
||
onBackgroundLocationRequired = {
|
||
mainViewModel.updateOnboardingState(OnboardingState.BACKGROUND_LOCATION_EXPLANATION)
|
||
},
|
||
onOnboardingFailed = ::handleOnboardingFailed
|
||
)
|
||
|
||
setContent {
|
||
BitchatTheme {
|
||
Scaffold(
|
||
modifier = Modifier.fillMaxSize(),
|
||
containerColor = MaterialTheme.colorScheme.background
|
||
) { innerPadding ->
|
||
OnboardingFlowScreen(modifier = Modifier
|
||
.fillMaxSize()
|
||
.padding(innerPadding)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Collect state changes in a lifecycle-aware manner
|
||
lifecycleScope.launch {
|
||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||
mainViewModel.onboardingState.collect { state ->
|
||
handleOnboardingStateChange(state)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Keep the unified mesh delegate attached when Wi-Fi Aware starts after the UI.
|
||
lifecycleScope.launch {
|
||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||
WifiAwareController.running.collect { running ->
|
||
if (running && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||
unifiedMeshService.delegate = chatViewModel
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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(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)
|
||
Log.d("BluetoothStatusUI", "BroadcastReceiver unregistered")
|
||
} 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) {
|
||
|
||
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() {
|
||
Log.d("MainActivity", "Checking onboarding status")
|
||
|
||
lifecycleScope.launch {
|
||
// Small delay to show the checking state
|
||
delay(500)
|
||
|
||
// First check Bluetooth status (always required)
|
||
checkBluetoothAndProceed()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check Bluetooth status and proceed with onboarding flow
|
||
*/
|
||
private fun checkBluetoothAndProceed() {
|
||
// Log.d("MainActivity", "Checking Bluetooth status")
|
||
|
||
// Check if user has skipped Bluetooth check for this session
|
||
if (mainViewModel.isBluetoothCheckSkipped.value) {
|
||
Log.d("MainActivity", "Bluetooth check skipped by user, proceeding to location check")
|
||
checkLocationAndProceed()
|
||
return
|
||
}
|
||
|
||
// For first-time users, skip Bluetooth check and go straight to permissions
|
||
// We'll check Bluetooth after permissions are granted
|
||
if (permissionManager.isFirstTimeLaunch()) {
|
||
Log.d("MainActivity", "First-time launch, skipping Bluetooth check - will check after permissions")
|
||
proceedWithPermissionCheck()
|
||
return
|
||
}
|
||
|
||
// For existing users, check Bluetooth status first
|
||
bluetoothStatusManager.logBluetoothStatus()
|
||
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
|
||
|
||
val bleRequired = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||
if (!bleRequired) {
|
||
// Skip BLE checks entirely when BLE is disabled in debug settings
|
||
checkLocationAndProceed()
|
||
return
|
||
}
|
||
when (mainViewModel.bluetoothStatus.value) {
|
||
BluetoothStatus.ENABLED -> {
|
||
// Bluetooth is enabled, check location services next
|
||
checkLocationAndProceed()
|
||
}
|
||
BluetoothStatus.DISABLED -> {
|
||
// Show Bluetooth enable screen (should have permissions as existing user)
|
||
Log.d("MainActivity", "Bluetooth disabled, showing enable screen")
|
||
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
|
||
mainViewModel.updateBluetoothLoading(false)
|
||
}
|
||
BluetoothStatus.NOT_SUPPORTED -> {
|
||
// Device doesn't support Bluetooth
|
||
android.util.Log.e("MainActivity", "Bluetooth not supported")
|
||
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
|
||
mainViewModel.updateBluetoothLoading(false)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Proceed with permission checking
|
||
*/
|
||
private fun proceedWithPermissionCheck() {
|
||
Log.d("MainActivity", "Proceeding with permission check")
|
||
|
||
lifecycleScope.launch {
|
||
delay(200) // Small delay for smooth transition
|
||
|
||
if (permissionManager.isFirstTimeLaunch()) {
|
||
Log.d("MainActivity", "First time launch, showing permission explanation")
|
||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||
} else if (permissionManager.areRequiredPermissionsGranted()) {
|
||
Log.d("MainActivity", "Existing user with required permissions")
|
||
if (permissionManager.needsBackgroundLocationPermission() &&
|
||
!permissionManager.isBackgroundLocationGranted() &&
|
||
!com.bitchat.android.onboarding.BackgroundLocationPreferenceManager.isSkipped(this@MainActivity)
|
||
) {
|
||
mainViewModel.updateOnboardingState(OnboardingState.BACKGROUND_LOCATION_EXPLANATION)
|
||
} else {
|
||
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
|
||
initializeApp()
|
||
}
|
||
} else {
|
||
Log.d("MainActivity", "Existing user missing permissions, showing explanation")
|
||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle Bluetooth enabled callback
|
||
*/
|
||
private fun handleBluetoothEnabled() {
|
||
Log.d("MainActivity", "Bluetooth enabled by user")
|
||
mainViewModel.updateBluetoothLoading(false)
|
||
mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED)
|
||
checkLocationAndProceed()
|
||
}
|
||
|
||
/**
|
||
* Check Location services status and proceed with onboarding flow
|
||
*/
|
||
private fun checkLocationAndProceed() {
|
||
Log.d("MainActivity", "Checking location services status")
|
||
|
||
// For first-time users, skip location check and go straight to permissions
|
||
// We'll check location after permissions are granted
|
||
if (permissionManager.isFirstTimeLaunch()) {
|
||
Log.d("MainActivity", "First-time launch, skipping location check - will check after permissions")
|
||
proceedWithPermissionCheck()
|
||
return
|
||
}
|
||
|
||
// For existing users, check location status
|
||
locationStatusManager.logLocationStatus()
|
||
mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
|
||
|
||
when (mainViewModel.locationStatus.value) {
|
||
LocationStatus.ENABLED -> {
|
||
// Location services enabled, check battery optimization next
|
||
checkBatteryOptimizationAndProceed()
|
||
}
|
||
LocationStatus.DISABLED -> {
|
||
// Show location enable screen (should have permissions as existing user)
|
||
Log.d("MainActivity", "Location services disabled, showing enable screen")
|
||
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
||
mainViewModel.updateLocationLoading(false)
|
||
}
|
||
LocationStatus.NOT_AVAILABLE -> {
|
||
// Device doesn't support location services (very unusual)
|
||
Log.e("MainActivity", "Location services not available")
|
||
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
||
mainViewModel.updateLocationLoading(false)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle Location enabled callback
|
||
*/
|
||
private fun handleLocationEnabled() {
|
||
Log.d("MainActivity", "Location services enabled by user")
|
||
mainViewModel.updateLocationLoading(false)
|
||
mainViewModel.updateLocationStatus(LocationStatus.ENABLED)
|
||
// Ensure Wi-Fi Aware starts now that location is enabled
|
||
com.bitchat.android.wifiaware.WifiAwareController.startIfPossible()
|
||
checkBatteryOptimizationAndProceed()
|
||
}
|
||
|
||
/**
|
||
* Handle Location disabled callback
|
||
*/
|
||
private fun handleLocationDisabled(message: String) {
|
||
Log.w("MainActivity", "Location services disabled or failed: $message")
|
||
mainViewModel.updateLocationLoading(false)
|
||
mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
|
||
|
||
when {
|
||
mainViewModel.locationStatus.value == LocationStatus.NOT_AVAILABLE -> {
|
||
// Show permanent error for devices without location services
|
||
mainViewModel.updateErrorMessage(message)
|
||
mainViewModel.updateOnboardingState(OnboardingState.ERROR)
|
||
}
|
||
else -> {
|
||
// Stay on location check screen for retry
|
||
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle Bluetooth disabled callback
|
||
*/
|
||
private fun handleBluetoothDisabled(message: String) {
|
||
Log.w("MainActivity", "Bluetooth disabled or failed: $message")
|
||
mainViewModel.updateBluetoothLoading(false)
|
||
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
|
||
|
||
when {
|
||
mainViewModel.bluetoothStatus.value == BluetoothStatus.NOT_SUPPORTED -> {
|
||
// Show permanent error for unsupported devices
|
||
mainViewModel.updateErrorMessage(message)
|
||
mainViewModel.updateOnboardingState(OnboardingState.ERROR)
|
||
}
|
||
message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> {
|
||
// During first-time onboarding, if Bluetooth enable fails due to permissions,
|
||
// proceed to permission explanation screen where user will grant permissions first
|
||
Log.d("MainActivity", "Bluetooth enable requires permissions, proceeding to permission explanation")
|
||
proceedWithPermissionCheck()
|
||
}
|
||
message.contains("Permission") -> {
|
||
// For existing users, redirect to permission explanation to grant missing permissions
|
||
Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation")
|
||
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
|
||
}
|
||
else -> {
|
||
// Stay on Bluetooth check screen for retry
|
||
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun handleOnboardingComplete() {
|
||
Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app")
|
||
|
||
// After permissions are granted, re-check Bluetooth, Location, and Battery Optimization status
|
||
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
||
val currentLocationStatus = locationStatusManager.checkLocationStatus()
|
||
val currentBatteryOptimizationStatus = when {
|
||
!batteryOptimizationManager.isBatteryOptimizationSupported() -> BatteryOptimizationStatus.NOT_SUPPORTED
|
||
batteryOptimizationManager.isBatteryOptimizationDisabled() -> BatteryOptimizationStatus.DISABLED
|
||
else -> BatteryOptimizationStatus.ENABLED
|
||
}
|
||
|
||
val bleRequired2 = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||
when {
|
||
bleRequired2 && currentBluetoothStatus != BluetoothStatus.ENABLED -> {
|
||
// Bluetooth still disabled, but now we have permissions to enable it
|
||
Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.")
|
||
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
|
||
Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.")
|
||
mainViewModel.updateLocationStatus(currentLocationStatus)
|
||
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
||
mainViewModel.updateLocationLoading(false)
|
||
}
|
||
currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> {
|
||
// Battery optimization still enabled, show battery optimization screen
|
||
android.util.Log.d("MainActivity", "Permissions granted, but battery optimization still enabled. Showing battery optimization screen.")
|
||
mainViewModel.updateBatteryOptimizationStatus(currentBatteryOptimizationStatus)
|
||
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
|
||
mainViewModel.updateBatteryOptimizationLoading(false)
|
||
}
|
||
else -> {
|
||
// Both are enabled, proceed to app initialization
|
||
Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization")
|
||
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
|
||
initializeApp()
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun handleOnboardingFailed(message: String) {
|
||
Log.e("MainActivity", "Onboarding failed: $message")
|
||
mainViewModel.updateErrorMessage(message)
|
||
mainViewModel.updateOnboardingState(OnboardingState.ERROR)
|
||
}
|
||
|
||
/**
|
||
* Check Battery Optimization status and proceed with onboarding flow
|
||
*/
|
||
private fun checkBatteryOptimizationAndProceed() {
|
||
android.util.Log.d("MainActivity", "Checking battery optimization status")
|
||
|
||
// For first-time users, skip battery optimization check and go straight to permissions
|
||
// We'll check battery optimization after permissions are granted
|
||
if (permissionManager.isFirstTimeLaunch()) {
|
||
android.util.Log.d("MainActivity", "First-time launch, skipping battery optimization check - will check after permissions")
|
||
proceedWithPermissionCheck()
|
||
return
|
||
}
|
||
|
||
// Check if user has previously skipped battery optimization
|
||
if (BatteryOptimizationPreferenceManager.isSkipped(this)) {
|
||
android.util.Log.d("MainActivity", "User previously skipped battery optimization, proceeding to permissions")
|
||
proceedWithPermissionCheck()
|
||
return
|
||
}
|
||
|
||
// For existing users, check battery optimization status
|
||
batteryOptimizationManager.logBatteryOptimizationStatus()
|
||
val currentBatteryOptimizationStatus = when {
|
||
!batteryOptimizationManager.isBatteryOptimizationSupported() -> BatteryOptimizationStatus.NOT_SUPPORTED
|
||
batteryOptimizationManager.isBatteryOptimizationDisabled() -> BatteryOptimizationStatus.DISABLED
|
||
else -> BatteryOptimizationStatus.ENABLED
|
||
}
|
||
mainViewModel.updateBatteryOptimizationStatus(currentBatteryOptimizationStatus)
|
||
|
||
when (currentBatteryOptimizationStatus) {
|
||
BatteryOptimizationStatus.DISABLED, BatteryOptimizationStatus.NOT_SUPPORTED -> {
|
||
// Battery optimization is disabled or not supported, proceed with permission check
|
||
proceedWithPermissionCheck()
|
||
}
|
||
BatteryOptimizationStatus.ENABLED -> {
|
||
// Show battery optimization disable screen
|
||
android.util.Log.d("MainActivity", "Battery optimization enabled, showing disable screen")
|
||
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
|
||
mainViewModel.updateBatteryOptimizationLoading(false)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle Battery Optimization disabled callback
|
||
*/
|
||
private fun handleBatteryOptimizationDisabled() {
|
||
android.util.Log.d("MainActivity", "Battery optimization disabled by user")
|
||
mainViewModel.updateBatteryOptimizationLoading(false)
|
||
mainViewModel.updateBatteryOptimizationStatus(BatteryOptimizationStatus.DISABLED)
|
||
proceedWithPermissionCheck()
|
||
}
|
||
|
||
/**
|
||
* Handle Battery Optimization failed callback
|
||
*/
|
||
private fun handleBatteryOptimizationFailed(message: String) {
|
||
android.util.Log.w("MainActivity", "Battery optimization disable failed: $message")
|
||
mainViewModel.updateBatteryOptimizationLoading(false)
|
||
val currentStatus = when {
|
||
!batteryOptimizationManager.isBatteryOptimizationSupported() -> BatteryOptimizationStatus.NOT_SUPPORTED
|
||
batteryOptimizationManager.isBatteryOptimizationDisabled() -> BatteryOptimizationStatus.DISABLED
|
||
else -> BatteryOptimizationStatus.ENABLED
|
||
}
|
||
mainViewModel.updateBatteryOptimizationStatus(currentStatus)
|
||
|
||
// Stay on battery optimization check screen for retry
|
||
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
|
||
}
|
||
|
||
private fun initializeApp() {
|
||
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
|
||
|
||
Log.d("MainActivity", "Permissions verified, initializing chat system")
|
||
|
||
// Initialize PoW preferences early in the initialization process
|
||
PoWPreferenceManager.init(this@MainActivity)
|
||
Log.d("MainActivity", "PoW preferences initialized")
|
||
|
||
// Initialize Location Notes Manager (extracted to separate file)
|
||
com.bitchat.android.nostr.LocationNotesInitializer.initialize(this@MainActivity)
|
||
|
||
// Ensure all permissions are still granted (user might have revoked in settings)
|
||
if (!permissionManager.areAllPermissionsGranted()) {
|
||
val missing = permissionManager.getMissingPermissions()
|
||
Log.w("MainActivity", "Permissions revoked during initialization: $missing")
|
||
handleOnboardingFailed("Some permissions were revoked. Please grant all permissions to continue.")
|
||
return@launch
|
||
}
|
||
|
||
// Set up unified mesh delegate and start enabled transports
|
||
unifiedMeshService.delegate = chatViewModel
|
||
unifiedMeshService.startServices()
|
||
|
||
Log.d("MainActivity", "Mesh service started successfully")
|
||
|
||
// Handle any notification intent
|
||
handleNotificationIntent(intent)
|
||
handleVerificationIntent(intent)
|
||
|
||
// Small delay to ensure mesh service is fully initialized
|
||
delay(500)
|
||
Log.d("MainActivity", "App initialization complete")
|
||
mainViewModel.updateOnboardingState(OnboardingState.COMPLETE)
|
||
} catch (e: Exception) {
|
||
Log.e("MainActivity", "Failed to initialize app", e)
|
||
handleOnboardingFailed("Failed to initialize the app: ${e.message}")
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onNewIntent(intent: Intent) {
|
||
super.onNewIntent(intent)
|
||
setIntent(intent)
|
||
|
||
// Check if this is a quit request from the notification
|
||
if (intent.getBooleanExtra("ACTION_QUIT_APP", false)) {
|
||
android.util.Log.d("MainActivity", "Quit request received, finishing activity")
|
||
finish()
|
||
return
|
||
}
|
||
|
||
com.bitchat.android.service.AppShutdownCoordinator.cancelPendingShutdown()
|
||
|
||
// Handle notification intents when app is already running
|
||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||
handleNotificationIntent(intent)
|
||
handleVerificationIntent(intent)
|
||
}
|
||
}
|
||
|
||
override fun onResume() {
|
||
super.onResume()
|
||
// Check Bluetooth and Location status on resume and handle accordingly
|
||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||
// Reattach mesh delegate to new ChatViewModel instance after Activity recreation
|
||
try { unifiedMeshService.delegate = chatViewModel } catch (_: Exception) { }
|
||
|
||
// Check if Bluetooth was disabled while app was backgrounded
|
||
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
||
val bleRequired = try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
|
||
if (bleRequired && currentBluetoothStatus != BluetoothStatus.ENABLED && !mainViewModel.isBluetoothCheckSkipped.value) {
|
||
Log.w("MainActivity", "Bluetooth disabled while app was backgrounded")
|
||
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
|
||
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
|
||
mainViewModel.updateBluetoothLoading(false)
|
||
return
|
||
}
|
||
|
||
// Check if location services were disabled while app was backgrounded
|
||
val currentLocationStatus = locationStatusManager.checkLocationStatus()
|
||
if (currentLocationStatus != LocationStatus.ENABLED) {
|
||
Log.w("MainActivity", "Location services disabled while app was backgrounded")
|
||
mainViewModel.updateLocationStatus(currentLocationStatus)
|
||
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
||
mainViewModel.updateLocationLoading(false)
|
||
} else {
|
||
// If location is enabled, ensure Wi-Fi Aware starts if it was blocked by location earlier
|
||
com.bitchat.android.wifiaware.WifiAwareController.startIfPossible()
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onPause() {
|
||
super.onPause()
|
||
// Only set background state if app is fully initialized
|
||
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||
// Detach UI delegate so the foreground service can own DM notifications while UI is closed
|
||
try { unifiedMeshService.delegate = null } catch (_: Exception) { }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle intents from notification clicks - open specific private chat or geohash chat
|
||
*/
|
||
private fun handleNotificationIntent(intent: Intent) {
|
||
val shouldOpenPrivateChat = intent.getBooleanExtra(
|
||
com.bitchat.android.ui.NotificationManager.EXTRA_OPEN_PRIVATE_CHAT,
|
||
false
|
||
)
|
||
|
||
val shouldOpenGeohashChat = intent.getBooleanExtra(
|
||
com.bitchat.android.ui.NotificationManager.EXTRA_OPEN_GEOHASH_CHAT,
|
||
false
|
||
)
|
||
|
||
when {
|
||
shouldOpenPrivateChat -> {
|
||
val peerID = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_PEER_ID)
|
||
val senderNickname = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_SENDER_NICKNAME)
|
||
|
||
if (peerID != null) {
|
||
Log.d("MainActivity", "Opening private chat with $senderNickname (peerID: $peerID) from notification")
|
||
|
||
// Open the private chat sheet with this peer
|
||
chatViewModel.showMeshPeerList()
|
||
chatViewModel.showPrivateChatSheet(peerID)
|
||
|
||
// Clear notifications for this sender since user is now viewing the chat
|
||
chatViewModel.clearNotificationsForSender(peerID)
|
||
}
|
||
}
|
||
|
||
shouldOpenGeohashChat -> {
|
||
val geohash = intent.getStringExtra(com.bitchat.android.ui.NotificationManager.EXTRA_GEOHASH)
|
||
|
||
if (geohash != null) {
|
||
Log.d("MainActivity", "Opening geohash chat #$geohash from notification")
|
||
|
||
// Switch to the geohash channel - create appropriate geohash channel level
|
||
val level = when (geohash.length) {
|
||
7 -> com.bitchat.android.geohash.GeohashChannelLevel.BLOCK
|
||
6 -> com.bitchat.android.geohash.GeohashChannelLevel.NEIGHBORHOOD
|
||
5 -> com.bitchat.android.geohash.GeohashChannelLevel.CITY
|
||
4 -> com.bitchat.android.geohash.GeohashChannelLevel.PROVINCE
|
||
2 -> com.bitchat.android.geohash.GeohashChannelLevel.REGION
|
||
else -> com.bitchat.android.geohash.GeohashChannelLevel.CITY // Default fallback
|
||
}
|
||
val geohashChannel = com.bitchat.android.geohash.GeohashChannel(level, geohash)
|
||
val channelId = com.bitchat.android.geohash.ChannelID.Location(geohashChannel)
|
||
chatViewModel.selectLocationChannel(channelId)
|
||
|
||
// Update current geohash state for notifications
|
||
chatViewModel.setCurrentGeohash(geohash)
|
||
|
||
// Clear notifications for this geohash since user is now viewing it
|
||
chatViewModel.clearNotificationsForGeohash(geohash)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private fun handleVerificationIntent(intent: Intent) {
|
||
val uri = intent.data ?: return
|
||
if (uri.scheme != "bitchat" || uri.host != "verify") return
|
||
|
||
chatViewModel.showVerificationSheet()
|
||
val qr = VerificationService.verifyScannedQR(uri.toString())
|
||
if (qr != null) {
|
||
chatViewModel.beginQRVerification(qr)
|
||
}
|
||
}
|
||
|
||
|
||
override fun onDestroy() {
|
||
super.onDestroy()
|
||
|
||
try { unregisterReceiver(forceFinishReceiver) } catch (_: Exception) { }
|
||
|
||
// Cleanup location status manager
|
||
try {
|
||
locationStatusManager.cleanup()
|
||
Log.d("MainActivity", "Location status manager cleaned up successfully")
|
||
} catch (e: Exception) {
|
||
Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}")
|
||
}
|
||
|
||
// Do not stop mesh here; ForegroundService owns lifecycle for background reliability
|
||
}
|
||
}
|