mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 04:45:21 +00:00
Merge branch 'main' into fix/wrong-cursor-location
This commit is contained in:
@@ -1,27 +1,38 @@
|
|||||||
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 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.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.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 +49,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,28 +59,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 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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -115,12 +105,33 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the onboarding process
|
// 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()
|
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 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) {
|
when (onboardingState) {
|
||||||
OnboardingState.CHECKING -> {
|
OnboardingState.CHECKING -> {
|
||||||
InitializingScreen()
|
InitializingScreen()
|
||||||
@@ -130,7 +141,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
BluetoothCheckScreen(
|
BluetoothCheckScreen(
|
||||||
status = bluetoothStatus,
|
status = bluetoothStatus,
|
||||||
onEnableBluetooth = {
|
onEnableBluetooth = {
|
||||||
isBluetoothLoading = true
|
mainViewModel.updateBluetoothLoading(true)
|
||||||
bluetoothStatusManager.requestEnableBluetooth()
|
bluetoothStatusManager.requestEnableBluetooth()
|
||||||
},
|
},
|
||||||
onRetry = {
|
onRetry = {
|
||||||
@@ -144,7 +155,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
LocationCheckScreen(
|
LocationCheckScreen(
|
||||||
status = locationStatus,
|
status = locationStatus,
|
||||||
onEnableLocation = {
|
onEnableLocation = {
|
||||||
isLocationLoading = true
|
mainViewModel.updateLocationLoading(true)
|
||||||
locationStatusManager.requestEnableLocation()
|
locationStatusManager.requestEnableLocation()
|
||||||
},
|
},
|
||||||
onRetry = {
|
onRetry = {
|
||||||
@@ -158,7 +169,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
BatteryOptimizationScreen(
|
BatteryOptimizationScreen(
|
||||||
status = batteryOptimizationStatus,
|
status = batteryOptimizationStatus,
|
||||||
onDisableBatteryOptimization = {
|
onDisableBatteryOptimization = {
|
||||||
isBatteryOptimizationLoading = true
|
mainViewModel.updateBatteryOptimizationLoading(true)
|
||||||
batteryOptimizationManager.requestDisableBatteryOptimization()
|
batteryOptimizationManager.requestDisableBatteryOptimization()
|
||||||
},
|
},
|
||||||
onRetry = {
|
onRetry = {
|
||||||
@@ -176,7 +187,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()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -216,7 +227,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
InitializationErrorScreen(
|
InitializationErrorScreen(
|
||||||
errorMessage = errorMessage,
|
errorMessage = errorMessage,
|
||||||
onRetry = {
|
onRetry = {
|
||||||
onboardingState = OnboardingState.CHECKING
|
mainViewModel.updateOnboardingState(OnboardingState.CHECKING)
|
||||||
checkOnboardingStatus()
|
checkOnboardingStatus()
|
||||||
},
|
},
|
||||||
onOpenSettings = {
|
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() {
|
private fun checkOnboardingStatus() {
|
||||||
android.util.Log.d("MainActivity", "Checking onboarding status")
|
android.util.Log.d("MainActivity", "Checking onboarding status")
|
||||||
|
|
||||||
@@ -255,9 +280,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()
|
||||||
@@ -265,14 +290,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)
|
||||||
android.util.Log.d("MainActivity", "Bluetooth disabled, showing enable screen")
|
android.util.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
|
||||||
android.util.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,14 +313,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
if (permissionManager.isFirstTimeLaunch()) {
|
if (permissionManager.isFirstTimeLaunch()) {
|
||||||
android.util.Log.d("MainActivity", "First time launch, showing permission explanation")
|
android.util.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()) {
|
||||||
android.util.Log.d("MainActivity", "Existing user with permissions, initializing app")
|
android.util.Log.d("MainActivity", "Existing user with permissions, initializing app")
|
||||||
onboardingState = OnboardingState.INITIALIZING
|
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
|
||||||
initializeApp()
|
initializeApp()
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.d("MainActivity", "Existing user missing permissions, showing explanation")
|
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() {
|
private fun handleBluetoothEnabled() {
|
||||||
android.util.Log.d("MainActivity", "Bluetooth enabled by user")
|
android.util.Log.d("MainActivity", "Bluetooth enabled by user")
|
||||||
isBluetoothLoading = false
|
mainViewModel.updateBluetoothLoading(false)
|
||||||
bluetoothStatus = BluetoothStatus.ENABLED
|
mainViewModel.updateBluetoothStatus(BluetoothStatus.ENABLED)
|
||||||
checkLocationAndProceed()
|
checkLocationAndProceed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,9 +351,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, check battery optimization next
|
// Location services enabled, check battery optimization next
|
||||||
checkBatteryOptimizationAndProceed()
|
checkBatteryOptimizationAndProceed()
|
||||||
@@ -336,14 +361,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)
|
||||||
android.util.Log.d("MainActivity", "Location services disabled, showing enable screen")
|
android.util.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)
|
||||||
android.util.Log.e("MainActivity", "Location services not available")
|
android.util.Log.e("MainActivity", "Location services not available")
|
||||||
onboardingState = OnboardingState.LOCATION_CHECK
|
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
|
||||||
isLocationLoading = false
|
mainViewModel.updateLocationLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,87 +378,28 @@ class MainActivity : ComponentActivity() {
|
|||||||
*/
|
*/
|
||||||
private fun handleLocationEnabled() {
|
private fun handleLocationEnabled() {
|
||||||
android.util.Log.d("MainActivity", "Location services enabled by user")
|
android.util.Log.d("MainActivity", "Location services enabled by user")
|
||||||
isLocationLoading = false
|
mainViewModel.updateLocationLoading(false)
|
||||||
locationStatus = LocationStatus.ENABLED
|
mainViewModel.updateLocationStatus(LocationStatus.ENABLED)
|
||||||
checkBatteryOptimizationAndProceed()
|
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
|
* Handle Location disabled callback
|
||||||
*/
|
*/
|
||||||
private fun handleLocationDisabled(message: String) {
|
private fun handleLocationDisabled(message: String) {
|
||||||
android.util.Log.w("MainActivity", "Location services disabled or failed: $message")
|
android.util.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -443,14 +409,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
*/
|
*/
|
||||||
private fun handleBluetoothDisabled(message: String) {
|
private fun handleBluetoothDisabled(message: String) {
|
||||||
android.util.Log.w("MainActivity", "Bluetooth disabled or failed: $message")
|
android.util.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,
|
||||||
@@ -461,11 +427,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
|
||||||
android.util.Log.d("MainActivity", "Bluetooth enable requires permissions, showing permission explanation")
|
android.util.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,31 +452,28 @@ 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
|
||||||
android.util.Log.d("MainActivity", "Permissions granted, but Bluetooth still disabled. Showing Bluetooth enable screen.")
|
android.util.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
|
||||||
android.util.Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.")
|
android.util.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)
|
||||||
}
|
}
|
||||||
currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> {
|
currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> {
|
||||||
// Battery optimization is still enabled, show disable screen
|
// Battery optimization still enabled, show battery optimization screen
|
||||||
android.util.Log.d("MainActivity", "Permissions granted, but Battery optimization still enabled. Showing Battery optimization disable screen.")
|
android.util.Log.d("MainActivity", "Permissions granted, but battery optimization still enabled. Showing battery optimization screen.")
|
||||||
batteryOptimizationStatus = currentBatteryOptimizationStatus
|
mainViewModel.updateBatteryOptimizationStatus(currentBatteryOptimizationStatus)
|
||||||
onboardingState = OnboardingState.BATTERY_OPTIMIZATION_CHECK
|
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
|
||||||
isBatteryOptimizationLoading = false
|
mainViewModel.updateBatteryOptimizationLoading(false)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Bluetooth, Location, and Battery Optimization are all properly configured, proceed to app initialization
|
// Both are enabled, proceed to app initialization
|
||||||
android.util.Log.d("MainActivity", "Bluetooth, Location, and Battery Optimization all properly configured, proceeding to initialization")
|
android.util.Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization")
|
||||||
bluetoothStatus = currentBluetoothStatus
|
mainViewModel.updateOnboardingState(OnboardingState.INITIALIZING)
|
||||||
locationStatus = currentLocationStatus
|
|
||||||
batteryOptimizationStatus = currentBatteryOptimizationStatus
|
|
||||||
onboardingState = OnboardingState.INITIALIZING
|
|
||||||
initializeApp()
|
initializeApp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -518,8 +481,72 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private fun handleOnboardingFailed(message: String) {
|
private fun handleOnboardingFailed(message: String) {
|
||||||
android.util.Log.e("MainActivity", "Onboarding failed: $message")
|
android.util.Log.e("MainActivity", "Onboarding failed: $message")
|
||||||
errorMessage = message
|
mainViewModel.updateErrorMessage(message)
|
||||||
onboardingState = OnboardingState.ERROR
|
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() {
|
private fun initializeApp() {
|
||||||
@@ -554,7 +581,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
delay(500)
|
delay(500)
|
||||||
|
|
||||||
android.util.Log.d("MainActivity", "App initialization complete")
|
android.util.Log.d("MainActivity", "App initialization complete")
|
||||||
onboardingState = OnboardingState.COMPLETE
|
mainViewModel.updateOnboardingState(OnboardingState.COMPLETE)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("MainActivity", "Failed to initialize app", e)
|
android.util.Log.e("MainActivity", "Failed to initialize app", e)
|
||||||
@@ -566,7 +593,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -574,7 +601,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)
|
||||||
@@ -583,9 +610,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
|
||||||
if (currentBluetoothStatus != BluetoothStatus.ENABLED) {
|
if (currentBluetoothStatus != BluetoothStatus.ENABLED) {
|
||||||
android.util.Log.w("MainActivity", "Bluetooth disabled while app was backgrounded")
|
android.util.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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,9 +620,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
val currentLocationStatus = locationStatusManager.checkLocationStatus()
|
val currentLocationStatus = locationStatusManager.checkLocationStatus()
|
||||||
if (currentLocationStatus != LocationStatus.ENABLED) {
|
if (currentLocationStatus != LocationStatus.ENABLED) {
|
||||||
android.util.Log.w("MainActivity", "Location services disabled while app was backgrounded")
|
android.util.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -603,7 +630,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)
|
||||||
@@ -639,7 +666,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
* Restart mesh services (for debugging/troubleshooting)
|
* Restart mesh services (for debugging/troubleshooting)
|
||||||
*/
|
*/
|
||||||
fun restartMeshServices() {
|
fun restartMeshServices() {
|
||||||
if (onboardingState == OnboardingState.COMPLETE) {
|
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
android.util.Log.d("MainActivity", "Restarting mesh services")
|
android.util.Log.d("MainActivity", "Restarting mesh services")
|
||||||
@@ -666,7 +693,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()
|
||||||
android.util.Log.d("MainActivity", "Mesh services stopped successfully")
|
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.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
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.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitchat.android.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Screen shown when checking battery optimization status or requesting battery optimization disable
|
* Screen shown when checking battery optimization status or requesting battery optimization disable
|
||||||
@@ -96,7 +98,7 @@ private fun BatteryOptimizationEnabledContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Battery Optimization Detected",
|
text = stringResource(R.string.battery_optimization_detected),
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(
|
style = MaterialTheme.typography.headlineSmall.copy(
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = colorScheme.onSurface
|
color = colorScheme.onSurface
|
||||||
@@ -105,7 +107,7 @@ private fun BatteryOptimizationEnabledContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
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(
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = colorScheme.onSurfaceVariant
|
color = colorScheme.onSurfaceVariant
|
||||||
),
|
),
|
||||||
@@ -123,7 +125,7 @@ private fun BatteryOptimizationEnabledContent(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Why disable battery optimization?",
|
text = stringResource(R.string.battery_optimization_why_disable),
|
||||||
style = MaterialTheme.typography.titleSmall.copy(
|
style = MaterialTheme.typography.titleSmall.copy(
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = colorScheme.onSurface
|
color = colorScheme.onSurface
|
||||||
@@ -131,7 +133,7 @@ private fun BatteryOptimizationEnabledContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
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(
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
color = colorScheme.onSurfaceVariant
|
color = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -154,7 +156,7 @@ private fun BatteryOptimizationEnabledContent(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
Text("Disable Battery Optimization")
|
Text(stringResource(R.string.battery_optimization_disable_button))
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -166,7 +168,7 @@ private fun BatteryOptimizationEnabledContent(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
enabled = !isLoading
|
enabled = !isLoading
|
||||||
) {
|
) {
|
||||||
Text("Check Again")
|
Text(stringResource(R.string.battery_optimization_check_again))
|
||||||
}
|
}
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -174,12 +176,12 @@ private fun BatteryOptimizationEnabledContent(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
enabled = !isLoading
|
enabled = !isLoading
|
||||||
) {
|
) {
|
||||||
Text("Skip for Now")
|
Text(stringResource(R.string.battery_optimization_skip))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
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(
|
style = MaterialTheme.typography.bodySmall.copy(
|
||||||
color = colorScheme.onSurfaceVariant
|
color = colorScheme.onSurfaceVariant
|
||||||
),
|
),
|
||||||
@@ -226,7 +228,7 @@ private fun BatteryOptimizationCheckingContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Battery Optimization Disabled",
|
text = stringResource(R.string.battery_optimization_disabled),
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(
|
style = MaterialTheme.typography.headlineSmall.copy(
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = colorScheme.onSurface
|
color = colorScheme.onSurface
|
||||||
@@ -235,7 +237,7 @@ private fun BatteryOptimizationCheckingContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "bitchat can run reliably in the background",
|
text = stringResource(R.string.battery_optimization_success_message),
|
||||||
style = MaterialTheme.typography.bodyLarge.copy(
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = colorScheme.onSurfaceVariant
|
color = colorScheme.onSurfaceVariant
|
||||||
),
|
),
|
||||||
@@ -270,7 +272,7 @@ private fun BatteryOptimizationNotSupportedContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Battery Optimization Not Required",
|
text = stringResource(R.string.battery_optimization_not_required),
|
||||||
style = MaterialTheme.typography.headlineSmall.copy(
|
style = MaterialTheme.typography.headlineSmall.copy(
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = colorScheme.onSurface
|
color = colorScheme.onSurface
|
||||||
@@ -279,7 +281,7 @@ private fun BatteryOptimizationNotSupportedContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
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(
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
color = colorScheme.onSurfaceVariant
|
color = colorScheme.onSurfaceVariant
|
||||||
),
|
),
|
||||||
@@ -290,7 +292,7 @@ private fun BatteryOptimizationNotSupportedContent(
|
|||||||
onClick = onRetry,
|
onClick = onRetry,
|
||||||
modifier = Modifier.fillMaxWidth()
|
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.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,7 +149,6 @@ 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 -> {
|
||||||
@@ -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,24 @@
|
|||||||
<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>
|
||||||
|
|
||||||
|
<!-- Battery Optimization Strings -->
|
||||||
|
<string name="battery_optimization_detected">Battery Optimization Detected</string>
|
||||||
|
<string name="battery_optimization_disabled">Battery Optimization Disabled</string>
|
||||||
|
<string name="battery_optimization_not_required">Battery Optimization Not Required</string>
|
||||||
|
<string name="battery_optimization_checking">Checking Battery Optimization</string>
|
||||||
|
<string name="battery_optimization_why_disable">Why disable battery optimization?</string>
|
||||||
|
<string name="battery_optimization_explanation">bitchat runs in the background to maintain mesh network connections with nearby devices. Battery optimization can interrupt these connections, causing messages to be delayed or missed.\n\nDisabling battery optimization ensures reliable peer-to-peer messaging.</string>
|
||||||
|
<string name="battery_optimization_disable_button">Disable Battery Optimization</string>
|
||||||
|
<string name="battery_optimization_note">Note: You can change this setting later in Android Settings > Apps > bitchat > Battery</string>
|
||||||
|
<string name="battery_optimization_not_supported_explanation">Your device doesn\'t require battery optimization settings. bitchat will run normally.</string>
|
||||||
|
<string name="battery_optimization_not_supported_message">Your device doesn\'t require battery optimization settings. bitchat will run normally.</string>
|
||||||
|
<string name="battery_optimization_success_message">bitchat can run reliably in the background</string>
|
||||||
|
<string name="battery_optimization_benefits">• Ensures reliable message delivery\n• Maintains mesh network connectivity\n• Allows background message relay\n• Prevents connection drops</string>
|
||||||
|
<string name="battery_optimization_check_again">Check Again</string>
|
||||||
|
<string name="battery_optimization_skip">Skip for Now</string>
|
||||||
|
<string name="battery_optimization_continue">Continue</string>
|
||||||
|
<string name="retry">Retry</string>
|
||||||
|
<string name="skip">Skip</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user