Merge branch 'master' of github.com:TouchInstinct/RoboSwag into token_manager

This commit is contained in:
Kirill Nayduik 2022-04-18 13:27:04 +03:00
commit 39c5ac1ff6
21 changed files with 407 additions and 16 deletions

1
client-services/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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'
}
}
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.touchin.client_services">
</manifest>

View File

@ -0,0 +1,5 @@
package ru.touchin.client_services
enum class MobileService {
HUAWEI_SERVICE, GOOGLE_SERVICE
}

View File

@ -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
}

View File

@ -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

View File

@ -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 <T> Flowable<out T>.dispatchTo(liveData: MutableLiveData<ContentEvent<T>>): 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 <T> Observable<out T>.dispatchTo(liveData: MutableLiveData<ContentEvent<T>>): 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 <T> Single<out T>.dispatchTo(liveData: MutableLiveData<ContentEvent<T>>): Disposable {
return untilDestroy(
{ data -> liveData.value = ContentEvent.Success(data) },
{ throwable -> liveData.value = ContentEvent.Error(throwable, liveData.value?.data) })
}
override fun <T> Maybe<out T>.dispatchTo(liveData: MutableLiveData<ContentEvent<T>>): 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<Event>): Disposable {
return untilDestroy(
{ liveData.value = Event.Complete },
{ throwable -> liveData.value = Event.Error(throwable) })
}
}

View File

@ -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"

View File

@ -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<NavArgs : Parcelable, Action : ViewAction, State : V
private val mediatorStore = MediatorStore(
listOfNotNull(
// Min api 24
// https://github.com/TouchInstinct/RoboSwag/issues/180
// LoggingMediator(this::class.simpleName!!).takeIf { BuildConfig.DEBUG }
LoggingMediator(this::class.simpleName).takeIf { BuildConfig.DEBUG }
)
)

View File

@ -1,13 +1,12 @@
package ru.touchin.roboswag.mvi_arch.mediator
import com.tylerthrailkill.helpers.prettyprint.pp
import ru.touchin.roboswag.core.log.Lc
import ru.touchin.roboswag.mvi_arch.marker.SideEffect
import ru.touchin.roboswag.mvi_arch.marker.StateChange
import ru.touchin.roboswag.mvi_arch.marker.ViewAction
import ru.touchin.roboswag.mvi_arch.marker.ViewState
class LoggingMediator(private val objectName: String) : Mediator {
class LoggingMediator(private val objectName: String?) : Mediator {
override fun onEffect(effect: SideEffect) {
logObject(
prefix = "New Effect:\n",
@ -40,10 +39,6 @@ class LoggingMediator(private val objectName: String) : Mediator {
prefix: String,
obj: T
) {
val builder = StringBuilder()
pp(obj = obj, writeTo = builder)
val prettyOutput = builder.toString()
Lc.d("$objectName: $prefix$prettyOutput\n")
Lc.d("$objectName: $prefix$obj\n")
}
}

1
recaptcha/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

33
recaptcha/README.md Normal file
View File

@ -0,0 +1,33 @@
recaptcha
=====
### Общее описание
Модуль содержит класс `CaptchaManager` - служит для проверки используемого сервиса (Huawei или Google) и показа диалога с каптчёй
В конструктуре `CaptchaManager` принимает два callback:
`onNewTokenReceived` - успешная проверка, возвращает токен
`processThrowable` - ошибка, возвращает `Throwable`
### Требования
Для использования модуля нужно добавить json файл с сервисами в корневую папку проекта:
1. Для Google - google-services.json
2. Для Huawei - agconnect-services.json
### Пример
Во `Fragment`
```kotlin
val manager = CaptchaManager(onNewTokenReceived = { token ->
viewModel.sendRequest(token)
}, processThrowable = { error ->
showError(error)
})
manager.showRecaptchaAlert(
activity = activity,
captchaKey = BuildConfig.CAPTCHA_TOKEN
)
```

44
recaptcha/build.gradle Normal file
View File

@ -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'
}
}
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.touchin.recaptcha">
</manifest>

View File

@ -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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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