Legg til fdroid-byggvariant uten Google Play Services
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 <noreply@anthropic.com>
This commit is contained in:
parent
f1b405950e
commit
ff4b3245f5
7 changed files with 529 additions and 2 deletions
|
|
@ -40,6 +40,16 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions += "distribution"
|
||||||
|
productFlavors {
|
||||||
|
create("standard") {
|
||||||
|
dimension = "distribution"
|
||||||
|
}
|
||||||
|
create("fdroid") {
|
||||||
|
dimension = "distribution"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
|
|
@ -88,8 +98,8 @@ dependencies {
|
||||||
// OSMDroid (offline-capable OpenStreetMap)
|
// OSMDroid (offline-capable OpenStreetMap)
|
||||||
implementation("org.osmdroid:osmdroid-android:6.1.20")
|
implementation("org.osmdroid:osmdroid-android:6.1.20")
|
||||||
|
|
||||||
// Google Play Services Location (precise GPS)
|
// Google Play Services Location (precise GPS) — standard flavor only
|
||||||
implementation("com.google.android.gms:play-services-location:21.3.0")
|
"standardImplementation"("com.google.android.gms:play-services-location:21.3.0")
|
||||||
|
|
||||||
// WorkManager (periodic widget updates)
|
// WorkManager (periodic widget updates)
|
||||||
implementation("androidx.work:work-runtime-ktx:2.9.1")
|
implementation("androidx.work:work-runtime-ktx:2.9.1")
|
||||||
|
|
|
||||||
|
|
@ -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<Location> = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<WidgetUpdateWorker>(
|
||||||
|
15, TimeUnit.MINUTES
|
||||||
|
).build()
|
||||||
|
|
||||||
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||||
|
WORK_NAME,
|
||||||
|
ExistingPeriodicWorkPolicy.KEEP,
|
||||||
|
request
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runOnce(context: Context) {
|
||||||
|
val request = OneTimeWorkRequestBuilder<WidgetUpdateWorker>().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<Location?> { 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue