Merge branch 'master' of github.com:TouchInstinct/RoboSwag into token_manager
This commit is contained in:
commit
39c5ac1ff6
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package ru.touchin.client_services
|
||||
|
||||
enum class MobileService {
|
||||
HUAWEI_SERVICE, GOOGLE_SERVICE
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) })
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
|
|
@ -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
|
||||
)
|
||||
```
|
||||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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")
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue