Put Pit Log under version control so the app and sync server can live on git.
This commit is contained in:
commit
1030bac79e
79 files changed
+8463
No files matched your search
@@ -0,0 +1,153 @@
|
||||
package com.racingapp.pitlog.model
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.os.Looper
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
data class LocalWeather(
|
||||
val airF: Int,
|
||||
val summary: String,
|
||||
)
|
||||
|
||||
class WeatherUnavailable(message: String) : Exception(message)
|
||||
|
||||
suspend fun readLocalWeather(context: Context): LocalWeather {
|
||||
val location = locate(context)
|
||||
val body = withContext(Dispatchers.IO) {
|
||||
fetchForecast(location.latitude, location.longitude)
|
||||
}
|
||||
return parseOpenMeteo(body)
|
||||
}
|
||||
|
||||
internal fun parseOpenMeteo(body: String): LocalWeather {
|
||||
val temp = numberAfter(body, "temperature_2m") ?: throw WeatherUnavailable("Couldn't read the weather here.")
|
||||
val code = numberAfter(body, "weather_code")?.toInt() ?: -1
|
||||
return LocalWeather(temp.roundToInt(), weatherWords(code))
|
||||
}
|
||||
|
||||
internal fun weatherWords(code: Int): String = when (code) {
|
||||
0 -> "Clear"
|
||||
1 -> "Mostly clear"
|
||||
2 -> "Partly cloudy"
|
||||
3 -> "Overcast"
|
||||
45, 48 -> "Fog"
|
||||
51, 53, 55, 56, 57 -> "Drizzle"
|
||||
61, 63, 65, 66, 67 -> "Rain"
|
||||
71, 73, 75, 77 -> "Snow"
|
||||
80, 81, 82 -> "Rain showers"
|
||||
85, 86 -> "Snow showers"
|
||||
95, 96, 99 -> "Thunderstorm"
|
||||
else -> "Weather"
|
||||
}
|
||||
|
||||
private fun numberAfter(body: String, key: String): Double? {
|
||||
val marker = "\"$key\""
|
||||
var search = 0
|
||||
while (search < body.length) {
|
||||
val start = body.indexOf(marker, search)
|
||||
if (start < 0) return null
|
||||
val colon = body.indexOf(':', start + marker.length)
|
||||
if (colon < 0) return null
|
||||
val slice = body.substring(colon + 1).trimStart()
|
||||
val end = slice.indexOfFirst { it != '-' && it != '.' && !it.isDigit() }
|
||||
val token = if (end < 0) slice else slice.substring(0, end)
|
||||
val number = token.toDoubleOrNull()
|
||||
if (number != null) return number
|
||||
search = colon + 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun fetchForecast(latitude: Double, longitude: Double): String {
|
||||
val url = URL(
|
||||
"https://api.open-meteo.com/v1/forecast" +
|
||||
"?latitude=$latitude&longitude=$longitude" +
|
||||
"¤t=temperature_2m,weather_code&temperature_unit=fahrenheit",
|
||||
)
|
||||
val connection = (url.openConnection() as HttpURLConnection).apply {
|
||||
connectTimeout = 8_000
|
||||
readTimeout = 8_000
|
||||
requestMethod = "GET"
|
||||
setRequestProperty("User-Agent", "PitLog")
|
||||
}
|
||||
try {
|
||||
if (connection.responseCode !in 200..299) {
|
||||
throw WeatherUnavailable("Couldn't read the weather here.")
|
||||
}
|
||||
return connection.inputStream.bufferedReader().use { it.readText() }
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun locate(context: Context): Location {
|
||||
val manager = context.getSystemService(LocationManager::class.java)
|
||||
?: throw WeatherUnavailable("Turn on location so the air temperature can be filled in.")
|
||||
val providers = locationProviders(manager)
|
||||
if (providers.isEmpty()) {
|
||||
throw WeatherUnavailable("Turn on location so the air temperature can be filled in.")
|
||||
}
|
||||
val recent = providers.firstNotNullOfOrNull { provider ->
|
||||
runCatching { manager.getLastKnownLocation(provider) }.getOrNull()
|
||||
}
|
||||
if (recent != null && System.currentTimeMillis() - recent.time < 30 * 60 * 1000) return recent
|
||||
for (provider in providers) {
|
||||
val fresh = try {
|
||||
requestLocation(manager, provider)
|
||||
} catch (_: SecurityException) {
|
||||
null
|
||||
}
|
||||
if (fresh != null) return fresh
|
||||
}
|
||||
return recent
|
||||
?: throw WeatherUnavailable("Turn on location so the air temperature can be filled in.")
|
||||
}
|
||||
|
||||
private fun locationProviders(manager: LocationManager): List<String> {
|
||||
val names = buildList {
|
||||
if (Build.VERSION.SDK_INT >= 31) add(LocationManager.FUSED_PROVIDER)
|
||||
add(LocationManager.NETWORK_PROVIDER)
|
||||
add(LocationManager.GPS_PROVIDER)
|
||||
}
|
||||
return names.filter { provider ->
|
||||
runCatching { manager.isProviderEnabled(provider) }.getOrDefault(false)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun requestLocation(manager: LocationManager, provider: String): Location? = withTimeoutOrNull(8_000) {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
val signal = CancellationSignal()
|
||||
cont.invokeOnCancellation { signal.cancel() }
|
||||
manager.getCurrentLocation(provider, signal, Runnable::run) { location ->
|
||||
if (cont.isActive) cont.resume(location)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
val listener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
manager.removeUpdates(this)
|
||||
if (cont.isActive) cont.resume(location)
|
||||
}
|
||||
}
|
||||
cont.invokeOnCancellation { manager.removeUpdates(listener) }
|
||||
manager.requestLocationUpdates(provider, 0L, 0f, listener, Looper.getMainLooper())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user