Merge branch 'main' into fix/wrong-cursor-location

This commit is contained in:
Faded
2025-07-18 05:19:19 +05:00
8 changed files with 391 additions and 202 deletions
@@ -1,27 +1,38 @@
package com.bitchat.android
import android.Manifest
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
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.material3.Surface
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.activity.OnBackPressedCallback
import androidx.activity.addCallback
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.Lifecycle
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.onboarding.*
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.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.theme.BitchatTheme
@@ -38,6 +49,7 @@ class MainActivity : ComponentActivity() {
// Core mesh service - managed at app level
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 {
@@ -47,29 +59,7 @@ class MainActivity : ComponentActivity() {
}
}
// UI state for onboarding flow
private var onboardingState by mutableStateOf(OnboardingState.CHECKING)
private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED)
private var locationStatus by mutableStateOf(LocationStatus.ENABLED)
private var batteryOptimizationStatus by mutableStateOf(BatteryOptimizationStatus.DISABLED)
private var errorMessage by mutableStateOf("")
private var isBluetoothLoading by mutableStateOf(false)
private var isLocationLoading by mutableStateOf(false)
private var isBatteryOptimizationLoading by mutableStateOf(false)
enum class OnboardingState {
CHECKING,
BLUETOOTH_CHECK,
LOCATION_CHECK,
BATTERY_OPTIMIZATION_CHECK,
PERMISSION_EXPLANATION,
PERMISSION_REQUESTING,
INITIALIZING,
COMPLETE,
ERROR
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -115,12 +105,33 @@ class MainActivity : ComponentActivity() {
}
}
// Start the onboarding process
checkOnboardingStatus()
// 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() {
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()
when (onboardingState) {
OnboardingState.CHECKING -> {
InitializingScreen()
@@ -130,7 +141,7 @@ class MainActivity : ComponentActivity() {
BluetoothCheckScreen(
status = bluetoothStatus,
onEnableBluetooth = {
isBluetoothLoading = true
mainViewModel.updateBluetoothLoading(true)
bluetoothStatusManager.requestEnableBluetooth()
},
onRetry = {
@@ -144,7 +155,7 @@ class MainActivity : ComponentActivity() {
LocationCheckScreen(
status = locationStatus,
onEnableLocation = {
isLocationLoading = true
mainViewModel.updateLocationLoading(true)
locationStatusManager.requestEnableLocation()
},
onRetry = {
@@ -158,7 +169,7 @@ class MainActivity : ComponentActivity() {
BatteryOptimizationScreen(
status = batteryOptimizationStatus,
onDisableBatteryOptimization = {
isBatteryOptimizationLoading = true
mainViewModel.updateBatteryOptimizationLoading(true)
batteryOptimizationManager.requestDisableBatteryOptimization()
},
onRetry = {
@@ -176,7 +187,7 @@ class MainActivity : ComponentActivity() {
PermissionExplanationScreen(
permissionCategories = permissionManager.getCategorizedPermissions(),
onContinue = {
onboardingState = OnboardingState.PERMISSION_REQUESTING
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_REQUESTING)
onboardingCoordinator.requestPermissions()
}
)
@@ -216,7 +227,7 @@ class MainActivity : ComponentActivity() {
InitializationErrorScreen(
errorMessage = errorMessage,
onRetry = {
onboardingState = OnboardingState.CHECKING
mainViewModel.updateOnboardingState(OnboardingState.CHECKING)
checkOnboardingStatus()
},
onOpenSettings = {
@@ -227,6 +238,20 @@ class MainActivity : ComponentActivity() {
}
}
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() {
android.util.Log.d("MainActivity", "Checking onboarding status")
@@ -255,9 +280,9 @@ class MainActivity : ComponentActivity() {
// For existing users, check Bluetooth status first
bluetoothStatusManager.logBluetoothStatus()
bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
when (bluetoothStatus) {
when (mainViewModel.bluetoothStatus.value) {
BluetoothStatus.ENABLED -> {
// Bluetooth is enabled, check location services next
checkLocationAndProceed()
@@ -265,14 +290,14 @@ class MainActivity : ComponentActivity() {
BluetoothStatus.DISABLED -> {
// Show Bluetooth enable screen (should have permissions as existing user)
android.util.Log.d("MainActivity", "Bluetooth disabled, showing enable screen")
onboardingState = OnboardingState.BLUETOOTH_CHECK
isBluetoothLoading = false
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
mainViewModel.updateBluetoothLoading(false)
}
BluetoothStatus.NOT_SUPPORTED -> {
// Device doesn't support Bluetooth
android.util.Log.e("MainActivity", "Bluetooth not supported")
onboardingState = OnboardingState.BLUETOOTH_CHECK
isBluetoothLoading = false
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
mainViewModel.updateBluetoothLoading(false)
}
}
}
@@ -288,14 +313,14 @@ class MainActivity : ComponentActivity() {
if (permissionManager.isFirstTimeLaunch()) {
android.util.Log.d("MainActivity", "First time launch, showing permission explanation")
onboardingState = OnboardingState.PERMISSION_EXPLANATION
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
} else if (permissionManager.areAllPermissionsGranted()) {
android.util.Log.d("MainActivity", "Existing user with permissions, initializing app")
onboardingState = OnboardingState.INITIALIZING
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
initializeApp()
} else {
android.util.Log.d("MainActivity", "Existing user missing permissions, showing explanation")
onboardingState = OnboardingState.PERMISSION_EXPLANATION
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
}
}
}
@@ -305,8 +330,8 @@ class MainActivity : ComponentActivity() {
*/
private fun handleBluetoothEnabled() {
android.util.Log.d("MainActivity", "Bluetooth enabled by user")
isBluetoothLoading = false
bluetoothStatus = BluetoothStatus.ENABLED
mainViewModel.updateBluetoothLoading(false)
mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED)
checkLocationAndProceed()
}
@@ -326,9 +351,9 @@ class MainActivity : ComponentActivity() {
// For existing users, check location status
locationStatusManager.logLocationStatus()
locationStatus = locationStatusManager.checkLocationStatus()
mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
when (locationStatus) {
when (mainViewModel.locationStatus.value) {
LocationStatus.ENABLED -> {
// Location services enabled, check battery optimization next
checkBatteryOptimizationAndProceed()
@@ -336,14 +361,14 @@ class MainActivity : ComponentActivity() {
LocationStatus.DISABLED -> {
// Show location enable screen (should have permissions as existing user)
android.util.Log.d("MainActivity", "Location services disabled, showing enable screen")
onboardingState = OnboardingState.LOCATION_CHECK
isLocationLoading = false
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
mainViewModel.updateLocationLoading(false)
}
LocationStatus.NOT_AVAILABLE -> {
// Device doesn't support location services (very unusual)
android.util.Log.e("MainActivity", "Location services not available")
onboardingState = OnboardingState.LOCATION_CHECK
isLocationLoading = false
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
mainViewModel.updateLocationLoading(false)
}
}
}
@@ -353,87 +378,28 @@ class MainActivity : ComponentActivity() {
*/
private fun handleLocationEnabled() {
android.util.Log.d("MainActivity", "Location services enabled by user")
isLocationLoading = false
locationStatus = LocationStatus.ENABLED
mainViewModel.updateLocationLoading(false)
mainViewModel.updateLocationStatus(LocationStatus.ENABLED)
checkBatteryOptimizationAndProceed()
}
/**
* 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
}
// For existing users, check battery optimization status
batteryOptimizationManager.logBatteryOptimizationStatus()
batteryOptimizationStatus = when {
!batteryOptimizationManager.isBatteryOptimizationSupported() -> BatteryOptimizationStatus.NOT_SUPPORTED
batteryOptimizationManager.isBatteryOptimizationDisabled() -> BatteryOptimizationStatus.DISABLED
else -> BatteryOptimizationStatus.ENABLED
}
when (batteryOptimizationStatus) {
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")
onboardingState = OnboardingState.BATTERY_OPTIMIZATION_CHECK
isBatteryOptimizationLoading = false
}
}
}
/**
* Handle Battery Optimization disabled callback
*/
private fun handleBatteryOptimizationDisabled() {
android.util.Log.d("MainActivity", "Battery optimization disabled by user")
isBatteryOptimizationLoading = false
batteryOptimizationStatus = BatteryOptimizationStatus.DISABLED
proceedWithPermissionCheck()
}
/**
* Handle Battery Optimization failed callback
*/
private fun handleBatteryOptimizationFailed(message: String) {
android.util.Log.w("MainActivity", "Battery optimization disable failed: $message")
isBatteryOptimizationLoading = false
// Don't treat this as a fatal error - proceed with onboarding
// User can always change this setting later
proceedWithPermissionCheck()
}
/**
* Handle Location disabled callback
*/
private fun handleLocationDisabled(message: String) {
android.util.Log.w("MainActivity", "Location services disabled or failed: $message")
isLocationLoading = false
locationStatus = locationStatusManager.checkLocationStatus()
mainViewModel.updateLocationLoading(false)
mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
when {
locationStatus == LocationStatus.NOT_AVAILABLE -> {
mainViewModel.locationStatus.value == LocationStatus.NOT_AVAILABLE -> {
// Show permanent error for devices without location services
errorMessage = message
onboardingState = OnboardingState.ERROR
mainViewModel.updateErrorMessage(message)
mainViewModel.updateOnboardingState(OnboardingState.ERROR)
}
else -> {
// Stay on location check screen for retry
onboardingState = OnboardingState.LOCATION_CHECK
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
}
}
}
@@ -443,14 +409,14 @@ class MainActivity : ComponentActivity() {
*/
private fun handleBluetoothDisabled(message: String) {
android.util.Log.w("MainActivity", "Bluetooth disabled or failed: $message")
isBluetoothLoading = false
bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
mainViewModel.updateBluetoothLoading(false)
mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
when {
bluetoothStatus == BluetoothStatus.NOT_SUPPORTED -> {
mainViewModel.bluetoothStatus.value == BluetoothStatus.NOT_SUPPORTED -> {
// Show permanent error for unsupported devices
errorMessage = message
onboardingState = OnboardingState.ERROR
mainViewModel.updateErrorMessage(message)
mainViewModel.updateOnboardingState(OnboardingState.ERROR)
}
message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> {
// During first-time onboarding, if Bluetooth enable fails due to permissions,
@@ -461,11 +427,11 @@ class MainActivity : ComponentActivity() {
message.contains("Permission") -> {
// For existing users, redirect to permission explanation to grant missing permissions
android.util.Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation")
onboardingState = OnboardingState.PERMISSION_EXPLANATION
mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
}
else -> {
// Stay on Bluetooth check screen for retry
onboardingState = OnboardingState.BLUETOOTH_CHECK
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
}
}
}
@@ -486,31 +452,28 @@ class MainActivity : ComponentActivity() {
currentBluetoothStatus != BluetoothStatus.ENABLED -> {
// Bluetooth still disabled, but now we have permissions to enable it
android.util.Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.")
bluetoothStatus = currentBluetoothStatus
onboardingState = OnboardingState.BLUETOOTH_CHECK
isBluetoothLoading = false
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
android.util.Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.")
locationStatus = currentLocationStatus
onboardingState = OnboardingState.LOCATION_CHECK
isLocationLoading = false
mainViewModel.updateLocationStatus(currentLocationStatus)
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
mainViewModel.updateLocationLoading(false)
}
currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> {
// Battery optimization is still enabled, show disable screen
android.util.Log.d("MainActivity", "Permissions granted, but Battery optimization still enabled. Showing Battery optimization disable screen.")
batteryOptimizationStatus = currentBatteryOptimizationStatus
onboardingState = OnboardingState.BATTERY_OPTIMIZATION_CHECK
isBatteryOptimizationLoading = false
// 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 -> {
// Bluetooth, Location, and Battery Optimization are all properly configured, proceed to app initialization
android.util.Log.d("MainActivity", "Bluetooth, Location, and Battery Optimization all properly configured, proceeding to initialization")
bluetoothStatus = currentBluetoothStatus
locationStatus = currentLocationStatus
batteryOptimizationStatus = currentBatteryOptimizationStatus
onboardingState = OnboardingState.INITIALIZING
// Both are enabled, proceed to app initialization
android.util.Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization")
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
initializeApp()
}
}
@@ -518,8 +481,72 @@ class MainActivity : ComponentActivity() {
private fun handleOnboardingFailed(message: String) {
android.util.Log.e("MainActivity", "Onboarding failed: $message")
errorMessage = message
onboardingState = OnboardingState.ERROR
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
}
// 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() {
@@ -554,7 +581,7 @@ class MainActivity : ComponentActivity() {
delay(500)
android.util.Log.d("MainActivity", "App initialization complete")
onboardingState = OnboardingState.COMPLETE
mainViewModel.updateOnboardingState(OnboardingState.COMPLETE)
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to initialize app", e)
@@ -566,7 +593,7 @@ class MainActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// Handle notification intents when app is already running
if (onboardingState == OnboardingState.COMPLETE) {
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
handleNotificationIntent(intent)
}
}
@@ -574,7 +601,7 @@ class MainActivity : ComponentActivity() {
override fun onResume() {
super.onResume()
// Check Bluetooth and Location status on resume and handle accordingly
if (onboardingState == OnboardingState.COMPLETE) {
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Set app foreground state
meshService.connectionManager.setAppBackgroundState(false)
chatViewModel.setAppBackgroundState(false)
@@ -583,9 +610,9 @@ class MainActivity : ComponentActivity() {
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
if (currentBluetoothStatus != BluetoothStatus.ENABLED) {
android.util.Log.w("MainActivity", "Bluetooth disabled while app was backgrounded")
bluetoothStatus = currentBluetoothStatus
onboardingState = OnboardingState.BLUETOOTH_CHECK
isBluetoothLoading = false
mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
mainViewModel.updateBluetoothLoading(false)
return
}
@@ -593,9 +620,9 @@ class MainActivity : ComponentActivity() {
val currentLocationStatus = locationStatusManager.checkLocationStatus()
if (currentLocationStatus != LocationStatus.ENABLED) {
android.util.Log.w("MainActivity", "Location services disabled while app was backgrounded")
locationStatus = currentLocationStatus
onboardingState = OnboardingState.LOCATION_CHECK
isLocationLoading = false
mainViewModel.updateLocationStatus(currentLocationStatus)
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
mainViewModel.updateLocationLoading(false)
}
}
}
@@ -603,7 +630,7 @@ class MainActivity : ComponentActivity() {
override fun onPause() {
super.onPause()
// Only set background state if app is fully initialized
if (onboardingState == OnboardingState.COMPLETE) {
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Set app background state
meshService.connectionManager.setAppBackgroundState(true)
chatViewModel.setAppBackgroundState(true)
@@ -639,7 +666,7 @@ class MainActivity : ComponentActivity() {
* Restart mesh services (for debugging/troubleshooting)
*/
fun restartMeshServices() {
if (onboardingState == OnboardingState.COMPLETE) {
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
lifecycleScope.launch {
try {
android.util.Log.d("MainActivity", "Restarting mesh services")
@@ -666,7 +693,7 @@ class MainActivity : ComponentActivity() {
}
// Stop mesh services if app was fully initialized
if (onboardingState == OnboardingState.COMPLETE) {
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
try {
meshService.stopServices()
android.util.Log.d("MainActivity", "Mesh services stopped successfully")
@@ -0,0 +1,70 @@
package com.bitchat.android
import androidx.lifecycle.ViewModel
import com.bitchat.android.onboarding.BluetoothStatus
import com.bitchat.android.onboarding.LocationStatus
import com.bitchat.android.onboarding.OnboardingState
import com.bitchat.android.onboarding.BatteryOptimizationStatus
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class MainViewModel : ViewModel() {
private val _onboardingState = MutableStateFlow(OnboardingState.CHECKING)
val onboardingState: StateFlow<OnboardingState> = _onboardingState.asStateFlow()
private val _bluetoothStatus = MutableStateFlow(BluetoothStatus.ENABLED)
val bluetoothStatus: StateFlow<BluetoothStatus> = _bluetoothStatus.asStateFlow()
private val _locationStatus = MutableStateFlow(LocationStatus.ENABLED)
val locationStatus: StateFlow<LocationStatus> = _locationStatus.asStateFlow()
private val _errorMessage = MutableStateFlow("")
val errorMessage: StateFlow<String> = _errorMessage.asStateFlow()
private val _isBluetoothLoading = MutableStateFlow(false)
val isBluetoothLoading: StateFlow<Boolean> = _isBluetoothLoading.asStateFlow()
private val _isLocationLoading = MutableStateFlow(false)
val isLocationLoading: StateFlow<Boolean> = _isLocationLoading.asStateFlow()
private val _batteryOptimizationStatus = MutableStateFlow(BatteryOptimizationStatus.ENABLED)
val batteryOptimizationStatus: StateFlow<BatteryOptimizationStatus> = _batteryOptimizationStatus.asStateFlow()
private val _isBatteryOptimizationLoading = MutableStateFlow(false)
val isBatteryOptimizationLoading: StateFlow<Boolean> = _isBatteryOptimizationLoading.asStateFlow()
// Public update functions for MainActivity
fun updateOnboardingState(state: OnboardingState) {
_onboardingState.value = state
}
fun updateBluetoothStatus(status: BluetoothStatus) {
_bluetoothStatus.value = status
}
fun updateLocationStatus(status: LocationStatus) {
_locationStatus.value = status
}
fun updateErrorMessage(message: String) {
_errorMessage.value = message
}
fun updateBluetoothLoading(loading: Boolean) {
_isBluetoothLoading.value = loading
}
fun updateLocationLoading(loading: Boolean) {
_isLocationLoading.value = loading
}
fun updateBatteryOptimizationStatus(status: BatteryOptimizationStatus) {
_batteryOptimizationStatus.value = status
}
fun updateBatteryOptimizationLoading(loading: Boolean) {
_isBatteryOptimizationLoading.value = loading
}
}
@@ -0,0 +1,57 @@
package com.bitchat.android.core.ui.utils
import androidx.compose.foundation.clickable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun Modifier.singleOrTripleClickable(
onSingleClick: () -> Unit,
onTripleClick: () -> Unit,
clickTimeThreshold: Long = 300L
): Modifier = composed {
var tapCount by remember { mutableIntStateOf(0) }
var lastTapTime by remember { mutableLongStateOf(0L) }
var singleClickJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }
val coroutineScope = rememberCoroutineScope()
this.clickable {
val currentTime = System.currentTimeMillis()
if (currentTime - lastTapTime < clickTimeThreshold) {
tapCount++
} else {
tapCount = 1
}
lastTapTime = currentTime
// Cancel any pending single click action
singleClickJob?.cancel()
singleClickJob = null
when (tapCount) {
1 -> {
// Wait to see if more taps come
singleClickJob = coroutineScope.launch {
delay(clickTimeThreshold)
if (tapCount == 1) {
onSingleClick()
}
}
}
3 -> {
// Triple click detected - execute immediately
onTripleClick()
tapCount = 0
}
}
// Reset after threshold if no triple click
if (tapCount > 3) {
tapCount = 0
}
}
}
@@ -11,10 +11,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.bitchat.android.R
/**
* Screen shown when checking battery optimization status or requesting battery optimization disable
@@ -96,7 +98,7 @@ private fun BatteryOptimizationEnabledContent(
)
Text(
text = "Battery Optimization Detected",
text = stringResource(R.string.battery_optimization_detected),
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onSurface
@@ -105,7 +107,7 @@ private fun BatteryOptimizationEnabledContent(
)
Text(
text = "bitchat needs to run in the background to maintain mesh network connections and relay messages for other users.",
text = stringResource(R.string.battery_optimization_explanation),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
),
@@ -123,7 +125,7 @@ private fun BatteryOptimizationEnabledContent(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "Why disable battery optimization?",
text = stringResource(R.string.battery_optimization_why_disable),
style = MaterialTheme.typography.titleSmall.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onSurface
@@ -131,7 +133,7 @@ private fun BatteryOptimizationEnabledContent(
)
Text(
text = "• Ensures reliable message delivery\n• Maintains mesh network connectivity\n• Allows background message relay\n• Prevents connection drops",
text = stringResource(R.string.battery_optimization_benefits),
style = MaterialTheme.typography.bodyMedium.copy(
color = colorScheme.onSurfaceVariant
)
@@ -154,7 +156,7 @@ private fun BatteryOptimizationEnabledContent(
)
Spacer(modifier = Modifier.width(8.dp))
}
Text("Disable Battery Optimization")
Text(stringResource(R.string.battery_optimization_disable_button))
}
Row(
@@ -166,7 +168,7 @@ private fun BatteryOptimizationEnabledContent(
modifier = Modifier.weight(1f),
enabled = !isLoading
) {
Text("Check Again")
Text(stringResource(R.string.battery_optimization_check_again))
}
TextButton(
@@ -174,12 +176,12 @@ private fun BatteryOptimizationEnabledContent(
modifier = Modifier.weight(1f),
enabled = !isLoading
) {
Text("Skip for Now")
Text(stringResource(R.string.battery_optimization_skip))
}
}
Text(
text = "Note: You can change this setting later in Android Settings > Apps > bitchat > Battery",
text = stringResource(R.string.battery_optimization_note),
style = MaterialTheme.typography.bodySmall.copy(
color = colorScheme.onSurfaceVariant
),
@@ -226,7 +228,7 @@ private fun BatteryOptimizationCheckingContent(
)
Text(
text = "Battery Optimization Disabled",
text = stringResource(R.string.battery_optimization_disabled),
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onSurface
@@ -235,7 +237,7 @@ private fun BatteryOptimizationCheckingContent(
)
Text(
text = "bitchat can run reliably in the background",
text = stringResource(R.string.battery_optimization_success_message),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
),
@@ -270,7 +272,7 @@ private fun BatteryOptimizationNotSupportedContent(
)
Text(
text = "Battery Optimization Not Required",
text = stringResource(R.string.battery_optimization_not_required),
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onSurface
@@ -279,7 +281,7 @@ private fun BatteryOptimizationNotSupportedContent(
)
Text(
text = "Your device doesn't require battery optimization settings. bitchat will run normally.",
text = stringResource(R.string.battery_optimization_not_supported_message),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
),
@@ -290,7 +292,7 @@ private fun BatteryOptimizationNotSupportedContent(
onClick = onRetry,
modifier = Modifier.fillMaxWidth()
) {
Text("Continue")
Text(stringResource(R.string.battery_optimization_continue))
}
}
}
@@ -0,0 +1,13 @@
package com.bitchat.android.onboarding
enum class OnboardingState {
CHECKING,
BLUETOOTH_CHECK,
LOCATION_CHECK,
BATTERY_OPTIMIZATION_CHECK,
PERMISSION_EXPLANATION,
PERMISSION_REQUESTING,
INITIALIZING,
COMPLETE,
ERROR
}
@@ -22,6 +22,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bitchat.android.core.ui.utils.singleOrTripleClickable
/**
* Header components for ChatScreen
@@ -148,8 +149,7 @@ fun ChatHeaderContent(
onShowAppInfo: () -> Unit
) {
val colorScheme = MaterialTheme.colorScheme
var tripleClickCount by remember { mutableStateOf(0) }
when {
selectedPrivatePeer != null -> {
// Private chat header - ensure state synchronization
@@ -181,15 +181,8 @@ fun ChatHeaderContent(
MainHeader(
nickname = nickname,
onNicknameChange = viewModel::setNickname,
onTitleClick = {
tripleClickCount++
if (tripleClickCount >= 3) {
tripleClickCount = 0
onTripleClick()
} else {
onShowAppInfo()
}
},
onTitleClick = onShowAppInfo,
onTripleTitleClick = onTripleClick,
onSidebarClick = onSidebarClick,
viewModel = viewModel
)
@@ -345,6 +338,7 @@ private fun MainHeader(
nickname: String,
onNicknameChange: (String) -> Unit,
onTitleClick: () -> Unit,
onTripleTitleClick: () -> Unit,
onSidebarClick: () -> Unit,
viewModel: ChatViewModel
) {
@@ -360,12 +354,18 @@ private fun MainHeader(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "bitchat*",
style = MaterialTheme.typography.headlineSmall,
color = colorScheme.primary,
modifier = Modifier.clickable { onTitleClick() }
modifier = Modifier.singleOrTripleClickable(
onSingleClick = onTitleClick,
onTripleClick = onTripleTitleClick
)
)
Spacer(modifier = Modifier.width(8.dp))
@@ -1,8 +1,7 @@
package com.bitchat.android.ui
import com.bitchat.android.R
import android.util.Log
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -16,11 +15,12 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
/**
* Sidebar components for ChatScreen
@@ -140,7 +140,7 @@ private fun SidebarHeader() {
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "YOUR NETWORK",
text = stringResource(id = R.string.your_network).uppercase(),
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
@@ -175,7 +175,7 @@ fun ChannelsSection(
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "CHANNELS",
text = stringResource(id = R.string.channels).uppercase(),
style = MaterialTheme.typography.labelSmall,
color = colorScheme.onSurface.copy(alpha = 0.6f),
fontWeight = FontWeight.Bold
@@ -255,7 +255,7 @@ fun PeopleSection(
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = "PEOPLE",
text = stringResource(id = R.string.people).uppercase(),
style = MaterialTheme.typography.labelSmall,
color = colorScheme.onSurface.copy(alpha = 0.6f),
fontWeight = FontWeight.Bold
@@ -264,7 +264,7 @@ fun PeopleSection(
if (connectedPeers.isEmpty()) {
Text(
text = "No one connected",
text = stringResource(id = R.string.no_one_connected),
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.onSurface.copy(alpha = 0.5f),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)