mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 02:25:21 +00:00
add location services check
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(),
|
||||
@@ -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 {
|
||||
|
||||
@@ -649,7 +649,7 @@ class BluetoothConnectionManager(
|
||||
// DEBUG: Log ALL scan results first
|
||||
val device = result.device
|
||||
val rssi = result.rssi
|
||||
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()}")
|
||||
@@ -789,7 +787,7 @@ class BluetoothConnectionManager(
|
||||
if (!hasBluetoothPermissions()) return
|
||||
|
||||
val deviceAddress = device.address
|
||||
Log.d(TAG, "Connecting to device: $deviceAddress")
|
||||
Log.d(TAG, "Connecting to bitchat device: $deviceAddress")
|
||||
|
||||
val gattCallback = object : BluetoothGattCallback() {
|
||||
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||
|
||||
@@ -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