mirror of
https://github.com/hmalik144/Weather-apps.git
synced 2025-12-10 02:05:20 +00:00
- Espresso mock network interceptor added (#9)
- Ability to stub end points in UI tests
This commit is contained in:
BIN
.idea/caches/build_file_checksums.ser
generated
BIN
.idea/caches/build_file_checksums.ser
generated
Binary file not shown.
@@ -17,7 +17,7 @@ android {
|
|||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 5
|
versionCode 5
|
||||||
versionName "3.0"
|
versionName "3.0"
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner "com.appttude.h_mal.atlas_weather.application.TestRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
buildConfigField "String", "ParamOne", "${paramOneEndPoint}"
|
buildConfigField "String", "ParamOne", "${paramOneEndPoint}"
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.application
|
||||||
|
|
||||||
|
import androidx.test.espresso.IdlingRegistry
|
||||||
|
import androidx.test.espresso.idling.CountingIdlingResource
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.location.MockLocationProvider
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.Api
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.interceptors.MockingNetworkInterceptor
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor
|
||||||
|
import java.io.BufferedReader
|
||||||
|
|
||||||
|
class TestAppClass : BaseAppClass() {
|
||||||
|
private val idlingResources = CountingIdlingResource("Data_loader")
|
||||||
|
private val mockingNetworkInterceptor = MockingNetworkInterceptor(idlingResources)
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
IdlingRegistry.getInstance().register(idlingResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createNetworkModule(): Api {
|
||||||
|
return NetworkModule().invoke<WeatherApi>(
|
||||||
|
NetworkConnectionInterceptor(this),
|
||||||
|
QueryParamsInterceptor(),
|
||||||
|
loggingInterceptor,
|
||||||
|
mockingNetworkInterceptor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLocationModule() = MockLocationProvider()
|
||||||
|
|
||||||
|
fun stubUrl(url: String, rawPath: String) {
|
||||||
|
val id = resources.getIdentifier(rawPath, "raw", packageName)
|
||||||
|
val iStream = resources.openRawResource(id)
|
||||||
|
val data = iStream.bufferedReader().use(BufferedReader::readText)
|
||||||
|
mockingNetworkInterceptor.addUrlStub(url = url, data = data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUrlStub(url: String){
|
||||||
|
mockingNetworkInterceptor.removeUrlStub(url = url)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.application
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.runner.AndroidJUnitRunner
|
||||||
|
|
||||||
|
class TestRunner : AndroidJUnitRunner() {
|
||||||
|
@Throws(
|
||||||
|
InstantiationException::class,
|
||||||
|
IllegalAccessException::class,
|
||||||
|
ClassNotFoundException::class
|
||||||
|
)
|
||||||
|
override fun newApplication(
|
||||||
|
cl: ClassLoader?,
|
||||||
|
className: String?,
|
||||||
|
context: Context?
|
||||||
|
): Application {
|
||||||
|
return super.newApplication(cl, TestAppClass::class.java.name, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.data.location
|
||||||
|
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||||
|
|
||||||
|
class MockLocationProvider : LocationProvider {
|
||||||
|
private val latLong = Pair(0.00, 0.00)
|
||||||
|
|
||||||
|
override suspend fun getCurrentLatLong() = latLong
|
||||||
|
override fun getLatLongFromLocationName(location: String) = latLong
|
||||||
|
|
||||||
|
override suspend fun getLocationNameFromLatLong(lat: Double, long: Double, type: LocationType): String {
|
||||||
|
return "Mock Location"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.data.network.interceptors
|
||||||
|
|
||||||
|
import androidx.test.espresso.idling.CountingIdlingResource
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.Protocol
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
|
||||||
|
class MockingNetworkInterceptor(
|
||||||
|
private val idlingResource: CountingIdlingResource
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
private var feedMap: MutableMap<String, String> = mutableMapOf()
|
||||||
|
private var urlCallTracker: MutableMap<String, Int> = mutableMapOf()
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
idlingResource.increment()
|
||||||
|
val original = chain.request()
|
||||||
|
val originalHttpUrl = original.url.toString().split("?")[0]
|
||||||
|
|
||||||
|
urlCallTracker.computeIfPresent(originalHttpUrl) { _, j ->
|
||||||
|
j + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
feedMap[originalHttpUrl]?.let { jsonPath ->
|
||||||
|
val body = jsonPath.toResponseBody("application/json".toMediaType())
|
||||||
|
|
||||||
|
val chainResponseBuilder = Response.Builder()
|
||||||
|
.code(200)
|
||||||
|
.protocol(Protocol.HTTP_1_1)
|
||||||
|
.request(original)
|
||||||
|
.message("OK")
|
||||||
|
.body(body)
|
||||||
|
idlingResource.decrement()
|
||||||
|
return chainResponseBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
idlingResource.decrement()
|
||||||
|
return chain.proceed(original)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addUrlStub(url: String, data: String) = feedMap.put(url, data)
|
||||||
|
fun removeUrlStub(url: String) = feedMap.remove(url)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.monoWeather.robot
|
||||||
|
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.BaseTestRobot
|
||||||
|
|
||||||
|
fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() }
|
||||||
|
class HomeScreenRobot : BaseTestRobot() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.rule.ActivityTestRule
|
||||||
|
import com.appttude.h_mal.atlas_weather.application.TestAppClass
|
||||||
|
import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.Stubs
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Rule
|
||||||
|
|
||||||
|
open class BaseTest() {
|
||||||
|
|
||||||
|
lateinit var testApp: TestAppClass
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
var mActivityTestRule: ActivityTestRule<MainActivity> = object : ActivityTestRule<MainActivity>(MainActivity::class.java) {
|
||||||
|
override fun beforeActivityLaunched() {
|
||||||
|
super.beforeActivityLaunched()
|
||||||
|
|
||||||
|
testApp = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
|
||||||
|
setupFeed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stubEndpoint(url: String, stub: Stubs) {
|
||||||
|
testApp.stubUrl(url, stub.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unstubEndpoint(url: String) {
|
||||||
|
testApp.removeUrlStub(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun setupFeed() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.test.rule.GrantPermissionRule
|
||||||
|
import com.appttude.h_mal.atlas_weather.monoWeather.robot.homeScreen
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.Stubs
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class HomePageUITest : BaseTest() {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
var mGrantPermissionRule: GrantPermissionRule =
|
||||||
|
GrantPermissionRule.grant(
|
||||||
|
"android.permission.ACCESS_COARSE_LOCATION")
|
||||||
|
|
||||||
|
override fun setupFeed() {
|
||||||
|
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadApp_validWeatherResponse_returnsValidPage() {
|
||||||
|
homeScreen {
|
||||||
|
verifyCurrentTemperature(2)
|
||||||
|
verifyCurrentLocation("Mock Location")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.utils
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.test.espresso.Espresso.onData
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.UiController
|
||||||
|
import androidx.test.espresso.ViewAction
|
||||||
|
import androidx.test.espresso.ViewInteraction
|
||||||
|
import androidx.test.espresso.action.ViewActions
|
||||||
|
import androidx.test.espresso.action.ViewActions.swipeDown
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||||
|
import org.hamcrest.CoreMatchers.allOf
|
||||||
|
import org.hamcrest.CoreMatchers.anything
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
|
||||||
|
open class BaseTestRobot {
|
||||||
|
|
||||||
|
fun fillEditText(resId: Int, text: String): ViewInteraction =
|
||||||
|
onView(withId(resId)).perform(ViewActions.replaceText(text), ViewActions.closeSoftKeyboard())
|
||||||
|
|
||||||
|
fun clickButton(resId: Int): ViewInteraction = onView((withId(resId))).perform(ViewActions.click())
|
||||||
|
|
||||||
|
fun textView(resId: Int): ViewInteraction = onView(withId(resId))
|
||||||
|
|
||||||
|
fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction
|
||||||
|
.check(ViewAssertions.matches(ViewMatchers.withText(text)))
|
||||||
|
|
||||||
|
fun matchText(resId: Int, text: String): ViewInteraction = matchText(textView(resId), text)
|
||||||
|
|
||||||
|
fun clickListItem(listRes: Int, position: Int) {
|
||||||
|
onData(anything())
|
||||||
|
.inAdapterView(allOf(withId(listRes)))
|
||||||
|
.atPosition(position).perform(ViewActions.click())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pullToRefresh(resId: Int){
|
||||||
|
onView(allOf(withId(resId), isDisplayed())).perform(swipeDown())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun waitFor(delay: Long): ViewAction? {
|
||||||
|
return object : ViewAction {
|
||||||
|
override fun getConstraints(): Matcher<View> = isRoot()
|
||||||
|
override fun getDescription(): String = "wait for $delay milliseconds"
|
||||||
|
override fun perform(uiController: UiController, v: View?) {
|
||||||
|
uiController.loopMainThreadForAtLeast(delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.utils
|
||||||
|
|
||||||
|
val FALLBACK_TIME: Long = 0L
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.utils
|
||||||
|
|
||||||
|
enum class Stubs(
|
||||||
|
val id: String
|
||||||
|
) {
|
||||||
|
Valid("valid_response"),
|
||||||
|
Invalid("invalid_response")
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package com.appttude.h_mal.atlas_weather.application
|
package com.appttude.h_mal.atlas_weather.application
|
||||||
|
|
||||||
import android.app.Application
|
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||||
import androidx.test.espresso.idling.CountingIdlingResource
|
|
||||||
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.Api
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
|
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
|
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
|
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
|
||||||
@@ -15,7 +16,6 @@ import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
|
|||||||
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import org.kodein.di.Kodein
|
import org.kodein.di.Kodein
|
||||||
import org.kodein.di.KodeinAware
|
|
||||||
import org.kodein.di.android.x.androidXModule
|
import org.kodein.di.android.x.androidXModule
|
||||||
import org.kodein.di.generic.bind
|
import org.kodein.di.generic.bind
|
||||||
import org.kodein.di.generic.instance
|
import org.kodein.di.generic.instance
|
||||||
@@ -24,30 +24,16 @@ import org.kodein.di.generic.singleton
|
|||||||
|
|
||||||
const val LOCATION_PERMISSION_REQUEST = 505
|
const val LOCATION_PERMISSION_REQUEST = 505
|
||||||
|
|
||||||
class AppClass : Application(), KodeinAware {
|
class AppClass : BaseAppClass() {
|
||||||
|
|
||||||
companion object {
|
override fun createNetworkModule(): Api {
|
||||||
// idling resource to be used for espresso testing
|
return NetworkModule().invoke<WeatherApi>(
|
||||||
// when we need to wait for async operations to complete
|
NetworkConnectionInterceptor(this),
|
||||||
val idlingResources = CountingIdlingResource("Data_loader")
|
QueryParamsInterceptor(),
|
||||||
|
loggingInterceptor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kodein creation of modules to be retrieve within the app
|
override fun createLocationModule() = LocationProviderImpl(this)
|
||||||
override val kodein = Kodein.lazy {
|
|
||||||
import(androidXModule(this@AppClass))
|
|
||||||
|
|
||||||
bind() from singleton { Gson() }
|
|
||||||
bind() from singleton { NetworkConnectionInterceptor(instance()) }
|
|
||||||
bind() from singleton { QueryParamsInterceptor() }
|
|
||||||
bind() from singleton { loggingInterceptor }
|
|
||||||
bind() from singleton { WeatherApi("https://api.openweathermap.org/data/2.5/", instance(), instance(), instance()) }
|
|
||||||
bind() from singleton { AppDatabase(instance()) }
|
|
||||||
bind() from singleton { PreferenceProvider(instance()) }
|
|
||||||
bind() from singleton { RepositoryImpl(instance(), instance(), instance()) }
|
|
||||||
bind() from singleton { SettingsRepositoryImpl(instance()) }
|
|
||||||
bind() from singleton { LocationProviderImpl(instance()) }
|
|
||||||
bind() from singleton { ServicesHelper(instance(), instance(), instance()) }
|
|
||||||
bind() from provider { ApplicationViewModelFactory(instance(), instance()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.application
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.test.espresso.idling.CountingIdlingResource
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.Api
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
|
||||||
|
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
|
||||||
|
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import org.kodein.di.Kodein
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.android.x.androidXModule
|
||||||
|
import org.kodein.di.generic.bind
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
import org.kodein.di.generic.provider
|
||||||
|
import org.kodein.di.generic.singleton
|
||||||
|
|
||||||
|
abstract class BaseAppClass : Application(), KodeinAware {
|
||||||
|
|
||||||
|
// Kodein creation of modules to be retrieve within the app
|
||||||
|
override val kodein = Kodein.lazy {
|
||||||
|
import(androidXModule(this@BaseAppClass))
|
||||||
|
|
||||||
|
bind() from singleton { createNetworkModule() as WeatherApi}
|
||||||
|
bind() from singleton { createLocationModule() }
|
||||||
|
|
||||||
|
bind() from singleton { Gson() }
|
||||||
|
bind() from singleton { AppDatabase(instance()) }
|
||||||
|
bind() from singleton { PreferenceProvider(instance()) }
|
||||||
|
bind() from singleton { RepositoryImpl(instance(), instance(), instance()) }
|
||||||
|
bind() from singleton { SettingsRepositoryImpl(instance()) }
|
||||||
|
bind() from singleton { ServicesHelper(instance(), instance(), instance()) }
|
||||||
|
bind() from provider { ApplicationViewModelFactory(instance(), instance()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun createNetworkModule() : Api
|
||||||
|
abstract fun createLocationModule() : LocationProvider
|
||||||
|
|
||||||
|
}
|
||||||
@@ -80,8 +80,6 @@ class LocationProviderImpl(
|
|||||||
handlerThread.start()
|
handlerThread.start()
|
||||||
// Now get the Looper from the HandlerThread
|
// Now get the Looper from the HandlerThread
|
||||||
// NOTE: This call will block until the HandlerThread gets control and initializes its Looper
|
// NOTE: This call will block until the HandlerThread gets control and initializes its Looper
|
||||||
// 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
|
val looper = handlerThread.looper
|
||||||
|
|
||||||
return suspendCoroutine { cont ->
|
return suspendCoroutine { cont ->
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.data.network
|
||||||
|
|
||||||
|
interface Api
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.data.network
|
||||||
|
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.buildOkHttpClient
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.createRetrofit
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
|
||||||
|
open class BaseNetworkModule {
|
||||||
|
// Declare the method we want/can change (no annotations)
|
||||||
|
open fun baseUrl() = "/"
|
||||||
|
|
||||||
|
inline fun <reified T: Api> invoke(
|
||||||
|
vararg interceptors: Interceptor
|
||||||
|
): Api {
|
||||||
|
|
||||||
|
val okHttpClient = buildOkHttpClient(*interceptors)
|
||||||
|
|
||||||
|
return createRetrofit(
|
||||||
|
baseUrl(),
|
||||||
|
okHttpClient,
|
||||||
|
T::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.data.network
|
||||||
|
|
||||||
|
class NetworkModule : BaseNetworkModule() {
|
||||||
|
override fun baseUrl(): String = "https://api.openweathermap.org/data/2.5/"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,17 +1,12 @@
|
|||||||
package com.appttude.h_mal.atlas_weather.data.network
|
package com.appttude.h_mal.atlas_weather.data.network
|
||||||
|
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
|
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
|
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.buildOkHttpClient
|
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.createRetrofit
|
|
||||||
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 okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
|
||||||
interface WeatherApi {
|
interface WeatherApi: Api {
|
||||||
|
|
||||||
@GET("onecall?")
|
@GET("onecall?")
|
||||||
suspend fun getFromApi(
|
suspend fun getFromApi(
|
||||||
@@ -21,27 +16,5 @@ interface WeatherApi {
|
|||||||
@Query("units") units: String = "metric"
|
@Query("units") units: String = "metric"
|
||||||
): Response<WeatherResponse>
|
): Response<WeatherResponse>
|
||||||
|
|
||||||
// invoke method creating an invocation of the api call
|
|
||||||
companion object{
|
|
||||||
operator fun invoke(
|
|
||||||
baseUrl: String,
|
|
||||||
networkConnectionInterceptor: NetworkConnectionInterceptor,
|
|
||||||
queryParamsInterceptor: QueryParamsInterceptor,
|
|
||||||
loggingInterceptor: HttpLoggingInterceptor
|
|
||||||
) : WeatherApi {
|
|
||||||
|
|
||||||
val okHttpClient = buildOkHttpClient(
|
|
||||||
networkConnectionInterceptor,
|
|
||||||
queryParamsInterceptor,
|
|
||||||
loggingInterceptor
|
|
||||||
)
|
|
||||||
|
|
||||||
return createRetrofit(
|
|
||||||
baseUrl,
|
|
||||||
okHttpClient,
|
|
||||||
WeatherApi::class.java
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import java.io.IOException
|
|||||||
|
|
||||||
class NetworkConnectionInterceptor(
|
class NetworkConnectionInterceptor(
|
||||||
context: Context
|
context: Context
|
||||||
) : Interceptor {
|
) : NetworkInterceptor {
|
||||||
|
|
||||||
private val applicationContext = context.applicationContext
|
private val applicationContext = context.applicationContext
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.data.network.interceptors
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
|
||||||
|
interface NetworkInterceptor : Interceptor
|
||||||
@@ -14,9 +14,8 @@ class QueryParamsInterceptor : Interceptor{
|
|||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val original = chain.request()
|
val original = chain.request()
|
||||||
val originalHttpUrl = original.url
|
|
||||||
|
|
||||||
val url = originalHttpUrl.newBuilder()
|
val url = original.url.newBuilder()
|
||||||
.addQueryParameter("appid", id)
|
.addQueryParameter("appid", id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.appttude.h_mal.atlas_weather.data.network.networkUtils
|
package com.appttude.h_mal.atlas_weather.data.network.networkUtils
|
||||||
|
|
||||||
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
|
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
|
||||||
|
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
@@ -13,7 +14,6 @@ val loggingInterceptor = HttpLoggingInterceptor().apply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun buildOkHttpClient(
|
fun buildOkHttpClient(
|
||||||
networkConnectionInterceptor: NetworkConnectionInterceptor,
|
|
||||||
vararg interceptor: Interceptor,
|
vararg interceptor: Interceptor,
|
||||||
timeoutSeconds: Long = 30L
|
timeoutSeconds: Long = 30L
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
@@ -21,11 +21,14 @@ fun buildOkHttpClient(
|
|||||||
val builder = OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
|
|
||||||
interceptor.forEach {
|
interceptor.forEach {
|
||||||
|
if (it is NetworkInterceptor) {
|
||||||
|
builder.addNetworkInterceptor(it)
|
||||||
|
} else {
|
||||||
builder.addInterceptor(it)
|
builder.addInterceptor(it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
builder.addNetworkInterceptor(networkConnectionInterceptor)
|
builder.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
|
||||||
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ 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.utils.FALLBACK_TIME
|
||||||
|
|
||||||
|
|
||||||
private const val FIVE_MINS = 300000L
|
|
||||||
class RepositoryImpl(
|
class RepositoryImpl(
|
||||||
private val api: WeatherApi,
|
private val api: WeatherApi,
|
||||||
private val db: AppDatabase,
|
private val db: AppDatabase,
|
||||||
@@ -53,7 +53,7 @@ class RepositoryImpl(
|
|||||||
?: return true
|
?: return true
|
||||||
val difference = System.currentTimeMillis() - lastSaved
|
val difference = System.currentTimeMillis() - lastSaved
|
||||||
|
|
||||||
return difference > FIVE_MINS
|
return difference > FALLBACK_TIME
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveLastSavedAt(locationName: String) {
|
override fun saveLastSavedAt(locationName: String) {
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.utils
|
||||||
|
|
||||||
|
val FALLBACK_TIME: Long = 300000L
|
||||||
4
app/src/main/res/raw/invalid_response.json
Normal file
4
app/src/main/res/raw/invalid_response.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"cod": 401,
|
||||||
|
"message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."
|
||||||
|
}
|
||||||
349
app/src/main/res/raw/valid_response.json
Normal file
349
app/src/main/res/raw/valid_response.json
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
{
|
||||||
|
"lat": 51.51,
|
||||||
|
"lon": -0.13,
|
||||||
|
"timezone": "Europe/London",
|
||||||
|
"timezone_offset": 3600,
|
||||||
|
"current": {
|
||||||
|
"dt": 1648932980,
|
||||||
|
"sunrise": 1648877626,
|
||||||
|
"sunset": 1648924449,
|
||||||
|
"temp": 2.14,
|
||||||
|
"feels_like": 0.13,
|
||||||
|
"pressure": 1025,
|
||||||
|
"humidity": 79,
|
||||||
|
"dew_point": -0.99,
|
||||||
|
"uvi": 0,
|
||||||
|
"clouds": 72,
|
||||||
|
"visibility": 10000,
|
||||||
|
"wind_speed": 1.92,
|
||||||
|
"wind_deg": 69,
|
||||||
|
"wind_gust": 4.81,
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 803,
|
||||||
|
"main": "Clouds",
|
||||||
|
"description": "broken clouds",
|
||||||
|
"icon": "04n"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"daily": [
|
||||||
|
{
|
||||||
|
"dt": 1648900800,
|
||||||
|
"sunrise": 1648877626,
|
||||||
|
"sunset": 1648924449,
|
||||||
|
"moonrise": 1648880220,
|
||||||
|
"moonset": 1648930140,
|
||||||
|
"moon_phase": 0.04,
|
||||||
|
"temp": {
|
||||||
|
"day": 8.5,
|
||||||
|
"min": 1,
|
||||||
|
"max": 8.97,
|
||||||
|
"night": 2.43,
|
||||||
|
"eve": 4.76,
|
||||||
|
"morn": 1
|
||||||
|
},
|
||||||
|
"feels_like": {
|
||||||
|
"day": 6.21,
|
||||||
|
"night": 0.54,
|
||||||
|
"eve": 2.22,
|
||||||
|
"morn": -1.74
|
||||||
|
},
|
||||||
|
"pressure": 1022,
|
||||||
|
"humidity": 35,
|
||||||
|
"dew_point": -6.02,
|
||||||
|
"wind_speed": 4.07,
|
||||||
|
"wind_deg": 41,
|
||||||
|
"wind_gust": 7.58,
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 804,
|
||||||
|
"main": "Clouds",
|
||||||
|
"description": "overcast clouds",
|
||||||
|
"icon": "04d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clouds": 100,
|
||||||
|
"pop": 0.27,
|
||||||
|
"uvi": 3.03
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dt": 1648987200,
|
||||||
|
"sunrise": 1648963890,
|
||||||
|
"sunset": 1649010949,
|
||||||
|
"moonrise": 1648967460,
|
||||||
|
"moonset": 1649020980,
|
||||||
|
"moon_phase": 0.07,
|
||||||
|
"temp": {
|
||||||
|
"day": 8.92,
|
||||||
|
"min": 0.99,
|
||||||
|
"max": 9.47,
|
||||||
|
"night": 5.62,
|
||||||
|
"eve": 8.19,
|
||||||
|
"morn": 1.01
|
||||||
|
},
|
||||||
|
"feels_like": {
|
||||||
|
"day": 7.63,
|
||||||
|
"night": 2.63,
|
||||||
|
"eve": 6.61,
|
||||||
|
"morn": -0.71
|
||||||
|
},
|
||||||
|
"pressure": 1026,
|
||||||
|
"humidity": 38,
|
||||||
|
"dew_point": -4.76,
|
||||||
|
"wind_speed": 3.99,
|
||||||
|
"wind_deg": 250,
|
||||||
|
"wind_gust": 9.75,
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 802,
|
||||||
|
"main": "Clouds",
|
||||||
|
"description": "scattered clouds",
|
||||||
|
"icon": "03d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clouds": 30,
|
||||||
|
"pop": 0,
|
||||||
|
"uvi": 3.04
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dt": 1649073600,
|
||||||
|
"sunrise": 1649050154,
|
||||||
|
"sunset": 1649097449,
|
||||||
|
"moonrise": 1649054880,
|
||||||
|
"moonset": 1649111880,
|
||||||
|
"moon_phase": 0.1,
|
||||||
|
"temp": {
|
||||||
|
"day": 9.43,
|
||||||
|
"min": 4.54,
|
||||||
|
"max": 12.17,
|
||||||
|
"night": 10.64,
|
||||||
|
"eve": 11.38,
|
||||||
|
"morn": 4.91
|
||||||
|
},
|
||||||
|
"feels_like": {
|
||||||
|
"day": 6.79,
|
||||||
|
"night": 10.01,
|
||||||
|
"eve": 10.87,
|
||||||
|
"morn": 0.46
|
||||||
|
},
|
||||||
|
"pressure": 1011,
|
||||||
|
"humidity": 93,
|
||||||
|
"dew_point": 8.41,
|
||||||
|
"wind_speed": 6.87,
|
||||||
|
"wind_deg": 245,
|
||||||
|
"wind_gust": 15.86,
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 500,
|
||||||
|
"main": "Rain",
|
||||||
|
"description": "light rain",
|
||||||
|
"icon": "10d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clouds": 100,
|
||||||
|
"pop": 0.92,
|
||||||
|
"rain": 1.95,
|
||||||
|
"uvi": 1.07
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dt": 1649160000,
|
||||||
|
"sunrise": 1649136419,
|
||||||
|
"sunset": 1649183949,
|
||||||
|
"moonrise": 1649142480,
|
||||||
|
"moonset": 0,
|
||||||
|
"moon_phase": 0.13,
|
||||||
|
"temp": {
|
||||||
|
"day": 13.7,
|
||||||
|
"min": 9.71,
|
||||||
|
"max": 14.06,
|
||||||
|
"night": 9.71,
|
||||||
|
"eve": 11.64,
|
||||||
|
"morn": 10.05
|
||||||
|
},
|
||||||
|
"feels_like": {
|
||||||
|
"day": 12.85,
|
||||||
|
"night": 6.63,
|
||||||
|
"eve": 10.66,
|
||||||
|
"morn": 9.25
|
||||||
|
},
|
||||||
|
"pressure": 1009,
|
||||||
|
"humidity": 66,
|
||||||
|
"dew_point": 7.47,
|
||||||
|
"wind_speed": 6.8,
|
||||||
|
"wind_deg": 258,
|
||||||
|
"wind_gust": 13,
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 500,
|
||||||
|
"main": "Rain",
|
||||||
|
"description": "light rain",
|
||||||
|
"icon": "10d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clouds": 99,
|
||||||
|
"pop": 0.36,
|
||||||
|
"rain": 0.13,
|
||||||
|
"uvi": 2.59
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dt": 1649246400,
|
||||||
|
"sunrise": 1649222684,
|
||||||
|
"sunset": 1649270449,
|
||||||
|
"moonrise": 1649230500,
|
||||||
|
"moonset": 1649202540,
|
||||||
|
"moon_phase": 0.17,
|
||||||
|
"temp": {
|
||||||
|
"day": 12.97,
|
||||||
|
"min": 8.53,
|
||||||
|
"max": 13.85,
|
||||||
|
"night": 8.53,
|
||||||
|
"eve": 9.68,
|
||||||
|
"morn": 9.51
|
||||||
|
},
|
||||||
|
"feels_like": {
|
||||||
|
"day": 11.94,
|
||||||
|
"night": 4.36,
|
||||||
|
"eve": 6.3,
|
||||||
|
"morn": 6.57
|
||||||
|
},
|
||||||
|
"pressure": 996,
|
||||||
|
"humidity": 62,
|
||||||
|
"dew_point": 5.84,
|
||||||
|
"wind_speed": 9.57,
|
||||||
|
"wind_deg": 260,
|
||||||
|
"wind_gust": 18.43,
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 500,
|
||||||
|
"main": "Rain",
|
||||||
|
"description": "light rain",
|
||||||
|
"icon": "10d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clouds": 100,
|
||||||
|
"pop": 0.86,
|
||||||
|
"rain": 1.95,
|
||||||
|
"uvi": 3.29
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dt": 1649332800,
|
||||||
|
"sunrise": 1649308949,
|
||||||
|
"sunset": 1649356949,
|
||||||
|
"moonrise": 1649319000,
|
||||||
|
"moonset": 1649292960,
|
||||||
|
"moon_phase": 0.2,
|
||||||
|
"temp": {
|
||||||
|
"day": 9.25,
|
||||||
|
"min": 4.63,
|
||||||
|
"max": 10.29,
|
||||||
|
"night": 7.17,
|
||||||
|
"eve": 8.81,
|
||||||
|
"morn": 4.63
|
||||||
|
},
|
||||||
|
"feels_like": {
|
||||||
|
"day": 6.57,
|
||||||
|
"night": 5.9,
|
||||||
|
"eve": 7.06,
|
||||||
|
"morn": -0.26
|
||||||
|
},
|
||||||
|
"pressure": 1000,
|
||||||
|
"humidity": 33,
|
||||||
|
"dew_point": -6.33,
|
||||||
|
"wind_speed": 9.1,
|
||||||
|
"wind_deg": 258,
|
||||||
|
"wind_gust": 21.32,
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 803,
|
||||||
|
"main": "Clouds",
|
||||||
|
"description": "broken clouds",
|
||||||
|
"icon": "04d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clouds": 66,
|
||||||
|
"pop": 0,
|
||||||
|
"uvi": 3.59
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dt": 1649419200,
|
||||||
|
"sunrise": 1649395215,
|
||||||
|
"sunset": 1649443449,
|
||||||
|
"moonrise": 1649408100,
|
||||||
|
"moonset": 1649382900,
|
||||||
|
"moon_phase": 0.23,
|
||||||
|
"temp": {
|
||||||
|
"day": 8,
|
||||||
|
"min": 3.94,
|
||||||
|
"max": 8.91,
|
||||||
|
"night": 4.79,
|
||||||
|
"eve": 7.09,
|
||||||
|
"morn": 3.94
|
||||||
|
},
|
||||||
|
"feels_like": {
|
||||||
|
"day": 6.02,
|
||||||
|
"night": 1.81,
|
||||||
|
"eve": 4.59,
|
||||||
|
"morn": 2.23
|
||||||
|
},
|
||||||
|
"pressure": 1000,
|
||||||
|
"humidity": 39,
|
||||||
|
"dew_point": -5.12,
|
||||||
|
"wind_speed": 4.52,
|
||||||
|
"wind_deg": 354,
|
||||||
|
"wind_gust": 9.29,
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 500,
|
||||||
|
"main": "Rain",
|
||||||
|
"description": "light rain",
|
||||||
|
"icon": "10d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clouds": 100,
|
||||||
|
"pop": 0.8,
|
||||||
|
"rain": 1.51,
|
||||||
|
"uvi": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dt": 1649505600,
|
||||||
|
"sunrise": 1649481482,
|
||||||
|
"sunset": 1649529949,
|
||||||
|
"moonrise": 1649497920,
|
||||||
|
"moonset": 1649472180,
|
||||||
|
"moon_phase": 0.25,
|
||||||
|
"temp": {
|
||||||
|
"day": 8.7,
|
||||||
|
"min": 1.91,
|
||||||
|
"max": 9.35,
|
||||||
|
"night": 5.29,
|
||||||
|
"eve": 7.66,
|
||||||
|
"morn": 1.91
|
||||||
|
},
|
||||||
|
"feels_like": {
|
||||||
|
"day": 5.76,
|
||||||
|
"night": 2.55,
|
||||||
|
"eve": 4.78,
|
||||||
|
"morn": -1.89
|
||||||
|
},
|
||||||
|
"pressure": 1016,
|
||||||
|
"humidity": 36,
|
||||||
|
"dew_point": -5.69,
|
||||||
|
"wind_speed": 5.65,
|
||||||
|
"wind_deg": 314,
|
||||||
|
"wind_gust": 11.58,
|
||||||
|
"weather": [
|
||||||
|
{
|
||||||
|
"id": 500,
|
||||||
|
"main": "Rain",
|
||||||
|
"description": "light rain",
|
||||||
|
"icon": "10d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clouds": 55,
|
||||||
|
"pop": 0.3,
|
||||||
|
"rain": 0.22,
|
||||||
|
"uvi": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user