mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 04:45:21 +00:00
Merge branch 'main' into code_readiblity_improvement
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user