From 57a9072b4c8a719e94550d681d0f15d2b2a6c422 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Sun, 8 Mar 2026 19:10:57 +0100 Subject: [PATCH] Legg til hybrid lokasjon, dataferskheit, widget og personvern (v1.2.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hybrid LocationProvider: prøver Play Services først, faller tilbake til LocationManager for degooglede einingar (F-Droid-kompatibel) - Dataferskheitsindikator i statuslinja med tre nivå (fersk/veke/gammal) - Heimeskjerm-widget som viser næraste tilfluktsrom med avstand - Personvernerklæring (PRIVACY.md) på engelsk og norsk Co-Authored-By: Claude Opus 4.6 --- PRIVACY.md | 67 ++++++ app/src/main/AndroidManifest.xml | 13 ++ .../java/no/naiv/tilfluktsrom/MainActivity.kt | 25 +++ .../tilfluktsrom/data/ShelterRepository.kt | 3 + .../tilfluktsrom/location/LocationProvider.kt | 190 +++++++++++++----- .../widget/ShelterWidgetProvider.kt | 167 +++++++++++++++ app/src/main/res/layout/activity_main.xml | 54 +++-- .../res/layout/widget_nearest_shelter.xml | 65 ++++++ app/src/main/res/values-nb/strings.xml | 11 + app/src/main/res/values-nn/strings.xml | 11 + app/src/main/res/values/strings.xml | 11 + app/src/main/res/xml/widget_info.xml | 9 + version.properties | 4 +- 13 files changed, 563 insertions(+), 67 deletions(-) create mode 100644 PRIVACY.md create mode 100644 app/src/main/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt create mode 100644 app/src/main/res/layout/widget_nearest_shelter.xml create mode 100644 app/src/main/res/xml/widget_info.xml 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