Refactor weather providers

This commit is contained in:
MM20 2021-09-20 22:23:03 +02:00
parent e77901eb6a
commit 65d4047cf3
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
9 changed files with 574 additions and 499 deletions

View File

@ -13,6 +13,7 @@ import com.afollestad.materialdialogs.list.listItemsSingleChoice
import de.mm20.launcher2.R import de.mm20.launcher2.R
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.WeatherProviders import de.mm20.launcher2.preferences.WeatherProviders
import de.mm20.launcher2.weather.WeatherLocation
import de.mm20.launcher2.weather.WeatherProvider import de.mm20.launcher2.weather.WeatherProvider
import de.mm20.launcher2.weather.WeatherViewModel import de.mm20.launcher2.weather.WeatherViewModel
import de.mm20.launcher2.weather.here.HereProvider import de.mm20.launcher2.weather.here.HereProvider
@ -89,7 +90,7 @@ class PreferencesWeatherFragment : PreferenceFragmentCompat() {
val provider = WeatherProvider.getInstance(requireContext()) val provider = WeatherProvider.getInstance(requireContext())
provider?.autoLocation = autoLocation provider?.autoLocation = autoLocation
provider?.resetLastUpdate() provider?.resetLastUpdate()
provider?.setLocation(null, "") provider?.setLocation(null)
ViewModelProvider(this).get(WeatherViewModel::class.java) ViewModelProvider(this).get(WeatherViewModel::class.java)
.requestUpdate(requireContext()) .requestUpdate(requireContext())
updateProviderPreferences() updateProviderPreferences()
@ -105,47 +106,28 @@ class PreferencesWeatherFragment : PreferenceFragmentCompat() {
val unitsPref = findPreference<Preference>("imperial_units")!! val unitsPref = findPreference<Preference>("imperial_units")!!
val providerPref = findPreference<Preference>("weather_provider")!! val providerPref = findPreference<Preference>("weather_provider")!!
locationPref.parent?.isVisible = provider != null locationPref.parent?.isVisible = provider != null
unitsPref.isVisible = provider != null unitsPref.isVisible = provider != null
provider ?: return provider ?: return
providerPref.summary = provider.name providerPref.summary = provider.name
autoLocationPref.isChecked = provider.autoLocation
if (provider.supportsAutoLocation) { locationPref.summary =
autoLocationPref.setSummary(R.string.preference_automatic_location_summary) if (provider.autoLocation) provider.getLastLocation()?.name else provider.getLocation()?.name
autoLocationPref.isChecked = provider.autoLocation
if (!provider.supportsManualLocation) {
autoLocationPref.isEnabled = false
autoLocationPref.isChecked = true
locationPref.isEnabled = false
locationPref.setSummary(R.string.preference_location_disabled_summary)
} else {
autoLocationPref.isEnabled = true
autoLocationPref.isChecked = provider.autoLocation
locationPref.isEnabled = true
locationPref.summary = provider.getLastLocation()
}
} else {
autoLocationPref.isEnabled = false
autoLocationPref.setSummary(R.string.preference_automatic_location_disabled_summary)
autoLocationPref.isChecked = false
}
} }
private fun onLookupCompleted(results: List<Pair<Any?, String>>) { private fun onLookupCompleted(results: List<WeatherLocation>) {
MaterialDialog(requireContext()) MaterialDialog(requireContext())
.listItems( .listItems(
items = results.map { it.second }, items = results.map { it.name },
waitForPositiveButton = false waitForPositiveButton = false
) { dialog, index, _ -> ) { dialog, index, _ ->
val provider = WeatherProvider.getInstance(requireContext()) val provider = WeatherProvider.getInstance(requireContext())
?: return@listItems dialog.dismiss() ?: return@listItems dialog.dismiss()
provider.resetLastUpdate() provider.resetLastUpdate()
provider.setLocation(results[index].first, results[index].second) provider.setLocation(results[index])
findPreference<Preference>("location")?.summary = results[index].second findPreference<Preference>("location")?.summary = results[index].name
ViewModelProvider(this).get(WeatherViewModel::class.java) ViewModelProvider(this).get(WeatherViewModel::class.java)
.requestUpdate(requireContext()) .requestUpdate(requireContext())
dialog.dismiss() dialog.dismiss()

View File

@ -0,0 +1,115 @@
package de.mm20.launcher2.weather
import android.location.Geocoder
import androidx.core.content.edit
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.ktx.getDouble
import de.mm20.launcher2.ktx.putDouble
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
/**
* A WeatherProvider that uses lat/lon locations only (instead of provider specific location IDs)
*/
abstract class LatLonWeatherProvider : WeatherProvider<LatLonWeatherLocation>() {
override suspend fun lookupLocation(query: String): List<LatLonWeatherLocation> {
if (!Geocoder.isPresent()) return emptyList()
val geocoder = Geocoder(context)
val locations =
withContext(Dispatchers.IO) {
geocoder.getFromLocationName(query, 10)
}
return locations.mapNotNull {
LatLonWeatherLocation(
lat = it.latitude,
lon = it.longitude,
name = it.formatToString()
)
}
}
override suspend fun loadWeatherData(
lat: Double,
lon: Double
): WeatherUpdateResult<LatLonWeatherLocation>? {
return try {
val locationName = Geocoder(context).getFromLocation(lat, lon, 1)
.firstOrNull()
?.formatToString() ?: "$lat/$lon"
loadWeatherData(
LatLonWeatherLocation(
name = locationName,
lat = lat,
lon = lon
)
)
} catch (e: IOException) {
CrashReporter.logException(e)
null
}
}
override fun setLocation(location: WeatherLocation?) {
location as LatLonWeatherLocation?
preferences.edit {
if (location == null) {
remove(LAT)
remove(LON)
remove(LOCATION_NAME)
} else {
putDouble(LAT, location.lat)
putDouble(LON, location.lon)
putString(LOCATION_NAME, location.name)
}
}
}
override fun getLocation(): LatLonWeatherLocation? {
val lat = preferences.getDouble(LAT) ?: return null
val lon = preferences.getDouble(LON) ?: return null
val name = preferences.getString(LOCATION_NAME, null) ?: return null
return LatLonWeatherLocation(
name = name,
lat = lat,
lon = lon
)
}
override fun saveLastLocation(location: LatLonWeatherLocation) {
preferences.edit {
putDouble(LAST_LAT, location.lat)
putDouble(LAST_LON, location.lon)
putString(LAST_LOCATION_NAME, location.name)
}
}
override fun getLastLocation(): LatLonWeatherLocation? {
val lat = preferences.getDouble(LAST_LAT) ?: return null
val lon = preferences.getDouble(LAST_LON) ?: return null
val name = preferences.getString(LAST_LOCATION_NAME, null) ?: return null
return LatLonWeatherLocation(
name = name,
lat = lat,
lon = lon
)
}
companion object {
private const val LAT = "lat"
private const val LON = "lon"
private const val LOCATION_NAME = "location_name"
private const val LAST_LAT = "last_lat"
private const val LAST_LON = "last_lon"
private const val LAST_LOCATION_NAME = "last_location_name"
}
}
data class LatLonWeatherLocation(
override val name: String,
val lat: Double,
val lon: Double
) : WeatherLocation

View File

@ -0,0 +1,5 @@
package de.mm20.launcher2.weather
interface WeatherLocation {
val name: String
}

View File

@ -1,51 +1,118 @@
package de.mm20.launcher2.weather package de.mm20.launcher2.weather
import android.Manifest
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.location.LocationManager
import androidx.core.content.edit
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.preferences.WeatherProviders import de.mm20.launcher2.preferences.WeatherProviders
import de.mm20.launcher2.weather.here.HereProvider import de.mm20.launcher2.weather.here.HereProvider
import de.mm20.launcher2.weather.metno.MetNoProvider import de.mm20.launcher2.weather.metno.MetNoProvider
import de.mm20.launcher2.weather.openweathermap.OpenWeatherMapProvider import de.mm20.launcher2.weather.openweathermap.OpenWeatherMapProvider
abstract class WeatherProvider { abstract class WeatherProvider<T : WeatherLocation> {
abstract val supportsAutoLocation: Boolean internal abstract val context: Context
abstract val supportsManualLocation: Boolean internal abstract val preferences: SharedPreferences
abstract var autoLocation: Boolean var autoLocation: Boolean
get() {
return preferences.getBoolean(AUTO_LOCATION, true)
}
set(value) {
preferences.edit {
putBoolean(AUTO_LOCATION, value)
}
}
abstract suspend fun fetchNewWeatherData(): List<Forecast>?
suspend fun fetchNewWeatherData(): List<Forecast>? {
val result: WeatherUpdateResult<T>
if (autoLocation) {
if (context.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) {
val lm = context.getSystemService<LocationManager>()!!
val location = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
if (location != null) {
result = loadWeatherData(location.latitude, location.longitude) ?: return null
} else {
val lastLocation = getLastLocation() ?: return null
result = loadWeatherData(lastLocation) ?: return null
}
} else {
val lastLocation = getLastLocation() ?: return null
result = loadWeatherData(lastLocation) ?: return null
}
} else {
val setLocation = getLocation() ?: return null
result = loadWeatherData(setLocation) ?: return null
}
saveLastLocation(result.location)
setLastUpdate(System.currentTimeMillis())
return result.forecasts
}
internal abstract suspend fun loadWeatherData(location: T): WeatherUpdateResult<T>?
internal abstract suspend fun loadWeatherData(lat: Double, lon: Double): WeatherUpdateResult<T>?
abstract fun isUpdateRequired(): Boolean abstract fun isUpdateRequired(): Boolean
abstract fun getLastUpdate(): Long fun getLastUpdate(): Long {
return preferences.getLong(LAST_UPDATE, 0)
}
private fun setLastUpdate(time: Long) {
preferences.edit {
putLong(LAST_UPDATE, time)
}
}
/** /**
* Lookup a location based on a string query. * Lookup a location based on a string query.
* @param query the location to lookup * @param query the location to lookup
* @return a list of Pair<Any?,String> with provider specific data of that location and its * @return a list of locations
* display name
*/ */
abstract suspend fun lookupLocation(query: String): List<Pair<Any?, String>> abstract suspend fun lookupLocation(query: String): List<T>
abstract fun setLocation(locationId: Any?, locationName: String) /**
* @param location must be of type T
*/
abstract fun setLocation(location: WeatherLocation?)
abstract fun getLocation(): T?
abstract fun isAvailable(): Boolean abstract fun isAvailable(): Boolean
abstract val name: String abstract val name: String
abstract fun getLastLocation(): T?
abstract fun saveLastLocation(location: T)
fun resetLastUpdate() {
preferences.edit {
putLong(LAST_UPDATE, 0L)
}
}
companion object { companion object {
fun getInstance(context: Context): WeatherProvider? { fun getInstance(context: Context): WeatherProvider<out WeatherLocation>? {
return when (LauncherPreferences.instance.weatherProvider) { return when (LauncherPreferences.instance.weatherProvider) {
WeatherProviders.OPENWEATHERMAP -> OpenWeatherMapProvider(context) WeatherProviders.OPENWEATHERMAP -> OpenWeatherMapProvider(context)
WeatherProviders.HERE -> HereProvider(context) WeatherProviders.HERE -> HereProvider(context)
else -> MetNoProvider(context) else -> MetNoProvider(context)
}.takeIf { it.isAvailable() } }.takeIf { it.isAvailable() }
} }
}
abstract fun getLastLocation(): String private const val LAST_UPDATE = "last_update"
abstract fun resetLastUpdate() private const val AUTO_LOCATION = "auto_location"
}
} }
data class WeatherUpdateResult<T : WeatherLocation>(
val forecasts: List<Forecast>,
val location: T
)

View File

@ -0,0 +1,52 @@
package de.mm20.launcher2.weather.here
import retrofit2.http.GET
import retrofit2.http.Query
data class HereGeocodeResult(
val Response: HereGeocodeResultResponse
)
data class HereGeocodeResultResponse(
val View: Array<HereGeocodeResultResponseView>?
)
data class HereGeocodeResultResponseView(
val Result: Array<HereGeocodeResultResponseViewResult>?
)
data class HereGeocodeResultResponseViewResult(
val Location: HereGeocodeResultResponseViewResultLocation?
)
data class HereGeocodeResultResponseViewResultLocation(
val LocationId: String?,
val LocationType: String?,
val DisplayPosition: HereGeocodeResultResponseViewResultLocationPosition?,
val Address: HereGeocodeResultResponseViewResultLocationAddress?
)
data class HereGeocodeResultResponseViewResultLocationPosition(
val Latitude: Double?,
val Longitude: Double?
)
data class HereGeocodeResultResponseViewResultLocationAddress(
val Label: String?,
val Country: String?,
val State: String?,
val County: String?,
val City: String?,
val District: String?,
val Street: String?,
val HouseNumber: String?,
val PostalCode: String?,
)
interface HereGeocodeApi {
@GET("geocode.json")
suspend fun geocode(
@Query("apiKey") apiKey: String,
@Query("searchtext") searchtext: String
): HereGeocodeResult
}

View File

@ -1,117 +1,68 @@
package de.mm20.launcher2.weather.here package de.mm20.launcher2.weather.here
import android.Manifest
import android.content.Context import android.content.Context
import android.location.LocationManager import android.content.SharedPreferences
import android.util.Log
import androidx.core.content.edit
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.checkPermission import de.mm20.launcher2.weather.*
import de.mm20.launcher2.ktx.getDouble import retrofit2.Retrofit
import de.mm20.launcher2.ktx.putDouble import retrofit2.converter.gson.GsonConverterFactory
import de.mm20.launcher2.weather.Forecast import retrofit2.create
import de.mm20.launcher2.weather.R
import de.mm20.launcher2.weather.WeatherProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.net.URLEncoder
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class HereProvider(val context: Context) : WeatherProvider() { class HereProvider(override val context: Context) : LatLonWeatherProvider() {
override val supportsAutoLocation = true
override val supportsManualLocation = true
override var autoLocation: Boolean
get() {
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.getBoolean(AUTO_LOCATION, true)
}
set(value) {
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.edit {
putBoolean(AUTO_LOCATION, value)
}
}
override suspend fun fetchNewWeatherData(): List<Forecast>? { private val retrofit by lazy {
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) Retrofit.Builder()
.baseUrl("https://weather.ls.hereapi.com/weather/1.0/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
private val hereWeatherService by lazy {
retrofit.create<HereWeatherApi>()
}
override val preferences: SharedPreferences
get() = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
override suspend fun loadWeatherData(location: LatLonWeatherLocation): WeatherUpdateResult<LatLonWeatherLocation>? {
return loadWeatherData(location.lat, location.lon)
}
override suspend fun loadWeatherData(
lat: Double,
lon: Double
): WeatherUpdateResult<LatLonWeatherLocation>? {
val updateTime = System.currentTimeMillis() val updateTime = System.currentTimeMillis()
var query: String? = null
if (autoLocation) {
var lat: Double? = null
var lon: Double? = null
if (context.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val location = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
lat = location?.latitude
lon = location?.longitude
if (lat != null && lon != null) {
prefs.edit {
putDouble(LAST_LAT, lat!!)
putDouble(LAST_LON, lon!!)
}
}
}
if (lat == null || lon == null) {
lat = prefs.getDouble(LAST_LAT)
lon = prefs.getDouble(LAST_LON)
}
if (lat != null && lon != null) query = "latitude=$lat&longitude=$lon"
}
if (!autoLocation || query == null) {
val name = prefs.getString(CITY_NAME, null) ?: return null
query = "name=$name"
}
val lang = Locale.getDefault().language val lang = Locale.getDefault().language
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.ROOT) val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.ROOT)
val forecastList = mutableListOf<Forecast>() val forecastList = mutableListOf<Forecast>()
try { try {
val httpClient = OkHttpClient() val apiKey = getApiKey() ?: return null
val forecastRequest = Request.Builder() val response = hereWeatherService.report(
.url("https://weather.ls.hereapi.com/weather/1.0/report.json?apiKey=${getApiKey()}&product=forecast_hourly&$query&language=$lang") apiKey = apiKey,
.get() language = lang,
.build() latitude = lat,
longitude = lon
)
val body = withContext(Dispatchers.IO) { val forecastLocation = response.hourlyForecasts?.forecastLocation ?: return null
httpClient.newCall(forecastRequest).execute().body?.string() val forecasts = forecastLocation.forecast ?: return null
} ?: run {
Log.e("MM20", "Here provider: forecast request returned null")
return null
}
val forecastLocation = JSONObject(body) val location = forecastLocation.city ?: return null
.getJSONObject("hourlyForecasts")
.getJSONObject("forecastLocation")
val forecasts = forecastLocation.getJSONArray("forecast")
val location = forecastLocation.getString("city")
val locationLong =
"${forecastLocation.getString("city")}, ${forecastLocation.getString("country")}"
if (autoLocation) { for (forecast in forecasts) {
prefs.edit {
putString(LAST_LOCATION, locationLong)
}
}
for (i in 0 until forecasts.length()) {
val forecast = forecasts.getJSONObject(i)
val timestamp = try { val timestamp = try {
dateFormat.parse(forecast.getString("utcTime"))?.time ?: continue dateFormat.parse(forecast.utcTime ?: continue)?.time ?: continue
} catch (e: ParseException) { } catch (e: ParseException) {
CrashReporter.logException(e) CrashReporter.logException(e)
return null return null
@ -121,24 +72,20 @@ class HereProvider(val context: Context) : WeatherProvider() {
if (timestamp + 1000 * 60 * 30 < System.currentTimeMillis()) continue if (timestamp + 1000 * 60 * 30 < System.currentTimeMillis()) continue
val condition = when { val condition = when {
!forecast.optString("precipitationDesc") !forecast.precipitationDesc.isNullOrEmpty() -> forecast.precipitationDesc
.isNullOrEmpty() -> forecast.optString("precipitationDesc") !forecast.skyDescription.isNullOrEmpty() -> forecast.skyDescription
!forecast.optString("skyDescription") !forecast.temperatureDesc.isNullOrEmpty() -> forecast.temperatureDesc
.isNullOrEmpty() -> forecast.optString("skyDescription") else -> forecast.description ?: continue
!forecast.optString("temperatureDesc")
.isNullOrEmpty() -> forecast.optString("temperatureDesc")
else -> forecast.optString("description")
} }
val humidity = forecast.getString("humidity").toIntOrNull() ?: 0 val humidity = forecast.humidity?.toIntOrNull() ?: 0
val icon = getIcon(forecast.getString("iconName")) val icon = getIcon(forecast.iconName ?: continue)
val night = forecast.getString("daylight") == "N" val night = forecast.daylight == "N"
val rain = forecast.getString("rainFall").toDoubleOrNull() ?: 0.0 val rain = forecast.rainFall?.toDoubleOrNull() ?: 0.0
val snow = forecast.getString("snowFall").toDoubleOrNull() ?: 0.0 val rainPercent = forecast.precipitationProbability?.toIntOrNull() ?: 0
val rainPercent = forecast.getString("precipitationProbability").toIntOrNull() ?: 0 val temperature = forecast.temperature?.toDoubleOrNull()?.plus(273.15)
val temperature = forecast.getString("temperature").toDoubleOrNull()?.plus(273.15)
?: 0.0 ?: 0.0
val windDir = forecast.getString("windDirection").toIntOrNull() ?: 0 val windDir = forecast.windDirection?.toIntOrNull() ?: 0
val windSpeed = forecast.getString("windSpeed").toDoubleOrNull() ?: 0.0 val windSpeed = forecast.windSpeed?.toDoubleOrNull() ?: 0.0
forecastList.add( forecastList.add(
Forecast( Forecast(
@ -162,19 +109,23 @@ class HereProvider(val context: Context) : WeatherProvider() {
) )
} }
return WeatherUpdateResult(
forecasts = forecastList,
location = LatLonWeatherLocation(
name = location,
lat = lat,
lon = lon
)
)
} catch (e: JSONException) {
} catch (e: Exception) {
CrashReporter.logException(e) CrashReporter.logException(e)
return null return null
} }
prefs.edit {
putLong(LAST_UPDATE, updateTime)
}
return forecastList
} }
private fun getIcon(iconName: String): Int { private fun getIcon(iconName: String): Int {
with(Forecast) { with(Forecast) {
return when (iconName) { return when (iconName) {
@ -334,62 +285,29 @@ class HereProvider(val context: Context) : WeatherProvider() {
return getLastUpdate() + (1000 * 60 * 60) <= System.currentTimeMillis() return getLastUpdate() + (1000 * 60 * 60) <= System.currentTimeMillis()
} }
override fun getLastUpdate(): Long { override suspend fun lookupLocation(query: String): List<LatLonWeatherLocation> {
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) val retrofit = Retrofit.Builder()
.getLong(LAST_UPDATE, 0) .baseUrl("https://geocoder.ls.hereapi.com/6.2/")
} .addConverterFactory(GsonConverterFactory.create())
override suspend fun lookupLocation(query: String): List<Pair<Any?, String>> {
val urlString =
"https://geocoder.ls.hereapi.com/6.2/geocode.json?apiKey=${getApiKey()}&searchtext=$query"
val client = OkHttpClient()
val request = Request.Builder()
.url(urlString)
.build() .build()
val geocodeService = retrofit.create<HereGeocodeApi>()
try { try {
val body = withContext(Dispatchers.IO) { val apiKey = getApiKey() ?: return emptyList()
val response = client.newCall(request).execute() val response = geocodeService.geocode(apiKey, query)
response.body?.string()
} ?: return emptyList() return response.Response.View?.getOrNull(0)?.Result?.mapNotNull {
val json = JSONObject(body) LatLonWeatherLocation(
val results = json name = it.Location?.Address?.Label ?: return@mapNotNull null,
.optJSONObject("Response") lat = it.Location.DisplayPosition?.Latitude ?: return@mapNotNull null,
?.optJSONArray("View") lon = it.Location.DisplayPosition.Longitude ?: return@mapNotNull null,
?.optJSONObject(0) )
?.optJSONArray("Result") ?: return emptyList() } ?: emptyList()
val locations = mutableListOf<Pair<Any?, String>>() } catch (e: Exception) {
for (i in 0 until results.length()) { CrashReporter.logException(e)
val result = results.getJSONObject(i)
val location = result.optJSONObject("Location") ?: continue
val name = location.optJSONObject("Address")?.getString("Label") ?: continue
locations.add(URLEncoder.encode(name, "UTF-8") to name)
}
return locations
} catch (e: JSONException) {
} catch (e: IOException) {
} }
return emptyList() return emptyList()
} }
override fun setLocation(locationId: Any?, locationName: String) {
val id = locationId as? String ?: return
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit {
putString(CITY_NAME, id)
putString(LAST_LOCATION, locationName)
}
}
override fun getLastLocation(): String {
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.getString(LAST_LOCATION, "")!!
}
override fun resetLastUpdate() {
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit {
putLong(LAST_UPDATE, 0)
}
}
private fun getApiKey(): String? { private fun getApiKey(): String? {
val resId = getApiKeyResId() val resId = getApiKeyResId()
if (resId != 0) return context.getString(resId) if (resId != 0) return context.getString(resId)
@ -410,11 +328,5 @@ class HereProvider(val context: Context) : WeatherProvider() {
companion object { companion object {
private const val PREFERENCES = "here" private const val PREFERENCES = "here"
private const val LAST_LAT = "last_lat"
private const val LAST_LON = "last_lon"
private const val LAST_UPDATE = "last_update"
private const val CITY_NAME = "city_name"
private const val LAST_LOCATION = "last_location"
private const val AUTO_LOCATION = "auto_location"
} }
} }

View File

@ -0,0 +1,62 @@
package de.mm20.launcher2.weather.here
import retrofit2.http.GET
import retrofit2.http.Query
data class HereWeatherResult(
val hourlyForecasts: HereWeatherResultForecasts?
)
data class HereWeatherResultForecasts(
val forecastLocation: HereWeatherResultForecastsLocation?
)
data class HereWeatherResultForecastsLocation(
val forecast: Array<HereWeatherResultForecastsLocationForecast>?,
val country: String?,
val state: String?,
val city: String?,
val latitude: Double?,
val longitude: Double?,
)
data class HereWeatherResultForecastsLocationForecast(
val daylight: String?,
val description: String?,
val skyInfo: String?,
val skyDescription: String?,
val temperature: String?,
val temperatureDesc: String?,
val comfort: String?,
val humidity: String?,
val dewPoint: String?,
val precipitationProbability: String?,
val precipitationDesc: String?,
val rainFall: String?,
val snowFall: String?,
val airInfo: String?,
val airDescription: String?,
val windSpeed: String?,
val windDirection: String?,
val windDesc: String?,
val windDescShort: String?,
val visibility: String?,
val icon: String?,
val iconName: String?,
val iconLink: String?,
val dayOfWeek: String?,
val weekday: String?,
val utcTime: String?,
val localTime: String?,
val localTimeFormat: String?,
)
interface HereWeatherApi {
@GET("report.json?product=forecast_hourly")
suspend fun report(
@Query("apiKey") apiKey: String,
@Query("language") language: String,
@Query("latitude") latitude: Double,
@Query("longitude") longitude: Double
): HereWeatherResult
}

View File

@ -2,26 +2,23 @@ package de.mm20.launcher2.weather.metno
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Geocoder import android.location.Geocoder
import android.location.LocationManager import android.location.LocationManager
import android.os.Build import android.os.Build
import android.util.Base64 import android.util.Base64
import android.util.Log
import androidx.core.content.edit import androidx.core.content.edit
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.checkPermission import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.formatToString import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.ktx.getDouble import de.mm20.launcher2.ktx.getDouble
import de.mm20.launcher2.ktx.putDouble import de.mm20.launcher2.ktx.putDouble
import de.mm20.launcher2.weather.Forecast import de.mm20.launcher2.weather.*
import de.mm20.launcher2.weather.R
import de.mm20.launcher2.weather.WeatherProvider
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.internal.userAgent
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import org.shredzone.commons.suncalc.SunTimes import org.shredzone.commons.suncalc.SunTimes
@ -31,149 +28,10 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MetNoProvider(val context: Context) : WeatherProvider() { class MetNoProvider(override val context: Context) : LatLonWeatherProvider() {
override val supportsAutoLocation: Boolean
get() = true
override val supportsManualLocation: Boolean
get() = Geocoder.isPresent()
override var autoLocation: Boolean
get() {
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.getBoolean(AUTO_LOCATION, true)
}
set(value) {
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.edit {
putBoolean(AUTO_LOCATION, value)
}
}
override suspend fun fetchNewWeatherData(): List<Forecast>? { override val preferences: SharedPreferences
var lat: Double? = null get() = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
var lon: Double? = null
var locationName: String? = null
val updateTime = System.currentTimeMillis()
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
val lastUpdate = prefs.getLong(LAST_UPDATE, 0L)
val httpDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ROOT)
val ifModifiedSince = httpDateFormat.format(Date(lastUpdate))
if (autoLocation &&
context.checkPermission(Manifest.permission.ACCESS_FINE_LOCATION)
) {
val lm = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val location = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
lat = location?.latitude
lon = location?.longitude
if (Geocoder.isPresent() && lat != null && lon != null) {
try {
locationName = Geocoder(context).getFromLocation(lat, lon, 1)
.firstOrNull()
?.formatToString() ?: "$lat/$lon"
prefs.edit {
putString(LAST_LOCATION_NAME, locationName)
lat?.let { putDouble(LAST_LAT, it) }
lon?.let { putDouble(LAST_LON, it) }
}
} catch (e: IOException) {
CrashReporter.logException(e)
return null
}
}
}
if (!autoLocation) {
if (!prefs.contains(LON) || !prefs.contains(LAT)) return null
lat = prefs.getDouble(LAT)
lon = prefs.getDouble(LON)
locationName = prefs.getString(LAST_LOCATION_NAME, null) ?: "$lat/$lon"
}
if (lat == null || lon == null) {
if (!prefs.contains(LAST_LON) || !prefs.contains(LAST_LAT)) return null
lat = prefs.getDouble(LAST_LAT)
lon = prefs.getDouble(LAST_LON)
locationName = prefs.getString(LAST_LOCATION_NAME, null) ?: "$lat/$lon"
}
if (lat == null || lon == null || locationName == null) return null
try {
val forecasts = mutableListOf<Forecast>()
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT)
val httpClient = OkHttpClient()
val latParam = String.format(Locale.ROOT, "%.4f", lat)
val lonParam = String.format(Locale.ROOT, "%.4f", lon)
val forecastRequest = Request.Builder()
.url("https://api.met.no/weatherapi/locationforecast/2.0/?lat=$latParam&lon=$lonParam")
.addHeader("User-Agent", getUserAgent() ?: return null)
.addHeader("If-Modified-Since", ifModifiedSince)
.get()
.build()
val response = httpClient.newCall(forecastRequest).execute()
val responseBody = response.body?.string() ?: return null
val json = JSONObject(responseBody)
val properties = json.getJSONObject("properties")
val meta = properties.getJSONObject("meta")
val updatedAt = dateFormat.parse(meta.getString("updated_at"))?.time
?: System.currentTimeMillis()
val timeseries = properties.getJSONArray("timeseries")
for (i in 0 until timeseries.length()) {
val fc = timeseries.getJSONObject(i)
val data = fc.getJSONObject("data")
val timestamp = dateFormat.parse(fc.getString("time"))?.time ?: continue
val details = data.getJSONObject("instant").getJSONObject("details")
var hours = 0
val nextHours = data.optJSONObject("next_1_hours")?.also { hours = 1 }
?: data.optJSONObject("next_6_hours")?.also { hours = 6 }
?: data.optJSONObject("next_12_hours")?.also { hours = 12 }
?: continue
val symbolCode = nextHours.optJSONObject("summary")?.getString("symbol_code")
?: continue
val precipitationAmount =
(nextHours.optJSONObject("details")?.optDouble("precipitation_amount")
?: 0.0) / hours
forecasts.add(
Forecast(
timestamp = timestamp,
temperature = details.getDouble("air_temperature") + 273.15,
updateTime = updatedAt,
clouds = details.getDouble("cloud_area_fraction").roundToInt(),
humidity = details.getDouble("relative_humidity"),
windDirection = details.getDouble("wind_from_direction"),
windSpeed = details.getDouble("wind_speed"),
pressure = details.getDouble("air_pressure_at_sea_level"),
location = locationName,
provider = context.getString(R.string.provider_metno),
providerUrl = "https://www.yr.no/",
icon = iconForCode(symbolCode),
condition = conditionForCode(symbolCode),
precipitation = precipitationAmount,
night = isNight(timestamp, lat, lon)
)
)
}
prefs.edit {
putLong(LAST_UPDATE, updateTime)
}
return forecasts
} catch (e: JSONException) {
CrashReporter.logException(e)
} catch (e: IOException) {
CrashReporter.logException(e)
}
return null
}
private fun isNight(timestamp: Long, lat: Double, lon: Double): Boolean { private fun isNight(timestamp: Long, lat: Double, lon: Double): Boolean {
val sunTimes = SunTimes.compute().on(Date(timestamp)).at(lat, lon).execute() val sunTimes = SunTimes.compute().on(Date(timestamp)).at(lat, lon).execute()
@ -201,10 +59,6 @@ class MetNoProvider(val context: Context) : WeatherProvider() {
} }
private fun isSnow(code: String): Boolean {
return code.contains("snow")
}
private fun conditionForCode(code: String): String { private fun conditionForCode(code: String): String {
return context.getString( return context.getString(
when (code.substringBefore("_")) { when (code.substringBefore("_")) {
@ -284,48 +138,6 @@ class MetNoProvider(val context: Context) : WeatherProvider() {
return getLastUpdate() + (1000 * 60 * 60) <= System.currentTimeMillis() return getLastUpdate() + (1000 * 60 * 60) <= System.currentTimeMillis()
} }
override fun getLastUpdate(): Long {
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.getLong(LAST_UPDATE, 0)
}
override suspend fun lookupLocation(query: String): List<Pair<Any?, String>> {
if (!Geocoder.isPresent()) return emptyList()
val geocoder = Geocoder(context)
val locations =
withContext(Dispatchers.IO) {
geocoder.getFromLocationName(query, 10)
}
return locations.mapNotNull {
(it.latitude to it.longitude) to it.formatToString()
}
}
/**
* locationId must be a Pair<Double, Double> with the latitude as first and longitude as second
* parameter
*/
override fun setLocation(locationId: Any?, locationName: String) {
if (locationId !is Pair<*, *>) return
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit {
putDouble(LAT, locationId.first as Double)
putDouble(LON, locationId.second as Double)
putString(LAST_LOCATION_NAME, locationName)
}
}
override fun getLastLocation(): String {
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.getString(LAST_LOCATION_NAME, "")!!
}
override fun resetLastUpdate() {
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.edit {
putLong(LAST_UPDATE, 0L)
}
}
private fun getUserAgent(): String? { private fun getUserAgent(): String? {
val contactData = getContactInfo() ?: return null val contactData = getContactInfo() ?: return null
@ -367,15 +179,87 @@ class MetNoProvider(val context: Context) : WeatherProvider() {
return context.resources.getIdentifier("metno_contact", "string", context.packageName) return context.resources.getIdentifier("metno_contact", "string", context.packageName)
} }
override suspend fun loadWeatherData(location: LatLonWeatherLocation): WeatherUpdateResult<LatLonWeatherLocation>? {
val lastUpdate = getLastUpdate()
val httpDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ROOT)
val ifModifiedSince = httpDateFormat.format(Date(lastUpdate))
try {
val forecasts = mutableListOf<Forecast>()
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT)
val httpClient = OkHttpClient()
val latParam = String.format(Locale.ROOT, "%.4f", location.lat)
val lonParam = String.format(Locale.ROOT, "%.4f", location.lon)
val forecastRequest = Request.Builder()
.url("https://api.met.no/weatherapi/locationforecast/2.0/?lat=$latParam&lon=$lonParam")
.addHeader("User-Agent", getUserAgent() ?: return null)
.addHeader("If-Modified-Since", ifModifiedSince)
.get()
.build()
val response = httpClient.newCall(forecastRequest).execute()
val responseBody = response.body?.string() ?: return null
val json = JSONObject(responseBody)
val properties = json.getJSONObject("properties")
val meta = properties.getJSONObject("meta")
val updatedAt = dateFormat.parse(meta.getString("updated_at"))?.time
?: System.currentTimeMillis()
val timeseries = properties.getJSONArray("timeseries")
for (i in 0 until timeseries.length()) {
val fc = timeseries.getJSONObject(i)
val data = fc.getJSONObject("data")
val timestamp = dateFormat.parse(fc.getString("time"))?.time ?: continue
val details = data.getJSONObject("instant").getJSONObject("details")
var hours = 0
val nextHours = data.optJSONObject("next_1_hours")?.also { hours = 1 }
?: data.optJSONObject("next_6_hours")?.also { hours = 6 }
?: data.optJSONObject("next_12_hours")?.also { hours = 12 }
?: continue
val symbolCode = nextHours.optJSONObject("summary")?.getString("symbol_code")
?: continue
val precipitationAmount =
(nextHours.optJSONObject("details")?.optDouble("precipitation_amount")
?: 0.0) / hours
forecasts.add(
Forecast(
timestamp = timestamp,
temperature = details.getDouble("air_temperature") + 273.15,
updateTime = updatedAt,
clouds = details.getDouble("cloud_area_fraction").roundToInt(),
humidity = details.getDouble("relative_humidity"),
windDirection = details.getDouble("wind_from_direction"),
windSpeed = details.getDouble("wind_speed"),
pressure = details.getDouble("air_pressure_at_sea_level"),
location = location.name,
provider = context.getString(R.string.provider_metno),
providerUrl = "https://www.yr.no/",
icon = iconForCode(symbolCode),
condition = conditionForCode(symbolCode),
precipitation = precipitationAmount,
night = isNight(timestamp, location.lat, location.lon)
)
)
}
return WeatherUpdateResult(
forecasts = forecasts,
location = location
)
} catch (e: JSONException) {
CrashReporter.logException(e)
} catch (e: IOException) {
CrashReporter.logException(e)
}
return null
}
companion object { companion object {
private const val PREFERENCES = "metno" private const val PREFERENCES = "metno"
private const val AUTO_LOCATION = "auto_location"
private const val LAST_UPDATE = "last_update"
private const val EXPIRES = "expires"
private const val LAT = "lat"
private const val LON = "lon"
private const val LAST_LAT = "last_lat"
private const val LAST_LON = "last_lon"
private const val LAST_LOCATION_NAME = "last_location_name"
} }
} }

View File

@ -1,48 +1,34 @@
package de.mm20.launcher2.weather.openweathermap package de.mm20.launcher2.weather.openweathermap
import android.Manifest
import android.content.Context import android.content.Context
import android.location.Location import android.content.SharedPreferences
import android.location.LocationManager
import android.util.Log
import androidx.core.content.edit import androidx.core.content.edit
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.checkPermission import de.mm20.launcher2.weather.*
import de.mm20.launcher2.weather.Forecast
import de.mm20.launcher2.weather.R
import de.mm20.launcher2.weather.WeatherProvider
import retrofit2.HttpException
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.io.IOException
import java.lang.Exception
import java.util.* import java.util.*
class OpenWeatherMapProvider(val context: Context) : WeatherProvider() { class OpenWeatherMapProvider(override val context: Context) :
WeatherProvider<OpenWeatherMapLocation>() {
val retrofit by lazy { private val retrofit by lazy {
Retrofit.Builder() Retrofit.Builder()
.baseUrl("https://api.openweathermap.org/data/2.5/") .baseUrl("https://api.openweathermap.org/data/2.5/")
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build() .build()
} }
val openWeatherMapService by lazy { private val openWeatherMapService by lazy {
retrofit.create(OpenWeatherMapApi::class.java) retrofit.create(OpenWeatherMapApi::class.java)
} }
override fun resetLastUpdate() {
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit {
putLong(LAST_UPDATE, 0)
}
}
override fun isUpdateRequired(): Boolean { override fun isUpdateRequired(): Boolean {
return getLastUpdate() + (1000 * 60 * 60) <= System.currentTimeMillis() return getLastUpdate() + (1000 * 60 * 60) <= System.currentTimeMillis()
} }
override suspend fun lookupLocation(query: String): List<Pair<Any?, String>> { override suspend fun lookupLocation(query: String): List<OpenWeatherMapLocation> {
val lang = Locale.getDefault().language val lang = Locale.getDefault().language
val response = try { val response = try {
@ -60,68 +46,37 @@ class OpenWeatherMapProvider(val context: Context) : WeatherProvider() {
val country = response.sys?.country ?: "" val country = response.sys?.country ?: ""
val cityId = response.id ?: return emptyList() val cityId = response.id ?: return emptyList()
val loc = "$city, $country" val loc = "$city, $country"
return listOf(cityId.toString() to loc) return listOf(
OpenWeatherMapLocation(
name = loc, id = cityId
)
)
} }
override fun setLocation(locationId: Any?, locationName: String) { override suspend fun loadWeatherData(location: OpenWeatherMapLocation): WeatherUpdateResult<OpenWeatherMapLocation>? {
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit { return fetchWeatherData(location = location)
putInt(CITY_ID, locationId as? Int ?: -1)
putString(LAST_LOCATION, locationName)
}
} }
override suspend fun loadWeatherData(
override var autoLocation: Boolean lat: Double,
get() { lon: Double
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) ): WeatherUpdateResult<OpenWeatherMapLocation>? {
.getBoolean(AUTO_LOCATION, true) return fetchWeatherData(lat = lat, lon = lon)
}
set(value) {
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.edit {
putBoolean(AUTO_LOCATION, value)
}
}
override val supportsAutoLocation: Boolean = true
override val supportsManualLocation: Boolean = true
override fun getLastUpdate(): Long {
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.getLong(LAST_UPDATE, 0)
} }
override fun getLastLocation(): String { private suspend fun fetchWeatherData(
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) lat: Double? = null,
.getString(LAST_LOCATION, "")!! lon: Double? = null,
} location: OpenWeatherMapLocation? = null
): WeatherUpdateResult<OpenWeatherMapLocation>? {
override suspend fun fetchNewWeatherData(): List<Forecast>? {
Log.d("MM20", "Updating weather data… (OpenWeatherMap)")
var cityId = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.getInt(CITY_ID, -1)
val lastCityId = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.getInt(LAST_CITY_ID, -1)
if (cityId == -1) cityId = lastCityId
val lm = context.getSystemService(
Context.LOCATION_SERVICE
) as LocationManager
var location: Location? = null
if (cityId == -1 && !context.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) {
Log.w("MM20", "Location permission is missing")
return null
}
if (context.checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION) && autoLocation) {
location = lm.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
}
val lang = Locale.getDefault().language val lang = Locale.getDefault().language
val currentWeather = try { val currentWeather = try {
openWeatherMapService.currentWeather( openWeatherMapService.currentWeather(
appid = getApiKey() ?: return null, appid = getApiKey() ?: return null,
id = cityId.takeIf { it != -1 && location == null }, id = location?.id?.takeIf { lat == null || lon == null },
lat = location?.latitude, lat = lat,
lon = location?.longitude, lon = lon,
lang = lang, lang = lang,
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -133,7 +88,7 @@ class OpenWeatherMapProvider(val context: Context) : WeatherProvider() {
val city = currentWeather.name val city = currentWeather.name
val country = currentWeather.sys?.country ?: return null val country = currentWeather.sys?.country ?: return null
cityId = currentWeather.id ?: return null val cityId = currentWeather.id ?: return null
val loc = "$city, $country" val loc = "$city, $country"
val forecasts = try { val forecasts = try {
@ -200,20 +155,15 @@ class OpenWeatherMapProvider(val context: Context) : WeatherProvider() {
) )
} }
) )
return WeatherUpdateResult(
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit { forecasts = forecastList,
putInt(CITY_ID, cityId) location = OpenWeatherMapLocation(
putInt(LAST_CITY_ID, cityId) name = loc,
putLong(LAST_UPDATE, System.currentTimeMillis()) id = cityId
putString(LAST_LOCATION, loc) )
} )
return forecastList
} }
private fun getApiKey(): String? { private fun getApiKey(): String? {
val resId = getApiKeyResId() val resId = getApiKeyResId()
if (resId != 0) return context.getString(resId) if (resId != 0) return context.getString(resId)
@ -258,12 +208,58 @@ class OpenWeatherMapProvider(val context: Context) : WeatherProvider() {
} }
} }
override val preferences: SharedPreferences
get() = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
override fun setLocation(location: WeatherLocation?) {
location as OpenWeatherMapLocation?
preferences.edit {
if (location == null) {
remove(CITY_ID)
remove(LOCATION)
} else {
putInt(CITY_ID, location.id)
putString(LOCATION, location.name)
}
}
}
override fun getLocation(): OpenWeatherMapLocation? {
val id = preferences.getInt(CITY_ID, -1).takeIf { it != -1 } ?: return null
val name = preferences.getString(LOCATION, null) ?: return null
return OpenWeatherMapLocation(
name = name,
id = id,
)
}
override fun getLastLocation(): OpenWeatherMapLocation? {
val id = preferences.getInt(LAST_CITY_ID, -1).takeIf { it != -1 } ?: return null
val name = preferences.getString(LAST_LOCATION, null) ?: return null
return OpenWeatherMapLocation(
name = name,
id = id,
)
}
override fun saveLastLocation(location: OpenWeatherMapLocation) {
preferences.edit {
putString(LAST_LOCATION, location.name)
putInt(LAST_CITY_ID, location.id)
}
}
companion object { companion object {
private const val PREFERENCES = "openweathermap" private const val PREFERENCES = "openweathermap"
private const val CITY_ID = "city_id" private const val CITY_ID = "city_id"
private const val LAST_CITY_ID = "last_city_id" private const val LAST_CITY_ID = "last_city_id"
private const val LAST_UPDATE = "last_update" private const val LAST_UPDATE = "last_update"
private const val LOCATION = "location"
private const val LAST_LOCATION = "last_location" private const val LAST_LOCATION = "last_location"
private const val AUTO_LOCATION = "auto_location"
} }
} }
data class OpenWeatherMapLocation(
override val name: String,
val id: Int
) : WeatherLocation