From 5acb95f191bcba86a5b0835fab4b64889a1809f7 Mon Sep 17 00:00:00 2001 From: hmalik144 Date: Sun, 23 Feb 2020 18:30:30 +0000 Subject: [PATCH] Initial commit --- .gitignore | 14 ++ .idea/.name | 1 + .idea/codeStyles/Project.xml | 125 +++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/dictionaries/h_mal.xml | 3 + .idea/gradle.xml | 19 ++ .idea/misc.xml | 17 ++ .idea/runConfigurations.xml | 12 ++ app/.gitignore | 1 + app/build.gradle | 71 ++++++++ app/proguard-rules.pro | 21 +++ .../ui/main/MainActivityTest.kt | 109 +++++++++++ app/src/main/AndroidManifest.xml | 26 +++ .../transportcodingchallenge/AppClass.kt | 32 ++++ .../data/model/RoadData.kt | 8 + .../data/network/ApiData.kt | 43 +++++ .../network/NetworkConnectionInterceptor.kt | 39 ++++ .../data/network/SafeApiRequest.kt | 33 ++++ .../data/repositories/Repository.kt | 25 +++ .../ui/main/CompletionListener.kt | 7 + .../ui/main/MainActivity.kt | 75 ++++++++ .../ui/main/MainViewModel.kt | 59 ++++++ .../ui/main/MainViewModelFactory.kt | 15 ++ .../utils/ToggleViewUtils.kt | 19 ++ .../drawable-v24/ic_launcher_foreground.xml | 34 ++++ .../res/drawable/ic_launcher_background.xml | 170 +++++++++++++++++ app/src/main/res/layout/main_activity.xml | 151 +++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes app/src/main/res/values/colors.xml | 8 + app/src/main/res/values/dimen.xml | 5 + app/src/main/res/values/keys.xml | 5 + app/src/main/res/values/strings.xml | 7 + app/src/main/res/values/styles.xml | 11 ++ .../ExampleUnitTest.kt | 17 ++ build.gradle | 28 +++ gradle.properties | 21 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++++++++++++++ gradlew.bat | 84 +++++++++ settings.gradle | 2 + 52 files changed, 1510 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.name create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/dictionaries/h_mal.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/example/h_mal/transportcodingchallenge/ui/main/MainActivityTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/AppClass.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/data/model/RoadData.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/ApiData.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/NetworkConnectionInterceptor.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/SafeApiRequest.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/data/repositories/Repository.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/CompletionListener.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainActivity.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainViewModel.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainViewModelFactory.kt create mode 100644 app/src/main/java/com/example/h_mal/transportcodingchallenge/utils/ToggleViewUtils.kt create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/main_activity.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimen.xml create mode 100644 app/src/main/res/values/keys.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/com/example/h_mal/transportcodingchallenge/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..1112e5d --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Transport Coding Challenge \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..45b5654 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/dictionaries/h_mal.xml b/.idea/dictionaries/h_mal.xml new file mode 100644 index 0000000..f32ad56 --- /dev/null +++ b/.idea/dictionaries/h_mal.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..169fd0d --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d5727af --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..3ff6d77 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,71 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +//kotlin kapt +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 29 + defaultConfig { + applicationId "com.example.h_mal.transportcodingchallenge" + minSdkVersion 24 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + dataBinding { + enabled = true + } + testOptions { + unitTests.returnDefaultValues = true + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.core:core-ktx:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + + //Retrofit and GSON + implementation 'com.squareup.retrofit2:retrofit:2.6.0' + implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + + //Kotlin Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0" + + // ViewModel and LiveData + implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" + + //New Material Design + implementation 'com.google.android.material:material:1.1.0-alpha10' + + //Kodein Dependency Injection + implementation "org.kodein.di:kodein-di-generic-jvm:6.2.1" + implementation "org.kodein.di:kodein-di-framework-android-x:6.2.1" + + //Android Navigation Architecture + implementation "androidx.navigation:navigation-fragment-ktx:2.2.0-alpha02" + implementation "androidx.navigation:navigation-ui-ktx:2.2.0-alpha02" + + androidTestImplementation 'androidx.test:rules:1.3.0-alpha04' + +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/example/h_mal/transportcodingchallenge/ui/main/MainActivityTest.kt b/app/src/androidTest/java/com/example/h_mal/transportcodingchallenge/ui/main/MainActivityTest.kt new file mode 100644 index 0000000..2975776 --- /dev/null +++ b/app/src/androidTest/java/com/example/h_mal/transportcodingchallenge/ui/main/MainActivityTest.kt @@ -0,0 +1,109 @@ +package com.example.h_mal.transportcodingchallenge.ui.main + + +import android.net.wifi.WifiManager +import android.view.View +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.action.ViewActions.* +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.matcher.RootMatchers.withDecorView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.filters.LargeTest +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import androidx.test.rule.ActivityTestRule +import com.example.h_mal.transportcodingchallenge.R +import org.hamcrest.Matcher +import org.hamcrest.Matchers.* +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@LargeTest +@RunWith(AndroidJUnit4ClassRunner::class) +class MainActivityTest { + + @Rule + @JvmField + var mActivityTestRule = ActivityTestRule(MainActivity::class.java) + + val submitButton = onView(allOf(withId(R.id.submit), withText("Submit"), isDisplayed())) + val searchEditText = onView(allOf(withId(R.id.search_box), isDisplayed())) + + @Test + fun testSubmit_emptySearch() { + submitButton.perform(click()) + + testToast("No Road ID inserted") + } + + @Test + fun testSubmit_SearchInvalidEntry() { + + searchEditText.perform(replaceText("A31222"), closeSoftKeyboard()) + submitButton.perform(click()) + waitFor(1000) + testToast( + "The following road id is not recognised: A31222" + ) + } + + @Test + fun testSubmit_SearchValidEntry() { + + searchEditText.perform(replaceText("A40"), closeSoftKeyboard()) + submitButton.perform(click()) + waitFor(1000) + + onView(allOf( + withId(R.id.road_id), + withText("A40"), + isDisplayed() + ) + ) + + onView( + allOf( + withText("Road Status"), + isDisplayed() + ) + ) + + onView( + allOf( + withText("Road Description"), + isDisplayed() + ) + ) + + } + + private fun testToast(toastText:String){ + onView(withText(toastText)) + .inRoot(withDecorView(not(`is`(mActivityTestRule.activity.window.decorView)))) + .check(matches(isDisplayed())) + + waitFor(2000) + } + + fun waitFor(delay: Long): ViewAction? { + return object : ViewAction { + override fun getConstraints(): Matcher { + return isRoot() + } + + override fun getDescription(): String { + return "wait for " + delay + "milliseconds" + } + + override fun perform(uiController: UiController, view: View?) { + uiController.loopMainThreadForAtLeast(delay) + } + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2747732 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/AppClass.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/AppClass.kt new file mode 100644 index 0000000..e5619bc --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/AppClass.kt @@ -0,0 +1,32 @@ +package com.example.h_mal.transportcodingchallenge + +import android.app.Application +import com.example.h_mal.transportcodingchallenge.data.network.ApiData +import com.example.h_mal.transportcodingchallenge.data.network.NetworkConnectionInterceptor +import com.example.h_mal.transportcodingchallenge.data.repositories.Repository +import com.example.h_mal.transportcodingchallenge.ui.main.MainViewModelFactory +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 + +class AppClass :Application(), KodeinAware{ + + //kodeinaware for dependency injection + override val kodein = Kodein.lazy { + //import relevent module + import(androidXModule(this@AppClass)) + + //create instance of network interceptor + bind() from singleton { NetworkConnectionInterceptor(instance()) } + //create an instance of Api data class for retrofit calls + bind() from singleton { ApiData(instance()) } + //create an instance of the repository with above api class inserted + bind() from singleton { Repository(instance(), instance()) } + //create an instance of viewmodel factory with above repository inserted + bind() from provider{ MainViewModelFactory(instance())} + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/model/RoadData.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/model/RoadData.kt new file mode 100644 index 0000000..7b6e476 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/model/RoadData.kt @@ -0,0 +1,8 @@ +package com.example.h_mal.transportcodingchallenge.data.model + +data class RoadData( + val displayName: String, + val statusSeverity: String, + val statusSeverityDescription: String +) + diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/ApiData.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/ApiData.kt new file mode 100644 index 0000000..efdf087 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/ApiData.kt @@ -0,0 +1,43 @@ +package com.example.h_mal.transportcodingchallenge.data.network + +import com.example.h_mal.transportcodingchallenge.data.model.RoadData + +import okhttp3.OkHttpClient +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface ApiData { + + + //Retrofit api call with path being the road id we input + @GET("{roadId}") + suspend fun getRoadData(@Path("roadId") roadId: String, + @Query("app_key") app_key: String, + @Query("app_id") app_id: String) : Response> + + companion object{ + //class invokation with the network interceptor passed + operator fun invoke( + networkConnectionInterceptor: NetworkConnectionInterceptor + ) : ApiData{ + + //build okhttpclient + val okkHttpclient = OkHttpClient.Builder() + .addInterceptor(networkConnectionInterceptor) + .build() + + //return our API data class + return Retrofit.Builder() + .client(okkHttpclient) + .baseUrl("https://api.tfl.gov.uk/Road/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(ApiData::class.java) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/NetworkConnectionInterceptor.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/NetworkConnectionInterceptor.kt new file mode 100644 index 0000000..7b097d3 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/NetworkConnectionInterceptor.kt @@ -0,0 +1,39 @@ +package com.example.h_mal.transportcodingchallenge.data.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException + +class NetworkConnectionInterceptor( + context: Context +) : Interceptor { + + private val applicationContext = context.applicationContext + + //check if there is an active data connection or throw error + override fun intercept(chain: Interceptor.Chain): Response { + if (!isInternetAvailable()) + throw IOException("Make sure you have an active data connection") + return chain.proceed(chain.request()) + } + + private fun isInternetAvailable(): Boolean { + var result = false + val connectivityManager = + applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager? + connectivityManager?.let { + it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply { + result = when { + hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + else -> false + } + } + } + return result + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/SafeApiRequest.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/SafeApiRequest.kt new file mode 100644 index 0000000..209414d --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/network/SafeApiRequest.kt @@ -0,0 +1,33 @@ +package com.example.h_mal.transportcodingchallenge.data.network + +import android.util.Log +import org.json.JSONException +import org.json.JSONObject +import retrofit2.Response +import java.io.IOException + +abstract class SafeApiRequest { + + //safe api call to safely return our response from the our object + // in the form of {@RoadObject} or throw an error based off the error message in the JSON + suspend fun apiRequest(call: suspend () -> Response) : T{ + val response = call.invoke() + if(response.isSuccessful){ + return response.body()!! + }else{ + val error = response.errorBody()?.string() + + val message = StringBuilder() + error?.let{ + try{ + message.append(JSONObject(it).getString("message")) + }catch(e: JSONException){ } + + } + Log.e("Network Error","Error Code: ${response.code()}") + + throw IOException(message.toString()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/repositories/Repository.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/repositories/Repository.kt new file mode 100644 index 0000000..1479001 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/data/repositories/Repository.kt @@ -0,0 +1,25 @@ +package com.example.h_mal.transportcodingchallenge.data.repositories + +import android.content.Context +import com.example.h_mal.transportcodingchallenge.R +import com.example.h_mal.transportcodingchallenge.data.model.RoadData +import com.example.h_mal.transportcodingchallenge.data.network.ApiData +import com.example.h_mal.transportcodingchallenge.data.network.SafeApiRequest + +class Repository( + private val api: ApiData, + context: Context +): SafeApiRequest(){ + + //retrieve the values from the resourse class + private val apiKey: String = context.getString(R.string.Api_key) + private val appId: String = context.getString(R.string.Application_ID) + + //api function to retrieve data from the api via retrofit + //return the data in the for of list as that is how the JSON is + suspend fun getRoadData(roadId: String): List?{ + return apiRequest { + api.getRoadData(roadId, apiKey, appId) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/CompletionListener.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/CompletionListener.kt new file mode 100644 index 0000000..2228da4 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/CompletionListener.kt @@ -0,0 +1,7 @@ +package com.example.h_mal.transportcodingchallenge.ui.main + +interface CompletionListener { + fun onStarted() + fun onSuccess() + fun onFailure(message: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainActivity.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainActivity.kt new file mode 100644 index 0000000..f728dd3 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainActivity.kt @@ -0,0 +1,75 @@ +package com.example.h_mal.transportcodingchallenge.ui.main + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.widget.Toast +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import com.example.h_mal.transportcodingchallenge.R +import com.example.h_mal.transportcodingchallenge.databinding.MainActivityBinding +import com.example.h_mal.transportcodingchallenge.utils.* +import kotlinx.android.synthetic.main.main_activity.* +import org.kodein.di.KodeinAware +import org.kodein.di.android.kodein +import org.kodein.di.generic.instance + +class MainActivity : AppCompatActivity(), KodeinAware, CompletionListener { + + //obtain viewmodel factory from the application instantiation(s) + override val kodein by kodein() + private val factory : MainViewModelFactory by instance() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + //setup view for databinding for data to be held in view model + //viewmodels outlive UI so its the best place to keep data + val binding: MainActivityBinding = DataBindingUtil.setContentView(this, R.layout.main_activity) + //create viewmodel class + val viewModel = ViewModelProviders.of(this,factory).get(MainViewModel::class.java) + //binding viewmodel to databing + binding.viewmodel = viewModel + + //setup completion listener + viewModel.completionListener = this + + //observe live data from the view model to change views accordingly + viewModel.roadLiveDate.observe(this, Observer { + result_box.show() + road_id.text = it.displayName + road_status.text = it.statusSeverity + road_description.text = it.statusSeverityDescription + }) + } + + //disable views while loading + private fun invalidateViews(){ + search_box.disable() + submit.disable() + } + + //enable views after loadning + private fun revalidateViews(){ + search_box.enable() + submit.enable() + } + + override fun onStarted() { + invalidateViews() + progress_circular.show() + } + + override fun onSuccess() { + revalidateViews() + progress_circular.hide() + } + + override fun onFailure(message: String) { + Toast.makeText(this,message, Toast.LENGTH_SHORT).show() + progress_circular.hide() + revalidateViews() + } + +} diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainViewModel.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainViewModel.kt new file mode 100644 index 0000000..e5f94bf --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainViewModel.kt @@ -0,0 +1,59 @@ +package com.example.h_mal.transportcodingchallenge.ui.main + +import android.view.View +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.example.h_mal.transportcodingchallenge.data.model.RoadData +import com.example.h_mal.transportcodingchallenge.data.repositories.Repository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.IOException + +class MainViewModel( + private val repository: Repository +) : ViewModel() { + + //completion listener + var completionListener: CompletionListener? = null + + //livedata to populate views in the UI + var roadLiveDate: MutableLiveData = MutableLiveData() + + //databinding the text in the edittext in the layout + var roadId: String? = null + + //databinding the onclick of the submit button in the view + fun submit(view: View?){ + //callback to the view that the operation has started + completionListener?.onStarted() + + //null check the input + if (roadId.isNullOrEmpty()){ + completionListener?.onFailure("No Road ID inserted") + return + } + + //launch a coroutine on main thread to fetch data + CoroutineScope(Dispatchers.Main).launch { + try { + //fetch data from the repository + val response = repository.getRoadData(roadId!!.trim()) + + //unwrap if response is not null + response?.get(0)?.let { + //update the live data + roadLiveDate.value = it + //callback to ciew that operation was successful + completionListener?.onSuccess() + return@launch + } + //callback operation was failed + completionListener?.onFailure("Failed to retrieve data") + }catch (e: IOException){ + //callback operation was failed + completionListener?.onFailure(e.message!!) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainViewModelFactory.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainViewModelFactory.kt new file mode 100644 index 0000000..8c4c02a --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/ui/main/MainViewModelFactory.kt @@ -0,0 +1,15 @@ +package com.example.h_mal.transportcodingchallenge.ui.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.example.h_mal.transportcodingchallenge.data.repositories.Repository + +@Suppress("UNCHECKED_CAST") +class MainViewModelFactory ( + private val repository: Repository +): ViewModelProvider.NewInstanceFactory(){ + + override fun create(modelClass: Class): T { + return MainViewModel(repository) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/transportcodingchallenge/utils/ToggleViewUtils.kt b/app/src/main/java/com/example/h_mal/transportcodingchallenge/utils/ToggleViewUtils.kt new file mode 100644 index 0000000..add700a --- /dev/null +++ b/app/src/main/java/com/example/h_mal/transportcodingchallenge/utils/ToggleViewUtils.kt @@ -0,0 +1,19 @@ +package com.example.h_mal.transportcodingchallenge.utils + +import android.view.View + +fun View.show(){ + visibility = View.VISIBLE +} + +fun View.hide(){ + visibility = View.GONE +} + +fun View.disable(){ + isEnabled = false +} + +fun View.enable(){ + isEnabled = true +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml new file mode 100644 index 0000000..48a5a4c --- /dev/null +++ b/app/src/main/res/layout/main_activity.xml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +