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 onboardingCoordinator: OnboardingCoordinator
private lateinit var bluetoothStatusManager: BluetoothStatusManager
private lateinit var locationStatusManager: LocationStatusManager
// Core mesh service - managed at app level
private lateinit var meshService: BluetoothMeshService
@@ -48,12 +49,15 @@ class MainActivity : ComponentActivity() {
// UI state for onboarding flow
private var onboardingState by mutableStateOf(OnboardingState.CHECKING)
private var bluetoothStatus by mutableStateOf(BluetoothStatus.ENABLED)
private var locationStatus by mutableStateOf(LocationStatus.ENABLED)
private var errorMessage by mutableStateOf("")
private var isBluetoothLoading by mutableStateOf(false)
private var isLocationLoading by mutableStateOf(false)
enum class OnboardingState {
CHECKING,
BLUETOOTH_CHECK,
LOCATION_CHECK,
PERMISSION_EXPLANATION,
PERMISSION_REQUESTING,
INITIALIZING,
@@ -75,6 +79,12 @@ class MainActivity : ComponentActivity() {
onBluetoothEnabled = ::handleBluetoothEnabled,
onBluetoothDisabled = ::handleBluetoothDisabled
)
locationStatusManager = LocationStatusManager(
activity = this,
context = this,
onLocationEnabled = ::handleLocationEnabled,
onLocationDisabled = ::handleLocationDisabled
)
onboardingCoordinator = OnboardingCoordinator(
activity = this,
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 -> {
PermissionExplanationScreen(
permissionCategories = permissionManager.getCategorizedPermissions(),
@@ -189,7 +213,7 @@ class MainActivity : ComponentActivity() {
* Check Bluetooth status and proceed with onboarding flow
*/
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
// We'll check Bluetooth after permissions are granted
@@ -205,8 +229,8 @@ class MainActivity : ComponentActivity() {
when (bluetoothStatus) {
BluetoothStatus.ENABLED -> {
// Bluetooth is enabled, proceed with permission/onboarding check
proceedWithPermissionCheck()
// Bluetooth is enabled, check location services next
checkLocationAndProceed()
}
BluetoothStatus.DISABLED -> {
// 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")
isBluetoothLoading = false
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()
}
/**
* 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
@@ -289,20 +382,33 @@ class MainActivity : ComponentActivity() {
}
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()
if (currentBluetoothStatus == BluetoothStatus.ENABLED) {
// Bluetooth is enabled, proceed to app initialization
onboardingState = OnboardingState.INITIALIZING
initializeApp()
} else {
// 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.")
bluetoothStatus = currentBluetoothStatus
onboardingState = OnboardingState.BLUETOOTH_CHECK
isBluetoothLoading = false
val currentLocationStatus = locationStatusManager.checkLocationStatus()
when {
currentBluetoothStatus != BluetoothStatus.ENABLED -> {
// 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.")
bluetoothStatus = currentBluetoothStatus
onboardingState = OnboardingState.BLUETOOTH_CHECK
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() {
super.onResume()
// Check Bluetooth status on resume and handle accordingly
// Check Bluetooth and Location status on resume and handle accordingly
if (onboardingState == OnboardingState.COMPLETE) {
// Set app foreground state
meshService.connectionManager.setAppBackgroundState(false)
@@ -376,6 +482,16 @@ class MainActivity : ComponentActivity() {
bluetoothStatus = currentBluetoothStatus
onboardingState = OnboardingState.BLUETOOTH_CHECK
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() {
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
if (onboardingState == OnboardingState.COMPLETE) {
try {
@@ -151,7 +151,7 @@ class BluetoothConnectionManager(
startPeriodicCleanup()
Log.i(TAG, "Power-optimized Bluetooth services started successfully")
Log.i(TAG, "Bluetooth services started successfully")
}
return true
@@ -316,7 +316,8 @@ class BluetoothConnectionManager(
*/
fun getDebugInfo(): String {
return buildString {
appendLine("=== Power-Optimized Bluetooth Connection Manager ===")
appendLine("=== Bluetooth Connection Manager ===")
appendLine("Bluetooth MAC Address: ${bluetoothAdapter?.address}")
appendLine("Active: $isActive")
appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}")
appendLine("Has Permissions: ${hasBluetoothPermissions()}")
@@ -648,8 +649,7 @@ class BluetoothConnectionManager(
// DEBUG: Log ALL scan results first
val device = result.device
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)
}
@@ -736,9 +736,7 @@ class BluetoothConnectionManager(
} else {
null
}
Log.d(TAG, "Processing bitchat device: $deviceAddress, name: '$deviceName', peerID: $extractedPeerID, RSSI: $rssi")
// Power-aware RSSI filtering
if (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
// Check if already connected OR already attempting to connect
if (connectedDevices.containsKey(deviceAddress)) {
Log.d(TAG, "Device $deviceAddress already connected, skipping")
// Log.d(TAG, "Device $deviceAddress already connected, skipping")
return
}
@@ -779,12 +777,6 @@ class BluetoothConnectionManager(
val attempts = (currentAttempt?.attempts ?: 0) + 1
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
connectToDevice(device, rssi)
}
@@ -795,6 +787,7 @@ class BluetoothConnectionManager(
if (!hasBluetoothPermissions()) return
val deviceAddress = device.address
Log.d(TAG, "Connecting to bitchat device: $deviceAddress")
val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
@@ -857,9 +850,7 @@ class BluetoothConnectionManager(
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
Log.d(TAG, "Client: Service discovery completed for $deviceAddress with status: $status")
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val service = gatt.getService(SERVICE_UUID)
if (service != null) {
@@ -916,16 +907,16 @@ class BluetoothConnectionManager(
}
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)
if (gatt == null) {
Log.e(TAG, "connectGatt returned null for $deviceAddress")
pendingConnections.remove(deviceAddress)
} else {
Log.d(TAG, "GATT connection initiated successfully for $deviceAddress")
Log.d(TAG, "Client: GATT connection initiated successfully for $deviceAddress")
}
} 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)
}
}
@@ -296,9 +296,7 @@ class BluetoothMeshService(private val context: Context) {
Log.i(TAG, "Starting Bluetooth mesh service with peer ID: $myPeerID")
if (connectionManager.startServices()) {
isActive = true
Log.i(TAG, "Bluetooth services started successfully")
isActive = true
// Send initial announcements after services are ready
serviceScope.launch {
delay(1000)
@@ -26,12 +26,12 @@ class PowerManager(private val context: Context) {
private const val MEDIUM_BATTERY = 50
// Scan duty cycle periods (ms)
private const val SCAN_ON_DURATION_NORMAL = 2000L // 2 seconds on
private const val SCAN_OFF_DURATION_NORMAL = 8000L // 8 seconds off
private const val SCAN_ON_DURATION_POWER_SAVE = 1000L // 1 second on
private const val SCAN_OFF_DURATION_POWER_SAVE = 15000L // 15 seconds off
private const val SCAN_ON_DURATION_ULTRA_LOW = 500L // 0.5 seconds on
private const val SCAN_OFF_DURATION_ULTRA_LOW = 30000L // 30 seconds off
private const val SCAN_ON_DURATION_NORMAL = 8000L // 8 seconds on
private const val SCAN_OFF_DURATION_NORMAL = 2000L // 2 seconds off
private const val SCAN_ON_DURATION_POWER_SAVE = 2000L // 2 seconds on
private const val SCAN_OFF_DURATION_POWER_SAVE = 8000L // 8 seconds off
private const val SCAN_ON_DURATION_ULTRA_LOW = 1000L // 1 second on
private const val SCAN_OFF_DURATION_ULTRA_LOW = 10000L // 10 seconds off
// Connection limits
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
*/
fun getScanSettings(): ScanSettings {
// CRITICAL FIX: Set reportDelay to 0 for all modes.
// 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,
// causing missed results if the scan stops before the delay is met.
val builder = ScanSettings.Builder()
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
fun getScanSettings(): ScanSettings {
// CRITICAL FIX: Set reportDelay to 0 for all modes.
// 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,
// causing missed results if the scan stops before the delay is met.
val builder = ScanSettings.Builder()
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
when (currentMode) {
PowerMode.PERFORMANCE -> builder
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
when (currentMode) {
PowerMode.PERFORMANCE -> builder
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
PowerMode.BALANCED -> builder
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
PowerMode.BALANCED -> builder
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
PowerMode.POWER_SAVER -> builder
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
PowerMode.POWER_SAVER -> builder
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
PowerMode.ULTRA_LOW_POWER -> builder
.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
}
return builder.setReportDelay(0).build()
PowerMode.ULTRA_LOW_POWER -> builder
.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
}
return builder.setReportDelay(0).build()
}
/**
* Get advertising settings optimized for current power mode
@@ -91,7 +91,7 @@ class BluetoothStatusManager(
* This should be called on every app startup
*/
fun checkBluetoothStatus(): BluetoothStatus {
Log.d(TAG, "Checking Bluetooth status")
// Log.d(TAG, "Checking Bluetooth status")
return when {
bluetoothAdapter == null -> {
@@ -103,7 +103,7 @@ class BluetoothStatusManager(
BluetoothStatus.DISABLED
}
else -> {
Log.d(TAG, "Bluetooth is enabled and ready")
// Log.d(TAG, "Bluetooth is enabled and ready")
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
}