refactor: update source architecture to support multiple capabilities from the same API

This commit is contained in:
PranshulGG committed 2026-08-19 16:33:53 +05:30
1 parent b30c4e7418
commit 9c901ef2cd
38 files changed
+708 -664

No files matched your search

@@ -2,9 +2,6 @@ package com.pranshulgg.weather_master_app.core.di.network
import com.pranshulgg.weather_master_app.core.network.github.GithubApi
import com.pranshulgg.weather_master_app.core.network.sources.address.nominatim.NominatimApi
import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiApi
import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiApi
import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuApi
import com.pranshulgg.weather_master_app.core.network.sources.alerts.fpas.FpasApi
import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApi
import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.WmoSevereWeatherApi
@@ -29,6 +26,7 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.metnorway.
import com.pranshulgg.weather_master_app.core.network.sources.weather.metoffice.MetOfficeApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.nws.NwsApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.openmeteo.OpenMeteoApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.openmeteo.airquality.OpenMeteoAqiApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.pirateweather.PirateWeatherApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.smhi.SmhiApi
import dagger.Module
@@ -113,14 +111,6 @@ object ApiModule {
@Singleton
fun provideMeteoamApi(): MeteoamApi = MeteoamApi.create()
@Provides
@Singleton
fun provideAccuAqiApi(): AccuAqiApi = AccuAqiApi.create()
@Provides
@Singleton
fun provideAlertsAccuApi(): AlertsAccuApi = AlertsAccuApi.create()
@Provides
@Singleton
fun provideAlertsWeatherApi(): AlertsWeatherApi = AlertsWeatherApi.create()
@@ -1,17 +1,6 @@
package com.pranshulgg.weather_master_app.core.di.weather
import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiApi
import com.pranshulgg.weather_master_app.core.network.sources.airquality.accu.AccuAqiRepository
import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiApi
import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiRepository
import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuApi
import com.pranshulgg.weather_master_app.core.network.sources.alerts.accu.AlertsAccuRepository
import com.pranshulgg.weather_master_app.core.network.sources.alerts.fpas.FpasApi
import com.pranshulgg.weather_master_app.core.network.sources.alerts.fpas.FpasRepository
import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApi
import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApiRepository
import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.WmoSevereWeatherApi
import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.WmoSevereWeatherRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.openmeteo.airquality.OpenMeteoAqiApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuApi
import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.aemet.AemetApi
@@ -71,16 +60,11 @@ object WeatherRepositoryModule {
fun provideOpenMeteoRepository(
dao: LocationsDao,
api: OpenMeteoApi,
weatherDao: WeatherDao
): OpenMeteoRepository = OpenMeteoRepository(dao, weatherDao, api)
weatherDao: WeatherDao,
airQualityDao: AirQualityDao,
airQualityApi: OpenMeteoAqiApi
): OpenMeteoRepository = OpenMeteoRepository(dao, weatherDao, api, airQualityApi, airQualityDao)
@Provides
@Singleton
fun provideOpenMeteoAqiRepository(
api: OpenMeteoAqiApi,
dao: AirQualityDao
): OpenMeteoAqiRepository =
OpenMeteoAqiRepository(api, dao)
@Provides
@Singleton
@@ -161,8 +145,19 @@ object WeatherRepositoryModule {
dao: LocationsDao,
api: AccuApi,
weatherDao: WeatherDao,
locationKeysDao: LocationKeysDao
): AccuRepository = AccuRepository(dao, weatherDao, api, locationKeysDao)
locationKeysDao: LocationKeysDao,
airQualityDao: AirQualityDao,
alertsDao: AlertsDao,
locationsDao: LocationsDao
): AccuRepository = AccuRepository(
dao,
weatherDao,
api,
locationKeysDao,
airQualityDao,
alertsDao,
locationsDao
)
@Provides
@Singleton
@@ -172,33 +167,6 @@ object WeatherRepositoryModule {
weatherDao: WeatherDao
): MeteoamRepository = MeteoamRepository(dao, weatherDao, api)
@Provides
@Singleton
fun provideAccuAqiRepository(
api: AccuAqiApi,
dao: AirQualityDao,
locationKeysDao: LocationKeysDao,
accuApi: AccuApi
): AccuAqiRepository = AccuAqiRepository(api, dao, locationKeysDao, accuApi)
@Provides
@Singleton
fun provideAccuAlertsRepository(
api: AlertsAccuApi,
dao: AlertsDao,
locationKeysDao: LocationKeysDao,
accuApi: AccuApi,
locationsDao: LocationsDao
): AlertsAccuRepository = AlertsAccuRepository(api, dao, locationKeysDao, accuApi, locationsDao)
@Provides
@Singleton
fun provideWeatherApiAlertsRepository(
api: AlertsWeatherApi,
dao: AlertsDao,
locationsDao: LocationsDao
): AlertsWeatherApiRepository = AlertsWeatherApiRepository(api, dao, locationsDao)
@Provides
@Singleton
fun provideIpmaRepository(
@@ -208,21 +176,6 @@ object WeatherRepositoryModule {
locationKeysDao: LocationKeysDao
): IpmaRepository = IpmaRepository(dao, weatherDao, api, locationKeysDao)
@Provides
@Singleton
fun provideWmoSevereWeatherAlertsRepository(
api: WmoSevereWeatherApi,
dao: AlertsDao,
locationsDao: LocationsDao
): WmoSevereWeatherRepository = WmoSevereWeatherRepository(api, dao, locationsDao)
@Provides
@Singleton
fun provideFpasAlertsRepository(
api: FpasApi,
dao: AlertsDao,
locationsDao: LocationsDao
): FpasRepository = FpasRepository(api, dao, locationsDao)
@Provides
@Singleton
@@ -266,8 +219,10 @@ object WeatherRepositoryModule {
dao: LocationsDao,
api: PirateWeatherApi,
weatherDao: WeatherDao,
apiKeysDao: ApiKeysDao
): PirateWeatherRepository = PirateWeatherRepository(dao, weatherDao, api, apiKeysDao)
apiKeysDao: ApiKeysDao,
alertsDao: AlertsDao
): PirateWeatherRepository =
PirateWeatherRepository(dao, weatherDao, api, apiKeysDao, alertsDao)
@Provides
@Singleton
@@ -1,8 +1,6 @@
package com.pranshulgg.weather_master_app.core.model.domain.location
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.openmeteo.OpenMeteoModel
data class Location(
@@ -14,13 +12,13 @@ data class Location(
val timezone: String,
val countryCode: String?,
val state: String,
val source: WeatherSource = WeatherSource.OPEN_METEO,
val source: Source = Source.OPEN_METEO,
val isFavorite: Boolean = false,
val isPinned: Boolean = false,
val isDefault: Boolean,
val isDeviceLocation: Boolean = false,
val alertSource: AlertSource = AlertSource.NONE,
val airQualitySource: AirQualitySource = AirQualitySource.OPEN_METEO,
val alertSource: Source = Source.NONE,
val airQualitySource: Source = Source.OPEN_METEO,
val customName: String? = null,
val openMeteoModel: OpenMeteoModel = OpenMeteoModel.BEST_MATCH,
val alertsLastFetchedAt: Long? = null
@@ -0,0 +1,252 @@
package com.pranshulgg.weather_master_app.core.model.sources
import com.pranshulgg.weather_master_app.R
// TODO: implement alerts / air quality for sources that support it
enum class Source(
val displayName: String,
val hourlyAggregationLimitHours: Int = 24, // stupid and should be removed
val displayLink: String,
val fullName: String,
val countryNameRes: Int? = null,
val requiresUserApiKey: Boolean = false, // Source must not be selectable until the user has provided their API key
val regionalButWorldwideSupport: Boolean = false,
val capabilities: Set<Capability>,
// val providesAlerts: Boolean = false,
// val providesAirQuality: Boolean = false,
) {
OPEN_METEO(
displayName = "Open Meteo",
fullName = "Open Meteo",
displayLink = "https://open-meteo.com/",
capabilities = setOf(Capability.WEATHER, Capability.AIR_QUALITY)
),
NWS(
displayName = "NWS",
fullName = "National Weather Service",
displayLink = "https://www.weather.gov/documentation/services-web-api",
countryNameRes = R.string.country_usa,
capabilities = setOf(Capability.WEATHER)
),
SMHI(
displayName = "SMHI",
fullName = "Swedish Meteorological and Hydrological Institute",
displayLink = "https://opendata.smhi.se",
countryNameRes = R.string.country_sweden,
capabilities = setOf(Capability.WEATHER)
),
DWD(
displayName = "DWD",
fullName = "Bright Sky DWD",
displayLink = "https://brightsky.dev",
countryNameRes = R.string.country_germany,
capabilities = setOf(Capability.WEATHER)
),
METEO_FRANCE(
displayName = "Météo-France",
fullName = "Météo-France",
displayLink = "https://meteofrance.com/",
regionalButWorldwideSupport = true,
capabilities = setOf(Capability.WEATHER)
),
ECCC(
displayName = "ECCC",
fullName = "Environment and Climate Change Canada",
displayLink = "https://app.weather.gc.ca/",
countryNameRes = R.string.country_canada,
capabilities = setOf(Capability.WEATHER)
),
FMI(
displayName = "FMI",
fullName = "Finnish Meteorological Institute",
displayLink = "https://en.ilmatieteenlaitos.fi/",
countryNameRes = R.string.country_finland,
capabilities = setOf(Capability.WEATHER)
),
CHINA(
displayName = "China",
fullName = "China National Environmental Monitoring Centre",
displayLink = "https://www.cnemc.cn/",
capabilities = setOf(Capability.WEATHER)
),
BMKG(
displayName = "BMKG",
fullName = "Badan Meteorologi, Klimatologi, dan Geofisika",
displayLink = "https://www.bmkg.go.id/",
countryNameRes = R.string.country_indonesia,
capabilities = setOf(Capability.WEATHER)
),
ACCU_WEATHER(
displayName = "AccuWeather",
fullName = "AccuWeather",
displayLink = "https://www.accuweather.com/",
capabilities = setOf(Capability.WEATHER, Capability.ALERTS, Capability.AIR_QUALITY)
),
METEO_AM(
displayName = "Meteo AM",
fullName = "Meteorologia Aeronautica Militare",
displayLink = "https://www.meteoam.it/",
countryNameRes = R.string.country_italy,
capabilities = setOf(Capability.WEATHER)
),
IPMA(
displayName = "IPMA",
fullName = "Instituto Português do Mar e da Atmosfera",
displayLink = "https://api.ipma.pt/",
countryNameRes = R.string.country_portugal,
capabilities = setOf(Capability.WEATHER)
),
GISMETEO(
displayName = "Gismeteo",
fullName = "Gismeteo",
displayLink = "https://www.gismeteo.ru/",
countryNameRes = R.string.country_russia,
regionalButWorldwideSupport = true,
capabilities = setOf(Capability.WEATHER)
),
MET_OFFICE(
displayName = "Met Office",
fullName = "Meteorological Office",
displayLink = "https://www.metoffice.gov.uk/",
countryNameRes = R.string.country_united_kingdom,
requiresUserApiKey = true,
regionalButWorldwideSupport = true,
capabilities = setOf(Capability.WEATHER)
),
PIRATE_WEATHER(
displayName = "Pirate Weather",
fullName = "Pirate Weather",
displayLink = "https://pirateweather.net/",
requiresUserApiKey = true,
regionalButWorldwideSupport = false,
capabilities = setOf(Capability.WEATHER, Capability.ALERTS)
),
IMD(
displayName = "IMD",
fullName = "India Meteorological Department",
displayLink = "https://mausam.imd.gov.in/",
countryNameRes = R.string.country_india,
capabilities = setOf(Capability.WEATHER)
),
MET_NORWAY(
displayName = "Met Norway",
fullName = "Met Norway",
displayLink = "https://api.met.no/",
regionalButWorldwideSupport = true,
capabilities = setOf(Capability.WEATHER)
),
AEMET(
displayName = "AEMET",
fullName = "Agencia Estatal de Meteorología",
displayLink = "https://opendata.aemet.es/centrodedescargas/altaUsuario",
countryNameRes = R.string.country_spain,
requiresUserApiKey = true,
capabilities = setOf(Capability.WEATHER)
),
WEATHER_API(
displayName = "WeatherApi",
fullName = "WeatherApi.com",
displayLink = "https://www.weatherapi.com/",
capabilities = setOf(Capability.ALERTS)
),
WMO_SEVERE_WEATHER(
displayName = "WMO Severe Weather",
fullName = "WMO Severe Weather Information Centre",
displayLink = "https://www.wmo.int/",
capabilities = setOf(Capability.ALERTS)
),
FPAS(
displayName = "FOSS Public Alert Server",
fullName = "FOSS Public Alert Server",
displayLink = "https://invent.kde.org/webapps/foss-public-alert-server",
capabilities = setOf(Capability.ALERTS)
),
NONE(
displayName = "None",
fullName = "",
displayLink = "",
capabilities = setOf(Capability.ALERTS, Capability.AIR_QUALITY)
),
CWA(
displayName = "CWA",
fullName = "Central Weather Administration",
displayLink = "https://opendata.cwa.gov.tw/",
countryNameRes = R.string.country_taiwan,
requiresUserApiKey = true,
capabilities = setOf(Capability.WEATHER)
);
// Sources that provide snow/rain as precipitation
fun providesSnowFall(): Boolean {
return when (this) {
MET_NORWAY -> false
DWD -> false
CHINA -> false
BMKG -> false
IMD -> false
CWA -> false
else -> true
}
}
}
// WE MAP EVERY WEATHER SOURCE HERE, AS THEY GET ADDED
private val sourcesByCountry = buildMap {
put("US", listOf(Source.NWS))
put("SE", listOf(Source.SMHI))
put("DE", listOf(Source.DWD))
put("CA", listOf(Source.ECCC))
put("FI", listOf(Source.FMI))
put("CN", listOf(Source.CHINA))
put("ID", listOf(Source.BMKG))
listOf("IT", "VA").forEach { put(it, listOf(Source.METEO_AM)) }
put("PT", listOf(Source.IPMA))
put("RU", listOf(Source.GISMETEO))
listOf("GB", "UK").forEach { put(it, listOf(Source.MET_OFFICE)) }
put("NO", listOf(Source.MET_NORWAY))
put("FR", listOf(Source.METEO_FRANCE))
put("ES", listOf(Source.AEMET))
put("IN", listOf(Source.IMD))
put("TW", listOf(Source.CWA))
}
fun getSourcesForCountry(countryCode: String?): List<Source> {
return sourcesByCountry[countryCode] ?: emptyList()
}
// GLOBAL SOURCES
private val sourcesGlobal = listOf(
Source.OPEN_METEO,
Source.MET_NORWAY,
Source.METEO_FRANCE,
Source.ACCU_WEATHER,
Source.GISMETEO,
Source.MET_OFFICE,
Source.PIRATE_WEATHER
)
fun getSourcesGlobal(): List<Source> {
return sourcesGlobal
}
fun Source.isSourceSupportedFor(countryCode: String?): Boolean {
val source = sourcesByCountry[countryCode] ?: return false
return this in source
}
fun Source.isGlobal(): Boolean {
return this in sourcesGlobal
}
enum class Capability {
WEATHER,
ALERTS,
AIR_QUALITY
}
@@ -1,198 +0,0 @@
package com.pranshulgg.weather_master_app.core.model.sources
import android.content.Context
import com.pranshulgg.weather_master_app.R
enum class WeatherSource(
val displayName: String,
val hourlyAggregationLimitHours: Int = 24, // stupid and should be removed
val displayLink: String,
val fullName: String,
val countryNameRes: Int? = null,
val requiresUserApiKey: Boolean = false, // Source must not be selectable until the user has provided their API key
val regionalButWorldwideSupport: Boolean = false
) {
OPEN_METEO(
displayName = "Open Meteo",
fullName = "Open Meteo",
displayLink = "https://open-meteo.com/"
),
NWS(
displayName = "NWS",
fullName = "National Weather Service",
displayLink = "https://www.weather.gov/documentation/services-web-api",
countryNameRes = R.string.country_usa
),
SMHI(
displayName = "SMHI",
fullName = "Swedish Meteorological and Hydrological Institute",
displayLink = "https://opendata.smhi.se",
countryNameRes = R.string.country_sweden
),
DWD(
displayName = "DWD",
fullName = "Bright Sky DWD",
displayLink = "https://brightsky.dev",
countryNameRes = R.string.country_germany
),
METEO_FRANCE(
displayName = "Météo-France",
fullName = "Météo-France",
displayLink = "https://meteofrance.com/",
regionalButWorldwideSupport = true
),
ECCC(
displayName = "ECCC",
fullName = "Environment and Climate Change Canada",
displayLink = "https://app.weather.gc.ca/",
countryNameRes = R.string.country_canada
),
FMI(
displayName = "FMI",
fullName = "Finnish Meteorological Institute",
displayLink = "https://en.ilmatieteenlaitos.fi/",
countryNameRes = R.string.country_finland
),
CHINA(
displayName = "China",
fullName = "China National Environmental Monitoring Centre",
displayLink = "https://www.cnemc.cn/"
),
BMKG(
displayName = "BMKG",
fullName = "Badan Meteorologi, Klimatologi, dan Geofisika",
displayLink = "https://www.bmkg.go.id/",
countryNameRes = R.string.country_indonesia
),
ACCU_WEATHER(
displayName = "AccuWeather",
fullName = "AccuWeather",
displayLink = "https://www.accuweather.com/"
),
METEO_AM(
displayName = "Meteo AM",
fullName = "Meteorologia Aeronautica Militare",
displayLink = "https://www.meteoam.it/",
countryNameRes = R.string.country_italy
),
IPMA(
displayName = "IPMA",
fullName = "Instituto Português do Mar e da Atmosfera",
displayLink = "https://api.ipma.pt/",
countryNameRes = R.string.country_portugal
),
GISMETEO(
displayName = "Gismeteo",
fullName = "Gismeteo",
displayLink = "https://www.gismeteo.ru/",
countryNameRes = R.string.country_russia,
regionalButWorldwideSupport = true
),
MET_OFFICE(
displayName = "Met Office",
fullName = "Meteorological Office",
displayLink = "https://www.metoffice.gov.uk/",
countryNameRes = R.string.country_united_kingdom,
requiresUserApiKey = true,
regionalButWorldwideSupport = true
),
PIRATE_WEATHER(
displayName = "Pirate Weather",
fullName = "Pirate Weather",
displayLink = "https://pirateweather.net/",
requiresUserApiKey = true,
regionalButWorldwideSupport = false
),
IMD(
displayName = "IMD",
fullName = "India Meteorological Department",
displayLink = "https://mausam.imd.gov.in/",
countryNameRes = R.string.country_india
),
MET_NORWAY(
displayName = "Met Norway",
fullName = "Met Norway",
displayLink = "https://api.met.no/",
regionalButWorldwideSupport = true
),
AEMET(
displayName = "AEMET",
fullName = "Agencia Estatal de Meteorología",
displayLink = "https://opendata.aemet.es/centrodedescargas/altaUsuario",
countryNameRes = R.string.country_spain,
requiresUserApiKey = true
),
CWA(
displayName = "CWA",
fullName = "Central Weather Administration",
displayLink = "https://opendata.cwa.gov.tw/",
countryNameRes = R.string.country_taiwan,
requiresUserApiKey = true
);
// Sources that provide snow/rain as precipitation
fun providesSnowFall(): Boolean {
return when (this) {
MET_NORWAY -> false
DWD -> false
CHINA -> false
BMKG -> false
IMD -> false
CWA -> false
else -> true
}
}
}
// WE MAP EVERY WEATHER SOURCE HERE, AS THEY GET ADDED
private val weatherSourcesByCountry = buildMap {
put("US", listOf(WeatherSource.NWS))
put("SE", listOf(WeatherSource.SMHI))
put("DE", listOf(WeatherSource.DWD))
put("CA", listOf(WeatherSource.ECCC))
put("FI", listOf(WeatherSource.FMI))
put("CN", listOf(WeatherSource.CHINA))
put("ID", listOf(WeatherSource.BMKG))
listOf("IT", "VA").forEach { put(it, listOf(WeatherSource.METEO_AM)) }
put("PT", listOf(WeatherSource.IPMA))
put("RU", listOf(WeatherSource.GISMETEO))
listOf("GB", "UK").forEach { put(it, listOf(WeatherSource.MET_OFFICE)) }
put("NO", listOf(WeatherSource.MET_NORWAY))
put("FR", listOf(WeatherSource.METEO_FRANCE))
put("ES", listOf(WeatherSource.AEMET))
put("IN", listOf(WeatherSource.IMD))
put("TW", listOf(WeatherSource.CWA))
}
fun getWeatherSourcesForCountry(countryCode: String?): List<WeatherSource> {
return weatherSourcesByCountry[countryCode] ?: emptyList()
}
// GLOBAL SOURCES
private val weatherSourcesGlobal = listOf(
WeatherSource.OPEN_METEO,
WeatherSource.MET_NORWAY,
WeatherSource.METEO_FRANCE,
WeatherSource.ACCU_WEATHER,
WeatherSource.GISMETEO,
WeatherSource.MET_OFFICE,
WeatherSource.PIRATE_WEATHER
)
fun getWeatherSourcesGlobal(): List<WeatherSource> {
return weatherSourcesGlobal
}
fun WeatherSource.isSourceSupportedFor(countryCode: String?): Boolean {
val source = weatherSourcesByCountry[countryCode] ?: return false
return this in source
}
fun WeatherSource.isGlobal(): Boolean {
return this in weatherSourcesGlobal
}
@@ -1,12 +1,16 @@
package com.pranshulgg.weather_master_app.core.model.weather
import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert
import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather
sealed class WeatherResult {
data class Success(val weather: Weather) : WeatherResult()
data class Success(val weather: Weather, val alerts: List<Alert> = emptyList()) :
WeatherResult()
data object RefreshNotAvailable : WeatherResult()
data class Error(val exception: Exception, val cacheWeather: Weather? = null) : WeatherResult()
}
enum class WeatherResultType {
@@ -2,7 +2,7 @@ package com.pranshulgg.weather_master_app.core.network.sources.search.geonames
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.data.local.mapper.locations.toDomain
import com.pranshulgg.weather_master_app.data.repository.SearchRepository
import com.pranshulgg.weather_master_app.data.repository.data.SearchRepository
import javax.inject.Inject
@@ -3,7 +3,7 @@ package com.pranshulgg.weather_master_app.core.utils.weather.forecast
import android.util.Log
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherDaily
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherHourly
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.utils.formatters.safeZoneId
import java.time.Instant
import java.time.ZoneId
@@ -34,7 +34,7 @@ fun findMatchingDaily(
fun findMatchingHourly(
data: List<WeatherHourly>,
currentMilli: Long,
source: WeatherSource,
source: Source,
zoneId: String,
alwaysReturn24Hrs: Boolean = false,
keepPastHour: Boolean = false
@@ -47,7 +47,7 @@ import com.pranshulgg.weather_master_app.data.local.entity.weather.units.AppWeat
AlertEntity::class,
ApiKeyEntity::class
],
version = 56,
version = 57,
autoMigrations = [
AutoMigration(from = 39, to = 40),
AutoMigration(from = 42, to = 43),
@@ -59,7 +59,8 @@ import com.pranshulgg.weather_master_app.data.local.entity.weather.units.AppWeat
AutoMigration(from = 50, to = 51),
AutoMigration(from = 52, to = 53),
AutoMigration(from = 53, to = 54),
AutoMigration(from = 54, to = 55)
AutoMigration(from = 54, to = 55),
AutoMigration(from = 56, to = 57)
]
)
abstract class WeatherMasterDatabase : RoomDatabase() {
@@ -5,11 +5,8 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.openmeteo.OpenMeteoModel
import com.pranshulgg.weather_master_app.data.local.entity.alerts.AlertEntity
import com.pranshulgg.weather_master_app.data.local.entity.location.WeatherLocationEntity
import com.pranshulgg.weather_master_app.data.local.entity.weather.WeatherWithRelations
import kotlinx.coroutines.flow.Flow
@@ -63,7 +60,7 @@ interface LocationsDao {
fun getAllLocationsCurrentWeather(): Flow<List<WeatherWithRelations>>
@Query("UPDATE weather_locations SET source = :source WHERE id = :id")
suspend fun updateSourceForLocation(id: String, source: WeatherSource)
suspend fun updateSourceForLocation(id: String, source: Source)
@Query("DELETE FROM weather_daily WHERE locationId = :id")
suspend fun deleteDailyDataForLocation(id: String)
@@ -81,10 +78,10 @@ interface LocationsDao {
suspend fun getLocationForId(id: String): WeatherLocationEntity
@Query("UPDATE weather_locations SET airQualitySource = :source WHERE id = :id")
suspend fun updateAirQualitySourceForLocation(id: String, source: AirQualitySource)
suspend fun updateAirQualitySourceForLocation(id: String, source: Source)
@Query("UPDATE weather_locations SET alertSource = :source WHERE id = :id")
suspend fun updateAlertSourceForLocation(id: String, source: AlertSource)
suspend fun updateAlertSourceForLocation(id: String, source: Source)
@Query("UPDATE weather_locations SET customName = :name WHERE id = :id")
@@ -5,8 +5,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.data.local.entity.weather.ApiKeyEntity
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.data.local.entity.weather.CurrentWeatherEntity
import com.pranshulgg.weather_master_app.data.local.entity.weather.DailyWeatherEntity
import com.pranshulgg.weather_master_app.data.local.entity.weather.HourlyWeatherEntity
@@ -54,7 +53,7 @@ interface WeatherDao {
)
suspend fun getHourlyDataForLocation(
locationId: String,
source: WeatherSource
source: Source
): List<HourlyWeatherEntity>
}
@@ -3,9 +3,7 @@ package com.pranshulgg.weather_master_app.data.local.entity.location
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.openmeteo.OpenMeteoModel
import kotlin.uuid.ExperimentalUuidApi
@@ -20,15 +18,15 @@ data class WeatherLocationEntity(
val lat: Double,
val lon: Double,
val timezone: String,
val source: WeatherSource,
val source: Source,
val state: String? = null,
val isFavorite: Boolean = false,
val isPinned: Boolean = false,
val countryCode: String? = null,
val isDefault: Boolean = false,
val isDeviceLocation: Boolean = false,
val alertSource: AlertSource,
val airQualitySource: AirQualitySource,
val alertSource: Source,
val airQualitySource: Source,
val customName: String? = null,
@ColumnInfo(name = "openMeteoModel", defaultValue = "BEST_MATCH")
@@ -1,10 +1,9 @@
package com.pranshulgg.weather_master_app.data.local.entity.weather
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.WeatherCondition
import com.pranshulgg.weather_master_app.core.model.weather.wind.WindDirection
import com.pranshulgg.weather_master_app.data.local.entity.location.WeatherLocationEntity
@@ -39,5 +38,5 @@ data class HourlyWeatherEntity(
val time: Long,
val precipitationProbability: Int?,
val cachedSource: WeatherSource? = null
val cachedSource: Source? = null
)
@@ -4,7 +4,7 @@ import com.pranshulgg.weather_master_app.core.model.sources.SearchSource
import com.pranshulgg.weather_master_app.core.network.sources.search.accu.AccuSearchRepository
import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.GeoNamesSearchRepository
import com.pranshulgg.weather_master_app.core.network.sources.search.openmeteo.OpenMeteoSearchRepository
import com.pranshulgg.weather_master_app.data.repository.SearchRepository
import com.pranshulgg.weather_master_app.data.repository.data.SearchRepository
import javax.inject.Inject
class SearchRepositoryProvider @Inject constructor(
@@ -1,6 +1,9 @@
package com.pranshulgg.weather_master_app.data.provider
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.network.sources.alerts.fpas.FpasRepository
import com.pranshulgg.weather_master_app.core.network.sources.alerts.weatherapi.AlertsWeatherApiRepository
import com.pranshulgg.weather_master_app.core.network.sources.alerts.wmosevereweather.WmoSevereWeatherRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.accu.AccuRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.aemet.AemetRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.bmkg.BmkgRepository
@@ -20,10 +23,12 @@ import com.pranshulgg.weather_master_app.core.network.sources.weather.nws.NwsRep
import com.pranshulgg.weather_master_app.core.network.sources.weather.openmeteo.OpenMeteoRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.pirateweather.PirateWeatherRepository
import com.pranshulgg.weather_master_app.core.network.sources.weather.smhi.SmhiRepository
import com.pranshulgg.weather_master_app.data.repository.WeatherRepository
import com.pranshulgg.weather_master_app.data.repository.data.AirQualityRepository
import com.pranshulgg.weather_master_app.data.repository.data.AlertRepository
import com.pranshulgg.weather_master_app.data.repository.data.WeatherRepository
import javax.inject.Inject
class WeatherRepositoryProvider @Inject constructor(
class SourceRepositoryProvider @Inject constructor(
private val openMeteoRepository: OpenMeteoRepository,
private val nwsRepository: NwsRepository,
private val metNorwayRepository: MetNorwayRepository,
@@ -42,30 +47,55 @@ class WeatherRepositoryProvider @Inject constructor(
private val aemetRepository: AemetRepository,
private val imdRepository: ImdRepository,
private val pirateWeatherRepository: PirateWeatherRepository,
private val cwaRepository: CwaRepository
) {
private val cwaRepository: CwaRepository,
fun getRepository(source: WeatherSource): WeatherRepository {
return when (source) {
WeatherSource.OPEN_METEO -> openMeteoRepository
WeatherSource.NWS -> nwsRepository
WeatherSource.MET_NORWAY -> metNorwayRepository
WeatherSource.SMHI -> smhiRepository
WeatherSource.DWD -> dwdRepository
WeatherSource.METEO_FRANCE -> meteoFranceRepository
WeatherSource.ECCC -> ecccRepository
WeatherSource.FMI -> fmiRepository
WeatherSource.CHINA -> chinaRepository
WeatherSource.BMKG -> bmkgRepository
WeatherSource.ACCU_WEATHER -> accuRepository
WeatherSource.METEO_AM -> meteoamRepository
WeatherSource.IPMA -> ipmaRepository
WeatherSource.GISMETEO -> gismeteoRepository
WeatherSource.MET_OFFICE -> metOfficeRepository
WeatherSource.AEMET -> aemetRepository
WeatherSource.IMD -> imdRepository
WeatherSource.PIRATE_WEATHER -> pirateWeatherRepository
WeatherSource.CWA -> cwaRepository
// ALERTS
private val alertsWeatherApiRepository: AlertsWeatherApiRepository,
private val wmoSevereWeatherRepository: WmoSevereWeatherRepository,
private val fpasRepository: FpasRepository,
) {
val repositories = listOf(
openMeteoRepository,
nwsRepository,
metNorwayRepository,
smhiRepository,
dwdRepository,
meteoFranceRepository,
ecccRepository,
fmiRepository,
chinaRepository,
bmkgRepository,
accuRepository,
meteoamRepository,
ipmaRepository,
gismeteoRepository,
metOfficeRepository,
aemetRepository,
imdRepository,
pirateWeatherRepository,
cwaRepository,
alertsWeatherApiRepository,
wmoSevereWeatherRepository,
fpasRepository,
)
fun getWeatherRepository(source: Source): WeatherRepository {
return repositories.filterIsInstance<WeatherRepository>().firstOrNull {
it.weatherSource == source
} ?: openMeteoRepository
}
fun getAlertRepository(source: Source): AlertRepository? {
return repositories.filterIsInstance<AlertRepository>().firstOrNull {
it.alertSource == source
}
}
fun getAirQualityRepository(source: Source): AirQualityRepository? {
return repositories.filterIsInstance<AirQualityRepository>().firstOrNull {
it.airQualitySource == source
}
}
@@ -4,18 +4,14 @@ import android.content.Context
import androidx.room.Transaction
import com.pranshulgg.weather_master_app.core.model.domain.AppException
import com.pranshulgg.weather_master_app.core.model.domain.airquality.AirQuality
import com.pranshulgg.weather_master_app.core.model.domain.alerts.Alert
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.openmeteo.OpenMeteoModel
import com.pranshulgg.weather_master_app.core.network.sources.address.nominatim.json.NominatimRepository
import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao
import com.pranshulgg.weather_master_app.data.local.dao.location.LocationsDao
import com.pranshulgg.weather_master_app.data.local.mapper.airquality.toDomain
import com.pranshulgg.weather_master_app.data.local.mapper.alerts.toDomain
import com.pranshulgg.weather_master_app.data.local.mapper.locations.toDomain
import com.pranshulgg.weather_master_app.data.local.mapper.locations.toEntity
import com.pranshulgg.weather_master_app.data.local.mapper.weather.toDomain
@@ -72,7 +68,7 @@ class LocationsRepository @Inject constructor(
}
suspend fun updateSourceForLocation(id: String, source: WeatherSource) {
suspend fun updateSourceForLocation(id: String, source: Source) {
dao.updateSourceForLocation(id, source)
}
@@ -80,11 +76,11 @@ class LocationsRepository @Inject constructor(
dao.updateOpenMeteoModelForLocation(id, model)
}
suspend fun updateAirQualitySourceForLocation(id: String, source: AirQualitySource) {
suspend fun updateAirQualitySourceForLocation(id: String, source: Source) {
dao.updateAirQualitySourceForLocation(id, source)
}
suspend fun updateAlertSourceForLocation(id: String, source: AlertSource) {
suspend fun updateAlertSourceForLocation(id: String, source: Source) {
dao.updateAlertSourceForLocation(id, source)
}
@@ -1,8 +1,7 @@
package com.pranshulgg.weather_master_app.data.repository
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.data.local.dao.airquality.AirQualityDao
import com.pranshulgg.weather_master_app.data.local.dao.alerts.AlertsDao
import com.pranshulgg.weather_master_app.data.local.dao.location.LocationKeysDao
@@ -25,55 +24,85 @@ class WeatherDataReconcilerRepository @Inject constructor(
* NWS might have saved grid points
* which are important to be removed from the DB so they don't end up stale
*/
suspend fun cleanUpStaleData(
previousSource: WeatherSource,
suspend fun reconcileSourceChange(
previous: Location,
updated: Location
) {
if (previous.source != updated.source) {
cleanUpStaleWeatherData(
previousSource = previous.source,
locationId = previous.id,
airQualitySource = updated.airQualitySource,
currentAlertSource = updated.alertSource
)
}
if (previous.airQualitySource != updated.airQualitySource) {
cleanUpStaleAirQualityData(
locationId = previous.id,
currentWeatherSource = updated.source,
currentAlertSource = updated.alertSource
)
}
if (previous.alertSource != updated.alertSource) {
cleanUpStaleAlertsData(
locationId = previous.id,
currentWeatherSource = updated.source,
airQualitySource = updated.airQualitySource
)
}
}
private suspend fun cleanUpStaleWeatherData(
previousSource: Source,
locationId: String,
airQualitySource: AirQualitySource,
currentAlertSource: AlertSource
airQualitySource: Source,
currentAlertSource: Source
) {
when (previousSource) {
WeatherSource.NWS -> nwsDao.deleteGridPointsForLocation(locationId)
WeatherSource.ACCU_WEATHER -> cleanAccuWeather(
Source.NWS -> nwsDao.deleteGridPointsForLocation(locationId)
Source.ACCU_WEATHER -> cleanAccuWeather(
locationId,
airQualitySource,
currentAlertSource
)
WeatherSource.IPMA -> locationKeysDao.getCityKeyForLocation(locationId)
else -> {}
}
}
private suspend fun cleanAccuWeather(
locationId: String, airQualitySource: AirQualitySource,
currentAlertSource: AlertSource
locationId: String, airQualitySource: Source,
currentAlertSource: Source
) {
if (airQualitySource != AirQualitySource.ACCU_WEATHER || currentAlertSource != AlertSource.ACCU_WEATHER) {
if (airQualitySource != Source.ACCU_WEATHER || currentAlertSource != Source.ACCU_WEATHER) {
locationKeysDao.deleteCityKeyForLocation(locationId)
}
}
suspend fun cleanUpStaleAirQualityData(
private suspend fun cleanUpStaleAirQualityData(
locationId: String,
currentWeatherSource: WeatherSource,
currentAlertSource: AlertSource
currentWeatherSource: Source,
currentAlertSource: Source
) {
airQualityDao.deleteCurrentAirQuality(locationId)
airQualityDao.deleteHourlyAirQuality(locationId)
if (currentWeatherSource != WeatherSource.ACCU_WEATHER || currentAlertSource != AlertSource.ACCU_WEATHER) {
if (currentWeatherSource != Source.ACCU_WEATHER || currentAlertSource != Source.ACCU_WEATHER) {
locationKeysDao.deleteCityKeyForLocation(locationId)
}
}
suspend fun cleanUpStaleAlertsData(
private suspend fun cleanUpStaleAlertsData(
locationId: String,
currentWeatherSource: WeatherSource,
airQualitySource: AirQualitySource,
currentWeatherSource: Source,
airQualitySource: Source,
) {
alertsDao.deleteAlertsForLocation(locationId)
if (currentWeatherSource != WeatherSource.ACCU_WEATHER || airQualitySource != AirQualitySource.ACCU_WEATHER) {
if (currentWeatherSource != Source.ACCU_WEATHER || airQualitySource != Source.ACCU_WEATHER) {
locationKeysDao.deleteCityKeyForLocation(locationId)
}
}
@@ -1,13 +0,0 @@
package com.pranshulgg.weather_master_app.data.repository
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
interface WeatherRepository {
suspend fun getWeather(
location: Location,
isManualRefresh: Boolean = false,
isForceRefresh: Boolean
): WeatherResult
}
@@ -1,8 +1,8 @@
package com.pranshulgg.weather_master_app.data.repository
package com.pranshulgg.weather_master_app.data.repository.data
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
interface SearchRepository {
suspend fun search(query: String): List<Location>
}
}
@@ -0,0 +1,105 @@
package com.pranshulgg.weather_master_app.data.repository.data
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult
import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResult
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResult
import com.pranshulgg.weather_master_app.data.provider.SourceRepositoryProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class SourceDataRepository @Inject constructor(
private val sourceRepositoryProvider: SourceRepositoryProvider
) {
suspend fun getData(
location: Location,
isManualRefresh: Boolean = false,
isForceRefresh: Boolean = false,
isForceRefreshForAirQuality: Boolean = false,
isForceRefreshForAlerts: Boolean = false,
onWeather: suspend (WeatherResult) -> Unit,
onAlerts: suspend (AlertResult) -> Unit,
onAirQuality: suspend (AirQualityResult) -> Unit,
) = coroutineScope {
val weatherSource = location.source
val alertSource = location.alertSource
val airQualitySource = location.airQualitySource
val weatherRepo = sourceRepositoryProvider.getWeatherRepository(weatherSource)
val weatherJob = async {
weatherRepo.getWeather(
location,
isManualRefresh,
isForceRefresh
)
}
if (alertSource == weatherSource && weatherRepo.providesAlerts) {
launch {
weatherJob.await()
val repo = sourceRepositoryProvider.getAlertRepository(alertSource)
if (repo != null) {
onAlerts(repo.getAlerts(location = location))
}
}
} else {
launch {
val repo = sourceRepositoryProvider.getAlertRepository(alertSource)
if (repo != null) {
onAlerts(
repo.getAlerts(
location = location,
isManualRefresh = isManualRefresh,
isForceRefresh = isForceRefreshForAlerts
)
)
}
}
}
if (airQualitySource == weatherSource && weatherRepo.providesAirQuality) {
launch {
weatherJob.await()
val repo = sourceRepositoryProvider.getAirQualityRepository(airQualitySource)
if (repo != null) {
onAirQuality(repo.getAirQuality(location = location))
}
}
} else {
launch {
val repo = sourceRepositoryProvider.getAirQualityRepository(airQualitySource)
if (repo != null) {
onAirQuality(
repo.getAirQuality(
location = location,
isManualRefresh = isManualRefresh,
isForceRefresh = isForceRefreshForAirQuality
)
)
}
}
}
launch {
onWeather(weatherJob.await())
}
}
}
@@ -0,0 +1,24 @@
package com.pranshulgg.weather_master_app.data.repository.data
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult
interface WeatherRepository {
val weatherSource: Source
// Keep them true if air quality or alerts are provided along with the weather response
// rather than a separate API endpoint
val providesAlerts: Boolean
get() = false
val providesAirQuality: Boolean
get() = false
suspend fun getWeather(
location: Location,
isManualRefresh: Boolean = false,
isForceRefresh: Boolean
): WeatherResult
}
@@ -2,8 +2,6 @@ package com.pranshulgg.weather_master_app.data.worker
import android.Manifest
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
@@ -12,7 +10,7 @@ import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherUnits
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult
import com.pranshulgg.weather_master_app.core.prefs.helper.PreferencesHelper
import com.pranshulgg.weather_master_app.data.provider.WeatherRepositoryProvider
import com.pranshulgg.weather_master_app.data.provider.SourceRepositoryProvider
import com.pranshulgg.weather_master_app.data.repository.LocationsRepository
import com.pranshulgg.weather_master_app.data.repository.WeatherUnitsRepository
import com.pranshulgg.weather_master_app.data.worker.gadgetbridge.sendGadgetBridgeWeatherData
@@ -22,13 +20,12 @@ import com.pranshulgg.weather_master_app.data.worker.widgets.WeatherWidgetUpdate
import com.pranshulgg.weather_master_app.data.worker.widgets.widgetWeatherMapper
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
@HiltWorker
class WeatherWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val repositoryProvider: WeatherRepositoryProvider,
private val repositoryProvider: SourceRepositoryProvider,
private val locationsRepository: LocationsRepository,
private val appVisibility: AppVisibility,
private val weatherUnitsRepository: WeatherUnitsRepository
@@ -64,7 +61,7 @@ class WeatherWorker @AssistedInject constructor(
// Get the repository
val repo = repositoryProvider.getRepository(default.source)
val repo = repositoryProvider.getWeatherRepository(default.source)
val result = repo.getWeather(
location = default,
@@ -1,16 +1,15 @@
package com.pranshulgg.weather_master_app.data.worker.widgets
import android.content.Context
import android.content.SharedPreferences
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult
import com.pranshulgg.weather_master_app.core.prefs.helper.PreferencesHelper
import com.pranshulgg.weather_master_app.data.provider.WeatherRepositoryProvider
import com.pranshulgg.weather_master_app.data.provider.SourceRepositoryProvider
import com.pranshulgg.weather_master_app.data.repository.LocationsRepository
import com.pranshulgg.weather_master_app.data.repository.WeatherUnitsRepository
import jakarta.inject.Inject
class WidgetReload @Inject constructor(
private val repositoryProvider: WeatherRepositoryProvider,
private val repositoryProvider: SourceRepositoryProvider,
private val locationsRepository: LocationsRepository,
private val weatherUnitsRepository: WeatherUnitsRepository
) {
@@ -24,7 +23,7 @@ class WidgetReload @Inject constructor(
if (defaultLocation == null || units == null) return
val repo = repositoryProvider.getRepository(defaultLocation.source)
val repo = repositoryProvider.getWeatherRepository(defaultLocation.source)
val result = repo.getWeather(
location = defaultLocation,
@@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherUnits
import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiRepository
import com.pranshulgg.weather_master_app.data.repository.LocationsRepository
import com.pranshulgg.weather_master_app.data.repository.WeatherUnitsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -1,8 +1,6 @@
package com.pranshulgg.weather_master_app.feature.editlocation
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@@ -22,7 +20,6 @@ import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -39,9 +36,7 @@ import androidx.navigation.NavController
import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.domain.weather.ApiKey
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.openmeteo.OpenMeteoModel
import com.pranshulgg.weather_master_app.core.ui.components.Gap
import com.pranshulgg.weather_master_app.core.ui.components.LargeTopBarScaffold
@@ -55,15 +50,14 @@ import com.pranshulgg.weather_master_app.feature.editlocation.ui.EditLocationBot
import com.pranshulgg.weather_master_app.feature.editlocation.ui.EditLocationScreenDialogs
import com.pranshulgg.weather_master_app.feature.shared.WeatherViewModel
import com.pranshulgg.weather_master_app.feature.shared.ui.SharedBottomSheet
import com.pranshulgg.weather_master_app.feature.shared.ui.SharedDialogs
data class EditLocationScreenUiState(
val location: Location? = null,
val isWeatherSourcesForLocationSheetOpen: Boolean = false,
val selectedWeatherSource: WeatherSource? = null,
val selectedAlertSource: AlertSource? = null,
val selectedAirQualitySource: AirQualitySource? = null,
val selectedWeatherSource: Source? = null,
val selectedAlertSource: Source? = null,
val selectedAirQualitySource: Source? = null,
val isAlertSourcesSheetOpen: Boolean = false,
val isAirQualitySourcesSheetOpen: Boolean = false,
val isEditLocationNameSheetOpen: Boolean = false,
@@ -115,28 +109,25 @@ fun EditLocationScreen(
var currentLocationName by remember { mutableStateOf(locationName) }
val effectiveWeatherSource =
uiState.selectedWeatherSource ?: uiState.location.source
val effectiveOpenMeteoModel =
uiState.selectedOpenMeteoModel
?: uiState.location.openMeteoModel
val selectedWeatherSourceString = buildString {
if (uiState.selectedWeatherSource != null) {
append(uiState.selectedWeatherSource.displayName)
} else {
append(uiState.location.source.displayName)
append(effectiveWeatherSource.displayName)
effectiveWeatherSource.countryNameRes?.let {
append(" (${stringResource(it)})")
}
if (uiState.selectedWeatherSource?.countryNameRes != null) {
append(" (${stringResource(uiState.selectedWeatherSource.countryNameRes)})")
} else if (uiState.location.source.countryNameRes != null && uiState.selectedWeatherSource == null) {
append(" (${stringResource(uiState.location.source.countryNameRes)})")
if (
effectiveWeatherSource == Source.OPEN_METEO) {
append(" (${effectiveOpenMeteoModel.displayName})")
}
if (uiState.selectedWeatherSource == WeatherSource.OPEN_METEO) {
append(" (${uiState.selectedOpenMeteoModel?.displayName})")
} else if (uiState.location.source == WeatherSource.OPEN_METEO && uiState.selectedWeatherSource == null) {
append(" (${uiState.location.openMeteoModel.displayName})")
}
}
LargeTopBarScaffold(
title = stringResource(R.string.location_edit),
navigationIcon = { NavigateUpBtn(navController) },
@@ -189,8 +180,8 @@ fun EditLocationScreen(
},
trailing = {
val showButton = if (uiState.selectedWeatherSource != null)
uiState.selectedWeatherSource == WeatherSource.OPEN_METEO else
uiState.location.source == WeatherSource.OPEN_METEO
uiState.selectedWeatherSource == Source.OPEN_METEO else
uiState.location.source == Source.OPEN_METEO
if (showButton) {
IconButton(
@@ -285,7 +276,7 @@ fun EditLocationScreen(
selectedSource = uiState.selectedWeatherSource ?: uiState.location.source,
onSave = {
viewModel.updateSelectedWeatherSource(it)
if (it == WeatherSource.OPEN_METEO) {
if (it == Source.OPEN_METEO) {
viewModel.updateSelectedOpenMeteoModel(
uiState.selectedOpenMeteoModel ?: uiState.location.openMeteoModel
)
@@ -4,12 +4,8 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.openmeteo.OpenMeteoModel
import com.pranshulgg.weather_master_app.core.ui.snackbar.SnackbarManager
import com.pranshulgg.weather_master_app.data.repository.ApiKeysRepository
import com.pranshulgg.weather_master_app.data.repository.LocationsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -54,15 +50,15 @@ class EditLocationViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(isWeatherSourcesForLocationSheetOpen = false)
}
fun updateSelectedWeatherSource(source: WeatherSource) {
fun updateSelectedWeatherSource(source: Source) {
_uiState.value = _uiState.value.copy(selectedWeatherSource = source)
}
fun updateSelectedAlertSource(source: AlertSource) {
fun updateSelectedAlertSource(source: Source) {
_uiState.value = _uiState.value.copy(selectedAlertSource = source)
}
fun updateSelectedAirQualitySource(source: AirQualitySource) {
fun updateSelectedAirQualitySource(source: Source) {
_uiState.value = _uiState.value.copy(selectedAirQualitySource = source)
}
@@ -24,13 +24,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.getWeatherSourcesForCountry
import com.pranshulgg.weather_master_app.core.model.sources.getWeatherSourcesGlobal
import com.pranshulgg.weather_master_app.core.model.sources.Capability
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.weather.openmeteo.OpenMeteoModel
import com.pranshulgg.weather_master_app.core.model.weather.openmeteo.OpenMeteoModelType
import com.pranshulgg.weather_master_app.core.ui.components.ActionBottomSheet
import com.pranshulgg.weather_master_app.core.ui.components.Gap
import com.pranshulgg.weather_master_app.core.ui.components.SettingSection
@@ -78,8 +74,8 @@ object EditLocationBottomSheet {
fun AlertSourcesSheet(
show: Boolean,
sheetState: SheetState,
selectedSource: AlertSource = AlertSource.NONE,
onSave: (AlertSource) -> Unit,
selectedSource: Source = Source.NONE,
onSave: (Source) -> Unit,
onDismiss: () -> Unit
) {
if (show) {
@@ -91,6 +87,9 @@ object EditLocationBottomSheet {
mutableStateOf(selectedSource)
}
val sources = Source.entries.filter {
Capability.ALERTS in it.capabilities
}
ActionBottomSheet(
sheetState = sheetState,
onCancel = { onDismiss() },
@@ -100,7 +99,7 @@ object EditLocationBottomSheet {
) {
SettingSection(
title = stringResource(R.string.global_sources),
tiles = AlertSource.entries.map { source ->
tiles = sources.map { source ->
val isSelected = currentSelectedSource == source
SettingTile.ActionTile(
@@ -129,8 +128,8 @@ object EditLocationBottomSheet {
fun AirQualitySourcesSheet(
show: Boolean,
sheetState: SheetState,
selectedSource: AirQualitySource = AirQualitySource.OPEN_METEO,
onSave: (AirQualitySource) -> Unit,
selectedSource: Source = Source.OPEN_METEO,
onSave: (Source) -> Unit,
onDismiss: () -> Unit
) {
if (show) {
@@ -142,6 +141,9 @@ object EditLocationBottomSheet {
mutableStateOf(selectedSource)
}
val sources = Source.entries.filter {
Capability.AIR_QUALITY in it.capabilities
}
ActionBottomSheet(
sheetState = sheetState,
onCancel = { onDismiss() },
@@ -151,7 +153,7 @@ object EditLocationBottomSheet {
) {
SettingSection(
title = stringResource(R.string.global_sources),
tiles = AirQualitySource.entries.map { source ->
tiles = sources.map { source ->
val isSelected = currentSelectedSource == source
SettingTile.ActionTile(
@@ -39,7 +39,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.domain.location.Address
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.ui.components.Gap
import com.pranshulgg.weather_master_app.core.ui.components.Symbol
@@ -279,7 +279,7 @@ fun DeviceLocation.toDomain(context: Context): Location {
timezone = ZoneId.systemDefault().id,
countryCode = "",
state = "",
source = WeatherSource.OPEN_METEO,
source = Source.OPEN_METEO,
isFavorite = false,
isPinned = false,
isDefault = false, // Repository can handle it
@@ -1,11 +1,9 @@
package com.pranshulgg.weather_master_app.feature.locations
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil.util.Logger
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.domain.toMessageRes
@@ -1,7 +1,5 @@
package com.pranshulgg.weather_master_app.feature.main
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -9,11 +7,9 @@ import androidx.compose.material3.SheetValue
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
@@ -30,7 +26,6 @@ import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherBlock
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherUnits
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.prefs.helper.PreferencesHelper
import com.pranshulgg.weather_master_app.core.ui.navigation.NavRoutes
import com.pranshulgg.weather_master_app.core.ui.snackbar.SnackbarManager
@@ -42,7 +37,6 @@ import com.pranshulgg.weather_master_app.feature.main.ui.MainScreenDialogs
import com.pranshulgg.weather_master_app.feature.main.ui.NavigationDrawer
import com.pranshulgg.weather_master_app.feature.shared.WeatherViewModel
import com.pranshulgg.weather_master_app.feature.shared.ui.SharedBottomSheet
import com.pranshulgg.weather_master_app.feature.shared.ui.SharedDialogs
import kotlinx.coroutines.launch
data class MainScreenWeatherUiState(
@@ -14,11 +14,11 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.pranshulgg.weather_master_app.core.model.domain.weather.Weather
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.ui.components.Gap
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.ui.res.stringResource
import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.sources.Source
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -27,7 +27,7 @@ fun CreditsBottomSection(weather: Weather?, onClick: () -> Unit) {
val source = weather?.location?.source
val sourceString = source?.displayName
?: WeatherSource.OPEN_METEO.displayName
?: Source.OPEN_METEO.displayName
val countryString = buildString {
if (source?.countryNameRes != null) {
@@ -28,12 +28,12 @@ fun WeatherAutoRefreshForeground(
// isInitialized guard avoids duplicating setActiveLocation()'s
// cold-start fetch (Android replays ON_START on resubscribe,
// which happens the moment `location` first becomes non-null).
if (weatherViewModel.uiState.value.isInitialized) {
weatherViewModel.getWeather(
location = it,
source = it.source
)
}
// if (weatherViewModel.uiState.value.isInitialized) {
// weatherViewModel.getWeather(
// location = it,
// source = it.source
// )
// }
}
}
@@ -1,35 +1,19 @@
package com.pranshulgg.weather_master_app.feature.main.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetState
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.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.ui.components.ActionBottomSheet
import com.pranshulgg.weather_master_app.core.ui.components.DialogBasic
import com.pranshulgg.weather_master_app.core.ui.components.Gap
import com.pranshulgg.weather_master_app.core.ui.components.SettingSection
import com.pranshulgg.weather_master_app.core.ui.components.SettingTile
import com.pranshulgg.weather_master_app.core.ui.components.SettingsTileIcon
import com.pranshulgg.weather_master_app.core.ui.components.Symbol
import com.pranshulgg.weather_master_app.feature.main.MainScreenViewModel
object MainScreenBottomSheets {
@@ -57,7 +41,7 @@ object MainScreenBottomSheets {
append(" (${stringResource(location.source.countryNameRes)})")
}
if (location.source == WeatherSource.OPEN_METEO) {
if (location.source == Source.OPEN_METEO) {
append(" (${location.openMeteoModel.displayName})")
}
@@ -86,7 +70,7 @@ object MainScreenBottomSheets {
)
)
)
if (location.airQualitySource != AirQualitySource.NONE) {
if (location.airQualitySource != Source.NONE) {
Gap(8.dp)
SettingSection(
title = stringResource(R.string.weather_air_quality),
@@ -102,7 +86,7 @@ object MainScreenBottomSheets {
)
)
}
if (location.alertSource != AlertSource.NONE) {
if (location.alertSource != Source.NONE) {
Gap(8.dp)
SettingSection(
title = "Alerts",
@@ -20,7 +20,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -37,9 +36,8 @@ import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.domain.weather.ApiKey
import com.pranshulgg.weather_master_app.core.model.sources.SearchSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.prefs.LocalAppPrefs
import com.pranshulgg.weather_master_app.core.utils.formatters.toTitleCase
import com.pranshulgg.weather_master_app.core.ui.components.EmptyContainerPlaceholder
import com.pranshulgg.weather_master_app.core.ui.components.Gap
import com.pranshulgg.weather_master_app.core.ui.components.LargeTopBarScaffold
@@ -49,6 +47,7 @@ import com.pranshulgg.weather_master_app.core.ui.components.SettingSection
import com.pranshulgg.weather_master_app.core.ui.components.SettingTile
import com.pranshulgg.weather_master_app.core.ui.components.Symbol
import com.pranshulgg.weather_master_app.core.ui.navigation.NavRoutes
import com.pranshulgg.weather_master_app.core.utils.formatters.toTitleCase
import com.pranshulgg.weather_master_app.feature.search.ui.SearchFloatingToolbar
import com.pranshulgg.weather_master_app.feature.search.ui.SearchScreenBottomSheets
import com.pranshulgg.weather_master_app.feature.shared.ui.SharedBottomSheet
@@ -174,7 +173,7 @@ fun SearchScreen(navController: NavController) {
SharedBottomSheet.WeatherSourcesForLocationSheet(
countryCode = selectedLocation?.countryCode,
show = uiState.isWeatherSourcesForLocationSheetOpen,
selectedSource = selectedLocation?.source ?: WeatherSource.OPEN_METEO,
selectedSource = selectedLocation?.source ?: Source.OPEN_METEO,
onSave = {
viewModel.hideWeatherSourcesForLocationSheet()
viewModel.saveLocation(
@@ -11,7 +11,7 @@ import com.pranshulgg.weather_master_app.core.model.sources.SearchSource
import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.domain.toAppException
import com.pranshulgg.weather_master_app.core.model.domain.toMessageRes
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.network.sources.search.geonames.GeoNamesTimezoneRepository
import com.pranshulgg.weather_master_app.core.prefs.AppPrefsState
import com.pranshulgg.weather_master_app.core.ui.snackbar.SnackbarManager
@@ -69,7 +69,7 @@ class SearchScreenViewModel @Inject constructor(
location: Location?,
onBack: () -> Unit,
onReset: () -> Unit,
weatherSource: WeatherSource
weatherSource: Source
) {
if (location == null) return
@@ -18,8 +18,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.ui.components.Gap
import com.pranshulgg.weather_master_app.core.ui.components.LargeTopBarScaffold
import com.pranshulgg.weather_master_app.core.ui.components.NavigateUpBtn
@@ -29,38 +28,11 @@ import com.pranshulgg.weather_master_app.core.ui.components.SettingsTileIcon
import com.pranshulgg.weather_master_app.core.ui.navigation.NavRoutes
private data class WeatherSourceScreen(
val displayName: String,
val fullName: String,
val displayLink: String,
val countryNameRes: Int? = null
)
@Composable
fun WeatherSourcesScreen(navController: NavController) {
val uriHandler = LocalUriHandler.current
val alertSources =
AlertSource.entries.filter { it.displayName !in WeatherSource.entries.map { source -> source.displayName } }
.filter { it != AlertSource.NONE }
.map {
WeatherSourceScreen(
displayName = it.displayName,
fullName = it.fullName,
displayLink = it.displayLink,
)
}
val weatherSources = WeatherSource.entries.map {
WeatherSourceScreen(
displayName = it.displayName,
fullName = it.fullName,
displayLink = it.displayLink,
countryNameRes = it.countryNameRes
)
}
val sources = weatherSources + alertSources
val sources = Source.entries
LargeTopBarScaffold(
title = stringResource(R.string.weather_sources),
@@ -1,7 +1,6 @@
package com.pranshulgg.weather_master_app.feature.shared
import android.content.Context
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
@@ -11,24 +10,19 @@ import com.pranshulgg.weather_master_app.core.model.domain.location.Location
import com.pranshulgg.weather_master_app.core.model.domain.toAppException
import com.pranshulgg.weather_master_app.core.model.domain.toMessageRes
import com.pranshulgg.weather_master_app.core.model.domain.weather.WeatherBlock
import com.pranshulgg.weather_master_app.core.model.sources.AirQualitySource
import com.pranshulgg.weather_master_app.core.model.sources.AlertSource
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.sources.isGlobal
import com.pranshulgg.weather_master_app.core.model.sources.isSourceSupportedFor
import com.pranshulgg.weather_master_app.core.model.weather.WeatherResult
import com.pranshulgg.weather_master_app.core.model.weather.airquality.AirQualityResult
import com.pranshulgg.weather_master_app.core.model.weather.alerts.AlertResult
import com.pranshulgg.weather_master_app.core.model.weather.openmeteo.OpenMeteoModel
import com.pranshulgg.weather_master_app.core.network.sources.airquality.openmeteo.OpenMeteoAqiRepository
import com.pranshulgg.weather_master_app.core.ui.snackbar.SnackbarManager
import com.pranshulgg.weather_master_app.data.provider.AirQualityRepositoryProvider
import com.pranshulgg.weather_master_app.data.provider.AlertsRepositoryProvider
import com.pranshulgg.weather_master_app.data.provider.WeatherRepositoryProvider
import com.pranshulgg.weather_master_app.data.repository.LocationsRepository
import com.pranshulgg.weather_master_app.data.repository.WeatherBlocksRepository
import com.pranshulgg.weather_master_app.data.repository.WeatherDataReconcilerRepository
import com.pranshulgg.weather_master_app.data.repository.WeatherUnitsRepository
import com.pranshulgg.weather_master_app.data.repository.data.SourceDataRepository
import com.pranshulgg.weather_master_app.data.worker.WeatherUpdateScheduler
import com.pranshulgg.weather_master_app.feature.main.MainScreenWeatherUiState
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -43,19 +37,15 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.minutes
@HiltViewModel
class WeatherViewModel @Inject constructor(
private val repo: WeatherRepositoryProvider,
private val locationsRepo: LocationsRepository,
appWeatherUnitsRepo: WeatherUnitsRepository,
private val weatherBlocksRepository: WeatherBlocksRepository,
private val openMeteoAqiRepository: OpenMeteoAqiRepository,
private val weatherDataReconcilerRepository: WeatherDataReconcilerRepository,
private val airQualityRepositoryProvider: AirQualityRepositoryProvider,
private val alertRepositoryProvider: AlertsRepositoryProvider,
private val sourceDataRepository: SourceDataRepository,
@ApplicationContext private val context: Context
) : ViewModel() {
@@ -116,7 +106,7 @@ class WeatherViewModel @Inject constructor(
fun getWeather(
location: Location,
source: WeatherSource,
source: Source,
isManualRefresh: Boolean = false,
isForceRefresh: Boolean = false,
isForceRefreshForAirQuality: Boolean = false,
@@ -125,7 +115,11 @@ class WeatherViewModel @Inject constructor(
setLoading(true)
weatherJob?.cancel()
val startTime = System.currentTimeMillis()
_uiState.value = _uiState.value.copy(isError = false, isUnsupportedSource = false)
_uiState.value = _uiState.value.copy(
isError = false,
isUnsupportedSource = false,
isAirQualityLoading = true
)
weatherJob = viewModelScope.launch {
@@ -151,17 +145,51 @@ class WeatherViewModel @Inject constructor(
}
}
// Run separately
if (!_uiState.value.isError) {
launch {
handleAirQuality(effectiveLocation, isManualRefresh, effectiveForceRefreshForAirQuality)
}
launch {
handleAlerts(effectiveLocation, isManualRefresh, effectiveForceRefreshForAlerts)
}
}
handleWeatherData(source, effectiveLocation, isManualRefresh, effectiveForceRefresh)
sourceDataRepository.getData(
location = effectiveLocation,
isManualRefresh = isManualRefresh,
isForceRefresh = effectiveForceRefresh,
isForceRefreshForAirQuality = effectiveForceRefreshForAirQuality,
isForceRefreshForAlerts = effectiveForceRefreshForAlerts,
onWeather = { result ->
handleWeatherData(result, effectiveLocation)
},
onAlerts = { result ->
handleAlerts(result)
},
onAirQuality = { result ->
handleAirQuality(result)
},
)
// handleWeatherData(source, effectiveLocation, isManualRefresh, effectiveForceRefresh)
// Run separately
// if (!_uiState.value.isError) {
// launch {
// handleAirQuality(
// effectiveLocation,
// isManualRefresh,
// effectiveForceRefreshForAirQuality
// )
// }
//
// Only run separately if the alert source is a different API of its own
// If not, then run after the weather has fetched
// if (!source.providesAlerts && effectiveLocation.alertSource.name != source.name) {
// launch {
// handleAlerts(
// effectiveLocation,
// isManualRefresh,
// effectiveForceRefreshForAlerts
// )
// }
// } else {
// handleAlertsFromWeatherSource(effectiveLocation)
// }
// }
val elapsed = System.currentTimeMillis() - startTime
val minLoadingTime = 1000L // 1s
@@ -203,9 +231,9 @@ class WeatherViewModel @Inject constructor(
fun handleSourceChangeForWeather(
location: Location,
source: WeatherSource,
airQualitySource: AirQualitySource,
alertSource: AlertSource,
source: Source,
airQualitySource: Source,
alertSource: Source,
openMeteoModel: OpenMeteoModel
) {
val updatedLocation = location.copy(
@@ -227,32 +255,11 @@ class WeatherViewModel @Inject constructor(
val allowForceRefreshForAirQuality = location.airQualitySource != airQualitySource
val allowForceRefreshForAlerts = location.alertSource != alertSource
if (allowForceRefreshForWeather) {
weatherDataReconcilerRepository.cleanUpStaleData(
location.source,
location.id,
airQualitySource,
alertSource
)
}
if (allowForceRefreshForAirQuality) {
weatherDataReconcilerRepository.cleanUpStaleAirQualityData(
location.id,
source,
alertSource
)
}
if (allowForceRefreshForAlerts) {
weatherDataReconcilerRepository.cleanUpStaleAlertsData(
location.id,
source,
airQualitySource
)
}
_uiState.value = _uiState.value.copy(
activeLocation = updatedLocation
weatherDataReconcilerRepository.reconcileSourceChange(
previous = location,
updated = updatedLocation
)
_uiState.value = _uiState.value.copy(activeLocation = updatedLocation)
getWeather(
updatedLocation,
source,
@@ -260,8 +267,6 @@ class WeatherViewModel @Inject constructor(
isForceRefreshForAirQuality = allowForceRefreshForAirQuality,
isForceRefreshForAlerts = allowForceRefreshForAlerts
)
}
}
@@ -297,18 +302,10 @@ class WeatherViewModel @Inject constructor(
return locationsRepo.updateDeviceLocationPosition()
}
private suspend fun handleWeatherData(
source: WeatherSource,
location: Location,
isManualRefresh: Boolean,
isForceRefresh: Boolean
) {
val repo = repo.getRepository(source)
private suspend fun handleWeatherData(result: WeatherResult, location: Location) {
when (val result = repo.getWeather(location, isManualRefresh, isForceRefresh)) {
when (result) {
is WeatherResult.Success -> {
_uiState.value = _uiState.value.copy(weather = result.weather, isInitialized = true)
@@ -319,8 +316,6 @@ class WeatherViewModel @Inject constructor(
val appExpectation = result.exception.toAppException()
SnackbarManager.show(appExpectation.toMessageRes())
_uiState.value = _uiState.value.copy(
isError = true,
weather = result.cacheWeather,
@@ -345,27 +340,11 @@ class WeatherViewModel @Inject constructor(
}
}
private suspend fun handleAirQuality(
location: Location,
isManualRefresh: Boolean,
isForceRefresh: Boolean
private fun handleAirQuality(
result: AirQualityResult
) {
if (_uiState.value.isAirQualityLoading) return
val repoAir = airQualityRepositoryProvider.getRepository(location.airQualitySource)
if (repoAir == null) {
_uiState.value = _uiState.value.copy(airQuality = null, isAirQualityLoading = false)
return
}
_uiState.value = _uiState.value.copy(isAirQualityLoading = true)
when (val result = repoAir.getAirQuality(location, isManualRefresh, isForceRefresh)) {
when (result) {
is AirQualityResult.Success -> {
_uiState.value =
_uiState.value.copy(
@@ -387,30 +366,14 @@ class WeatherViewModel @Inject constructor(
}
private suspend fun handleAlerts(
location: Location,
isManualRefresh: Boolean,
isForceRefresh: Boolean
) {
private fun handleAlerts(result: AlertResult) {
val repoAlert = alertRepositoryProvider.getRepository(location.alertSource)
if (repoAlert == null) {
_uiState.value = _uiState.value.copy(alerts = emptyList())
return
}
when (val result = repoAlert.getAlerts(location, isManualRefresh, isForceRefresh)) {
when (result) {
is AlertResult.Success -> {
_uiState.value =
_uiState.value.copy(
alerts = result.alerts
)
}
// Fail silently, we just won't show the Air quality in the UI
@@ -426,7 +389,7 @@ class WeatherViewModel @Inject constructor(
fun startAutoRefresh(
location: Location,
source: WeatherSource
source: Source
) {
if (autoRefreshJob?.isActive == true) return
@@ -1,40 +1,25 @@
package com.pranshulgg.weather_master_app.feature.shared.ui
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.pranshulgg.weather_master_app.R
import com.pranshulgg.weather_master_app.core.model.domain.weather.ApiKey
import com.pranshulgg.weather_master_app.core.model.sources.WeatherSource
import com.pranshulgg.weather_master_app.core.model.sources.getWeatherSourcesForCountry
import com.pranshulgg.weather_master_app.core.model.sources.getWeatherSourcesGlobal
import com.pranshulgg.weather_master_app.core.model.sources.Source
import com.pranshulgg.weather_master_app.core.model.sources.getSourcesForCountry
import com.pranshulgg.weather_master_app.core.model.sources.getSourcesGlobal
import com.pranshulgg.weather_master_app.core.ui.components.ActionBottomSheet
import com.pranshulgg.weather_master_app.core.ui.components.DialogBasic
import com.pranshulgg.weather_master_app.core.ui.components.Gap
import com.pranshulgg.weather_master_app.core.ui.components.RadioRow
import com.pranshulgg.weather_master_app.core.ui.components.SettingSection
import com.pranshulgg.weather_master_app.core.ui.components.SettingTile
import com.pranshulgg.weather_master_app.core.ui.components.SettingsTileIcon
import com.pranshulgg.weather_master_app.core.ui.components.Symbol
import com.pranshulgg.weather_master_app.core.ui.theme.ShapeRadius
import com.pranshulgg.weather_master_app.feature.shared.components.ChangelogContent
object SharedBottomSheet {
@@ -44,18 +29,18 @@ object SharedBottomSheet {
countryCode: String?,
show: Boolean,
sheetState: SheetState,
selectedSource: WeatherSource = WeatherSource.OPEN_METEO,
selectedSource: Source = Source.OPEN_METEO,
isEditing: Boolean = false,
onSave: (WeatherSource) -> Unit,
onSave: (Source) -> Unit,
onDismiss: () -> Unit,
onClickApiConfig: () -> Unit,
apiKeys: List<ApiKey>,
) {
if (show) {
val recommendedSources = getWeatherSourcesForCountry(countryCode?.uppercase())
val globalSources = getWeatherSourcesGlobal()
val recommendedSources = getSourcesForCountry(countryCode?.uppercase())
val globalSources = getSourcesGlobal()
val isApiKeyAvailable: (WeatherSource) -> Boolean = { source ->
val isApiKeyAvailable: (Source) -> Boolean = { source ->
if (source.requiresUserApiKey) {
apiKeys.isNotEmpty()
&& apiKeys
@@ -78,6 +63,22 @@ object SharedBottomSheet {
)
}
val handeSelection: (Source) -> Unit = {
if (!it.requiresUserApiKey) {
currentSelectedSource = it
} else if (!isApiKeyAvailable(it)) {
onClickApiConfig()
} else {
currentSelectedSource = it
}
}
val description: (Source) -> String? = {
if (it.requiresUserApiKey &&
!isApiKeyAvailable(it)
) "Requires API key" else null
}
ActionBottomSheet(
@@ -106,20 +107,11 @@ object SharedBottomSheet {
)
},
title = source.displayName + countryString,
description = if (source.requiresUserApiKey && !isApiKeyAvailable(
source
)
) "Requires API key" else null,
description = description(source),
colorDesc = MaterialTheme.colorScheme.error,
selected = isSelected,
onClick = {
if (!source.requiresUserApiKey) {
currentSelectedSource = source
} else if (!isApiKeyAvailable(source)) {
onClickApiConfig()
} else {
currentSelectedSource = source
}
handeSelection(source)
}
)
}
@@ -143,8 +135,9 @@ object SharedBottomSheet {
},
title = source.displayName + countryString,
selected = isSelected,
description = description(source),
onClick = {
currentSelectedSource = source
handeSelection(source)
},
)
}
@@ -169,19 +162,10 @@ object SharedBottomSheet {
},
title = source.displayName + countryString,
selected = isSelected,
description = if (source.requiresUserApiKey && !isApiKeyAvailable(
source
)
) "Requires API key" else null,
description = description(source),
colorDesc = MaterialTheme.colorScheme.error,
onClick = {
if (!source.requiresUserApiKey) {
currentSelectedSource = source
} else if (!isApiKeyAvailable(source)) {
onClickApiConfig()
} else {
currentSelectedSource = source
}
handeSelection(source)
},
)
}