diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml
deleted file mode 100644
index a6aa5f0..0000000
--- a/.idea/assetWizardSettings.xml
+++ /dev/null
@@ -1,407 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTest.kt
index af29076..02c3347 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTest.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTest.kt
@@ -8,10 +8,9 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
-import android.view.WindowManager
import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
-import androidx.test.espresso.Root
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.assertion.ViewAssertions
@@ -20,15 +19,12 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.application.TestAppClass
-import com.appttude.h_mal.atlas_weather.helpers.BaseCustomMatcher
-import com.appttude.h_mal.atlas_weather.helpers.BaseViewAction
+import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.helpers.SnapshotRule
import com.appttude.h_mal.atlas_weather.utils.Stubs
import kotlinx.coroutines.runBlocking
-import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
-import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -46,6 +42,8 @@ open class BaseTest(
private lateinit var testActivity: Activity
private lateinit var decorView: View
+ private val prefs by lazy { PreferenceProvider(ApplicationProvider.getApplicationContext()) }
+
@get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
@@ -87,6 +85,8 @@ open class BaseTest(
testApp.stubLocation(location, lat, long)
}
+ fun clearPrefs() = prefs.clearPrefs()
+
fun getActivity() = testActivity
@After
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTestRobot.kt
index b62750f..8f367eb 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTestRobot.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseTestRobot.kt
@@ -5,6 +5,7 @@ import android.view.View
import android.widget.DatePicker
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
@@ -28,6 +29,8 @@ import org.hamcrest.Matcher
@SuppressWarnings("unused")
open class BaseTestRobot {
+ fun goBack() = Espresso.pressBack()
+
fun fillEditText(resId: Int, text: String?): ViewInteraction =
onView(withId(resId)).perform(
ViewActions.replaceText(text),
@@ -151,4 +154,8 @@ open class BaseTestRobot {
)
)
}
+
+ fun openMenuItem() {
+ matchView(R.id.settings_fragment).perform(click())
+ }
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/prefs/PreferenceProviderTest.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/prefs/PreferenceProviderTest.kt
new file mode 100644
index 0000000..9af037b
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/prefs/PreferenceProviderTest.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/testSuite/RoomDatabaseTests.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/testSuite/RoomDatabaseTests.kt
index 5fe68d9..6bfdeea 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/testSuite/RoomDatabaseTests.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/testSuite/RoomDatabaseTests.kt
@@ -3,7 +3,6 @@ package com.appttude.h_mal.atlas_weather.testSuite
import android.os.Build
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
-import androidx.room.util.UUIDUtil
import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
import com.appttude.h_mal.atlas_weather.data.room.Converter
@@ -11,18 +10,13 @@ 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.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
-import com.appttude.h_mal.atlas_weather.test.BuildConfig
import com.appttude.h_mal.atlas_weather.utils.getOrAwaitValue
-import io.mockk.every
import io.mockk.mockk
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.Assert.*
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mockito.mock
import java.util.UUID
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/SettingsRobot.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/SettingsRobot.kt
new file mode 100644
index 0000000..ee50ee6
--- /dev/null
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/robot/SettingsRobot.kt
@@ -0,0 +1,48 @@
+package com.appttude.h_mal.monoWeather.robot
+
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.contrib.RecyclerViewActions
+import androidx.test.espresso.matcher.RootMatchers.isDialog
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.appttude.h_mal.atlas_weather.BaseTestRobot
+import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.model.types.UnitType
+
+
+fun settingsScreen(func: SettingsScreen.() -> Unit) = SettingsScreen().apply { func() }
+class SettingsScreen : BaseTestRobot() {
+
+ fun selectWeatherUnits(unitType: UnitType) {
+ onView(withId(androidx.preference.R.id.recycler_view))
+ .perform(
+ RecyclerViewActions.actionOnItem(
+ ViewMatchers.hasDescendant(withText(R.string.weather_units)),
+ click()))
+ val label = when (unitType) {
+ UnitType.METRIC -> "Metric"
+ UnitType.IMPERIAL -> "Imperial"
+ }
+
+ onView(withText(label))
+ .inRoot(isDialog())
+ .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
+ .perform(click())
+ }
+
+
+ fun verifyCurrentTemperature(temperature: Int) =
+ matchText(R.id.temp_main_4, temperature.toString())
+
+ fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location)
+ fun refresh() = pullToRefresh(R.id.swipe_refresh)
+
+ fun verifyUnableToRetrieve() {
+ matchText(R.id.header_text, R.string.retrieve_warning)
+ matchText(R.id.body_text, R.string.empty_retrieve_warning)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageUITest.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageUITest.kt
index 284a7f1..1bc556e 100644
--- a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageUITest.kt
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/monoWeather/tests/HomePageUITest.kt
@@ -2,8 +2,10 @@ package com.appttude.h_mal.monoWeather.tests
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.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
+import com.appttude.h_mal.monoWeather.robot.settingsScreen
import com.appttude.h_mal.monoWeather.robot.weatherScreen
import org.junit.Test
@@ -11,6 +13,7 @@ class HomePageUITest : BaseTest(MainActivity::class.java) {
override fun beforeLaunch() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
+ clearPrefs()
}
@Test
@@ -21,4 +24,25 @@ class HomePageUITest : BaseTest(MainActivity::class.java) {
verifyCurrentLocation("Mock Location")
}
}
+
+ @Test
+ fun loadApp_changeToImperial_returnsValidPage() {
+ weatherScreen {
+ isDisplayed()
+ verifyCurrentTemperature(2)
+ verifyCurrentLocation("Mock Location")
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Imperial)
+ openMenuItem()
+ }
+ settingsScreen {
+ selectWeatherUnits(UnitType.IMPERIAL)
+ goBack()
+ }
+ weatherScreen {
+ isDisplayed()
+ refresh()
+ verifyCurrentTemperature(58)
+ verifyCurrentLocation("Mock Location")
+ }
+ }
}
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/application/BaseAppClass.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/application/BaseAppClass.kt
index ebd54a1..98b2a84 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/application/BaseAppClass.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/application/BaseAppClass.kt
@@ -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
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/base/BasePreferencesFragment.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/base/BasePreferencesFragment.kt
new file mode 100644
index 0000000..9fec7e9
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/base/BasePreferencesFragment.kt
@@ -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(@XmlRes private val preferencesResId: Int) :
+ PreferenceFragmentCompat(),
+ KodeinAware {
+
+ override val kodein by kodein()
+ private val factory by instance()
+
+ val viewModel: V by getFragmentViewModel()
+
+ var mActivity: BaseActivity? = null
+ private fun getFragmentViewModel(): Lazy =
+ createViewModelLazy(getGenericClassAt(0), { viewModelStore }, factoryProducer = { factory })
+
+ private var shortAnimationDuration by Delegates.notNull()
+
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/BaseAndroidViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/BaseAndroidViewModel.kt
new file mode 100644
index 0000000..8db3155
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/BaseAndroidViewModel.kt
@@ -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()
+ val uiState: LiveData = _uiState
+
+
+ fun onStart() {
+ _uiState.postValue(ViewState.HasStarted)
+ }
+
+ fun onSuccess(result: T) {
+ _uiState.postValue(ViewState.HasData(result))
+ }
+
+ protected fun onError(error: E) {
+ _uiState.postValue(ViewState.HasError(error))
+ }
+
+ protected var job: Job? = null
+
+ fun cancelOperation() {
+ CoroutineScope(Dispatchers.IO).launch {
+ job?.run {
+ cancelAndJoin()
+ onSuccess(Unit)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/BaseViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/BaseViewModel.kt
index c4895c5..c207add 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/BaseViewModel.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/BaseViewModel.kt
@@ -4,8 +4,13 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
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 BaseViewModel: ViewModel() {
+open class BaseViewModel : ViewModel() {
private val _uiState = MutableLiveData()
val uiState: LiveData = _uiState
@@ -22,4 +27,15 @@ open class BaseViewModel: ViewModel() {
protected fun onError(error: E) {
_uiState.postValue(ViewState.HasError(error))
}
+
+ protected var job: Job? = null
+
+ fun cancelOperation() {
+ CoroutineScope(Dispatchers.IO).launch {
+ job?.run {
+ cancelAndJoin()
+ onSuccess(Unit)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/WeatherViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/WeatherViewModel.kt
deleted file mode 100644
index be19ec3..0000000
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/base/baseViewModels/WeatherViewModel.kt
+++ /dev/null
@@ -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.EntityItem
-import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
-
-abstract class WeatherViewModel : BaseViewModel() {
-
- fun createFullWeather(
- weather: WeatherResponse,
- location: String
- ): FullWeather {
- return FullWeather(weather).apply {
- temperatureUnit = "°C"
- locationString = location
- }
- }
-
- fun createWeatherEntity(
- locationId: String,
- weather: FullWeather
- ): EntityItem {
- weather.apply {
- locationString = locationId
- }
-
- return EntityItem(locationId, weather)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/WeatherSource.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/WeatherSource.kt
new file mode 100644
index 0000000..b19e568
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/WeatherSource.kt
@@ -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,
+ 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,
+ 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,
+ 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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProvider.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProvider.kt
index 8b17a98..0a72264 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProvider.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProvider.kt
@@ -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
fun getLatLongFromLocationName(location: String): Pair
suspend fun getLocationNameFromLatLong(
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImpl.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImpl.kt
index a79c826..a809c08 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImpl.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImpl.kt
@@ -5,13 +5,8 @@ import android.annotation.SuppressLint
import android.content.Context
import android.location.Geocoder
import android.location.Location
-import android.location.LocationManager
-import android.os.HandlerThread
import androidx.annotation.RequiresPermission
import com.appttude.h_mal.atlas_weather.model.types.LocationType
-import com.google.android.gms.location.LocationCallback
-import com.google.android.gms.location.LocationRequest
-import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.google.android.gms.tasks.CancellationToken
@@ -19,21 +14,23 @@ import com.google.android.gms.tasks.OnTokenCanceledListener
import kotlinx.coroutines.tasks.await
import java.io.IOException
import java.util.*
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
class LocationProviderImpl(
private val applicationContext: Context
) : LocationProvider, LocationHelper(applicationContext) {
- private var locationManager =
- applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
private val client = LocationServices.getFusedLocationProviderClient(applicationContext)
private val geoCoder: Geocoder by lazy { Geocoder(applicationContext, Locale.getDefault()) }
@RequiresPermission(value = ACCESS_COARSE_LOCATION)
override suspend fun getCurrentLatLong(): Pair {
- val location = client.lastLocation.await() ?: getAFreshLocation()
+ val lastLocation = client.lastLocation.await()
+ lastLocation?.let {
+ val delta = it.time - System.currentTimeMillis()
+ if (delta < 300000) return it.getLatLonPair()
+ }
+
+ val location = getAFreshLocation()
return location?.getLatLonPair() ?: throw IOException("Unable to get location")
}
@@ -68,57 +65,16 @@ class LocationProviderImpl(
@SuppressLint("MissingPermission")
private suspend fun getAFreshLocation(): Location? {
- return client.getCurrentLocation(Priority.PRIORITY_LOW_POWER, object : CancellationToken() {
- override fun isCancellationRequested(): Boolean = false
- override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken = this
- }).await()
+ return client.getCurrentLocation(
+ Priority.PRIORITY_HIGH_ACCURACY,
+ cancellationToken
+ ).await()
}
- @SuppressLint("MissingPermission")
- private suspend fun requestFreshLocation(): Location? {
- val handlerThread = HandlerThread("MyHandlerThread")
- handlerThread.start()
- // Now get the Looper from the HandlerThread
- // NOTE: This call will block until the HandlerThread gets control and initializes its Looper
- val looper = handlerThread.looper
-
- return suspendCoroutine { cont ->
- val callback = object : LocationCallback() {
- override fun onLocationResult(p0: LocationResult) {
- client.removeLocationUpdates(this)
- cont.resume(p0.lastLocation)
- }
- }
-
- with(locationManager!!) {
- when {
- isProviderEnabled(LocationManager.GPS_PROVIDER) -> {
- client.requestLocationUpdates(
- createLocationRequest(Priority.PRIORITY_HIGH_ACCURACY),
- callback,
- looper
- )
- }
-
- isProviderEnabled(LocationManager.NETWORK_PROVIDER) -> {
- client.requestLocationUpdates(
- createLocationRequest(Priority.PRIORITY_LOW_POWER),
- callback,
- looper
- )
- }
-
- else -> {
- cont.resume(null)
- }
- }
- }
-
- }
+ private val cancellationToken = object : CancellationToken() {
+ override fun isCancellationRequested(): Boolean = false
+ override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken =
+ this
}
- private fun createLocationRequest(priority: Int) = LocationRequest.create()
- .setPriority(priority)
- .setNumUpdates(1)
- .setExpirationDuration(1000)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/prefs/PreferencesProvider.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/prefs/PreferencesProvider.kt
index 693191f..bbea858 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/prefs/PreferencesProvider.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/prefs/PreferencesProvider.kt
@@ -2,13 +2,16 @@ package com.appttude.h_mal.atlas_weather.data.prefs
import android.content.Context
import android.content.SharedPreferences
+import androidx.annotation.VisibleForTesting
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 +33,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 +53,18 @@ 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
+ }
+
+ @VisibleForTesting
+ fun clearPrefs() {
+ preference.edit().clear().apply()
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/Repository.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/Repository.kt
index ce0c3d9..43a20d4 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/Repository.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/Repository.kt
@@ -3,6 +3,7 @@ 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.EntityItem
+import com.appttude.h_mal.atlas_weather.model.types.UnitType
interface Repository {
@@ -18,4 +19,5 @@ interface Repository {
suspend fun deleteSavedWeatherEntry(locationName: String): Boolean
fun getSavedLocations(): List
suspend fun getSingleWeather(locationName: String): EntityItem
+ fun getUnitType() : UnitType
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt
index ebfa238..bf12df3 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt
@@ -7,6 +7,7 @@ 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.EntityItem
+import com.appttude.h_mal.atlas_weather.model.types.UnitType
import com.appttude.h_mal.atlas_weather.utils.FALLBACK_TIME
@@ -20,7 +21,7 @@ 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(entityItem: EntityItem) {
@@ -65,11 +66,15 @@ class RepositoryImpl(
}
override fun getSavedLocations(): List {
- return prefs.getAllKeys().toList()
+ return prefs.getAllKeysExcludingCurrent().toList()
}
override suspend fun getSingleWeather(locationName: String): EntityItem {
return db.getWeatherDao().getCurrentFullWeatherSingle(locationName)
}
+ override fun getUnitType(): UnitType {
+ return prefs.getUnitsType()
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/SettingsRepository.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/SettingsRepository.kt
index 48dd734..103c2c5 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/SettingsRepository.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/SettingsRepository.kt
@@ -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
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/SettingsRepositoryImpl.kt
index 5764118..8f387a3 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/SettingsRepositoryImpl.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/SettingsRepositoryImpl.kt
@@ -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()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/ServicesHelper.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/ServicesHelper.kt
index 5485fdc..e336231 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/ServicesHelper.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/ServicesHelper.kt
@@ -5,15 +5,19 @@ import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable
import androidx.annotation.RequiresPermission
+import com.appttude.h_mal.atlas_weather.R
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.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
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.WidgetState
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
import com.appttude.h_mal.atlas_weather.utils.toSmallDayName
import com.squareup.picasso.Picasso
@@ -58,6 +62,77 @@ class ServicesHelper(
}
}
+ @RequiresPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
+ suspend fun fetchWidgetData(): WidgetState {
+ // Data was loaded within last 5 minutes - no need to retrieve again
+ if (!repository.isSearchValid(CURRENT_LOCATION)) {
+ val data = getWidgetWeatherCollection()
+ data?.let {
+ return WidgetState.HasData(it)
+ }
+ }
+
+ // Try and retrieve location
+ val latLong = try {
+ locationProvider.getCurrentLatLong()
+ } catch (e: IOException) {
+ val data = getWidgetWeatherCollection()
+ data?.let {
+ return WidgetState.HasData(it)
+ }
+
+ val error = WidgetError(
+ icon = R.drawable.ic_baseline_cloud_off_24,
+ errorMessage = "Failed to retrieve location, check location"
+ )
+ return WidgetState.HasError(error)
+ }
+
+ // Get weather from api
+ val weather = try {
+ repository
+ .getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
+ } catch (e: IOException) {
+ val data = getWidgetWeatherCollection()
+ data?.let {
+ return WidgetState.HasData(it)
+ }
+
+ val error = WidgetError(
+ icon = R.drawable.ic_baseline_cloud_off_24,
+ errorMessage = "Failed to retrieve weather data, check connection"
+ )
+ return WidgetState.HasError(error)
+ }
+
+ val currentLocation = try {
+ locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
+ } catch (e: IOException) {
+ val data = getWidgetWeatherCollection()
+ data?.let {
+ return WidgetState.HasData(it)
+ }
+
+ val error = WidgetError(
+ icon = R.drawable.ic_baseline_cloud_off_24,
+ errorMessage = "Failed to retrieve location name"
+ )
+ return WidgetState.HasError(error)
+ }
+
+ val fullWeather = FullWeather(weather).apply {
+ temperatureUnit = if (repository.getUnitType() == UnitType.METRIC) "°C" else "°F"
+ locationString = currentLocation
+ }
+ val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
+ // Save data to database
+ repository.saveLastSavedAt(CURRENT_LOCATION)
+ repository.saveCurrentWeatherToRoom(entityItem)
+
+ val data = createWidgetWeatherCollection(entityItem, currentLocation)
+ return WidgetState.HasData(data)
+ }
+
suspend fun getWidgetWeather(): WidgetData? {
return try {
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
@@ -127,6 +202,33 @@ class ServicesHelper(
}
}
+ 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()
+ val epoc = System.currentTimeMillis()
+
+ WidgetData(locationName, bitmap, temp, epoc)
+ }
+
+ val list = mutableListOf()
+
+ result.weather.daily?.drop(1)?.dropLast(2)?.forEach { dailyWeather ->
+ val day = dailyWeather.dt?.toSmallDayName()
+ val icon = dailyWeather.icon
+ val temp = dailyWeather.max?.toInt().toString()
+
+ val item = InnerWidgetCellData(day, icon, temp)
+ list.add(item)
+ }
+ list.toList()
+
+ return WidgetWeatherCollection(widgetData, list)
+ }
+
private suspend fun getBitmapFromUrl(imageAddress: String?): Bitmap? {
return suspendCoroutine { cont ->
Picasso.get().load(imageAddress).into(object : Target {
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/model/types/UnitType.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/model/types/UnitType.kt
new file mode 100644
index 0000000..295d7af
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/model/types/UnitType.kt
@@ -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
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/model/widget/WidgetError.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/model/widget/WidgetError.kt
new file mode 100644
index 0000000..323cf23
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/model/widget/WidgetError.kt
@@ -0,0 +1,9 @@
+package com.appttude.h_mal.atlas_weather.model.widget
+
+import androidx.annotation.DrawableRes
+
+data class WidgetError(
+ @DrawableRes
+ val icon: Int,
+ val errorMessage: String
+)
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/model/widget/WidgetState.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/model/widget/WidgetState.kt
new file mode 100644
index 0000000..400139a
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/model/widget/WidgetState.kt
@@ -0,0 +1,6 @@
+package com.appttude.h_mal.atlas_weather.model.widget
+
+sealed class WidgetState {
+ class HasData(val data: T) : WidgetState()
+ class HasError(val error: T) : WidgetState()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/MainActivity.kt
index 748233d..af883f7 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/MainActivity.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/ui/MainActivity.kt
@@ -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
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt
index 84fd85c..7b1a1af 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt
@@ -1,3 +1,3 @@
package com.appttude.h_mal.atlas_weather.utils
-val FALLBACK_TIME: Long = 300000L
\ No newline at end of file
+const val FALLBACK_TIME: Long = 300000L
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/ApplicationViewModelFactory.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/ApplicationViewModelFactory.kt
index 23b48c7..c476a9e 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/ApplicationViewModelFactory.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/ApplicationViewModelFactory.kt
@@ -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")
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/MainViewModel.kt
index ac81c12..489f75a 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/MainViewModel.kt
@@ -2,24 +2,23 @@ package com.appttude.h_mal.atlas_weather.viewmodel
import android.Manifest
import androidx.annotation.RequiresPermission
+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.EntityItem
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 {
- it?.let {
+ weatherSource.repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever { w ->
+ w?.let {
val weather = WeatherDisplay(it)
onSuccess(weather)
}
@@ -29,27 +28,14 @@ class MainViewModel(
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
fun fetchData() {
onStart()
- CoroutineScope(Dispatchers.IO).launch {
+ job = CoroutineScope(Dispatchers.IO).launch {
try {
- // Has the search been conducted in the last 5 minutes
- val entityItem = 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)
- EntityItem(CURRENT_LOCATION, fullWeather)
- } else {
- repository.getSingleWeather(CURRENT_LOCATION)
- }
- // Save data if not null
- repository.saveLastSavedAt(CURRENT_LOCATION)
- repository.saveCurrentWeatherToRoom(entityItem)
+ // Get location
+ val latLong = locationProvider.getCurrentLatLong()
+ weatherSource.getWeather(latLon = latLong)
} catch (e: Exception) {
- onError(e.message!!)
+ e.printStackTrace()
+ onError(e.message ?: "Retrieving weather failed")
}
}
}
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/SettingsViewModel.kt
new file mode 100644
index 0000000..c0ed835
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/SettingsViewModel.kt
@@ -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.core.app.ActivityCompat
+import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.application.BaseAppClass
+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().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) {
+ e.printStackTrace()
+ onError(e.message ?: "Retrieving weather failed")
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModel.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModel.kt
index 699f5de..4927f71 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModel.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModel.kt
@@ -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.EntityItem
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()
- repository.loadWeatherList().forEach { locationName ->
+ val list = mutableListOf>()
+ 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): EntityItem {
- 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)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/widget/BaseWidgetServiceIntentClass.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/widget/BaseWidgetServiceIntentClass.kt
index 9e9c483..f0c0d95 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/widget/BaseWidgetServiceIntentClass.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/widget/BaseWidgetServiceIntentClass.kt
@@ -22,7 +22,7 @@ abstract class BaseWidgetServiceIntentClass : JobIntentSe
lateinit var appWidgetManager: AppWidgetManager
lateinit var appWidgetIds: IntArray
- fun initBaseWidget(componentName: ComponentName) {
+ fun initializeWidgetData(componentName: ComponentName) {
appWidgetManager = AppWidgetManager.getInstance(baseContext)
appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
}
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/widget/WidgetJobServiceIntent.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/widget/WidgetJobServiceIntent.kt
index e545be8..0553f9c 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/widget/WidgetJobServiceIntent.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/widget/WidgetJobServiceIntent.kt
@@ -1,25 +1,21 @@
package com.appttude.h_mal.atlas_weather.widget
-import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.icu.text.SimpleDateFormat
-import android.os.PowerManager
import android.widget.RemoteViews
-import androidx.core.app.ActivityCompat.checkSelfPermission
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
+import com.appttude.h_mal.atlas_weather.model.widget.WidgetError
+import com.appttude.h_mal.atlas_weather.model.widget.WidgetState.HasData
+import com.appttude.h_mal.atlas_weather.model.widget.WidgetState.HasError
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
import com.appttude.h_mal.atlas_weather.ui.MainActivity
-import com.appttude.h_mal.atlas_weather.utils.isInternetAvailable
-import com.appttude.h_mal.atlas_weather.utils.tryOrNullSuspended
import com.appttude.h_mal.atlas_weather.widget.WidgetState.*
-import com.appttude.h_mal.atlas_weather.widget.WidgetState.Companion.getWidgetState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -46,61 +42,33 @@ class WidgetJobServiceIntent : BaseWidgetServiceIntentClass() {
executeWidgetUpdate()
}
+ @SuppressLint("MissingPermission")
private fun executeWidgetUpdate() {
val componentName = ComponentName(this, NewAppWidget::class.java)
- initBaseWidget(componentName)
+ initializeWidgetData(componentName)
- initiateWidgetUpdate(getCurrentWidgetState())
- }
-
- private fun initiateWidgetUpdate(state: WidgetState) {
- when (state) {
- NO_LOCATION, SCREEN_ON_CONNECTION_UNAVAILABLE -> updateErrorWidget(state)
- SCREEN_ON_CONNECTION_AVAILABLE -> updateWidget(false)
- SCREEN_OFF_CONNECTION_AVAILABLE -> updateWidget(true)
- SCREEN_OFF_CONNECTION_UNAVAILABLE -> return
- }
- }
-
- private fun updateWidget(fromStorage: Boolean) {
CoroutineScope(Dispatchers.IO).launch {
- val result = getWidgetWeather(fromStorage)
- appWidgetIds.forEach { id -> setupView(id, result) }
+ setLoadingView()
+
+ val widgetState = helper.fetchWidgetData()
+ appWidgetIds.forEach { id ->
+ when (widgetState) {
+ is HasData<*> -> {
+ val data = widgetState.data as WidgetWeatherCollection
+ setupView(id, data)
+ }
+
+ is HasError<*> -> {
+ if (widgetState.error is WidgetError) {
+ val error = widgetState.error
+ setupErrorView(id, error)
+ }
+ }
+ }
+ }
}
}
- private fun updateErrorWidget(state: WidgetState) {
- appWidgetIds.forEach { id -> setEmptyView(id, state) }
- }
-
- private fun getCurrentWidgetState(): WidgetState {
- val pm = getSystemService(POWER_SERVICE) as PowerManager
- val isScreenOn = pm.isInteractive
- val locationGranted =
- checkSelfPermission(this, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED
- val internetAvailable = isInternetAvailable(this.applicationContext)
-
- return getWidgetState(locationGranted, isScreenOn, internetAvailable)
- }
-
- @SuppressLint("MissingPermission")
- suspend fun getWidgetWeather(storageOnly: Boolean): WidgetWeatherCollection? {
- return tryOrNullSuspended {
- if (!storageOnly) helper.fetchData()
- helper.getWidgetWeatherCollection()
- }
- }
-
- private fun setEmptyView(appWidgetId: Int, state: WidgetState) {
- val error = when (state) {
- NO_LOCATION -> "No Location Permission"
- SCREEN_ON_CONNECTION_UNAVAILABLE -> "No network available"
- else -> "No data"
- }
-
- val views = createRemoteView(R.layout.weather_app_widget)
- bindErrorView(appWidgetId, views, error)
- }
private fun setupView(
appWidgetId: Int,
@@ -117,6 +85,49 @@ class WidgetJobServiceIntent : BaseWidgetServiceIntentClass() {
}
}
+ private fun setupErrorView(
+ appWidgetId: Int,
+ error: WidgetError
+ ) {
+ val views = createRemoteView(R.layout.weather_app_widget)
+// setLastUpdated(views, collection?.widgetData?.timeStamp)
+ views.setInt(R.id.whole_widget_view, "setBackgroundColor", helper.getWidgetBackground())
+
+ bindEmptyView(appWidgetId, views, error.errorMessage)
+ }
+
+ @SuppressLint("DiscouragedApi")
+ private fun setLoadingView() {
+ appWidgetIds.forEach { id ->
+ val views = createRemoteView(R.layout.weather_app_widget)
+ views.setInt(R.id.whole_widget_view, "setBackgroundColor", helper.getWidgetBackground())
+
+ val clickUpdate = createUpdatePendingIntent(NewAppWidget::class.java, id)
+
+ views.apply {
+ setTextViewText(R.id.widget_current_location, "Loading...")
+ setImageViewResource(R.id.location_icon, R.drawable.location_flag)
+ setOnClickPendingIntent(R.id.widget_current_location, clickUpdate)
+
+ (0..4).forEach { i ->
+ val dayId: Int =
+ resources.getIdentifier("widget_item_day_$i", "id", packageName)
+ val imageId: Int =
+ resources.getIdentifier("widget_item_image_$i", "id", packageName)
+ val tempId: Int =
+ resources.getIdentifier("widget_item_temp_high_$i", "id", packageName)
+
+ views.setTextViewText(dayId, "loading")
+ views.setTextViewText(tempId, "")
+ setImageViewResource(imageId, R.drawable.baseline_refresh_24)
+ }
+
+ // Instruct the widget manager to update the widget
+ appWidgetManager.updateAppWidget(id, views)
+ }
+ }
+ }
+
override fun bindErrorView(
widgetId: Int,
views: RemoteViews,
diff --git a/app/src/main/res/drawable/baseline_refresh_24.xml b/app/src/main/res/drawable/baseline_refresh_24.xml
new file mode 100644
index 0000000..788bfdf
--- /dev/null
+++ b/app/src/main/res/drawable/baseline_refresh_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a00ebe6..fdfd652 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -31,4 +31,12 @@
Unable to retrieve weather
Make sure you are connected to the internet and have location permissions granted
No weather to display
+ Units
+ widget_black_background
+ Weather units
+
+
+ - Metric
+ - Imperial
+
diff --git a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/WorldItemFragment.kt b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/WorldItemFragment.kt
index 3454063..feee132 100644
--- a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/WorldItemFragment.kt
+++ b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/WorldItemFragment.kt
@@ -16,22 +16,22 @@ import kotlinx.android.synthetic.main.fragment_home.swipe_refresh
class WorldItemFragment : BaseFragment(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(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?) {
diff --git a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/settings/SettingsFragment.kt b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/settings/SettingsFragment.kt
index eba82cd..2c99fc5 100644
--- a/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/settings/SettingsFragment.kt
+++ b/app/src/monoWeather/java/com/appttude/h_mal/monoWeather/ui/settings/SettingsFragment.kt
@@ -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(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)
+ }
}
\ No newline at end of file
diff --git a/app/src/monoWeather/res/layout/mono_item_one.xml b/app/src/monoWeather/res/layout/mono_item_one.xml
index e6cbe7f..fb12f91 100644
--- a/app/src/monoWeather/res/layout/mono_item_one.xml
+++ b/app/src/monoWeather/res/layout/mono_item_one.xml
@@ -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" />
+ app:layout_constraintTop_toTopOf="@id/temp_main_4"
+ tools:text="@string/degrees_c" />
+ app:srcCompat="@drawable/cloud_symbol"
+ app:tint="@color/colorAccent" />
+
\ No newline at end of file
diff --git a/app/src/test/java/com/appttude/h_mal/atlas_weather/data/WeatherSourceTest.kt b/app/src/test/java/com/appttude/h_mal/atlas_weather/data/WeatherSourceTest.kt
new file mode 100644
index 0000000..db5b3e3
--- /dev/null
+++ b/app/src/test/java/com/appttude/h_mal/atlas_weather/data/WeatherSourceTest.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImplTest.kt b/app/src/test/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImplTest.kt
index dd10e6e..bb78ac1 100644
--- a/app/src/test/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImplTest.kt
+++ b/app/src/test/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImplTest.kt
@@ -6,21 +6,17 @@ 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.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 com.nhaarman.mockitokotlin2.doAnswer
import io.mockk.MockKAnnotations
import io.mockk.coEvery
-import io.mockk.coJustRun
import io.mockk.every
import io.mockk.impl.annotations.MockK
-import io.mockk.justRun
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
-import org.mockito.ArgumentMatchers.anyString
-import retrofit2.Response
import java.io.IOException
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
@@ -94,6 +90,7 @@ class RepositoryImplTest : BaseTest() {
//Act
//create a successful retrofit response
+ every { prefs.getUnitsType() } returns (UnitType.METRIC)
coEvery { api.getFromApi("", "") }.returns(mockResponse)
// Assert
@@ -110,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
diff --git a/app/src/test/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModelTest.kt
index 5b311e2..a410070 100644
--- a/app/src/test/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModelTest.kt
+++ b/app/src/test/java/com/appttude/h_mal/atlas_weather/viewmodel/WorldViewModelTest.kt
@@ -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.EntityItem
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 entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
- temperatureUnit = "°C"
- locationString = CURRENT_LOCATION
- })
+ val latlon = any>()
// 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(entityItem) } 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>(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>(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>(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>(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>(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>(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>(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>(viewModel.uiState.getOrAwaitValue())
- }
+ viewModel.deleteLocation(location)
- @Test
- fun searchAboveFallbackTime_validLocation_validReturn() {
- // Arrange
- val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
- temperatureUnit = "°C"
- locationString = CURRENT_LOCATION
- })
-
- // Act
- coEvery { repository.getSingleWeather(CURRENT_LOCATION) }.returns(entityItem)
- 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(entityItem) } returns Unit
-
- viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
-
- // Assert
- sleep(300)
+ sleep(100)
assertIs>(viewModel.uiState.getOrAwaitValue())
}
+
}
\ No newline at end of file