mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 07:25:20 +00:00
Manually disable location (#347)
* manually disable location, design needs to be fixed * fix ui
This commit is contained in:
@@ -72,11 +72,16 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
private val _isLoadingLocation = MutableLiveData(false)
|
private val _isLoadingLocation = MutableLiveData(false)
|
||||||
val isLoadingLocation: LiveData<Boolean> = _isLoadingLocation
|
val isLoadingLocation: LiveData<Boolean> = _isLoadingLocation
|
||||||
|
|
||||||
|
// Add a new LiveData property to track if location services are enabled by user
|
||||||
|
private val _locationServicesEnabled = MutableLiveData(false)
|
||||||
|
val locationServicesEnabled: LiveData<Boolean> = _locationServicesEnabled
|
||||||
|
|
||||||
init {
|
init {
|
||||||
updatePermissionState()
|
updatePermissionState()
|
||||||
// Initialize DataManager and load persisted channel selection
|
// Initialize DataManager and load persisted settings
|
||||||
dataManager = com.bitchat.android.ui.DataManager(context)
|
dataManager = com.bitchat.android.ui.DataManager(context)
|
||||||
loadPersistedChannelSelection()
|
loadPersistedChannelSelection()
|
||||||
|
loadLocationServicesState()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public API (matching iOS interface)
|
// MARK: - Public API (matching iOS interface)
|
||||||
@@ -108,7 +113,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
* Refresh available channels from current location
|
* Refresh available channels from current location
|
||||||
*/
|
*/
|
||||||
fun refreshChannels() {
|
fun refreshChannels() {
|
||||||
if (_permissionState.value == PermissionState.AUTHORIZED) {
|
if (_permissionState.value == PermissionState.AUTHORIZED && isLocationServicesEnabled()) {
|
||||||
requestOneShotLocation()
|
requestOneShotLocation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,13 +129,20 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isLocationServicesEnabled()) {
|
||||||
|
Log.w(TAG, "Cannot start live refresh - location services disabled by user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel existing timer
|
// Cancel existing timer
|
||||||
refreshTimer?.cancel()
|
refreshTimer?.cancel()
|
||||||
|
|
||||||
// Start new timer with coroutines
|
// Start new timer with coroutines
|
||||||
refreshTimer = CoroutineScope(Dispatchers.IO).launch {
|
refreshTimer = CoroutineScope(Dispatchers.IO).launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
requestOneShotLocation()
|
if (isLocationServicesEnabled()) {
|
||||||
|
requestOneShotLocation()
|
||||||
|
}
|
||||||
delay(interval)
|
delay(interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,6 +197,48 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
_teleported.postValue(teleported)
|
_teleported.postValue(teleported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable location services (user-controlled toggle)
|
||||||
|
*/
|
||||||
|
fun enableLocationServices() {
|
||||||
|
Log.d(TAG, "enableLocationServices() called by user")
|
||||||
|
_locationServicesEnabled.postValue(true)
|
||||||
|
saveLocationServicesState(true)
|
||||||
|
|
||||||
|
// If we have permission, start location operations
|
||||||
|
if (_permissionState.value == PermissionState.AUTHORIZED) {
|
||||||
|
requestOneShotLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable location services (user-controlled toggle)
|
||||||
|
*/
|
||||||
|
fun disableLocationServices() {
|
||||||
|
Log.d(TAG, "disableLocationServices() called by user")
|
||||||
|
_locationServicesEnabled.postValue(false)
|
||||||
|
saveLocationServicesState(false)
|
||||||
|
|
||||||
|
// Stop any ongoing location operations
|
||||||
|
endLiveRefresh()
|
||||||
|
|
||||||
|
// Clear available channels when location is disabled
|
||||||
|
_availableChannels.postValue(emptyList())
|
||||||
|
_locationNames.postValue(emptyMap())
|
||||||
|
|
||||||
|
// If user had a location channel selected, switch back to mesh
|
||||||
|
if (_selectedChannel.value is ChannelID.Location) {
|
||||||
|
select(ChannelID.Mesh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if location services are enabled by the user
|
||||||
|
*/
|
||||||
|
fun isLocationServicesEnabled(): Boolean {
|
||||||
|
return _locationServicesEnabled.value ?: false
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Location Operations
|
// MARK: - Location Operations
|
||||||
|
|
||||||
private fun requestOneShotLocation() {
|
private fun requestOneShotLocation() {
|
||||||
@@ -572,6 +626,34 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
Log.d(TAG, "Cleared persisted channel selection")
|
Log.d(TAG, "Cleared persisted channel selection")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Location Services State Persistence
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save location services enabled state to persistent storage
|
||||||
|
*/
|
||||||
|
private fun saveLocationServicesState(enabled: Boolean) {
|
||||||
|
try {
|
||||||
|
dataManager?.saveLocationServicesEnabled(enabled)
|
||||||
|
Log.d(TAG, "Saved location services state: $enabled")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to save location services state: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load persisted location services state from storage
|
||||||
|
*/
|
||||||
|
private fun loadLocationServicesState() {
|
||||||
|
try {
|
||||||
|
val enabled = dataManager?.isLocationServicesEnabled() ?: false
|
||||||
|
_locationServicesEnabled.postValue(enabled)
|
||||||
|
Log.d(TAG, "Loaded location services state: $enabled")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to load location services state: ${e.message}")
|
||||||
|
_locationServicesEnabled.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup resources
|
* Cleanup resources
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -62,6 +62,17 @@ class DataManager(private val context: Context) {
|
|||||||
Log.d(TAG, "Cleared last geohash channel")
|
Log.d(TAG, "Cleared last geohash channel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Location Services State
|
||||||
|
|
||||||
|
fun saveLocationServicesEnabled(enabled: Boolean) {
|
||||||
|
prefs.edit().putBoolean("location_services_enabled", enabled).apply()
|
||||||
|
Log.d(TAG, "Saved location services enabled state: $enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLocationServicesEnabled(): Boolean {
|
||||||
|
return prefs.getBoolean("location_services_enabled", false)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Channel Data Management
|
// MARK: - Channel Data Management
|
||||||
|
|
||||||
fun loadChannelData(): Pair<Set<String>, Set<String>> {
|
fun loadChannelData(): Pair<Set<String>, Set<String>> {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.text.BasicTextField
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PinDrop
|
import androidx.compose.material.icons.filled.PinDrop
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -20,7 +21,6 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||||
@@ -52,6 +52,7 @@ fun LocationChannelsSheet(
|
|||||||
val selectedChannel by locationManager.selectedChannel.observeAsState()
|
val selectedChannel by locationManager.selectedChannel.observeAsState()
|
||||||
val teleported by locationManager.teleported.observeAsState(false)
|
val teleported by locationManager.teleported.observeAsState(false)
|
||||||
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
|
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
|
||||||
|
val locationServicesEnabled by locationManager.locationServicesEnabled.observeAsState(false)
|
||||||
|
|
||||||
// CRITICAL FIX: Observe reactive participant counts for real-time updates
|
// CRITICAL FIX: Observe reactive participant counts for real-time updates
|
||||||
val geohashParticipantCounts by viewModel.geohashParticipantCounts.observeAsState(emptyMap())
|
val geohashParticipantCounts by viewModel.geohashParticipantCounts.observeAsState(emptyMap())
|
||||||
@@ -109,59 +110,76 @@ fun LocationChannelsSheet(
|
|||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Permission handling
|
// Location Services Control - Show permission handling if enabled
|
||||||
when (permissionState) {
|
if (locationServicesEnabled) {
|
||||||
LocationChannelManager.PermissionState.NOT_DETERMINED -> {
|
when (permissionState) {
|
||||||
Button(
|
LocationChannelManager.PermissionState.NOT_DETERMINED -> {
|
||||||
onClick = { locationManager.enableLocationChannels() },
|
Button(
|
||||||
colors = ButtonDefaults.buttonColors(
|
onClick = { locationManager.enableLocationChannels() },
|
||||||
containerColor = standardGreen.copy(alpha = 0.12f),
|
colors = ButtonDefaults.buttonColors(
|
||||||
contentColor = standardGreen
|
containerColor = standardGreen.copy(alpha = 0.12f),
|
||||||
),
|
contentColor = standardGreen
|
||||||
modifier = Modifier.fillMaxWidth()
|
),
|
||||||
) {
|
modifier = Modifier.fillMaxWidth()
|
||||||
Text(
|
|
||||||
text = "get location and my geohashes",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontFamily = FontFamily.Monospace
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationChannelManager.PermissionState.DENIED,
|
|
||||||
LocationChannelManager.PermissionState.RESTRICTED -> {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = "location permission denied. enable in settings to use location channels.",
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
|
||||||
)
|
|
||||||
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = Uri.fromParts("package", context.packageName, null)
|
|
||||||
}
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "open settings",
|
text = "grant location permission",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontFamily = FontFamily.Monospace
|
fontFamily = FontFamily.Monospace
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LocationChannelManager.PermissionState.AUTHORIZED -> {
|
LocationChannelManager.PermissionState.DENIED,
|
||||||
// Authorized - show channels below
|
LocationChannelManager.PermissionState.RESTRICTED -> {
|
||||||
}
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = "location permission denied. enable in settings to use location channels.",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = Color.Red.copy(alpha = 0.8f)
|
||||||
|
)
|
||||||
|
|
||||||
null -> {
|
TextButton(
|
||||||
// Loading state
|
onClick = {
|
||||||
CircularProgressIndicator()
|
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", context.packageName, null)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "open settings",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationChannelManager.PermissionState.AUTHORIZED -> {
|
||||||
|
Text(
|
||||||
|
text = "✓ location permission granted",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = standardGreen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "checking permissions...",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,8 +203,8 @@ fun LocationChannelsSheet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nearby options
|
// Nearby options (only show if location services are enabled)
|
||||||
if (availableChannels.isNotEmpty()) {
|
if (availableChannels.isNotEmpty() && locationServicesEnabled) {
|
||||||
items(availableChannels) { channel ->
|
items(availableChannels) { channel ->
|
||||||
val coverage = coverageString(channel.geohash.length)
|
val coverage = coverageString(channel.geohash.length)
|
||||||
val nameBase = locationNames[channel.level]
|
val nameBase = locationNames[channel.level]
|
||||||
@@ -210,7 +228,7 @@ fun LocationChannelsSheet(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED) {
|
} else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) {
|
||||||
item {
|
item {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
@@ -348,28 +366,39 @@ fun LocationChannelsSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer action - remove location access
|
// Location services toggle button
|
||||||
if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED) {
|
item {
|
||||||
item {
|
Button(
|
||||||
Button(
|
onClick = {
|
||||||
onClick = {
|
if (locationServicesEnabled) {
|
||||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
locationManager.disableLocationServices()
|
||||||
data = Uri.fromParts("package", context.packageName, null)
|
} else {
|
||||||
}
|
locationManager.enableLocationServices()
|
||||||
context.startActivity(intent)
|
}
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = if (locationServicesEnabled) {
|
||||||
|
Color.Red.copy(alpha = 0.08f)
|
||||||
|
} else {
|
||||||
|
standardGreen.copy(alpha = 0.12f)
|
||||||
},
|
},
|
||||||
colors = ButtonDefaults.buttonColors(
|
contentColor = if (locationServicesEnabled) {
|
||||||
containerColor = Color.Red.copy(alpha = 0.08f),
|
Color(0xFFBF1A1A)
|
||||||
contentColor = Color(0xFFBF1A1A)
|
} else {
|
||||||
),
|
standardGreen
|
||||||
modifier = Modifier.fillMaxWidth()
|
}
|
||||||
) {
|
),
|
||||||
Text(
|
modifier = Modifier.fillMaxWidth()
|
||||||
text = "remove location access",
|
) {
|
||||||
fontSize = 12.sp,
|
Text(
|
||||||
fontFamily = FontFamily.Monospace
|
text = if (locationServicesEnabled) {
|
||||||
)
|
"disable location services"
|
||||||
}
|
} else {
|
||||||
|
"enable location services"
|
||||||
|
},
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontFamily = FontFamily.Monospace
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,12 +409,14 @@ fun LocationChannelsSheet(
|
|||||||
// Lifecycle management
|
// Lifecycle management
|
||||||
LaunchedEffect(isPresented) {
|
LaunchedEffect(isPresented) {
|
||||||
if (isPresented) {
|
if (isPresented) {
|
||||||
// Refresh channels when opening
|
// Refresh channels when opening (only if location services are enabled)
|
||||||
if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED) {
|
if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) {
|
||||||
locationManager.refreshChannels()
|
locationManager.refreshChannels()
|
||||||
}
|
}
|
||||||
// Begin periodic refresh while sheet is open
|
// Begin periodic refresh while sheet is open (only if location services are enabled)
|
||||||
locationManager.beginLiveRefresh()
|
if (locationServicesEnabled) {
|
||||||
|
locationManager.beginLiveRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
// Begin multi-channel sampling for counts
|
// Begin multi-channel sampling for counts
|
||||||
val geohashes = availableChannels.map { it.geohash }
|
val geohashes = availableChannels.map { it.geohash }
|
||||||
@@ -398,7 +429,14 @@ fun LocationChannelsSheet(
|
|||||||
|
|
||||||
// React to permission changes
|
// React to permission changes
|
||||||
LaunchedEffect(permissionState) {
|
LaunchedEffect(permissionState) {
|
||||||
if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED) {
|
if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) {
|
||||||
|
locationManager.refreshChannels()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// React to location services enable/disable
|
||||||
|
LaunchedEffect(locationServicesEnabled) {
|
||||||
|
if (locationServicesEnabled && permissionState == LocationChannelManager.PermissionState.AUTHORIZED) {
|
||||||
locationManager.refreshChannels()
|
locationManager.refreshChannels()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user