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) + } +}