diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 674414f..5ef3273 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,17 +4,15 @@
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 99e86f7..4246f14 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -5,7 +5,7 @@
-
+
@@ -18,12 +18,15 @@
+
+
+
-
+
@@ -35,11 +38,14 @@
+
+
+
-
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 60f4be0..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
index 7f68460..72f00ed 100644
--- a/.idea/runConfigurations.xml
+++ b/.idea/runConfigurations.xml
@@ -3,6 +3,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
index a09cb07..974ff0b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,81 +5,105 @@ plugins {
id 'kotlin-kapt'
}
+Properties properties = new Properties()
+boolean propertiesFileExists = project.rootProject.file('local.properties').canRead()
+if (propertiesFileExists) properties.load(project.rootProject.file('local.properties').newDataInputStream())
+
+def keystoreFile = project.rootProject.file("app/keystore.jks")
+
android {
- compileSdkVersion 32
+ compileSdk = Integer.parseInt(TARGET_SDK_VERSION)
+ namespace "com.appttude.h_mal.easycc"
defaultConfig {
applicationId "com.appttude.h_mal.easycc"
- minSdkVersion 21
- targetSdkVersion 32
+ minSdkVersion MIN_SDK_VERSION
+ targetSdkVersion TARGET_SDK_VERSION
versionCode 5
versionName "4.1"
testInstrumentationRunner "com.appttude.h_mal.easycc.application.TestRunner"
}
+ signingConfigs {
+ release {
+ storePassword System.getProperty("RELEASE_STORE_PASSWORD") ?: properties.getProperty('RELEASE_STORE_PASSWORD')
+ keyPassword System.getProperty("RELEASE_KEY_PASSWORD") ?: properties.getProperty('RELEASE_KEY_PASSWORD')
+ keyAlias System.getProperty("RELEASE_KEY_ALIAS") ?: properties.getProperty('RELEASE_KEY_ALIAS')
+ storeFile keystoreFile.exists() ? keystoreFile : null
+ }
+ }
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
+
+ staging {
+ initWith debug
+ manifestPlaceholders = [hostName:"internal.example.com"]
+ applicationIdSuffix ".debugStaging"
+ }
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = '1.8'
+ jvmTarget = '17'
}
buildFeatures {
viewBinding true
}
+
+// java {
+// toolchain {
+// languageVersion.set(JavaLanguageVersion.of(21))
+// }
+// }
}
dependencies {
- implementation 'androidx.core:core-ktx:1.8.0'
- implementation 'androidx.appcompat:appcompat:1.4.2'
- implementation 'com.google.android.material:material:1.6.1'
- implementation 'androidx.annotation:annotation:1.4.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
- implementation 'androidx.activity:activity-ktx:1.5.1'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
- androidTestImplementation 'androidx.test:rules:1.4.0'
- implementation 'org.jetbrains.kotlin:kotlin-test:1.7.10'
+ implementation "androidx.core:core-ktx:$ANDROID_CORE_VERSION"
+ implementation "androidx.appcompat:appcompat:$APP_COMPAT"
+ implementation "androidx.annotation:annotation:$ANNOTATION_VERSION"
+ implementation "androidx.constraintlayout:constraintlayout:$CONSTR_LAYOUT_VERSION"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:$ANDROID_LIFECYCLE"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$ANDROID_LIFECYCLE"
+ implementation "androidx.activity:activity-ktx:$ACTIVITY_VERSION"
+ testImplementation "junit:junit:$JUNIT_VERSION"
+ androidTestImplementation "androidx.test.ext:junit:$TEST_JUNIT_VERSION"
+ androidTestImplementation "androidx.test.espresso:espresso-core:$ESPRESSO_VERSION"
+ androidTestImplementation "androidx.test:rules:$TEST_RULE"
+ implementation "androidx.tracing:tracing:1.1.0"
+ implementation "org.jetbrains.kotlin:kotlin-test:$KOTLIN_VERSION"
// Coroutines
- def coroutines_version = "1.6.2"
- testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$KOTLINX_COROUTINES"
//Retrofit and GSON
- def retrofit_ver = "2.8.1"
- implementation "com.squareup.retrofit2:retrofit:$retrofit_ver"
- implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver"
- implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"
+ implementation "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
+ implementation "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
+ implementation "com.squareup.okhttp3:logging-interceptor:$OKHTTP_VERSION"
// ViewModel and LiveData
- implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
+ implementation "androidx.lifecycle:lifecycle-extensions:$LIFECYCLE_EXTENSION"
//New Material Design
- implementation 'com.google.android.material:material:1.6.1'
+ implementation "com.google.android.material:material:$MATERIAL_VERSION"
// Hilt dependency injection
- def hilt_ver = "2.43.2"
- implementation "com.google.dagger:hilt-android:$hilt_ver"
- kapt "com.google.dagger:hilt-compiler:$hilt_ver"
- androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_ver"
- kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_ver"
+ implementation "com.google.dagger:hilt-android:$HILT_VERSION"
+ kapt "com.google.dagger:hilt-compiler:$HILT_VERSION"
+ androidTestImplementation "com.google.dagger:hilt-android-testing:$HILT_VERSION"
+ kaptAndroidTest "com.google.dagger:hilt-android-compiler:$HILT_VERSION"
//mockito and livedata testing
- testImplementation 'org.mockito:mockito-inline:2.13.0'
- testImplementation 'androidx.arch.core:core-testing:2.1.0'
+ testImplementation "org.mockito:mockito-core:$MOKITO_CORE_VERSION"
+ testImplementation "androidx.arch.core:core-testing:$CORE_TEST_VERSION"
- implementation "androidx.preference:preference-ktx:1.2.0"
+ implementation "androidx.preference:preference-ktx:$PREFERENCES_VERSION"
//mock websever for testing retrofit responses
- testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0"
+ testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
- implementation "org.jetbrains.kotlin:kotlin-test:1.7.10"
+ implementation "org.jetbrains.kotlin:kotlin-test:1.9.20"
}
diff --git a/app/src/androidTest/java/com/appttude/h_mal/easycc/application/modules/MockRepository.kt b/app/src/androidTest/java/com/appttude/h_mal/easycc/application/modules/MockRepository.kt
index eaef8af..3a133db 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/easycc/application/modules/MockRepository.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/easycc/application/modules/MockRepository.kt
@@ -4,42 +4,20 @@ import com.appttude.h_mal.easycc.application.TestRunner.Companion.idlingResource
import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse
import com.appttude.h_mal.easycc.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.data.repository.Repository
+import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.models.CurrencyObject
import kotlinx.coroutines.delay
import javax.inject.Inject
class MockRepository @Inject constructor() : Repository {
- override suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): ResponseObject {
+ override suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): CurrencyModel {
idlingResources.increment()
delay(500)
- return ResponseObject(
- results = mapOf(
- Pair(
- "AUD_GBP", CurrencyObject(
- id = "AUD_GBP",
- fr = "AUD",
- to = "GBP",
- value = 0.546181
- )
- )
- )
- ).also {
- idlingResources.decrement()
- }
- }
-
- override suspend fun getBackupDataFromApi(
- fromCurrency: String,
- toCurrency: String
- ): CurrencyResponse {
- idlingResources.increment()
- delay(500)
- return CurrencyResponse(
- rates = mapOf(Pair("GBP", 0.54638)),
- amount = 1.0,
- base = "AUD",
- date = "2021-06-11"
+ return CurrencyModel(
+ from = "AUD",
+ to = "GBP",
+ rate = 0.546181
).also {
idlingResources.decrement()
}
diff --git a/app/src/androidTest/java/com/appttude/h_mal/easycc/helper/UiTestHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/easycc/helper/UiTestHelper.kt
new file mode 100644
index 0000000..9fc7d1f
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/easycc/helper/UiTestHelper.kt
@@ -0,0 +1,87 @@
+package com.appttude.h_mal.easycc.helper
+
+import android.view.View
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.NoMatchingViewException
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.ViewInteraction
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.espresso.util.TreeIterables
+import org.hamcrest.Matcher
+import java.lang.Thread.sleep
+
+/**
+ * Perform action of implicitly waiting for a certain view.
+ * This differs from EspressoExtensions.searchFor in that,
+ * upon failure to locate an element, it will fetch a new root view
+ * in which to traverse searching for our @param match
+ *
+ * @param viewMatcher ViewMatcher used to find our view
+ */
+fun waitForView(
+ viewMatcher: Matcher,
+ waitMillis: Int = 5000,
+ waitMillisPerTry: Long = 100
+): ViewInteraction {
+
+ // Derive the max tries
+ val maxTries = waitMillis / waitMillisPerTry.toInt()
+
+ var tries = 0
+
+ for (i in 0..maxTries)
+ try {
+ // Track the amount of times we've tried
+ tries++
+
+ // Search the root for the view
+ onView(isRoot()).perform(searchFor(viewMatcher))
+
+ // If we're here, we found our view. Now return it
+ return onView(viewMatcher)
+
+ } catch (e: Exception) {
+
+ if (tries == maxTries) {
+ throw e
+ }
+ sleep(waitMillisPerTry)
+ }
+
+ throw Exception("Error finding a view matching $viewMatcher")
+}
+
+/**
+ * Perform action of waiting for a certain view within a single root view
+ * @param viewMatcher Generic Matcher used to find our view
+ */
+fun searchFor(viewMatcher: Matcher): ViewAction {
+
+ return object : ViewAction {
+
+ override fun getConstraints(): Matcher = isRoot()
+ override fun getDescription(): String {
+ return "searching for view $this in the root view"
+ }
+
+ override fun perform(uiController: UiController, view: View) {
+ var tries = 0
+ val childViews: Iterable = TreeIterables.breadthFirstViewTraversal(view)
+
+ // Look for the match in the tree of child views
+ childViews.forEach {
+ tries++
+ if (viewMatcher.matches(it)) {
+ // found the view
+ return
+ }
+ }
+
+ throw NoMatchingViewException.Builder()
+ .withRootView(view)
+ .withViewMatcher(viewMatcher)
+ .build()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/easycc/robots/CurrencyRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/easycc/robots/CurrencyRobot.kt
index 3142b42..8a9f4f7 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/easycc/robots/CurrencyRobot.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/easycc/robots/CurrencyRobot.kt
@@ -6,7 +6,9 @@ import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
import com.appttude.h_mal.easycc.R
+import com.appttude.h_mal.easycc.helper.waitForView
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
@@ -18,48 +20,48 @@ fun currencyRobot(func: CurrencyRobot.() -> Unit) = CurrencyRobot()
class CurrencyRobot {
fun clickOnTopList() {
- Espresso.onView(ViewMatchers.withId(R.id.currencyOne)).perform(ViewActions.click())
+ waitForView(withId(R.id.currencyOne)).perform(ViewActions.click())
}
fun clickOnBottomList() {
- Espresso.onView(ViewMatchers.withId(R.id.currencyTwo)).perform(ViewActions.click())
+ waitForView(withId(R.id.currencyTwo)).perform(ViewActions.click())
}
fun searchInCurrencyList(search: String) {
- Espresso.onView(ViewMatchers.withId(R.id.search_text))
+ waitForView(withId(R.id.search_text))
.perform(ViewActions.replaceText(search), ViewActions.closeSoftKeyboard())
}
fun enterValueInTopEditText(text: String) {
- Espresso.onView(ViewMatchers.withId(R.id.topInsertValue))
+ waitForView(withId(R.id.topInsertValue))
.perform(ViewActions.replaceText(text), ViewActions.closeSoftKeyboard())
}
fun selectItemInCurrencyList() {
- Espresso.onData(Matchers.anything())
- .inAdapterView(
- Matchers.allOf(
- ViewMatchers.withId(R.id.list_view),
- childAtPosition(
- ViewMatchers.withClassName(Matchers.`is`("androidx.cardview.widget.CardView")),
- 0
- )
- )
+ val viewMatcher = Matchers.allOf(
+ withId(R.id.list_view),
+ childAtPosition(
+ ViewMatchers.withClassName(Matchers.`is`("androidx.cardview.widget.CardView")),
+ 0
)
+ )
+ waitForView(viewMatcher)
+ Espresso.onData(Matchers.anything())
+ .inAdapterView(viewMatcher)
.atPosition(0)
.perform(ViewActions.click())
}
fun assertTextInTop(text: String) {
Espresso.onView(
- ViewMatchers.withId(R.id.topInsertValue)
+ withId(R.id.topInsertValue)
).check(ViewAssertions.matches(ViewMatchers.withText(text)))
}
fun assertTextInBottom(text: String) {
- Espresso.onView(
- ViewMatchers.withId(R.id.bottomInsertValues)
+ waitForView(
+ withId(R.id.bottomInsertValues)
).check(ViewAssertions.matches(ViewMatchers.withText(text)))
}
diff --git a/build.gradle b/build.gradle
index 1457a90..d0e0ab1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,11 +1,16 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ ext {
+ agp_version = '8.1.0'
+ }
+}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id 'com.android.application' version '7.2.2' apply false
- id 'com.android.library' version '7.2.2' apply false
- id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
- id 'com.google.dagger.hilt.android' version '2.43.2' apply false
+ id 'com.android.application' version "$GRADLE_PLUGIN_VERSION" apply false
+ id 'com.android.library' version "$GRADLE_PLUGIN_VERSION" apply false
+ id 'org.jetbrains.kotlin.android' version "$KOTLIN_VERSION" apply false
+ id 'com.google.dagger.hilt.android' version "$HILT_VERSION" apply false
+ id 'com.autonomousapps.dependency-analysis' version "$GRADLE_ANALYZE_VERSION"
}
task clean(type: Delete) {
delete rootProject.buildDir
-}
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 4a5855f..e60a42a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,12 +6,67 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
-android.useAndroidX=true
-android.enableJetifier=true
\ No newline at end of file
+
+# Plugin versions
+ANDROID_CORE_VERSION = 1.9.0
+CUSTOM_VIEW = 1.1.0
+TEST_RULE = 1.4.0
+ANNOTATION_VERSION = 1.9.0
+FRAGMENT_VERSION = 1.8.0
+ACTIVITY_VERSION = 1.6.0
+MATERIAL_VERSION = 1.6.1
+APP_COMPAT = 1.6.0
+CONSTR_LAYOUT_VERSION = 2.1.4
+ANDROID_LIFECYCLE = 2.5.1
+RECYCLER_VIEW = 1.3.2
+SWIPE_REFRESH = 1.1.0
+PERMISSIONS_DISPATCHER = 4.9.2
+LIFECYCLE_EXTENSION = 2.2.0
+TOMTOM_MAP = 2.4807
+NAVIGATION_VERSION = 2.7.7
+PREFERENCES_VERSION = 1.2.0
+RETROFIT_VERSION = 2.9.0
+OKHTTP_VERSION = 4.9.0
+MOKITO_CORE_VERSION = 5.12.0
+CORE_TEST_VERSION = 2.2.0
+MOCKK_VERSION = 1.13.12
+TEST_JUNIT_VERSION = 1.2.0
+TEST_RUNNER_VERSION = 1.5.2
+ESPRESSO_VERSION = 3.6.0
+HAMCREST_VERSION = 2.2
+JUNIT_VERSION = 4.13.2
+KODEIN_VERSION = 6.2.1
+ROOM_VERSION = 2.6.1
+KOTLINX_COROUTINES = 1.6.2
+TEST_KTX_VERSION = 1.6.0
+ANDROIDX_TEST = 1.6.0
+TEST_MONITOR = 1.7.0
+GOOGLE_PLAY_SERVICE = 21.3.0
+GOOGLE_SERVICES = 4.3.15
+GSON = 2.10.1
+GUAVA = 33.2.1-android
+ANDROID_LIBRARY = 8.5.0
+ANDROID_APPLICATION = 8.5.0
+GRADLE_PLUGIN_VERSION = 8.1.0
+KOTLIN_VERSION = 1.9.20
+KOTLIN_GRADLE_PLUGIN = 1.6.21
+GRADLE_ANALYZE_VERSION = 2.0.0
+KOTLINX_COROUTINES_RX2 = 1.9.0
+HILT_VERSION = 2.48
+
+# Android configuration
+TARGET_SDK_VERSION = 33
+MIN_SDK_VERSION = 21
+
+# Gradle parameters
+org.gradle.jvmargs = -Xmx1536m
+
+# AndroidX
+android.useAndroidX = true
+android.enableJetifier = true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f63e218..9c3e330 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Thu Aug 04 22:17:29 BST 2022
+#Thu May 15 19:21:24 BST 2025
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
-zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755