diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index ba14306..cc7c455 100644 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/app/build.gradle b/app/build.gradle index a28e903..22fb4ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,7 +17,7 @@ android { targetSdkVersion 30 versionCode 5 versionName "3.0" - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + testInstrumentationRunner "com.appttude.h_mal.atlas_weather.application.TestRunner" vectorDrawables.useSupportLibrary = true buildConfigField "String", "ParamOne", "${paramOneEndPoint}" diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestAppClass.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestAppClass.kt new file mode 100644 index 0000000..9fdb4a8 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestAppClass.kt @@ -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( + 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) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestRunner.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestRunner.kt new file mode 100644 index 0000000..fc5af94 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/application/TestRunner.kt @@ -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) + } + +} diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/MockLocationProvider.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/MockLocationProvider.kt new file mode 100644 index 0000000..8f36fe7 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/MockLocationProvider.kt @@ -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" + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/MockingNetworkInterceptor.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/MockingNetworkInterceptor.kt new file mode 100644 index 0000000..eff2b18 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/MockingNetworkInterceptor.kt @@ -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 = mutableMapOf() + private var urlCallTracker: MutableMap = 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) + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/robot/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/robot/HomeScreenRobot.kt new file mode 100644 index 0000000..61e5364 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/robot/HomeScreenRobot.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/BaseTest.kt new file mode 100644 index 0000000..4489e41 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/BaseTest.kt @@ -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 = object : ActivityTestRule(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() {} +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITest.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITest.kt new file mode 100644 index 0000000..5972ca7 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITest.kt @@ -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") + } + } +} diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/BaseTestRobot.kt new file mode 100644 index 0000000..599989e --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/BaseTestRobot.kt @@ -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 = isRoot() + override fun getDescription(): String = "wait for $delay milliseconds" + override fun perform(uiController: UiController, v: View?) { + uiController.loopMainThreadForAtLeast(delay) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt new file mode 100644 index 0000000..281d3e1 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt @@ -0,0 +1,3 @@ +package com.appttude.h_mal.atlas_weather.utils + +val FALLBACK_TIME: Long = 0L \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Stubs.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Stubs.kt new file mode 100644 index 0000000..610fc11 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/Stubs.kt @@ -0,0 +1,8 @@ +package com.appttude.h_mal.atlas_weather.utils + +enum class Stubs( + val id: String +) { + Valid("valid_response"), + Invalid("invalid_response") +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/application/AppClass.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/application/AppClass.kt index f19d7ec..3af5874 100644 --- a/app/src/main/java/com/appttude/h_mal/atlas_weather/application/AppClass.kt +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/application/AppClass.kt @@ -1,8 +1,9 @@ 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 @@ -15,7 +16,6 @@ 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 @@ -24,30 +24,16 @@ import org.kodein.di.generic.singleton const val LOCATION_PERMISSION_REQUEST = 505 -class AppClass : Application(), KodeinAware { +class AppClass : BaseAppClass() { - companion object { - // idling resource to be used for espresso testing - // when we need to wait for async operations to complete - val idlingResources = CountingIdlingResource("Data_loader") + override fun createNetworkModule(): Api { + return NetworkModule().invoke( + NetworkConnectionInterceptor(this), + QueryParamsInterceptor(), + loggingInterceptor + ) } - // Kodein creation of modules to be retrieve within the app - 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()) } - } + override fun createLocationModule() = LocationProviderImpl(this) } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/application/BaseAppClass.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/application/BaseAppClass.kt new file mode 100644 index 0000000..e19dc00 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/application/BaseAppClass.kt @@ -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 + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImpl.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImpl.kt index a26356b..8a1f9bf 100644 --- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImpl.kt +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImpl.kt @@ -80,8 +80,6 @@ class LocationProviderImpl( handlerThread.start() // Now get the Looper from the HandlerThread // 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 return suspendCoroutine { cont -> diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/Api.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/Api.kt new file mode 100644 index 0000000..4a3d64d --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/Api.kt @@ -0,0 +1,3 @@ +package com.appttude.h_mal.atlas_weather.data.network + +interface Api \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/BaseNetworkModule.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/BaseNetworkModule.kt new file mode 100644 index 0000000..bb02716 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/BaseNetworkModule.kt @@ -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 invoke( + vararg interceptors: Interceptor + ): Api { + + val okHttpClient = buildOkHttpClient(*interceptors) + + return createRetrofit( + baseUrl(), + okHttpClient, + T::class.java + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/NetworkModule.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/NetworkModule.kt new file mode 100644 index 0000000..93cae53 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/NetworkModule.kt @@ -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/" +} + diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/WeatherApi.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/WeatherApi.kt index 656787c..00bb356 100644 --- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/WeatherApi.kt +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/WeatherApi.kt @@ -1,17 +1,12 @@ 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 okhttp3.logging.HttpLoggingInterceptor import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query -interface WeatherApi { +interface WeatherApi: Api { @GET("onecall?") suspend fun getFromApi( @@ -21,27 +16,5 @@ interface WeatherApi { @Query("units") units: String = "metric" ): Response - // 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 - ) - } - } } diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/NetworkConnectionInterceptor.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/NetworkConnectionInterceptor.kt index 9fec2ce..5467db8 100644 --- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/NetworkConnectionInterceptor.kt +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/NetworkConnectionInterceptor.kt @@ -7,7 +7,7 @@ import java.io.IOException class NetworkConnectionInterceptor( context: Context -) : Interceptor { +) : NetworkInterceptor { private val applicationContext = context.applicationContext diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/NetworkInterceptor.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/NetworkInterceptor.kt new file mode 100644 index 0000000..8a23bb8 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/NetworkInterceptor.kt @@ -0,0 +1,5 @@ +package com.appttude.h_mal.atlas_weather.data.network.interceptors + +import okhttp3.Interceptor + +interface NetworkInterceptor : Interceptor \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/QueryParamsInterceptor.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/QueryParamsInterceptor.kt index 61f19bb..4cfffd7 100644 --- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/QueryParamsInterceptor.kt +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/interceptors/QueryParamsInterceptor.kt @@ -14,14 +14,13 @@ class QueryParamsInterceptor : Interceptor{ override fun intercept(chain: Interceptor.Chain): Response { val original = chain.request() - val originalHttpUrl = original.url - val url = originalHttpUrl.newBuilder() + val url = original.url.newBuilder() .addQueryParameter("appid", id) .build() // Request customization: add request headers - val requestBuilder= original.newBuilder().url(url) + val requestBuilder = original.newBuilder().url(url) val request: Request = requestBuilder.build() return chain.proceed(request) diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/networkUtils/RetrofitComponents.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/networkUtils/RetrofitComponents.kt index c136a8f..33a628e 100644 --- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/networkUtils/RetrofitComponents.kt +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/network/networkUtils/RetrofitComponents.kt @@ -1,6 +1,7 @@ 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.NetworkInterceptor import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -13,7 +14,6 @@ val loggingInterceptor = HttpLoggingInterceptor().apply { } fun buildOkHttpClient( - networkConnectionInterceptor: NetworkConnectionInterceptor, vararg interceptor: Interceptor, timeoutSeconds: Long = 30L ): OkHttpClient { @@ -21,11 +21,14 @@ fun buildOkHttpClient( val builder = OkHttpClient.Builder() interceptor.forEach { - builder.addInterceptor(it) + if (it is NetworkInterceptor) { + builder.addNetworkInterceptor(it) + } else { + builder.addInterceptor(it) + } } - builder.addNetworkInterceptor(networkConnectionInterceptor) - .connectTimeout(timeoutSeconds, TimeUnit.SECONDS) + builder.connectTimeout(timeoutSeconds, TimeUnit.SECONDS) .writeTimeout(timeoutSeconds, TimeUnit.SECONDS) .readTimeout(timeoutSeconds, TimeUnit.SECONDS) diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt index bb394c2..dcd2272 100644 --- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/repository/RepositoryImpl.kt @@ -7,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.room.AppDatabase 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( private val api: WeatherApi, private val db: AppDatabase, @@ -53,7 +53,7 @@ class RepositoryImpl( ?: return true val difference = System.currentTimeMillis() - lastSaved - return difference > FIVE_MINS + return difference > FALLBACK_TIME } override fun saveLastSavedAt(locationName: String) { diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt new file mode 100644 index 0000000..84fd85c --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/utils/Constants.kt @@ -0,0 +1,3 @@ +package com.appttude.h_mal.atlas_weather.utils + +val FALLBACK_TIME: Long = 300000L \ No newline at end of file diff --git a/app/src/main/res/raw/invalid_response.json b/app/src/main/res/raw/invalid_response.json new file mode 100644 index 0000000..ea6c80f --- /dev/null +++ b/app/src/main/res/raw/invalid_response.json @@ -0,0 +1,4 @@ +{ + "cod": 401, + "message": "Invalid API key. Please see http://openweathermap.org/faq#error401 for more info." +} \ No newline at end of file diff --git a/app/src/main/res/raw/valid_response.json b/app/src/main/res/raw/valid_response.json new file mode 100644 index 0000000..3425619 --- /dev/null +++ b/app/src/main/res/raw/valid_response.json @@ -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 + } + ] +} \ No newline at end of file