diff --git a/client-services/.gitignore b/client-services/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/client-services/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/client-services/build.gradle b/client-services/build.gradle
new file mode 100644
index 0000000..6799856
--- /dev/null
+++ b/client-services/build.gradle
@@ -0,0 +1,35 @@
+apply from: "../android-configs/lib-config.gradle"
+apply plugin: 'com.huawei.agconnect'
+
+dependencies {
+ implementation "androidx.core:core"
+ implementation "androidx.annotation:annotation"
+ implementation "com.google.android.gms:play-services-base"
+ implementation "com.huawei.hms:base"
+
+ constraints {
+ implementation("androidx.core:core") {
+ version {
+ require '1.0.0'
+ }
+ }
+
+ implementation("androidx.annotation:annotation") {
+ version {
+ require '1.1.0'
+ }
+ }
+
+ implementation("com.google.android.gms:play-services-base") {
+ version {
+ require '18.0.1'
+ }
+ }
+
+ implementation("com.huawei.hms:base") {
+ version {
+ require '6.3.0.303'
+ }
+ }
+ }
+}
diff --git a/client-services/src/main/AndroidManifest.xml b/client-services/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4390a88
--- /dev/null
+++ b/client-services/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/client-services/src/main/java/ru/touchin/client_services/MobileService.kt b/client-services/src/main/java/ru/touchin/client_services/MobileService.kt
new file mode 100644
index 0000000..6514e3a
--- /dev/null
+++ b/client-services/src/main/java/ru/touchin/client_services/MobileService.kt
@@ -0,0 +1,5 @@
+package ru.touchin.client_services
+
+enum class MobileService {
+ HUAWEI_SERVICE, GOOGLE_SERVICE
+}
diff --git a/client-services/src/main/java/ru/touchin/client_services/ServicesUtils.kt b/client-services/src/main/java/ru/touchin/client_services/ServicesUtils.kt
new file mode 100644
index 0000000..1cc5b2a
--- /dev/null
+++ b/client-services/src/main/java/ru/touchin/client_services/ServicesUtils.kt
@@ -0,0 +1,28 @@
+package ru.touchin.client_services
+
+import android.content.Context
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
+import com.huawei.hms.api.HuaweiApiAvailability
+
+/**
+ * A class with utils for interacting with Google, Huawei services
+ */
+
+class ServicesUtils {
+
+ fun getCurrentService(context: Context): MobileService = when {
+ checkHuaweiServices(context) -> MobileService.HUAWEI_SERVICE
+ checkGooglePlayServices(context) -> MobileService.GOOGLE_SERVICE
+ else -> MobileService.GOOGLE_SERVICE
+ }
+
+ private fun checkHuaweiServices(context: Context): Boolean =
+ HuaweiApiAvailability.getInstance()
+ .isHuaweiMobileNoticeAvailable(context) == ConnectionResult.SUCCESS
+
+ private fun checkGooglePlayServices(context: Context): Boolean =
+ GoogleApiAvailability.getInstance()
+ .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
+
+}
diff --git a/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/RxViewModel.kt b/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/RxViewModel.kt
index 74b8f67..5919bdf 100644
--- a/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/RxViewModel.kt
+++ b/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/RxViewModel.kt
@@ -8,7 +8,7 @@ import androidx.annotation.CallSuper
*/
open class RxViewModel(
private val destroyable: BaseDestroyable = BaseDestroyable(),
- private val liveDataDispatcher: BaseLiveDataDispatcher = BaseLiveDataDispatcher(destroyable)
+ private val liveDataDispatcher: LiveDataDispatcher = BaseLiveDataDispatcher(destroyable)
) : ViewModel(), Destroyable by destroyable, LiveDataDispatcher by liveDataDispatcher {
@CallSuper
diff --git a/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/TestableLiveDataDispatcher.kt b/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/TestableLiveDataDispatcher.kt
new file mode 100644
index 0000000..c7ea674
--- /dev/null
+++ b/lifecycle-rx/src/main/java/ru/touchin/lifecycle/viewmodel/TestableLiveDataDispatcher.kt
@@ -0,0 +1,49 @@
+package ru.touchin.lifecycle.viewmodel
+
+import androidx.lifecycle.MutableLiveData
+import io.reactivex.Completable
+import io.reactivex.Flowable
+import io.reactivex.Maybe
+import io.reactivex.Observable
+import io.reactivex.Single
+import io.reactivex.disposables.Disposable
+import ru.touchin.lifecycle.event.ContentEvent
+import ru.touchin.lifecycle.event.Event
+
+class TestableLiveDataDispatcher(
+ private val destroyable: BaseDestroyable = BaseDestroyable()
+) : LiveDataDispatcher, Destroyable by destroyable {
+
+ override fun Flowable.dispatchTo(liveData: MutableLiveData>): Disposable {
+ return untilDestroy(
+ { data -> liveData.value = ContentEvent.Success(data) },
+ { throwable -> liveData.value = ContentEvent.Error(throwable, liveData.value?.data) },
+ { liveData.value = ContentEvent.Complete(liveData.value?.data) })
+ }
+
+ override fun Observable.dispatchTo(liveData: MutableLiveData>): Disposable {
+ return untilDestroy(
+ { data -> liveData.value = ContentEvent.Success(data) },
+ { throwable -> liveData.value = ContentEvent.Error(throwable, liveData.value?.data) },
+ { liveData.value = ContentEvent.Complete(liveData.value?.data) })
+ }
+
+ override fun Single.dispatchTo(liveData: MutableLiveData>): Disposable {
+ return untilDestroy(
+ { data -> liveData.value = ContentEvent.Success(data) },
+ { throwable -> liveData.value = ContentEvent.Error(throwable, liveData.value?.data) })
+ }
+
+ override fun Maybe.dispatchTo(liveData: MutableLiveData>): Disposable {
+ return untilDestroy(
+ { data -> liveData.value = ContentEvent.Success(data) },
+ { throwable -> liveData.value = ContentEvent.Error(throwable, liveData.value?.data) },
+ { liveData.value = ContentEvent.Complete(liveData.value?.data) })
+ }
+
+ override fun Completable.dispatchTo(liveData: MutableLiveData): Disposable {
+ return untilDestroy(
+ { liveData.value = Event.Complete },
+ { throwable -> liveData.value = Event.Error(throwable) })
+ }
+}
diff --git a/mvi-arch/build.gradle b/mvi-arch/build.gradle
index 1532294..77b2a22 100644
--- a/mvi-arch/build.gradle
+++ b/mvi-arch/build.gradle
@@ -28,8 +28,6 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android")
- implementation("com.tylerthrailkill.helpers:pretty-print:2.0.2")
-
def fragmentVersion = "1.2.1"
def lifecycleVersion = "2.2.0"
def coroutinesVersion = "1.3.7"
diff --git a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt
index 8b4b170..ede36e5 100644
--- a/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt
+++ b/mvi-arch/src/main/java/ru/touchin/roboswag/mvi_arch/core/MviViewModel.kt
@@ -9,8 +9,10 @@ import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
+import ru.touchin.mvi_arch.BuildConfig
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
import ru.touchin.roboswag.mvi_arch.marker.ViewState
+import ru.touchin.roboswag.mvi_arch.mediator.LoggingMediator
import ru.touchin.roboswag.mvi_arch.mediator.MediatorStore
/**
@@ -40,9 +42,7 @@ abstract class MviViewModel
+ viewModel.sendRequest(token)
+ }, processThrowable = { error ->
+ showError(error)
+ })
+
+manager.showRecaptchaAlert(
+ activity = activity,
+ captchaKey = BuildConfig.CAPTCHA_TOKEN
+)
+```
diff --git a/recaptcha/build.gradle b/recaptcha/build.gradle
new file mode 100644
index 0000000..51b4418
--- /dev/null
+++ b/recaptcha/build.gradle
@@ -0,0 +1,44 @@
+apply from: "../android-configs/lib-config.gradle"
+apply plugin: 'com.huawei.agconnect'
+
+dependencies {
+ implementation project(':client-services')
+
+ implementation "androidx.core:core"
+ implementation "androidx.annotation:annotation"
+ implementation "com.google.android.gms:play-services-safetynet"
+ implementation "com.google.android.gms:play-services-base"
+ implementation "com.huawei.hms:safetydetect"
+
+ constraints {
+ implementation("androidx.core:core") {
+ version {
+ require '1.0.0'
+ }
+ }
+
+ implementation("androidx.annotation:annotation") {
+ version {
+ require '1.1.0'
+ }
+ }
+
+ implementation("com.google.android.gms:play-services-safetynet") {
+ version {
+ require '18.0.1'
+ }
+ }
+
+ implementation("com.google.android.gms:play-services-base") {
+ version {
+ require '18.0.1'
+ }
+ }
+
+ implementation("com.huawei.hms:safetydetect") {
+ version {
+ require '4.0.3.300'
+ }
+ }
+ }
+}
diff --git a/recaptcha/src/main/AndroidManifest.xml b/recaptcha/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..17ef42a
--- /dev/null
+++ b/recaptcha/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/recaptcha/src/main/java/ru/touchin/recaptcha/CaptchaClient.kt b/recaptcha/src/main/java/ru/touchin/recaptcha/CaptchaClient.kt
new file mode 100644
index 0000000..207a6ef
--- /dev/null
+++ b/recaptcha/src/main/java/ru/touchin/recaptcha/CaptchaClient.kt
@@ -0,0 +1,24 @@
+package ru.touchin.recaptcha
+
+import android.app.Activity
+
+abstract class CaptchaClient(
+ private val onNewTokenReceived: (String) -> Unit = {},
+ private val processThrowable: (Throwable) -> Unit = {}
+) {
+
+ abstract fun showCaptcha(activity: Activity, captchaKey: String)
+
+ protected fun onServiceTokenResponse(newToken: String?) {
+ if (!newToken.isNullOrBlank()) {
+ onNewTokenReceived.invoke(newToken)
+ } else {
+ processThrowable.invoke(EmptyCaptchaTokenException())
+ }
+ }
+
+}
+
+class EmptyCaptchaKeyException : Throwable("Captcha key is empty")
+
+class EmptyCaptchaTokenException : Throwable("Captcha token is empty")
diff --git a/recaptcha/src/main/java/ru/touchin/recaptcha/CaptchaManager.kt b/recaptcha/src/main/java/ru/touchin/recaptcha/CaptchaManager.kt
new file mode 100644
index 0000000..2e5145b
--- /dev/null
+++ b/recaptcha/src/main/java/ru/touchin/recaptcha/CaptchaManager.kt
@@ -0,0 +1,34 @@
+package ru.touchin.recaptcha
+
+import android.app.Activity
+import ru.touchin.client_services.MobileService
+import ru.touchin.client_services.ServicesUtils
+
+/**
+ * A class for displaying a dialog with a captcha
+ * with a check on the current service of the application
+ *
+ * @param onNewTokenReceived - callback for a successful captcha check, return token
+ * @param processThrowable - callback for a captcha check error, return throwable
+ */
+
+class CaptchaManager(
+ private val onNewTokenReceived: (String) -> Unit,
+ private val processThrowable: (Throwable) -> Unit
+) {
+
+ private val clientsMap = mapOf(
+ MobileService.GOOGLE_SERVICE to GoogleCaptchaClient(onNewTokenReceived, processThrowable),
+ MobileService.HUAWEI_SERVICE to HuaweiCaptchaClient(onNewTokenReceived, processThrowable)
+ )
+
+ fun showRecaptchaAlert(activity: Activity, captchaKey: String) {
+ if (captchaKey.isBlank()) {
+ processThrowable.invoke(EmptyCaptchaKeyException())
+ } else {
+ val service = ServicesUtils().getCurrentService(activity)
+ clientsMap[service]?.showCaptcha(activity, captchaKey)
+ }
+ }
+
+}
diff --git a/recaptcha/src/main/java/ru/touchin/recaptcha/GoogleCaptchaClient.kt b/recaptcha/src/main/java/ru/touchin/recaptcha/GoogleCaptchaClient.kt
new file mode 100644
index 0000000..b25bd34
--- /dev/null
+++ b/recaptcha/src/main/java/ru/touchin/recaptcha/GoogleCaptchaClient.kt
@@ -0,0 +1,20 @@
+package ru.touchin.recaptcha
+
+import android.app.Activity
+import com.google.android.gms.safetynet.SafetyNet
+
+class GoogleCaptchaClient(
+ onNewTokenReceived: (String) -> Unit,
+ private val processThrowable: (Throwable) -> Unit
+) : CaptchaClient(onNewTokenReceived, processThrowable) {
+
+ override fun showCaptcha(activity: Activity, captchaKey: String) {
+ SafetyNet.getClient(activity)
+ .verifyWithRecaptcha(captchaKey)
+ .addOnSuccessListener(activity) { response ->
+ onServiceTokenResponse(response?.tokenResult)
+ }
+ .addOnFailureListener(activity, processThrowable)
+ }
+
+}
diff --git a/recaptcha/src/main/java/ru/touchin/recaptcha/HuaweiCaptchaClient.kt b/recaptcha/src/main/java/ru/touchin/recaptcha/HuaweiCaptchaClient.kt
new file mode 100644
index 0000000..2dd8c20
--- /dev/null
+++ b/recaptcha/src/main/java/ru/touchin/recaptcha/HuaweiCaptchaClient.kt
@@ -0,0 +1,25 @@
+package ru.touchin.recaptcha
+
+import android.app.Activity
+import com.huawei.hms.support.api.safetydetect.SafetyDetect
+
+class HuaweiCaptchaClient(
+ onNewTokenReceived: (String) -> Unit,
+ private val processThrowable: (Throwable) -> Unit
+) : CaptchaClient(onNewTokenReceived, processThrowable) {
+
+ override fun showCaptcha(activity: Activity, captchaKey: String) {
+ val huaweiSafetyDetectClient = SafetyDetect.getClient(activity)
+
+ huaweiSafetyDetectClient.initUserDetect()
+ .addOnSuccessListener {
+ huaweiSafetyDetectClient.userDetection(captchaKey)
+ .addOnSuccessListener { response ->
+ onServiceTokenResponse(response?.responseToken)
+ }
+ .addOnFailureListener(activity, processThrowable)
+ }
+ .addOnFailureListener(activity, processThrowable)
+ }
+
+}
diff --git a/utils/build.gradle b/utils/build.gradle
index fe6fd6a..513c848 100644
--- a/utils/build.gradle
+++ b/utils/build.gradle
@@ -1,27 +1,54 @@
apply from: "../android-configs/lib-config.gradle"
dependencies {
+ def coreVersion = '1.0.0'
+ def annotationVersion = '1.1.0'
+ def materialVersion = '1.2.0-rc01'
+ def jodaVersion = '2.10.2'
+ def junitVersion = '4.13.2'
+
implementation project(':kotlin-extensions')
implementation "androidx.core:core"
implementation "androidx.annotation:annotation"
implementation "com.google.android.material:material"
+ implementation "net.danlew:android.joda"
+ implementation "junit:junit"
+ testImplementation "joda-time:joda-time"
constraints {
implementation("androidx.core:core") {
version {
- require '1.0.0'
+ require(coreVersion)
}
}
implementation("androidx.annotation:annotation") {
version {
- require '1.1.0'
+ require(annotationVersion)
}
}
implementation("com.google.android.material:material") {
version {
- require '1.2.0-rc01'
+ require(materialVersion)
+ }
+ }
+
+ implementation("net.danlew:android.joda") {
+ version {
+ require(jodaVersion)
+ }
+ }
+
+ testImplementation("joda-time:joda-time") {
+ version {
+ require(jodaVersion)
+ }
+ }
+
+ implementation("junit:junit") {
+ version {
+ require(junitVersion)
}
}
}
diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/DateFormatUtils.kt b/utils/src/main/java/ru/touchin/roboswag/core/utils/DateFormatUtils.kt
new file mode 100644
index 0000000..9444416
--- /dev/null
+++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/DateFormatUtils.kt
@@ -0,0 +1,32 @@
+package ru.touchin.roboswag.core.utils
+
+import org.joda.time.DateTime
+import org.joda.time.format.DateTimeFormat
+
+/**
+ * Util object for handling some cases with DateTime e.g. parsing string to DateTime object
+ */
+object DateFormatUtils {
+
+ enum class Format(val formatValue: String) {
+ DATE_TIME_FORMAT("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"),
+ DATE_FORMAT("yyyy-MM-dd"),
+ TIME_FORMAT("HH:mm:ssZ")
+ }
+
+ /**
+ * @return the result of parsed string value
+ * @param value is string value of date time in right format
+ * @param format is date time format for parsing string value.
+ * Default value is [Format.DATE_TIME_FORMAT]
+ * @param defaultValue is value returned in case of exception
+ */
+ fun fromString(
+ value: String,
+ format: Format = Format.DATE_TIME_FORMAT,
+ defaultValue: DateTime? = null
+ ): DateTime? = runCatching { value.parse(format.formatValue) }.getOrDefault(defaultValue)
+
+ private fun String.parse(format: String) = DateTimeFormat.forPattern(format).parseDateTime(this)
+
+}
diff --git a/utils/src/test/java/DateFormatUtilsTest.kt b/utils/src/test/java/DateFormatUtilsTest.kt
new file mode 100644
index 0000000..8036ede
--- /dev/null
+++ b/utils/src/test/java/DateFormatUtilsTest.kt
@@ -0,0 +1,28 @@
+import org.joda.time.DateTime
+import org.junit.Assert
+import org.junit.Test
+import ru.touchin.roboswag.core.utils.DateFormatUtils
+
+class DateFormatUtilsTest {
+
+ @Test
+ fun `Assert Date format parsing`() {
+ val dateTime = DateFormatUtils.fromString(
+ value = "2015-04-29",
+ format = DateFormatUtils.Format.DATE_FORMAT
+ )
+ Assert.assertEquals(DateTime(2015, 4, 29, 0, 0, 0), dateTime)
+ }
+
+ @Test
+ fun `Assert Date format parsing with default value`() {
+ val currentDateTime = DateTime.now()
+
+ val dateTime = DateFormatUtils.fromString(
+ value = "2015-04-29",
+ format = DateFormatUtils.Format.DATE_TIME_FORMAT,
+ defaultValue = currentDateTime
+ )
+ Assert.assertEquals(currentDateTime, dateTime)
+ }
+}