mirror of
https://github.com/PranshulGG/WeatherMaster.git
synced 2026-10-22 02:52:37 -05:00
weather alerts
This commit is contained in:
1 parent
c2757ea4b5
commit
f36498c5c2
14 files changed
+482
No files matched your search
@@ -0,0 +1,15 @@
|
||||
package com.pranshulgg.weather_master_app.core.model.domain.alerts
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity
|
||||
|
||||
|
||||
data class Alert(
|
||||
val locationId: String,
|
||||
val event: String,
|
||||
val severity: AlertSeverity?,
|
||||
val effective: Long,
|
||||
val expires: Long,
|
||||
val description: String,
|
||||
val source: String?,
|
||||
val lastUpdatedInMilli: Long
|
||||
)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.pranshulgg.weather_master_app.core.model.weather.alerts
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert
|
||||
|
||||
|
||||
sealed class AlertResult {
|
||||
data class Success(val alerts: List<Alert>) : AlertResult()
|
||||
data class Error(val exception: Exception, val cacheAlerts: List<Alert> = emptyList()) :
|
||||
AlertResult()
|
||||
}
|
||||
|
||||
enum class AlertResultType {
|
||||
RETURN_CACHE,
|
||||
ERROR
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.pranshulgg.weather_master_app.core.model.weather.alerts
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.pranshulgg.weather_master_app.R
|
||||
|
||||
enum class AlertSeverity(
|
||||
val label: Int,
|
||||
val color: Color
|
||||
) {
|
||||
CRITICAL(
|
||||
R.string.alert_severity_critical,
|
||||
Color(0xFFD32F2F)
|
||||
),
|
||||
HIGH(
|
||||
R.string.alert_severity_high,
|
||||
Color(0xFFF57C00)
|
||||
),
|
||||
MODERATE(
|
||||
R.string.alert_severity_medium,
|
||||
Color(0xFFFBC02D)
|
||||
),
|
||||
LOW(
|
||||
R.string.alert_severity_low,
|
||||
Color(0xFF388E3C)
|
||||
),
|
||||
UNKNOWN(
|
||||
R.string.alert_severity_unknown,
|
||||
Color(0xFF757575)
|
||||
)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.pranshulgg.weather_master_app.core.network.sources.alerts.accu
|
||||
|
||||
import com.pranshulgg.weather_master_app.BuildConfig
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.AccuAqiForecastJson
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.json.AccuAqiJson
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.json.AlertsAccuJson
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
interface AlertsAccuApi {
|
||||
|
||||
@GET("alerts/v1/{locationKey}")
|
||||
suspend fun fetchAlerts(
|
||||
@Path("locationKey") locationKey: String,
|
||||
@Query("details") details: Boolean = true
|
||||
): Response<List<AlertsAccuJson>>
|
||||
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://api.accuweather.com/"
|
||||
|
||||
fun create(): AlertsAccuApi {
|
||||
|
||||
|
||||
val auth = Interceptor { chain ->
|
||||
val original = chain.request()
|
||||
|
||||
val newUrl =
|
||||
original.url.newBuilder().addQueryParameter("apikey", BuildConfig.ACCU_KEY)
|
||||
.build()
|
||||
val request = original.newBuilder().url(newUrl).build()
|
||||
|
||||
chain.proceed(request)
|
||||
}
|
||||
|
||||
val client = OkHttpClient.Builder()
|
||||
.addInterceptor(auth)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(AlertsAccuApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.pranshulgg.weather_master_app.core.network.sources.alerts.accu
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.AppException
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResult
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResultType
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuApi
|
||||
import com.pranshulgg.weather_master_app.core.utils.weather.cache.shouldReturnAlertsCache
|
||||
import com.pranshulgg.weather_master_app.data.local.dao.airquality.accu.AccuDao
|
||||
import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao
|
||||
import com.pranshulgg.weather_master_app.data.local.entity.airquality.accu.AccuEntity
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.alerts.sources.accu.toDomain
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toDomain
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toEntity
|
||||
import com.pranshulgg.weather_master_app.data.local.mapper.weather.sources.accu.toDomain
|
||||
import com.pranshulgg.weather_master_app.data.repository.AlertRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class AlertsAccuRepository @Inject constructor(
|
||||
private val api: AlertsAccuApi,
|
||||
private val dao: AlertsDao,
|
||||
private val accuDao: AccuDao,
|
||||
private val accuApi: AccuApi,
|
||||
) : AlertRepository {
|
||||
override suspend fun getAlerts(
|
||||
location: Location,
|
||||
isManualRefresh: Boolean,
|
||||
isForceRefresh: Boolean
|
||||
): AlertResult = withContext(Dispatchers.IO) {
|
||||
|
||||
val cache = dao.getAlertsForLocation(location.id)
|
||||
val shouldReturnCache = shouldReturnAlertsCache(cache, isManualRefresh, isForceRefresh)
|
||||
|
||||
when (shouldReturnCache) {
|
||||
AlertResultType.RETURN_CACHE -> return@withContext AlertResult.Success(cache.map { it!!.toDomain() })
|
||||
else -> {}
|
||||
}
|
||||
|
||||
return@withContext try {
|
||||
val locationKey = accuDao.getCityKeyForLocation(location.id)?.toDomain()?.cityKey
|
||||
?: accuApi.getLocationKey("${location.latitude},${location.longitude}").body()?.key
|
||||
?: return@withContext AlertResult.Error(exception = AppException.Unknown())
|
||||
|
||||
val response = api.fetchAlerts(locationKey)
|
||||
val body = response.body()
|
||||
?: return@withContext AlertResult.Error(exception = AppException.Unknown())
|
||||
|
||||
val domain = body.map { it.toDomain(location.id) }
|
||||
|
||||
accuDao.insertCityKey(
|
||||
AccuEntity(
|
||||
locationId = location.id,
|
||||
cityKey = locationKey
|
||||
)
|
||||
)
|
||||
dao.insertAlerts(
|
||||
domain.map { it.toEntity(location.id) },
|
||||
location.id
|
||||
)
|
||||
|
||||
AlertResult.Success(domain)
|
||||
|
||||
} catch (e: Exception) {
|
||||
AlertResult.Error(exception = e, cacheAlerts = cache.map { it!!.toDomain() })
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.json
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
data class AlertsAccuJson(
|
||||
@SerializedName("Description") val event: AlertsAccuEventJson,
|
||||
@SerializedName("AlarmLevel") val alarmLevel: String?,
|
||||
@SerializedName("Source") val source: String?,
|
||||
@SerializedName("Area") val area: List<AlertsAccuAreaJson>,
|
||||
)
|
||||
|
||||
data class AlertsAccuEventJson(
|
||||
@SerializedName("English") val english: String,
|
||||
@SerializedName("Localized") val localized: String
|
||||
)
|
||||
|
||||
data class AlertsAccuAreaJson(
|
||||
@SerializedName("EpochStartTime") val epochStartTimeSeconds: Long,
|
||||
@SerializedName("EpochEndTime") val epochEndTimeSeconds: Long,
|
||||
@SerializedName("Text") val text: String,
|
||||
|
||||
)
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
package com.pranshulgg.weather_master_app.core.utils.weather.cache
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResultType
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResultType
|
||||
import com.pranshulgg.weather_master_app.core.utils.weather.cache.CacheConfig.AUTO_REFRESH_MAX_MINUTES
|
||||
import com.pranshulgg.weather_master_app.core.utils.weather.cache.CacheConfig.MANUAL_REFRESH_MINUTES
|
||||
import com.pranshulgg.weather_master_app.data.local.entity.airquality.AirQualityWithRelations
|
||||
import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
fun shouldReturnAlertsCache(
|
||||
cache: List<AlertEntity?>,
|
||||
isManualRefresh: Boolean,
|
||||
isForceRefresh: Boolean
|
||||
): AlertResultType {
|
||||
|
||||
if (isForceRefresh) return AlertResultType.ERROR
|
||||
|
||||
|
||||
if (cache.isEmpty() || cache[0] == null) {
|
||||
return AlertResultType.ERROR
|
||||
}
|
||||
|
||||
|
||||
val cacheMilli = cache[0]!!.lastUpdatedInMilli
|
||||
val ageMillis = System.currentTimeMillis() - cacheMilli
|
||||
val ageMinutes = TimeUnit.MILLISECONDS.toMinutes(ageMillis)
|
||||
|
||||
val tooEarly = isManualRefresh && ageMinutes < MANUAL_REFRESH_MINUTES
|
||||
val maxAge = if (isManualRefresh) MANUAL_REFRESH_MINUTES else AUTO_REFRESH_MAX_MINUTES
|
||||
|
||||
if (tooEarly) return AlertResultType.RETURN_CACHE
|
||||
|
||||
val shouldReturnCache = ageMinutes < maxAge
|
||||
|
||||
return if (shouldReturnCache) AlertResultType.RETURN_CACHE else AlertResultType.ERROR
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.pranshulgg.weather_master_app.data.local.dao.alerts
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import com.pranshulgg.weather_master_app.data.local.entity.airquality.CurrentAirQualityEntity
|
||||
import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity
|
||||
|
||||
|
||||
@Dao
|
||||
interface AlertsDao {
|
||||
|
||||
@Transaction
|
||||
suspend fun insertAlerts(alerts: List<AlertEntity>, locationId: String) {
|
||||
deleteAlertsForLocation(locationId)
|
||||
insertAlertList(alerts)
|
||||
}
|
||||
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
|
||||
suspend fun insertAlertList(alerts: List<AlertEntity>)
|
||||
|
||||
@Query("SELECT * FROM alerts WHERE locationId = :locationId")
|
||||
suspend fun getAlertsForLocation(locationId: String): List<AlertEntity?>
|
||||
|
||||
@Query("DELETE FROM alerts WHERE locationId = :locationId")
|
||||
suspend fun deleteAlertsForLocation(locationId: String)
|
||||
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.pranshulgg.weather_master_app.data.local.entity.alerts
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity
|
||||
import com.pranshulgg.weather_master_app.data.local.entity.location.WeatherLocationEntity
|
||||
|
||||
|
||||
@Entity(
|
||||
tableName = "alerts",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = WeatherLocationEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["locationId"],
|
||||
onDelete = ForeignKey.Companion.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [Index("locationId")],
|
||||
)
|
||||
data class AlertEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val locationId: String,
|
||||
|
||||
val event: String,
|
||||
val severity: AlertSeverity?,
|
||||
val effective: Long,
|
||||
val expires: Long,
|
||||
val description: String,
|
||||
val source: String?,
|
||||
val lastUpdatedInMilli: Long
|
||||
)
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.pranshulgg.weather_master_app.data.local.mapper.alerts
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert
|
||||
import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity
|
||||
|
||||
|
||||
fun Alert.toEntity(locationId: String): AlertEntity {
|
||||
return AlertEntity(
|
||||
locationId = locationId,
|
||||
event = event,
|
||||
severity = severity,
|
||||
effective = effective,
|
||||
expires = expires,
|
||||
description = description,
|
||||
source = source,
|
||||
lastUpdatedInMilli = lastUpdatedInMilli
|
||||
)
|
||||
}
|
||||
|
||||
fun AlertEntity.toDomain(): Alert {
|
||||
return Alert(
|
||||
locationId = locationId,
|
||||
event = event,
|
||||
severity = severity,
|
||||
effective = effective,
|
||||
expires = expires,
|
||||
description = description,
|
||||
source = source,
|
||||
lastUpdatedInMilli = lastUpdatedInMilli
|
||||
)
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.pranshulgg.weather_master_app.data.local.mapper.alerts.sources.accu
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.json.AlertsAccuJson
|
||||
import com.pranshulgg.weather_master_app.core.utils.extensions.DateTimeExtensions.secondsToMilliseconds
|
||||
|
||||
|
||||
fun AlertsAccuJson.toDomain(locationId: String): Alert {
|
||||
return Alert(
|
||||
locationId = locationId,
|
||||
event = event.english,
|
||||
severity = getSeverity(alarmLevel),
|
||||
effective = area[0].epochStartTimeSeconds.secondsToMilliseconds(),
|
||||
expires = area[0].epochEndTimeSeconds.secondsToMilliseconds(),
|
||||
description = area[0].text,
|
||||
source = source,
|
||||
lastUpdatedInMilli = System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSeverity(severity: String?): AlertSeverity {
|
||||
return when (severity) {
|
||||
"Red" -> AlertSeverity.CRITICAL
|
||||
"Orange" -> AlertSeverity.HIGH
|
||||
"Yellow" -> AlertSeverity.MODERATE
|
||||
else -> AlertSeverity.UNKNOWN
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.pranshulgg.weather_master_app.data.provider
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
|
||||
import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuRepository
|
||||
import com.pranshulgg.weather_master_app.data.repository.AlertRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class AlertsRepositoryProvider @Inject constructor(
|
||||
private val alertsAccuRepository: AlertsAccuRepository
|
||||
) {
|
||||
|
||||
fun getRepository(source: AlertSource): AlertRepository? {
|
||||
return when (source) {
|
||||
AlertSource.ACCU_WEATHER -> alertsAccuRepository
|
||||
AlertSource.WEATHER_API -> alertsAccuRepository
|
||||
AlertSource.NONE -> null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.pranshulgg.weather_master_app.data.repository
|
||||
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResult
|
||||
|
||||
interface AlertRepository {
|
||||
suspend fun getAlerts(
|
||||
location: Location,
|
||||
isManualRefresh: Boolean = false,
|
||||
isForceRefresh: Boolean
|
||||
): AlertResult
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.pranshulgg.weather_master_app.feature.main.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pranshulgg.weather_master_app.R
|
||||
import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert
|
||||
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertSeverity
|
||||
import com.pranshulgg.weather_master_app.core.prefs.AppPrefs
|
||||
import com.pranshulgg.weather_master_app.core.prefs.AppPrefsState
|
||||
import com.pranshulgg.weather_master_app.core.ui.components.SettingSection
|
||||
import com.pranshulgg.weather_master_app.core.ui.components.Symbol
|
||||
import com.pranshulgg.weather_master_app.core.ui.theme.ShadowElevation
|
||||
import com.pranshulgg.weather_master_app.core.utils.formatters.safeZoneId
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
fun AlertsSection(alerts: List<Alert>, prefs: AppPrefsState, zoneId: String) {
|
||||
|
||||
val pattern = if (prefs.is24HrTimeFormat) "MMM dd, HH:mm" else "MMM dd, hh:mm a"
|
||||
|
||||
|
||||
val formatter: (Long) -> String = {
|
||||
val formatter = DateTimeFormatter.ofPattern(pattern)
|
||||
val instant = Instant.ofEpochMilli(it)
|
||||
val dateTime = instant.atZone(safeZoneId(zoneId)).toLocalDateTime()
|
||||
formatter.format(dateTime)
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
shadowElevation = ShadowElevation.level2
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
alerts.forEach {
|
||||
ListItem(
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
),
|
||||
leadingContent = {
|
||||
Symbol(
|
||||
R.drawable.warning_24px,
|
||||
color = it.severity?.color ?: AlertSeverity.UNKNOWN.color,
|
||||
size = 32.dp
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(it.event) },
|
||||
supportingContent = {
|
||||
Text("${formatter(it.effective)} • ${formatter(it.expires)}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user