mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 12:25:20 +00:00
* Automated update of relay data - Sun Sep 21 06:21:05 UTC 2025 * Automated update of relay data - Sun Sep 28 06:20:40 UTC 2025 * refactor: new close button like ios(but not liquid glass) * Automated update of relay data - Sun Oct 5 06:20:09 UTC 2025 * Automated update of relay data - Sun Oct 12 06:20:12 UTC 2025 * Automated update of relay data - Sun Oct 19 06:21:51 UTC 2025 * Automated update of relay data - Sun Oct 26 06:21:31 UTC 2025 * Automated update of relay data - Sun Nov 2 06:22:16 UTC 2025 * Automated update of relay data - Sun Nov 9 06:21:43 UTC 2025 * Automated update of relay data - Sun Nov 16 06:22:37 UTC 2025 * Automated update of relay data - Sun Nov 23 06:22:51 UTC 2025 * Automated update of relay data - Sun Nov 30 06:24:08 UTC 2025 * Automated update of relay data - Sun Dec 7 06:22:59 UTC 2025 * Automated update of relay data - Sun Dec 14 06:24:33 UTC 2025 * Automated update of relay data - Sun Dec 21 06:24:49 UTC 2025 * Automated update of relay data - Sun Dec 28 06:25:38 UTC 2025 * feat: Add ZXing dependency for QR code scanning * feat: Request camera permission for QR verification * Add QR verification payloads and mesh wiring * Wire verification state, system messages, and notifications * Add verification sheets and UI affordances * Show verified badges in sidebar and add strings * Persist fingerprint caches for offline verification * Handle bitchat://verify deep links * feat: Replace zxing-android-embedded with ML Kit and CameraX * Refactor(Verification): Replace zxing with MLKit for QR scanning * Replace `AndroidView` with `CameraXViewfinder` for camera preview * Refactor QR verification: Extract VerificationHandler and fix concurrency issues * Extract and translate strings for QR verification feature * Fix build errors: Escape ampersands in strings and restore missing methods in ChatViewModel * return to main * return to main 2 --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
822 lines
37 KiB
Kotlin
822 lines
37 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.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.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.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 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) 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)
|
|
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,
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
},
|
|
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.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")
|
|
|
|
// 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())
|
|
|
|
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.areAllPermissionsGranted()) {
|
|
Log.d("MainActivity", "Existing user with permissions, initializing app")
|
|
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)
|
|
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
|
|
}
|
|
|
|
when {
|
|
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 mesh service delegate and start services
|
|
meshService.delegate = chatViewModel
|
|
meshService.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 { meshService.delegate = chatViewModel } catch (_: Exception) { }
|
|
// Set app foreground state
|
|
meshService.connectionManager.setAppBackgroundState(false)
|
|
chatViewModel.setAppBackgroundState(false)
|
|
|
|
// Check if Bluetooth was disabled while app was backgrounded
|
|
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
|
if (currentBluetoothStatus != BluetoothStatus.ENABLED) {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
// Only set background state if app is fully initialized
|
|
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
|
// Set app background state
|
|
meshService.connectionManager.setAppBackgroundState(true)
|
|
chatViewModel.setAppBackgroundState(true)
|
|
// Detach UI delegate so the foreground service can own DM notifications while UI is closed
|
|
try { meshService.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 with this peer
|
|
chatViewModel.startPrivateChat(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
|
|
}
|
|
}
|