From 95e08e30e6d5b74fc67d6cb2a6b0fb38d31e217f Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Tue, 10 Mar 2026 19:25:46 +0100 Subject: [PATCH 01/25] Oppdater README med skjermbilete og bruksrettleiing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Legg til skjermbilete frå appen i toppen av README, ei «Slik fungerer appen»-seksjon som forklarar kart, kompass og sivilforsvarsinfo, og lenkjer til personvernerklæring og Standing on Shoulders-dokument. Co-Authored-By: Claude Opus 4.6 --- README.md | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8ddb151..89f6146 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,31 @@ Finn nærmeste offentlige tilfluktsrom i Norge. Appen er bygd for nødsituasjoner og fungerer uten internett etter første gangs bruk. +

+ Kartvisning med tilfluktsrom i Bergen sentrum + Valt tilfluktsrom med avstand og kapasitet + Kompassnavigasjon mot tilfluktsrom + Sivilforsvarsinfo: kva du skal gjere om alarmen går +

+ +## Slik fungerer appen + +**Kartvisning** — Appen viser alle 556 offentlige tilfluktsrom i Norge på eit OpenStreetMap-kart. Dei tre nærmaste tilfluktsromma visast i botnen med avstand, kapasitet og romnummer. Trykk på ei kart-markering eller eit listelement for å velje eit tilfluktsrom. + +**Kompassnavigasjon** — Trykk på kompassknappen for å byte til retningspil-visning. Ein stor pil peikar mot det valde tilfluktsrommet, med avstand i meter eller kilometer. Fungerer utan internett — berre GPS og kompassensor. + +**Sivilforsvarsinfo** — Trykk på info-knappen for å sjå trinn-for-trinn-rettleiing frå DSB om kva du skal gjere når alarmen går: viktig melding-signal, flyalarm, finn dekning, lytt til NRK på DAB-radio, og faren over. + ## Funksjoner -- **Finn nærmeste tilfluktsrom** — viser de tre nærmeste tilfluktsrommene med avstand og kapasitet -- **Kompassnavigasjon** — retningspil som peker mot valgt tilfluktsrom -- **Frakoblet kart** — kartfliser lagres automatisk for bruk uten nett -- **Velg fritt** — trykk på en hvilken som helst markør i kartet for å navigere dit -- **Flerspråklig** — engelsk, bokmål og nynorsk +- **Finn nærmaste tilfluktsrom** — viser dei tre nærmaste med avstand og kapasitet +- **Kompassnavigasjon** — retningspil som peikar mot valt tilfluktsrom +- **Frakobla kart** — kartfliser lagrast automatisk for bruk utan nett +- **Velg fritt** — trykk på ei markering i kartet for å navigere dit +- **Del tilfluktsrom** — send adresse, kapasitet og koordinatar til andre +- **Sivilforsvarsinfo** — rettleiing frå DSB om kva du skal gjere når alarmen går +- **Hjemmeskjerm-widget** — viser nærmaste tilfluktsrom utan å opne appen +- **Fleirspråkleg** — engelsk, bokmål og nynorsk ## Plattformer @@ -77,6 +95,10 @@ Appen er designet etter «offline-first»-prinsippet: - Databaseoppdateringer er atomiske (transaksjon) for å unngå datatap - Ingen persondata lagres — kun tilfluktsromdata og kartfliser +## Personvern + +Appen samlar ikkje inn persondata og har ingen analyse- eller sporingstenester. Posisjonsdata vert berre brukt lokalt på eininga for å finne nærmaste tilfluktsrom. Sjå [PRIVACY.md](PRIVACY.md) for fullstendig personvernerklæring. + ## Opphavsrett Copyright (c) Ole-Morten Duesund @@ -85,4 +107,8 @@ Copyright (c) Ole-Morten Duesund Kildekoden er lisensiert under [Mozilla Public License 2.0](LICENSE). -Appen bruker åpne data og tjenester fra flere kilder. Se [SOURCES.md](SOURCES.md) for en fullstendig oversikt over datakilder, URL-er og lisenser. +Appen bruker opne data og tenester frå fleire kjelder. Sjå [SOURCES.md](SOURCES.md) for ei fullstendig oversikt over datakjelder, URL-ar og lisensar. + +## Sjå også + +- [STANDING_ON_SHOULDERS.md](STANDING_ON_SHOULDERS.md) — estimat over dei ~116 000 menneska som har gjort denne appen mogleg From 4c801e86b1b1f35487bd11a497ce1860029a9b73 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Tue, 10 Mar 2026 19:30:17 +0100 Subject: [PATCH 02/25] =?UTF-8?q?Rett=20opp=20blandinga=20av=20nynorsk=20o?= =?UTF-8?q?g=20bokm=C3=A5l=20i=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README-en hadde ein del nynorskformer (eit, dei, sjå, peikar, lagrast, osv.) blanda inn i det som elles var bokmål. Retta til konsekvent bokmål gjennom heile dokumentet. Co-Authored-By: Claude Opus 4.6 --- README.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 89f6146..20bfee9 100644 --- a/README.md +++ b/README.md @@ -4,29 +4,29 @@ Finn nærmeste offentlige tilfluktsrom i Norge. Appen er bygd for nødsituasjone

Kartvisning med tilfluktsrom i Bergen sentrum - Valt tilfluktsrom med avstand og kapasitet + Valgt tilfluktsrom med avstand og kapasitet Kompassnavigasjon mot tilfluktsrom - Sivilforsvarsinfo: kva du skal gjere om alarmen går + Sivilforsvarsinfo: hva du skal gjøre om alarmen går

