Refactor weather providers
This commit is contained in:
parent
e77901eb6a
commit
65d4047cf3
@ -13,6 +13,7 @@ import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||
import de.mm20.launcher2.R
|
||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||
import de.mm20.launcher2.preferences.WeatherProviders
|
||||
import de.mm20.launcher2.weather.WeatherLocation
|
||||
import de.mm20.launcher2.weather.WeatherProvider
|
||||
import de.mm20.launcher2.weather.WeatherViewModel
|
||||
import de.mm20.launcher2.weather.here.HereProvider
|
||||
@ -89,7 +90,7 @@ class PreferencesWeatherFragment : PreferenceFragmentCompat() {
|
||||
val provider = WeatherProvider.getInstance(requireContext())
|
||||
provider?.autoLocation = autoLocation
|
||||
provider?.resetLastUpdate()
|
||||
provider?.setLocation(null, "")
|
||||
provider?.setLocation(null)
|
||||
ViewModelProvider(this).get(WeatherViewModel::class.java)
|
||||
.requestUpdate(requireContext())
|
||||
updateProviderPreferences()
|
||||
@ -105,47 +106,28 @@ class PreferencesWeatherFragment : PreferenceFragmentCompat() {
|
||||
val unitsPref = findPreference<Preference>("imperial_units")!!
|
||||
val providerPref = findPreference<Preference>("weather_provider")!!
|
||||
|
||||
|
||||
|
||||
locationPref.parent?.isVisible = provider != null
|
||||
unitsPref.isVisible = provider != null
|
||||
|
||||
provider ?: return
|
||||
|
||||
providerPref.summary = provider.name
|
||||
|
||||
if (provider.supportsAutoLocation) {
|
||||
autoLocationPref.setSummary(R.string.preference_automatic_location_summary)
|
||||
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
|
||||
}
|
||||
autoLocationPref.isChecked = provider.autoLocation
|
||||
locationPref.summary =
|
||||
if (provider.autoLocation) provider.getLastLocation()?.name else provider.getLocation()?.name
|
||||
}
|
||||
|
||||
private fun onLookupCompleted(results: List<Pair<Any?, String>>) {
|
||||
private fun onLookupCompleted(results: List<WeatherLocation>) {
|
||||
MaterialDialog(requireContext())
|
||||
.listItems(
|
||||
items = results.map { it.second },
|
||||
items = results.map { it.name },
|
||||
waitForPositiveButton = false
|
||||
) { dialog, index, _ ->
|
||||
val provider = WeatherProvider.getInstance(requireContext())
|
||||
?: return@listItems dialog.dismiss()
|
||||
provider.resetLastUpdate()
|
||||
provider.setLocation(results[index].first, results[index].second)
|
||||
findPreference<Preference>("location")?.summary = results[index].second
|
||||
provider.setLocation(results[index])
|
||||
findPreference<Preference>("location")?.summary = results[index].name
|
||||
ViewModelProvider(this).get(WeatherViewModel::class.java)
|
||||
.requestUpdate(requireContext())
|
||||
dialog.dismiss()
|
||||
|
||||
@ -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
|
||||
@ -0,0 +1,5 @@
|
||||
package de.mm20.launcher2.weather
|
||||
|
||||
interface WeatherLocation {
|
||||
val name: String
|
||||
}
|
||||
@ -1,51 +1,118 @@
|
||||
package de.mm20.launcher2.weather
|
||||
|
||||
import android.Manifest
|
||||
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.WeatherProviders
|
||||
import de.mm20.launcher2.weather.here.HereProvider
|
||||
import de.mm20.launcher2.weather.metno.MetNoProvider
|
||||
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 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.
|
||||
* @param query the location to lookup
|
||||
* @return a list of Pair<Any?,String> with provider specific data of that location and its
|
||||
* display name
|
||||
* @return a list of locations
|
||||
*/
|
||||
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 val name: String
|
||||
|
||||
abstract fun getLastLocation(): T?
|
||||
|
||||
abstract fun saveLastLocation(location: T)
|
||||
|
||||
fun resetLastUpdate() {
|
||||
preferences.edit {
|
||||
putLong(LAST_UPDATE, 0L)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getInstance(context: Context): WeatherProvider? {
|
||||
fun getInstance(context: Context): WeatherProvider<out WeatherLocation>? {
|
||||
return when (LauncherPreferences.instance.weatherProvider) {
|
||||
WeatherProviders.OPENWEATHERMAP -> OpenWeatherMapProvider(context)
|
||||
WeatherProviders.HERE -> HereProvider(context)
|
||||
else -> MetNoProvider(context)
|
||||
}.takeIf { it.isAvailable() }
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getLastLocation(): String
|
||||
abstract fun resetLastUpdate()
|
||||
}
|
||||
private const val LAST_UPDATE = "last_update"
|
||||
private const val AUTO_LOCATION = "auto_location"
|
||||
}
|
||||
}
|
||||
|
||||
data class WeatherUpdateResult<T : WeatherLocation>(
|
||||
val forecasts: List<Forecast>,
|
||||
val location: T
|
||||
)
|
||||
@ -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
|
||||
}
|
||||
@ -1,117 +1,68 @@
|
||||
package de.mm20.launcher2.weather.here
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.location.LocationManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import android.content.SharedPreferences
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.checkPermission
|
||||
import de.mm20.launcher2.ktx.getDouble
|
||||
import de.mm20.launcher2.ktx.putDouble
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
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 de.mm20.launcher2.weather.*
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.create
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class HereProvider(val context: Context) : WeatherProvider() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
class HereProvider(override val context: Context) : LatLonWeatherProvider() {
|
||||
|
||||
override suspend fun fetchNewWeatherData(): List<Forecast>? {
|
||||
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
|
||||
private val retrofit by lazy {
|
||||
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()
|
||||
|
||||
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 dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.ROOT)
|
||||
|
||||
val forecastList = mutableListOf<Forecast>()
|
||||
|
||||
try {
|
||||
val httpClient = OkHttpClient()
|
||||
val apiKey = getApiKey() ?: return null
|
||||
|
||||
val forecastRequest = Request.Builder()
|
||||
.url("https://weather.ls.hereapi.com/weather/1.0/report.json?apiKey=${getApiKey()}&product=forecast_hourly&$query&language=$lang")
|
||||
.get()
|
||||
.build()
|
||||
val response = hereWeatherService.report(
|
||||
apiKey = apiKey,
|
||||
language = lang,
|
||||
latitude = lat,
|
||||
longitude = lon
|
||||
)
|
||||
|
||||
val body = withContext(Dispatchers.IO) {
|
||||
httpClient.newCall(forecastRequest).execute().body?.string()
|
||||
} ?: run {
|
||||
Log.e("MM20", "Here provider: forecast request returned null")
|
||||
return null
|
||||
}
|
||||
val forecastLocation = response.hourlyForecasts?.forecastLocation ?: return null
|
||||
val forecasts = forecastLocation.forecast ?: return null
|
||||
|
||||
val forecastLocation = JSONObject(body)
|
||||
.getJSONObject("hourlyForecasts")
|
||||
.getJSONObject("forecastLocation")
|
||||
val forecasts = forecastLocation.getJSONArray("forecast")
|
||||
val location = forecastLocation.city ?: return null
|
||||
|
||||
val location = forecastLocation.getString("city")
|
||||
val locationLong =
|
||||
"${forecastLocation.getString("city")}, ${forecastLocation.getString("country")}"
|
||||
|
||||
if (autoLocation) {
|
||||
prefs.edit {
|
||||
putString(LAST_LOCATION, locationLong)
|
||||
}
|
||||
}
|
||||
|
||||
for (i in 0 until forecasts.length()) {
|
||||
val forecast = forecasts.getJSONObject(i)
|
||||
for (forecast in forecasts) {
|
||||
|
||||
val timestamp = try {
|
||||
dateFormat.parse(forecast.getString("utcTime"))?.time ?: continue
|
||||
dateFormat.parse(forecast.utcTime ?: continue)?.time ?: continue
|
||||
} catch (e: ParseException) {
|
||||
CrashReporter.logException(e)
|
||||
return null
|
||||
@ -121,24 +72,20 @@ class HereProvider(val context: Context) : WeatherProvider() {
|
||||
if (timestamp + 1000 * 60 * 30 < System.currentTimeMillis()) continue
|
||||
|
||||
val condition = when {
|
||||
!forecast.optString("precipitationDesc")
|
||||
.isNullOrEmpty() -> forecast.optString("precipitationDesc")
|
||||
!forecast.optString("skyDescription")
|
||||
.isNullOrEmpty() -> forecast.optString("skyDescription")
|
||||
!forecast.optString("temperatureDesc")
|
||||
.isNullOrEmpty() -> forecast.optString("temperatureDesc")
|
||||
else -> forecast.optString("description")
|
||||
!forecast.precipitationDesc.isNullOrEmpty() -> forecast.precipitationDesc
|
||||
!forecast.skyDescription.isNullOrEmpty() -> forecast.skyDescription
|
||||
!forecast.temperatureDesc.isNullOrEmpty() -> forecast.temperatureDesc
|
||||
else -> forecast.description ?: continue
|
||||
}
|
||||
val humidity = forecast.getString("humidity").toIntOrNull() ?: 0
|
||||
val icon = getIcon(forecast.getString("iconName"))
|
||||
val night = forecast.getString("daylight") == "N"
|
||||
val rain = forecast.getString("rainFall").toDoubleOrNull() ?: 0.0
|
||||
val snow = forecast.getString("snowFall").toDoubleOrNull() ?: 0.0
|
||||
val rainPercent = forecast.getString("precipitationProbability").toIntOrNull() ?: 0
|
||||
val temperature = forecast.getString("temperature").toDoubleOrNull()?.plus(273.15)
|
||||
val humidity = forecast.humidity?.toIntOrNull() ?: 0
|
||||
val icon = getIcon(forecast.iconName ?: continue)
|
||||
val night = forecast.daylight == "N"
|
||||
val rain = forecast.rainFall?.toDoubleOrNull() ?: 0.0
|
||||
val rainPercent = forecast.precipitationProbability?.toIntOrNull() ?: 0
|
||||
val temperature = forecast.temperature?.toDoubleOrNull()?.plus(273.15)
|
||||
?: 0.0
|
||||
val windDir = forecast.getString("windDirection").toIntOrNull() ?: 0
|
||||
val windSpeed = forecast.getString("windSpeed").toDoubleOrNull() ?: 0.0
|
||||
val windDir = forecast.windDirection?.toIntOrNull() ?: 0
|
||||
val windSpeed = forecast.windSpeed?.toDoubleOrNull() ?: 0.0
|
||||
|
||||
forecastList.add(
|
||||
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)
|
||||
return null
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
putLong(LAST_UPDATE, updateTime)
|
||||
}
|
||||
|
||||
return forecastList
|
||||
}
|
||||
|
||||
|
||||
private fun getIcon(iconName: String): Int {
|
||||
with(Forecast) {
|
||||
return when (iconName) {
|
||||
@ -334,62 +285,29 @@ class HereProvider(val context: Context) : WeatherProvider() {
|
||||
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>> {
|
||||
val urlString =
|
||||
"https://geocoder.ls.hereapi.com/6.2/geocode.json?apiKey=${getApiKey()}&searchtext=$query"
|
||||
val client = OkHttpClient()
|
||||
val request = Request.Builder()
|
||||
.url(urlString)
|
||||
override suspend fun lookupLocation(query: String): List<LatLonWeatherLocation> {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl("https://geocoder.ls.hereapi.com/6.2/")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
val geocodeService = retrofit.create<HereGeocodeApi>()
|
||||
try {
|
||||
val body = withContext(Dispatchers.IO) {
|
||||
val response = client.newCall(request).execute()
|
||||
response.body?.string()
|
||||
} ?: return emptyList()
|
||||
val json = JSONObject(body)
|
||||
val results = json
|
||||
.optJSONObject("Response")
|
||||
?.optJSONArray("View")
|
||||
?.optJSONObject(0)
|
||||
?.optJSONArray("Result") ?: return emptyList()
|
||||
val locations = mutableListOf<Pair<Any?, String>>()
|
||||
for (i in 0 until results.length()) {
|
||||
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) {
|
||||
val apiKey = getApiKey() ?: return emptyList()
|
||||
val response = geocodeService.geocode(apiKey, query)
|
||||
|
||||
return response.Response.View?.getOrNull(0)?.Result?.mapNotNull {
|
||||
LatLonWeatherLocation(
|
||||
name = it.Location?.Address?.Label ?: return@mapNotNull null,
|
||||
lat = it.Location.DisplayPosition?.Latitude ?: return@mapNotNull null,
|
||||
lon = it.Location.DisplayPosition.Longitude ?: return@mapNotNull null,
|
||||
)
|
||||
} ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
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? {
|
||||
val resId = getApiKeyResId()
|
||||
if (resId != 0) return context.getString(resId)
|
||||
@ -410,11 +328,5 @@ class HereProvider(val context: Context) : WeatherProvider() {
|
||||
|
||||
companion object {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -2,26 +2,23 @@ package de.mm20.launcher2.weather.metno
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Geocoder
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.checkPermission
|
||||
import de.mm20.launcher2.ktx.formatToString
|
||||
import de.mm20.launcher2.ktx.getDouble
|
||||
import de.mm20.launcher2.ktx.putDouble
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
import de.mm20.launcher2.weather.R
|
||||
import de.mm20.launcher2.weather.WeatherProvider
|
||||
import de.mm20.launcher2.weather.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.internal.userAgent
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.shredzone.commons.suncalc.SunTimes
|
||||
@ -31,149 +28,10 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MetNoProvider(val context: Context) : WeatherProvider() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
class MetNoProvider(override val context: Context) : LatLonWeatherProvider() {
|
||||
|
||||
override suspend fun fetchNewWeatherData(): List<Forecast>? {
|
||||
var lat: Double? = null
|
||||
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
|
||||
}
|
||||
override val preferences: SharedPreferences
|
||||
get() = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
|
||||
|
||||
private fun isNight(timestamp: Long, lat: Double, lon: Double): Boolean {
|
||||
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 {
|
||||
return context.getString(
|
||||
when (code.substringBefore("_")) {
|
||||
@ -284,48 +138,6 @@ class MetNoProvider(val context: Context) : WeatherProvider() {
|
||||
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? {
|
||||
val contactData = getContactInfo() ?: return null
|
||||
|
||||
@ -367,15 +179,87 @@ class MetNoProvider(val context: Context) : WeatherProvider() {
|
||||
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 {
|
||||
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"
|
||||
}
|
||||
}
|
||||
@ -1,48 +1,34 @@
|
||||
package de.mm20.launcher2.weather.openweathermap
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.util.Log
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.checkPermission
|
||||
import de.mm20.launcher2.weather.Forecast
|
||||
import de.mm20.launcher2.weather.R
|
||||
import de.mm20.launcher2.weather.WeatherProvider
|
||||
import retrofit2.HttpException
|
||||
import de.mm20.launcher2.weather.*
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
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()
|
||||
.baseUrl("https://api.openweathermap.org/data/2.5/")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
val openWeatherMapService by lazy {
|
||||
private val openWeatherMapService by lazy {
|
||||
retrofit.create(OpenWeatherMapApi::class.java)
|
||||
}
|
||||
|
||||
override fun resetLastUpdate() {
|
||||
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit {
|
||||
putLong(LAST_UPDATE, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isUpdateRequired(): Boolean {
|
||||
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 response = try {
|
||||
@ -60,68 +46,37 @@ class OpenWeatherMapProvider(val context: Context) : WeatherProvider() {
|
||||
val country = response.sys?.country ?: ""
|
||||
val cityId = response.id ?: return emptyList()
|
||||
val loc = "$city, $country"
|
||||
return listOf(cityId.toString() to loc)
|
||||
return listOf(
|
||||
OpenWeatherMapLocation(
|
||||
name = loc, id = cityId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun setLocation(locationId: Any?, locationName: String) {
|
||||
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit {
|
||||
putInt(CITY_ID, locationId as? Int ?: -1)
|
||||
putString(LAST_LOCATION, locationName)
|
||||
}
|
||||
override suspend fun loadWeatherData(location: OpenWeatherMapLocation): WeatherUpdateResult<OpenWeatherMapLocation>? {
|
||||
return fetchWeatherData(location = location)
|
||||
}
|
||||
|
||||
|
||||
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 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 suspend fun loadWeatherData(
|
||||
lat: Double,
|
||||
lon: Double
|
||||
): WeatherUpdateResult<OpenWeatherMapLocation>? {
|
||||
return fetchWeatherData(lat = lat, lon = lon)
|
||||
}
|
||||
|
||||
override fun getLastLocation(): String {
|
||||
return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
|
||||
.getString(LAST_LOCATION, "")!!
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
private suspend fun fetchWeatherData(
|
||||
lat: Double? = null,
|
||||
lon: Double? = null,
|
||||
location: OpenWeatherMapLocation? = null
|
||||
): WeatherUpdateResult<OpenWeatherMapLocation>? {
|
||||
val lang = Locale.getDefault().language
|
||||
|
||||
val currentWeather = try {
|
||||
openWeatherMapService.currentWeather(
|
||||
appid = getApiKey() ?: return null,
|
||||
id = cityId.takeIf { it != -1 && location == null },
|
||||
lat = location?.latitude,
|
||||
lon = location?.longitude,
|
||||
id = location?.id?.takeIf { lat == null || lon == null },
|
||||
lat = lat,
|
||||
lon = lon,
|
||||
lang = lang,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
@ -133,7 +88,7 @@ class OpenWeatherMapProvider(val context: Context) : WeatherProvider() {
|
||||
|
||||
val city = currentWeather.name
|
||||
val country = currentWeather.sys?.country ?: return null
|
||||
cityId = currentWeather.id ?: return null
|
||||
val cityId = currentWeather.id ?: return null
|
||||
val loc = "$city, $country"
|
||||
|
||||
val forecasts = try {
|
||||
@ -200,20 +155,15 @@ class OpenWeatherMapProvider(val context: Context) : WeatherProvider() {
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit {
|
||||
putInt(CITY_ID, cityId)
|
||||
putInt(LAST_CITY_ID, cityId)
|
||||
putLong(LAST_UPDATE, System.currentTimeMillis())
|
||||
putString(LAST_LOCATION, loc)
|
||||
}
|
||||
|
||||
return forecastList
|
||||
|
||||
|
||||
return WeatherUpdateResult(
|
||||
forecasts = forecastList,
|
||||
location = OpenWeatherMapLocation(
|
||||
name = loc,
|
||||
id = cityId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun getApiKey(): String? {
|
||||
val resId = getApiKeyResId()
|
||||
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 {
|
||||
private const val PREFERENCES = "openweathermap"
|
||||
private const val CITY_ID = "city_id"
|
||||
private const val LAST_CITY_ID = "last_city_id"
|
||||
private const val LAST_UPDATE = "last_update"
|
||||
private const val LOCATION = "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
|
||||
Loading…
x
Reference in New Issue
Block a user