mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 00:45:22 +00:00
Merge pull request #103 from permissionlesstech/adjust-scan-intervals
adjust scan intervals and reduce logging
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user