- new weather api added

This commit is contained in:
2024-09-30 10:02:14 +01:00
parent 0aff414b1c
commit 24da85d20d
28 changed files with 21502 additions and 121 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
package com.appttude.h_mal.atlas_weather.utils package com.appttude.h_mal.atlas_weather.utils
const val baseUrl = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/"
enum class Stubs( enum class Stubs(
val id: String val id: String
) { ) {
@@ -7,5 +8,6 @@ enum class Stubs(
Imperial("valid_response_imperial"), Imperial("valid_response_imperial"),
WrongLocation("wrong_location_response"), WrongLocation("wrong_location_response"),
InvalidKey("invalid_api_key_response"), InvalidKey("invalid_api_key_response"),
Sydney("valid_response_metric_sydney") Sydney("valid_response_metric_sydney"),
New("new_response")
} }

View File

@@ -24,10 +24,7 @@ class SettingsScreen : BaseTestRobot() {
RecyclerViewActions.actionOnItem<ViewHolder>( RecyclerViewActions.actionOnItem<ViewHolder>(
ViewMatchers.hasDescendant(withText(R.string.weather_units)), ViewMatchers.hasDescendant(withText(R.string.weather_units)),
click())) click()))
val label = when (unitType) { val label = unitType.getLabel()
UnitType.METRIC -> "Metric"
UnitType.IMPERIAL -> "Imperial"
}
onView(withText(label)) onView(withText(label))
.inRoot(isDialog()) .inRoot(isDialog())

View File

@@ -7,7 +7,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.location.MockLocationProvider import com.appttude.h_mal.atlas_weather.data.location.MockLocationProvider
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.NewWeatherApi
import com.appttude.h_mal.atlas_weather.data.network.interceptors.MockingNetworkInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.MockingNetworkInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
@@ -32,13 +32,13 @@ class TestAppClass : AppClass() {
IdlingRegistry.getInstance().register(idlingResources) IdlingRegistry.getInstance().register(idlingResources)
} }
override fun createNetworkModule(): WeatherApi { override fun createNetworkModule(): NewWeatherApi {
return NetworkModule().invoke<WeatherApi>( return NetworkModule().invoke<NewWeatherApi>(
mockingNetworkInterceptor, mockingNetworkInterceptor,
NetworkConnectionInterceptor(this), NetworkConnectionInterceptor(this),
QueryParamsInterceptor(), QueryParamsInterceptor(),
loggingInterceptor loggingInterceptor
) as WeatherApi ) as NewWeatherApi
} }
override fun createLocationModule(): LocationProvider { override fun createLocationModule(): LocationProvider {

View File

@@ -13,6 +13,7 @@ import com.appttude.h_mal.atlas_weather.BaseTestRobot
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView
import com.appttude.h_mal.atlas_weather.model.types.UnitType import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.types.UnitType.Companion.getLabel
fun settingsScreen(func: SettingsScreen.() -> Unit) = SettingsScreen().apply { func() } fun settingsScreen(func: SettingsScreen.() -> Unit) = SettingsScreen().apply { func() }
@@ -24,10 +25,7 @@ class SettingsScreen : BaseTestRobot() {
RecyclerViewActions.actionOnItem<ViewHolder>( RecyclerViewActions.actionOnItem<ViewHolder>(
ViewMatchers.hasDescendant(withText(R.string.weather_units)), ViewMatchers.hasDescendant(withText(R.string.weather_units)),
click())) click()))
val label = when (unitType) { val label = unitType.getLabel()
UnitType.METRIC -> "Metric"
UnitType.IMPERIAL -> "Imperial"
}
onView(withText(label)) onView(withText(label))
.inRoot(isDialog()) .inRoot(isDialog())

View File

@@ -5,17 +5,16 @@ import com.appttude.h_mal.atlas_weather.BaseTest
import com.appttude.h_mal.atlas_weather.model.types.UnitType import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.ui.MainActivity import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs import com.appttude.h_mal.atlas_weather.utils.Stubs
import com.appttude.h_mal.atlas_weather.utils.baseUrl
import com.appttude.h_mal.monoWeather.robot.furtherInfoScreen import com.appttude.h_mal.monoWeather.robot.furtherInfoScreen
import com.appttude.h_mal.monoWeather.robot.settingsScreen import com.appttude.h_mal.monoWeather.robot.settingsScreen
import com.appttude.h_mal.monoWeather.robot.weatherScreen import com.appttude.h_mal.monoWeather.robot.weatherScreen
import okio.IOException
import org.junit.Test import org.junit.Test
import tools.fastlane.screengrab.Screengrab
class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) { class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
override fun beforeLaunch() { override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric) stubEndpoint(baseUrl, Stubs.New)
clearPrefs() clearPrefs()
} }

