Legg til om-side, personvernerklæring og sikkerheitsforbetring
Om-side (Android + PWA): - Ny AboutDialog med personvernerklæring, datakjelder og opphavsrett - Opphavsrett flytta frå sivilforsvardialogen til om-sida - Tilgjengeleg via «Om denne appen»-lenke i sivilforsvarsdialogen (Android) og ny om-knapp i statuslinja (PWA) - Lokalisert til en/nb/nn Personvern og sikkerheit: - Lagra GPS-posisjon utløper etter 24 timar (widget_prefs) - Widget viser «Trykk for å oppdatere» når posisjon manglar eller er utløpt - Eigendefinert User-Agent (Tilfluktsrom/1.6.1) i OkHttp - Content Security Policy (CSP) meta-tag i PWA - Tenararbeidar bufrar berre HTTP 200-svar (ikkje opake) - Kartbuffer-metadata runda til ~11km presisjon i localStorage - crossorigin="anonymous" på Leaflet CSS i18n-opprydding: - Unicode-escapes erstatta med UTF-8-teikn i nb.ts og nn.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6ba35add2f
commit
c1ac68e746
21 changed files with 469 additions and 34 deletions
|
|
@ -211,9 +211,12 @@ class ShelterWidgetProvider : AppWidgetProvider() {
|
|||
return getSavedLocation(context)
|
||||
}
|
||||
|
||||
/** Returns null if older than 24 hours to avoid retaining stale location data. */
|
||||
private fun getSavedLocation(context: Context): Location? {
|
||||
val prefs = context.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE)
|
||||
if (!prefs.contains("last_lat")) return null
|
||||
val age = System.currentTimeMillis() - prefs.getLong("last_time", 0L)
|
||||
if (age > 24 * 60 * 60 * 1000L) return null
|
||||
return Location("saved").apply {
|
||||
latitude = prefs.getFloat("last_lat", 0f).toDouble()
|
||||
longitude = prefs.getFloat("last_lon", 0f).toDouble()
|
||||
|
|
|
|||
|
|
@ -69,9 +69,12 @@ class WidgetUpdateWorker(
|
|||
return Result.success()
|
||||
}
|
||||
|
||||
/** Returns null if older than 24 hours. */
|
||||
private fun getSavedLocation(): Location? {
|
||||
val prefs = applicationContext.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE)
|
||||
if (!prefs.contains("last_lat")) return null
|
||||
val age = System.currentTimeMillis() - prefs.getLong("last_time", 0L)
|
||||
if (age > 24 * 60 * 60 * 1000L) return null
|
||||
return Location("saved").apply {
|
||||
latitude = prefs.getFloat("last_lat", 0f).toDouble()
|
||||
longitude = prefs.getFloat("last_lon", 0f).toDouble()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import kotlinx.coroutines.CancellationException
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONArray
|
||||
|
|
@ -43,6 +44,11 @@ class ShelterRepository(private val context: Context) {
|
|||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.addInterceptor(Interceptor { chain ->
|
||||
chain.proceed(chain.request().newBuilder()
|
||||
.header("User-Agent", "Tilfluktsrom/1.6.1")
|
||||
.build())
|
||||
})
|
||||
.build()
|
||||
|
||||
/** Reactive stream of all shelters from local cache. */
|
||||
|
|
|
|||
43
app/src/main/java/no/naiv/tilfluktsrom/ui/AboutDialog.kt
Normal file
43
app/src/main/java/no/naiv/tilfluktsrom/ui/AboutDialog.kt
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package no.naiv.tilfluktsrom.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import no.naiv.tilfluktsrom.R
|
||||
|
||||
/**
|
||||
* Full-screen dialog showing app info, privacy statement, and copyright.
|
||||
* Static content — all text comes from string resources for offline use and i18n.
|
||||
*/
|
||||
class AboutDialog : DialogFragment() {
|
||||
|
||||
companion object {
|
||||
const val TAG = "AboutDialog"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.Theme_Tilfluktsrom_Dialog)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return inflater.inflate(R.layout.dialog_about, container, false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
dialog?.window?.apply {
|
||||
setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package no.naiv.tilfluktsrom.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
|
@ -32,6 +31,13 @@ class CivilDefenseInfoDialog : DialogFragment() {
|
|||
return inflater.inflate(R.layout.dialog_civil_defense, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
view.findViewById<View>(R.id.aboutLink)?.setOnClickListener {
|
||||
AboutDialog().show(parentFragmentManager, AboutDialog.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
dialog?.window?.apply {
|
||||
|
|
|
|||
104
app/src/main/res/layout/dialog_about.xml
Normal file
104
app/src/main/res/layout/dialog_about.xml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/status_bar_bg"
|
||||
android:padding="20dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Title -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/about_title"
|
||||
android:textColor="@color/shelter_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<!-- App description -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/about_description"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- Privacy section -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/about_privacy_title"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/about_privacy_body"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- Data sources section -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/about_data_title"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/about_data_body"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- Stored on device section -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:text="@string/about_stored_title"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/about_stored_body"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- Copyright -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_copyright"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="11sp" />
|
||||
|
||||
<!-- Open source -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/about_open_source"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="11sp" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
|
@ -114,14 +114,16 @@
|
|||
android:textSize="12sp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
<!-- Copyright notice -->
|
||||
<!-- About link -->
|
||||
<TextView
|
||||
android:id="@+id/aboutLink"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/app_copyright"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="11sp" />
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/action_about"
|
||||
android:textColor="@color/shelter_primary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,18 @@
|
|||
<string name="civil_defense_step5_body">Én sammenhengende tone på omtrent 30 sekunder. Faren eller angrepet er over. Fortsett å følge instruksjoner fra myndighetene.</string>
|
||||
<string name="civil_defense_source">Kilde: DSB (Direktoratet for samfunnssikkerhet og beredskap)</string>
|
||||
|
||||
<!-- Om -->
|
||||
<string name="about_title">Om Tilfluktsrom</string>
|
||||
<string name="about_description">Tilfluktsrom hjelper deg med å finne nærmeste offentlige tilfluktsrom i Norge. Appen er laget for å fungere uten internett etter første oppsett — du trenger ikke nett for å finne tilfluktsrom, navigere med kompass eller dele posisjonen din.</string>
|
||||
<string name="about_privacy_title">Personvern</string>
|
||||
<string name="about_privacy_body">Denne appen samler ikke inn, sender eller deler noen personopplysninger. Det finnes ingen analyse, sporing eller tredjepartstjenester.\n\nGPS-posisjonen din brukes bare lokalt på enheten din for å finne tilfluktsrom i nærheten, og sendes aldri til noen server.</string>
|
||||
<string name="about_data_title">Datakilder</string>
|
||||
<string name="about_data_body">Tilfluktsromdata er offentlig informasjon fra Geonorge (Kartverket). Kartfliser lastes fra OpenStreetMap. Begge lagres lokalt for frakoblet bruk.</string>
|
||||
<string name="about_stored_title">Lagret på enheten din</string>
|
||||
<string name="about_stored_body">• Tilfluktsromdatabase (offentlige data fra Geonorge)\n• Kartfliser for frakoblet bruk\n• Din siste GPS-posisjon (for hjemmeskjerm-widgeten)\n\nIngen data forlater enheten din bortsett fra forespørsler om å laste ned tilfluktsromdata og kartfliser.</string>
|
||||
<string name="about_open_source">Åpen kildekode — kode.naiv.no/olemd/tilfluktsrom</string>
|
||||
<string name="action_about">Om denne appen</string>
|
||||
|
||||
<!-- Opphavsrett -->
|
||||
<string name="app_copyright">Opphavsrett © Ole-Morten Duesund</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,18 @@
|
|||
<string name="civil_defense_step5_body">Éin samanhengande tone på omtrent 30 sekund. Faren eller åtaket er over. Hald fram med å følgje instruksjonar frå styresmaktene.</string>
|
||||
<string name="civil_defense_source">Kjelde: DSB (Direktoratet for samfunnstryggleik og beredskap)</string>
|
||||
|
||||
<!-- Om -->
|
||||
<string name="about_title">Om Tilfluktsrom</string>
|
||||
<string name="about_description">Tilfluktsrom hjelper deg med å finne næraste offentlege tilfluktsrom i Noreg. Appen er laga for å fungere utan internett etter fyrste oppsett — du treng ikkje nett for å finne tilfluktsrom, navigere med kompass eller dele posisjonen din.</string>
|
||||
<string name="about_privacy_title">Personvern</string>
|
||||
<string name="about_privacy_body">Denne appen samlar ikkje inn, sender eller deler nokon personopplysingar. Det finst ingen analyse, sporing eller tredjepartstenester.\n\nGPS-posisjonen din vert berre brukt lokalt på eininga di for å finne tilfluktsrom i nærleiken, og vert aldri sendt til nokon tenar.</string>
|
||||
<string name="about_data_title">Datakjelder</string>
|
||||
<string name="about_data_body">Tilfluktsromdata er offentleg informasjon frå Geonorge (Kartverket). Kartfliser vert lasta frå OpenStreetMap. Begge vert lagra lokalt for fråkopla bruk.</string>
|
||||
<string name="about_stored_title">Lagra på eininga di</string>
|
||||
<string name="about_stored_body">• Tilfluktsromdatabase (offentlege data frå Geonorge)\n• Kartfliser for fråkopla bruk\n• Din siste GPS-posisjon (for heimeskjerm-widgeten)\n\nIngen data forlèt eininga di bortsett frå førespurnader om å laste ned tilfluktsromdata og kartfliser.</string>
|
||||
<string name="about_open_source">Open kjeldekode — kode.naiv.no/olemd/tilfluktsrom</string>
|
||||
<string name="action_about">Om denne appen</string>
|
||||
|
||||
<!-- Opphavsrett -->
|
||||
<string name="app_copyright">Opphavsrett © Ole-Morten Duesund</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -85,6 +85,18 @@
|
|||
<string name="civil_defense_step5_body">One continuous tone lasting approximately 30 seconds. The danger or attack is over. Continue to follow instructions from authorities.</string>
|
||||
<string name="civil_defense_source">Source: DSB (Norwegian Directorate for Civil Protection)</string>
|
||||
|
||||
<!-- About -->
|
||||
<string name="about_title">About Tilfluktsrom</string>
|
||||
<string name="about_description">Tilfluktsrom helps you find the nearest public shelter in Norway. The app is designed to work offline after initial setup — no internet required to find shelters, navigate by compass, or share your location.</string>
|
||||
<string name="about_privacy_title">Privacy</string>
|
||||
<string name="about_privacy_body">This app does not collect, transmit, or share any personal data. There are no analytics, tracking, or third-party services.\n\nYour GPS location is used only on your device to find nearby shelters and is never sent to any server.</string>
|
||||
<string name="about_data_title">Data sources</string>
|
||||
<string name="about_data_body">Shelter data is public information from Geonorge (Norwegian Mapping Authority). Map tiles are loaded from OpenStreetMap. Both are cached locally for offline use.</string>
|
||||
<string name="about_stored_title">Stored on your device</string>
|
||||
<string name="about_stored_body">• Shelter database (public data from Geonorge)\n• Map tiles for offline use\n• Your last GPS position (for the home screen widget)\n\nNo data leaves your device except requests to download shelter data and map tiles.</string>
|
||||
<string name="about_open_source">Open source — kode.naiv.no/olemd/tilfluktsrom</string>
|
||||
<string name="action_about">About this app</string>
|
||||
|
||||
<!-- Copyright -->
|
||||
<string name="app_copyright">Copyright © Ole-Morten Duesund</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -252,10 +252,13 @@ class ShelterWidgetProvider : AppWidgetProvider() {
|
|||
return getSavedLocation(context)
|
||||
}
|
||||
|
||||
/** Read the last GPS fix persisted by MainActivity to SharedPreferences. */
|
||||
/** Read the last GPS fix persisted by MainActivity to SharedPreferences.
|
||||
* Returns null if older than 24 hours to avoid retaining stale location data. */
|
||||
private fun getSavedLocation(context: Context): Location? {
|
||||
val prefs = context.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE)
|
||||
if (!prefs.contains("last_lat")) return null
|
||||
val age = System.currentTimeMillis() - prefs.getLong("last_time", 0L)
|
||||
if (age > 24 * 60 * 60 * 1000L) return null
|
||||
return Location("saved").apply {
|
||||
latitude = prefs.getFloat("last_lat", 0f).toDouble()
|
||||
longitude = prefs.getFloat("last_lon", 0f).toDouble()
|
||||
|
|
|
|||
|
|
@ -84,10 +84,13 @@ class WidgetUpdateWorker(
|
|||
return Result.success()
|
||||
}
|
||||
|
||||
/** Read the last GPS fix persisted by MainActivity. */
|
||||
/** Read the last GPS fix persisted by MainActivity.
|
||||
* Returns null if older than 24 hours. */
|
||||
private fun getSavedLocation(): Location? {
|
||||
val prefs = applicationContext.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE)
|
||||
if (!prefs.contains("last_lat")) return null
|
||||
val age = System.currentTimeMillis() - prefs.getLong("last_time", 0L)
|
||||
if (age > 24 * 60 * 60 * 1000L) return null
|
||||
return Location("saved").apply {
|
||||
latitude = prefs.getFloat("last_lat", 0f).toDouble()
|
||||
longitude = prefs.getFloat("last_lon", 0f).toDouble()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue