mirror of
https://github.com/torlando-tech/columba.git
synced 2025-12-22 05:37:07 +00:00
feat: implement configurable location precision with uncertainty circles
Add configurable location coarsening for privacy: - Presets: Precise, Neighborhood (~100m), City (~1km), Region (~10km) - Sender coarsens coordinates to grid before sending - Sends approxRadius in telemetry so recipient knows precision - Recipient renders semi-transparent uncertainty circle on map - Fix settings persistence when navigating away from settings page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import kotlinx.serialization.Serializable
|
||||
* @property ts Timestamp when location was captured (millis since epoch)
|
||||
* @property expires When sharing ends (millis since epoch), null for indefinite
|
||||
* @property cease If true, recipient should delete sender's location (sharing stopped)
|
||||
* @property approxRadius Coarsening radius in meters (0 = precise, >0 = approximate)
|
||||
*/
|
||||
@Serializable
|
||||
data class LocationTelemetry(
|
||||
@@ -24,6 +25,7 @@ data class LocationTelemetry(
|
||||
val ts: Long,
|
||||
val expires: Long? = null,
|
||||
val cease: Boolean = false,
|
||||
val approxRadius: Int = 0,
|
||||
) {
|
||||
companion object {
|
||||
const val TYPE_LOCATION_SHARE = "location_share"
|
||||
|
||||
@@ -86,7 +86,7 @@ class SettingsRepository
|
||||
// Location sharing preferences
|
||||
val LOCATION_SHARING_ENABLED = booleanPreferencesKey("location_sharing_enabled")
|
||||
val DEFAULT_SHARING_DURATION = stringPreferencesKey("default_sharing_duration")
|
||||
val LOCATION_PRECISION = stringPreferencesKey("location_precision")
|
||||
val LOCATION_PRECISION_RADIUS = intPreferencesKey("location_precision_radius")
|
||||
}
|
||||
|
||||
// Notification preferences
|
||||
@@ -867,25 +867,25 @@ class SettingsRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Flow of the location precision setting.
|
||||
* Values: "PRECISE" (GPS accuracy) or "APPROXIMATE" (reduced accuracy).
|
||||
* Defaults to "PRECISE" if not set.
|
||||
* Flow of the location precision radius in meters.
|
||||
* 0 = Precise (no coarsening), >0 = coarsening radius in meters.
|
||||
* Defaults to 0 (precise) if not set.
|
||||
*/
|
||||
val locationPrecisionFlow: Flow<String> =
|
||||
val locationPrecisionRadiusFlow: Flow<Int> =
|
||||
context.dataStore.data
|
||||
.map { preferences ->
|
||||
preferences[PreferencesKeys.LOCATION_PRECISION] ?: "PRECISE"
|
||||
preferences[PreferencesKeys.LOCATION_PRECISION_RADIUS] ?: 0
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Save the location precision setting.
|
||||
* Save the location precision radius.
|
||||
*
|
||||
* @param precision "PRECISE" or "APPROXIMATE"
|
||||
* @param radiusMeters 0 for precise, or coarsening radius in meters (100, 1000, 10000, etc.)
|
||||
*/
|
||||
suspend fun saveLocationPrecision(precision: String) {
|
||||
suspend fun saveLocationPrecisionRadius(radiusMeters: Int) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.LOCATION_PRECISION] = precision
|
||||
preferences[PreferencesKeys.LOCATION_PRECISION_RADIUS] = radiusMeters
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.lxmf.messenger.data.db.dao.ReceivedLocationDao
|
||||
import com.lxmf.messenger.data.db.entity.ReceivedLocationEntity
|
||||
import com.lxmf.messenger.data.model.LocationTelemetry
|
||||
import com.lxmf.messenger.di.ApplicationScope
|
||||
import com.lxmf.messenger.repository.SettingsRepository
|
||||
import com.lxmf.messenger.reticulum.protocol.ReticulumProtocol
|
||||
import com.lxmf.messenger.reticulum.protocol.ServiceReticulumProtocol
|
||||
import com.lxmf.messenger.ui.model.SharingDuration
|
||||
@@ -27,6 +28,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -35,6 +37,7 @@ import org.json.JSONObject
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Represents an active location sharing session.
|
||||
@@ -67,6 +70,7 @@ class LocationSharingManager
|
||||
@ApplicationContext private val context: Context,
|
||||
private val reticulumProtocol: ReticulumProtocol,
|
||||
private val receivedLocationDao: ReceivedLocationDao,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
@ApplicationScope private val scope: CoroutineScope,
|
||||
) {
|
||||
companion object {
|
||||
@@ -375,13 +379,20 @@ class LocationSharingManager
|
||||
// Find the first expired session end time among all sessions
|
||||
val earliestExpiry = sessions.mapNotNull { it.endTime }.minOrNull()
|
||||
|
||||
// Get precision radius setting (0 = precise, >0 = coarsen to that radius)
|
||||
val precisionRadius = settingsRepository.locationPrecisionRadiusFlow.first()
|
||||
|
||||
// Coarsen location if needed
|
||||
val (finalLat, finalLng) = coarsenLocation(location.latitude, location.longitude, precisionRadius)
|
||||
|
||||
val telemetry =
|
||||
LocationTelemetry(
|
||||
lat = location.latitude,
|
||||
lng = location.longitude,
|
||||
acc = location.accuracy,
|
||||
lat = finalLat,
|
||||
lng = finalLng,
|
||||
acc = if (precisionRadius > 0) precisionRadius.toFloat() else location.accuracy,
|
||||
ts = System.currentTimeMillis(),
|
||||
expires = earliestExpiry,
|
||||
approxRadius = precisionRadius,
|
||||
)
|
||||
|
||||
val json = Json.encodeToString(telemetry)
|
||||
@@ -399,7 +410,7 @@ class LocationSharingManager
|
||||
)
|
||||
|
||||
result.onSuccess {
|
||||
Log.d(TAG, "Location sent to ${session.displayName}")
|
||||
Log.d(TAG, "Location sent to ${session.displayName} (approxRadius=$precisionRadius)")
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "Failed to send location to ${session.displayName}", e)
|
||||
}
|
||||
@@ -410,6 +421,24 @@ class LocationSharingManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Coarsen location coordinates to a grid based on the specified radius.
|
||||
*
|
||||
* @param lat Latitude in decimal degrees
|
||||
* @param lng Longitude in decimal degrees
|
||||
* @param radiusMeters Coarsening radius in meters (0 = no coarsening)
|
||||
* @return Pair of coarsened (lat, lng)
|
||||
*/
|
||||
private fun coarsenLocation(lat: Double, lng: Double, radiusMeters: Int): Pair<Double, Double> {
|
||||
if (radiusMeters <= 0) return Pair(lat, lng)
|
||||
|
||||
// Convert radius to degrees (approximate: 111km per degree at equator)
|
||||
val gridSizeDegrees = radiusMeters / 111_000.0
|
||||
val coarseLat = (lat / gridSizeDegrees).roundToInt() * gridSizeDegrees
|
||||
val coarseLng = (lng / gridSizeDegrees).roundToInt() * gridSizeDegrees
|
||||
return Pair(coarseLat, coarseLng)
|
||||
}
|
||||
|
||||
private fun startListeningForLocationTelemetry() {
|
||||
// Cast to ServiceReticulumProtocol to access the locationTelemetryFlow
|
||||
val serviceProtocol = reticulumProtocol as? ServiceReticulumProtocol
|
||||
@@ -450,6 +479,7 @@ class LocationSharingManager
|
||||
val acc = json.getDouble("acc").toFloat()
|
||||
val ts = json.getLong("ts")
|
||||
val expires = if (json.has("expires") && !json.isNull("expires")) json.getLong("expires") else null
|
||||
val approxRadius = json.optInt("approxRadius", 0)
|
||||
|
||||
val entity =
|
||||
ReceivedLocationEntity(
|
||||
@@ -461,10 +491,11 @@ class LocationSharingManager
|
||||
timestamp = ts,
|
||||
expiresAt = expires,
|
||||
receivedAt = System.currentTimeMillis(),
|
||||
approximateRadius = approxRadius,
|
||||
)
|
||||
|
||||
receivedLocationDao.insert(entity)
|
||||
Log.d(TAG, "Stored location from $senderHash: ($lat, $lng)")
|
||||
Log.d(TAG, "Stored location from $senderHash: ($lat, $lng) approxRadius=$approxRadius")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse/store received location: $locationJson", e)
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ fun MapScreen(
|
||||
val sourceId = "contact-markers-source"
|
||||
val layerId = "contact-markers-layer"
|
||||
|
||||
// Create GeoJSON features from contact markers with state property
|
||||
// Create GeoJSON features from contact markers with state and approximateRadius properties
|
||||
val features = state.contactMarkers.map { marker ->
|
||||
Feature.fromGeometry(
|
||||
Point.fromLngLat(marker.longitude, marker.latitude)
|
||||
@@ -311,6 +311,7 @@ fun MapScreen(
|
||||
addStringProperty("name", marker.displayName)
|
||||
addStringProperty("hash", marker.destinationHash)
|
||||
addStringProperty("state", marker.state.name) // FRESH, STALE, or EXPIRED_GRACE_PERIOD
|
||||
addNumberProperty("approximateRadius", marker.approximateRadius) // meters, 0 = precise
|
||||
}
|
||||
}
|
||||
val featureCollection = FeatureCollection.fromFeatures(features)
|
||||
@@ -323,6 +324,47 @@ fun MapScreen(
|
||||
// Add new source and layers with data-driven styling based on marker state
|
||||
style.addSource(GeoJsonSource(sourceId, featureCollection))
|
||||
|
||||
// Uncertainty circle layer for approximate locations (rendered behind main marker)
|
||||
// Only visible when approximateRadius > 0
|
||||
val uncertaintyLayerId = "contact-markers-uncertainty-layer"
|
||||
style.addLayer(
|
||||
CircleLayer(uncertaintyLayerId, sourceId).withProperties(
|
||||
// Circle radius scales with zoom - converts meters to screen pixels
|
||||
// At zoom 15, 1 pixel ≈ 1 meter, so we scale accordingly
|
||||
PropertyFactory.circleRadius(
|
||||
Expression.interpolate(
|
||||
Expression.linear(),
|
||||
Expression.zoom(),
|
||||
// At lower zooms, show smaller radius (it's farther out)
|
||||
Expression.stop(10, Expression.division(Expression.get("approximateRadius"), Expression.literal(30))),
|
||||
Expression.stop(12, Expression.division(Expression.get("approximateRadius"), Expression.literal(10))),
|
||||
Expression.stop(15, Expression.division(Expression.get("approximateRadius"), Expression.literal(3))),
|
||||
Expression.stop(18, Expression.product(Expression.get("approximateRadius"), Expression.literal(0.8))),
|
||||
)
|
||||
),
|
||||
// Semi-transparent fill
|
||||
PropertyFactory.circleColor(
|
||||
Expression.color(android.graphics.Color.parseColor("#FF5722")) // Orange
|
||||
),
|
||||
PropertyFactory.circleOpacity(
|
||||
Expression.literal(0.15f)
|
||||
),
|
||||
// Dashed stroke for the uncertainty boundary
|
||||
PropertyFactory.circleStrokeWidth(
|
||||
Expression.literal(2f)
|
||||
),
|
||||
PropertyFactory.circleStrokeColor(
|
||||
Expression.color(android.graphics.Color.parseColor("#FF5722")) // Orange
|
||||
),
|
||||
PropertyFactory.circleStrokeOpacity(
|
||||
Expression.literal(0.4f)
|
||||
),
|
||||
).withFilter(
|
||||
// Only show for locations with approximateRadius > 0
|
||||
Expression.gt(Expression.get("approximateRadius"), Expression.literal(0))
|
||||
)
|
||||
)
|
||||
|
||||
// CircleLayer for the filled circle
|
||||
style.addLayer(
|
||||
CircleLayer(layerId, sourceId).withProperties(
|
||||
|
||||
@@ -178,8 +178,8 @@ fun SettingsScreen(
|
||||
onStopAllSharing = { viewModel.stopAllSharing() },
|
||||
defaultDuration = state.defaultSharingDuration,
|
||||
onDefaultDurationChange = { viewModel.setDefaultSharingDuration(it) },
|
||||
locationPrecision = state.locationPrecision,
|
||||
onLocationPrecisionChange = { viewModel.setLocationPrecision(it) },
|
||||
locationPrecisionRadius = state.locationPrecisionRadius,
|
||||
onLocationPrecisionRadiusChange = { viewModel.setLocationPrecisionRadius(it) },
|
||||
)
|
||||
|
||||
MessageDeliveryRetrievalCard(
|
||||
|
||||
@@ -60,8 +60,8 @@ fun LocationSharingCard(
|
||||
onStopAllSharing: () -> Unit,
|
||||
defaultDuration: String,
|
||||
onDefaultDurationChange: (String) -> Unit,
|
||||
locationPrecision: String,
|
||||
onLocationPrecisionChange: (String) -> Unit,
|
||||
locationPrecisionRadius: Int,
|
||||
onLocationPrecisionRadiusChange: (Int) -> Unit,
|
||||
) {
|
||||
var showDurationPicker by remember { mutableStateOf(false) }
|
||||
var showPrecisionPicker by remember { mutableStateOf(false) }
|
||||
@@ -136,7 +136,7 @@ fun LocationSharingCard(
|
||||
// Location precision picker
|
||||
SettingsRow(
|
||||
label = "Location precision",
|
||||
value = getPrecisionDisplayText(locationPrecision),
|
||||
value = getPrecisionRadiusDisplayText(locationPrecisionRadius),
|
||||
onClick = { showPrecisionPicker = true },
|
||||
)
|
||||
}
|
||||
@@ -156,10 +156,10 @@ fun LocationSharingCard(
|
||||
|
||||
// Precision picker dialog
|
||||
if (showPrecisionPicker) {
|
||||
PrecisionPickerDialog(
|
||||
currentPrecision = locationPrecision,
|
||||
onPrecisionSelected = {
|
||||
onLocationPrecisionChange(it)
|
||||
PrecisionRadiusPickerDialog(
|
||||
currentRadius = locationPrecisionRadius,
|
||||
onRadiusSelected = {
|
||||
onLocationPrecisionRadiusChange(it)
|
||||
showPrecisionPicker = false
|
||||
},
|
||||
onDismiss = { showPrecisionPicker = false },
|
||||
@@ -311,10 +311,21 @@ private fun DurationPickerDialog(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Precision radius presets for the picker.
|
||||
*/
|
||||
private enum class PrecisionPreset(val radiusMeters: Int, val displayName: String, val description: String) {
|
||||
PRECISE(0, "Precise", "Exact GPS location"),
|
||||
NEIGHBORHOOD(100, "Neighborhood", "~100m radius"),
|
||||
CITY(1000, "City", "~1km radius"),
|
||||
REGION(10000, "Region", "~10km radius"),
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun PrecisionPickerDialog(
|
||||
currentPrecision: String,
|
||||
onPrecisionSelected: (String) -> Unit,
|
||||
private fun PrecisionRadiusPickerDialog(
|
||||
currentRadius: Int,
|
||||
onRadiusSelected: (Int) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
@@ -323,24 +334,19 @@ private fun PrecisionPickerDialog(
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
"Choose the accuracy of your shared location:",
|
||||
"Choose how precisely your location is shared:",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
PrecisionOption(
|
||||
title = "Precise",
|
||||
description = "Full GPS accuracy for exact location",
|
||||
isSelected = currentPrecision == "PRECISE",
|
||||
onClick = { onPrecisionSelected("PRECISE") },
|
||||
)
|
||||
|
||||
PrecisionOption(
|
||||
title = "Approximate",
|
||||
description = "Reduced accuracy (~100m radius)",
|
||||
isSelected = currentPrecision == "APPROXIMATE",
|
||||
onClick = { onPrecisionSelected("APPROXIMATE") },
|
||||
)
|
||||
PrecisionPreset.entries.forEach { preset ->
|
||||
PrecisionRadiusOption(
|
||||
title = preset.displayName,
|
||||
description = preset.description,
|
||||
isSelected = currentRadius == preset.radiusMeters,
|
||||
onClick = { onRadiusSelected(preset.radiusMeters) },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@@ -352,7 +358,7 @@ private fun PrecisionPickerDialog(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PrecisionOption(
|
||||
private fun PrecisionRadiusOption(
|
||||
title: String,
|
||||
description: String,
|
||||
isSelected: Boolean,
|
||||
@@ -412,12 +418,14 @@ private fun getDurationDisplayText(durationName: String): String {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display text for a precision setting.
|
||||
* Get display text for a precision radius setting.
|
||||
*/
|
||||
private fun getPrecisionDisplayText(precision: String): String {
|
||||
return when (precision) {
|
||||
"PRECISE" -> "Precise"
|
||||
"APPROXIMATE" -> "Approximate"
|
||||
else -> "Precise"
|
||||
private fun getPrecisionRadiusDisplayText(radiusMeters: Int): String {
|
||||
return when (radiusMeters) {
|
||||
0 -> "Precise"
|
||||
100 -> "Neighborhood (~100m)"
|
||||
1000 -> "City (~1km)"
|
||||
10000 -> "Region (~10km)"
|
||||
else -> if (radiusMeters >= 1000) "${radiusMeters / 1000}km" else "${radiusMeters}m"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ data class ContactMarker(
|
||||
val timestamp: Long = 0L,
|
||||
val expiresAt: Long? = null,
|
||||
val state: MarkerState = MarkerState.FRESH,
|
||||
val approximateRadius: Int = 0, // Coarsening radius in meters (0 = precise)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -165,6 +166,7 @@ class MapViewModel
|
||||
timestamp = loc.timestamp,
|
||||
expiresAt = loc.expiresAt,
|
||||
state = markerState,
|
||||
approximateRadius = loc.approximateRadius,
|
||||
)
|
||||
}
|
||||
}.collect { markers ->
|
||||
|
||||
@@ -70,7 +70,7 @@ data class SettingsState(
|
||||
val locationSharingEnabled: Boolean = true,
|
||||
val activeSharingSessions: List<com.lxmf.messenger.service.SharingSession> = emptyList(),
|
||||
val defaultSharingDuration: String = "ONE_HOUR",
|
||||
val locationPrecision: String = "PRECISE",
|
||||
val locationPrecisionRadius: Int = 0,
|
||||
)
|
||||
|
||||
@Suppress("TooManyFunctions", "LargeClass") // ViewModel with many user interaction methods is expected
|
||||
@@ -117,6 +117,8 @@ class SettingsViewModel
|
||||
|
||||
init {
|
||||
loadSettings()
|
||||
// Always load location sharing settings (not dependent on monitors)
|
||||
loadLocationSharingSettings()
|
||||
if (enableMonitors) {
|
||||
startSharedInstanceMonitor()
|
||||
startSharedInstanceAvailabilityMonitor()
|
||||
@@ -238,6 +240,11 @@ class SettingsViewModel
|
||||
transportNodeEnabled = transportNodeEnabled,
|
||||
// Message delivery state
|
||||
defaultDeliveryMethod = defaultDeliveryMethod,
|
||||
// Preserve location sharing state from loadLocationSharingSettings()
|
||||
locationSharingEnabled = _state.value.locationSharingEnabled,
|
||||
activeSharingSessions = _state.value.activeSharingSessions,
|
||||
defaultSharingDuration = _state.value.defaultSharingDuration,
|
||||
locationPrecisionRadius = _state.value.locationPrecisionRadius,
|
||||
)
|
||||
}.distinctUntilChanged().collect { newState ->
|
||||
val previousState = _state.value
|
||||
@@ -1109,18 +1116,10 @@ class SettingsViewModel
|
||||
// Location sharing methods
|
||||
|
||||
/**
|
||||
* Start monitoring location sharing state from the LocationSharingManager
|
||||
* and settings repository.
|
||||
* Load location sharing settings from the repository.
|
||||
* Called unconditionally to ensure settings persist across navigation.
|
||||
*/
|
||||
private fun startLocationSharingMonitor() {
|
||||
// Monitor active sharing sessions
|
||||
viewModelScope.launch {
|
||||
locationSharingManager.activeSessions.collect { sessions ->
|
||||
_state.value = _state.value.copy(activeSharingSessions = sessions)
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor location sharing settings
|
||||
private fun loadLocationSharingSettings() {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.locationSharingEnabledFlow.collect { enabled ->
|
||||
_state.value = _state.value.copy(locationSharingEnabled = enabled)
|
||||
@@ -1132,8 +1131,20 @@ class SettingsViewModel
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
settingsRepository.locationPrecisionFlow.collect { precision ->
|
||||
_state.value = _state.value.copy(locationPrecision = precision)
|
||||
settingsRepository.locationPrecisionRadiusFlow.collect { radiusMeters ->
|
||||
_state.value = _state.value.copy(locationPrecisionRadius = radiusMeters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring active location sharing sessions from the LocationSharingManager.
|
||||
* Only called when monitors are enabled.
|
||||
*/
|
||||
private fun startLocationSharingMonitor() {
|
||||
viewModelScope.launch {
|
||||
locationSharingManager.activeSessions.collect { sessions ->
|
||||
_state.value = _state.value.copy(activeSharingSessions = sessions)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1185,14 +1196,14 @@ class SettingsViewModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the location precision.
|
||||
* Set the location precision radius.
|
||||
*
|
||||
* @param precision "PRECISE" or "APPROXIMATE"
|
||||
* @param radiusMeters 0 for precise, or coarsening radius in meters (100, 1000, 10000, etc.)
|
||||
*/
|
||||
fun setLocationPrecision(precision: String) {
|
||||
fun setLocationPrecisionRadius(radiusMeters: Int) {
|
||||
viewModelScope.launch {
|
||||
settingsRepository.saveLocationPrecision(precision)
|
||||
Log.d(TAG, "Location precision set to: $precision")
|
||||
settingsRepository.saveLocationPrecisionRadius(radiusMeters)
|
||||
Log.d(TAG, "Location precision radius set to: ${radiusMeters}m")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@ data class ReceivedLocationEntity(
|
||||
val timestamp: Long, // When the location was captured (from sender)
|
||||
val expiresAt: Long?, // When sharing ends (null = indefinite)
|
||||
val receivedAt: Long, // When we received this update
|
||||
val approximateRadius: Int = 0, // Coarsening radius in meters (0 = precise)
|
||||
)
|
||||
|
||||
@@ -1068,7 +1068,8 @@ object DatabaseModule {
|
||||
accuracy REAL NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
expiresAt INTEGER,
|
||||
receivedAt INTEGER NOT NULL
|
||||
receivedAt INTEGER NOT NULL,
|
||||
approximateRadius INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user