Actionable banner når posisjon ikke er tilgjengelig

Tidligere sto statusteksten igjen på «Venter på GPS…» uansett om
årsaken var manglende tillatelse, avslåtte stedstjenester eller
bare at GPS-en ikke hadde fått fix ennå. For en nødsituasjonsapp
er det en reell feilmodus: brukeren får ingen hint om hva som kan
gjøres for å finne nærmeste tilfluktsrom.

Ny noLocationBanner plasseres øverst i innholdsområdet (rett under
statuslinjen) slik at den ikke kolliderer med de flytende
handlingsknappene over bunnarket, og viser én av tre tilstander:

1. Tillatelse avslått eller ikke gitt — «Posisjonstilgang
   nødvendig for å finne nærmeste tilfluktsrom. Du kan også
   trykke på et merke i kartet.» + «Gi tilgang» som åpner
   ACTION_APPLICATION_DETAILS_SETTINGS.
2. Tillatelse gitt, men stedstjenester slått av — «Stedstjenester
   er slått av. Aktiver dem eller velg et tilfluktsrom fra
   kartet.» + «Aktiver» som åpner ACTION_LOCATION_SOURCE_SETTINGS.
3. Begge OK — banner er skjult og den eksisterende «Venter på
   GPS…»-teksten gjelder.

Helperen updateLocationStatusBanner() kalles fra loadData(),
permission-result-kallbacket og onResume(), slik at banneret
oppdaterer seg både ved appstart, umiddelbart etter avslag, og
når brukeren kommer tilbake fra systeminnstillingene.
AlertDialog-en ved permanent avslag er fjernet til fordel for det
ikke-modale banneret, som lar brukeren fortsatt pan-ne kartet og
velge tilfluktsrom manuelt. Toasten på mykt avslag er beholdt som
en kort bekreftelse. API-nivå-fallbacket bruker
LocationManager.isLocationEnabled på API 28+, isProviderEnabled
for GPS/Network på API 26–27.

Verifisert på emulator i alle fire tilstander (avslag → App
Settings, tjeneste-av → Posisjonsinnstillinger, gjenopprettet).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-04-20 10:32:02 +02:00
commit f0c4a1f5b4
5 changed files with 97 additions and 22 deletions

View file

