Improve error visibility and eliminate silent failures
- LocationProvider: close Flow with SecurityException when permission is missing (instead of silently completing), log trySend failures, handle lastLocation failure callback - loadData(): wrap in top-level try-catch so database errors don't leave the app in a broken loading state - Cache retry button: show "Waiting for GPS" toast when location is null instead of silently doing nothing - selectShelterByData: use NaN for distance/bearing when no GPS fix yet, so UI shows "—" instead of misleading "0 m" - DistanceUtils.formatDistance: handle NaN gracefully - GeoJSON parser: require valid lokalId (primary key) — reject shelters with missing ID to prevent DB collisions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
55761718e1
commit
c129de0a12
4 changed files with 92 additions and 55 deletions
|
|
@ -193,18 +193,20 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.cacheRetryButton.setOnClickListener {
|
binding.cacheRetryButton.setOnClickListener {
|
||||||
currentLocation?.let { loc ->
|
val loc = currentLocation
|
||||||
if (isNetworkAvailable()) {
|
if (loc == null) {
|
||||||
startCaching(loc.latitude, loc.longitude)
|
Toast.makeText(this, R.string.status_no_location, Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else if (!isNetworkAvailable()) {
|
||||||
Toast.makeText(this, R.string.error_download_failed, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.error_download_failed, Toast.LENGTH_SHORT).show()
|
||||||
}
|
} else {
|
||||||
|
startCaching(loc.latitude, loc.longitude)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadData() {
|
private fun loadData() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
val hasData = repository.hasCachedData()
|
val hasData = repository.hasCachedData()
|
||||||
|
|
||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
|
|
@ -253,6 +255,13 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to initialize shelter data", e)
|
||||||
|
hideLoading()
|
||||||
|
binding.statusText.text = getString(R.string.error_download_failed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -384,7 +393,12 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ShelterWithDistance(shelter = shelter, distanceMeters = 0.0, bearingDegrees = 0.0)
|
// No GPS yet — use NaN to signal "unknown distance"
|
||||||
|
ShelterWithDistance(
|
||||||
|
shelter = shelter,
|
||||||
|
distanceMeters = Double.NaN,
|
||||||
|
bearingDegrees = Double.NaN
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
userSelectedShelter = true
|
userSelectedShelter = true
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,14 @@ object ShelterGeoJsonParser {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Require a valid primary key — without it shelters can collide in the DB
|
||||||
|
val lokalId = properties.optString("lokalId", null)
|
||||||
|
if (lokalId.isNullOrBlank()) {
|
||||||
|
Log.w(TAG, "Skipping shelter at index $i: missing lokalId")
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
val plasser = properties.optInt("plasser", 0)
|
val plasser = properties.optInt("plasser", 0)
|
||||||
if (plasser < 0) {
|
if (plasser < 0) {
|
||||||
Log.w(TAG, "Skipping shelter at index $i: negative capacity ($plasser)")
|
Log.w(TAG, "Skipping shelter at index $i: negative capacity ($plasser)")
|
||||||
|
|
@ -100,7 +108,7 @@ object ShelterGeoJsonParser {
|
||||||
|
|
||||||
shelters.add(
|
shelters.add(
|
||||||
Shelter(
|
Shelter(
|
||||||
lokalId = properties.optString("lokalId", "unknown-$i"),
|
lokalId = lokalId,
|
||||||
romnr = properties.optInt("romnr", 0),
|
romnr = properties.optInt("romnr", 0),
|
||||||
plasser = plasser,
|
plasser = plasser,
|
||||||
adresse = properties.optString("adresse", ""),
|
adresse = properties.optString("adresse", ""),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ import kotlinx.coroutines.flow.callbackFlow
|
||||||
/**
|
/**
|
||||||
* Provides GPS location updates using the Fused Location Provider.
|
* Provides GPS location updates using the Fused Location Provider.
|
||||||
* Emits location updates as a Flow for reactive consumption.
|
* Emits location updates as a Flow for reactive consumption.
|
||||||
|
*
|
||||||
|
* Closes the Flow with a SecurityException if location permission is not granted,
|
||||||
|
* so callers can detect and handle this failure explicitly.
|
||||||
*/
|
*/
|
||||||
class LocationProvider(private val context: Context) {
|
class LocationProvider(private val context: Context) {
|
||||||
|
|
||||||
|
|
@ -34,22 +37,28 @@ class LocationProvider(private val context: Context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stream of location updates. Emits the last known location first (if available),
|
* Stream of location updates. Emits the last known location first (if available),
|
||||||
* then continuous updates.
|
* then continuous updates. Throws SecurityException if permission is not granted.
|
||||||
*/
|
*/
|
||||||
fun locationUpdates(): Flow<Location> = callbackFlow {
|
fun locationUpdates(): Flow<Location> = callbackFlow {
|
||||||
if (!hasLocationPermission()) {
|
if (!hasLocationPermission()) {
|
||||||
Log.w(TAG, "Location permission not granted")
|
close(SecurityException("Location permission not granted"))
|
||||||
close()
|
|
||||||
return@callbackFlow
|
return@callbackFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get last known location for immediate display
|
// Try to get last known location for immediate display
|
||||||
try {
|
try {
|
||||||
fusedClient.lastLocation.addOnSuccessListener { location ->
|
fusedClient.lastLocation
|
||||||
|
.addOnSuccessListener { location ->
|
||||||
if (location != null) {
|
if (location != null) {
|
||||||
trySend(location)
|
val result = trySend(location)
|
||||||
|
if (result.isFailure) {
|
||||||
|
Log.w(TAG, "Failed to emit last known location")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
Log.w(TAG, "Could not get last known location", e)
|
||||||
|
}
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.e(TAG, "SecurityException getting last location", e)
|
Log.e(TAG, "SecurityException getting last location", e)
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +73,12 @@ class LocationProvider(private val context: Context) {
|
||||||
|
|
||||||
val callback = object : LocationCallback() {
|
val callback = object : LocationCallback() {
|
||||||
override fun onLocationResult(result: LocationResult) {
|
override fun onLocationResult(result: LocationResult) {
|
||||||
result.lastLocation?.let { trySend(it) }
|
result.lastLocation?.let { location ->
|
||||||
|
val sendResult = trySend(location)
|
||||||
|
if (sendResult.isFailure) {
|
||||||
|
Log.w(TAG, "Failed to emit location update")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +90,7 @@ class LocationProvider(private val context: Context) {
|
||||||
)
|
)
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.e(TAG, "SecurityException requesting location updates", e)
|
Log.e(TAG, "SecurityException requesting location updates", e)
|
||||||
close()
|
close(e)
|
||||||
return@callbackFlow
|
return@callbackFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ object DistanceUtils {
|
||||||
* Format distance for display: meters if <1km, otherwise km with one decimal.
|
* Format distance for display: meters if <1km, otherwise km with one decimal.
|
||||||
*/
|
*/
|
||||||
fun formatDistance(meters: Double): String {
|
fun formatDistance(meters: Double): String {
|
||||||
|
if (meters.isNaN()) return "—"
|
||||||
return if (meters < 1000) {
|
return if (meters < 1000) {
|
||||||
"${meters.toInt()} m"
|
"${meters.toInt()} m"
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue