Merge branch 'main' into code_readiblity_improvement

This commit is contained in:
ganpat singh
2025-07-18 10:57:13 +05:30
committed by GitHub
7 changed files with 264 additions and 118 deletions
@@ -1,28 +1,36 @@
package com.bitchat.android package com.bitchat.android
import android.Manifest
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity 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.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.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.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.ChatScreen
import com.bitchat.android.ui.ChatViewModel import com.bitchat.android.ui.ChatViewModel
import com.bitchat.android.ui.theme.BitchatTheme import com.bitchat.android.ui.theme.BitchatTheme
@@ -38,6 +46,7 @@ class MainActivity : ComponentActivity() {
// Core mesh service - managed at app level // Core mesh service - managed at app level
private lateinit var meshService: BluetoothMeshService private lateinit var meshService: BluetoothMeshService
private val mainViewModel: MainViewModel by viewModels()
private val chatViewModel: ChatViewModel by viewModels { private val chatViewModel: ChatViewModel by viewModels {
object : ViewModelProvider.Factory { object : ViewModelProvider.Factory {
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T { override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
@@ -47,25 +56,6 @@ 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 errorMessage by mutableStateOf("")
private var isBluetoothLoading by mutableStateOf(false)
private var isLocationLoading by mutableStateOf(false)
enum class OnboardingState {
CHECKING,
BLUETOOTH_CHECK,
LOCATION_CHECK,
PERMISSION_EXPLANATION,
PERMISSION_REQUESTING,
INITIALIZING,
COMPLETE,
ERROR
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -104,12 +94,31 @@ class MainActivity : ComponentActivity() {
} }
} }
// Start the onboarding process // Collect state changes in a lifecycle-aware manner
checkOnboardingStatus() 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 @Composable
private fun OnboardingFlowScreen() { private fun OnboardingFlowScreen() {
val onboardingState by mainViewModel.onboardingState.collectAsState()
val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState()
val locationStatus by mainViewModel.locationStatus.collectAsState()
val errorMessage by mainViewModel.errorMessage.collectAsState()
val isBluetoothLoading by mainViewModel.isBluetoothLoading.collectAsState()
val isLocationLoading by mainViewModel.isLocationLoading.collectAsState()
when (onboardingState) { when (onboardingState) {
OnboardingState.CHECKING -> { OnboardingState.CHECKING -> {
InitializingScreen() InitializingScreen()
@@ -119,7 +128,7 @@ class MainActivity : ComponentActivity() {
BluetoothCheckScreen( BluetoothCheckScreen(
status = bluetoothStatus, status = bluetoothStatus,
onEnableBluetooth = { onEnableBluetooth = {
isBluetoothLoading = true mainViewModel.updateBluetoothLoading(true)
bluetoothStatusManager.requestEnableBluetooth() bluetoothStatusManager.requestEnableBluetooth()
}, },
onRetry = { onRetry = {
@@ -133,7 +142,7 @@ class MainActivity : ComponentActivity() {
LocationCheckScreen( LocationCheckScreen(
status = locationStatus, status = locationStatus,
onEnableLocation = { onEnableLocation = {
isLocationLoading = true mainViewModel.updateLocationLoading(true)
locationStatusManager.requestEnableLocation() locationStatusManager.requestEnableLocation()
}, },
onRetry = { onRetry = {
@@ -147,7 +156,7 @@ class MainActivity : ComponentActivity() {
PermissionExplanationScreen( PermissionExplanationScreen(
permissionCategories = permissionManager.getCategorizedPermissions(), permissionCategories = permissionManager.getCategorizedPermissions(),
onContinue = { onContinue = {
onboardingState = OnboardingState.PERMISSION_REQUESTING mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_REQUESTING)
onboardingCoordinator.requestPermissions() onboardingCoordinator.requestPermissions()
} }
) )
@@ -187,7 +196,7 @@ class MainActivity : ComponentActivity() {
InitializationErrorScreen( InitializationErrorScreen(
errorMessage = errorMessage, errorMessage = errorMessage,
onRetry = { onRetry = {
onboardingState = OnboardingState.CHECKING mainViewModel.updateOnboardingState(OnboardingState.CHECKING)
checkOnboardingStatus() checkOnboardingStatus()
}, },
onOpenSettings = { onOpenSettings = {
@@ -198,6 +207,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() { private fun checkOnboardingStatus() {
Log.d("MainActivity", "Checking onboarding status") Log.d("MainActivity", "Checking onboarding status")
@@ -226,9 +249,9 @@ class MainActivity : ComponentActivity() {
// For existing users, check Bluetooth status first // For existing users, check Bluetooth status first
bluetoothStatusManager.logBluetoothStatus() bluetoothStatusManager.logBluetoothStatus()
bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
when (bluetoothStatus) { when (mainViewModel.bluetoothStatus.value) {
BluetoothStatus.ENABLED -> { BluetoothStatus.ENABLED -> {
// Bluetooth is enabled, check location services next // Bluetooth is enabled, check location services next
checkLocationAndProceed() checkLocationAndProceed()
@@ -236,14 +259,14 @@ class MainActivity : ComponentActivity() {
BluetoothStatus.DISABLED -> { BluetoothStatus.DISABLED -> {
// Show Bluetooth enable screen (should have permissions as existing user) // Show Bluetooth enable screen (should have permissions as existing user)
Log.d("MainActivity", "Bluetooth disabled, showing enable screen") Log.d("MainActivity", "Bluetooth disabled, showing enable screen")
onboardingState = OnboardingState.BLUETOOTH_CHECK mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
isBluetoothLoading = false mainViewModel.updateBluetoothLoading(false)
} }
BluetoothStatus.NOT_SUPPORTED -> { BluetoothStatus.NOT_SUPPORTED -> {
// Device doesn't support Bluetooth // Device doesn't support Bluetooth
Log.e("MainActivity", "Bluetooth not supported") android.util.Log.e("MainActivity", "Bluetooth not supported")
onboardingState = OnboardingState.BLUETOOTH_CHECK mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
isBluetoothLoading = false mainViewModel.updateBluetoothLoading(false)
} }
} }
} }
@@ -259,14 +282,14 @@ class MainActivity : ComponentActivity() {
if (permissionManager.isFirstTimeLaunch()) { if (permissionManager.isFirstTimeLaunch()) {
Log.d("MainActivity", "First time launch, showing permission explanation") Log.d("MainActivity", "First time launch, showing permission explanation")
onboardingState = OnboardingState.PERMISSION_EXPLANATION mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
} else if (permissionManager.areAllPermissionsGranted()) { } else if (permissionManager.areAllPermissionsGranted()) {
Log.d("MainActivity", "Existing user with permissions, initializing app") Log.d("MainActivity", "Existing user with permissions, initializing app")
onboardingState = OnboardingState.INITIALIZING mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
initializeApp() initializeApp()
} else { } else {
Log.d("MainActivity", "Existing user missing permissions, showing explanation") Log.d("MainActivity", "Existing user missing permissions, showing explanation")
onboardingState = OnboardingState.PERMISSION_EXPLANATION mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
} }
} }
} }
@@ -276,8 +299,8 @@ class MainActivity : ComponentActivity() {
*/ */
private fun handleBluetoothEnabled() { private fun handleBluetoothEnabled() {
Log.d("MainActivity", "Bluetooth enabled by user") Log.d("MainActivity", "Bluetooth enabled by user")
isBluetoothLoading = false mainViewModel.updateBluetoothLoading(false)
bluetoothStatus = BluetoothStatus.ENABLED mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED)
checkLocationAndProceed() checkLocationAndProceed()
} }
@@ -297,9 +320,9 @@ class MainActivity : ComponentActivity() {
// For existing users, check location status // For existing users, check location status
locationStatusManager.logLocationStatus() locationStatusManager.logLocationStatus()
locationStatus = locationStatusManager.checkLocationStatus() mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
when (locationStatus) { when (mainViewModel.locationStatus.value) {
LocationStatus.ENABLED -> { LocationStatus.ENABLED -> {
// Location services enabled, proceed with permission/onboarding check // Location services enabled, proceed with permission/onboarding check
proceedWithPermissionCheck() proceedWithPermissionCheck()
@@ -307,14 +330,14 @@ class MainActivity : ComponentActivity() {
LocationStatus.DISABLED -> { LocationStatus.DISABLED -> {
// Show location enable screen (should have permissions as existing user) // Show location enable screen (should have permissions as existing user)
Log.d("MainActivity", "Location services disabled, showing enable screen") Log.d("MainActivity", "Location services disabled, showing enable screen")
onboardingState = OnboardingState.LOCATION_CHECK mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
isLocationLoading = false mainViewModel.updateLocationLoading(false)
} }
LocationStatus.NOT_AVAILABLE -> { LocationStatus.NOT_AVAILABLE -> {
// Device doesn't support location services (very unusual) // Device doesn't support location services (very unusual)
Log.e("MainActivity", "Location services not available") Log.e("MainActivity", "Location services not available")
onboardingState = OnboardingState.LOCATION_CHECK mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
isLocationLoading = false mainViewModel.updateLocationLoading(false)
} }
} }
} }
@@ -324,8 +347,8 @@ class MainActivity : ComponentActivity() {
*/ */
private fun handleLocationEnabled() { private fun handleLocationEnabled() {
Log.d("MainActivity", "Location services enabled by user") Log.d("MainActivity", "Location services enabled by user")
isLocationLoading = false mainViewModel.updateLocationLoading(false)
locationStatus = LocationStatus.ENABLED mainViewModel.updateLocationStatus(LocationStatus.ENABLED)
proceedWithPermissionCheck() proceedWithPermissionCheck()
} }
@@ -334,18 +357,18 @@ class MainActivity : ComponentActivity() {
*/ */
private fun handleLocationDisabled(message: String) { private fun handleLocationDisabled(message: String) {
Log.w("MainActivity", "Location services disabled or failed: $message") Log.w("MainActivity", "Location services disabled or failed: $message")
isLocationLoading = false mainViewModel.updateLocationLoading(false)
locationStatus = locationStatusManager.checkLocationStatus() mainViewModel.updateLocationStatus(locationStatusManager.checkLocationStatus())
when { when {
locationStatus == LocationStatus.NOT_AVAILABLE -> { mainViewModel.locationStatus.value == LocationStatus.NOT_AVAILABLE -> {
// Show permanent error for devices without location services // Show permanent error for devices without location services
errorMessage = message mainViewModel.updateErrorMessage(message)
onboardingState = OnboardingState.ERROR mainViewModel.updateOnboardingState(OnboardingState.ERROR)
} }
else -> { else -> {
// Stay on location check screen for retry // Stay on location check screen for retry
onboardingState = OnboardingState.LOCATION_CHECK mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
} }
} }
} }
@@ -355,14 +378,14 @@ class MainActivity : ComponentActivity() {
*/ */
private fun handleBluetoothDisabled(message: String) { private fun handleBluetoothDisabled(message: String) {
Log.w("MainActivity", "Bluetooth disabled or failed: $message") Log.w("MainActivity", "Bluetooth disabled or failed: $message")
isBluetoothLoading = false mainViewModel.updateBluetoothLoading(false)
bluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() mainViewModel.updateBluetoothStatus(bluetoothStatusManager.checkBluetoothStatus())
when { when {
bluetoothStatus == BluetoothStatus.NOT_SUPPORTED -> { mainViewModel.bluetoothStatus.value == BluetoothStatus.NOT_SUPPORTED -> {
// Show permanent error for unsupported devices // Show permanent error for unsupported devices
errorMessage = message mainViewModel.updateErrorMessage(message)
onboardingState = OnboardingState.ERROR mainViewModel.updateOnboardingState(OnboardingState.ERROR)
} }
message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> { message.contains("Permission") && permissionManager.isFirstTimeLaunch() -> {
// During first-time onboarding, if Bluetooth enable fails due to permissions, // During first-time onboarding, if Bluetooth enable fails due to permissions,
@@ -373,11 +396,11 @@ class MainActivity : ComponentActivity() {
message.contains("Permission") -> { message.contains("Permission") -> {
// For existing users, redirect to permission explanation to grant missing permissions // For existing users, redirect to permission explanation to grant missing permissions
Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation") Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation")
onboardingState = OnboardingState.PERMISSION_EXPLANATION mainViewModel.updateOnboardingState(OnboardingState.PERMISSION_EXPLANATION)
} }
else -> { else -> {
// Stay on Bluetooth check screen for retry // Stay on Bluetooth check screen for retry
onboardingState = OnboardingState.BLUETOOTH_CHECK mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
} }
} }
} }
@@ -393,21 +416,21 @@ class MainActivity : ComponentActivity() {
currentBluetoothStatus != BluetoothStatus.ENABLED -> { currentBluetoothStatus != BluetoothStatus.ENABLED -> {
// Bluetooth still disabled, but now we have permissions to enable it // Bluetooth still disabled, but now we have permissions to enable it
Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.") Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.")
bluetoothStatus = currentBluetoothStatus mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
onboardingState = OnboardingState.BLUETOOTH_CHECK mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
isBluetoothLoading = false mainViewModel.updateBluetoothLoading(false)
} }
currentLocationStatus != LocationStatus.ENABLED -> { currentLocationStatus != LocationStatus.ENABLED -> {
// Location services still disabled, but now we have permissions to enable it // 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.") Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.")
locationStatus = currentLocationStatus mainViewModel.updateLocationStatus(currentLocationStatus)
onboardingState = OnboardingState.LOCATION_CHECK mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
isLocationLoading = false mainViewModel.updateLocationLoading(false)
} }
else -> { else -> {
// Both are enabled, proceed to app initialization // Both are enabled, proceed to app initialization
Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization") Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization")
onboardingState = OnboardingState.INITIALIZING mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
initializeApp() initializeApp()
} }
} }
@@ -415,8 +438,8 @@ class MainActivity : ComponentActivity() {
private fun handleOnboardingFailed(message: String) { private fun handleOnboardingFailed(message: String) {
Log.e("MainActivity", "Onboarding failed: $message") Log.e("MainActivity", "Onboarding failed: $message")
errorMessage = message mainViewModel.updateErrorMessage(message)
onboardingState = OnboardingState.ERROR mainViewModel.updateOnboardingState(OnboardingState.ERROR)
} }
private fun initializeApp() { private fun initializeApp() {
@@ -449,10 +472,8 @@ class MainActivity : ComponentActivity() {
// Small delay to ensure mesh service is fully initialized // Small delay to ensure mesh service is fully initialized
delay(500) delay(500)
Log.d("MainActivity", "App initialization complete") Log.d("MainActivity", "App initialization complete")
onboardingState = OnboardingState.COMPLETE mainViewModel.updateOnboardingState(OnboardingState.COMPLETE)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MainActivity", "Failed to initialize app", e) Log.e("MainActivity", "Failed to initialize app", e)
handleOnboardingFailed("Failed to initialize the app: ${e.message}") handleOnboardingFailed("Failed to initialize the app: ${e.message}")
@@ -463,7 +484,7 @@ class MainActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
// Handle notification intents when app is already running // Handle notification intents when app is already running
if (onboardingState == OnboardingState.COMPLETE) { if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
handleNotificationIntent(intent) handleNotificationIntent(intent)
} }
} }
@@ -471,7 +492,7 @@ class MainActivity : ComponentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Check Bluetooth and Location status on resume and handle accordingly // Check Bluetooth and Location status on resume and handle accordingly
if (onboardingState == OnboardingState.COMPLETE) { if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Set app foreground state // Set app foreground state
meshService.connectionManager.setAppBackgroundState(false) meshService.connectionManager.setAppBackgroundState(false)
chatViewModel.setAppBackgroundState(false) chatViewModel.setAppBackgroundState(false)
@@ -480,9 +501,9 @@ class MainActivity : ComponentActivity() {
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
if (currentBluetoothStatus != BluetoothStatus.ENABLED) { if (currentBluetoothStatus != BluetoothStatus.ENABLED) {
Log.w("MainActivity", "Bluetooth disabled while app was backgrounded") Log.w("MainActivity", "Bluetooth disabled while app was backgrounded")
bluetoothStatus = currentBluetoothStatus mainViewModel.updateBluetoothStatus(currentBluetoothStatus)
onboardingState = OnboardingState.BLUETOOTH_CHECK mainViewModel.updateOnboardingState(OnboardingState.BLUETOOTH_CHECK)
isBluetoothLoading = false mainViewModel.updateBluetoothLoading(false)
return return
} }
@@ -490,9 +511,9 @@ class MainActivity : ComponentActivity() {
val currentLocationStatus = locationStatusManager.checkLocationStatus() val currentLocationStatus = locationStatusManager.checkLocationStatus()
if (currentLocationStatus != LocationStatus.ENABLED) { if (currentLocationStatus != LocationStatus.ENABLED) {
Log.w("MainActivity", "Location services disabled while app was backgrounded") Log.w("MainActivity", "Location services disabled while app was backgrounded")
locationStatus = currentLocationStatus mainViewModel.updateLocationStatus(currentLocationStatus)
onboardingState = OnboardingState.LOCATION_CHECK mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
isLocationLoading = false mainViewModel.updateLocationLoading(false)
} }
} }
} }
@@ -500,7 +521,7 @@ class MainActivity : ComponentActivity() {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
// Only set background state if app is fully initialized // Only set background state if app is fully initialized
if (onboardingState == OnboardingState.COMPLETE) { if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
// Set app background state // Set app background state
meshService.connectionManager.setAppBackgroundState(true) meshService.connectionManager.setAppBackgroundState(true)
chatViewModel.setAppBackgroundState(true) chatViewModel.setAppBackgroundState(true)
@@ -544,7 +565,7 @@ class MainActivity : ComponentActivity() {
} }
// Stop mesh services if app was fully initialized // Stop mesh services if app was fully initialized
if (onboardingState == OnboardingState.COMPLETE) { if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
try { try {
meshService.stopServices() meshService.stopServices()
Log.d("MainActivity", "Mesh services stopped successfully") Log.d("MainActivity", "Mesh services stopped successfully")
@@ -0,0 +1,55 @@
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 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()
// 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
}
}
@@ -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
}
}
}
@@ -0,0 +1,12 @@
package com.bitchat.android.onboarding
enum class OnboardingState {
CHECKING,
BLUETOOTH_CHECK,
LOCATION_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.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.bitchat.android.core.ui.utils.singleOrTripleClickable
/** /**
* Header components for ChatScreen * Header components for ChatScreen
@@ -148,8 +149,7 @@ fun ChatHeaderContent(
onShowAppInfo: () -> Unit onShowAppInfo: () -> Unit
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
var tripleClickCount by remember { mutableStateOf(0) }
when { when {
selectedPrivatePeer != null -> { selectedPrivatePeer != null -> {
// Private chat header - ensure state synchronization // Private chat header - ensure state synchronization
@@ -181,15 +181,8 @@ fun ChatHeaderContent(
MainHeader( MainHeader(
nickname = nickname, nickname = nickname,
onNicknameChange = viewModel::setNickname, onNicknameChange = viewModel::setNickname,
onTitleClick = { onTitleClick = onShowAppInfo,
tripleClickCount++ onTripleTitleClick = onTripleClick,
if (tripleClickCount >= 3) {
tripleClickCount = 0
onTripleClick()
} else {
onShowAppInfo()
}
},
onSidebarClick = onSidebarClick, onSidebarClick = onSidebarClick,
viewModel = viewModel viewModel = viewModel
) )
@@ -345,6 +338,7 @@ private fun MainHeader(
nickname: String, nickname: String,
onNicknameChange: (String) -> Unit, onNicknameChange: (String) -> Unit,
onTitleClick: () -> Unit, onTitleClick: () -> Unit,
onTripleTitleClick: () -> Unit,
onSidebarClick: () -> Unit, onSidebarClick: () -> Unit,
viewModel: ChatViewModel viewModel: ChatViewModel
) { ) {
@@ -360,12 +354,18 @@ private fun MainHeader(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
Text( Text(
text = "bitchat*", text = "bitchat*",
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = colorScheme.primary, color = colorScheme.primary,
modifier = Modifier.clickable { onTitleClick() } modifier = Modifier.singleOrTripleClickable(
onSingleClick = onTitleClick,
onTripleClick = onTripleTitleClick
)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
@@ -1,8 +1,7 @@
package com.bitchat.android.ui package com.bitchat.android.ui
import com.bitchat.android.R
import android.util.Log import android.util.Log
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
/** /**
* Sidebar components for ChatScreen * Sidebar components for ChatScreen
@@ -140,7 +140,7 @@ private fun SidebarHeader() {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = "YOUR NETWORK", text = stringResource(id = R.string.your_network).uppercase(),
style = MaterialTheme.typography.titleMedium.copy( style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace
@@ -175,7 +175,7 @@ fun ChannelsSection(
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Text( Text(
text = "CHANNELS", text = stringResource(id = R.string.channels).uppercase(),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = colorScheme.onSurface.copy(alpha = 0.6f), color = colorScheme.onSurface.copy(alpha = 0.6f),
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@@ -255,7 +255,7 @@ fun PeopleSection(
) )
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
Text( Text(
text = "PEOPLE", text = stringResource(id = R.string.people).uppercase(),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = colorScheme.onSurface.copy(alpha = 0.6f), color = colorScheme.onSurface.copy(alpha = 0.6f),
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@@ -264,7 +264,7 @@ fun PeopleSection(
if (connectedPeers.isEmpty()) { if (connectedPeers.isEmpty()) {
Text( Text(
text = "No one connected", text = stringResource(id = R.string.no_one_connected),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = colorScheme.onSurface.copy(alpha = 0.5f), color = colorScheme.onSurface.copy(alpha = 0.5f),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
+1
View File
@@ -16,4 +16,5 @@
<string name="online_users">Online Users</string> <string name="online_users">Online Users</string>
<string name="no_one_connected">No one connected</string> <string name="no_one_connected">No one connected</string>
<string name="emergency_clear_hint">Triple tap to clear all data</string> <string name="emergency_clear_hint">Triple tap to clear all data</string>
<string name="your_network">Your Network</string>
</resources> </resources>