Merge branch 'main' into code_readiblity_improvement

This commit is contained in:
callebtc
2025-07-19 09:41:09 +02:00
committed by GitHub
12 changed files with 750 additions and 19 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "com.bitchat.android" applicationId = "com.bitchat.android"
minSdk = libs.versions.minSdk.get().toInt() minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 3 versionCode = 4
versionName = "0.7" versionName = "0.7.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
+3
View File
@@ -19,6 +19,9 @@
<!-- Haptic feedback permission --> <!-- Haptic feedback permission -->
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<!-- Battery optimization permission -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- Hardware features --> <!-- Hardware features -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true" /> <uses-feature android:name="android.hardware.bluetooth" android:required="true" />
@@ -22,6 +22,9 @@ import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.onboarding.BluetoothCheckScreen import com.bitchat.android.onboarding.BluetoothCheckScreen
import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.BluetoothStatus
import com.bitchat.android.onboarding.BluetoothStatusManager 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.InitializationErrorScreen
import com.bitchat.android.onboarding.InitializingScreen import com.bitchat.android.onboarding.InitializingScreen
import com.bitchat.android.onboarding.LocationCheckScreen import com.bitchat.android.onboarding.LocationCheckScreen
@@ -43,6 +46,7 @@ class MainActivity : ComponentActivity() {
private lateinit var onboardingCoordinator: OnboardingCoordinator private lateinit var onboardingCoordinator: OnboardingCoordinator
private lateinit var bluetoothStatusManager: BluetoothStatusManager private lateinit var bluetoothStatusManager: BluetoothStatusManager
private lateinit var locationStatusManager: LocationStatusManager private lateinit var locationStatusManager: LocationStatusManager
private lateinit var batteryOptimizationManager: BatteryOptimizationManager
// Core mesh service - managed at app level // Core mesh service - managed at app level
private lateinit var meshService: BluetoothMeshService private lateinit var meshService: BluetoothMeshService
@@ -56,6 +60,8 @@ class MainActivity : ComponentActivity() {
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -76,6 +82,12 @@ class MainActivity : ComponentActivity() {
onLocationEnabled = ::handleLocationEnabled, onLocationEnabled = ::handleLocationEnabled,
onLocationDisabled = ::handleLocationDisabled onLocationDisabled = ::handleLocationDisabled
) )
batteryOptimizationManager = BatteryOptimizationManager(
activity = this,
context = this,
onBatteryOptimizationDisabled = ::handleBatteryOptimizationDisabled,
onBatteryOptimizationFailed = ::handleBatteryOptimizationFailed
)
onboardingCoordinator = OnboardingCoordinator( onboardingCoordinator = OnboardingCoordinator(
activity = this, activity = this,
permissionManager = permissionManager, permissionManager = permissionManager,
@@ -115,9 +127,11 @@ class MainActivity : ComponentActivity() {
val onboardingState by mainViewModel.onboardingState.collectAsState() val onboardingState by mainViewModel.onboardingState.collectAsState()
val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState() val bluetoothStatus by mainViewModel.bluetoothStatus.collectAsState()
val locationStatus by mainViewModel.locationStatus.collectAsState() val locationStatus by mainViewModel.locationStatus.collectAsState()
val batteryOptimizationStatus by mainViewModel.batteryOptimizationStatus.collectAsState()
val errorMessage by mainViewModel.errorMessage.collectAsState() val errorMessage by mainViewModel.errorMessage.collectAsState()
val isBluetoothLoading by mainViewModel.isBluetoothLoading.collectAsState() val isBluetoothLoading by mainViewModel.isBluetoothLoading.collectAsState()
val isLocationLoading by mainViewModel.isLocationLoading.collectAsState() val isLocationLoading by mainViewModel.isLocationLoading.collectAsState()
val isBatteryOptimizationLoading by mainViewModel.isBatteryOptimizationLoading.collectAsState()
when (onboardingState) { when (onboardingState) {
OnboardingState.CHECKING -> { OnboardingState.CHECKING -> {
@@ -152,6 +166,24 @@ class MainActivity : ComponentActivity() {
) )
} }
OnboardingState.BATTERY_OPTIMIZATION_CHECK -> {
BatteryOptimizationScreen(
status = batteryOptimizationStatus,
onDisableBatteryOptimization = {
mainViewModel.updateBatteryOptimizationLoading(true)
batteryOptimizationManager.requestDisableBatteryOptimization()
},
onRetry = {
checkBatteryOptimizationAndProceed()
},
onSkip = {
// Skip battery optimization and proceed
proceedWithPermissionCheck()
},
isLoading = isBatteryOptimizationLoading
)
}
OnboardingState.PERMISSION_EXPLANATION -> { OnboardingState.PERMISSION_EXPLANATION -> {
PermissionExplanationScreen( PermissionExplanationScreen(
permissionCategories = permissionManager.getCategorizedPermissions(), permissionCategories = permissionManager.getCategorizedPermissions(),
@@ -324,8 +356,8 @@ class MainActivity : ComponentActivity() {
when (mainViewModel.locationStatus.value) { when (mainViewModel.locationStatus.value) {
LocationStatus.ENABLED -> { LocationStatus.ENABLED -> {
// Location services enabled, proceed with permission/onboarding check // Location services enabled, check battery optimization next
proceedWithPermissionCheck() checkBatteryOptimizationAndProceed()
} }
LocationStatus.DISABLED -> { LocationStatus.DISABLED -> {
// Show location enable screen (should have permissions as existing user) // Show location enable screen (should have permissions as existing user)
@@ -349,7 +381,7 @@ class MainActivity : ComponentActivity() {
Log.d("MainActivity", "Location services enabled by user") Log.d("MainActivity", "Location services enabled by user")
mainViewModel.updateLocationLoading(false) mainViewModel.updateLocationLoading(false)
mainViewModel.updateLocationStatus(LocationStatus.ENABLED) mainViewModel.updateLocationStatus(LocationStatus.ENABLED)
proceedWithPermissionCheck() checkBatteryOptimizationAndProceed()
} }
/** /**
@@ -408,9 +440,14 @@ class MainActivity : ComponentActivity() {
private fun handleOnboardingComplete() { private fun handleOnboardingComplete() {
Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app") Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app")
// After permissions are granted, re-check both Bluetooth and Location status // After permissions are granted, re-check Bluetooth, Location, and Battery Optimization status
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
val currentLocationStatus = locationStatusManager.checkLocationStatus() val currentLocationStatus = locationStatusManager.checkLocationStatus()
val currentBatteryOptimizationStatus = when {
!batteryOptimizationManager.isBatteryOptimizationSupported() -> BatteryOptimizationStatus.NOT_SUPPORTED
batteryOptimizationManager.isBatteryOptimizationDisabled() -> BatteryOptimizationStatus.DISABLED
else -> BatteryOptimizationStatus.ENABLED
}
when { when {
currentBluetoothStatus != BluetoothStatus.ENABLED -> { currentBluetoothStatus != BluetoothStatus.ENABLED -> {
@@ -427,6 +464,13 @@ class MainActivity : ComponentActivity() {
mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK) mainViewModel.updateOnboardingState(OnboardingState.LOCATION_CHECK)
mainViewModel.updateLocationLoading(false) mainViewModel.updateLocationLoading(false)
} }
currentBatteryOptimizationStatus == BatteryOptimizationStatus.ENABLED -> {
// Battery optimization still enabled, show battery optimization screen
android.util.Log.d("MainActivity", "Permissions granted, but battery optimization still enabled. Showing battery optimization screen.")
mainViewModel.updateBatteryOptimizationStatus(currentBatteryOptimizationStatus)
mainViewModel.updateOnboardingState(OnboardingState.BATTERY_OPTIMIZATION_CHECK)
mainViewModel.updateBatteryOptimizationLoading(false)
}
else -> { 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")
@@ -442,6 +486,70 @@ class MainActivity : ComponentActivity() {
mainViewModel.updateOnboardingState(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() {
Log.d("MainActivity", "Starting app initialization") Log.d("MainActivity", "Starting app initialization")
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.BluetoothStatus
import com.bitchat.android.onboarding.LocationStatus import com.bitchat.android.onboarding.LocationStatus
import com.bitchat.android.onboarding.OnboardingState import com.bitchat.android.onboarding.OnboardingState
import com.bitchat.android.onboarding.BatteryOptimizationStatus
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -28,6 +29,12 @@ class MainViewModel : ViewModel() {
private val _isLocationLoading = MutableStateFlow(false) private val _isLocationLoading = MutableStateFlow(false)
val isLocationLoading: StateFlow<Boolean> = _isLocationLoading.asStateFlow() 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 // Public update functions for MainActivity
fun updateOnboardingState(state: OnboardingState) { fun updateOnboardingState(state: OnboardingState) {
_onboardingState.value = state _onboardingState.value = state
@@ -52,4 +59,12 @@ class MainViewModel : ViewModel() {
fun updateLocationLoading(loading: Boolean) { fun updateLocationLoading(loading: Boolean) {
_isLocationLoading.value = loading _isLocationLoading.value = loading
} }
fun updateBatteryOptimizationStatus(status: BatteryOptimizationStatus) {
_batteryOptimizationStatus.value = status
}
fun updateBatteryOptimizationLoading(loading: Boolean) {
_isBatteryOptimizationLoading.value = loading
}
} }
@@ -0,0 +1,177 @@
package com.bitchat.android.onboarding
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
/**
* Manages battery optimization settings for the app
* Handles checking if the app is whitelisted from battery optimization
* and requesting the user to disable battery optimization
*/
class BatteryOptimizationManager(
private val activity: ComponentActivity,
private val context: Context,
private val onBatteryOptimizationDisabled: () -> Unit,
private val onBatteryOptimizationFailed: (String) -> Unit
) {
companion object {
private const val TAG = "BatteryOptimizationManager"
}
private var batteryOptimizationLauncher: ActivityResultLauncher<Intent>? = null
init {
setupBatteryOptimizationLauncher()
}
/**
* Setup the battery optimization request launcher
*/
private fun setupBatteryOptimizationLauncher() {
batteryOptimizationLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
Log.d(TAG, "Battery optimization request result: ${result.resultCode}")
// Check if battery optimization is now disabled
if (isBatteryOptimizationDisabled()) {
Log.d(TAG, "Battery optimization successfully disabled")
onBatteryOptimizationDisabled()
} else {
Log.w(TAG, "Battery optimization still enabled after user interaction")
// Don't treat this as a failure - user might have chosen not to disable it
// We'll proceed anyway but log the status
onBatteryOptimizationDisabled()
}
}
}
/**
* Check if battery optimization is disabled for this app
*/
fun isBatteryOptimizationDisabled(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val isIgnoring = powerManager.isIgnoringBatteryOptimizations(context.packageName)
Log.d(TAG, "Battery optimization disabled: $isIgnoring")
isIgnoring
} catch (e: Exception) {
Log.e(TAG, "Error checking battery optimization status", e)
// If we can't check, assume it's enabled (more conservative)
false
}
} else {
// Battery optimization doesn't exist on Android < 6.0
Log.d(TAG, "Battery optimization not applicable for Android < 6.0")
true
}
}
/**
* Request to disable battery optimization for this app
*/
fun requestDisableBatteryOptimization() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
Log.d(TAG, "Requesting to disable battery optimization")
val intent = Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = Uri.parse("package:${context.packageName}")
}
// Check if the intent can be resolved
if (intent.resolveActivity(context.packageManager) != null) {
batteryOptimizationLauncher?.launch(intent)
} else {
Log.w(TAG, "Battery optimization settings not available, opening general settings")
openBatteryOptimizationSettings()
}
} catch (e: Exception) {
Log.e(TAG, "Error requesting battery optimization disable", e)
onBatteryOptimizationFailed("Unable to open battery optimization settings: ${e.message}")
}
} else {
Log.d(TAG, "Battery optimization not applicable for Android < 6.0")
onBatteryOptimizationDisabled()
}
}
/**
* Open general battery optimization settings if direct request fails
*/
private fun openBatteryOptimizationSettings() {
try {
val intent = Intent().apply {
action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
}
if (intent.resolveActivity(context.packageManager) != null) {
batteryOptimizationLauncher?.launch(intent)
} else {
// Fallback to general application settings
openAppSettings()
}
} catch (e: Exception) {
Log.e(TAG, "Error opening battery optimization settings", e)
onBatteryOptimizationFailed("Unable to open settings: ${e.message}")
}
}
/**
* Open app settings as a last resort
*/
private fun openAppSettings() {
try {
val intent = Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", context.packageName, null)
}
batteryOptimizationLauncher?.launch(intent)
} catch (e: Exception) {
Log.e(TAG, "Error opening app settings", e)
onBatteryOptimizationFailed("Unable to open app settings: ${e.message}")
}
}
/**
* Check if battery optimization is supported on this device
*/
fun isBatteryOptimizationSupported(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}
/**
* Get battery optimization status for logging
*/
fun getBatteryOptimizationStatus(): String {
return when {
!isBatteryOptimizationSupported() -> "Not supported (Android < 6.0)"
isBatteryOptimizationDisabled() -> "Disabled (app is whitelisted)"
else -> "Enabled (app is being optimized)"
}
}
/**
* Log battery optimization status for debugging
*/
fun logBatteryOptimizationStatus() {
Log.d(TAG, "Battery optimization status: ${getBatteryOptimizationStatus()}")
}
}
enum class BatteryOptimizationStatus {
ENABLED,
DISABLED,
NOT_SUPPORTED
}
@@ -0,0 +1,298 @@
package com.bitchat.android.onboarding
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.bitchat.android.R
/**
* Screen shown when checking battery optimization status or requesting battery optimization disable
*/
@Composable
fun BatteryOptimizationScreen(
status: BatteryOptimizationStatus,
onDisableBatteryOptimization: () -> Unit,
onRetry: () -> Unit,
onSkip: () -> Unit,
isLoading: Boolean = false
) {
val colorScheme = MaterialTheme.colorScheme
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
when (status) {
BatteryOptimizationStatus.ENABLED -> {
BatteryOptimizationEnabledContent(
onDisableBatteryOptimization = onDisableBatteryOptimization,
onRetry = onRetry,
onSkip = onSkip,
colorScheme = colorScheme,
isLoading = isLoading
)
}
BatteryOptimizationStatus.DISABLED -> {
BatteryOptimizationCheckingContent(
colorScheme = colorScheme
)
}
BatteryOptimizationStatus.NOT_SUPPORTED -> {
BatteryOptimizationNotSupportedContent(
onRetry = onRetry,
colorScheme = colorScheme
)
}
}
}
}
@Composable
private fun BatteryOptimizationEnabledContent(
onDisableBatteryOptimization: () -> Unit,
onRetry: () -> Unit,
onSkip: () -> Unit,
colorScheme: ColorScheme,
isLoading: Boolean
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "bitchat*",
style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = colorScheme.primary
)
)
Spacer(modifier = Modifier.height(16.dp))
Icon(
imageVector = Icons.Outlined.BatteryAlert,
contentDescription = "Battery Optimization",
modifier = Modifier.size(64.dp),
tint = colorScheme.error
)
Text(
text = stringResource(R.string.battery_optimization_detected),
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onSurface
),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.battery_optimization_explanation),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
),
textAlign = TextAlign.Center
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = colorScheme.surfaceVariant.copy(alpha = 0.5f)
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = stringResource(R.string.battery_optimization_why_disable),
style = MaterialTheme.typography.titleSmall.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onSurface
)
)
Text(
text = stringResource(R.string.battery_optimization_benefits),
style = MaterialTheme.typography.bodyMedium.copy(
color = colorScheme.onSurfaceVariant
)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onDisableBatteryOptimization,
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
color = colorScheme.onPrimary
)
Spacer(modifier = Modifier.width(8.dp))
}
Text(stringResource(R.string.battery_optimization_disable_button))
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedButton(
onClick = onRetry,
modifier = Modifier.weight(1f),
enabled = !isLoading
) {
Text(stringResource(R.string.battery_optimization_check_again))
}
TextButton(
onClick = onSkip,
modifier = Modifier.weight(1f),
enabled = !isLoading
) {
Text(stringResource(R.string.battery_optimization_skip))
}
}
Text(
text = stringResource(R.string.battery_optimization_note),
style = MaterialTheme.typography.bodySmall.copy(
color = colorScheme.onSurfaceVariant
),
textAlign = TextAlign.Center
)
}
}
@Composable
private fun BatteryOptimizationCheckingContent(
colorScheme: ColorScheme
) {
Column(
verticalArrangement = Arrangement.spacedBy(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "bitchat*",
style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = colorScheme.primary
)
)
val infiniteTransition = rememberInfiniteTransition(label = "rotation")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "rotation"
)
Icon(
imageVector = Icons.Filled.BatteryStd,
contentDescription = "Checking Battery Optimization",
modifier = Modifier
.size(64.dp)
.rotate(rotation),
tint = colorScheme.primary
)
Text(
text = stringResource(R.string.battery_optimization_disabled),
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onSurface
),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.battery_optimization_success_message),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
),
textAlign = TextAlign.Center
)
}
}
@Composable
private fun BatteryOptimizationNotSupportedContent(
onRetry: () -> Unit,
colorScheme: ColorScheme
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "bitchat*",
style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = colorScheme.primary
)
)
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = "Battery Optimization Not Supported",
modifier = Modifier.size(64.dp),
tint = colorScheme.primary
)
Text(
text = stringResource(R.string.battery_optimization_not_required),
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onSurface
),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.battery_optimization_not_supported_message),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
),
textAlign = TextAlign.Center
)
Button(
onClick = onRetry,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.battery_optimization_continue))
}
}
}
@@ -4,6 +4,7 @@ enum class OnboardingState {
CHECKING, CHECKING,
BLUETOOTH_CHECK, BLUETOOTH_CHECK,
LOCATION_CHECK, LOCATION_CHECK,
BATTERY_OPTIMIZATION_CHECK,
PERMISSION_EXPLANATION, PERMISSION_EXPLANATION,
PERMISSION_REQUESTING, PERMISSION_REQUESTING,
INITIALIZING, INITIALIZING,
@@ -237,6 +237,7 @@ private fun getPermissionEmoji(permissionType: PermissionType): String {
PermissionType.NEARBY_DEVICES -> "📱" PermissionType.NEARBY_DEVICES -> "📱"
PermissionType.PRECISE_LOCATION -> "📍" PermissionType.PRECISE_LOCATION -> "📍"
PermissionType.NOTIFICATIONS -> "🔔" PermissionType.NOTIFICATIONS -> "🔔"
PermissionType.BATTERY_OPTIMIZATION -> "🔋"
PermissionType.OTHER -> "🔧" PermissionType.OTHER -> "🔧"
} }
} }
@@ -246,6 +247,7 @@ private fun getPermissionIconColor(permissionType: PermissionType): Color {
PermissionType.NEARBY_DEVICES -> Color(0xFF2196F3) // Blue PermissionType.NEARBY_DEVICES -> Color(0xFF2196F3) // Blue
PermissionType.PRECISE_LOCATION -> Color(0xFFFF9800) // Orange PermissionType.PRECISE_LOCATION -> Color(0xFFFF9800) // Orange
PermissionType.NOTIFICATIONS -> Color(0xFF4CAF50) // Green PermissionType.NOTIFICATIONS -> Color(0xFF4CAF50) // Green
PermissionType.BATTERY_OPTIMIZATION -> Color(0xFFF44336) // Red
PermissionType.OTHER -> Color(0xFF9C27B0) // Purple PermissionType.OTHER -> Color(0xFF9C27B0) // Purple
} }
} }
@@ -4,6 +4,7 @@ import android.Manifest
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.PowerManager
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -86,6 +87,31 @@ class PermissionManager(private val context: Context) {
return getRequiredPermissions().all { isPermissionGranted(it) } return getRequiredPermissions().all { isPermissionGranted(it) }
} }
/**
* Check if battery optimization is disabled for this app
*/
fun isBatteryOptimizationDisabled(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
powerManager.isIgnoringBatteryOptimizations(context.packageName)
} catch (e: Exception) {
Log.e(TAG, "Error checking battery optimization status", e)
false
}
} else {
// Battery optimization doesn't exist on Android < 6.0
true
}
}
/**
* Check if battery optimization is supported on this device
*/
fun isBatteryOptimizationSupported(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}
/** /**
* Get the list of permissions that are missing * Get the list of permissions that are missing
*/ */
@@ -152,6 +178,19 @@ class PermissionManager(private val context: Context) {
) )
} }
// Battery optimization category (if applicable)
if (isBatteryOptimizationSupported()) {
categories.add(
PermissionCategory(
type = PermissionType.BATTERY_OPTIMIZATION,
description = "Disable battery optimization to ensure bitchat runs reliably in the background and maintains mesh network connections",
permissions = listOf("BATTERY_OPTIMIZATION"), // Custom identifier
isGranted = isBatteryOptimizationDisabled(),
systemDescription = "Allow bitchat to run without battery restrictions"
)
)
}
return categories return categories
} }
@@ -208,5 +247,6 @@ enum class PermissionType(val nameValue: String) {
NEARBY_DEVICES("Nearby Devices"), NEARBY_DEVICES("Nearby Devices"),
PRECISE_LOCATION("Precise Location"), PRECISE_LOCATION("Precise Location"),
NOTIFICATIONS("Notifications"), NOTIFICATIONS("Notifications"),
BATTERY_OPTIMIZATION("Battery Optimization"),
OTHER("Other") OTHER("Other")
} }
@@ -22,9 +22,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.TextRange
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.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@@ -65,7 +67,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList()) val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList())
val showAppInfo by viewModel.showAppInfo.observeAsState(false) val showAppInfo by viewModel.showAppInfo.observeAsState(false)
var messageText by remember { mutableStateOf("") } var messageText by remember { mutableStateOf(TextFieldValue("")) }
var showPasswordPrompt by remember { mutableStateOf(false) } var showPasswordPrompt by remember { mutableStateOf(false) }
var showPasswordDialog by remember { mutableStateOf(false) } var showPasswordDialog by remember { mutableStateOf(false) }
var passwordInput by remember { mutableStateOf("") } var passwordInput by remember { mutableStateOf("") }
@@ -112,20 +114,24 @@ fun ChatScreen(viewModel: ChatViewModel) {
// Input area - stays at bottom // Input area - stays at bottom
ChatInputSection( ChatInputSection(
messageText = messageText, messageText = messageText,
onMessageTextChange = { newText: String -> onMessageTextChange = { newText: TextFieldValue ->
messageText = newText messageText = newText
viewModel.updateCommandSuggestions(newText) viewModel.updateCommandSuggestions(newText.text)
}, },
onSend = { onSend = {
if (messageText.trim().isNotEmpty()) { if (messageText.text.trim().isNotEmpty()) {
viewModel.sendMessage(messageText.trim()) viewModel.sendMessage(messageText.text.trim())
messageText = "" messageText = TextFieldValue("")
} }
}, },
showCommandSuggestions = showCommandSuggestions, showCommandSuggestions = showCommandSuggestions,
commandSuggestions = commandSuggestions, commandSuggestions = commandSuggestions,
onSuggestionClick = { suggestion: CommandSuggestion -> onSuggestionClick = { suggestion: CommandSuggestion ->
messageText = viewModel.selectCommandSuggestion(suggestion) val commandText = viewModel.selectCommandSuggestion(suggestion)
messageText = TextFieldValue(
text = commandText,
selection = TextRange(commandText.length)
)
}, },
selectedPrivatePeer = selectedPrivatePeer, selectedPrivatePeer = selectedPrivatePeer,
currentChannel = currentChannel, currentChannel = currentChannel,
@@ -195,8 +201,8 @@ fun ChatScreen(viewModel: ChatViewModel) {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ChatInputSection( private fun ChatInputSection(
messageText: String, messageText: TextFieldValue,
onMessageTextChange: (String) -> Unit, onMessageTextChange: (TextFieldValue) -> Unit,
onSend: () -> Unit, onSend: () -> Unit,
showCommandSuggestions: Boolean, showCommandSuggestions: Boolean,
commandSuggestions: List<CommandSuggestion>, commandSuggestions: List<CommandSuggestion>,
@@ -15,23 +15,79 @@ 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.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight 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.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
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.focus.onFocusChanged
import androidx.compose.ui.text.withStyle
/** /**
* Input components for ChatScreen * Input components for ChatScreen
* Extracted from ChatScreen.kt for better organization * Extracted from ChatScreen.kt for better organization
*/ */
/**
* VisualTransformation that styles slash commands with background and color
* while preserving cursor positioning and click handling
*/
class SlashCommandVisualTransformation : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val slashCommandRegex = Regex("(/\\w+)(?=\\s|$)")
val annotatedString = buildAnnotatedString {
var lastIndex = 0
slashCommandRegex.findAll(text.text).forEach { match ->
// Add text before the match
if (match.range.first > lastIndex) {
append(text.text.substring(lastIndex, match.range.first))
}
// Add the styled slash command
withStyle(
style = SpanStyle(
color = Color(0xFF00FF7F), // Bright green
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
background = Color(0xFF2D2D2D) // Dark gray background
)
) {
append(match.value)
}
lastIndex = match.range.last + 1
}
// Add remaining text
if (lastIndex < text.text.length) {
append(text.text.substring(lastIndex))
}
}
return TransformedText(
text = annotatedString,
offsetMapping = OffsetMapping.Identity
)
}
}
@Composable @Composable
fun MessageInput( fun MessageInput(
value: String, value: TextFieldValue,
onValueChange: (String) -> Unit, onValueChange: (TextFieldValue) -> Unit,
onSend: () -> Unit, onSend: () -> Unit,
selectedPrivatePeer: String?, selectedPrivatePeer: String?,
currentChannel: String?, currentChannel: String?,
@@ -39,6 +95,7 @@ fun MessageInput(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val isFocused = remember { mutableStateOf(false) }
Row( Row(
modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding modifier = modifier.padding(horizontal = 12.dp, vertical = 8.dp), // Reduced padding
@@ -70,7 +127,12 @@ fun MessageInput(
cursorBrush = SolidColor(colorScheme.primary), cursorBrush = SolidColor(colorScheme.primary),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
keyboardActions = KeyboardActions(onSend = { onSend() }), keyboardActions = KeyboardActions(onSend = { onSend() }),
modifier = Modifier.weight(1f) visualTransformation = SlashCommandVisualTransformation(),
modifier = Modifier
.weight(1f)
.onFocusChanged { focusState ->
isFocused.value = focusState.isFocused
}
) )
Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing Spacer(modifier = Modifier.width(8.dp)) // Reduced spacing
+19
View File
@@ -17,4 +17,23 @@
<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> <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>