Add OSM search provider (#611)
* add openstreetmaps module in data * fix injection * retrofit2 implementation * tokenization * partial rewrite of OpeningTime.fromOverpassElement * finish rewrite of OpeningTime.fromOverpassElement * fix merge x) * configurable search radius * add settings section and disable setting explicit timeout values for http-client to alleviate issues during debugging * settings screen localization * enable radius slider only when locations are enabled * fix dayRange parsing, add barebones UI * add files to git * add location listener in SearchableItemVM that gets activated by LocationItem * add heading listener * Calculations, UI additions * rename settings to LocationsSettings * use android location library for bearing calculations * location fix * rotation fix, demo UI * working buttons for launching map and website (if available) * finish botched UI * improve overpass query by utilizing regex for fuzzy search results * add link to documentation for further reference * localization comments * remove wikipedia minification setting * schema version, default radius 1.5km * move osm-specific opening-time parsing to OsmLocation * refactor with callbackFlow * remember flow and set minimum distance update to 1m * refactor for replacementIcon, add imperial unit option * 'open until' UI fix * implement serializers * catch errors in deserializer * hacky live sorting by distance * give max priority to bestmatch determined by SearchVM * add yards as additional step to metersToLocalizzedString * move http-client from serializers of osmlocation to companion object for cache updating * add setting for custom URL * round yards to int * add botched map preview * unbotch map tiles, draw user location in map (proof of concept) * - create MapTiles Composable - add border around map - add indicators for location and userlocation, when on map * fix default imperial units setting * fix tint color * add OSM attribution string * display loading animation when tiles can't be shown yet * create compose preview of maptiles * UI work * being glad that API's just return null instead of throwing information * tryStartActivity * aniimate card row placement * Text alignment, padding * Rotation -PI/PI wrap fix * fix direction arrow rotation when screen is upside down * more icons * icons, settings, localization - consider other tags than "amenity" when determining location category - add many more location categories with corresponding icons - add settings to disable map theming and hide search results with LocationCategory.OTHER - add default localizations for settings * catch errors when deserializing location category * move location and heading functions to Context.kt in extensions * fix hideUncategorized criterion * add pre-sorting by distance for location results in SearchVM by injecting Context into search() * specify receiver parameters in ktx.Context lambdas * move pose logic and context dependency to new module devicepose with DevicePoseProvider * git, add the frickin' module * search overpass for nodes and ways include category for parcel_locker already start searching for queries extending length 2 * make openingTimes immutable * OsmRepository changes - include telephone number - don't try to repeatedly update cache if there is no value to be updated to - deduplicate results with same label by category and distance (100m) - include fixmeurl to point to openstreetmaps.org/fixthemap * ask for center in overpass API to compute center coordinates of ways * search for brand * add chemist location category * restaurant / fastfood icon shenanigans * actually add the icons :| * add leisure tag for leisure:fitness_centre * return to 'open until'/'opens in'/'open next' * adding missing UI features - bug report dialog - call button if phone number exists - grid item popup * refactor to handle 24/7 locations more comfortably * hide hours in 'opens_in' when they are zero * show maptiles such that user is always in view * drawing adjustments * cache previous zoom level to speed up tile coordinate calculations * using remember * using MutableIntState * fix logic that determines whether tiles are loading * fix for numTiles == 9 * one plus one is two plus one makes three quick maths * animate user location indicator, remember calculations * fix off by one when determining next opening hour * second attempt to fix upside down arrow rotation (probably fine now) * logging * reconsider declination, inject samplingPeriod * undoing the merge undo * move localization string to i18n * revert reordering by distance * refactor .distanceTo * make Location abstract class to override compareTo with cached distance to correct sort order in search results * when it is if when you could use when * replace Pair with dataclass * condition check order * not creating objects with undefined locations, removing suspend from getCategory() * inject permissionsmanager as constructor parameter * Store OSM settings in decentralized datastore * Update searchable content in database on launch * Refactor, add mechanism to load updated searchable data lazily * Cache all OSM data in launcher database * Add pin to favorites button to location results * Add sealed class UpdateResult that is returned by awaiting updatedSelf of DeferredSearchable - update on success - set flag on temporarily unavailable (TODO add some UI indication) - delete and invalidate VM on permanently unavailable (Display some message window to user?) * Move sorting of Locations from OsmRepository to SearchVM using cached location in DevicePoseProvider, if available * make use of cached location in code * make use of DevicePoseProvider in WeatherRepository * inject via koin * increase getLocation().timeout() to 10 minutes since we are asking for locations only every hour, so 10 minutes seem reasonable (?) * poll new location every time * add icon for cached results where results are temporarily unavailable * Refactor DeferredSearchable to UpdatableSearchable that receives a closure to retrieve an updated self. - moved timestamp (formerly `updatedAt`) to UpdatableSearchable - moved logic whether to update searchable to `requestUpdatedSearchable` in SearchableItemVM, which gets triggered every time the details of the item are shown - keep track in SearchableVM whether we should retry updating, possibly bypassing a timestamp value that is not old enough - show toast upon permanently unavailable - animate "cached_searchable" icon - make "cached_searchable" icon clickable to show toast explaining the situation * logging on PermanentlyUnavailable * refactor OsmRepository.update() * MapTheming adjustments. There is now darkmode, hooray! * remove outdated comment * code tidying * remove unnecessary LaunchedEffect * make outdated badge only clickable when actually outdated * deserialize opening schedule * set deserialized props to null if strings are blank * also consider contact:* tagging scheme for website & phone * tweaks * git add Result.kt * don't search for locations if network is not allowed * merge fixes * Move location search settings to preferences module * Change wording and order of location search preferences * Limit location search results * Order location search results by distance * Use a sequence * Android Studio's suggestion wasn't as fleshed out as one would hope * Add proguard rules * Rename TileMapRepository to MapTileLoader --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
@@ -51,5 +51,6 @@ dependencies {
|
||||
implementation(project(":core:preferences"))
|
||||
implementation(project(":core:permissions"))
|
||||
implementation(project(":core:i18n"))
|
||||
implementation(project(":core:devicepose"))
|
||||
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
package de.mm20.launcher2.weather
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.work.*
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.ktx.checkPermission
|
||||
import de.mm20.launcher2.devicepose.DevicePoseProvider
|
||||
import de.mm20.launcher2.ktx.or
|
||||
import de.mm20.launcher2.permissions.PermissionGroup
|
||||
import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.plugin.PluginRepository
|
||||
@@ -24,8 +21,9 @@ import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
interface WeatherRepository {
|
||||
fun getProviders(): Flow<List<WeatherProviderInfo>>
|
||||
@@ -41,21 +39,20 @@ internal class WeatherRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val database: AppDatabase,
|
||||
private val settings: WeatherSettings,
|
||||
private val pluginRepository: PluginRepository,
|
||||
private val pluginRepository: PluginRepository
|
||||
) : WeatherRepository, KoinComponent {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
|
||||
|
||||
private val permissionsManager: PermissionsManager by inject()
|
||||
|
||||
private val hasLocationPermission = permissionsManager.hasPermission(PermissionGroup.Location)
|
||||
|
||||
|
||||
override fun getForecasts(limit: Int?): Flow<List<Forecast>> {
|
||||
return database.weatherDao().getForecasts(limit ?: 99999)
|
||||
.map { it.map { Forecast(it) } }
|
||||
}
|
||||
|
||||
override fun getDailyForecasts(): Flow<List<DailyForecast>> {
|
||||
return database.weatherDao().getForecasts()
|
||||
.map { it.map { Forecast(it) } }
|
||||
@@ -73,7 +70,7 @@ internal class WeatherRepositoryImpl(
|
||||
|
||||
init {
|
||||
val weatherRequest =
|
||||
PeriodicWorkRequest.Builder(WeatherUpdateWorker::class.java, 60, TimeUnit.MINUTES)
|
||||
PeriodicWorkRequestBuilder<WeatherUpdateWorker>(Duration.ofMinutes(60))
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
"weather",
|
||||
@@ -134,7 +131,7 @@ internal class WeatherRepositoryImpl(
|
||||
|
||||
|
||||
private fun requestUpdate() {
|
||||
val weatherRequest = OneTimeWorkRequest.Builder(WeatherUpdateWorker::class.java)
|
||||
val weatherRequest = OneTimeWorkRequestBuilder<WeatherUpdateWorker>()
|
||||
.addTag("weather")
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueue(weatherRequest)
|
||||
@@ -151,15 +148,35 @@ internal class WeatherRepositoryImpl(
|
||||
|
||||
override fun getProviders(): Flow<List<WeatherProviderInfo>> {
|
||||
val providers = mutableListOf<WeatherProviderInfo>()
|
||||
providers.add(WeatherProviderInfo(BrightSkyProvider.Id, context.getString(R.string.provider_brightsky)))
|
||||
providers.add(
|
||||
WeatherProviderInfo(
|
||||
BrightSkyProvider.Id,
|
||||
context.getString(R.string.provider_brightsky)
|
||||
)
|
||||
)
|
||||
if (OpenWeatherMapProvider.isAvailable(context)) {
|
||||
providers.add(WeatherProviderInfo(OpenWeatherMapProvider.Id, context.getString(R.string.provider_openweathermap)))
|
||||
providers.add(
|
||||
WeatherProviderInfo(
|
||||
OpenWeatherMapProvider.Id,
|
||||
context.getString(R.string.provider_openweathermap)
|
||||
)
|
||||
)
|
||||
}
|
||||
if (MetNoProvider.isAvailable(context)) {
|
||||
providers.add(WeatherProviderInfo(MetNoProvider.Id, context.getString(R.string.provider_metno)))
|
||||
providers.add(
|
||||
WeatherProviderInfo(
|
||||
MetNoProvider.Id,
|
||||
context.getString(R.string.provider_metno)
|
||||
)
|
||||
)
|
||||
}
|
||||
if (HereProvider.isAvailable(context)) {
|
||||
providers.add(WeatherProviderInfo(HereProvider.Id, context.getString(R.string.provider_here)))
|
||||
providers.add(
|
||||
WeatherProviderInfo(
|
||||
HereProvider.Id,
|
||||
context.getString(R.string.provider_here)
|
||||
)
|
||||
)
|
||||
}
|
||||
val pluginProviders = pluginRepository.findMany(type = PluginType.Weather, enabled = true)
|
||||
return pluginProviders.map {
|
||||
@@ -170,11 +187,14 @@ internal class WeatherRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
class WeatherUpdateWorker(val context: Context, params: WorkerParameters) :
|
||||
CoroutineWorker(context, params), KoinComponent {
|
||||
class WeatherUpdateWorker(
|
||||
val context: Context,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params), KoinComponent {
|
||||
|
||||
private val appDatabase: AppDatabase by inject()
|
||||
private val settings: WeatherSettings by inject()
|
||||
private val locationProvider: DevicePoseProvider by inject()
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Log.d("WeatherUpdateWorker", "Requesting weather data")
|
||||
@@ -218,15 +238,9 @@ class WeatherUpdateWorker(val context: Context, params: WorkerParameters) :
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLastKnownLocation(): LatLon? {
|
||||
val lm = context.getSystemService<LocationManager>()!!
|
||||
var location: Location? = null
|
||||
if (context.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
|
||||
location = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
}
|
||||
if (location == null && context.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) {
|
||||
location = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
|
||||
}
|
||||
return location?.let { LatLon(it.latitude, it.longitude) }
|
||||
}
|
||||
@OptIn(FlowPreview::class)
|
||||
private suspend fun getLastKnownLocation(): LatLon? =
|
||||
locationProvider.getLocation().timeout(10.minutes).firstOrNull().or {
|
||||
locationProvider.lastLocation
|
||||
}?.let { LatLon(it.latitude, it.longitude) }
|
||||
}
|
||||
Reference in New Issue
Block a user