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.SensorManager
import android.location.Location
import android.location.LocationManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
@ -95,27 +97,9 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
if (fineGranted || coarseGranted) {
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 {
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?) {
@ -259,6 +243,8 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
}
private fun loadData() {
updateLocationStatusBanner()
lifecycleScope.launch {
try {
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() {
// Use repeatOnLifecycle(STARTED) so GPS stops when Activity is paused
lifecycleScope.launch {
@ -757,6 +781,10 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
binding.mapView.onResume()
myLocationOverlay?.enableMyLocation()
// Re-check permission + location-services state so the banner updates
// when the user returns from Settings.
updateLocationStatusBanner()
val sm = sensorManager ?: return
// Try rotation vector first (best compass source)

View file

@ -76,7 +76,7 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:contentDescription="@string/a11y_map"
app:layout_constraintTop_toBottomOf="@id/statusBar"
app:layout_constraintTop_toBottomOf="@id/noLocationBanner"
app:layout_constraintBottom_toTopOf="@id/bottomSheet" />
<!-- Direction arrow overlay (shown when toggled) -->
@ -87,7 +87,7 @@
android:background="@color/compass_bg"
android:contentDescription="@string/a11y_compass"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/statusBar"
app:layout_constraintTop_toBottomOf="@id/noLocationBanner"
app:layout_constraintBottom_toTopOf="@id/bottomSheet">
<no.naiv.tilfluktsrom.ui.DirectionArrowView
@ -149,6 +149,41 @@
app:backgroundTint="@color/shelter_primary"
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 -->
<LinearLayout
android:id="@+id/noCacheBanner"

View file

@ -30,7 +30,11 @@
<string name="action_cache_now">Lagre nå</string>
<string name="action_reset_navigation">Tilbakestill navigasjonsvisning</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="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 -->
<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_reset_navigation">Tilbakestill navigasjonsvising</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="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 -->
<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_reset_navigation">Reset navigation view</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="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 -->
<string name="permission_location_title">Location permission required</string>