## Slik fungerer appen -**Kartvisning** — Appen viser alle 556 offentlige tilfluktsrom i Norge på eit OpenStreetMap-kart. Dei tre nærmaste tilfluktsromma visast i botnen med avstand, kapasitet og romnummer. Trykk på ei kart-markering eller eit listelement for å velje eit tilfluktsrom. +**Kartvisning** — Appen viser alle 556 offentlige tilfluktsrom i Norge på et OpenStreetMap-kart. De tre nærmeste tilfluktsrommene vises i bunnen med avstand, kapasitet og romnummer. Trykk på en kartmarkering eller et listeelement for å velge et tilfluktsrom. -**Kompassnavigasjon** — Trykk på kompassknappen for å byte til retningspil-visning. Ein stor pil peikar mot det valde tilfluktsrommet, med avstand i meter eller kilometer. Fungerer utan internett — berre GPS og kompassensor. +**Kompassnavigasjon** — Trykk på kompassknappen for å bytte til retningspil-visning. En stor pil peker mot det valgte tilfluktsrommet, med avstand i meter eller kilometer. Fungerer uten internett — bare GPS og kompassensor. -**Sivilforsvarsinfo** — Trykk på info-knappen for å sjå trinn-for-trinn-rettleiing frå DSB om kva du skal gjere når alarmen går: viktig melding-signal, flyalarm, finn dekning, lytt til NRK på DAB-radio, og faren over. +**Sivilforsvarsinfo** — Trykk på info-knappen for å se trinn-for-trinn-veiledning fra DSB om hva du skal gjøre når alarmen går: viktig melding-signal, flyalarm, finn dekning, lytt til NRK på DAB-radio, og faren over. ## Funksjoner -- **Finn nærmaste tilfluktsrom** — viser dei tre nærmaste med avstand og kapasitet -- **Kompassnavigasjon** — retningspil som peikar mot valt tilfluktsrom -- **Frakobla kart** — kartfliser lagrast automatisk for bruk utan nett -- **Velg fritt** — trykk på ei markering i kartet for å navigere dit -- **Del tilfluktsrom** — send adresse, kapasitet og koordinatar til andre -- **Sivilforsvarsinfo** — rettleiing frå DSB om kva du skal gjere når alarmen går -- **Hjemmeskjerm-widget** — viser nærmaste tilfluktsrom utan å opne appen -- **Fleirspråkleg** — engelsk, bokmål og nynorsk +- **Finn nærmeste tilfluktsrom** — viser de tre nærmeste med avstand og kapasitet +- **Kompassnavigasjon** — retningspil som peker mot valgt tilfluktsrom +- **Frakoblet kart** — kartfliser lagres automatisk for bruk uten nett +- **Velg fritt** — trykk på en markering i kartet for å navigere dit +- **Del tilfluktsrom** — send adresse, kapasitet og koordinater til andre +- **Sivilforsvarsinfo** — veiledning fra DSB om hva du skal gjøre når alarmen går +- **Hjemmeskjerm-widget** — viser nærmeste tilfluktsrom uten å åpne appen +- **Flerspråklig** — engelsk, bokmål og nynorsk ## Plattformer @@ -97,7 +97,7 @@ Appen er designet etter «offline-first»-prinsippet: ## Personvern -Appen samlar ikkje inn persondata og har ingen analyse- eller sporingstenester. Posisjonsdata vert berre brukt lokalt på eininga for å finne nærmaste tilfluktsrom. Sjå [PRIVACY.md](PRIVACY.md) for fullstendig personvernerklæring. +Appen samler ikke inn persondata og har ingen analyse- eller sporingstjenester. Posisjonsdata brukes bare lokalt på enheten for å finne nærmeste tilfluktsrom. Se [PRIVACY.md](PRIVACY.md) for fullstendig personvernerklæring. ## Opphavsrett @@ -107,8 +107,8 @@ Copyright (c) Ole-Morten Duesund Kildekoden er lisensiert under [Mozilla Public License 2.0](LICENSE). -Appen bruker opne data og tenester frå fleire kjelder. Sjå [SOURCES.md](SOURCES.md) for ei fullstendig oversikt over datakjelder, URL-ar og lisensar. +Appen bruker åpne data og tjenester fra flere kilder. Se [SOURCES.md](SOURCES.md) for en fullstendig oversikt over datakilder, URL-er og lisenser. -## Sjå også +## Se også -- [STANDING_ON_SHOULDERS.md](STANDING_ON_SHOULDERS.md) — estimat over dei ~116 000 menneska som har gjort denne appen mogleg +- [STANDING_ON_SHOULDERS.md](STANDING_ON_SHOULDERS.md) — estimat over de ~116 000 menneskene som har gjort denne appen mulig From f1b405950e6f53133eb1dee3ccbc8ba8da4ef360 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Tue, 10 Mar 2026 20:09:29 +0100 Subject: [PATCH 03/25] Forbedre estimater i STANDING_ON_SHOULDERS med kilder Erstatt grove estimater med reelle bidragstall fra GitHub, OpenHub og prosjektsider. Legg til kildelenker for ~20 prosjekter, Mermaid-diagram, og seksjon med interessante detaljer (SQLite med ~4 utviklere, Leaflet fra Ukraina). Legg til Forgejo/Gitea og GitHub som manglende lag. Oppdatert totalestimat: ~119 000 mennesker. Co-Authored-By: Claude Opus 4.6 --- README.md | 2 +- STANDING_ON_SHOULDERS.md | 196 ++++++++++++++++++++++----------------- 2 files changed, 114 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 20bfee9..8a79b98 100644 --- a/README.md +++ b/README.md @@ -111,4 +111,4 @@ Appen bruker åpne data og tjenester fra flere kilder. Se [SOURCES.md](SOURCES.m ## Se også -- [STANDING_ON_SHOULDERS.md](STANDING_ON_SHOULDERS.md) — estimat over de ~116 000 menneskene som har gjort denne appen mulig +- [STANDING_ON_SHOULDERS.md](STANDING_ON_SHOULDERS.md) — estimat over de ~119 000 menneskene som har gjort denne appen mulig diff --git a/STANDING_ON_SHOULDERS.md b/STANDING_ON_SHOULDERS.md index e33c1bd..f61b1e2 100644 --- a/STANDING_ON_SHOULDERS.md +++ b/STANDING_ON_SHOULDERS.md @@ -5,96 +5,107 @@ Tilfluktsrom is a small Android app — about 900 source files — that helps Norwegians find their nearest public shelter in an emergency. One person built it in under a day. But that was only possible because of the accumulated work of -roughly **100,000–120,000 identifiable people**, spanning decades, countries, and -disciplines. +roughly **119,000 people**, spanning decades, countries, and disciplines. -This document traces the human effort behind every layer of the stack. +This document traces the human effort behind every layer of the stack, with +sources for each estimate. + +```mermaid +pie title People behind each layer + "Map data (OSM)" : 50000 + "Linux kernel" : 20000 + "Physical infrastructure" : 10500 + "Build tools & dev infra" : 6700 + "AI-assisted development" : 6000 + "Shelter data & builders" : 6000 + "Internet & standards" : 5250 + "OS & runtimes (excl. Linux)" : 3800 + "Libraries (Android)" : 2100 + "Libraries (PWA)" : 1750 + "Hosting & distribution" : 5700 + "Programming languages" : 1600 +``` --- ## Layer 0: Physical Infrastructure — GPS & Sensors (~10,500 people) -| Component | Role | Est. people | -|---|---|---| -| GPS constellation | 31 satellites, maintained by US Space Force | ~5,000 | -| Magnetometer/compass sensors | Enable the direction arrow to point at shelters | ~500 | -| ARM architecture | The CPU instruction set running every Android device | ~5,000 | +| Component | Role | Est. people | Source | +|---|---|---|---| +| GPS constellation | 31 satellites, maintained by US Space Force | ~5,000 | Industry estimate; [GPS.gov](https://www.gps.gov/) | +| Magnetometer/compass sensors | Enable the direction arrow to point at shelters | ~500 | Industry estimate | +| ARM architecture | The CPU instruction set running every Android device | ~5,000 | [Arm had 8,330 employees in 2025](https://www.macrotrends.net/stocks/charts/ARM/arm-holdings/number-of-employees); ~5,000 estimated over the architecture's 40-year history | Before a single line of code runs, hardware designed by tens of thousands of engineers must be in orbit, in your pocket, and on the circuit board. ## Layer 1: Internet & Standards (~5,250 people) -| Component | Role | Est. people | -|---|---|---| -| TCP/IP, DNS, HTTP, TLS | The protocols that carry shelter data from server to phone | ~5,000 | -| GeoJSON specification | The format the shelter data is published in (IETF RFC 7946) | ~50 | -| EPSG / coordinate reference systems | The math behind UTM33N → WGS84 coordinate conversion | ~200 | +| Component | Role | Est. people | Source | +|---|---|---|---| +| TCP/IP, DNS, HTTP, TLS | The protocols that carry shelter data from server to phone | ~5,000 | Cumulative IETF/W3C contributors over decades | +| GeoJSON specification | The format the shelter data is published in (IETF RFC 7946) | ~50 | [RFC 7946 authors + WG](https://datatracker.ietf.org/doc/html/rfc7946) | +| EPSG / coordinate reference systems | The math behind UTM33N → WGS84 coordinate conversion | ~200 | [IOGP Geomatics Committee](https://epsg.org/) | -## Layer 2: Operating Systems & Runtimes (~27,200 people) +## Layer 2: Operating Systems & Runtimes (~23,800 people) -| Component | Role | Est. people | -|---|---|---| -| Linux kernel | Foundation of Android; ~20,000 documented unique contributors | ~20,000 | -| Android (AOSP) | The mobile OS, built on Linux by Google + community | ~5,000 | -| JVM / OpenJDK + Java | The language runtime Kotlin compiles to | ~2,000 | -| ART (Android Runtime) | Replaced Dalvik; runs the compiled bytecode | ~200 | +| Component | Role | Est. people | Source | +|---|---|---|---| +| Linux kernel | Foundation of Android | ~20,000 | [Linux Foundation: ~20,000+ unique contributors since 2005](https://www.linuxfoundation.org/blog/blog/2017-linux-kernel-report-highlights-developers-roles-accelerating-pace-change) | +| Android (AOSP) | Mobile OS, incl. ART runtime | ~2,000 | [ResearchGate study: ~1,563 contributors](https://www.researchgate.net/figure/Top-Companies-Contributing-to-Android-Projects_tbl3_236631958); likely higher now | +| OpenJDK | The Java runtime Kotlin compiles to | ~1,800 | [GitHub: ~1,779 contributors](https://github.com/openjdk/jdk) | -## Layer 3: Programming Languages (~1,200 people) +## Layer 3: Programming Languages (~1,600 people) -| Language | Origin | Est. people | -|---|---|---| -| Kotlin | JetBrains (Czech Republic/Russia) + community | ~500 | -| TypeScript | Microsoft + community (for the PWA) | ~500 | -| Groovy / Kotlin DSL | Gradle build scripts | ~200 | +| Language | Origin | Contributors | Source | +|---|---|---|---| +| Kotlin | JetBrains + community | ~765 | [GitHub: JetBrains/kotlin](https://github.com/JetBrains/kotlin) | +| TypeScript | Microsoft + community (for the PWA) | ~823 | [GitHub: microsoft/TypeScript](https://github.com/microsoft/TypeScript) | -## Layer 4: Build Tools & Dev Infrastructure (~5,400 people) +## Layer 4: Build Tools & Dev Infrastructure (~6,700 people) -| Tool | Role | Est. people | -|---|---|---| -| Gradle | Build automation | ~500 | -| Android Gradle Plugin | Android-specific build pipeline | ~200 | -| KSP (Kotlin Symbol Processing) | Code generation for Room database | ~100 | -| R8 / ProGuard | Release minification and optimization | ~100 | -| Vite | PWA bundler | ~800 | -| Bun | Package manager and JS runtime | ~400 | -| Git | Version control | ~1,500 | -| Android Studio / IntelliJ | IDE (JetBrains + Google) | ~1,500 | -| Maven Central, Google Maven, npm | Package registry infrastructure | ~300 | +| Tool | Role | Contributors | Source | +|---|---|---|---| +| Gradle | Build automation | ~869 | [GitHub: gradle/gradle](https://github.com/gradle/gradle) | +| Android Gradle Plugin | Android-specific build pipeline | ~200 | Google internal; estimate | +| KSP (Kotlin Symbol Processing) | Code generation for Room database | ~100 | Estimate based on [GitHub: google/ksp](https://github.com/google/ksp) | +| R8 / ProGuard | Release minification and optimization | ~100 | Estimate | +| Vite | PWA bundler | ~1,100 | [GitHub: vitejs/vite](https://github.com/vitejs/vite) | +| Bun | Package manager and JS runtime | ~733 | [GitHub: oven-sh/bun](https://github.com/oven-sh/bun) | +| Git | Version control | ~1,820 | [GitHub: git/git](https://github.com/git/git) | +| Android Studio / IntelliJ | IDE | ~1,500 | Estimate; JetBrains + Google | +| Maven Central, Google Maven, npm | Package registry infrastructure | ~300 | Estimate | -## Layer 5: Libraries — Android App (~2,550 people) +## Layer 5: Libraries — Android App (~2,100 people) -| Library | What it does | Est. people | -|---|---|---| -| AndroidX (Core, AppCompat, Activity, Lifecycle) | UI and app architecture foundation | ~800 | -| Material Design | Visual design language, research, and components | ~500 | -| ConstraintLayout | Flexible screen layouts | ~100 | -| Room | Type-safe SQLite wrapper for the shelter cache | ~200 | -| WorkManager | Periodic home screen widget updates | ~150 | -| Kotlinx Coroutines | Async data loading without blocking the UI | ~200 | -| OkHttp (Square) | Downloads the GeoJSON ZIP from Geonorge | ~200 | -| OSMDroid | Offline OpenStreetMap rendering | ~150 | -| Play Services Location | FusedLocationProvider for precise GPS | ~200 | -| SQLite | The embedded database engine | ~50 | +| Library | What it does | Contributors | Source | +|---|---|---|---| +| AndroidX (Core, AppCompat, Room, WorkManager, etc.) | UI, architecture, database, scheduling | ~1,000 | [GitHub: androidx/androidx](https://github.com/androidx/androidx) monorepo | +| Material Design Components | Visual design language and components | ~199 | [GitHub: material-components-android](https://github.com/material-components/material-components-android) | +| Kotlinx Coroutines | Async data loading without blocking the UI | ~308 | [GitHub: Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) | +| OkHttp | Downloads the GeoJSON ZIP from Geonorge | ~287 | [GitHub: square/okhttp](https://github.com/square/okhttp) | +| OSMDroid | Offline OpenStreetMap rendering | ~105 | [GitHub: osmdroid/osmdroid](https://github.com/osmdroid/osmdroid) | +| Play Services Location | FusedLocationProvider for precise GPS | ~200 | Google internal; estimate | +| SQLite | The embedded database engine | **~4** | [sqlite.org/crew.html](https://sqlite.org/crew.html) — the most deployed database in the world, maintained by 3–4 people | -## Layer 6: Libraries — PWA (~1,350 people) +## Layer 6: Libraries — PWA (~1,750 people) -| Library | Role | Est. people | -|---|---|---| -| Leaflet | Interactive web maps (created in Ukraine) | ~800 | -| leaflet.offline | Offline tile caching | ~20 | -| idb | IndexedDB wrapper for offline storage | ~30 | -| vite-plugin-pwa | Service worker and Workbox integration | ~100 | -| Vitest | Test framework | ~400 | +| Library | Role | Contributors | Source | +|---|---|---|---| +| Leaflet | Interactive web maps (created in Ukraine) | ~865 | [GitHub: Leaflet/Leaflet](https://github.com/Leaflet/Leaflet) | +| leaflet.offline | Offline tile caching | ~20 | Estimate based on GitHub | +| idb | IndexedDB wrapper for offline storage | ~30 | Estimate based on GitHub | +| vite-plugin-pwa | Service worker and Workbox integration | ~100 | Estimate based on GitHub | +| Vitest | Test framework | ~718 | [GitHub: vitest-dev/vitest](https://github.com/vitest-dev/vitest) | ## Layer 7: Data — The Content That Makes It Useful (~56,000 people) -| Source | Role | Est. people | -|---|---|---| -| OpenStreetMap | Global map data; ~2M registered mappers, ~10,000+ active in Norway | ~50,000 | -| Kartverket / Geonorge | Norwegian Mapping Authority; national geodata infrastructure | ~800 | -| DSB (Direktoratet for samfunnssikkerhet og beredskap) | Created and maintains the public shelter registry | ~200 | -| The shelter builders | Construction, engineering, civil defense planning since the Cold War | ~5,000+ | +| Source | Role | Est. people | Source link | +|---|---|---|---| +| OpenStreetMap | Global map data | ~50,000 | [~2.25M have ever edited; ~50,000 active monthly](https://wiki.openstreetmap.org/wiki/Stats) | +| Kartverket / Geonorge | Norwegian Mapping Authority; national geodata infrastructure | ~800 | [kartverket.no](https://www.kartverket.no/) | +| DSB | Created and maintains the public shelter registry | ~200 | [dsb.no](https://www.dsb.no/) | +| The shelter builders | Construction, engineering, civil defense planning since the Cold War | ~5,000 | Estimate based on ~556 shelters built 1950s–80s | The app's data exists because of Cold War civil defense planning. The shelters were built in the 1950s–80s, digitized by DSB, published via Geonorge's open @@ -103,18 +114,20 @@ GeoJSON file. ## Layer 8: AI-Assisted Development (~6,000 people) -| Component | Role | Est. people | -|---|---|---| -| Anthropic / Claude | Researchers, engineers, safety team | ~1,000 | -| ML research lineage | Transformers, attention, RLHF, scaling laws — across academia & industry | ~5,000 | -| Training data | The collective written output of humanity | incalculable | +| Component | Role | Est. people | Source | +|---|---|---|---| +| Anthropic / Claude | Researchers, engineers, safety team | ~1,000 | [anthropic.com](https://www.anthropic.com/) | +| ML research lineage | Transformers, attention, RLHF, scaling laws — across academia & industry | ~5,000 | Estimate across all contributing institutions | +| Training data | The collective written output of humanity | incalculable | | -## Layer 9: Distribution (~500 people) +## Layer 9: Hosting & Distribution (~5,700 people) -| Component | Role | Est. people | -|---|---|---| -| F-Droid | Open-source app store infrastructure and review | ~300 | -| Fastlane | Metadata and screenshot tooling | ~200 | +| Component | Role | Contributors | Source | +|---|---|---|---| +| Forgejo / Gitea | Hosts this project at kode.naiv.no; Forgejo forked from Gitea in 2022 | ~800 | [Forgejo: ~230 contributors](https://codeberg.org/forgejo/forgejo); [Gitea: go-gitea/gitea](https://github.com/go-gitea/gitea) | +| GitHub | Mirror repo + hosts nearly all upstream dependencies | ~3,000 | [~5,000 employees, ~50% engineers](https://kinsta.com/blog/github-statistics/) | +| F-Droid | Open-source app store infrastructure and review | ~150 | [GitLab: fdroid](https://gitlab.com/fdroid); estimate | +| Fastlane | Metadata and screenshot tooling | ~1,524 | [GitHub: fastlane/fastlane](https://github.com/fastlane/fastlane) | --- @@ -124,15 +137,15 @@ GeoJSON file. |---|---| | Physical infrastructure (GPS, ARM, sensors) | ~10,500 | | Internet & standards | ~5,250 | -| Operating systems & runtimes | ~27,200 | -| Programming languages | ~1,200 | -| Build tools & dev infrastructure | ~5,400 | -| Direct libraries (Android) | ~2,550 | -| Direct libraries (PWA) | ~1,350 | +| Operating systems & runtimes | ~23,800 | +| Programming languages | ~1,600 | +| Build tools & dev infrastructure | ~6,700 | +| Direct libraries (Android) | ~2,100 | +| Direct libraries (PWA) | ~1,750 | | Data (maps, shelters, geodesy) | ~56,000 | | AI-assisted development | ~6,000 | -| Distribution | ~500 | -| **Conservative total** | **~116,000** | +| Hosting & distribution | ~5,700 | +| **Conservative total** | **~119,000** | This is conservative. It excludes: @@ -147,9 +160,26 @@ Including OpenStreetMap's full contributor base and hardware, the number crosses --- +## Notable details + +- **SQLite** — the most widely deployed database engine in the world (in every + phone, browser, and operating system) — is maintained by + [3–4 people](https://sqlite.org/crew.html). It powers every shelter lookup + in this app. + +- **Leaflet** — the JavaScript mapping library used by the PWA — was created by + [Volodymyr Agafonkin](https://agafonkin.com/) in Kyiv, Ukraine. An emergency + shelter app built with a mapping library from a country at war. + +- **OpenStreetMap** has ~10 million registered accounts, but only ~50,000 are + active in any given month. The map tiles this app displays are the work of a + dedicated minority. + +--- + ## Perspective -For every line of application code, roughly 100,000 people made the tools, data, +For every line of application code, roughly 119,000 people made the tools, data, and infrastructure that line depends on. No single company, country, or organization could have built this stack alone. Linux (Finland → global), Kotlin (Czech Republic/Russia → JetBrains), OSM (UK → global), GPS (US military → From ff4b3245f55980d341169559da073624bebe6886 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Tue, 10 Mar 2026 20:25:47 +0100 Subject: [PATCH 04/25] Legg til fdroid-byggvariant uten Google Play Services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splitt LocationProvider, ShelterWidgetProvider og WidgetUpdateWorker i to varianter: standard (med Play Services for bedre GPS) og fdroid (kun AOSP LocationManager). Play Services-avhengigheten er nå begrenset til standardImplementation. Begge varianter bygger og har identisk funksjonalitet — fdroid-varianten mangler bare FusedLocationProviderClient som en ekstra lokasjonskilde. Co-Authored-By: Claude Opus 4.6 --- app/build.gradle.kts | 14 +- .../tilfluktsrom/location/LocationProvider.kt | 119 ++++++++ .../widget/ShelterWidgetProvider.kt | 265 ++++++++++++++++++ .../tilfluktsrom/widget/WidgetUpdateWorker.kt | 133 +++++++++ .../tilfluktsrom/location/LocationProvider.kt | 0 .../widget/ShelterWidgetProvider.kt | 0 .../tilfluktsrom/widget/WidgetUpdateWorker.kt | 0 7 files changed, 529 insertions(+), 2 deletions(-) create mode 100644 app/src/fdroid/java/no/naiv/tilfluktsrom/location/LocationProvider.kt create mode 100644 app/src/fdroid/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt create mode 100644 app/src/fdroid/java/no/naiv/tilfluktsrom/widget/WidgetUpdateWorker.kt rename app/src/{main => standard}/java/no/naiv/tilfluktsrom/location/LocationProvider.kt (100%) rename app/src/{main => standard}/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt (100%) rename app/src/{main => standard}/java/no/naiv/tilfluktsrom/widget/WidgetUpdateWorker.kt (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d905c26..6270317 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -40,6 +40,16 @@ android { } } + flavorDimensions += "distribution" + productFlavors { + create("standard") { + dimension = "distribution" + } + create("fdroid") { + dimension = "distribution" + } + } + buildTypes { release { isMinifyEnabled = true @@ -88,8 +98,8 @@ dependencies { // OSMDroid (offline-capable OpenStreetMap) implementation("org.osmdroid:osmdroid-android:6.1.20") - // Google Play Services Location (precise GPS) - implementation("com.google.android.gms:play-services-location:21.3.0") + // Google Play Services Location (precise GPS) — standard flavor only + "standardImplementation"("com.google.android.gms:play-services-location:21.3.0") // WorkManager (periodic widget updates) implementation("androidx.work:work-runtime-ktx:2.9.1") diff --git a/app/src/fdroid/java/no/naiv/tilfluktsrom/location/LocationProvider.kt b/app/src/fdroid/java/no/naiv/tilfluktsrom/location/LocationProvider.kt new file mode 100644 index 0000000..87e19af --- /dev/null +++ b/app/src/fdroid/java/no/naiv/tilfluktsrom/location/LocationProvider.kt @@ -0,0 +1,119 @@ +package no.naiv.tilfluktsrom.location + +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 kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +/** + * Provides GPS location updates using AOSP LocationManager. + * + * F-Droid flavor: no Google Play Services dependency. Uses GPS + Network providers + * directly via LocationManager (available on all Android 8.0+ devices). + */ +class LocationProvider(private val context: Context) { + + companion object { + private const val TAG = "LocationProvider" + private const val UPDATE_INTERVAL_MS = 5000L + } + + init { + Log.d(TAG, "Location backend: LocationManager (F-Droid build)") + } + + /** + * Stream of location updates. Emits the last known location first (if available), + * then continuous updates. Throws SecurityException if permission is not granted. + */ + fun locationUpdates(): Flow = callbackFlow { + if (!hasLocationPermission()) { + close(SecurityException("Location permission not granted")) + return@callbackFlow + } + + 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") + } + } + } catch (e: SecurityException) { + Log.e(TAG, "SecurityException getting last known location", e) + } + + // 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") + } + } + + // 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", e) + close(e) + return@callbackFlow + } + + awaitClose { + locationManager.removeUpdates(listener) + } + } + + fun hasLocationPermission(): Boolean { + return ContextCompat.checkSelfPermission( + context, Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + } +} diff --git a/app/src/fdroid/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt b/app/src/fdroid/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt new file mode 100644 index 0000000..b144b01 --- /dev/null +++ b/app/src/fdroid/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt @@ -0,0 +1,265 @@ +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.os.Build +import android.os.CancellationSignal +import android.text.format.DateFormat +import android.util.Log +import android.widget.RemoteViews +import androidx.core.content.ContextCompat +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 +import java.util.concurrent.TimeUnit + +/** + * Home screen widget showing the nearest shelter with distance. + * + * F-Droid flavor: uses LocationManager only (no Google Play Services). + */ +class ShelterWidgetProvider : AppWidgetProvider() { + + companion object { + private const val TAG = "ShelterWidget" + const val ACTION_REFRESH = "no.naiv.tilfluktsrom.widget.REFRESH" + private const val EXTRA_LATITUDE = "lat" + private const val EXTRA_LONGITUDE = "lon" + + fun requestUpdate(context: Context) { + val intent = Intent(context, ShelterWidgetProvider::class.java).apply { + action = ACTION_REFRESH + } + context.sendBroadcast(intent) + } + + fun requestUpdateWithLocation(context: Context, latitude: Double, longitude: Double) { + val intent = Intent(context, ShelterWidgetProvider::class.java).apply { + action = ACTION_REFRESH + putExtra(EXTRA_LATITUDE, latitude) + putExtra(EXTRA_LONGITUDE, longitude) + } + context.sendBroadcast(intent) + } + } + + override fun onEnabled(context: Context) { + super.onEnabled(context) + WidgetUpdateWorker.schedule(context) + } + + override fun onDisabled(context: Context) { + super.onDisabled(context) + WidgetUpdateWorker.cancel(context) + } + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + WidgetUpdateWorker.schedule(context) + updateAllWidgetsAsync(context, null) + } + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + + if (intent.action == ACTION_REFRESH) { + val providedLocation = if (intent.hasExtra(EXTRA_LATITUDE)) { + Location("widget").apply { + latitude = intent.getDoubleExtra(EXTRA_LATITUDE, 0.0) + longitude = intent.getDoubleExtra(EXTRA_LONGITUDE, 0.0) + } + } else null + + updateAllWidgetsAsync(context, providedLocation) + } + } + + private fun updateAllWidgetsAsync(context: Context, providedLocation: Location?) { + val pendingResult = goAsync() + Thread { + try { + val appWidgetManager = AppWidgetManager.getInstance(context) + val widgetIds = appWidgetManager.getAppWidgetIds( + ComponentName(context, ShelterWidgetProvider::class.java) + ) + val location = providedLocation ?: getBestLocation(context) + for (appWidgetId in widgetIds) { + updateWidget(context, appWidgetManager, appWidgetId, location) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to update widgets", e) + } finally { + pendingResult.finish() + } + }.start() + } + + private fun updateWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + location: Location? + ) { + val views = RemoteViews(context.packageName, R.layout.widget_nearest_shelter) + + val openAppIntent = Intent(context, MainActivity::class.java) + val openAppPending = PendingIntent.getActivity( + context, 0, openAppIntent, PendingIntent.FLAG_IMMUTABLE + ) + views.setOnClickPendingIntent(R.id.widgetRoot, openAppPending) + + 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) + + if (ContextCompat.checkSelfPermission( + context, Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + showFallback(context, views, context.getString(R.string.widget_open_app)) + appWidgetManager.updateAppWidget(appWidgetId, views) + return + } + + if (location == null) { + showFallback(context, views, context.getString(R.string.widget_no_location)) + appWidgetManager.updateAppWidget(appWidgetId, views) + return + } + + val shelters = try { + val dao = ShelterDatabase.getInstance(context).shelterDao() + kotlinx.coroutines.runBlocking { dao.getAllSheltersList() } + } catch (e: Exception) { + Log.e(TAG, "Failed to query shelters", e) + emptyList() + } + + if (shelters.isEmpty()) { + showFallback(context, views, context.getString(R.string.widget_no_data)) + appWidgetManager.updateAppWidget(appWidgetId, views) + return + } + + val nearest = ShelterFinder.findNearest( + shelters, location.latitude, location.longitude, 1 + ).firstOrNull() + + if (nearest == null) { + showFallback(context, views, context.getString(R.string.widget_no_data)) + appWidgetManager.updateAppWidget(appWidgetId, views) + return + } + + 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) + ) + views.setTextViewText(R.id.widgetTimestamp, formatTimestamp(context)) + + appWidgetManager.updateAppWidget(appWidgetId, views) + } + + private fun showFallback(context: Context, views: RemoteViews, message: String) { + views.setTextViewText(R.id.widgetAddress, message) + views.setTextViewText(R.id.widgetDetails, "") + views.setTextViewText(R.id.widgetDistance, "") + views.setTextViewText(R.id.widgetTimestamp, formatTimestamp(context)) + } + + private fun formatTimestamp(context: Context): String { + val format = DateFormat.getTimeFormat(context) + val timeStr = format.format(System.currentTimeMillis()) + return context.getString(R.string.widget_updated_at, timeStr) + } + + /** + * Get the best available location via LocationManager or SharedPreferences. + * Safe to call from a background thread. + */ + private fun getBestLocation(context: Context): Location? { + if (ContextCompat.checkSelfPermission( + context, Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) return null + + val lmLocation = getLocationManagerLocation(context) + if (lmLocation != null) return lmLocation + + return getSavedLocation(context) + } + + private fun getSavedLocation(context: Context): Location? { + val prefs = context.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE) + if (!prefs.contains("last_lat")) return null + return Location("saved").apply { + latitude = prefs.getFloat("last_lat", 0f).toDouble() + longitude = prefs.getFloat("last_lon", 0f).toDouble() + } + } + + private fun getLocationManagerLocation(context: Context): Location? { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) + as? LocationManager ?: return null + + try { + val lastGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + val lastNetwork = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) + val cached = listOfNotNull(lastGps, lastNetwork).maxByOrNull { it.time } + if (cached != null) return cached + } catch (e: SecurityException) { + Log.e(TAG, "SecurityException getting last known location", e) + return null + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val provider = when { + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) -> + LocationManager.NETWORK_PROVIDER + locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) -> + LocationManager.GPS_PROVIDER + else -> return null + } + try { + val latch = java.util.concurrent.CountDownLatch(1) + var result: Location? = null + val signal = CancellationSignal() + locationManager.getCurrentLocation( + provider, signal, context.mainExecutor + ) { location -> + result = location + latch.countDown() + } + latch.await(10, TimeUnit.SECONDS) + signal.cancel() + return result + } catch (e: Exception) { + Log.e(TAG, "Active location request failed", e) + } + } + + return null + } +} diff --git a/app/src/fdroid/java/no/naiv/tilfluktsrom/widget/WidgetUpdateWorker.kt b/app/src/fdroid/java/no/naiv/tilfluktsrom/widget/WidgetUpdateWorker.kt new file mode 100644 index 0000000..1b83c09 --- /dev/null +++ b/app/src/fdroid/java/no/naiv/tilfluktsrom/widget/WidgetUpdateWorker.kt @@ -0,0 +1,133 @@ +package no.naiv.tilfluktsrom.widget + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationManager +import android.os.Build +import android.os.CancellationSignal +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume + +/** + * Periodic background worker that refreshes the home screen widget. + * + * F-Droid flavor: uses LocationManager only (no Google Play Services). + */ +class WidgetUpdateWorker( + context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params) { + + companion object { + private const val TAG = "WidgetUpdateWorker" + private const val WORK_NAME = "widget_update" + private const val LOCATION_TIMEOUT_MS = 10_000L + + fun schedule(context: Context) { + val request = PeriodicWorkRequestBuilder( + 15, TimeUnit.MINUTES + ).build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + WORK_NAME, + ExistingPeriodicWorkPolicy.KEEP, + request + ) + } + + fun runOnce(context: Context) { + val request = OneTimeWorkRequestBuilder().build() + WorkManager.getInstance(context).enqueue(request) + } + + fun cancel(context: Context) { + WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME) + } + } + + override suspend fun doWork(): Result { + val location = requestFreshLocation() ?: getSavedLocation() + if (location != null) { + ShelterWidgetProvider.requestUpdateWithLocation( + applicationContext, location.latitude, location.longitude + ) + } else { + ShelterWidgetProvider.requestUpdate(applicationContext) + } + return Result.success() + } + + private fun getSavedLocation(): Location? { + val prefs = applicationContext.getSharedPreferences("widget_prefs", Context.MODE_PRIVATE) + if (!prefs.contains("last_lat")) return null + return Location("saved").apply { + latitude = prefs.getFloat("last_lat", 0f).toDouble() + longitude = prefs.getFloat("last_lon", 0f).toDouble() + } + } + + private suspend fun requestFreshLocation(): Location? { + val context = applicationContext + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED + ) return null + + return requestViaLocationManager() + } + + private suspend fun requestViaLocationManager(): Location? { + val locationManager = applicationContext.getSystemService(Context.LOCATION_SERVICE) + as? LocationManager ?: return null + + val provider = when { + locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) -> + LocationManager.GPS_PROVIDER + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) -> + LocationManager.NETWORK_PROVIDER + else -> return null + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return requestCurrentLocation(locationManager, provider) + } + // API 26-29: fall back to passive cache + return try { + locationManager.getLastKnownLocation(provider) + } catch (e: SecurityException) { + null + } + } + + private suspend fun requestCurrentLocation(locationManager: LocationManager, provider: String): Location? { + return try { + withTimeoutOrNull(LOCATION_TIMEOUT_MS) { + suspendCancellableCoroutine { cont -> + val signal = CancellationSignal() + locationManager.getCurrentLocation( + provider, + signal, + applicationContext.mainExecutor + ) { location -> + if (cont.isActive) cont.resume(location) + } + cont.invokeOnCancellation { signal.cancel() } + } + } + } catch (e: SecurityException) { + Log.e(TAG, "SecurityException requesting location via LocationManager", e) + null + } + } +} diff --git a/app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt b/app/src/standard/java/no/naiv/tilfluktsrom/location/LocationProvider.kt similarity index 100% rename from app/src/main/java/no/naiv/tilfluktsrom/location/LocationProvider.kt rename to app/src/standard/java/no/naiv/tilfluktsrom/location/LocationProvider.kt diff --git a/app/src/main/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt b/app/src/standard/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt similarity index 100% rename from app/src/main/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt rename to app/src/standard/java/no/naiv/tilfluktsrom/widget/ShelterWidgetProvider.kt diff --git a/app/src/main/java/no/naiv/tilfluktsrom/widget/WidgetUpdateWorker.kt b/app/src/standard/java/no/naiv/tilfluktsrom/widget/WidgetUpdateWorker.kt similarity index 100% rename from app/src/main/java/no/naiv/tilfluktsrom/widget/WidgetUpdateWorker.kt rename to app/src/standard/java/no/naiv/tilfluktsrom/widget/WidgetUpdateWorker.kt From 6903fb7e7081659d7631ea394ed5aa32bb88310e Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Tue, 10 Mar 2026 20:29:53 +0100 Subject: [PATCH 05/25] Flytt signeringskonfigurasjon til gitignorert keystore.properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Passord og keystoresti var hardkodet i build.gradle.kts og synlig i det offentlige repoet. Flyttet til keystore.properties som er gitignorert. Bygget hopper over signering om filen mangler, slik at F-Droid kan bygge og signere med sin egen nøkkel. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + app/build.gradle.kts | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index e3bdbed..f827122 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ .cxx local.properties /app/build +keystore.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6270317..9ae75bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,12 +30,15 @@ android { signingConfigs { create("release") { - val keystorePath = System.getProperty("user.home") + "/.android/tilfluktsrom-release.jks" - if (file(keystorePath).exists()) { - storeFile = file(keystorePath) - storePassword = "tilfluktsrom" - keyAlias = "tilfluktsrom" - keyPassword = "tilfluktsrom" + val keystorePropsFile = rootProject.file("keystore.properties") + if (keystorePropsFile.exists()) { + val keystoreProps = Properties().apply { + keystorePropsFile.inputStream().use { load(it) } + } + storeFile = file(keystoreProps.getProperty("storeFile")) + storePassword = keystoreProps.getProperty("storePassword") + keyAlias = keystoreProps.getProperty("keyAlias") + keyPassword = keystoreProps.getProperty("keyPassword") } } } From 9925e7ec6466d535c3ad7a3cb5c7643e03a2f38c Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Tue, 10 Mar 2026 20:35:33 +0100 Subject: [PATCH 06/25] Bump versjon til v1.6.0 (versionCode 9) Co-Authored-By: Claude Opus 4.6 --- fastlane/metadata/android/en-US/changelogs/9.txt | 2 ++ fastlane/metadata/android/nb-NO/changelogs/9.txt | 2 ++ fastlane/metadata/android/nn-NO/changelogs/9.txt | 2 ++ version.properties | 4 ++-- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/9.txt create mode 100644 fastlane/metadata/android/nb-NO/changelogs/9.txt create mode 100644 fastlane/metadata/android/nn-NO/changelogs/9.txt diff --git a/fastlane/metadata/android/en-US/changelogs/9.txt b/fastlane/metadata/android/en-US/changelogs/9.txt new file mode 100644 index 0000000..5c85529 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/9.txt @@ -0,0 +1,2 @@ +- Add F-Droid build flavor without Google Play Services +- Move signing config out of source code diff --git a/fastlane/metadata/android/nb-NO/changelogs/9.txt b/fastlane/metadata/android/nb-NO/changelogs/9.txt new file mode 100644 index 0000000..49d8038 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/9.txt @@ -0,0 +1,2 @@ +- Legg til F-Droid-byggvariant uten Google Play Services +- Flytt signeringskonfigurasjon ut av kildekoden diff --git a/fastlane/metadata/android/nn-NO/changelogs/9.txt b/fastlane/metadata/android/nn-NO/changelogs/9.txt new file mode 100644 index 0000000..b71da28 --- /dev/null +++ b/fastlane/metadata/android/nn-NO/changelogs/9.txt @@ -0,0 +1,2 @@ +- Legg til F-Droid-byggvariant utan Google Play Services +- Flytt signeringskonfigurasjon ut av kjeldekoden diff --git a/version.properties b/version.properties index e9b517b..ff0fb89 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ versionMajor=1 -versionMinor=5 +versionMinor=6 versionPatch=0 -versionCode=8 +versionCode=9 From 9a00f07362d8a619714eba42a206ce09f8042960 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Tue, 10 Mar 2026 20:48:22 +0100 Subject: [PATCH 07/25] Fiks F-Droid-metadata: fjern punktum i kort beskrivelse, legg til ikon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F-Droid krever at short_description.txt ikke slutter med punktum. Erstatt to setninger med én sammenhengende frase med tankestrek. Legg til 512x512 icon.png generert fra appens adaptive ikon-vektorer. Co-Authored-By: Claude Opus 4.6 --- fastlane/metadata/android/en-US/images/icon.png | Bin 0 -> 5451 bytes .../metadata/android/en-US/short_description.txt | 2 +- fastlane/metadata/android/nb-NO/images/icon.png | Bin 0 -> 5451 bytes .../metadata/android/nb-NO/short_description.txt | 2 +- fastlane/metadata/android/nn-NO/images/icon.png | Bin 0 -> 5451 bytes .../metadata/android/nn-NO/short_description.txt | 2 +- 6 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/en-US/images/icon.png create mode 100644 fastlane/metadata/android/nb-NO/images/icon.png create mode 100644 fastlane/metadata/android/nn-NO/images/icon.png diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f156be8527ecaabe24ea721b4ab5ca9d8829bde6 GIT binary patch literal 5451 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%0D_s);U8jb6&0PHIclmp-$0 ze$WK(I5_sL|49NjF!5r%TEz;vhp3|1rlnjZsL7>;q~QXw5p(4-=jYQJ476I zP1i!VcQZ+UR7J`=KquyC!!#~)cywy8yRiRUA5F-8qO57L%byJ>6uVWAsoK}h@bPyC zjNA%Y&YM{h+PHNV%6f-Y?U`{&brjh68>=_k+u>_3H9Gd%V}aaF>?uiM4f9$TGpwTy zB5w%Y8)~goM{ir~ihYl4_ZXwof=2IAxQkBaj`UbleXp%4muZ)YfcD_1LrJ{&3^3%!(s1nK1- z#h)*j*Sg%zrRWisyL3{O1T~w9>=TK zZem8pYW8fs9H6ifLgiHu#P{Z8$h`get9ze5W|VE~I=L|8l5L7S!h&hd!gYIgDn+zJzIeiS~bp3=R<$~#01?|@Zzfc)<9fbLnxfRhyVQE}Ay-s3r zQ=tw)Jbc9DP0`QW!d~R$bm6VMdR?<{m#~$beA`g;&bzz7^vP=@PGK~D+{(T(%V+Ky zH}+pI7NsX@YXY`0H`+4u6#tO6wP&1s^s`E4e)HG2TGxjy^*>8ig@tgFW6OHN=Vp!& z=RNr(@gsv?$xo6qhH^JJJ5(O~PrYDTNu}aAMwfF!;24|RNp8bVK5d=f_*@k;sWd5 zd*~jsF-$vo6C3M!@?4R_P;Ef+QaCjpbkjrl=bm)U?>^>b&IG;ehC(EjLBUtkGG)#} z@7@f(t&RtpLp8yU zG;~vp?#i@Mi;aH0!|YsL6QI*F(QJUv&f7MmO^j~iFc`X$vKFQVr5-!ImEXvoOAQI+ za9=UtT$B?ZzI!)AySjRV(X|+CyF9i>X%QK(EBZC^^jqh6;mUJOSe8~sxNS&7S;%Cd z+p_(bZ267cn)h|TR+)p5(N80kEp~lV?E0v)IHDOuR0XCu2k-dLo4@wS0eFk;@*QY8Thd73r zb@_E_nXy&ICQc69jaBB;TWhcYjzK4<4Fokl&@8IXTFHArcV)ay?A0~XQoO2FTvy|Y z4ZTNGi`E6m`Phl|RrV0`E0d#+vB5S1rw^)LjNoZ?pdj+!8rc6?32;~T%`bQHkdltO RP<0eszGR|X_@~p|e*^aV?wtSt literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index 1c45bf5..ae7bd40 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -Find the nearest public emergency shelter in Norway. Works offline. +Find the nearest public emergency shelter in Norway — works offline diff --git a/fastlane/metadata/android/nb-NO/images/icon.png b/fastlane/metadata/android/nb-NO/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f156be8527ecaabe24ea721b4ab5ca9d8829bde6 GIT binary patch literal 5451 zcmeHL`8(9z`#&@0qmiRvWvH(U_7A=+dQJiWC%|Rhi)H4 zGZ^M^qd(*gU62YX=rX)!Um3%LZX~-g;kN@*G$y^J%?4&}XDqMnz^W$mYG-$AW;NS9 z(YEEPFfJ^1fCXgn!vR+!00=PvF0uiP>3_xlci!+xA0JYs6`3(4vgGOPf1nFOj>t`R zE*jcsRc)-*jS(1Ypv9E`@cih05L;Nx<#v zBuw*G9#`@3I9he?6o0!43>9UqQ~^ zJ7;1D511#5TI^fOV;tB3qi5wnH7guJR44WQh9(GNO~+`zcG3}3PB^CUT*_9?bC|C< z_@1-+o7EAHF`>H8XI^1#jt9>k7N#GJuZjSF8k&4yz#a+4rD(sYw%NeA#KLd*5`u4) zf14#71Jp~>zZc^r3M`A5zk4^hK;H4+v_Dwk7^RQDH#2Yy%)ETJFX>FoLxQ!Y{s_2y z{SFV9x%Vz2)tR_?2DD3C?4z5(nhqdIk@qalu(T_|vet}t1tBB|%StfX_0we~vBNR0 zq^;S=Bi+~F_3z{vT?%|z76{_70>j3DRW$%!??Kw4-9O@e4na&g&mf%Ua|%Qd=j0g# zH9jXYf|#JdaD`a3U|)2Nc2l0i$T~n(*&^a-GY~%uE+oCHb8#k4af1x!`J(fDU&Psf zWXe{zs~?O(w3Se-y=OR z-__q0+l)B{H+3b4yBM>Ia1le~8GYRHrg*ri3OQWegmwN3m~ZIsN_!r;#Z6R~V<_OA zvrG^uJURT_X%+?wyq9NKsPQ$YgYQlKSey9BJ8T#eqg{V#KIl(5PjP1UZ4b%LYb>wl zHNlz)-txPBQxsV*Pa91x^4pwwn4AMXJTPuJQ-&}xH`mdnobTLFcNirB1IQ}?+&u<} zSqSi-5a;tlK(`xj9m*lhb5@(Vg)`r$c1EBr^+9fWZC|e%Z+lDApY+dg_nVb$=FO;>OJmOy(#>($`*3c5>+9}Z zLs7mj=#Q61{ZAc=61X&+8-n!qtB9Z>hM0T{{hiZ)+4bUo88_xFZTU zXJ50_gp*iZ!GW483yNJ@D(Maqqiow*&=P4e5DUSyYYG2y>_0%Cqzjrq+_xAwP|!=Y ziUZ^upLvyp;m@ALTMvj5-A+~vZVu>@6uZj zi1aks^f>eut!A=|4i3}9v-v_F)*T{@=QSv$b+tcA{WlM3?3e8CwkQ_AUlZ0arg)eD zPV8ZfG6E&A-L$4wxHV*@c~?!YSk2SHj)9Ud#@Ebq%XwB3OG_d|G6C1;FS-MwP9u=E zPKV;$4JYbo(5MnI4T&*fO-FQ(GjElfMT75RIc?y+^ONViu;vygJry|jb9NBSO-ws- ziV}kG{*&cqC-4;kYQw?(R!-EB&TRNI9$53`y6ne4D{uDOD2l_MCG7R&=>zd{zXHfS z4-(1Q&1Z(msPvW4ONlQTr@^8aK3;gp13(cnWfP7i--{?rhP@)D zyitibhQzp>jp<#h_kXpdZ&+s&F9>VyqKZCMpmtJ42gNqIi8}?Fiw4;X1r;(a&%pOt zZbNBmSE^3!i{C?9x8GeNNT!o!E`wD+Z~3M5HaY(U!R+Z*mtoCuz}cI!fKcU$$FCf_Q z3fjD+SX+D=8>a9D=a#1&_2cSAf#>g!5$!3eP92wQu6~WNtVEz}q&SaDHwjwdlIFHE zjM`s-@9yPEgCXJwE4#2<)segqE~w9Wae{7r-O{5)$nj$=8|G8(!sl%Fjh|~`?Ihsr z(ed>o`_lBFKp1!(zPYyBMZ33L#&Y=p8Az7&y0ZLQ!tH?DRy!| zqSb#=3oANf3|5s@&B7;fS93qcurx+S=NI@E`s?l*7~ArxkhtO$7_89P#~cAX1V>6$|gw`nxYYBt%+2>2;9Hr zk5ZaR;*O(V+pKxuraY5lRz_5=`mvW1SL~ivoCZohL8UdWRdBi4pC=`v#<=Ww;ikeS z?J0*GM{lH?2}6P*^ThTihr=!$OM+O=Aej9VBqpppW}Oj!NZjv-)aa-uE|#C0X#3vB zux+K~lw0x~*)x2<${LYn9VohvWEP-^L^gtETf6&z=B=!zTI#?}byPe(Ice7Vvqx0uxHlPytf(lvIWmRA zMt$a!^o!$+Gk~$a{Bp!tpwj=NbNt@Tvbj1)DT5iCEOaJ~dA4a1cLlgF_zBoP zXbVhB4NNJVbl!BdgPYpD)NiHL2q#+j&Hnr=pBA%V2<8_pu&QiM%ozFIHgy6|e+uXy zroLL`5Qnu7Rs~iFYk%9eaD|)t?f)VTHJle%l6P?3bR<9>5S85+$KUPaHQ98x&m*s9 z5LtU#^uhsOlmZV$es>|HZGVf0xHYP58$LW5Ns5;B^u!BSxaVrakY6)1|B=w{Q|dPw z()zk^>@K)32OXn8x=yOwdoPEDFU|w^oUS7=$A|Flct7*577ta{byx68cr(W9nhky1_2@-=| zh-wv3R&Q2|>hkoU2$!lqx&%IMuB%rCtyzxhwRHupN(sS`t_A5@-;FYbHzEfpK5G#d z;9Nb=>5Wx^)WN2>v6}4ah|FajT{u_2{!k@{@d#2xkYpenQGSgfksAdN75JJvlbjxi zc&%b;a6NWUPYn2spEn&V?W@J#R@tb+3lwkiB!l}*h&=MVz{QJ8e6rf5pAMrGVMx#1 zl)~W82{*WtBzFfID5A#uJKYErb$jE4nRT0D~ezznWbo$u* zt5+91g{6z^C-?(K#hb)IT61Kyf1|ld)XZ$XZpdiw%%0D_s);U8jb6&0PHIclmp-$0 ze$WK(I5_sL|49NjF!5r%TEz;vhp3|1rlnjZsL7>;q~QXw5p(4-=jYQJ476I zP1i!VcQZ+UR7J`=KquyC!!#~)cywy8yRiRUA5F-8qO57L%byJ>6uVWAsoK}h@bPyC zjNA%Y&YM{h+PHNV%6f-Y?U`{&brjh68>=_k+u>_3H9Gd%V}aaF>?uiM4f9$TGpwTy zB5w%Y8)~goM{ir~ihYl4_ZXwof=2IAxQkBaj`UbleXp%4muZ)YfcD_1LrJ{&3^3%!(s1nK1- z#h)*j*Sg%zrRWisyL3{O1T~w9>=TK zZem8pYW8fs9H6ifLgiHu#P{Z8$h`get9ze5W|VE~I=L|8l5L7S!h&hd!gYIgDn+zJzIeiS~bp3=R<$~#01?|@Zzfc)<9fbLnxfRhyVQE}Ay-s3r zQ=tw)Jbc9DP0`QW!d~R$bm6VMdR?<{m#~$beA`g;&bzz7^vP=@PGK~D+{(T(%V+Ky zH}+pI7NsX@YXY`0H`+4u6#tO6wP&1s^s`E4e)HG2TGxjy^*>8ig@tgFW6OHN=Vp!& z=RNr(@gsv?$xo6qhH^JJJ5(O~PrYDTNu}aAMwfF!;24|RNp8bVK5d=f_*@k;sWd5 zd*~jsF-$vo6C3M!@?4R_P;Ef+QaCjpbkjrl=bm)U?>^>b&IG;ehC(EjLBUtkGG)#} z@7@f(t&RtpLp8yU zG;~vp?#i@Mi;aH0!|YsL6QI*F(QJUv&f7MmO^j~iFc`X$vKFQVr5-!ImEXvoOAQI+ za9=UtT$B?ZzI!)AySjRV(X|+CyF9i>X%QK(EBZC^^jqh6;mUJOSe8~sxNS&7S;%Cd z+p_(bZ267cn)h|TR+)p5(N80kEp~lV?E0v)IHDOuR0XCu2k-dLo4@wS0eFk;@*QY8Thd73r zb@_E_nXy&ICQc69jaBB;TWhcYjzK4<4Fokl&@8IXTFHArcV)ay?A0~XQoO2FTvy|Y z4ZTNGi`E6m`Phl|RrV0`E0d#+vB5S1rw^)LjNoZ?pdj+!8rc6?32;~T%`bQHkdltO RP<0eszGR|X_@~p|e*^aV?wtSt literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/nb-NO/short_description.txt b/fastlane/metadata/android/nb-NO/short_description.txt index d376c35..09ec3b6 100644 --- a/fastlane/metadata/android/nb-NO/short_description.txt +++ b/fastlane/metadata/android/nb-NO/short_description.txt @@ -1 +1 @@ -Finn nærmeste offentlige tilfluktsrom i Norge. Fungerer uten nett. +Finn nærmeste offentlige tilfluktsrom i Norge — fungerer uten nett diff --git a/fastlane/metadata/android/nn-NO/images/icon.png b/fastlane/metadata/android/nn-NO/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f156be8527ecaabe24ea721b4ab5ca9d8829bde6 GIT binary patch literal 5451 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%0D_s);U8jb6&0PHIclmp-$0 ze$WK(I5_sL|49NjF!5r%TEz;vhp3|1rlnjZsL7>;q~QXw5p(4-=jYQJ476I zP1i!VcQZ+UR7J`=KquyC!!#~)cywy8yRiRUA5F-8qO57L%byJ>6uVWAsoK}h@bPyC zjNA%Y&YM{h+PHNV%6f-Y?U`{&brjh68>=_k+u>_3H9Gd%V}aaF>?uiM4f9$TGpwTy zB5w%Y8)~goM{ir~ihYl4_ZXwof=2IAxQkBaj`UbleXp%4muZ)YfcD_1LrJ{&3^3%!(s1nK1- z#h)*j*Sg%zrRWisyL3{O1T~w9>=TK zZem8pYW8fs9H6ifLgiHu#P{Z8$h`get9ze5W|VE~I=L|8l5L7S!h&hd!gYIgDn+zJzIeiS~bp3=R<$~#01?|@Zzfc)<9fbLnxfRhyVQE}Ay-s3r zQ=tw)Jbc9DP0`QW!d~R$bm6VMdR?<{m#~$beA`g;&bzz7^vP=@PGK~D+{(T(%V+Ky zH}+pI7NsX@YXY`0H`+4u6#tO6wP&1s^s`E4e)HG2TGxjy^*>8ig@tgFW6OHN=Vp!& z=RNr(@gsv?$xo6qhH^JJJ5(O~PrYDTNu}aAMwfF!;24|RNp8bVK5d=f_*@k;sWd5 zd*~jsF-$vo6C3M!@?4R_P;Ef+QaCjpbkjrl=bm)U?>^>b&IG;ehC(EjLBUtkGG)#} z@7@f(t&RtpLp8yU zG;~vp?#i@Mi;aH0!|YsL6QI*F(QJUv&f7MmO^j~iFc`X$vKFQVr5-!ImEXvoOAQI+ za9=UtT$B?ZzI!)AySjRV(X|+CyF9i>X%QK(EBZC^^jqh6;mUJOSe8~sxNS&7S;%Cd z+p_(bZ267cn)h|TR+)p5(N80kEp~lV?E0v)IHDOuR0XCu2k-dLo4@wS0eFk;@*QY8Thd73r zb@_E_nXy&ICQc69jaBB;TWhcYjzK4<4Fokl&@8IXTFHArcV)ay?A0~XQoO2FTvy|Y z4ZTNGi`E6m`Phl|RrV0`E0d#+vB5S1rw^)LjNoZ?pdj+!8rc6?32;~T%`bQHkdltO RP<0eszGR|X_@~p|e*^aV?wtSt literal 0 HcmV?d00001 diff --git a/fastlane/metadata/android/nn-NO/short_description.txt b/fastlane/metadata/android/nn-NO/short_description.txt index 462d4b2..e567414 100644 --- a/fastlane/metadata/android/nn-NO/short_description.txt +++ b/fastlane/metadata/android/nn-NO/short_description.txt @@ -1 +1 @@ -Finn næraste offentlege tilfluktsrom i Noreg. Fungerer utan nett. +Finn næraste offentlege tilfluktsrom i Noreg — fungerer utan nett From 8e684f868e93671d7254218c28d467461762512a Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Tue, 10 Mar 2026 21:16:05 +0100 Subject: [PATCH 08/25] Flytt versjon direkte inn i build.gradle.kts for F-Droid-kompatibilitet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fdroidserver parser build.gradle.kts med regex for å finne versionCode og versionName. Den dynamiske Properties-lastingen fra version.properties ga "Couldn't find any version information". Erstatt med literale verdier som fdroidserver kan lese. Co-Authored-By: Claude Opus 4.6 --- app/build.gradle.kts | 14 ++------------ version.properties | 4 ---- 2 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 version.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ae75bc..3b0c67c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,11 +6,6 @@ plugins { id("com.google.devtools.ksp") } -// Read version from shared version.properties -val versionProps = Properties().apply { - rootProject.file("version.properties").inputStream().use { load(it) } -} - android { namespace = "no.naiv.tilfluktsrom" compileSdk = 35 @@ -19,13 +14,8 @@ android { applicationId = "no.naiv.tilfluktsrom" minSdk = 26 targetSdk = 35 - versionCode = versionProps.getProperty("versionCode").toInt() - versionName = "${versionProps.getProperty("versionMajor")}." + - "${versionProps.getProperty("versionMinor")}." + - versionProps.getProperty("versionPatch") - - // Make version available in BuildConfig - buildConfigField("String", "VERSION_DISPLAY", "\"$versionName\"") + versionCode = 9 + versionName = "1.6.0" } signingConfigs { diff --git a/version.properties b/version.properties deleted file mode 100644 index ff0fb89..0000000 --- a/version.properties +++ /dev/null @@ -1,4 +0,0 @@ -versionMajor=1 -versionMinor=6 -versionPatch=0 -versionCode=9 From f5c064ab92cdd694607df4c4afd96928e6da3780 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Sat, 14 Mar 2026 13:15:14 +0100 Subject: [PATCH 09/25] Bump versjon til v1.6.1 (versionCode 10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fjern dependency metadata frå APK-signeringsblokka (dependenciesInfo) for F-Droid-kompatibilitet Co-Authored-By: Claude Opus 4.6 (1M context) --- app/build.gradle.kts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3b0c67c..c04a5f9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,13 @@ android { applicationId = "no.naiv.tilfluktsrom" minSdk = 26 targetSdk = 35 - versionCode = 9 - versionName = "1.6.0" + versionCode = 10 + versionName = "1.6.1" + } + + dependenciesInfo { + includeInApk = false + includeInBundle = false } signingConfigs { From f9f8ac3d60e361bed9f2d560adcce72235206580 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Sat, 14 Mar 2026 18:30:36 +0100 Subject: [PATCH 10/25] Dokumenter byggvariantar og distribusjon i CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 261261a..ec7e9a5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,15 @@ no.naiv.tilfluktsrom/ ./gradlew assembleDebug ``` +## Build Variants +- **standard**: Includes Google Play Services for better GPS accuracy +- **fdroid**: AOSP-only, no Google dependencies + +## Distribution +- **Forgejo** (primary): `kode.naiv.no/olemd/tilfluktsrom` — releases with both APK variants +- **GitHub** (mirror): `github.com/olemd/tilfluktsrom` — automatically mirrored from Forgejo, do not push manually +- **F-Droid**: Metadata maintained in a separate fdroiddata repo (GitLab fork). F-Droid builds from source using the `fdroid` variant and signs with the F-Droid key. + ## i18n - Default (English): `res/values/strings.xml` - Norwegian Bokmål: `res/values-nb/strings.xml` From 6ba35add2f2352fd148bfe23a2a5ef2a9bb7bdec Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 23 Mar 2026 14:02:32 +0100 Subject: [PATCH 11/25] =?UTF-8?q?Legg=20til=20TalkBack-st=C3=B8tte=20og=20?= =?UTF-8?q?nordindikator=20p=C3=A5=20kompasset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tilgjengelegheit (Android + PWA): - Semantiske landemerke (header, main, aside, role=dialog) - aria-live-regionar for statusoppdateringar og lasteoverlegg - Fokusindikatorar (:focus-visible) og prefers-reduced-motion - Auka trykkmål til 48dp (infoknapp, oppdater, del, widget) - contentDescription på kart, kompass og framdriftsindikator - aria-current og role=listitem på tilfluktsromliste - Fokusfangst og fokusgjenoppretting i lasteoverlegg - Ikkje-farge-indikator (▶) for valt tilfluktsrom - Dynamisk lang-attributt basert på oppdaga språk - Lokaliserte aria-label (en/nb/nn) Nordindikator: - DirectionArrowView teiknar diskret «N»-markør på omkrinsen - Roterer uavhengig av hovudpila for kompasskalibrering - Berre på stor kompassvisning, ikkje minipila Co-Authored-By: Claude Opus 4.6 (1M context) --- .../java/no/naiv/tilfluktsrom/MainActivity.kt | 4 +- .../tilfluktsrom/ui/DirectionArrowView.kt | 58 +++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 18 ++++-- .../res/layout/widget_nearest_shelter.xml | 5 +- app/src/main/res/values-nb/strings.xml | 2 + app/src/main/res/values-nn/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + pwa/index.html | 34 +++++------ pwa/src/app.ts | 13 +++++ pwa/src/i18n/en.ts | 4 ++ pwa/src/i18n/i18n.ts | 7 ++- pwa/src/i18n/nb.ts | 4 ++ pwa/src/i18n/nn.ts | 4 ++ pwa/src/styles/main.css | 27 +++++++++ pwa/src/ui/compass-view.ts | 56 ++++++++++++++++++ pwa/src/ui/loading-overlay.ts | 17 +++++- pwa/src/ui/shelter-list.ts | 19 ++++-- 17 files changed, 240 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt b/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt index 2f11f27..e1fd8e0 100644 --- a/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt +++ b/app/src/main/java/no/naiv/tilfluktsrom/MainActivity.kt @@ -522,10 +522,11 @@ class MainActivity : AppCompatActivity(), SensorEventListener { R.string.direction_arrow_description, distanceText ) - // Update compass view + // Update compass view (large arrow gets a north indicator) binding.compassDistanceText.text = distanceText binding.compassAddressText.text = selected.shelter.adresse binding.directionArrow.setDirection(arrowAngle) + binding.directionArrow.setNorthAngle(-deviceHeading) binding.directionArrow.contentDescription = getString( R.string.direction_arrow_description, distanceText ) @@ -840,6 +841,7 @@ class MainActivity : AppCompatActivity(), SensorEventListener { val arrowAngle = bearing - deviceHeading binding.directionArrow.setDirection(arrowAngle) + binding.directionArrow.setNorthAngle(-deviceHeading) binding.miniArrow.setDirection(arrowAngle) } diff --git a/app/src/main/java/no/naiv/tilfluktsrom/ui/DirectionArrowView.kt b/app/src/main/java/no/naiv/tilfluktsrom/ui/DirectionArrowView.kt index edd3154..aa50e8f 100644 --- a/app/src/main/java/no/naiv/tilfluktsrom/ui/DirectionArrowView.kt +++ b/app/src/main/java/no/naiv/tilfluktsrom/ui/DirectionArrowView.kt @@ -17,6 +17,9 @@ import no.naiv.tilfluktsrom.R * rotationAngle = shelterBearing - deviceHeading * This gives the direction the user needs to walk, adjusted for which * way they're currently facing. + * + * Optionally draws a discrete north indicator on the perimeter so users + * can validate compass calibration against a known direction. */ class DirectionArrowView @JvmOverloads constructor( context: Context, @@ -25,6 +28,7 @@ class DirectionArrowView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr) { private var rotationAngle = 0f + private var northAngle = Float.NaN private val arrowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = context.getColor(R.color.shelter_primary) @@ -37,7 +41,18 @@ class DirectionArrowView @JvmOverloads constructor( strokeWidth = 4f } + private val northPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = 0x99CFD8DC.toInt() // text_secondary at ~60% opacity + style = Paint.Style.FILL + } + + private val northTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = 0x99CFD8DC.toInt() + textAlign = Paint.Align.CENTER + } + private val arrowPath = Path() + private val northPath = Path() /** * Set the rotation angle in degrees. @@ -48,6 +63,16 @@ class DirectionArrowView @JvmOverloads constructor( invalidate() } + /** + * Set the angle to north in the view's coordinate space. + * This is typically -deviceHeading (where north is on screen). + * Set to Float.NaN to hide the north indicator. + */ + fun setNorthAngle(degrees: Float) { + northAngle = degrees + invalidate() + } + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) @@ -55,6 +80,11 @@ class DirectionArrowView @JvmOverloads constructor( val cy = height / 2f val size = minOf(width, height) * 0.4f + // Draw north indicator first (behind the main arrow) + if (!northAngle.isNaN()) { + drawNorthIndicator(canvas, cx, cy, size) + } + canvas.save() canvas.rotate(rotationAngle, cx, cy) @@ -74,4 +104,32 @@ class DirectionArrowView @JvmOverloads constructor( canvas.restore() } + + /** + * Draw a small north indicator: a tiny triangle and "N" label + * placed on the perimeter of the view, pointing inward toward center. + */ + private fun drawNorthIndicator(canvas: Canvas, cx: Float, cy: Float, arrowSize: Float) { + val radius = arrowSize * 1.35f + val tickSize = arrowSize * 0.1f + + // Scale "N" text relative to the view + northTextPaint.textSize = arrowSize * 0.18f + + canvas.save() + canvas.rotate(northAngle, cx, cy) + + // Small triangle at the top of the perimeter circle + northPath.reset() + northPath.moveTo(cx, cy - radius) + northPath.lineTo(cx - tickSize, cy - radius - tickSize * 1.8f) + northPath.lineTo(cx + tickSize, cy - radius - tickSize * 1.8f) + northPath.close() + canvas.drawPath(northPath, northPaint) + + // "N" label just outside the triangle + canvas.drawText("N", cx, cy - radius - tickSize * 2.2f, northTextPaint) + + canvas.restore() + } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d5c3c30..4c8701e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -31,14 +31,15 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:accessibilityLiveRegion="polite" android:textColor="@color/status_text" android:textSize="12sp" tools:text="@string/status_ready" /> @@ -80,6 +82,7 @@ android:layout_width="match_parent" android:layout_height="0dp" android:background="@color/compass_bg" + android:contentDescription="@string/a11y_compass" android:visibility="gone" app:layout_constraintTop_toBottomOf="@id/statusBar" app:layout_constraintBottom_toTopOf="@id/bottomSheet"> @@ -223,8 +226,8 @@ diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index bab61b6..3dc77cf 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -67,6 +67,8 @@ Retning til tilfluktsrom, %s unna %1$s, %2$s, %3$d plasser Upresist kompass - %s + Tilfluktsromkart + Kompassnavigasjon Sivilforsvarsinformasjon diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index a4de381..129289d 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -67,6 +67,8 @@ Retning til tilfluktsrom, %s unna %1$s, %2$s, %3$d plassar Upresis kompass - %s + Tilfluktsromkart + Kompassnavigasjon Sivilforsvarsinformasjon diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 43b3945..c742ab4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,6 +67,8 @@ Direction to shelter, %s away %1$s, %2$s, %3$d places Low accuracy - %s + Shelter map + Compass navigation Civil defense information diff --git a/pwa/index.html b/pwa/index.html index bfbd7e6..c6088c1 100644 --- a/pwa/index.html +++ b/pwa/index.html @@ -1,5 +1,5 @@ - + @@ -16,47 +16,47 @@
-
- +
+ -
-
-
+
+
+ -
+ -
+ -
-
+
+
+
-
-
-
+