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 - UI test for unit change
This commit is contained in:
407
.idea/assetWizardSettings.xml
generated
407
.idea/assetWizardSettings.xml
generated
@@ -1,407 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="WizardSettings">
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="imageWizard">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="confirmationStep">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="resourceDirectory" value="monoWeather" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="imageAssetPanel">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="actionbar">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="clipArt">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
<entry key="imagePath" value="C:\Users\h_mal\AppData\Local\Temp\ic_home_black_24dp.xml" />
|
|
||||||
<entry key="paddingPercent" value="-10" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="clipartAsset">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material_design_icons/action/ic_home_black_24dp.xml" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="text">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="textAsset">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="outputName" value="ic_home" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="launcher">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="foregroundClipArt">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="ffffff" />
|
|
||||||
<entry key="imagePath" value="C:\Users\h_mal\AppData\Local\Temp\baseline_public_24.xml" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="foregroundClipartAsset">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material/icons/materialicons/public/baseline_public_24.xml" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="foregroundImage">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="foregroundText">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="foregroundTextAsset">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="backgroundAssetType" value="COLOR" />
|
|
||||||
<entry key="backgroundColor" value="000000" />
|
|
||||||
<entry key="foregroundAssetType" value="CLIP_ART" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="launcherLegacy">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="clipArt">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="ffffff" />
|
|
||||||
<entry key="imagePath" value="C:\Users\h_mal\AppData\Local\Temp\ic_grade_black_24dp.xml" />
|
|
||||||
<entry key="paddingPercent" value="-10" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="clipartAsset">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material_design_icons/action/ic_grade_black_24dp.xml" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="image">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
<entry key="imagePath" value="C:\Users\h_mal\Desktop\Atlas weather\notif.png" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="text">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="textAsset">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="assetType" value="IMAGE" />
|
|
||||||
<entry key="iconShape" value="NONE" />
|
|
||||||
<entry key="imageAsset" value="C:\Users\h_mal\Desktop\Atlas weather\notif.png" />
|
|
||||||
<entry key="outputName" value="ic_notif" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="notification">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="clipArt">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
<entry key="imagePath" value="C:\Users\h_mal\AppData\Local\Temp\ic_android_black_24dp.xml" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="text">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="textAsset">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="tvBanner">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="foregroundText">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="tvChannel">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="foregroundClipArt">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="imagePath" value="C:\Users\h_mal\AppData\Local\Temp\ic_android_black_24dp.xml" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="foregroundImage">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="foregroundText">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="foregroundTextAsset">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="vectorWizard">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="vectorAssetStep">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="clipartAsset">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material/icons/materialicons/cloud_queue/baseline_cloud_queue_24.xml" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="ffffff" />
|
|
||||||
<entry key="outputName" value="ic_baseline_cloud_queue_24" />
|
|
||||||
<entry key="sourceFile" value="D:\Android Studio Projects\Private work\Altas_-_Weather" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -8,10 +8,9 @@ import android.content.Intent
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.espresso.Espresso
|
import androidx.test.espresso.Espresso
|
||||||
import androidx.test.espresso.Root
|
|
||||||
import androidx.test.espresso.UiController
|
import androidx.test.espresso.UiController
|
||||||
import androidx.test.espresso.ViewAction
|
import androidx.test.espresso.ViewAction
|
||||||
import androidx.test.espresso.assertion.ViewAssertions
|
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.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.GrantPermissionRule
|
import androidx.test.rule.GrantPermissionRule
|
||||||
import com.appttude.h_mal.atlas_weather.application.TestAppClass
|
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.data.prefs.PreferenceProvider
|
||||||
import com.appttude.h_mal.atlas_weather.helpers.BaseViewAction
|
|
||||||
import com.appttude.h_mal.atlas_weather.helpers.SnapshotRule
|
import com.appttude.h_mal.atlas_weather.helpers.SnapshotRule
|
||||||
import com.appttude.h_mal.atlas_weather.utils.Stubs
|
import com.appttude.h_mal.atlas_weather.utils.Stubs
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.hamcrest.Description
|
|
||||||
import org.hamcrest.Matcher
|
import org.hamcrest.Matcher
|
||||||
import org.hamcrest.Matchers
|
import org.hamcrest.Matchers
|
||||||
import org.hamcrest.TypeSafeMatcher
|
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@@ -46,6 +42,8 @@ open class BaseTest<A : Activity>(
|
|||||||
private lateinit var testActivity: Activity
|
private lateinit var testActivity: Activity
|
||||||
private lateinit var decorView: View
|
private lateinit var decorView: View
|
||||||
|
|
||||||
|
private val prefs by lazy { PreferenceProvider(ApplicationProvider.getApplicationContext()) }
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
|
var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||||
|
|
||||||
@@ -87,6 +85,8 @@ open class BaseTest<A : Activity>(
|
|||||||
testApp.stubLocation(location, lat, long)
|
testApp.stubLocation(location, lat, long)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearPrefs() = prefs.clearPrefs()
|
||||||
|
|
||||||
fun getActivity() = testActivity
|
fun getActivity() = testActivity
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.view.View
|
|||||||
import android.widget.DatePicker
|
import android.widget.DatePicker
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
import androidx.test.espresso.Espresso.onData
|
import androidx.test.espresso.Espresso.onData
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.UiController
|
import androidx.test.espresso.UiController
|
||||||
@@ -28,6 +29,8 @@ import org.hamcrest.Matcher
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
open class BaseTestRobot {
|
open class BaseTestRobot {
|
||||||
|
|
||||||
|
fun goBack() = Espresso.pressBack()
|
||||||
|
|
||||||
fun fillEditText(resId: Int, text: String?): ViewInteraction =
|
fun fillEditText(resId: Int, text: String?): ViewInteraction =
|
||||||
onView(withId(resId)).perform(
|
onView(withId(resId)).perform(
|
||||||
ViewActions.replaceText(text),
|
ViewActions.replaceText(text),
|
||||||
@@ -151,4 +154,8 @@ open class BaseTestRobot {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openMenuItem() {
|
||||||
|
matchView(R.id.settings_fragment).perform(click())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package com.appttude.h_mal.atlas_weather.testSuite
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.util.UUIDUtil
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
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.AppDatabase
|
||||||
import com.appttude.h_mal.atlas_weather.data.room.Converter
|
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.CURRENT_LOCATION
|
||||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
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 com.appttude.h_mal.atlas_weather.utils.getOrAwaitValue
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import org.mockito.ArgumentMatchers.any
|
|
||||||
import org.mockito.ArgumentMatchers.anyString
|
|
||||||
import org.mockito.Mockito.mock
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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<ViewHolder>(
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.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.ui.MainActivity
|
||||||
import com.appttude.h_mal.atlas_weather.utils.Stubs
|
import com.appttude.h_mal.atlas_weather.utils.Stubs
|
||||||
|
import com.appttude.h_mal.monoWeather.robot.settingsScreen
|
||||||
import com.appttude.h_mal.monoWeather.robot.weatherScreen
|
import com.appttude.h_mal.monoWeather.robot.weatherScreen
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
|
|||||||
|
|
||||||
override fun beforeLaunch() {
|
override fun beforeLaunch() {
|
||||||
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
|
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Metric)
|
||||||
|
clearPrefs()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -21,4 +24,25 @@ class HomePageUITest : BaseTest<MainActivity>(MainActivity::class.java) {
|
|||||||
verifyCurrentLocation("Mock Location")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.appttude.h_mal.atlas_weather.application
|
package com.appttude.h_mal.atlas_weather.application
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.WeatherSource
|
||||||
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
|
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
|
||||||
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
|
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 { RepositoryImpl(instance(), instance(), instance()) }
|
||||||
bind() from singleton { SettingsRepositoryImpl(instance()) }
|
bind() from singleton { SettingsRepositoryImpl(instance()) }
|
||||||
bind() from singleton { ServicesHelper(instance(), instance(), 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
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,13 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.appttude.h_mal.atlas_weather.model.ViewState
|
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<ViewState>()
|
private val _uiState = MutableLiveData<ViewState>()
|
||||||
val uiState: LiveData<ViewState> = _uiState
|
val uiState: LiveData<ViewState> = _uiState
|
||||||
@@ -22,4 +27,15 @@ open class BaseViewModel: ViewModel() {
|
|||||||
protected fun <E : Any> onError(error: E) {
|
protected fun <E : Any> onError(error: E) {
|
||||||
_uiState.postValue(ViewState.HasError(error))
|
_uiState.postValue(ViewState.HasError(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected var job: Job? = null
|
||||||
|
|
||||||
|
fun cancelOperation() {
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
job?.run {
|
||||||
|
cancelAndJoin()
|
||||||
|
onSuccess(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
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
|
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||||
|
|
||||||
interface LocationProvider {
|
interface LocationProvider {
|
||||||
|
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||||
suspend fun getCurrentLatLong(): Pair<Double, Double>
|
suspend fun getCurrentLatLong(): Pair<Double, Double>
|
||||||
fun getLatLongFromLocationName(location: String): Pair<Double, Double>
|
fun getLatLongFromLocationName(location: String): Pair<Double, Double>
|
||||||
suspend fun getLocationNameFromLatLong(
|
suspend fun getLocationNameFromLatLong(
|
||||||
|
|||||||
@@ -5,13 +5,8 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.location.Geocoder
|
import android.location.Geocoder
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
|
||||||
import android.os.HandlerThread
|
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
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.LocationServices
|
||||||
import com.google.android.gms.location.Priority
|
import com.google.android.gms.location.Priority
|
||||||
import com.google.android.gms.tasks.CancellationToken
|
import com.google.android.gms.tasks.CancellationToken
|
||||||
@@ -19,21 +14,23 @@ import com.google.android.gms.tasks.OnTokenCanceledListener
|
|||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
|
|
||||||
class LocationProviderImpl(
|
class LocationProviderImpl(
|
||||||
private val applicationContext: Context
|
private val applicationContext: Context
|
||||||
) : LocationProvider, LocationHelper(applicationContext) {
|
) : LocationProvider, LocationHelper(applicationContext) {
|
||||||
private var locationManager =
|
|
||||||
applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
|
|
||||||
private val client = LocationServices.getFusedLocationProviderClient(applicationContext)
|
private val client = LocationServices.getFusedLocationProviderClient(applicationContext)
|
||||||
private val geoCoder: Geocoder by lazy { Geocoder(applicationContext, Locale.getDefault()) }
|
private val geoCoder: Geocoder by lazy { Geocoder(applicationContext, Locale.getDefault()) }
|
||||||
|
|
||||||
@RequiresPermission(value = ACCESS_COARSE_LOCATION)
|
@RequiresPermission(value = ACCESS_COARSE_LOCATION)
|
||||||
override suspend fun getCurrentLatLong(): Pair<Double, Double> {
|
override suspend fun getCurrentLatLong(): Pair<Double, Double> {
|
||||||
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")
|
return location?.getLatLonPair() ?: throw IOException("Unable to get location")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,57 +65,16 @@ class LocationProviderImpl(
|
|||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
private suspend fun getAFreshLocation(): Location? {
|
private suspend fun getAFreshLocation(): Location? {
|
||||||
return client.getCurrentLocation(Priority.PRIORITY_LOW_POWER, object : CancellationToken() {
|
return client.getCurrentLocation(
|
||||||
override fun isCancellationRequested(): Boolean = false
|
Priority.PRIORITY_HIGH_ACCURACY,
|
||||||
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken = this
|
cancellationToken
|
||||||
}).await()
|
).await()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
private val cancellationToken = object : CancellationToken() {
|
||||||
private suspend fun requestFreshLocation(): Location? {
|
override fun isCancellationRequested(): Boolean = false
|
||||||
val handlerThread = HandlerThread("MyHandlerThread")
|
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken =
|
||||||
handlerThread.start()
|
this
|
||||||
// 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 fun createLocationRequest(priority: Int) = LocationRequest.create()
|
|
||||||
.setPriority(priority)
|
|
||||||
.setNumUpdates(1)
|
|
||||||
.setExpirationDuration(1000)
|
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,16 @@ package com.appttude.h_mal.atlas_weather.data.prefs
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared preferences to save & load last timestamp
|
* Shared preferences to save & load last timestamp
|
||||||
*/
|
*/
|
||||||
const val LOCATION_CONST = "location_"
|
const val LOCATION_CONST = "location_"
|
||||||
|
const val UNIT_CONST = "UnitType"
|
||||||
|
|
||||||
class PreferenceProvider(
|
class PreferenceProvider(
|
||||||
context: Context
|
context: Context
|
||||||
@@ -30,7 +33,7 @@ class PreferenceProvider(
|
|||||||
return preference.getLong(locationName, 0L)
|
return preference.getLong(locationName, 0L)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllKeys() = preference.all.keys.apply {
|
fun getAllKeysExcludingCurrent() = preference.all.keys.apply {
|
||||||
remove(CURRENT_LOCATION)
|
remove(CURRENT_LOCATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,4 +53,18 @@ class PreferenceProvider(
|
|||||||
return preference.getBoolean("widget_black_background", false)
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.appttude.h_mal.atlas_weather.data.repository
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
||||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||||
|
|
||||||
interface Repository {
|
interface Repository {
|
||||||
|
|
||||||
@@ -18,4 +19,5 @@ interface Repository {
|
|||||||
suspend fun deleteSavedWeatherEntry(locationName: String): Boolean
|
suspend fun deleteSavedWeatherEntry(locationName: String): Boolean
|
||||||
fun getSavedLocations(): List<String>
|
fun getSavedLocations(): List<String>
|
||||||
suspend fun getSingleWeather(locationName: String): EntityItem
|
suspend fun getSingleWeather(locationName: String): EntityItem
|
||||||
|
fun getUnitType() : UnitType
|
||||||
}
|
}
|
||||||
@@ -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.prefs.PreferenceProvider
|
||||||
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
|
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.data.room.entity.EntityItem
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||||
import com.appttude.h_mal.atlas_weather.utils.FALLBACK_TIME
|
import com.appttude.h_mal.atlas_weather.utils.FALLBACK_TIME
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ class RepositoryImpl(
|
|||||||
lat: String,
|
lat: String,
|
||||||
long: String
|
long: String
|
||||||
): WeatherResponse {
|
): WeatherResponse {
|
||||||
return responseUnwrap { api.getFromApi(lat, long) }
|
return responseUnwrap { api.getFromApi(lat, long, units = prefs.getUnitsType().name.lowercase()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) {
|
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) {
|
||||||
@@ -65,11 +66,15 @@ class RepositoryImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getSavedLocations(): List<String> {
|
override fun getSavedLocations(): List<String> {
|
||||||
return prefs.getAllKeys().toList()
|
return prefs.getAllKeysExcludingCurrent().toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSingleWeather(locationName: String): EntityItem {
|
override suspend fun getSingleWeather(locationName: String): EntityItem {
|
||||||
return db.getWeatherDao().getCurrentFullWeatherSingle(locationName)
|
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
|
package com.appttude.h_mal.atlas_weather.data.repository
|
||||||
|
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||||
|
|
||||||
interface SettingsRepository {
|
interface SettingsRepository {
|
||||||
fun isNotificationsEnabled(): Boolean
|
fun isNotificationsEnabled(): Boolean
|
||||||
fun setFirstTime()
|
fun setFirstTime()
|
||||||
fun getFirstTime(): Boolean
|
fun getFirstTime(): Boolean
|
||||||
fun isBlackBackground(): Boolean
|
fun isBlackBackground(): Boolean
|
||||||
|
fun saveUnitType(unitType: UnitType)
|
||||||
|
fun getUnitType(): UnitType
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.appttude.h_mal.atlas_weather.data.repository
|
package com.appttude.h_mal.atlas_weather.data.repository
|
||||||
|
|
||||||
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
|
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||||
|
|
||||||
class SettingsRepositoryImpl(
|
class SettingsRepositoryImpl(
|
||||||
private val prefs: PreferenceProvider
|
private val prefs: PreferenceProvider
|
||||||
@@ -12,4 +13,13 @@ class SettingsRepositoryImpl(
|
|||||||
override fun getFirstTime(): Boolean = prefs.getFirstTimeRun()
|
override fun getFirstTime(): Boolean = prefs.getFirstTimeRun()
|
||||||
|
|
||||||
override fun isBlackBackground() = prefs.isWidgetBackground()
|
override fun isBlackBackground() = prefs.isWidgetBackground()
|
||||||
|
|
||||||
|
|
||||||
|
override fun saveUnitType(unitType: UnitType) {
|
||||||
|
prefs.setUnitsType(unitType)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getUnitType(): UnitType {
|
||||||
|
return prefs.getUnitsType()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,15 +5,19 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.annotation.RequiresPermission
|
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.location.LocationProvider
|
||||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||||
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
|
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
|
||||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.types.UnitType
|
||||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||||
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
|
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
|
||||||
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
|
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
|
||||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetData
|
import com.appttude.h_mal.atlas_weather.model.widget.WidgetData
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.widget.WidgetError
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.widget.WidgetState
|
||||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
|
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
|
||||||
import com.appttude.h_mal.atlas_weather.utils.toSmallDayName
|
import com.appttude.h_mal.atlas_weather.utils.toSmallDayName
|
||||||
import com.squareup.picasso.Picasso
|
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? {
|
suspend fun getWidgetWeather(): WidgetData? {
|
||||||
return try {
|
return try {
|
||||||
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
|
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<InnerWidgetCellData>()
|
||||||
|
|
||||||
|
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? {
|
private suspend fun getBitmapFromUrl(imageAddress: String?): Bitmap? {
|
||||||
return suspendCoroutine { cont ->
|
return suspendCoroutine { cont ->
|
||||||
Picasso.get().load(imageAddress).into(object : Target {
|
Picasso.get().load(imageAddress).into(object : Target {
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.model.widget
|
||||||
|
|
||||||
|
sealed class WidgetState {
|
||||||
|
class HasData<T : Any>(val data: T) : WidgetState()
|
||||||
|
class HasError<T : Any>(val error: T) : WidgetState()
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package com.appttude.h_mal.atlas_weather.ui
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
@@ -10,7 +9,6 @@ import androidx.navigation.ui.setupActionBarWithNavController
|
|||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import com.appttude.h_mal.atlas_weather.R
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
import com.appttude.h_mal.atlas_weather.base.BaseActivity
|
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 com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
import kotlinx.android.synthetic.main.activity_main_navigation.toolbar
|
import kotlinx.android.synthetic.main.activity_main_navigation.toolbar
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package com.appttude.h_mal.atlas_weather.utils
|
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
|
package com.appttude.h_mal.atlas_weather.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
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.location.LocationProvider
|
||||||
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
|
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
|
||||||
|
|
||||||
|
|
||||||
class ApplicationViewModelFactory(
|
class ApplicationViewModelFactory(
|
||||||
|
private val application: Application,
|
||||||
private val locationProvider: LocationProvider,
|
private val locationProvider: LocationProvider,
|
||||||
private val repository: RepositoryImpl
|
private val source: WeatherSource,
|
||||||
|
private val settingsRepository: SettingsRepository
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -17,12 +21,16 @@ class ApplicationViewModelFactory(
|
|||||||
return when {
|
return when {
|
||||||
isAssignableFrom(WorldViewModel::class.java) -> WorldViewModel(
|
isAssignableFrom(WorldViewModel::class.java) -> WorldViewModel(
|
||||||
locationProvider,
|
locationProvider,
|
||||||
repository
|
source
|
||||||
)
|
)
|
||||||
|
|
||||||
isAssignableFrom(MainViewModel::class.java) -> MainViewModel(
|
isAssignableFrom(MainViewModel::class.java) -> MainViewModel(
|
||||||
locationProvider,
|
locationProvider,
|
||||||
repository
|
source
|
||||||
|
)
|
||||||
|
|
||||||
|
isAssignableFrom(SettingsViewModel::class.java) -> SettingsViewModel(
|
||||||
|
application, locationProvider, source, settingsRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
|
|||||||
@@ -2,24 +2,23 @@ package com.appttude.h_mal.atlas_weather.viewmodel
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import androidx.annotation.RequiresPermission
|
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.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.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.model.forecast.WeatherDisplay
|
||||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.WeatherViewModel
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val locationProvider: LocationProvider,
|
private val locationProvider: LocationProvider,
|
||||||
private val repository: Repository
|
private val weatherSource: WeatherSource
|
||||||
) : WeatherViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever {
|
weatherSource.repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever { w ->
|
||||||
it?.let {
|
w?.let {
|
||||||
val weather = WeatherDisplay(it)
|
val weather = WeatherDisplay(it)
|
||||||
onSuccess(weather)
|
onSuccess(weather)
|
||||||
}
|
}
|
||||||
@@ -29,27 +28,14 @@ class MainViewModel(
|
|||||||
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
|
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||||
fun fetchData() {
|
fun fetchData() {
|
||||||
onStart()
|
onStart()
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
job = CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
// Has the search been conducted in the last 5 minutes
|
// Get location
|
||||||
val entityItem = if (repository.isSearchValid(CURRENT_LOCATION)) {
|
val latLong = locationProvider.getCurrentLatLong()
|
||||||
// Get location
|
weatherSource.getWeather(latLon = latLong)
|
||||||
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)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
onError(e.message!!)
|
e.printStackTrace()
|
||||||
|
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.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<BaseAppClass>().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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
package com.appttude.h_mal.atlas_weather.viewmodel
|
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.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.forecast.WeatherDisplay
|
||||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.WeatherViewModel
|
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@@ -16,14 +18,12 @@ const val ALL_LOADED = "all_loaded"
|
|||||||
|
|
||||||
class WorldViewModel(
|
class WorldViewModel(
|
||||||
private val locationProvider: LocationProvider,
|
private val locationProvider: LocationProvider,
|
||||||
private val repository: Repository
|
private val weatherSource: WeatherSource
|
||||||
) : WeatherViewModel() {
|
) : BaseViewModel() {
|
||||||
private var currentLocation: String? = null
|
private var currentLocation: String? = null
|
||||||
|
|
||||||
private val weatherListLiveData = repository.loadRoomWeatherLiveData()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
weatherListLiveData.observeForever {
|
weatherSource.repository.loadRoomWeatherLiveData().observeForever {
|
||||||
val list = it.map { data ->
|
val list = it.map { data ->
|
||||||
WeatherDisplay(data)
|
WeatherDisplay(data)
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ class WorldViewModel(
|
|||||||
|
|
||||||
fun getSingleLocation(locationName: String) {
|
fun getSingleLocation(locationName: String) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val entity = repository.getSingleWeather(locationName)
|
val entity = weatherSource.repository.getSingleWeather(locationName)
|
||||||
val item = WeatherDisplay(entity)
|
val item = WeatherDisplay(entity)
|
||||||
onSuccess(item)
|
onSuccess(item)
|
||||||
}
|
}
|
||||||
@@ -47,14 +47,8 @@ class WorldViewModel(
|
|||||||
onStart()
|
onStart()
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val weatherEntity = if (repository.isSearchValid(locationName)) {
|
searchWeatherForLocation(locationName)
|
||||||
createWeatherEntity(locationName)
|
|
||||||
} else {
|
|
||||||
repository.getSingleWeather(locationName)
|
|
||||||
}
|
|
||||||
onSuccess(Unit)
|
onSuccess(Unit)
|
||||||
repository.saveCurrentWeatherToRoom(weatherEntity)
|
|
||||||
repository.saveLastSavedAt(weatherEntity.id)
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
onError(e.message!!)
|
onError(e.message!!)
|
||||||
}
|
}
|
||||||
@@ -64,29 +58,20 @@ class WorldViewModel(
|
|||||||
fun fetchDataForSingleLocationSearch(locationName: String) {
|
fun fetchDataForSingleLocationSearch(locationName: String) {
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
onStart()
|
onStart()
|
||||||
// Check if location exists
|
// Check if location already exists
|
||||||
if (repository.getSavedLocations().contains(locationName)) {
|
if (weatherSource.repository.getSavedLocations().contains(locationName)) {
|
||||||
onError("$locationName already exists")
|
onError("$locationName already exists")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get weather from api
|
val weather = searchWeatherForLocation(locationName)
|
||||||
val entityItem = createWeatherEntity(locationName)
|
val retrievedLocation = weather.locationString
|
||||||
|
// Check if location exists in stored
|
||||||
// retrieved location name
|
if (weatherSource.repository.getSavedLocations().contains(retrievedLocation)) {
|
||||||
val retrievedLocation = locationProvider.getLocationNameFromLatLong(
|
|
||||||
entityItem.weather.lat,
|
|
||||||
entityItem.weather.lon,
|
|
||||||
LocationType.City
|
|
||||||
)
|
|
||||||
if (repository.getSavedLocations().contains(retrievedLocation)) {
|
|
||||||
onError("$retrievedLocation already exists")
|
onError("$retrievedLocation already exists")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
// Save data if not null
|
|
||||||
repository.saveCurrentWeatherToRoom(entityItem)
|
|
||||||
repository.saveLastSavedAt(retrievedLocation)
|
|
||||||
onSuccess("$retrievedLocation saved")
|
onSuccess("$retrievedLocation saved")
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
onError(e.message!!)
|
onError(e.message!!)
|
||||||
@@ -96,30 +81,24 @@ class WorldViewModel(
|
|||||||
|
|
||||||
fun fetchAllLocations() {
|
fun fetchAllLocations() {
|
||||||
onStart()
|
onStart()
|
||||||
if (!repository.isSearchValid(ALL_LOADED)) {
|
if (!weatherSource.repository.isSearchValid(ALL_LOADED)) {
|
||||||
onSuccess(Unit)
|
onSuccess(Unit)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val list = mutableListOf<EntityItem>()
|
val list = mutableListOf<Deferred<FullWeather>>()
|
||||||
repository.loadWeatherList().forEach { locationName ->
|
weatherSource.repository.loadWeatherList().forEach { locationName ->
|
||||||
// If search not valid move onto next in loop
|
// If search not valid move onto next in loop
|
||||||
if (!repository.isSearchValid(locationName)) return@forEach
|
if (!weatherSource.repository.isSearchValid(locationName)) return@forEach
|
||||||
|
|
||||||
try {
|
val task = async{ searchWeatherForLocation(locationName) }
|
||||||
val entity = createWeatherEntity(locationName)
|
list.add(task)
|
||||||
list.add(entity)
|
|
||||||
repository.saveLastSavedAt(locationName)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
repository.saveWeatherListToRoom(list)
|
list.awaitAll()
|
||||||
repository.saveLastSavedAt(ALL_LOADED)
|
onSuccess(Unit)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
onError(e.message!!)
|
onError(e.message!!)
|
||||||
} finally {
|
|
||||||
onSuccess(Unit)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,34 +107,29 @@ class WorldViewModel(
|
|||||||
onStart()
|
onStart()
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
try {
|
try {
|
||||||
val success = repository.deleteSavedWeatherEntry(locationName)
|
val success = weatherSource.repository.deleteSavedWeatherEntry(locationName)
|
||||||
if (!success) {
|
if (!success) {
|
||||||
onError("Failed to delete")
|
onError("Failed to delete")
|
||||||
|
} else {
|
||||||
|
onSuccess(Unit)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
onError(e.message!!)
|
onError(e.message!!)
|
||||||
} finally {
|
|
||||||
onSuccess(Unit)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getWeather(locationName: String): WeatherResponse {
|
private suspend fun searchWeatherForLocation(locationName: String): FullWeather {
|
||||||
// Get location
|
// Get location
|
||||||
val latLong =
|
val latLong =
|
||||||
locationProvider.getLatLongFromLocationName(locationName)
|
locationProvider.getLatLongFromLocationName(locationName)
|
||||||
val lat = latLong.first
|
// Search for location from provider (provider location name maybe different from #locationName)
|
||||||
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)
|
|
||||||
val location =
|
val location =
|
||||||
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon, LocationType.City)
|
locationProvider.getLocationNameFromLatLong(
|
||||||
val fullWeather = createFullWeather(weather, location)
|
latLong.first,
|
||||||
return createWeatherEntity(location, fullWeather)
|
latLong.second,
|
||||||
|
LocationType.City
|
||||||
|
)
|
||||||
|
return weatherSource.getWeather(latLong, location, LocationType.City)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ abstract class BaseWidgetServiceIntentClass<T : AppWidgetProvider> : JobIntentSe
|
|||||||
lateinit var appWidgetManager: AppWidgetManager
|
lateinit var appWidgetManager: AppWidgetManager
|
||||||
lateinit var appWidgetIds: IntArray
|
lateinit var appWidgetIds: IntArray
|
||||||
|
|
||||||
fun initBaseWidget(componentName: ComponentName) {
|
fun initializeWidgetData(componentName: ComponentName) {
|
||||||
appWidgetManager = AppWidgetManager.getInstance(baseContext)
|
appWidgetManager = AppWidgetManager.getInstance(baseContext)
|
||||||
appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
|
appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
package com.appttude.h_mal.atlas_weather.widget
|
package com.appttude.h_mal.atlas_weather.widget
|
||||||
|
|
||||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
||||||
import android.icu.text.SimpleDateFormat
|
import android.icu.text.SimpleDateFormat
|
||||||
import android.os.PowerManager
|
|
||||||
import android.widget.RemoteViews
|
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.R
|
||||||
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
|
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.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.model.widget.WidgetWeatherCollection
|
||||||
import com.appttude.h_mal.atlas_weather.ui.MainActivity
|
import com.appttude.h_mal.atlas_weather.ui.MainActivity
|
||||||
import com.appttude.h_mal.atlas_weather.utils.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.*
|
||||||
import com.appttude.h_mal.atlas_weather.widget.WidgetState.Companion.getWidgetState
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -46,61 +42,33 @@ class WidgetJobServiceIntent : BaseWidgetServiceIntentClass<NewAppWidget>() {
|
|||||||
executeWidgetUpdate()
|
executeWidgetUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
private fun executeWidgetUpdate() {
|
private fun executeWidgetUpdate() {
|
||||||
val componentName = ComponentName(this, NewAppWidget::class.java)
|
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 {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val result = getWidgetWeather(fromStorage)
|
setLoadingView()
|
||||||
appWidgetIds.forEach { id -> setupView(id, result) }
|
|
||||||
|
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(
|
private fun setupView(
|
||||||
appWidgetId: Int,
|
appWidgetId: Int,
|
||||||
@@ -117,6 +85,49 @@ class WidgetJobServiceIntent : BaseWidgetServiceIntentClass<NewAppWidget>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
override fun bindErrorView(
|
||||||
widgetId: Int,
|
widgetId: Int,
|
||||||
views: RemoteViews,
|
views: RemoteViews,
|
||||||
|
|||||||
5
app/src/main/res/drawable/baseline_refresh_24.xml
Normal file
5
app/src/main/res/drawable/baseline_refresh_24.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||||
|
</vector>
|
||||||
@@ -31,4 +31,12 @@
|
|||||||
<string name="retrieve_warning">Unable to retrieve weather</string>
|
<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="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="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 name="weather_units">Weather units</string>
|
||||||
|
|
||||||
|
<string-array name="units">
|
||||||
|
<item>Metric</item>
|
||||||
|
<item>Imperial</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,22 +16,22 @@ import kotlinx.android.synthetic.main.fragment_home.swipe_refresh
|
|||||||
|
|
||||||
class WorldItemFragment : BaseFragment<WorldViewModel>(R.layout.fragment_home) {
|
class WorldItemFragment : BaseFragment<WorldViewModel>(R.layout.fragment_home) {
|
||||||
|
|
||||||
private var param1: String? = null
|
private var retrievedLocationName: String? = null
|
||||||
|
|
||||||
private lateinit var recyclerAdapter: WeatherRecyclerAdapter
|
private lateinit var recyclerAdapter: WeatherRecyclerAdapter
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
param1 = WorldItemFragmentArgs.fromBundle(requireArguments()).locationName
|
retrievedLocationName = WorldItemFragmentArgs.fromBundle(requireArguments()).locationName
|
||||||
param1?.let { viewModel.setLocation(it) }
|
retrievedLocationName?.let { viewModel.setLocation(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
recyclerAdapter = WeatherRecyclerAdapter {
|
recyclerAdapter = WeatherRecyclerAdapter {
|
||||||
val directions =
|
val directions = WorldItemFragmentDirections
|
||||||
WorldItemFragmentDirections.actionWorldItemFragmentToFurtherDetailsFragment(it)
|
.actionWorldItemFragmentToFurtherDetailsFragment(it)
|
||||||
navigateTo(directions)
|
navigateTo(directions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,14 +42,14 @@ class WorldItemFragment : BaseFragment<WorldViewModel>(R.layout.fragment_home) {
|
|||||||
|
|
||||||
swipe_refresh.apply {
|
swipe_refresh.apply {
|
||||||
setOnRefreshListener {
|
setOnRefreshListener {
|
||||||
param1?.let {
|
retrievedLocationName?.let {
|
||||||
viewModel.fetchDataForSingleLocation(it)
|
viewModel.fetchDataForSingleLocation(it)
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
param1?.let { viewModel.getSingleLocation(it) }
|
retrievedLocationName?.let { viewModel.getSingleLocation(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSuccess(data: Any?) {
|
override fun onSuccess(data: Any?) {
|
||||||
|
|||||||
@@ -1,46 +1,24 @@
|
|||||||
package com.appttude.h_mal.monoWeather.ui.settings
|
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.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?) {
|
override fun preferenceChanged(key: String) {
|
||||||
setPreferencesFromResource(R.xml.prefs_screen, rootKey)
|
when (key) {
|
||||||
|
"UnitType" -> viewModel.refreshWeatherData()
|
||||||
//listener on changed sort order preference:
|
"widget_black_background" -> {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
viewModel.updateWidget()
|
||||||
prefs.registerOnSharedPreferenceChangeListener { _, key ->
|
displayToast("Widget background has been updates")
|
||||||
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 onSuccess(data: Any?) {
|
||||||
|
super.onSuccess(data)
|
||||||
|
if (data is String) displayToast(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:contentDescription="@string/image_string"
|
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
|
<TextView
|
||||||
android:id="@+id/location_main_4"
|
android:id="@+id/location_main_4"
|
||||||
@@ -51,11 +51,11 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="9dp"
|
android:layout_marginTop="9dp"
|
||||||
android:text="@string/degrees_c"
|
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:layout_constraintLeft_toRightOf="@id/temp_main_4"
|
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
|
<ImageView
|
||||||
android:id="@+id/icon_main_4"
|
android:id="@+id/icon_main_4"
|
||||||
@@ -64,11 +64,11 @@
|
|||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:layout_marginBottom="12dp"
|
android:layout_marginBottom="12dp"
|
||||||
android:contentDescription="@string/image_string"
|
android:contentDescription="@string/image_string"
|
||||||
app:tint="@color/colorAccent"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/temp_main_4"
|
app:layout_constraintTop_toBottomOf="@id/temp_main_4"
|
||||||
app:srcCompat="@drawable/cloud_symbol" />
|
app:srcCompat="@drawable/cloud_symbol"
|
||||||
|
app:tint="@color/colorAccent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/condition_main_4"
|
android:id="@+id/condition_main_4"
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
android:title="@string/weather_units"
|
||||||
|
android:entries="@array/units"
|
||||||
|
android:entryValues="@array/units"
|
||||||
|
android:defaultValue="Metric"
|
||||||
|
android:key="UnitType"
|
||||||
|
/>
|
||||||
<PreferenceCategory android:title="Widget Settings">
|
<PreferenceCategory android:title="Widget Settings">
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:key="widget_black_background"
|
android:key="@string/widget_black_background"
|
||||||
android:title="Set widget background black" />
|
android:title="Set widget background black" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.prefs.PreferenceProvider
|
||||||
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
|
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.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.appttude.h_mal.atlas_weather.utils.BaseTest
|
||||||
import com.nhaarman.mockitokotlin2.any
|
import com.nhaarman.mockitokotlin2.any
|
||||||
import com.nhaarman.mockitokotlin2.doAnswer
|
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coJustRun
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.impl.annotations.MockK
|
import io.mockk.impl.annotations.MockK
|
||||||
import io.mockk.justRun
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.mockito.ArgumentMatchers.anyString
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
@@ -94,6 +90,7 @@ class RepositoryImplTest : BaseTest() {
|
|||||||
|
|
||||||
//Act
|
//Act
|
||||||
//create a successful retrofit response
|
//create a successful retrofit response
|
||||||
|
every { prefs.getUnitsType() } returns (UnitType.METRIC)
|
||||||
coEvery { api.getFromApi("", "") }.returns(mockResponse)
|
coEvery { api.getFromApi("", "") }.returns(mockResponse)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -110,6 +107,7 @@ class RepositoryImplTest : BaseTest() {
|
|||||||
|
|
||||||
//Act
|
//Act
|
||||||
//create a successful retrofit response
|
//create a successful retrofit response
|
||||||
|
every { prefs.getUnitsType() } returns (UnitType.METRIC)
|
||||||
coEvery { api.getFromApi(any(), any()) } returns (mockResponse)
|
coEvery { api.getFromApi(any(), any()) } returns (mockResponse)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
package com.appttude.h_mal.atlas_weather.viewmodel
|
package com.appttude.h_mal.atlas_weather.viewmodel
|
||||||
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
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.location.LocationProviderImpl
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
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.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.ViewState
|
||||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
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.BaseTest
|
||||||
import com.appttude.h_mal.atlas_weather.utils.getOrAwaitValue
|
import com.appttude.h_mal.atlas_weather.utils.getOrAwaitValue
|
||||||
import com.appttude.h_mal.atlas_weather.utils.sleep
|
import com.appttude.h_mal.atlas_weather.utils.sleep
|
||||||
|
import com.nhaarman.mockitokotlin2.any
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.impl.annotations.InjectMockKs
|
import io.mockk.impl.annotations.InjectMockKs
|
||||||
import io.mockk.impl.annotations.MockK
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.impl.annotations.RelaxedMockK
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.mockito.ArgumentMatchers
|
||||||
|
import org.mockito.ArgumentMatchers.anyList
|
||||||
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertIs
|
import kotlin.test.assertIs
|
||||||
|
|
||||||
|
|
||||||
@@ -32,8 +37,8 @@ class WorldViewModelTest : BaseTest() {
|
|||||||
@InjectMockKs
|
@InjectMockKs
|
||||||
lateinit var viewModel: WorldViewModel
|
lateinit var viewModel: WorldViewModel
|
||||||
|
|
||||||
@MockK(relaxed = true)
|
@RelaxedMockK
|
||||||
lateinit var repository: Repository
|
lateinit var weatherSource: WeatherSource
|
||||||
|
|
||||||
@MockK
|
@MockK
|
||||||
lateinit var locationProvider: LocationProviderImpl
|
lateinit var locationProvider: LocationProviderImpl
|
||||||
@@ -43,30 +48,19 @@ class WorldViewModelTest : BaseTest() {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
|
|
||||||
weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java)
|
weatherResponse = getTestData("weather_sample.json", WeatherResponse::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun fetchDataForSingleLocation_validLocation_validReturn() {
|
fun fetchDataForSingleLocation_validLocation_validReturn() {
|
||||||
// Arrange
|
// Arrange
|
||||||
val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
val latlon = any<Pair<Double, Double>>()
|
||||||
temperatureUnit = "°C"
|
|
||||||
locationString = CURRENT_LOCATION
|
|
||||||
})
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
|
|
||||||
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns Pair(
|
coEvery { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } returns Pair(
|
||||||
weatherResponse.lat,
|
weatherResponse.lat,
|
||||||
weatherResponse.lon
|
weatherResponse.lon
|
||||||
)
|
)
|
||||||
coEvery {
|
|
||||||
repository.getWeatherFromApi(
|
|
||||||
weatherResponse.lat.toString(),
|
|
||||||
weatherResponse.lon.toString()
|
|
||||||
)
|
|
||||||
}.returns(weatherResponse)
|
|
||||||
coEvery {
|
coEvery {
|
||||||
locationProvider.getLocationNameFromLatLong(
|
locationProvider.getLocationNameFromLatLong(
|
||||||
weatherResponse.lat,
|
weatherResponse.lat,
|
||||||
@@ -74,66 +68,209 @@ class WorldViewModelTest : BaseTest() {
|
|||||||
LocationType.City
|
LocationType.City
|
||||||
)
|
)
|
||||||
}.returns(CURRENT_LOCATION)
|
}.returns(CURRENT_LOCATION)
|
||||||
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
|
coEvery {
|
||||||
coEvery { repository.saveCurrentWeatherToRoom(entityItem) } returns Unit
|
weatherSource.getWeather(
|
||||||
|
latlon,
|
||||||
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
|
CURRENT_LOCATION,
|
||||||
|
locationType = LocationType.City
|
||||||
|
)
|
||||||
|
} returns FullWeather(weatherResponse)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
viewModel.uiState.observeForever {
|
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
|
||||||
println(it.javaClass.name)
|
|
||||||
|
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())
|
assertIs<ViewState.HasData<*>>(viewModel.uiState.getOrAwaitValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun fetchDataForSingleLocation_invalidLocation_invalidReturn() {
|
fun deleteLocation_validLocations_validReturn() {
|
||||||
// Arrange
|
// Arrange
|
||||||
val location = CURRENT_LOCATION
|
val location = anyString()
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
every { locationProvider.getLatLongFromLocationName(CURRENT_LOCATION) } throws IOException("Unable to get location")
|
coEvery { weatherSource.repository.deleteSavedWeatherEntry(location) } returns true
|
||||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(true)
|
|
||||||
coEvery {
|
|
||||||
repository.getWeatherFromApi(
|
|
||||||
weatherResponse.lat.toString(),
|
|
||||||
weatherResponse.lon.toString()
|
|
||||||
)
|
|
||||||
}.returns(weatherResponse)
|
|
||||||
|
|
||||||
viewModel.fetchDataForSingleLocation(location)
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
sleep(300)
|
viewModel.deleteLocation(location)
|
||||||
assertIs<ViewState.HasError<*>>(viewModel.uiState.getOrAwaitValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
sleep(100)
|
||||||
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)
|
|
||||||
assertIs<ViewState.HasData<*>>(viewModel.uiState.getOrAwaitValue())
|
assertIs<ViewState.HasData<*>>(viewModel.uiState.getOrAwaitValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user