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 create(modelClass: Class): 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 } }