View File

@@ -3,7 +3,7 @@ package com.appttude.h_mal.atlas_weather.application
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.NewWeatherApi
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor
@@ -11,12 +11,12 @@ import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
open class AppClass : BaseAppClass() { open class AppClass : BaseAppClass() {
override fun createNetworkModule(): WeatherApi { override fun createNetworkModule(): NewWeatherApi {
return NetworkModule().invoke<WeatherApi>( return NetworkModule().invoke<NewWeatherApi>(
NetworkConnectionInterceptor(this), NetworkConnectionInterceptor(this),
QueryParamsInterceptor(), QueryParamsInterceptor(),
loggingInterceptor loggingInterceptor
) as WeatherApi ) as NewWeatherApi
} }
override fun createLocationModule(): LocationProvider = LocationProviderImpl(this) override fun createLocationModule(): LocationProvider = LocationProviderImpl(this)

View File

@@ -3,7 +3,7 @@ package com.appttude.h_mal.atlas_weather.application
import android.app.Application import android.app.Application
import com.appttude.h_mal.atlas_weather.data.WeatherSource import com.appttude.h_mal.atlas_weather.data.WeatherSource
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.Api
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl
@@ -12,7 +12,6 @@ import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.google.gson.Gson import com.google.gson.Gson
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
import org.kodein.di.KodeinContainer
import org.kodein.di.android.x.androidXModule import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind import org.kodein.di.generic.bind
import org.kodein.di.generic.instance import org.kodein.di.generic.instance
@@ -41,7 +40,7 @@ abstract class BaseAppClass : Application(), KodeinAware {
bind() from singleton { WeatherSource(instance(), instance()) } bind() from singleton { WeatherSource(instance(), instance()) }
} }
abstract fun createNetworkModule(): WeatherApi abstract fun createNetworkModule(): Api
abstract fun createLocationModule(): LocationProvider abstract fun createLocationModule(): LocationProvider
abstract fun createRoomDatabase(): AppDatabase abstract fun createRoomDatabase(): AppDatabase

View File

@@ -7,6 +7,7 @@ import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.types.LocationType import com.appttude.h_mal.atlas_weather.model.types.LocationType
import com.appttude.h_mal.atlas_weather.model.types.UnitType import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.utils.getSymbol
import java.io.IOException import java.io.IOException
class WeatherSource( class WeatherSource(
@@ -38,7 +39,7 @@ class WeatherSource(
// get data from database // get data from database
val weatherEntity = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION) val weatherEntity = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
// check unit type - if same do nothing // check unit type - if same do nothing
val units = if (repository.getUnitType() == UnitType.METRIC) "°C" else "°F" val units = repository.getUnitType().getSymbol()
if (weatherEntity.weather.temperatureUnit == units) return weatherEntity.weather if (weatherEntity.weather.temperatureUnit == units) return weatherEntity.weather
// load data for forced // load data for forced
return fetchWeather( return fetchWeather(
@@ -55,11 +56,13 @@ class WeatherSource(
// Get weather from api // Get weather from api
val weather = repository val weather = repository
.getWeatherFromApi(latLon.first.toString(), latLon.second.toString()) .getWeatherFromApi(latLon.first.toString(), latLon.second.toString())
val lat = weather.latitude ?: latLon.first
val long = weather.longitude ?: latLon.second
val currentLocation = val currentLocation =
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon, locationType) locationProvider.getLocationNameFromLatLong(lat, long, locationType)
val unit = repository.getUnitType() val unit = repository.getUnitType().getSymbol()
val fullWeather = FullWeather(weather).apply { val fullWeather = weather.mapData().apply {
temperatureUnit = if (unit == UnitType.METRIC) "°C" else "°F" temperatureUnit = unit
locationString = currentLocation locationString = currentLocation
} }
val entityItem = EntityItem(locationName, fullWeather) val entityItem = EntityItem(locationName, fullWeather)

View File

@@ -1,6 +1,6 @@
package com.appttude.h_mal.atlas_weather.data.network package com.appttude.h_mal.atlas_weather.data.network
class NetworkModule : BaseNetworkModule() { class NetworkModule : BaseNetworkModule() {
override fun baseUrl(): String = "https://api.openweathermap.org/data/2.5/" override fun baseUrl(): String = "https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/"
} }

View File

@@ -0,0 +1,22 @@
package com.appttude.h_mal.atlas_weather.data.network
import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface NewWeatherApi : Api {
// Todo: change the location
// Todo: add endpoint for lat/long
@GET("{location}")
suspend fun getFromApi(
@Query("contentType") exclude: String = "json",
@Query("unitGroup") units: String = "uk",
@Path("location") location: String
): Response<WeatherApiResponse>
}

View File

@@ -16,7 +16,7 @@ class QueryParamsInterceptor : Interceptor {
val original = chain.request() val original = chain.request()
val url = original.url.newBuilder() val url = original.url.newBuilder()
.addQueryParameter("appid", id) .addQueryParameter("key", id)
.build() .build()
// Request customization: add request headers // Request customization: add request headers

View File

@@ -1,11 +1,13 @@
package com.appttude.h_mal.atlas_weather.data.network.networkUtils package com.appttude.h_mal.atlas_weather.data.network.networkUtils
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor
import com.google.gson.GsonBuilder
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.lang.reflect.Modifier
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
val loggingInterceptor = HttpLoggingInterceptor().apply { val loggingInterceptor = HttpLoggingInterceptor().apply {
@@ -34,6 +36,13 @@ fun buildOkHttpClient(
return builder.build() return builder.build()
} }
fun createGsonConverterFactory(): GsonConverterFactory {
val gson = GsonBuilder()
.excludeFieldsWithModifiers(Modifier.TRANSIENT)
.create()
return GsonConverterFactory.create(gson)
}
fun <T> createRetrofit( fun <T> createRetrofit(
baseUrl: String, baseUrl: String,
okHttpClient: OkHttpClient, okHttpClient: OkHttpClient,

View File

@@ -1,13 +1,13 @@
package com.appttude.h_mal.atlas_weather.data.repository package com.appttude.h_mal.atlas_weather.data.repository
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.types.UnitType import com.appttude.h_mal.atlas_weather.model.types.UnitType
interface Repository { interface Repository {
suspend fun getWeatherFromApi(lat: String, long: String): WeatherResponse suspend fun getWeatherFromApi(lat: String, long: String): WeatherApiResponse
suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem)
suspend fun saveWeatherListToRoom(list: List<EntityItem>) suspend fun saveWeatherListToRoom(list: List<EntityItem>)
fun loadRoomWeatherLiveData(): LiveData<List<EntityItem>> fun loadRoomWeatherLiveData(): LiveData<List<EntityItem>>

View File

@@ -1,8 +1,8 @@
package com.appttude.h_mal.atlas_weather.data.repository package com.appttude.h_mal.atlas_weather.data.repository
import com.appttude.h_mal.atlas_weather.data.network.NewWeatherApi
import com.appttude.h_mal.atlas_weather.data.network.ResponseUnwrap import com.appttude.h_mal.atlas_weather.data.network.ResponseUnwrap
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
@@ -12,7 +12,7 @@ import com.appttude.h_mal.atlas_weather.utils.FALLBACK_TIME
class RepositoryImpl( class RepositoryImpl(
private val api: WeatherApi, private val api: NewWeatherApi,
private val db: AppDatabase, private val db: AppDatabase,
private val prefs: PreferenceProvider private val prefs: PreferenceProvider
) : Repository, ResponseUnwrap() { ) : Repository, ResponseUnwrap() {
@@ -20,8 +20,8 @@ class RepositoryImpl(
override suspend fun getWeatherFromApi( override suspend fun getWeatherFromApi(
lat: String, lat: String,
long: String long: String
): WeatherResponse { ): WeatherApiResponse {
return responseUnwrap { api.getFromApi(lat, long, units = prefs.getUnitsType().name.lowercase()) } return responseUnwrap { api.getFromApi(location = lat + long) }
} }
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) { override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) {

View File

@@ -11,14 +11,13 @@ import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetData import com.appttude.h_mal.atlas_weather.model.widget.WidgetData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetError import com.appttude.h_mal.atlas_weather.model.widget.WidgetError
import com.appttude.h_mal.atlas_weather.model.widget.WidgetState import com.appttude.h_mal.atlas_weather.model.widget.WidgetState
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
import com.appttude.h_mal.atlas_weather.utils.getSymbol
import com.appttude.h_mal.atlas_weather.utils.toSmallDayName import com.appttude.h_mal.atlas_weather.utils.toSmallDayName
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import com.squareup.picasso.Target import com.squareup.picasso.Target
@@ -45,9 +44,11 @@ class ServicesHelper(
// Get weather from api // Get weather from api
val weather = repository val weather = repository
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString()) .getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
val lat = weather.latitude ?: latLong.first
val long = weather.longitude ?: latLong.second
val currentLocation = val currentLocation =
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon) locationProvider.getLocationNameFromLatLong(lat, long)
val fullWeather = FullWeather(weather).apply { val fullWeather = weather.mapData().apply {
temperatureUnit = "°C" temperatureUnit = "°C"
locationString = currentLocation locationString = currentLocation
} }
@@ -105,8 +106,11 @@ class ServicesHelper(
return WidgetState.HasError(error) return WidgetState.HasError(error)
} }
val lat = weather.latitude ?: latLong.first
val long = weather.longitude ?: latLong.second
val currentLocation = try { val currentLocation = try {
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon) locationProvider.getLocationNameFromLatLong(lat, long)
} catch (e: IOException) { } catch (e: IOException) {
val data = getWidgetWeatherCollection() val data = getWidgetWeatherCollection()
data?.let { data?.let {
@@ -120,8 +124,8 @@ class ServicesHelper(
return WidgetState.HasError(error) return WidgetState.HasError(error)
} }
val fullWeather = FullWeather(weather).apply { val fullWeather = weather.mapData().apply {
temperatureUnit = if (repository.getUnitType() == UnitType.METRIC) "°C" else "°F" temperatureUnit = repository.getUnitType().getSymbol()
locationString = currentLocation locationString = currentLocation
} }
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather) val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.atlas_weather.model
interface DataMapper <T: Any> {
fun mapData(): T
}

View File

@@ -0,0 +1,28 @@
package com.appttude.h_mal.atlas_weather.model
enum class IconMapper(val label: String) {
snow("13d"),
snow_showers_day("13d"),
snow_showers_night("13n"),
thunder_rain("11d"),
thunder_showers_day("11d"),
thunder_showers_night("11n"),
rain("10d"),
showers_day("10d"),
showers_night("10n"),
fog("50d"),
wind("50d"),
cloudy("04d"),
partly_cloudy_day("03d"),
partly_cloudy_night("03n"),
clear_day("01d"),
clear_night("01n");
companion object{
fun findIconCode(iconId: String?): String? {
val label = iconId?.replace("-", "_")
val enumName = IconMapper.entries.find { it.name == label }
return enumName?.label
}
}
}

View File

@@ -10,7 +10,7 @@ data class WeatherDisplay(
val averageTemp: Double?, val averageTemp: Double?,
var unit: String?, var unit: String?,
var location: String?, var location: String?,
val iconURL: String?, var iconURL: String?,
val description: String?, val description: String?,
val hourly: List<Hour>?, val hourly: List<Hour>?,
val forecast: List<Forecast>?, val forecast: List<Forecast>?,

View File

@@ -8,11 +8,15 @@ enum class UnitType {
companion object { companion object {
fun getByName(name: String?): UnitType? { fun getByName(name: String?): UnitType? {
return values().firstOrNull { return entries.firstOrNull {
it.name.lowercase(Locale.ROOT) == name?.lowercase( it.name.lowercase(Locale.ROOT) == name?.lowercase(
Locale.ROOT Locale.ROOT
) )
} }
} }
fun UnitType.getLabel() = name.lowercase().replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
} }
} }

View File

@@ -1,6 +1,8 @@
package com.appttude.h_mal.atlas_weather.model.weather package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Current import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Current
import com.appttude.h_mal.atlas_weather.data.network.response.weather.CurrentConditions
import com.appttude.h_mal.atlas_weather.model.IconMapper
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
data class Current( data class Current(
@@ -42,4 +44,24 @@ data class Current(
dailyItem.humidity, dailyItem.humidity,
dailyItem.windSpeed dailyItem.windSpeed
) )
constructor(currentConditions: CurrentConditions?): this(
dt = currentConditions?.datetimeEpoch,
sunrise = currentConditions?.sunriseEpoch,
sunset = currentConditions?.sunsetEpoch,
temp = currentConditions?.temp,
visibility = currentConditions?.visibility?.toInt(),
uvi = currentConditions?.uvindex?.toDouble(),
pressure = currentConditions?.pressure?.toInt(),
clouds = currentConditions?.cloudcover?.toInt(),
feelsLike = currentConditions?.feelslike,
windDeg = currentConditions?.winddir?.toInt(),
dewPoint = currentConditions?.dew,
icon = generateIconUrlString(IconMapper.findIconCode(currentConditions?.icon)),
description = currentConditions?.conditions,
main = currentConditions?.conditions,
id = currentConditions?.datetimeEpoch,
humidity = currentConditions?.humidity?.toInt(),
windSpeed = currentConditions?.windspeed
)
} }

View File

@@ -1,6 +1,8 @@
package com.appttude.h_mal.atlas_weather.model.weather package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.DailyItem import com.appttude.h_mal.atlas_weather.data.network.response.forecast.DailyItem
import com.appttude.h_mal.atlas_weather.data.network.response.weather.Days
import com.appttude.h_mal.atlas_weather.model.IconMapper
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
@@ -50,5 +52,30 @@ data class DailyWeather(
dailyItem.rain dailyItem.rain
) )
constructor(days: Days) : this(
days.datetimeEpoch,
days.sunriseEpoch,
days.sunsetEpoch,
days.tempmin,
days.tempmax,
days.temp?.toDouble(),
days.feelslike,
days.pressure?.toInt(),
days.humidity?.toInt(),
days.dew,
days.windspeed,
days.winddir?.toInt(),
generateIconUrlString(
IconMapper.findIconCode(days.icon)
),
days.description,
days.conditions,
days.datetimeEpoch,
days.cloudcover?.toInt(),
days.precipprob?.toDouble(),
days.uvindex?.toDouble(),
days.precip?.toDouble()
)
} }

View File

@@ -2,8 +2,10 @@ package com.appttude.h_mal.atlas_weather.model.weather
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.model.IconMapper
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Hour as ForecastHour import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Hour as ForecastHour
import com.appttude.h_mal.atlas_weather.data.network.response.weather.Hours as WeatherHour
data class Hour( data class Hour(
@@ -24,6 +26,12 @@ data class Hour(
hour.temp, hour.temp,
generateIconUrlString(hour.weather?.getOrNull(0)?.icon) generateIconUrlString(hour.weather?.getOrNull(0)?.icon)
) )
constructor(weatherHour: WeatherHour) : this(
weatherHour.datetimeEpoch,
weatherHour.temp,
generateIconUrlString(IconMapper.findIconCode(weatherHour.icon))
)
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeValue(dt) parcel.writeValue(dt)

View File

@@ -1,5 +1,7 @@
package com.appttude.h_mal.atlas_weather.utils package com.appttude.h_mal.atlas_weather.utils
import com.appttude.h_mal.atlas_weather.model.types.UnitType
fun generateIconUrlString(icon: String?): String? { fun generateIconUrlString(icon: String?): String? {
return icon?.let { return icon?.let {
@@ -9,4 +11,6 @@ fun generateIconUrlString(icon: String?): String? {
.append("@2x.png") .append("@2x.png")
.toString() .toString()
} }
} }
fun UnitType.getSymbol(): String = if (this == UnitType.METRIC) "°C" else "°F"

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.atlas_weather.data package com.appttude.h_mal.atlas_weather.data
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
@@ -18,6 +18,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.io.IOException import java.io.IOException
import kotlin.properties.Delegates
import kotlin.test.assertEquals import kotlin.test.assertEquals
class WeatherSourceTest : BaseTest() { class WeatherSourceTest : BaseTest() {
@@ -31,41 +32,43 @@ class WeatherSourceTest : BaseTest() {
@MockK @MockK
lateinit var locationProvider: LocationProviderImpl lateinit var locationProvider: LocationProviderImpl
private lateinit var weatherResponse: WeatherResponse private lateinit var weatherResponse: WeatherApiResponse
private var lat by Delegates.notNull<Double>()
private var long by Delegates.notNull<Double>()
private lateinit var latlon: Pair<Double, Double>
private lateinit var fullWeather: FullWeather
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java) weatherResponse = getTestData("new_response.json", WeatherApiResponse::class.java)
lat = weatherResponse.latitude!!
long = weatherResponse.longitude!!
latlon = Pair(lat, long)
fullWeather = weatherResponse.mapData().apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
} }
@Test @Test
fun fetchDataForSingleLocation_validLocation_validReturn() { fun fetchDataForSingleLocation_validLocation_validReturn() {
// Arrange // Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
val fullWeather = FullWeather(weatherResponse).apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather) val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
// Act // Act
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true) every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
coEvery { coEvery {
repository.getWeatherFromApi( repository.getWeatherFromApi(
weatherResponse.lat.toString(), lat.toString(),
weatherResponse.lon.toString() long.toString()
) )
}.returns(weatherResponse) }.returns(weatherResponse)
coEvery { coEvery {
locationProvider.getLocationNameFromLatLong( locationProvider.getLocationNameFromLatLong(
weatherResponse.lat, lat,
weatherResponse.lon, long,
LocationType.City LocationType.City
) )
}.returns(CURRENT_LOCATION) }.returns(CURRENT_LOCATION)
@@ -82,17 +85,13 @@ class WeatherSourceTest : BaseTest() {
@Test(expected = IOException::class) @Test(expected = IOException::class)
fun fetchDataForSingleLocation_failedWeatherApi_invalidReturn() { fun fetchDataForSingleLocation_failedWeatherApi_invalidReturn() {
// Arrange // Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
// Act // Act
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true) every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
coEvery { coEvery {
repository.getWeatherFromApi( repository.getWeatherFromApi(
weatherResponse.lat.toString(), lat.toString(),
weatherResponse.lon.toString() long.toString()
) )
} throws IOException("Unable fetch data") } throws IOException("Unable fetch data")
@@ -103,23 +102,19 @@ class WeatherSourceTest : BaseTest() {
@Test(expected = IOException::class) @Test(expected = IOException::class)
fun fetchDataForSingleLocation_failedLocation_invalidReturn() { fun fetchDataForSingleLocation_failedLocation_invalidReturn() {
// Arrange // Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
// Act // Act
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true) every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
coEvery { coEvery {
repository.getWeatherFromApi( repository.getWeatherFromApi(
weatherResponse.lat.toString(), lat.toString(),
weatherResponse.lon.toString() long.toString()
) )
} returns weatherResponse } returns weatherResponse
coEvery { coEvery {
locationProvider.getLocationNameFromLatLong( locationProvider.getLocationNameFromLatLong(
weatherResponse.lat, lat,
weatherResponse.lon long
) )
}.throws(IOException()) }.throws(IOException())
@@ -130,14 +125,6 @@ class WeatherSourceTest : BaseTest() {
@Test @Test
fun searchAboveFallbackTime_validLocation_validReturn() { fun searchAboveFallbackTime_validLocation_validReturn() {
// Arrange // Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
val fullWeather = FullWeather(weatherResponse).apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather) val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
// Act // Act
@@ -153,14 +140,6 @@ class WeatherSourceTest : BaseTest() {
@Test @Test
fun forceFetchDataForSingleLocation_validLocation_validReturn() { fun forceFetchDataForSingleLocation_validLocation_validReturn() {
// Arrange // Arrange
val latlon = Pair(
weatherResponse.lat,
weatherResponse.lon
)
val fullWeather = FullWeather(weatherResponse).apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather) val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
// Act // Act
@@ -169,14 +148,14 @@ class WeatherSourceTest : BaseTest() {
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
coEvery { coEvery {
repository.getWeatherFromApi( repository.getWeatherFromApi(
weatherResponse.lat.toString(), weatherResponse.latitude.toString(),
weatherResponse.lon.toString() weatherResponse.longitude.toString()
) )
}.returns(weatherResponse) }.returns(weatherResponse)
coEvery { coEvery {
locationProvider.getLocationNameFromLatLong( locationProvider.getLocationNameFromLatLong(
weatherResponse.lat, lat,
weatherResponse.lon, long,
LocationType.City LocationType.City
) )
}.returns(CURRENT_LOCATION) }.returns(CURRENT_LOCATION)

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.atlas_weather.data.repository package com.appttude.h_mal.atlas_weather.data.repository
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.NewWeatherApi
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
@@ -29,8 +29,7 @@ class RepositoryImplTest : BaseTest() {
lateinit var repository: RepositoryImpl lateinit var repository: RepositoryImpl
@MockK @MockK lateinit var api: NewWeatherApi
lateinit var api: WeatherApi
@MockK @MockK
lateinit var db: AppDatabase lateinit var db: AppDatabase
@@ -86,29 +85,29 @@ class RepositoryImplTest : BaseTest() {
@Test @Test
fun getWeatherFromApi_validLatLong_validSearch() { fun getWeatherFromApi_validLatLong_validSearch() {
//Arrange //Arrange
val mockResponse = createSuccessfulRetrofitMock<WeatherResponse>() val mockResponse = createSuccessfulRetrofitMock<WeatherApiResponse>()
//Act //Act
//create a successful retrofit response //create a successful retrofit response
every { prefs.getUnitsType() } returns (UnitType.METRIC) every { prefs.getUnitsType() } returns (UnitType.METRIC)
coEvery { api.getFromApi("", "") }.returns(mockResponse) coEvery { api.getFromApi(location = "") }.returns(mockResponse)
// Assert // Assert
runBlocking { runBlocking {
val result = repository.getWeatherFromApi("", "") val result = repository.getWeatherFromApi("", "")
assertIs<WeatherResponse>(result) assertIs<WeatherApiResponse>(result)
} }
} }
@Test @Test
fun getWeatherFromApi_validLatLong_invalidResponse() { fun getWeatherFromApi_validLatLong_invalidResponse() {
//Arrange //Arrange
val mockResponse = createErrorRetrofitMock<WeatherResponse>() val mockResponse = createErrorRetrofitMock<WeatherApiResponse>()
//Act //Act
//create a successful retrofit response //create a successful retrofit response
every { prefs.getUnitsType() } returns (UnitType.METRIC) every { prefs.getUnitsType() } returns (UnitType.METRIC)
coEvery { api.getFromApi(any(), any()) } returns (mockResponse) coEvery { api.getFromApi(location = any()) } returns (mockResponse)
// Assert // Assert
val ioExceptionReturned = assertFailsWith<IOException> { val ioExceptionReturned = assertFailsWith<IOException> {

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.atlas_weather.helper package com.appttude.h_mal.atlas_weather.helper
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse import com.appttude.h_mal.atlas_weather.data.network.response.weather.WeatherApiResponse
import com.appttude.h_mal.atlas_weather.data.repository.Repository import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
@@ -17,6 +17,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.io.IOException import java.io.IOException
import kotlin.properties.Delegates
class ServicesHelperTest : BaseTest() { class ServicesHelperTest : BaseTest() {
@@ -31,40 +32,45 @@ class ServicesHelperTest : BaseTest() {
@MockK @MockK
lateinit var locationProvider: LocationProviderImpl lateinit var locationProvider: LocationProviderImpl
lateinit var weatherResponse: WeatherResponse lateinit var weatherResponse: WeatherApiResponse
private var lat by Delegates.notNull<Double>()
private var long by Delegates.notNull<Double>()
private lateinit var latlon: Pair<Double, Double>
private lateinit var fullWeather: FullWeather
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
helper = ServicesHelper(repository, settingsRepository, locationProvider) helper = ServicesHelper(repository, settingsRepository, locationProvider)
weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java) weatherResponse = getTestData("new_response.json", WeatherApiResponse::class.java)
lat = weatherResponse.latitude!!
long = weatherResponse.longitude!!
latlon = Pair(lat, long)
fullWeather = weatherResponse.mapData().apply {
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
}
} }
@Test @Test
fun testWidgetDataAsync_successfulResponse() = runBlocking { fun testWidgetDataAsync_successfulResponse() = runBlocking {
// Arrange // Arrange
val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply { val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
temperatureUnit = "°C"
locationString = CURRENT_LOCATION
})
// Act // Act
coEvery { locationProvider.getCurrentLatLong() } returns Pair( coEvery { locationProvider.getCurrentLatLong() } returns Pair(lat, long)
weatherResponse.lat,
weatherResponse.lon
)
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true) every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
coEvery { coEvery {
repository.getWeatherFromApi( repository.getWeatherFromApi(
weatherResponse.lat.toString(), lat.toString(),
weatherResponse.lon.toString() long.toString()
) )
}.returns(weatherResponse) }.returns(weatherResponse)
coEvery { coEvery {
locationProvider.getLocationNameFromLatLong( locationProvider.getLocationNameFromLatLong(
weatherResponse.lat, lat,
weatherResponse.lon long
) )
}.returns(CURRENT_LOCATION) }.returns(CURRENT_LOCATION)
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit

File diff suppressed because it is too large Load Diff