Merge pull request #103 from permissionlesstech/adjust-scan-intervals

adjust scan intervals and reduce logging
This commit is contained in:
callebtc
2025-07-13 20:47:33 +02:00
committed by GitHub
7 changed files with 732 additions and 74 deletions
@@ -33,6 +33,7 @@ class MainActivity : ComponentActivity() {
private lateinit var permissionManager: PermissionManager private lateinit var permissionManager: PermissionManager
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
// Core mesh service - managed at app level // Core mesh service - managed at app level
private lateinit var meshService: BluetoothMeshService private lateinit var meshService: BluetoothMeshService
@@ -48,12 +49,15 @@ class MainActivity : ComponentActivity() {
// UI state for onboarding flow // UI state for onboarding flow
private var onboardingState by mutableStateOf(OnboardingState.CHECKING) private var onboardingState by mutableStateOf(OnboardingState.CHECKING)
private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED) private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED)
private var locationStatus by mutableStateOf(LocationStatus.ENABLED)
private var errorMessage by mutableStateOf("") private var errorMessage by mutableStateOf("")
private var isBluetoothLoading by mutableStateOf(false) private var isBluetoothLoading by mutableStateOf(false)
private var isLocationLoading by mutableStateOf(false)
enum class OnboardingState { enum class OnboardingState {
CHECKING, CHECKING,
BLUETOOTH_CHECK, BLUETOOTH_CHECK,
LOCATION_CHECK,
PERMISSION_EXPLANATION, PERMISSION_EXPLANATION,
PERMISSION_REQUESTING, PERMISSION_REQUESTING,
INITIALIZING, INITIALIZING,
@@ -75,6 +79,12 @@ class MainActivity : ComponentActivity() {
onBluetoothEnabled = ::handleBluetoothEnabled, onBluetoothEnabled = ::handleBluetoothEnabled,
onBluetoothDisabled = ::handleBluetoothDisabled onBluetoothDisabled = ::handleBluetoothDisabled
) )
locationStatusManager = LocationStatusManager(
activity = this,
context = this,
onLocationEnabled = ::handleLocationEnabled,
onLocationDisabled = ::handleLocationDisabled
)
onboardingCoordinator = OnboardingCoordinator( onboardingCoordinator = OnboardingCoordinator(
activity = this, activity = this,
permissionManager = permissionManager, permissionManager = permissionManager,
@@ -118,6 +128,20 @@ class MainActivity : ComponentActivity() {
) )
} }
OnboardingState.LOCATION_CHECK -> {
LocationCheckScreen(
status = locationStatus,
onEnableLocation = {
isLocationLoading = true
locationStatusManager.requestEnableLocation()
},
onRetry = {
checkLocationAndProceed()
},
isLoading = isLocationLoading
)
}
OnboardingState.PERMISSION_EXPLANATION -> { OnboardingState.PERMISSION_EXPLANATION -> {
PermissionExplanationScreen( PermissionExplanationScreen(
permissionCategories = permissionManager.getCategorizedPermissions(), permissionCategories = permissionManager.getCategorizedPermissions(),
@@ -189,7 +213,7 @@ class MainActivity : ComponentActivity() {
* Check Bluetooth status and proceed with onboarding flow * Check Bluetooth status and proceed with onboarding flow
*/ */
private fun checkBluetoothAndProceed() { private fun checkBluetoothAndProceed() {
android.util.Log.d("MainActivity", "Checking Bluetooth status") // android.util.Log.d("MainActivity", "Checking Bluetooth status")
// For first-time users, skip Bluetooth check and go straight to permissions // For first-time users, skip Bluetooth check and go straight to permissions
// We'll check Bluetooth after permissions are granted // We'll check Bluetooth after permissions are granted
@@ -205,8 +229,8 @@ class MainActivity : ComponentActivity() {
when (bluetoothStatus) { when (bluetoothStatus) {
BluetoothStatus.ENABLED -> { BluetoothStatus.ENABLED -> {
// Bluetooth is enabled, proceed with permission/onboarding check // Bluetooth is enabled, check location services next
proceedWithPermissionCheck() checkLocationAndProceed()
} }
BluetoothStatus.DISABLED -> { BluetoothStatus.DISABLED -> {
// Show Bluetooth enable screen (should have permissions as existing user) // Show Bluetooth enable screen (should have permissions as existing user)
@@ -253,8 +277,77 @@ class MainActivity : ComponentActivity() {
android.util.Log.d("MainActivity", "Bluetooth enabled by user") android.util.Log.d("MainActivity", "Bluetooth enabled by user")
isBluetoothLoading = false isBluetoothLoading = false
bluetoothStatus = BluetoothStatus.ENABLED bluetoothStatus = BluetoothStatus.ENABLED
checkLocationAndProceed()
}
/**
* Check Location services status and proceed with onboarding flow
*/
private fun checkLocationAndProceed() {
android.util.Log.d("MainActivity", "Checking location services status")
// For first-time users, skip location check and go straight to permissions
// We'll check location after permissions are granted
if (permissionManager.isFirstTimeLaunch()) {
android.util.Log.d("MainActivity", "First-time launch, skipping location check - will check after permissions")
proceedWithPermissionCheck()
return
}
// For existing users, check location status
locationStatusManager.logLocationStatus()
locationStatus = locationStatusManager.checkLocationStatus()
when (locationStatus) {
LocationStatus.ENABLED -> {
// Location services enabled, proceed with permission/onboarding check
proceedWithPermissionCheck()
}
LocationStatus.DISABLED -> {
// Show location enable screen (should have permissions as existing user)
android.util.Log.d("MainActivity", "Location services disabled, showing enable screen")
onboardingState = OnboardingState.LOCATION_CHECK
isLocationLoading = false
}
LocationStatus.NOT_AVAILABLE -> {
// Device doesn't support location services (very unusual)
android.util.Log.e("MainActivity", "Location services not available")
onboardingState = OnboardingState.LOCATION_CHECK
isLocationLoading = false
}
}
}
/**
* Handle Location enabled callback
*/
private fun handleLocationEnabled() {
android.util.Log.d("MainActivity", "Location services enabled by user")
isLocationLoading = false
locationStatus = LocationStatus.ENABLED
proceedWithPermissionCheck() proceedWithPermissionCheck()
} }
/**
* Handle Location disabled callback
*/
private fun handleLocationDisabled(message: String) {
android.util.Log.w("MainActivity", "Location services disabled or failed: $message")
isLocationLoading = false
locationStatus = locationStatusManager.checkLocationStatus()
when {
locationStatus == LocationStatus.NOT_AVAILABLE -> {
// Show permanent error for devices without location services
errorMessage = message
onboardingState = OnboardingState.ERROR
}
else -> {
// Stay on location check screen for retry
onboardingState = OnboardingState.LOCATION_CHECK
}
}
}
/** /**
* Handle Bluetooth disabled callback * Handle Bluetooth disabled callback
@@ -289,20 +382,33 @@ class MainActivity : ComponentActivity() {
} }
private fun handleOnboardingComplete() { private fun handleOnboardingComplete() {
android.util.Log.d("MainActivity", "Onboarding completed, checking Bluetooth again before initializing app") android.util.Log.d("MainActivity", "Onboarding completed, checking Bluetooth and Location before initializing app")
// After permissions are granted, re-check Bluetooth status // After permissions are granted, re-check both Bluetooth and Location status
val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus() val currentBluetoothStatus = bluetoothStatusManager.checkBluetoothStatus()
if (currentBluetoothStatus == BluetoothStatus.ENABLED) { val currentLocationStatus = locationStatusManager.checkLocationStatus()
// Bluetooth is enabled, proceed to app initialization
onboardingState = OnboardingState.INITIALIZING when {
initializeApp() currentBluetoothStatus != BluetoothStatus.ENABLED -> {
} else { // 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
bluetoothStatus = currentBluetoothStatus onboardingState = OnboardingState.BLUETOOTH_CHECK
onboardingState = OnboardingState.BLUETOOTH_CHECK isBluetoothLoading = false
isBluetoothLoading = false }
currentLocationStatus != LocationStatus.ENABLED -> {
// Location services still disabled, but now we have permissions to enable it
android.util.Log.d("MainActivity", "Permissions granted, but Location services still disabled. Showing Location enable screen.")
locationStatus = currentLocationStatus
onboardingState = OnboardingState.LOCATION_CHECK
isLocationLoading = false
}
else -> {
// Both are enabled, proceed to app initialization
android.util.Log.d("MainActivity", "Both Bluetooth and Location services are enabled, proceeding to initialization")
onboardingState = OnboardingState.INITIALIZING
initializeApp()
}
} }
} }
@@ -363,7 +469,7 @@ class MainActivity : ComponentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Check Bluetooth status on resume and handle accordingly // Check Bluetooth and Location status on resume and handle accordingly
if (onboardingState == OnboardingState.COMPLETE) { if (onboardingState == OnboardingState.COMPLETE) {
// Set app foreground state // Set app foreground state
meshService.connectionManager.setAppBackgroundState(false) meshService.connectionManager.setAppBackgroundState(false)
@@ -376,6 +482,16 @@ class MainActivity : ComponentActivity() {
bluetoothStatus = currentBluetoothStatus bluetoothStatus = currentBluetoothStatus
onboardingState = OnboardingState.BLUETOOTH_CHECK onboardingState = OnboardingState.BLUETOOTH_CHECK
isBluetoothLoading = false isBluetoothLoading = false
return
}
// Check if location services were disabled while app was backgrounded
val currentLocationStatus = locationStatusManager.checkLocationStatus()
if (currentLocationStatus != LocationStatus.ENABLED) {
android.util.Log.w("MainActivity", "Location services disabled while app was backgrounded")
locationStatus = currentLocationStatus
onboardingState = OnboardingState.LOCATION_CHECK
isLocationLoading = false
} }
} }
} }
@@ -436,6 +552,15 @@ class MainActivity : ComponentActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
// Cleanup location status manager
try {
locationStatusManager.cleanup()
android.util.Log.d("MainActivity", "Location status manager cleaned up successfully")
} catch (e: Exception) {
android.util.Log.w("MainActivity", "Error cleaning up location status manager: ${e.message}")
}
// Stop mesh services if app was fully initialized // Stop mesh services if app was fully initialized
if (onboardingState == OnboardingState.COMPLETE) { if (onboardingState == OnboardingState.COMPLETE) {
try { try {
@@ -151,7 +151,7 @@ class BluetoothConnectionManager(
startPeriodicCleanup() startPeriodicCleanup()
Log.i(TAG, "Power-optimized Bluetooth services started successfully") Log.i(TAG, "Bluetooth services started successfully")
} }
return true return true
@@ -316,7 +316,8 @@ class BluetoothConnectionManager(
*/ */
fun getDebugInfo(): String { fun getDebugInfo(): String {
return buildString { return buildString {
appendLine("=== Power-Optimized Bluetooth Connection Manager ===") appendLine("=== Bluetooth Connection Manager ===")
appendLine("Bluetooth MAC Address: ${bluetoothAdapter?.address}")
appendLine("Active: $isActive") appendLine("Active: $isActive")
appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}") appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}")
appendLine("Has Permissions: ${hasBluetoothPermissions()}") appendLine("Has Permissions: ${hasBluetoothPermissions()}")
@@ -648,8 +649,7 @@ class BluetoothConnectionManager(
// DEBUG: Log ALL scan results first // DEBUG: Log ALL scan results first
val device = result.device val device = result.device
val rssi = result.rssi val rssi = result.rssi
val scanRecord = result.scanRecord // Log.d(TAG, "Scan result: device: ${device.address}, Name: '${device.name}', RSSI: $rssi")
Log.d(TAG, "Scan result: device: ${device.address}, Name: '${device.name}', RSSI: $rssi")
handleScanResult(result) handleScanResult(result)
} }
@@ -736,9 +736,7 @@ class BluetoothConnectionManager(
} else { } else {
null null
} }
Log.d(TAG, "Processing bitchat device: $deviceAddress, name: '$deviceName', peerID: $extractedPeerID, RSSI: $rssi")
// Power-aware RSSI filtering // Power-aware RSSI filtering
if (rssi < powerManager.getRSSIThreshold()) { if (rssi < powerManager.getRSSIThreshold()) {
Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}") Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}")
@@ -748,7 +746,7 @@ class BluetoothConnectionManager(
// CRITICAL FIX: Prevent multiple simultaneous connections to same device // CRITICAL FIX: Prevent multiple simultaneous connections to same device
// Check if already connected OR already attempting to connect // Check if already connected OR already attempting to connect
if (connectedDevices.containsKey(deviceAddress)) { if (connectedDevices.containsKey(deviceAddress)) {
Log.d(TAG, "Device $deviceAddress already connected, skipping") // Log.d(TAG, "Device $deviceAddress already connected, skipping")
return return
} }
@@ -779,12 +777,6 @@ class BluetoothConnectionManager(
val attempts = (currentAttempt?.attempts ?: 0) + 1 val attempts = (currentAttempt?.attempts ?: 0) + 1
pendingConnections[deviceAddress] = ConnectionAttempt(attempts) pendingConnections[deviceAddress] = ConnectionAttempt(attempts)
if (extractedPeerID != null) {
Log.i(TAG, "Initiating connection to peer $extractedPeerID at $deviceAddress (RSSI: $rssi, attempt: $attempts)")
} else {
Log.i(TAG, "Initiating connection to device with bitchat service at $deviceAddress (RSSI: $rssi, attempt: $attempts)")
}
// Start connection immediately while holding lock // Start connection immediately while holding lock
connectToDevice(device, rssi) connectToDevice(device, rssi)
} }
@@ -795,6 +787,7 @@ class BluetoothConnectionManager(
if (!hasBluetoothPermissions()) return if (!hasBluetoothPermissions()) return
val deviceAddress = device.address val deviceAddress = device.address
Log.d(TAG, "Connecting to bitchat device: $deviceAddress")
val gattCallback = object : BluetoothGattCallback() { val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
@@ -857,9 +850,7 @@ class BluetoothConnectionManager(
} }
} }
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
Log.d(TAG, "Client: Service discovery completed for $deviceAddress with status: $status")
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
val service = gatt.getService(SERVICE_UUID) val service = gatt.getService(SERVICE_UUID)
if (service != null) { if (service != null) {
@@ -916,16 +907,16 @@ class BluetoothConnectionManager(
} }
try { try {
Log.d(TAG, "Attempting GATT connection to $deviceAddress with autoConnect=false") Log.d(TAG, "Client: Attempting GATT connection to $deviceAddress with autoConnect=false")
val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
if (gatt == null) { if (gatt == null) {
Log.e(TAG, "connectGatt returned null for $deviceAddress") Log.e(TAG, "connectGatt returned null for $deviceAddress")
pendingConnections.remove(deviceAddress) pendingConnections.remove(deviceAddress)
} else { } else {
Log.d(TAG, "GATT connection initiated successfully for $deviceAddress") Log.d(TAG, "Client: GATT connection initiated successfully for $deviceAddress")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Exception connecting to $deviceAddress: ${e.message}") Log.e(TAG, "Client: Exception connecting to $deviceAddress: ${e.message}")
pendingConnections.remove(deviceAddress) pendingConnections.remove(deviceAddress)
} }
} }
@@ -296,9 +296,7 @@ class BluetoothMeshService(private val context: Context) {
Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID") Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID")
if (connectionManager.startServices()) { if (connectionManager.startServices()) {
isActive = true isActive = true
Log.i(TAG, "Bluetooth services started successfully")
// Send initial announcements after services are ready // Send initial announcements after services are ready
serviceScope.launch { serviceScope.launch {
delay(1000) delay(1000)
@@ -26,12 +26,12 @@ class PowerManager(private val context: Context) {
private const val MEDIUM_BATTERY = 50 private const val MEDIUM_BATTERY = 50
// Scan duty cycle periods (ms) // Scan duty cycle periods (ms)
private const val SCAN_ON_DURATION_NORMAL = 2000L // 2 seconds on private const val SCAN_ON_DURATION_NORMAL = 8000L // 8 seconds on
private const val SCAN_OFF_DURATION_NORMAL = 8000L // 8 seconds off private const val SCAN_OFF_DURATION_NORMAL = 2000L // 2 seconds off
private const val SCAN_ON_DURATION_POWER_SAVE = 1000L // 1 second on private const val SCAN_ON_DURATION_POWER_SAVE = 2000L // 2 seconds on
private const val SCAN_OFF_DURATION_POWER_SAVE = 15000L // 15 seconds off private const val SCAN_OFF_DURATION_POWER_SAVE = 8000L // 8 seconds off
private const val SCAN_ON_DURATION_ULTRA_LOW = 500L // 0.5 seconds on private const val SCAN_ON_DURATION_ULTRA_LOW = 1000L // 1 second on
private const val SCAN_OFF_DURATION_ULTRA_LOW = 30000L // 30 seconds off private const val SCAN_OFF_DURATION_ULTRA_LOW = 10000L // 10 seconds off
// Connection limits // Connection limits
private const val MAX_CONNECTIONS_NORMAL = 8 private const val MAX_CONNECTIONS_NORMAL = 8
@@ -112,38 +112,38 @@ class PowerManager(private val context: Context) {
/** /**
* Get scan settings optimized for current power mode * Get scan settings optimized for current power mode
*/ */
fun getScanSettings(): ScanSettings { fun getScanSettings(): ScanSettings {
// CRITICAL FIX: Set reportDelay to 0 for all modes. // CRITICAL FIX: Set reportDelay to 0 for all modes.
// When using a custom duty cycle, we want scan results delivered immediately, // When using a custom duty cycle, we want scan results delivered immediately,
// not batched. A non-zero report delay can conflict with the scan window, // not batched. A non-zero report delay can conflict with the scan window,
// causing missed results if the scan stops before the delay is met. // causing missed results if the scan stops before the delay is met.
val builder = ScanSettings.Builder() val builder = ScanSettings.Builder()
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
when (currentMode) { when (currentMode) {
PowerMode.PERFORMANCE -> builder PowerMode.PERFORMANCE -> builder
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
PowerMode.BALANCED -> builder PowerMode.BALANCED -> builder
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
PowerMode.POWER_SAVER -> builder PowerMode.POWER_SAVER -> builder
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY) .setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
PowerMode.ULTRA_LOW_POWER -> builder PowerMode.ULTRA_LOW_POWER -> builder
.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY) .setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
}
return builder.setReportDelay(0).build()
} }
return builder.setReportDelay(0).build()
}
/** /**
* Get advertising settings optimized for current power mode * Get advertising settings optimized for current power mode
@@ -91,7 +91,7 @@ class BluetoothStatusManager(
* This should be called on every app startup * This should be called on every app startup
*/ */
fun checkBluetoothStatus(): BluetoothStatus { fun checkBluetoothStatus(): BluetoothStatus {
Log.d(TAG, "Checking Bluetooth status") // Log.d(TAG, "Checking Bluetooth status")
return when { return when {
bluetoothAdapter == null -> { bluetoothAdapter == null -> {
@@ -103,7 +103,7 @@ class BluetoothStatusManager(
BluetoothStatus.DISABLED BluetoothStatus.DISABLED
} }
else -> { else -> {
Log.d(TAG, "Bluetooth is enabled and ready") // Log.d(TAG, "Bluetooth is enabled and ready")
BluetoothStatus.ENABLED BluetoothStatus.ENABLED
} }
} }
@@ -0,0 +1,297 @@
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.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
/**
* Screen shown when checking location services status or requesting location services enable
*/
@Composable
fun LocationCheckScreen(
status: LocationStatus,
onEnableLocation: () -> Unit,
onRetry: () -> Unit,
isLoading: Boolean = false
) {
val colorScheme = MaterialTheme.colorScheme
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
when (status) {
LocationStatus.DISABLED -> {
LocationDisabledContent(
onEnableLocation = onEnableLocation,
onRetry = onRetry,
colorScheme = colorScheme,
isLoading = isLoading
)
}
LocationStatus.NOT_AVAILABLE -> {
LocationNotAvailableContent(
colorScheme = colorScheme
)
}
LocationStatus.ENABLED -> {
LocationCheckingContent(
colorScheme = colorScheme
)
}
}
}
}
@Composable
private fun LocationDisabledContent(
onEnableLocation: () -> Unit,
onRetry: () -> Unit,
colorScheme: ColorScheme,
isLoading: Boolean
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Location icon - using LocationOn outlined icon in app's green color
Icon(
imageVector = Icons.Outlined.LocationOn,
contentDescription = "Location Services",
modifier = Modifier.size(64.dp),
tint = Color(0xFF00C851) // App's main green color
)
Text(
text = "Location Services Required",
style = MaterialTheme.typography.headlineSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = colorScheme.primary
),
textAlign = TextAlign.Center
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Privacy assurance section
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Filled.Security,
contentDescription = "Privacy",
tint = Color(0xFF4CAF50),
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Privacy First",
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Bold,
color = colorScheme.onSurface
)
)
}
Text(
text = "bitchat does NOT track your location or use GPS.\n\nLocation services are required by Android for Bluetooth scanning to work properly. This is an Android system requirement.",
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "bitchat needs location services for:",
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface
),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "• Bluetooth device scanning (Android requirement)\n" +
"• Discovering nearby users on mesh network\n" +
"• Creating connections without internet\n" +
"• No GPS tracking or location collection",
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
)
}
}
if (isLoading) {
LocationLoadingIndicator()
} else {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = onEnableLocation,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF00C851) // App's main green color
)
) {
Text(
text = "Open Location Settings",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
),
modifier = Modifier.padding(vertical = 4.dp)
)
}
OutlinedButton(
onClick = onRetry,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Check Again",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
),
modifier = Modifier.padding(vertical = 4.dp)
)
}
}
}
}
}
@Composable
private fun LocationNotAvailableContent(
colorScheme: ColorScheme
) {
Column(
verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Error icon
Icon(
imageVector = Icons.Filled.ErrorOutline,
contentDescription = "Error",
modifier = Modifier.size(64.dp),
tint = colorScheme.error
)
Text(
text = "Location Services Unavailable",
style = MaterialTheme.typography.headlineSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
color = colorScheme.error
),
textAlign = TextAlign.Center
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = colorScheme.errorContainer.copy(alpha = 0.1f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Text(
text = "Location services are not available on this device. This is unusual as location services are standard on Android devices.\n\nbitchat needs location services for Bluetooth scanning to work properly (Android requirement). Without this, the app cannot discover nearby users.",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface
),
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center
)
}
}
}
@Composable
private fun LocationCheckingContent(
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
),
textAlign = TextAlign.Center
)
LocationLoadingIndicator()
Text(
text = "Checking location services...",
style = MaterialTheme.typography.bodyLarge.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.7f)
)
)
}
}
@Composable
private fun LocationLoadingIndicator() {
// Animated rotation for the loading indicator
val infiniteTransition = rememberInfiniteTransition(label = "location_loading")
val rotationAngle by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "rotation"
)
Box(
modifier = Modifier.size(60.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier
.fillMaxSize()
.rotate(rotationAngle),
color = Color(0xFF4CAF50), // Location green
strokeWidth = 3.dp
)
}
}
@@ -0,0 +1,247 @@
package com.bitchat.android.onboarding
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.location.LocationManager
import android.os.Build
import android.provider.Settings
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
/**
* Manages Location Services enable/disable state and user prompts
* Checks location services status on every app startup
* Note: This is for system location services, not location permissions
*/
class LocationStatusManager(
private val activity: ComponentActivity,
private val context: Context,
private val onLocationEnabled: () -> Unit,
private val onLocationDisabled: (String) -> Unit
) {
companion object {
private const val TAG = "LocationStatusManager"
}
private var locationSettingsLauncher: ActivityResultLauncher<Intent>? = null
private var locationManager: LocationManager? = null
private var locationStateReceiver: BroadcastReceiver? = null
init {
setupLocationManager()
setupLocationSettingsLauncher()
setupLocationStateReceiver()
}
/**
* Setup LocationManager reference
*/
private fun setupLocationManager() {
try {
locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
Log.d(TAG, "LocationManager initialized: ${locationManager != null}")
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize LocationManager", e)
locationManager = null
}
}
/**
* Setup launcher for location settings request
*/
private fun setupLocationSettingsLauncher() {
locationSettingsLauncher = activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val isEnabled = isLocationEnabled()
Log.d(TAG, "Location settings request result: $isEnabled (result code: ${result.resultCode})")
if (isEnabled) {
onLocationEnabled()
} else {
onLocationDisabled("Location services are required for Bluetooth scanning on Android. Please enable location services to continue.")
}
}
}
/**
* Setup broadcast receiver to listen for location settings changes
*/
private fun setupLocationStateReceiver() {
locationStateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == LocationManager.MODE_CHANGED_ACTION ||
intent.action == LocationManager.PROVIDERS_CHANGED_ACTION) {
Log.d(TAG, "Location settings changed, checking status")
val isEnabled = isLocationEnabled()
if (isEnabled) {
onLocationEnabled()
} else {
onLocationDisabled("Location services have been disabled.")
}
}
}
}
// Register receiver for location changes
val filter = IntentFilter().apply {
addAction(LocationManager.MODE_CHANGED_ACTION)
addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
}
context.registerReceiver(locationStateReceiver, filter)
}
/**
* Check if location services are enabled (system-wide setting)
* Uses proper API depending on Android version
*/
fun isLocationEnabled(): Boolean {
return try {
locationManager?.let { lm ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// API 28+ (Android 9) - Modern approach
lm.isLocationEnabled
} else {
// Older devices - Check individual providers
lm.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
} ?: false
} catch (e: Exception) {
Log.w(TAG, "Error checking location enabled state: ${e.message}")
false
}
}
/**
* Check location services status
* This should be called on every app startup
*/
fun checkLocationStatus(): LocationStatus {
Log.d(TAG, "Checking location services status")
return when {
locationManager == null -> {
Log.e(TAG, "LocationManager not available on this device")
LocationStatus.NOT_AVAILABLE
}
!isLocationEnabled() -> {
Log.w(TAG, "Location services are disabled")
LocationStatus.DISABLED
}
else -> {
Log.d(TAG, "Location services are enabled and ready")
LocationStatus.ENABLED
}
}
}
/**
* Request user to enable location services
* Opens system location settings screen
*/
fun requestEnableLocation() {
Log.d(TAG, "Requesting user to enable location services")
try {
val enableLocationIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
locationSettingsLauncher?.launch(enableLocationIntent)
} catch (e: Exception) {
Log.e(TAG, "Failed to request location enable", e)
onLocationDisabled("Failed to open location settings: ${e.message}")
}
}
/**
* Handle location status check result
*/
fun handleLocationStatus(status: LocationStatus) {
when (status) {
LocationStatus.ENABLED -> {
Log.d(TAG, "Location services enabled, proceeding")
onLocationEnabled()
}
LocationStatus.DISABLED -> {
Log.d(TAG, "Location services disabled, requesting enable")
requestEnableLocation()
}
LocationStatus.NOT_AVAILABLE -> {
Log.e(TAG, "Location services not available")
onLocationDisabled("Location services are not available on this device.")
}
}
}
/**
* Get user-friendly status message
*/
fun getStatusMessage(status: LocationStatus): String {
return when (status) {
LocationStatus.ENABLED -> "Location services are enabled and ready"
LocationStatus.DISABLED -> "Location services are disabled. Please enable location services for Bluetooth scanning."
LocationStatus.NOT_AVAILABLE -> "Location services are not available on this device."
}
}
/**
* Get detailed diagnostics
*/
fun getDiagnostics(): String {
return buildString {
appendLine("Location Services Status Diagnostics:")
appendLine("LocationManager available: ${locationManager != null}")
appendLine("Location services enabled: ${isLocationEnabled()}")
appendLine("Current status: ${checkLocationStatus()}")
appendLine("Android version: ${Build.VERSION.SDK_INT}")
locationManager?.let { lm ->
try {
appendLine("GPS provider enabled: ${lm.isProviderEnabled(LocationManager.GPS_PROVIDER)}")
appendLine("Network provider enabled: ${lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)}")
} catch (e: Exception) {
appendLine("Provider details: [Error: ${e.message}]")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
appendLine("Using modern isLocationEnabled() API")
} else {
appendLine("Using legacy provider check API")
}
}
}
}
/**
* Log current location status for debugging
*/
fun logLocationStatus() {
Log.d(TAG, getDiagnostics())
}
/**
* Cleanup resources - call this when activity is destroyed
*/
fun cleanup() {
locationStateReceiver?.let { receiver ->
try {
context.unregisterReceiver(receiver)
Log.d(TAG, "Location state receiver unregistered")
} catch (e: Exception) {
Log.w(TAG, "Error unregistering location state receiver: ${e.message}")
}
}
}
}
/**
* Location services status enum
*/
enum class LocationStatus {
ENABLED,
DISABLED,
NOT_AVAILABLE
}