@ -10,9 +10,11 @@ import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
import android.hardware.SensorManager import android.hardware.SensorManager
import android.location.Location import android.location.Location
import android.location.LocationManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
@ -94,28 +96,10 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
if (fineGranted || coarseGranted) { if (fineGranted || coarseGranted) {
startLocationUpdates() startLocationUpdates()
} else {
// Check if user permanently denied (don't show rationale = permanently denied)
val shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale(
this, Manifest.permission.ACCESS_FINE_LOCATION
)
if (!shouldShowRationale) {
// Permission permanently denied — guide user to settings
AlertDialog.Builder(this)
.setTitle(R.string.permission_location_title)
.setMessage(R.string.permission_denied)
.setPositiveButton(android.R.string.ok) { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
} else { } else {
Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_LONG).show()
} }
} updateLocationStatusBanner()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -259,6 +243,8 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
} }
private fun loadData() { private fun loadData() {
updateLocationStatusBanner()
lifecycleScope.launch { lifecycleScope.launch {
try { try {
var hasData = repository.hasCachedData() var hasData = repository.hasCachedData()
@ -378,6 +364,44 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
) )
} }
private fun updateLocationStatusBanner() {
val banner = binding.noLocationBanner
val text = binding.locationBannerText
val action = binding.locationBannerAction
when {
!locationProvider.hasLocationPermission() -> {
text.setText(R.string.status_location_permission_needed)
action.setText(R.string.action_grant_permission)
action.setOnClickListener {
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
})
}
banner.visibility = View.VISIBLE
}
!isLocationServicesEnabled() -> {
text.setText(R.string.status_location_services_off)
action.setText(R.string.action_location_settings)
action.setOnClickListener {
startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS))
}
banner.visibility = View.VISIBLE
}
else -> banner.visibility = View.GONE
}
}
private fun isLocationServicesEnabled(): Boolean {
val lm = getSystemService(Context.LOCATION_SERVICE) as? LocationManager ?: return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
lm.isLocationEnabled
} else {
lm.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
}
}
private fun startLocationUpdates() { private fun startLocationUpdates() {
// Use repeatOnLifecycle(STARTED) so GPS stops when Activity is paused // Use repeatOnLifecycle(STARTED) so GPS stops when Activity is paused
lifecycleScope.launch { lifecycleScope.launch {
@ -757,6 +781,10 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
binding.mapView.onResume() binding.mapView.onResume()
myLocationOverlay?.enableMyLocation() myLocationOverlay?.enableMyLocation()
// Re-check permission + location-services state so the banner updates
// when the user returns from Settings.
updateLocationStatusBanner()
val sm = sensorManager ?: return val sm = sensorManager ?: return
// Try rotation vector first (best compass source) // Try rotation vector first (best compass source)

View file

@ -76,7 +76,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:contentDescription="@string/a11y_map" android:contentDescription="@string/a11y_map"
app:layout_constraintTop_toBottomOf="@id/statusBar" app:layout_constraintTop_toBottomOf="@id/noLocationBanner"
app:layout_constraintBottom_toTopOf="@id/bottomSheet" /> app:layout_constraintBottom_toTopOf="@id/bottomSheet" />
<!-- Direction arrow overlay (shown when toggled) --> <!-- Direction arrow overlay (shown when toggled) -->
@ -87,7 +87,7 @@
android:background="@color/compass_bg" android:background="@color/compass_bg"
android:contentDescription="@string/a11y_compass" android:contentDescription="@string/a11y_compass"
android:visibility="gone" android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/statusBar" app:layout_constraintTop_toBottomOf="@id/noLocationBanner"
app:layout_constraintBottom_toTopOf="@id/bottomSheet"> app:layout_constraintBottom_toTopOf="@id/bottomSheet">
<no.naiv.tilfluktsrom.ui.DirectionArrowView <no.naiv.tilfluktsrom.ui.DirectionArrowView
@ -149,6 +149,41 @@
app:backgroundTint="@color/shelter_primary" app:backgroundTint="@color/shelter_primary"
app:tint="@color/white" /> app:tint="@color/white" />
<!-- Warning banner: location unavailable (permission denied or services off).
Placed at the top of the content area (below the status bar) so it never
collides with the floating action buttons anchored above the bottom sheet. -->
<LinearLayout
android:id="@+id/noLocationBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/warning_bg"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/statusBar">
<TextView
android:id="@+id/locationBannerText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/warning_text"
android:textSize="12sp"
tools:text="@string/status_location_permission_needed" />
<com.google.android.material.button.MaterialButton
android:id="@+id/locationBannerAction"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:textColor="@color/warning_text"
android:textSize="12sp"
android:minHeight="0dp"
tools:text="@string/action_grant_permission" />
</LinearLayout>
<!-- Warning banner: no offline map cache --> <!-- Warning banner: no offline map cache -->
<LinearLayout <LinearLayout
android:id="@+id/noCacheBanner" android:id="@+id/noCacheBanner"

View file

@ -30,7 +30,11 @@
<string name="action_cache_now">Lagre nå</string> <string name="action_cache_now">Lagre nå</string>
<string name="action_reset_navigation">Tilbakestill navigasjonsvisning</string> <string name="action_reset_navigation">Tilbakestill navigasjonsvisning</string>
<string name="action_share">Del tilfluktsrom</string> <string name="action_share">Del tilfluktsrom</string>
<string name="action_grant_permission">Gi tilgang</string>
<string name="action_location_settings">Aktiver</string>
<string name="warning_no_map_cache">Ingen frakoblet kart lagret. Kartet krever internett.</string> <string name="warning_no_map_cache">Ingen frakoblet kart lagret. Kartet krever internett.</string>
<string name="status_location_permission_needed">Posisjonstilgang nødvendig for å finne nærmeste tilfluktsrom. Du kan også trykke på et merke i kartet.</string>
<string name="status_location_services_off">Stedstjenester er slått av. Aktiver dem eller velg et tilfluktsrom fra kartet.</string>
<!-- Tillatelser --> <!-- Tillatelser -->
<string name="permission_location_title">Posisjonstillatelse kreves</string> <string name="permission_location_title">Posisjonstillatelse kreves</string>

View file

@ -30,7 +30,11 @@
<string name="action_cache_now">Lagre no</string> <string name="action_cache_now">Lagre no</string>
<string name="action_reset_navigation">Tilbakestill navigasjonsvising</string> <string name="action_reset_navigation">Tilbakestill navigasjonsvising</string>
<string name="action_share">Del tilfluktsrom</string> <string name="action_share">Del tilfluktsrom</string>
<string name="action_grant_permission">Gje tilgang</string>
<string name="action_location_settings">Aktiver</string>
<string name="warning_no_map_cache">Ingen fråkopla kart lagra. Kartet krev internett.</string> <string name="warning_no_map_cache">Ingen fråkopla kart lagra. Kartet krev internett.</string>
<string name="status_location_permission_needed">Posisjonstilgang trengst for å finne næraste tilfluktsrom. Du kan òg trykke på eit merke i kartet.</string>
<string name="status_location_services_off">Stedstenester er slått av. Aktiver dei eller vel eit tilfluktsrom frå kartet.</string>
<!-- Løyve --> <!-- Løyve -->
<string name="permission_location_title">Posisjonsløyve krevst</string> <string name="permission_location_title">Posisjonsløyve krevst</string>

View file

@ -30,7 +30,11 @@
<string name="action_cache_now">Cache now</string> <string name="action_cache_now">Cache now</string>
<string name="action_reset_navigation">Reset navigation view</string> <string name="action_reset_navigation">Reset navigation view</string>
<string name="action_share">Share shelter</string> <string name="action_share">Share shelter</string>
<string name="action_grant_permission">Grant access</string>
<string name="action_location_settings">Enable</string>
<string name="warning_no_map_cache">No offline map cached. Map requires internet.</string> <string name="warning_no_map_cache">No offline map cached. Map requires internet.</string>
<string name="status_location_permission_needed">Location access needed to find the nearest shelter. You can also tap a marker on the map.</string>
<string name="status_location_services_off">Location services are off. Enable them or pick a shelter from the map.</string>
<!-- Permissions --> <!-- Permissions -->
<string name="permission_location_title">Location permission required</string> <string name="permission_location_title">Location permission required</string>