Legg til automatisk widgetoppdatering og de-Google-retningslinjer (v1.3.1)
Widget oppdaterer seg sjølv via WorkManager kvar 15. min i bakgrunnen, og i sanntid når appen er open og mottek GPS-oppdateringar. Oppdaterer CLAUDE.md med de-Google-kompatibilitetsprinsipp. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a813edc0c3
commit
3c49dbdcde
6 changed files with 95 additions and 5 deletions
18
CLAUDE.md
18
CLAUDE.md
|
|
@ -3,13 +3,28 @@
|
||||||
## Project Overview
|
## Project Overview
|
||||||
Android app (Kotlin) that helps find the nearest public shelter (tilfluktsrom) in Norway during emergencies. Offline-first design: must work without internet after initial data cache.
|
Android app (Kotlin) that helps find the nearest public shelter (tilfluktsrom) in Norway during emergencies. Offline-first design: must work without internet after initial data cache.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
### De-Google Compatibility
|
||||||
|
The app must work on devices without Google Play Services (e.g. LineageOS, GrapheneOS, /e/OS). Every feature that uses a Google-specific API must have a fallback that works without it. Use Google Play Services when available for better accuracy/performance, but never as a hard dependency.
|
||||||
|
|
||||||
|
**Pattern**: Check for Play Services at runtime, fall back to AOSP/standard APIs.
|
||||||
|
- **Location**: Prefer FusedLocationProviderClient (Play Services) → fall back to LocationManager (AOSP)
|
||||||
|
- **Maps**: OSMDroid (no Google dependency)
|
||||||
|
- **Database**: Room/SQLite (no Google dependency)
|
||||||
|
- **Background work**: WorkManager (works without Play Services via built-in scheduler)
|
||||||
|
|
||||||
|
### Offline-First
|
||||||
|
This is an emergency app. Assume internet and infrastructure may be degraded or unavailable. All core functionality (finding nearest shelter, compass navigation, sharing location) must work offline after initial data cache. Avoid solutions that depend on external servers being reachable.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
- **Language**: Kotlin, targeting Android API 26+ (Android 8.0+)
|
- **Language**: Kotlin, targeting Android API 26+ (Android 8.0+)
|
||||||
- **Build**: Gradle 8.7, AGP 8.5.2, KSP for Room annotation processing
|
- **Build**: Gradle 8.7, AGP 8.5.2, KSP for Room annotation processing
|
||||||
- **Maps**: OSMDroid (offline-capable OpenStreetMap)
|
- **Maps**: OSMDroid (offline-capable OpenStreetMap)
|
||||||
- **Database**: Room (SQLite) for shelter data cache
|
- **Database**: Room (SQLite) for shelter data cache
|
||||||
- **HTTP**: OkHttp for data downloads
|
- **HTTP**: OkHttp for data downloads
|
||||||
- **Location**: Google Play Services Fused Location Provider
|
- **Location**: FusedLocationProviderClient (Play Services) with LocationManager fallback
|
||||||
|
- **Background**: WorkManager for periodic widget updates
|
||||||
- **UI**: Traditional Views with ViewBinding
|
- **UI**: Traditional Views with ViewBinding
|
||||||
|
|
||||||
## Key Data Flow
|
## Key Data Flow
|
||||||
|
|
@ -29,6 +44,7 @@ no.naiv.tilfluktsrom/
|
||||||
├── data/ # Room entities, DAO, repository, GeoJSON parser, map cache
|
├── data/ # Room entities, DAO, repository, GeoJSON parser, map cache
|
||||||
├── location/ # GPS location provider, nearest shelter finder
|
├── location/ # GPS location provider, nearest shelter finder
|
||||||
├── ui/ # Custom views (DirectionArrowView), adapters
|
├── ui/ # Custom views (DirectionArrowView), adapters
|
||||||
|
├── widget/ # Home screen widget, WorkManager periodic updater
|
||||||
└── util/ # Coordinate conversion (UTM→WGS84), distance calculations
|
└── util/ # Coordinate conversion (UTM→WGS84), distance calculations
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,9 @@ dependencies {
|
||||||
// Google Play Services Location (precise GPS)
|
// Google Play Services Location (precise GPS)
|
||||||
implementation("com.google.android.gms:play-services-location:21.3.0")
|
implementation("com.google.android.gms:play-services-location:21.3.0")
|
||||||
|
|
||||||
|
// WorkManager (periodic widget updates)
|
||||||
|
implementation("androidx.work:work-runtime-ktx:2.9.1")
|
||||||
|
|
||||||
// Coroutines
|
// Coroutines
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import no.naiv.tilfluktsrom.location.ShelterFinder
|
||||||
import no.naiv.tilfluktsrom.location.ShelterWithDistance
|
import no.naiv.tilfluktsrom.location.ShelterWithDistance
|
||||||
import no.naiv.tilfluktsrom.ui.ShelterListAdapter
|
import no.naiv.tilfluktsrom.ui.ShelterListAdapter
|
||||||
import no.naiv.tilfluktsrom.util.DistanceUtils
|
import no.naiv.tilfluktsrom.util.DistanceUtils
|
||||||
|
import no.naiv.tilfluktsrom.widget.ShelterWidgetProvider
|
||||||
import org.osmdroid.util.GeoPoint
|
import org.osmdroid.util.GeoPoint
|
||||||
import org.osmdroid.views.CustomZoomButtonsController
|
import org.osmdroid.views.CustomZoomButtonsController
|
||||||
import org.osmdroid.views.overlay.Marker
|
import org.osmdroid.views.overlay.Marker
|
||||||
|
|
@ -414,6 +415,7 @@ class MainActivity : AppCompatActivity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedShelterUI()
|
updateSelectedShelterUI()
|
||||||
|
ShelterWidgetProvider.requestUpdate(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,11 @@ import no.naiv.tilfluktsrom.util.DistanceUtils
|
||||||
/**
|
/**
|
||||||
* Home screen widget showing the nearest shelter with distance.
|
* Home screen widget showing the nearest shelter with distance.
|
||||||
*
|
*
|
||||||
* Update strategy: no automatic periodic updates (updatePeriodMillis=0).
|
* Update strategy:
|
||||||
* Updates only when the user taps the refresh button, which sends ACTION_REFRESH.
|
* - Background: WorkManager runs every 15 min while widget exists
|
||||||
|
* - Live: MainActivity sends ACTION_REFRESH on each GPS location update
|
||||||
|
* - Manual: user taps the refresh button on the widget
|
||||||
|
*
|
||||||
* Tapping the widget body opens MainActivity.
|
* Tapping the widget body opens MainActivity.
|
||||||
*
|
*
|
||||||
* Uses LocationManager directly (not the hybrid LocationProvider) because
|
* Uses LocationManager directly (not the hybrid LocationProvider) because
|
||||||
|
|
@ -36,6 +39,24 @@ class ShelterWidgetProvider : AppWidgetProvider() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ShelterWidget"
|
private const val TAG = "ShelterWidget"
|
||||||
const val ACTION_REFRESH = "no.naiv.tilfluktsrom.widget.REFRESH"
|
const val ACTION_REFRESH = "no.naiv.tilfluktsrom.widget.REFRESH"
|
||||||
|
|
||||||
|
/** Trigger a widget refresh from anywhere (e.g. MainActivity on location update). */
|
||||||
|
fun requestUpdate(context: Context) {
|
||||||
|
val intent = Intent(context, ShelterWidgetProvider::class.java).apply {
|
||||||
|
action = ACTION_REFRESH
|
||||||
|
}
|
||||||
|
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(
|
override fun onUpdate(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package no.naiv.tilfluktsrom.widget
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Periodic background worker that refreshes the home screen widget.
|
||||||
|
*
|
||||||
|
* Scheduled every 15 minutes (WorkManager's minimum interval).
|
||||||
|
* Simply triggers the widget's existing update logic via broadcast.
|
||||||
|
*/
|
||||||
|
class WidgetUpdateWorker(
|
||||||
|
context: Context,
|
||||||
|
params: WorkerParameters
|
||||||
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result {
|
||||||
|
ShelterWidgetProvider.requestUpdate(applicationContext)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val WORK_NAME = "widget_update"
|
||||||
|
|
||||||
|
/** Schedule periodic widget updates. Safe to call multiple times. */
|
||||||
|
fun schedule(context: Context) {
|
||||||
|
val request = PeriodicWorkRequestBuilder<WidgetUpdateWorker>(
|
||||||
|
15, TimeUnit.MINUTES
|
||||||
|
).build()
|
||||||
|
|
||||||
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||||
|
WORK_NAME,
|
||||||
|
ExistingPeriodicWorkPolicy.KEEP,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancel periodic updates (e.g. when all widgets are removed). */
|
||||||
|
fun cancel(context: Context) {
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
versionMajor=1
|
versionMajor=1
|
||||||
versionMinor=3
|
versionMinor=3
|
||||||
versionPatch=0
|
versionPatch=1
|
||||||
versionCode=4
|
versionCode=5
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue