mirror of
https://github.com/hmalik144/Weather-apps.git
synced 2025-12-10 02:05:20 +00:00
- change location retrieval accuracy
- change location retrieval caching from location provider - expand unit test suite - code refactoring - Imperial units added
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
package com.appttude.h_mal.atlas_weather.data.prefs
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import java.lang.Thread.sleep
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
class PreferenceProviderTest {
|
||||
|
||||
lateinit var preferenceProvider: PreferenceProvider
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
preferenceProvider = PreferenceProvider(ApplicationProvider.getApplicationContext())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveAndGetLastSavedAt() {
|
||||
// Arrange
|
||||
val input = anyString()
|
||||
|
||||
// Act
|
||||
preferenceProvider.saveLastSavedAt(input)
|
||||
|
||||
// Assert
|
||||
val result = preferenceProvider.getLastSavedAt(input)
|
||||
runBlocking { sleep(100) }
|
||||
assert(result < System.currentTimeMillis())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAllKeysAndDeleteKeys() {
|
||||
// Arrange
|
||||
val listOfLocations = listOf(CURRENT_LOCATION, "sydney", "london", "berlin", "dublin")
|
||||
|
||||
// Act
|
||||
listOfLocations.forEach { preferenceProvider.saveLastSavedAt(it) }
|
||||
|
||||
// Assert
|
||||
val result = preferenceProvider.getAllKeysExcludingCurrent()
|
||||
assert(result.size > 0)
|
||||
assertFalse { result.contains(CURRENT_LOCATION) }
|
||||
|
||||
// Act
|
||||
listOfLocations.forEach{ preferenceProvider.deleteLocation(it)}
|
||||
|
||||
// Assert
|
||||
val deletedResults = preferenceProvider.getAllKeysExcludingCurrent()
|
||||
assertFalse { deletedResults.containsAll(listOfLocations) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAndGetFirstTimeRun() {
|
||||
// Act
|
||||
preferenceProvider.setFirstTimeRun()
|
||||
runBlocking { sleep(100) }
|
||||
|
||||
// Assert
|
||||
val result = preferenceProvider.getFirstTimeRun()
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setAndGetUnitsType() {
|
||||
// Arrange
|
||||
val input = UnitType.values()[kotlin.random.Random.nextInt(UnitType.values().size)]
|
||||
|
||||
// Act
|
||||
preferenceProvider.setUnitsType(input)
|
||||
|
||||
// Assert
|
||||
val result = preferenceProvider.getUnitsType()
|
||||
assertEquals(result, input)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
|
||||
import com.appttude.h_mal.atlas_weather.data.room.Converter
|
||||
import com.appttude.h_mal.atlas_weather.data.room.WeatherDao
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
import com.appttude.h_mal.atlas_weather.utils.getOrAwaitValue
|
||||
import io.mockk.mockk
|
||||
@@ -92,16 +92,16 @@ class RoomDatabaseTests {
|
||||
assertNull(dao.getCurrentFullWeatherSingle(id))
|
||||
}
|
||||
|
||||
private fun createEntity(id: String = CURRENT_LOCATION): WeatherEntity {
|
||||
private fun createEntity(id: String = CURRENT_LOCATION): EntityItem {
|
||||
val weather = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
FullWeather()
|
||||
} else {
|
||||
mockk<FullWeather>()
|
||||
}
|
||||
return WeatherEntity(id, weather)
|
||||
return EntityItem(id, weather)
|
||||
}
|
||||
|
||||
private fun createEntityList(size: Int = 4): List<WeatherEntity> {
|
||||
private fun createEntityList(size: Int = 4): List<EntityItem> {
|
||||
return (0.. size).map {
|
||||
val id = UUID.randomUUID().toString()
|
||||
createEntity(id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.appttude.h_mal.atlas_weather.application
|
||||
|
||||
import android.app.Application
|
||||
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.network.WeatherApi
|
||||
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
|
||||
@@ -33,7 +34,8 @@ abstract class BaseAppClass : Application(), KodeinAware {
|
||||
bind() from singleton { RepositoryImpl(instance(), instance(), instance()) }
|
||||
bind() from singleton { SettingsRepositoryImpl(instance()) }
|
||||
bind() from singleton { ServicesHelper(instance(), instance(), instance()) }
|
||||
bind() from provider { ApplicationViewModelFactory(instance(), instance()) }
|
||||
bind() from singleton { WeatherSource(instance(), instance()) }
|
||||
bind() from provider { ApplicationViewModelFactory(this@BaseAppClass, instance(), instance(),instance()) }
|
||||
}
|
||||
|
||||
abstract fun createNetworkModule(): WeatherApi
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.appttude.h_mal.atlas_weather.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.XmlRes
|
||||
import androidx.fragment.app.createViewModelLazy
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.BaseAndroidViewModel
|
||||
import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt
|
||||
import com.appttude.h_mal.atlas_weather.model.ViewState
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
||||
import org.kodein.di.KodeinAware
|
||||
import org.kodein.di.android.x.kodein
|
||||
import org.kodein.di.generic.instance
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Suppress("EmptyMethod", "EmptyMethod")
|
||||
abstract class BasePreferencesFragment<V : BaseAndroidViewModel>(@XmlRes private val preferencesResId: Int) :
|
||||
PreferenceFragmentCompat(),
|
||||
KodeinAware {
|
||||
|
||||
override val kodein by kodein()
|
||||
private val factory by instance<ApplicationViewModelFactory>()
|
||||
|
||||
val viewModel: V by getFragmentViewModel()
|
||||
|
||||
var mActivity: BaseActivity? = null
|
||||
private fun getFragmentViewModel(): Lazy<V> =
|
||||
createViewModelLazy(getGenericClassAt(0), { viewModelStore }, factoryProducer = { factory })
|
||||
|
||||
private var shortAnimationDuration by Delegates.notNull<Int>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mActivity = activity as BaseActivity
|
||||
configureObserver()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(preferencesResId, rootKey)
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
prefs.registerOnSharedPreferenceChangeListener { _, s ->
|
||||
preferenceChanged(s)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureObserver() {
|
||||
viewModel.uiState.observe(viewLifecycleOwner) {
|
||||
when (it) {
|
||||
is ViewState.HasStarted -> onStarted()
|
||||
is ViewState.HasData<*> -> onSuccess(it.data)
|
||||
is ViewState.HasError<*> -> onFailure(it.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun preferenceChanged(key: String) { }
|
||||
|
||||
/**
|
||||
* Called in case of starting operation liveData in viewModel
|
||||
*/
|
||||
open fun onStarted() {
|
||||
mActivity?.onStarted()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in case of success or some data emitted from the liveData in viewModel
|
||||
*/
|
||||
open fun onSuccess(data: Any?) {
|
||||
mActivity?.onSuccess(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in case of failure or some error emitted from the liveData in viewModel
|
||||
*/
|
||||
open fun onFailure(error: Any?) {
|
||||
mActivity?.onFailure(error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.appttude.h_mal.atlas_weather.base.baseViewModels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.appttude.h_mal.atlas_weather.model.ViewState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class BaseAndroidViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val _uiState = MutableLiveData<ViewState>()
|
||||
val uiState: LiveData<ViewState> = _uiState
|
||||
|
||||
|
||||
fun onStart() {
|
||||
_uiState.postValue(ViewState.HasStarted)
|
||||
}
|
||||
|
||||
fun <T : Any> onSuccess(result: T) {
|
||||
_uiState.postValue(ViewState.HasData(result))
|
||||
}
|
||||
|
||||
protected fun <E : Any> onError(error: E) {
|
||||
_uiState.postValue(ViewState.HasError(error))
|
||||
}
|
||||
|
||||
protected var job: Job? = null
|
||||
|
||||
fun cancelOperation() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
job?.run {
|
||||
cancelAndJoin()
|
||||
onSuccess(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.internal.wait
|
||||
import java.util.concurrent.CancellationException
|
||||
|
||||
open class BaseViewModel : ViewModel() {
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.appttude.h_mal.atlas_weather.base.baseViewModels
|
||||
|
||||
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
|
||||
abstract class WeatherViewModel : BaseViewModel() {
|
||||
|
||||
fun createFullWeather(
|
||||
weather: WeatherResponse,
|
||||
locationName: String
|
||||
): FullWeather {
|
||||
return FullWeather(weather).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = locationName
|
||||
}
|
||||
}
|
||||
|
||||
fun createWeatherEntity(
|
||||
locationName: String,
|
||||
weather: FullWeather
|
||||
): WeatherEntity {
|
||||
weather.apply {
|
||||
locationString = locationName
|
||||
}
|
||||
|
||||
return WeatherEntity(locationName, weather)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.appttude.h_mal.atlas_weather.data
|
||||
|
||||
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
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.model.types.LocationType
|
||||
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
import java.io.IOException
|
||||
|
||||
class WeatherSource(
|
||||
val repository: Repository,
|
||||
private val locationProvider: LocationProvider
|
||||
) {
|
||||
|
||||
@Throws(IOException::class)
|
||||
suspend fun getWeather(
|
||||
latLon: Pair<Double, Double>,
|
||||
locationName: String? = null,
|
||||
locationType: LocationType = LocationType.Town
|
||||
): FullWeather {
|
||||
val location = locationName ?: CURRENT_LOCATION
|
||||
|
||||
// Has the search been conducted in the last 5 minutes
|
||||
return if (repository.isSearchValid(location)) {
|
||||
fetchWeather(latLon, location, locationType)
|
||||
} else {
|
||||
val weather = repository.getSingleWeather(location)
|
||||
repository.saveCurrentWeatherToRoom(weather)
|
||||
weather.weather
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
suspend fun forceFetchWeather(latLon: Pair<Double, Double>,
|
||||
locationType: LocationType = LocationType.Town): FullWeather {
|
||||
// get data from database
|
||||
val weatherEntity = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
|
||||
// check unit type - if same do nothing
|
||||
val units = if (repository.getUnitType() == UnitType.METRIC) "°C" else "°F"
|
||||
if (weatherEntity.weather.temperatureUnit == units) return weatherEntity.weather
|
||||
// load data for forced
|
||||
return fetchWeather(
|
||||
Pair(latLon.first, latLon.second),
|
||||
CURRENT_LOCATION, locationType
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchWeather(
|
||||
latLon: Pair<Double, Double>,
|
||||
locationName: String,
|
||||
locationType: LocationType = LocationType.Town
|
||||
): FullWeather {
|
||||
// Get weather from api
|
||||
val weather = repository
|
||||
.getWeatherFromApi(latLon.first.toString(), latLon.second.toString())
|
||||
val currentLocation =
|
||||
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon, locationType)
|
||||
val unit = repository.getUnitType()
|
||||
val fullWeather = FullWeather(weather).apply {
|
||||
temperatureUnit = if (unit == UnitType.METRIC) "°C" else "°F"
|
||||
locationString = currentLocation
|
||||
}
|
||||
val entityItem = EntityItem(locationName, fullWeather)
|
||||
// Save data if not null
|
||||
repository.saveLastSavedAt(locationName)
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
|
||||
return fullWeather
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.appttude.h_mal.atlas_weather.data.location
|
||||
|
||||
import android.Manifest
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||
|
||||
interface LocationProvider {
|
||||
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
suspend fun getCurrentLatLong(): Pair<Double, Double>
|
||||
fun getLatLongFromLocationName(location: String): Pair<Double, Double>
|
||||
suspend fun getLocationNameFromLatLong(
|
||||
|
||||
@@ -3,11 +3,8 @@ package com.appttude.h_mal.atlas_weather.data.location
|
||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Address
|
||||
import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||
import com.google.android.gms.location.LocationServices
|
||||
|
||||
@@ -4,11 +4,13 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||
|
||||
/**
|
||||
* Shared preferences to save & load last timestamp
|
||||
*/
|
||||
const val LOCATION_CONST = "location_"
|
||||
const val UNIT_CONST = "UnitType"
|
||||
|
||||
class PreferenceProvider(
|
||||
context: Context
|
||||
@@ -30,7 +32,7 @@ class PreferenceProvider(
|
||||
return preference.getLong(locationName, 0L)
|
||||
}
|
||||
|
||||
fun getAllKeys() = preference.all.keys.apply {
|
||||
fun getAllKeysExcludingCurrent() = preference.all.keys.apply {
|
||||
remove(CURRENT_LOCATION)
|
||||
}
|
||||
|
||||
@@ -50,4 +52,13 @@ class PreferenceProvider(
|
||||
return preference.getBoolean("widget_black_background", false)
|
||||
}
|
||||
|
||||
fun setUnitsType(type: UnitType) {
|
||||
preference.edit().putString(UNIT_CONST, type.name).apply()
|
||||
}
|
||||
|
||||
fun getUnitsType(): UnitType {
|
||||
val unit = preference.getString(UNIT_CONST, UnitType.METRIC.name)
|
||||
return UnitType.getByName(unit) ?: UnitType.METRIC
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,20 +2,22 @@ package com.appttude.h_mal.atlas_weather.data.repository
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||
|
||||
interface Repository {
|
||||
|
||||
suspend fun getWeatherFromApi(lat: String, long: String): WeatherResponse
|
||||
suspend fun saveCurrentWeatherToRoom(weatherEntity: WeatherEntity)
|
||||
suspend fun saveWeatherListToRoom(list: List<WeatherEntity>)
|
||||
fun loadRoomWeatherLiveData(): LiveData<List<WeatherEntity>>
|
||||
suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem)
|
||||
suspend fun saveWeatherListToRoom(list: List<EntityItem>)
|
||||
fun loadRoomWeatherLiveData(): LiveData<List<EntityItem>>
|
||||
suspend fun loadWeatherList(): List<String>
|
||||
fun loadCurrentWeatherFromRoom(id: String): LiveData<WeatherEntity>
|
||||
suspend fun loadSingleCurrentWeatherFromRoom(id: String): WeatherEntity
|
||||
fun loadCurrentWeatherFromRoom(id: String): LiveData<EntityItem>
|
||||
suspend fun loadSingleCurrentWeatherFromRoom(id: String): EntityItem
|
||||
fun isSearchValid(locationName: String): Boolean
|
||||
fun saveLastSavedAt(locationName: String)
|
||||
suspend fun deleteSavedWeatherEntry(locationName: String): Boolean
|
||||
fun getSavedLocations(): List<String>
|
||||
suspend fun getSingleWeather(locationName: String): WeatherEntity
|
||||
suspend fun getSingleWeather(locationName: String): EntityItem
|
||||
fun getUnitType() : UnitType
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherRe
|
||||
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.room.AppDatabase
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
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.utils.FALLBACK_TIME
|
||||
|
||||
|
||||
@@ -20,15 +21,15 @@ class RepositoryImpl(
|
||||
lat: String,
|
||||
long: String
|
||||
): WeatherResponse {
|
||||
return responseUnwrap { api.getFromApi(lat, long) }
|
||||
return responseUnwrap { api.getFromApi(lat, long, units = prefs.getUnitsType().name.lowercase()) }
|
||||
}
|
||||
|
||||
override suspend fun saveCurrentWeatherToRoom(weatherEntity: WeatherEntity) {
|
||||
db.getWeatherDao().upsertFullWeather(weatherEntity)
|
||||
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) {
|
||||
db.getWeatherDao().upsertFullWeather(entityItem)
|
||||
}
|
||||
|
||||
override suspend fun saveWeatherListToRoom(
|
||||
list: List<WeatherEntity>
|
||||
list: List<EntityItem>
|
||||
) {
|
||||
db.getWeatherDao().upsertListOfFullWeather(list)
|
||||
}
|
||||
@@ -65,11 +66,15 @@ class RepositoryImpl(
|
||||
}
|
||||
|
||||
override fun getSavedLocations(): List<String> {
|
||||
return prefs.getAllKeys().toList()
|
||||
return prefs.getAllKeysExcludingCurrent().toList()
|
||||
}
|
||||
|
||||
override suspend fun getSingleWeather(locationName: String): WeatherEntity {
|
||||
override suspend fun getSingleWeather(locationName: String): EntityItem {
|
||||
return db.getWeatherDao().getCurrentFullWeatherSingle(locationName)
|
||||
}
|
||||
|
||||
override fun getUnitType(): UnitType {
|
||||
return prefs.getUnitsType()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.appttude.h_mal.atlas_weather.data.repository
|
||||
|
||||
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||
|
||||
interface SettingsRepository {
|
||||
fun isNotificationsEnabled(): Boolean
|
||||
fun setFirstTime()
|
||||
fun getFirstTime(): Boolean
|
||||
fun isBlackBackground(): Boolean
|
||||
fun saveUnitType(unitType: UnitType)
|
||||
fun getUnitType(): UnitType
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.appttude.h_mal.atlas_weather.data.repository
|
||||
|
||||
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
|
||||
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||
|
||||
class SettingsRepositoryImpl(
|
||||
private val prefs: PreferenceProvider
|
||||
@@ -12,4 +13,13 @@ class SettingsRepositoryImpl(
|
||||
override fun getFirstTime(): Boolean = prefs.getFirstTimeRun()
|
||||
|
||||
override fun isBlackBackground() = prefs.isWidgetBackground()
|
||||
|
||||
|
||||
override fun saveUnitType(unitType: UnitType) {
|
||||
prefs.setUnitsType(unitType)
|
||||
}
|
||||
|
||||
override fun getUnitType(): UnitType {
|
||||
return prefs.getUnitsType()
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
|
||||
@Database(
|
||||
entities = [WeatherEntity::class],
|
||||
version = 1,
|
||||
entities = [EntityItem::class],
|
||||
version = 2,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converter::class)
|
||||
|
||||
@@ -6,30 +6,30 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
|
||||
@Dao
|
||||
interface WeatherDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun upsertFullWeather(item: WeatherEntity)
|
||||
fun upsertFullWeather(item: EntityItem)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun upsertListOfFullWeather(items: List<WeatherEntity>)
|
||||
fun upsertListOfFullWeather(items: List<EntityItem>)
|
||||
|
||||
@Query("SELECT * FROM WeatherEntity WHERE id = :userId LIMIT 1")
|
||||
fun getCurrentFullWeather(userId: String): LiveData<WeatherEntity>
|
||||
@Query("SELECT * FROM EntityItem WHERE id = :userId LIMIT 1")
|
||||
fun getCurrentFullWeather(userId: String): LiveData<EntityItem>
|
||||
|
||||
@Query("SELECT * FROM WeatherEntity WHERE id = :userId LIMIT 1")
|
||||
fun getCurrentFullWeatherSingle(userId: String): WeatherEntity
|
||||
@Query("SELECT * FROM EntityItem WHERE id = :userId LIMIT 1")
|
||||
fun getCurrentFullWeatherSingle(userId: String): EntityItem
|
||||
|
||||
@Query("SELECT * FROM WeatherEntity WHERE id != :id")
|
||||
fun getAllFullWeatherWithoutCurrent(id: String = CURRENT_LOCATION): LiveData<List<WeatherEntity>>
|
||||
@Query("SELECT * FROM EntityItem WHERE id != :id")
|
||||
fun getAllFullWeatherWithoutCurrent(id: String = CURRENT_LOCATION): LiveData<List<EntityItem>>
|
||||
|
||||
@Query("SELECT * FROM WeatherEntity WHERE id != :id")
|
||||
fun getWeatherListWithoutCurrent(id: String = CURRENT_LOCATION): List<WeatherEntity>
|
||||
@Query("SELECT * FROM EntityItem WHERE id != :id")
|
||||
fun getWeatherListWithoutCurrent(id: String = CURRENT_LOCATION): List<EntityItem>
|
||||
|
||||
@Query("DELETE FROM WeatherEntity WHERE id = :userId")
|
||||
@Query("DELETE FROM EntityItem WHERE id = :userId")
|
||||
fun deleteEntry(userId: String): Int
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
const val CURRENT_LOCATION = "CurrentLocation"
|
||||
|
||||
@Entity
|
||||
data class WeatherEntity(
|
||||
data class EntityItem(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: String,
|
||||
val weather: FullWeather
|
||||
@@ -10,7 +10,8 @@ import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||
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.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
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.InnerWidgetData
|
||||
@@ -50,10 +51,10 @@ class ServicesHelper(
|
||||
temperatureUnit = "°C"
|
||||
locationString = currentLocation
|
||||
}
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, fullWeather)
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
// Save data if not null
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(weatherEntity)
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
@@ -120,15 +121,15 @@ class ServicesHelper(
|
||||
}
|
||||
|
||||
val fullWeather = FullWeather(weather).apply {
|
||||
temperatureUnit = "°C"
|
||||
temperatureUnit = if (repository.getUnitType() == UnitType.METRIC) "°C" else "°F"
|
||||
locationString = currentLocation
|
||||
}
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, fullWeather)
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
// Save data to database
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(weatherEntity)
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
|
||||
val data = createWidgetWeatherCollection(weatherEntity, currentLocation)
|
||||
val data = createWidgetWeatherCollection(entityItem, currentLocation)
|
||||
return WidgetState.HasData(data)
|
||||
}
|
||||
|
||||
@@ -201,7 +202,10 @@ class ServicesHelper(
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWidgetWeatherCollection(result: WeatherEntity, locationName: String): WidgetWeatherCollection {
|
||||
private fun createWidgetWeatherCollection(
|
||||
result: EntityItem,
|
||||
locationName: String
|
||||
): WidgetWeatherCollection {
|
||||
val widgetData = result.weather.let {
|
||||
val bitmap = it.current?.icon
|
||||
val temp = it.current?.temp?.toInt().toString()
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.appttude.h_mal.atlas_weather.model.forecast
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.Hour
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ data class WeatherDisplay(
|
||||
) {
|
||||
}
|
||||
|
||||
constructor(entity: WeatherEntity) : this(
|
||||
constructor(entity: EntityItem) : this(
|
||||
entity.weather.current?.temp,
|
||||
entity.weather.temperatureUnit,
|
||||
entity.id,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.appttude.h_mal.atlas_weather.model.types
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
enum class UnitType {
|
||||
METRIC,
|
||||
IMPERIAL;
|
||||
|
||||
companion object {
|
||||
fun getByName(name: String?): UnitType? {
|
||||
return values().firstOrNull {
|
||||
it.name.lowercase(Locale.ROOT) == name?.lowercase(
|
||||
Locale.ROOT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.appttude.h_mal.atlas_weather.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
@@ -10,7 +9,6 @@ import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.appttude.h_mal.atlas_weather.base.BaseActivity
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import kotlinx.android.synthetic.main.activity_main_navigation.toolbar
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package com.appttude.h_mal.atlas_weather.utils
|
||||
|
||||
val FALLBACK_TIME: Long = 300000L
|
||||
const val FALLBACK_TIME: Long = 300000L
|
||||
@@ -1,14 +1,18 @@
|
||||
package com.appttude.h_mal.atlas_weather.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
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.repository.RepositoryImpl
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
|
||||
|
||||
|
||||
class ApplicationViewModelFactory(
|
||||
private val application: Application,
|
||||
private val locationProvider: LocationProvider,
|
||||
private val repository: RepositoryImpl
|
||||
private val source: WeatherSource,
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -17,12 +21,16 @@ class ApplicationViewModelFactory(
|
||||
return when {
|
||||
isAssignableFrom(WorldViewModel::class.java) -> WorldViewModel(
|
||||
locationProvider,
|
||||
repository
|
||||
source
|
||||
)
|
||||
|
||||
isAssignableFrom(MainViewModel::class.java) -> MainViewModel(
|
||||
locationProvider,
|
||||
repository
|
||||
source
|
||||
)
|
||||
|
||||
isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(
|
||||
application, locationProvider, source, settingsRepository
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||
|
||||
@@ -2,23 +2,23 @@ package com.appttude.h_mal.atlas_weather.viewmodel
|
||||
|
||||
import android.Manifest
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.BaseViewModel
|
||||
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.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.WeatherViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainViewModel(
|
||||
private val locationProvider: LocationProvider,
|
||||
private val repository: Repository
|
||||
) : WeatherViewModel() {
|
||||
private val weatherSource: WeatherSource
|
||||
) : BaseViewModel() {
|
||||
|
||||
init {
|
||||
repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever {w ->
|
||||
weatherSource.repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever { w ->
|
||||
w?.let {
|
||||
val weather = WeatherDisplay(it)
|
||||
onSuccess(weather)
|
||||
@@ -31,25 +31,11 @@ class MainViewModel(
|
||||
onStart()
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
// Has the search been conducted in the last 5 minutes
|
||||
val weatherEntity = if (repository.isSearchValid(CURRENT_LOCATION)) {
|
||||
// Get location
|
||||
val latLong = locationProvider.getCurrentLatLong()
|
||||
// Get weather from api
|
||||
val weather = repository
|
||||
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
|
||||
val currentLocation =
|
||||
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
|
||||
val fullWeather = createFullWeather(weather, currentLocation)
|
||||
WeatherEntity(CURRENT_LOCATION, fullWeather)
|
||||
} else {
|
||||
repository.getSingleWeather(CURRENT_LOCATION)
|
||||
}
|
||||
// Save data if not null
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(weatherEntity)
|
||||
// Get location
|
||||
val latLong = locationProvider.getCurrentLatLong()
|
||||
weatherSource.getWeather(latLon = latLong)
|
||||
} catch (e: Exception) {
|
||||
onError(e.message!!)
|
||||
onError(e.message ?: "Retrieving weather failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.appttude.h_mal.atlas_weather.viewmodel
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Application
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.appttude.h_mal.atlas_weather.application.AppClass
|
||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.BaseAndroidViewModel
|
||||
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.repository.SettingsRepository
|
||||
import com.appttude.h_mal.atlas_weather.widget.NewAppWidget
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
class SettingsViewModel(
|
||||
application: Application,
|
||||
private val locationProvider: LocationProvider,
|
||||
private val weatherSource: WeatherSource,
|
||||
private val settingsRepository: SettingsRepository
|
||||
) : BaseAndroidViewModel(application) {
|
||||
|
||||
private fun getContext() = getApplication<AppClass>().applicationContext
|
||||
|
||||
fun updateWidget() {
|
||||
val context = getContext()
|
||||
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
|
||||
val widgetManager = AppWidgetManager.getInstance(context)
|
||||
val ids =
|
||||
widgetManager.getAppWidgetIds(
|
||||
ComponentName(
|
||||
context,
|
||||
NewAppWidget::class.java
|
||||
)
|
||||
)
|
||||
AppWidgetManager.getInstance(context)
|
||||
.notifyAppWidgetViewDataChanged(ids, R.id.whole_widget_view)
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
fun refreshWeatherData() {
|
||||
onStart()
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
getContext(),
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
// Get location
|
||||
val latLong = locationProvider.getCurrentLatLong()
|
||||
weatherSource.forceFetchWeather(latLong)
|
||||
}
|
||||
updateWidget()
|
||||
val units = settingsRepository.getUnitType().name.lowercase(Locale.ROOT)
|
||||
onSuccess("Units have been changes to $units")
|
||||
} catch (e: Exception) {
|
||||
onError(e.message ?: "Retrieving weather failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
package com.appttude.h_mal.atlas_weather.viewmodel
|
||||
|
||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.BaseViewModel
|
||||
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.network.response.forecast.WeatherResponse
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.WeatherViewModel
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
|
||||
@@ -16,14 +18,12 @@ const val ALL_LOADED = "all_loaded"
|
||||
|
||||
class WorldViewModel(
|
||||
private val locationProvider: LocationProvider,
|
||||
private val repository: Repository
|
||||
) : WeatherViewModel() {
|
||||
private val weatherSource: WeatherSource
|
||||
) : BaseViewModel() {
|
||||
private var currentLocation: String? = null
|
||||
|
||||
private val weatherListLiveData = repository.loadRoomWeatherLiveData()
|
||||
|
||||
init {
|
||||
weatherListLiveData.observeForever {
|
||||
weatherSource.repository.loadRoomWeatherLiveData().observeForever {
|
||||
val list = it.map { data ->
|
||||
WeatherDisplay(data)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class WorldViewModel(
|
||||
|
||||
fun getSingleLocation(locationName: String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val entity = repository.getSingleWeather(locationName)
|
||||
val entity = weatherSource.repository.getSingleWeather(locationName)
|
||||
val item = WeatherDisplay(entity)
|
||||
onSuccess(item)
|
||||
}
|
||||
@@ -47,14 +47,8 @@ class WorldViewModel(
|
||||
onStart()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val weatherEntity = if (repository.isSearchValid(locationName)) {
|
||||
createWeatherEntity(locationName)
|
||||
} else {
|
||||
repository.getSingleWeather(locationName)
|
||||
}
|
||||
searchWeatherForLocation(locationName)
|
||||
onSuccess(Unit)
|
||||
repository.saveCurrentWeatherToRoom(weatherEntity)
|
||||
repository.saveLastSavedAt(weatherEntity.id)
|
||||
} catch (e: IOException) {
|
||||
onError(e.message!!)
|
||||
}
|
||||
@@ -64,29 +58,20 @@ class WorldViewModel(
|
||||
fun fetchDataForSingleLocationSearch(locationName: String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
onStart()
|
||||
// Check if location exists
|
||||
if (repository.getSavedLocations().contains(locationName)) {
|
||||
// Check if location already exists
|
||||
if (weatherSource.repository.getSavedLocations().contains(locationName)) {
|
||||
onError("$locationName already exists")
|
||||
return@launch
|
||||
}
|
||||
|
||||
try {
|
||||
// Get weather from api
|
||||
val entityItem = createWeatherEntity(locationName)
|
||||
|
||||
// retrieved location name
|
||||
val retrievedLocation = locationProvider.getLocationNameFromLatLong(
|
||||
entityItem.weather.lat,
|
||||
entityItem.weather.lon,
|
||||
LocationType.City
|
||||
)
|
||||
if (repository.getSavedLocations().contains(retrievedLocation)) {
|
||||
val weather = searchWeatherForLocation(locationName)
|
||||
val retrievedLocation = weather.locationString
|
||||
// Check if location exists in stored
|
||||
if (weatherSource.repository.getSavedLocations().contains(retrievedLocation)) {
|
||||
onError("$retrievedLocation already exists")
|
||||
return@launch
|
||||
}
|
||||
// Save data if not null
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
repository.saveLastSavedAt(retrievedLocation)
|
||||
onSuccess("$retrievedLocation saved")
|
||||
} catch (e: IOException) {
|
||||
onError(e.message!!)
|
||||
@@ -96,30 +81,24 @@ class WorldViewModel(
|
||||
|
||||
fun fetchAllLocations() {
|
||||
onStart()
|
||||
if (!repository.isSearchValid(ALL_LOADED)) {
|
||||
if (!weatherSource.repository.isSearchValid(ALL_LOADED)) {
|
||||
onSuccess(Unit)
|
||||
return
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val list = mutableListOf<WeatherEntity>()
|
||||
repository.loadWeatherList().forEach { locationName ->
|
||||
val list = mutableListOf<Deferred<FullWeather>>()
|
||||
weatherSource.repository.loadWeatherList().forEach { locationName ->
|
||||
// If search not valid move onto next in loop
|
||||
if (!repository.isSearchValid(locationName)) return@forEach
|
||||
if (!weatherSource.repository.isSearchValid(locationName)) return@forEach
|
||||
|
||||
try {
|
||||
val entity = createWeatherEntity(locationName)
|
||||
list.add(entity)
|
||||
repository.saveLastSavedAt(locationName)
|
||||
} catch (e: IOException) {
|
||||
}
|
||||
val task = async{ searchWeatherForLocation(locationName) }
|
||||
list.add(task)
|
||||
}
|
||||
repository.saveWeatherListToRoom(list)
|
||||
repository.saveLastSavedAt(ALL_LOADED)
|
||||
list.awaitAll()
|
||||
onSuccess(Unit)
|
||||
} catch (e: IOException) {
|
||||
onError(e.message!!)
|
||||
} finally {
|
||||
onSuccess(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,34 +107,29 @@ class WorldViewModel(
|
||||
onStart()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val success = repository.deleteSavedWeatherEntry(locationName)
|
||||
val success = weatherSource.repository.deleteSavedWeatherEntry(locationName)
|
||||
if (!success) {
|
||||
onError("Failed to delete")
|
||||
} else {
|
||||
onSuccess(Unit)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
onError(e.message!!)
|
||||
} finally {
|
||||
onSuccess(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getWeather(locationName: String): WeatherResponse {
|
||||
private suspend fun searchWeatherForLocation(locationName: String): FullWeather {
|
||||
// Get location
|
||||
val latLong =
|
||||
locationProvider.getLatLongFromLocationName(locationName)
|
||||
val lat = latLong.first
|
||||
val lon = latLong.second
|
||||
|
||||
// Get weather from api
|
||||
return repository.getWeatherFromApi(lat.toString(), lon.toString())
|
||||
}
|
||||
|
||||
private suspend fun createWeatherEntity(locationName: String): WeatherEntity {
|
||||
val weather = getWeather(locationName)
|
||||
// Search for location from provider (provider location name maybe different from #locationName)
|
||||
val location =
|
||||
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon, LocationType.City)
|
||||
val fullWeather = createFullWeather(weather, location)
|
||||
return createWeatherEntity(location, fullWeather)
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
latLong.first,
|
||||
latLong.second,
|
||||
LocationType.City
|
||||
)
|
||||
return weatherSource.getWeather(latLong, location, LocationType.City)
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,6 @@ class WidgetJobServiceIntent : BaseWidgetServiceIntentClass<NewAppWidget>() {
|
||||
setupErrorView(id, error)
|
||||
}
|
||||
}
|
||||
|
||||
else -> return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,11 @@
|
||||
<string name="retrieve_warning">Unable to retrieve weather</string>
|
||||
<string name="empty_retrieve_warning">Make sure you are connected to the internet and have location permissions granted</string>
|
||||
<string name="no_weather_to_display">No weather to display</string>
|
||||
<string name="unit_key">Units</string>
|
||||
<string name="widget_black_background">widget_black_background</string>
|
||||
|
||||
<string-array name="units">
|
||||
<item>Metric</item>
|
||||
<item>Imperial</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -16,22 +16,22 @@ import kotlinx.android.synthetic.main.fragment_home.swipe_refresh
|
||||
|
||||
class WorldItemFragment : BaseFragment<WorldViewModel>(R.layout.fragment_home) {
|
||||
|
||||
private var param1: String? = null
|
||||
private var retrievedLocationName: String? = null
|
||||
|
||||
private lateinit var recyclerAdapter: WeatherRecyclerAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
param1 = WorldItemFragmentArgs.fromBundle(requireArguments()).locationName
|
||||
param1?.let { viewModel.setLocation(it) }
|
||||
retrievedLocationName = WorldItemFragmentArgs.fromBundle(requireArguments()).locationName
|
||||
retrievedLocationName?.let { viewModel.setLocation(it) }
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
recyclerAdapter = WeatherRecyclerAdapter {
|
||||
val directions =
|
||||
WorldItemFragmentDirections.actionWorldItemFragmentToFurtherDetailsFragment(it)
|
||||
val directions = WorldItemFragmentDirections
|
||||
.actionWorldItemFragmentToFurtherDetailsFragment(it)
|
||||
navigateTo(directions)
|
||||
}
|
||||
|
||||
@@ -42,14 +42,14 @@ class WorldItemFragment : BaseFragment<WorldViewModel>(R.layout.fragment_home) {
|
||||
|
||||
swipe_refresh.apply {
|
||||
setOnRefreshListener {
|
||||
param1?.let {
|
||||
retrievedLocationName?.let {
|
||||
viewModel.fetchDataForSingleLocation(it)
|
||||
isRefreshing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
param1?.let { viewModel.getSingleLocation(it) }
|
||||
retrievedLocationName?.let { viewModel.getSingleLocation(it) }
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Any?) {
|
||||
|
||||
@@ -1,46 +1,24 @@
|
||||
package com.appttude.h_mal.monoWeather.ui.settings
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.appttude.h_mal.atlas_weather.widget.NewAppWidget
|
||||
import com.appttude.h_mal.atlas_weather.base.BasePreferencesFragment
|
||||
import com.appttude.h_mal.atlas_weather.utils.displayToast
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.SettingsViewModel
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
class SettingsFragment : BasePreferencesFragment<SettingsViewModel>(R.xml.prefs_screen) {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.prefs_screen, rootKey)
|
||||
|
||||
//listener on changed sort order preference:
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
prefs.registerOnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == "temp_units") {
|
||||
val intent = Intent(requireContext(), NewAppWidget::class.java)
|
||||
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
val ids = AppWidgetManager.getInstance(requireContext())
|
||||
.getAppWidgetIds(ComponentName(requireContext(), NewAppWidget::class.java))
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||
requireContext().sendBroadcast(intent)
|
||||
}
|
||||
|
||||
if (key == "widget_black_background") {
|
||||
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
|
||||
val widgetManager = AppWidgetManager.getInstance(requireContext())
|
||||
val ids =
|
||||
widgetManager.getAppWidgetIds(
|
||||
ComponentName(
|
||||
requireContext(),
|
||||
NewAppWidget::class.java
|
||||
)
|
||||
)
|
||||
AppWidgetManager.getInstance(requireContext())
|
||||
.notifyAppWidgetViewDataChanged(ids, R.id.whole_widget_view)
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||
requireContext().sendBroadcast(intent)
|
||||
override fun preferenceChanged(key: String) {
|
||||
when (key) {
|
||||
"UnitType" -> viewModel.refreshWeatherData()
|
||||
"widget_black_background" -> {
|
||||
viewModel.updateWidget()
|
||||
displayToast("Widget background has been updates")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Any?) {
|
||||
super.onSuccess(data)
|
||||
if (data is String) displayToast(data)
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:contentDescription="@string/image_string"
|
||||
app:tint="@color/colorAccent"
|
||||
app:srcCompat="@drawable/maps_and_flags" />
|
||||
app:srcCompat="@drawable/maps_and_flags"
|
||||
app:tint="@color/colorAccent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/location_main_4"
|
||||
@@ -51,11 +51,11 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="9dp"
|
||||
android:text="@string/degrees_c"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintLeft_toRightOf="@id/temp_main_4"
|
||||
app:layout_constraintTop_toTopOf="@id/temp_main_4" />
|
||||
app:layout_constraintTop_toTopOf="@id/temp_main_4"
|
||||
tools:text="@string/degrees_c" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_main_4"
|
||||
@@ -64,11 +64,11 @@
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:contentDescription="@string/image_string"
|
||||
app:tint="@color/colorAccent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/temp_main_4"
|
||||
app:srcCompat="@drawable/cloud_symbol" />
|
||||
app:srcCompat="@drawable/cloud_symbol"
|
||||
app:tint="@color/colorAccent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/condition_main_4"
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<ListPreference
|
||||
android:title="Weather units"
|
||||
android:entries="@array/units"
|
||||
android:entryValues="@array/units"
|
||||
android:defaultValue="Metric"
|
||||
android:key="UnitType"
|
||||
/>
|
||||
<PreferenceCategory android:title="Widget Settings">
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="widget_black_background"
|
||||
android:key="@string/widget_black_background"
|
||||
android:title="Set widget background black" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
@@ -0,0 +1,196 @@
|
||||
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.network.response.forecast.WeatherResponse
|
||||
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.EntityItem
|
||||
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.weather.FullWeather
|
||||
import com.appttude.h_mal.atlas_weather.utils.BaseTest
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.InjectMockKs
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class WeatherSourceTest : BaseTest() {
|
||||
|
||||
@InjectMockKs
|
||||
lateinit var weatherSource: WeatherSource
|
||||
|
||||
@MockK
|
||||
lateinit var repository: RepositoryImpl
|
||||
|
||||
@MockK
|
||||
lateinit var locationProvider: LocationProviderImpl
|
||||
|
||||
private lateinit var weatherResponse: WeatherResponse
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchDataForSingleLocation_validLocation_validReturn() {
|
||||
// Arrange
|
||||
val latlon = Pair(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon
|
||||
)
|
||||
val fullWeather = FullWeather(weatherResponse).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = CURRENT_LOCATION
|
||||
}
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
|
||||
|
||||
// Act
|
||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
|
||||
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
|
||||
coEvery {
|
||||
repository.getWeatherFromApi(
|
||||
weatherResponse.lat.toString(),
|
||||
weatherResponse.lon.toString()
|
||||
)
|
||||
}.returns(weatherResponse)
|
||||
coEvery {
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon,
|
||||
LocationType.City
|
||||
)
|
||||
}.returns(CURRENT_LOCATION)
|
||||
every { repository.getUnitType() } returns UnitType.METRIC
|
||||
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(entityItem) } returns Unit
|
||||
|
||||
// Assert
|
||||
val result =
|
||||
runBlocking { weatherSource.getWeather(latlon, locationType = LocationType.City) }
|
||||
assertEquals(result, fullWeather)
|
||||
}
|
||||
|
||||
@Test(expected = IOException::class)
|
||||
fun fetchDataForSingleLocation_failedWeatherApi_invalidReturn() {
|
||||
// Arrange
|
||||
val latlon = Pair(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon
|
||||
)
|
||||
|
||||
// Act
|
||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
|
||||
coEvery {
|
||||
repository.getWeatherFromApi(
|
||||
weatherResponse.lat.toString(),
|
||||
weatherResponse.lon.toString()
|
||||
)
|
||||
} throws IOException("Unable fetch data")
|
||||
|
||||
// Assert
|
||||
runBlocking { weatherSource.getWeather(latlon) }
|
||||
}
|
||||
|
||||
@Test(expected = IOException::class)
|
||||
fun fetchDataForSingleLocation_failedLocation_invalidReturn() {
|
||||
// Arrange
|
||||
val latlon = Pair(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon
|
||||
)
|
||||
|
||||
// Act
|
||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
|
||||
coEvery {
|
||||
repository.getWeatherFromApi(
|
||||
weatherResponse.lat.toString(),
|
||||
weatherResponse.lon.toString()
|
||||
)
|
||||
} returns weatherResponse
|
||||
coEvery {
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon
|
||||
)
|
||||
}.throws(IOException())
|
||||
|
||||
// Assert
|
||||
runBlocking { weatherSource.getWeather(latlon) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchAboveFallbackTime_validLocation_validReturn() {
|
||||
// Arrange
|
||||
val latlon = Pair(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon
|
||||
)
|
||||
val fullWeather = FullWeather(weatherResponse).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = CURRENT_LOCATION
|
||||
}
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
|
||||
// Act
|
||||
coEvery { repository.getSingleWeather(CURRENT_LOCATION) }.returns(entityItem)
|
||||
coEvery { repository.saveCurrentWeatherToRoom(entityItem) }.returns(Unit)
|
||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(false)
|
||||
|
||||
// Assert
|
||||
val result = runBlocking { weatherSource.getWeather(latlon) }
|
||||
assertEquals(result, fullWeather)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forceFetchDataForSingleLocation_validLocation_validReturn() {
|
||||
// Arrange
|
||||
val latlon = Pair(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon
|
||||
)
|
||||
val fullWeather = FullWeather(weatherResponse).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = CURRENT_LOCATION
|
||||
}
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
|
||||
// Act
|
||||
coEvery { repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION) } returns entityItem
|
||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
|
||||
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
|
||||
coEvery {
|
||||
repository.getWeatherFromApi(
|
||||
weatherResponse.lat.toString(),
|
||||
weatherResponse.lon.toString()
|
||||
)
|
||||
}.returns(weatherResponse)
|
||||
coEvery {
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon,
|
||||
LocationType.City
|
||||
)
|
||||
}.returns(CURRENT_LOCATION)
|
||||
every { repository.getUnitType() } returns UnitType.METRIC
|
||||
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(entityItem) } returns Unit
|
||||
|
||||
// Assert
|
||||
val result = runBlocking {
|
||||
weatherSource.forceFetchWeather(
|
||||
latlon,
|
||||
locationType = LocationType.City
|
||||
)
|
||||
}
|
||||
assertEquals(result, fullWeather)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherRe
|
||||
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.room.AppDatabase
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
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.utils.BaseTest
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import io.mockk.MockKAnnotations
|
||||
@@ -89,6 +90,7 @@ class RepositoryImplTest : BaseTest() {
|
||||
|
||||
//Act
|
||||
//create a successful retrofit response
|
||||
every { prefs.getUnitsType() } returns (UnitType.METRIC)
|
||||
coEvery { api.getFromApi("", "") }.returns(mockResponse)
|
||||
|
||||
// Assert
|
||||
@@ -105,6 +107,7 @@ class RepositoryImplTest : BaseTest() {
|
||||
|
||||
//Act
|
||||
//create a successful retrofit response
|
||||
every { prefs.getUnitsType() } returns (UnitType.METRIC)
|
||||
coEvery { api.getFromApi(any(), any()) } returns (mockResponse)
|
||||
|
||||
// Assert
|
||||
@@ -119,7 +122,7 @@ class RepositoryImplTest : BaseTest() {
|
||||
@Test
|
||||
fun loadWeatherList_validResponse() {
|
||||
// Arrange
|
||||
val elements = listOf<WeatherEntity>(
|
||||
val elements = listOf<EntityItem>(
|
||||
mockk { every { id } returns any() },
|
||||
mockk { every { id } returns any() },
|
||||
mockk { every { id } returns any() },
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherRe
|
||||
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.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
import com.appttude.h_mal.atlas_weather.utils.BaseTest
|
||||
import io.mockk.MockKAnnotations
|
||||
@@ -44,7 +44,7 @@ class ServicesHelperTest : BaseTest() {
|
||||
@Test
|
||||
fun testWidgetDataAsync_successfulResponse() = runBlocking {
|
||||
// Arrange
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = CURRENT_LOCATION
|
||||
})
|
||||
@@ -68,7 +68,7 @@ class ServicesHelperTest : BaseTest() {
|
||||
)
|
||||
}.returns(CURRENT_LOCATION)
|
||||
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(weatherEntity) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(entityItem) } returns Unit
|
||||
|
||||
// Assert
|
||||
val result = helper.fetchData()
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
package com.appttude.h_mal.atlas_weather.viewmodel
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.appttude.h_mal.atlas_weather.data.WeatherSource
|
||||
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.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.ViewState
|
||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
import com.appttude.h_mal.atlas_weather.utils.BaseTest
|
||||
import com.appttude.h_mal.atlas_weather.utils.getOrAwaitValue
|
||||
import com.appttude.h_mal.atlas_weather.utils.sleep
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.InjectMockKs
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.impl.annotations.RelaxedMockK
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.ArgumentMatchers.anyList
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import java.io.IOException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
|
||||
@@ -32,8 +37,8 @@ class WorldViewModelTest : BaseTest() {
|
||||
@InjectMockKs
|
||||
lateinit var viewModel: WorldViewModel
|
||||
|
||||
@MockK(relaxed = true)
|
||||
lateinit var repository: Repository
|
||||
@RelaxedMockK
|
||||
lateinit var weatherSource: WeatherSource
|
||||
|
||||
@MockK
|
||||
lateinit var locationProvider: LocationProviderImpl
|
||||
@@ -43,30 +48,19 @@ class WorldViewModelTest : BaseTest() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchDataForSingleLocation_validLocation_validReturn() {
|
||||
// Arrange
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = CURRENT_LOCATION
|
||||
})
|
||||
val latlon = any<Pair<Double, Double>>()
|
||||
|
||||
// Act
|
||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
|
||||
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns Pair(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon
|
||||
)
|
||||
coEvery {
|
||||
repository.getWeatherFromApi(
|
||||
weatherResponse.lat.toString(),
|
||||
weatherResponse.lon.toString()
|
||||
)
|
||||
}.returns(weatherResponse)
|
||||
coEvery {
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
weatherResponse.lat,
|
||||
@@ -74,66 +68,209 @@ class WorldViewModelTest : BaseTest() {
|
||||
LocationType.City
|
||||
)
|
||||
}.returns(CURRENT_LOCATION)
|
||||
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(weatherEntity) } returns Unit
|
||||
|
||||
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
|
||||
coEvery {
|
||||
weatherSource.getWeather(
|
||||
latlon,
|
||||
CURRENT_LOCATION,
|
||||
locationType = LocationType.City
|
||||
)
|
||||
} returns FullWeather(weatherResponse)
|
||||
|
||||
// Assert
|
||||
viewModel.uiState.observeForever {
|
||||
println(it.javaClass.name)
|
||||
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
|
||||
|
||||
sleep(100)
|
||||
assertIs<ViewState.HasData<*>>(viewModel.uiState.getOrAwaitValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchDataForSingleLocation_failedLocation_validReturn() {
|
||||
// Arrange
|
||||
val errorMessage = ArgumentMatchers.anyString()
|
||||
|
||||
// Act
|
||||
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns Pair(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon
|
||||
)
|
||||
coEvery {
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon,
|
||||
LocationType.City
|
||||
)
|
||||
} throws IOException(errorMessage)
|
||||
|
||||
// Assert
|
||||
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
|
||||
sleep(100)
|
||||
val observerResults = viewModel.uiState.getOrAwaitValue()
|
||||
assertIs<ViewState.HasError<*>>(observerResults)
|
||||
assertEquals(observerResults.error as String, errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchDataForSingleLocation_failedApi_validReturn() {
|
||||
// Arrange
|
||||
val latlon = Pair(weatherResponse.lat, weatherResponse.lon)
|
||||
val errorMessage = ArgumentMatchers.anyString()
|
||||
|
||||
// Act
|
||||
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns Pair(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon
|
||||
)
|
||||
coEvery {
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon,
|
||||
LocationType.City
|
||||
)
|
||||
}.returns(CURRENT_LOCATION)
|
||||
coEvery {
|
||||
weatherSource.getWeather(
|
||||
latlon,
|
||||
CURRENT_LOCATION,
|
||||
locationType = LocationType.City
|
||||
)
|
||||
} throws IOException(errorMessage)
|
||||
|
||||
// Assert
|
||||
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
|
||||
sleep(100)
|
||||
val observerResults = viewModel.uiState.getOrAwaitValue()
|
||||
assertIs<ViewState.HasError<*>>(observerResults)
|
||||
assertEquals(observerResults.error as String, errorMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchDataForSingleLocationSearch_validLocation_validReturn() {
|
||||
// Arrange
|
||||
val latlon = Pair(weatherResponse.lat, weatherResponse.lon)
|
||||
|
||||
// Act
|
||||
every { weatherSource.repository.getSavedLocations() } returns anyList()
|
||||
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
|
||||
coEvery {
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon,
|
||||
LocationType.City
|
||||
)
|
||||
}.returns(CURRENT_LOCATION)
|
||||
coEvery {
|
||||
weatherSource.getWeather(
|
||||
latlon,
|
||||
CURRENT_LOCATION,
|
||||
locationType = LocationType.City
|
||||
)
|
||||
} returns FullWeather(weatherResponse).apply { locationString = CURRENT_LOCATION }
|
||||
|
||||
// Assert
|
||||
viewModel.fetchDataForSingleLocationSearch(CURRENT_LOCATION)
|
||||
|
||||
sleep(100)
|
||||
val result = viewModel.uiState.getOrAwaitValue()
|
||||
assertIs<ViewState.HasData<*>>(result)
|
||||
assertEquals(result.data as String, "$CURRENT_LOCATION saved")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchDataForSingleLocationSearch_locationAlreadyExists_errorReceived() {
|
||||
// Act
|
||||
every { weatherSource.repository.getSavedLocations() } returns listOf(CURRENT_LOCATION)
|
||||
|
||||
// Assert
|
||||
viewModel.fetchDataForSingleLocationSearch(CURRENT_LOCATION)
|
||||
|
||||
sleep(100)
|
||||
val result = viewModel.uiState.getOrAwaitValue()
|
||||
assertIs<ViewState.HasError<*>>(result)
|
||||
assertEquals(result.error as String, "$CURRENT_LOCATION already exists")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchDataForSingleLocationSearch_retrievedLocationExists_validError() {
|
||||
// Arrange
|
||||
val latlon = Pair(weatherResponse.lat, weatherResponse.lon)
|
||||
val retrievedLocation = anyString()
|
||||
|
||||
// Act
|
||||
every { weatherSource.repository.getSavedLocations() } returns listOf(retrievedLocation)
|
||||
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns latlon
|
||||
coEvery {
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
weatherResponse.lat,
|
||||
weatherResponse.lon,
|
||||
LocationType.City
|
||||
)
|
||||
}.returns(CURRENT_LOCATION)
|
||||
coEvery {
|
||||
weatherSource.getWeather(
|
||||
latlon,
|
||||
CURRENT_LOCATION,
|
||||
locationType = LocationType.City
|
||||
)
|
||||
} returns FullWeather(weatherResponse).apply { locationString = retrievedLocation }
|
||||
|
||||
// Assert
|
||||
viewModel.fetchDataForSingleLocationSearch(CURRENT_LOCATION)
|
||||
|
||||
sleep(100)
|
||||
val result = viewModel.uiState.getOrAwaitValue()
|
||||
assertIs<ViewState.HasError<*>>(result)
|
||||
assertEquals(result.error as String, "$retrievedLocation already exists")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchAllLocations_validLocations_validReturn() {
|
||||
// Arrange
|
||||
val listOfPlaces = listOf("Sydney", "London", "Cairo")
|
||||
|
||||
// Act
|
||||
listOfPlaces.forEachIndexed { index, s ->
|
||||
every { weatherSource.repository.isSearchValid(s) } returns true
|
||||
coEvery { locationProvider.getLatLongFromLocationName(s) } returns Pair(
|
||||
index.toDouble(),
|
||||
index.toDouble()
|
||||
)
|
||||
coEvery {
|
||||
locationProvider.getLocationNameFromLatLong(
|
||||
index.toDouble(),
|
||||
index.toDouble(),
|
||||
LocationType.City
|
||||
)
|
||||
}.returns(s)
|
||||
coEvery {
|
||||
weatherSource.getWeather(
|
||||
Pair(index.toDouble(), index.toDouble()),
|
||||
s,
|
||||
LocationType.City
|
||||
)
|
||||
}
|
||||
}
|
||||
coEvery { weatherSource.repository.loadWeatherList() } returns listOfPlaces
|
||||
|
||||
sleep(3000)
|
||||
// Assert
|
||||
viewModel.fetchAllLocations()
|
||||
|
||||
sleep(100)
|
||||
assertIs<ViewState.HasData<*>>(viewModel.uiState.getOrAwaitValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchDataForSingleLocation_invalidLocation_invalidReturn() {
|
||||
fun deleteLocation_validLocations_validReturn() {
|
||||
// Arrange
|
||||
val location = CURRENT_LOCATION
|
||||
val location = anyString()
|
||||
|
||||
// Act
|
||||
every { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } throws IOException("Unable to get location")
|
||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
|
||||
coEvery {
|
||||
repository.getWeatherFromApi(
|
||||
weatherResponse.lat.toString(),
|
||||
weatherResponse.lon.toString()
|
||||
)
|
||||
}.returns(weatherResponse)
|
||||
|
||||
viewModel.fetchDataForSingleLocation(location)
|
||||
coEvery { weatherSource.repository.deleteSavedWeatherEntry(location) } returns true
|
||||
|
||||
// Assert
|
||||
sleep(300)
|
||||
assertIs<ViewState.HasError<*>>(viewModel.uiState.getOrAwaitValue())
|
||||
}
|
||||
viewModel.deleteLocation(location)
|
||||
|
||||
@Test
|
||||
fun searchAboveFallbackTime_validLocation_validReturn() {
|
||||
// Arrange
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = CURRENT_LOCATION
|
||||
})
|
||||
|
||||
// Act
|
||||
coEvery { repository.getSingleWeather(CURRENT_LOCATION) }.returns(weatherEntity)
|
||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(false)
|
||||
coEvery {
|
||||
repository.getWeatherFromApi(
|
||||
weatherResponse.lat.toString(),
|
||||
weatherResponse.lon.toString()
|
||||
)
|
||||
}.returns(weatherResponse)
|
||||
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(weatherEntity) } returns Unit
|
||||
|
||||
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
|
||||
|
||||
// Assert
|
||||
sleep(300)
|
||||
sleep(100)
|
||||
assertIs<ViewState.HasData<*>>(viewModel.uiState.getOrAwaitValue())
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user