diff --git a/PRIVACY.md b/PRIVACY.md
new file mode 100644
index 0000000..1d6a505
--- /dev/null
+++ b/PRIVACY.md
@@ -0,0 +1,67 @@
+# Privacy Policy / Personvernerklæring
+
+**Tilfluktsrom — Norwegian Emergency Shelter Finder**
+
+*Last updated: 2026-03-08*
+
+---
+
+## English
+
+Tilfluktsrom is an open-source emergency shelter finder app. **We do not collect, store, or transmit any personal data.**
+
+### What the app does with your data
+
+- **Location**: Your GPS location is used **only on-device** to calculate the distance and direction to the nearest shelter. Your location is never sent to any server.
+- **Shelter data**: Downloaded from [Geonorge](https://www.geonorge.no/) (a Norwegian government geographic data service). No user-identifying information is included in these requests.
+- **Map tiles**: Fetched from OpenStreetMap via standard HTTP requests. No tracking or user identification is performed.
+
+### What the app does NOT do
+
+- No analytics or telemetry
+- No advertising
+- No cookies or local tracking
+- No user accounts or registration
+- No third-party SDKs that collect data
+- No data sharing with any third party
+
+### Permissions
+
+- **Location**: Required to find the nearest shelter. Used only on-device.
+- **Internet**: Required to download shelter data and map tiles. No personal data is transmitted.
+- **Storage** (Android 8–9 only): Used for offline map tile cache.
+
+### Contact
+
+For questions about this privacy policy, open an issue at the project repository or contact the developer.
+
+---
+
+## Norsk
+
+Tilfluktsrom er en åpen kildekode-app for å finne offentlige tilfluktsrom. **Vi samler ikke inn, lagrer eller overfører noen personopplysninger.**
+
+### Hva appen gjør med dine data
+
+- **Posisjon**: GPS-posisjonen din brukes **kun lokalt på enheten** for å beregne avstand og retning til nærmeste tilfluktsrom. Posisjonen din sendes aldri til noen server.
+- **Tilfluktsromdata**: Lastes ned fra [Geonorge](https://www.geonorge.no/) (en norsk offentlig geografisk datatjeneste). Ingen brukeridentifiserende informasjon sendes.
+- **Kartfliser**: Hentes fra OpenStreetMap via standard HTTP-forespørsler. Ingen sporing eller brukeridentifikasjon utføres.
+
+### Hva appen IKKE gjør
+
+- Ingen analyse eller telemetri
+- Ingen reklame
+- Ingen informasjonskapsler eller lokal sporing
+- Ingen brukerkontoer eller registrering
+- Ingen tredjeparts-SDK-er som samler data
+- Ingen deling av data med tredjeparter
+
+### Tillatelser
+
+- **Posisjon**: Nødvendig for å finne nærmeste tilfluktsrom. Brukes kun lokalt på enheten.
+- **Internett**: Nødvendig for å laste ned tilfluktsromdata og kartfliser. Ingen personopplysninger overføres.
+- **Lagring** (kun Android 8–9): Brukes for frakoblet kartflislager.
+
+### Kontakt
+
+For spørsmål om denne personvernerklæringen, opprett en sak i prosjektets repository eller kontakt utvikleren.
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index be4c3fe..f411fb5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,5 +29,18 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt b/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt
index 82568c0..4b353c5 100644
--- a/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt
+++ b/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt
@@ -17,6 +17,7 @@ import android.provider.Settings
import android.util.Log
import android.view.View
import android.widget.Toast
+import java.util.concurrent.TimeUnit
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
@@ -237,6 +238,7 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
repository.getAllShelters().collectLatest { shelters ->
allShelters = shelters
binding.statusText.text = getString(R.string.status_shelters_loaded, shelters.size)
+ updateFreshnessIndicator()
updateShelterMarkers()
currentLocation?.let { updateNearestShelters(it) }
}
@@ -588,6 +590,7 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
binding.statusText.text = getString(R.string.status_updating)
val success = repository.refreshData()
if (success) {
+ updateFreshnessIndicator()
Toast.makeText(this@MainActivity, R.string.update_success, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@MainActivity, R.string.update_failed, Toast.LENGTH_SHORT).show()
@@ -595,6 +598,28 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
}
}
+ /** Update the freshness indicator below the status bar with color-coded age. */
+ private fun updateFreshnessIndicator() {
+ val lastUpdate = repository.getLastUpdateMs()
+ if (lastUpdate == 0L) {
+ binding.dataFreshnessText.visibility = View.GONE
+ return
+ }
+ val daysSince = TimeUnit.MILLISECONDS.toDays(
+ System.currentTimeMillis() - lastUpdate
+ ).toInt()
+
+ val (textRes, colorRes) = when {
+ daysSince == 0 -> R.string.freshness_fresh to R.color.text_secondary
+ daysSince <= 7 -> R.string.freshness_week to R.color.shelter_accent
+ else -> R.string.freshness_old to R.color.shelter_primary
+ }
+
+ binding.dataFreshnessText.text = getString(textRes, daysSince)
+ binding.dataFreshnessText.setTextColor(ContextCompat.getColor(this, colorRes))
+ binding.dataFreshnessText.visibility = View.VISIBLE
+ }
+
private fun showLoading(message: String) {
binding.loadingOverlay.visibility = View.VISIBLE
binding.loadingText.text = message
diff --git a/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterRepository.kt b/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterRepository.kt
index 839c972..4298c39 100644
--- a/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterRepository.kt
+++ b/app/src/main/java/no/naiv/tilfluktsrom/data/ShelterRepository.kt
@@ -51,6 +51,9 @@ class ShelterRepository(private val context: Context) {
/** Check if we have cached shelter data. */
suspend fun hasCachedData(): Boolean = dao.count() > 0
+ /** Timestamp (epoch ms) of the last successful data update, or 0 if never updated. */
+ fun getLastUpdateMs(): Long = prefs.getLong(KEY_LAST_UPDATE, 0)
+
/** Check if the cached data is stale and should be refreshed. */
fun isDataStale(): Boolean {
val lastUpdate = prefs.getLong(KEY_LAST_UPDATE, 0)
diff --git a/app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt b/app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt
index 9a378af..0bc5aea 100644
--- a/app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt
+++ b/app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt
@@ -4,9 +4,14 @@ import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.os.Bundle
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
@@ -18,11 +23,12 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
/**
- * Provides GPS location updates using the Fused Location Provider.
- * Emits location updates as a Flow for reactive consumption.
+ * Provides GPS location updates with automatic fallback.
*
- * Closes the Flow with a SecurityException if location permission is not granted,
- * so callers can detect and handle this failure explicitly.
+ * Uses FusedLocationProviderClient when Google Play Services is available (most devices),
+ * and falls back to LocationManager (GPS + Network providers) for degoogled/F-Droid devices.
+ *
+ * The public API is identical regardless of backend: locationUpdates() emits a Flow.
*/
class LocationProvider(private val context: Context) {
@@ -32,8 +38,12 @@ class LocationProvider(private val context: Context) {
private const val FASTEST_INTERVAL_MS = 2000L
}
- private val fusedClient: FusedLocationProviderClient =
- LocationServices.getFusedLocationProviderClient(context)
+ /** Checked once at construction — Play Services availability won't change at runtime. */
+ private val usePlayServices: Boolean = isPlayServicesAvailable()
+
+ init {
+ Log.d(TAG, "Location backend: ${if (usePlayServices) "Play Services" else "LocationManager"}")
+ }
/**
* Stream of location updates. Emits the last known location first (if available),
@@ -45,57 +55,132 @@ class LocationProvider(private val context: Context) {
return@callbackFlow
}
- // Try to get last known location for immediate display
- try {
- fusedClient.lastLocation
- .addOnSuccessListener { location ->
- if (location != null) {
- val result = trySend(location)
- if (result.isFailure) {
- Log.w(TAG, "Failed to emit last known location")
+ if (usePlayServices) {
+ val fusedClient: FusedLocationProviderClient =
+ LocationServices.getFusedLocationProviderClient(context)
+
+ // Emit last known location immediately for fast first display
+ try {
+ fusedClient.lastLocation
+ .addOnSuccessListener { location ->
+ if (location != null) {
+ 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) {
+ Log.e(TAG, "SecurityException getting last location", e)
+ }
+
+ val locationRequest = LocationRequest.Builder(
+ Priority.PRIORITY_HIGH_ACCURACY,
+ UPDATE_INTERVAL_MS
+ ).apply {
+ setMinUpdateIntervalMillis(FASTEST_INTERVAL_MS)
+ setWaitForAccurateLocation(false)
+ }.build()
+
+ val callback = object : LocationCallback() {
+ override fun onLocationResult(result: LocationResult) {
+ result.lastLocation?.let { location ->
+ val sendResult = trySend(location)
+ if (sendResult.isFailure) {
+ Log.w(TAG, "Failed to emit location update")
}
}
}
- .addOnFailureListener { e ->
- Log.w(TAG, "Could not get last known location", e)
- }
- } catch (e: SecurityException) {
- Log.e(TAG, "SecurityException getting last location", e)
- }
+ }
- val locationRequest = LocationRequest.Builder(
- Priority.PRIORITY_HIGH_ACCURACY,
- UPDATE_INTERVAL_MS
- ).apply {
- setMinUpdateIntervalMillis(FASTEST_INTERVAL_MS)
- setWaitForAccurateLocation(false)
- }.build()
+ try {
+ fusedClient.requestLocationUpdates(
+ locationRequest,
+ callback,
+ Looper.getMainLooper()
+ )
+ } catch (e: SecurityException) {
+ Log.e(TAG, "SecurityException requesting location updates", e)
+ close(e)
+ return@callbackFlow
+ }
- val callback = object : LocationCallback() {
- override fun onLocationResult(result: LocationResult) {
- result.lastLocation?.let { location ->
- val sendResult = trySend(location)
- if (sendResult.isFailure) {
- Log.w(TAG, "Failed to emit location update")
+ awaitClose {
+ fusedClient.removeLocationUpdates(callback)
+ }
+ } else {
+ // Fallback: LocationManager for devices without Play Services
+ val locationManager = context.getSystemService(Context.LOCATION_SERVICE)
+ as? LocationManager
+
+ if (locationManager == null) {
+ close(IllegalStateException("LocationManager not available"))
+ return@callbackFlow
+ }
+
+ // Emit best last known location immediately (pick most recent of GPS/Network)
+ try {
+ val lastGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
+ val lastNetwork = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
+ val best = listOfNotNull(lastGps, lastNetwork).maxByOrNull { it.time }
+ if (best != null) {
+ val result = trySend(best)
+ if (result.isFailure) {
+ Log.w(TAG, "Failed to emit last known location (fallback)")
}
}
+ } catch (e: SecurityException) {
+ Log.e(TAG, "SecurityException getting last known location (fallback)", e)
}
- }
- try {
- fusedClient.requestLocationUpdates(
- locationRequest,
- callback,
- Looper.getMainLooper()
- )
- } catch (e: SecurityException) {
- Log.e(TAG, "SecurityException requesting location updates", e)
- close(e)
- return@callbackFlow
- }
+ // LocationListener compatible with API 26-28 (onStatusChanged required before API 29)
+ val listener = object : LocationListener {
+ override fun onLocationChanged(location: Location) {
+ val sendResult = trySend(location)
+ if (sendResult.isFailure) {
+ Log.w(TAG, "Failed to emit location update (fallback)")
+ }
+ }
- awaitClose {
- fusedClient.removeLocationUpdates(callback)
+ // Required for API 26-28 compatibility (deprecated from API 29+)
+ @Deprecated("Deprecated in API 29")
+ override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
+ override fun onProviderEnabled(provider: String) {}
+ override fun onProviderDisabled(provider: String) {}
+ }
+
+ try {
+ // Request from both providers: GPS is accurate, Network gives faster first fix
+ if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
+ locationManager.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ UPDATE_INTERVAL_MS,
+ 0f,
+ listener,
+ Looper.getMainLooper()
+ )
+ }
+ if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
+ locationManager.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER,
+ UPDATE_INTERVAL_MS,
+ 0f,
+ listener,
+ Looper.getMainLooper()
+ )
+ }
+ } catch (e: SecurityException) {
+ Log.e(TAG, "SecurityException requesting location updates (fallback)", e)
+ close(e)
+ return@callbackFlow
+ }
+
+ awaitClose {
+ locationManager.removeUpdates(listener)
+ }
}
}
@@ -104,4 +189,15 @@ class LocationProvider(private val context: Context) {
context, Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
+
+ private fun isPlayServicesAvailable(): Boolean {
+ return try {
+ val result = GoogleApiAvailability.getInstance()
+ .isGooglePlayServicesAvailable(context)
+ result == ConnectionResult.SUCCESS
+ } catch (e: Exception) {
+ // Play Services library might not even be resolvable on some ROMs
+ false
+ }
+ }
}
diff --git a/app/src/main/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt b/app/src/main/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt
new file mode 100644
index 0000000..ef16094
--- /dev/null
+++ b/app/src/main/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt
@@ -0,0 +1,167 @@
+package no.naiv.tilfluktsrom.widget
+
+import android.Manifest
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.location.Location
+import android.location.LocationManager
+import android.util.Log
+import android.widget.RemoteViews
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.runBlocking
+import no.naiv.tilfluktsrom.MainActivity
+import no.naiv.tilfluktsrom.R
+import no.naiv.tilfluktsrom.data.ShelterDatabase
+import no.naiv.tilfluktsrom.location.ShelterFinder
+import no.naiv.tilfluktsrom.util.DistanceUtils
+
+/**
+ * Home screen widget showing the nearest shelter with distance.
+ *
+ * Update strategy: no automatic periodic updates (updatePeriodMillis=0).
+ * Updates only when the user taps the refresh button, which sends ACTION_REFRESH.
+ * Tapping the widget body opens MainActivity.
+ *
+ * Uses LocationManager directly (not the hybrid LocationProvider) because
+ * BroadcastReceiver context makes FusedLocationProviderClient setup awkward.
+ * For a one-shot getLastKnownLocation, LocationManager is equally effective.
+ */
+class ShelterWidgetProvider : AppWidgetProvider() {
+
+ companion object {
+ private const val TAG = "ShelterWidget"
+ const val ACTION_REFRESH = "no.naiv.tilfluktsrom.widget.REFRESH"
+ }
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ for (appWidgetId in appWidgetIds) {
+ updateWidget(context, appWidgetManager, appWidgetId)
+ }
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+
+ if (intent.action == ACTION_REFRESH) {
+ val appWidgetManager = AppWidgetManager.getInstance(context)
+ val widgetIds = appWidgetManager.getAppWidgetIds(
+ ComponentName(context, ShelterWidgetProvider::class.java)
+ )
+ for (appWidgetId in widgetIds) {
+ updateWidget(context, appWidgetManager, appWidgetId)
+ }
+ }
+ }
+
+ private fun updateWidget(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetId: Int
+ ) {
+ val views = RemoteViews(context.packageName, R.layout.widget_nearest_shelter)
+
+ // Tapping widget body opens the app
+ val openAppIntent = Intent(context, MainActivity::class.java)
+ val openAppPending = PendingIntent.getActivity(
+ context, 0, openAppIntent, PendingIntent.FLAG_IMMUTABLE
+ )
+ views.setOnClickPendingIntent(R.id.widgetRoot, openAppPending)
+
+ // Refresh button sends our custom broadcast
+ val refreshIntent = Intent(context, ShelterWidgetProvider::class.java).apply {
+ action = ACTION_REFRESH
+ }
+ val refreshPending = PendingIntent.getBroadcast(
+ context, 0, refreshIntent, PendingIntent.FLAG_IMMUTABLE
+ )
+ views.setOnClickPendingIntent(R.id.widgetRefreshButton, refreshPending)
+
+ // Check location permission
+ if (ContextCompat.checkSelfPermission(
+ context, Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ showFallback(views, context.getString(R.string.widget_open_app))
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ return
+ }
+
+ // Get last known location from LocationManager
+ val location = getLastKnownLocation(context)
+ if (location == null) {
+ showFallback(views, context.getString(R.string.widget_no_location))
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ return
+ }
+
+ // Query shelters from Room (fast: ~556 rows, <10ms)
+ val shelters = try {
+ val dao = ShelterDatabase.getInstance(context).shelterDao()
+ runBlocking { dao.getAllSheltersList() }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to query shelters", e)
+ emptyList()
+ }
+
+ if (shelters.isEmpty()) {
+ showFallback(views, context.getString(R.string.widget_no_data))
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ return
+ }
+
+ // Find nearest shelter
+ val nearest = ShelterFinder.findNearest(
+ shelters, location.latitude, location.longitude, 1
+ ).firstOrNull()
+
+ if (nearest == null) {
+ showFallback(views, context.getString(R.string.widget_no_data))
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ return
+ }
+
+ // Show shelter info
+ views.setTextViewText(R.id.widgetAddress, nearest.shelter.adresse)
+ views.setTextViewText(
+ R.id.widgetDetails,
+ context.getString(R.string.shelter_capacity, nearest.shelter.plasser)
+ )
+ views.setTextViewText(
+ R.id.widgetDistance,
+ DistanceUtils.formatDistance(nearest.distanceMeters)
+ )
+
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ }
+
+ /** Show a fallback message when location or data is unavailable. */
+ private fun showFallback(views: RemoteViews, message: String) {
+ views.setTextViewText(R.id.widgetAddress, message)
+ views.setTextViewText(R.id.widgetDetails, "")
+ views.setTextViewText(R.id.widgetDistance, "")
+ }
+
+ /** Get the best last known location from GPS and Network providers. */
+ private fun getLastKnownLocation(context: Context): Location? {
+ val locationManager = context.getSystemService(Context.LOCATION_SERVICE)
+ as? LocationManager ?: return null
+
+ return try {
+ val lastGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
+ val lastNetwork = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
+ listOfNotNull(lastGps, lastNetwork).maxByOrNull { it.time }
+ } catch (e: SecurityException) {
+ Log.e(TAG, "SecurityException getting last known location", e)
+ null
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 121dc34..fefe456 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -15,28 +15,46 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/status_bar_bg"
- android:gravity="center_vertical"
- android:orientation="horizontal"
- android:padding="8dp"
+ android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent">
-
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingHorizontal="8dp"
+ android:paddingVertical="4dp">
-
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_nearest_shelter.xml b/app/src/main/res/layout/widget_nearest_shelter.xml
new file mode 100644
index 0000000..5ac2ba1
--- /dev/null
+++ b/app/src/main/res/layout/widget_nearest_shelter.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index df27742..59b9fcf 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -44,6 +44,17 @@
Tilfluktsromdata oppdatert
Oppdatering mislyktes — bruker lagrede data
+
+ Viser n\u00e6rmeste tilfluktsrom med avstand
+ \u00c5pne appen for posisjon
+ Ingen tilfluktsromdata
+ Trykk for \u00e5 oppdatere
+
+
+ Data er oppdatert
+ Data er %d dager gammel
+ Data er utdatert
+
Retning til tilfluktsrom, %s unna
diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml
index d26bb97..d1dc1a0 100644
--- a/app/src/main/res/values-nn/strings.xml
+++ b/app/src/main/res/values-nn/strings.xml
@@ -44,6 +44,17 @@
Tilfluktsromdata oppdatert
Oppdatering mislukkast — brukar lagra data
+
+ Viser n\u00e6raste tilfluktsrom med avstand
+ Opne appen for posisjon
+ Ingen tilfluktsromdata
+ Trykk for \u00e5 oppdatere
+
+
+ Data er oppdatert
+ Data er %d dagar gammal
+ Data er utdatert
+
Retning til tilfluktsrom, %s unna
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 92cd274..80d4f9d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -44,6 +44,17 @@
Shelter data updated
Update failed — using cached data
+
+ Shows nearest shelter with distance
+ Open app for location
+ No shelter data
+ Tap to refresh
+
+
+ Data is up to date
+ Data is %d days old
+ Data is outdated
+
Direction to shelter, %s away
diff --git a/app/src/main/res/xml/widget_info.xml b/app/src/main/res/xml/widget_info.xml
new file mode 100644
index 0000000..f08bb61
--- /dev/null
+++ b/app/src/main/res/xml/widget_info.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/version.properties b/version.properties
index acf8f7c..4536802 100644
--- a/version.properties
+++ b/version.properties
@@ -1,4 +1,4 @@
versionMajor=1
-versionMinor=1
+versionMinor=2
versionPatch=0
-versionCode=2
+versionCode=3