Updated on 2026-08-14

This commit is contained in:
Tangem 2020-03-18 16:35:59 +03:00
parent adb12edd24
commit 58462ba360
11 changed files with 186 additions and 155 deletions

View file

@ -0,0 +1,44 @@
package com.tangem.tangemtest._arch
import android.util.Log
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
/**
[REDACTED_AUTHOR]
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending: AtomicBoolean = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(it)
}
})
}
@MainThread
override fun setValue(@Nullable t: T?) {
mPending.set(true)
super.setValue(t)
}
@MainThread
fun call() {
setValue(null)
}
companion object {
private const val TAG = "SingleLiveEvent"
}
}

View file

@ -18,7 +18,7 @@ abstract class BaseMvpFragment : Fragment(), MvpView, OnBackPressHandler {
protected lateinit var mainView: View
protected var presenter: MvpPresenter<in MvpView>? = null
protected abstract fun getMvpPresenter(): MvpPresenter<in MvpView>?
protected abstract fun getMvpPresenter(): MvpPresenter<in MvpView>
override fun onAttach(context: Context) {
super.onAttach(context)

View file

@ -11,7 +11,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tangemtest.R
import com.tangem.tangemtest.commons.Action
import com.tangem.tangemtest.commons.ActionType
import com.tangem.tangemtest.commons.NavigateOptions
import com.tangem.tangemtest.commons.getDefaultNavigationOptions
import kotlinx.android.synthetic.main.fg_entry_point.*
@ -55,16 +55,16 @@ class EntryPointFragment : Fragment() {
private fun getNavigateOptions(): MutableList<NavigateOptions> {
return mutableListOf(
NavigateOptions(Action.Scan, R.id.action_nav_entry_point_to_nav_scan),
NavigateOptions(Action.Sign, R.id.action_nav_entry_point_to_nav_sign),
NavigateOptions(Action.CreateWallet, R.id.action_nav_entry_point_to_nav_wallet_create),
NavigateOptions(Action.PurgeWallet, R.id.action_nav_entry_point_to_nav_wallet_purge),
NavigateOptions(Action.ReadIssuerData, R.id.action_nav_entry_point_to_nav_issuer_read_data),
NavigateOptions(Action.WriteIssuerData, R.id.action_nav_entry_point_to_nav_issuer_write_data),
NavigateOptions(Action.ReadIssuerExData, R.id.action_nav_entry_point_to_nav_issuer_read_ex_data),
NavigateOptions(Action.WriteIssuerExData, R.id.action_nav_entry_point_to_nav_issuer_write_ex_data),
NavigateOptions(Action.ReadUserData, R.id.action_nav_entry_point_to_nav_user_read_data),
NavigateOptions(Action.WriteUserData, R.id.action_nav_entry_point_to_nav_user_write_data)
NavigateOptions(ActionType.Scan, R.id.action_nav_entry_point_to_nav_scan),
NavigateOptions(ActionType.Sign, R.id.action_nav_entry_point_to_nav_sign),
NavigateOptions(ActionType.CreateWallet, R.id.action_nav_entry_point_to_nav_wallet_create),
NavigateOptions(ActionType.PurgeWallet, R.id.action_nav_entry_point_to_nav_wallet_purge),
NavigateOptions(ActionType.ReadIssuerData, R.id.action_nav_entry_point_to_nav_issuer_read_data),
NavigateOptions(ActionType.WriteIssuerData, R.id.action_nav_entry_point_to_nav_issuer_write_data),
NavigateOptions(ActionType.ReadIssuerExData, R.id.action_nav_entry_point_to_nav_issuer_read_ex_data),
NavigateOptions(ActionType.WriteIssuerExData, R.id.action_nav_entry_point_to_nav_issuer_write_ex_data),
NavigateOptions(ActionType.ReadUserData, R.id.action_nav_entry_point_to_nav_user_read_data),
NavigateOptions(ActionType.WriteUserData, R.id.action_nav_entry_point_to_nav_user_write_data)
)
}
}

View file

@ -55,10 +55,10 @@ class SignAction : BaseCardAction() {
}
override fun getActionByTag(tag: TlvTag, attrs: AttrForAction): ((ActionCallback) -> Unit)? {
if (tag != TlvTag.CardId) return null
val pCardId = ParamsManager.findParameter(tag, attrs.paramsList) ?: return null
return if (pCardId.data == null) { callback -> ScanAction().executeMainAction(attrs, callback) } else null
return when (tag) {
TlvTag.CardId -> { callback -> ScanAction().executeMainAction(attrs, callback) }
else -> null
}
}
private fun findHashes(): Array<ByteArray> {

View file

@ -1,18 +1,18 @@
package com.tangem.tangemtest.card_use_cases.models.params.manager
import com.tangem.tangemtest.commons.Action
import com.tangem.tangemtest.commons.ActionType
import ru.dev.gbixahue.eu4d.lib.kotlin.common.BaseTypedHolder
/**
[REDACTED_AUTHOR]
*/
class ParamsManagerFactory : BaseTypedHolder<Action, ParamsManager>() {
class ParamsManagerFactory : BaseTypedHolder<ActionType, ParamsManager>() {
companion object {
fun createFactory(): ParamsManagerFactory {
return ParamsManagerFactory().apply {
register(Action.Scan, ScanParamsManager())
register(Action.Sign, SignParamsManager())
register(ActionType.Scan, ScanParamsManager())
register(ActionType.Sign, SignParamsManager())
}
}
}

View file

@ -12,10 +12,15 @@ import androidx.lifecycle.Observer
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.tangem.CardManager
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tangemtest.R
import com.tangem.tangemtest.card_use_cases.models.params.manager.ParamsManager
import com.tangem.tangemtest.card_use_cases.models.params.manager.ParamsManagerFactory
import com.tangem.tangemtest.card_use_cases.ui.widgets.ParameterWidget
import com.tangem.tangemtest.card_use_cases.view_models.ActionViewModelFactory
import com.tangem.tangemtest.card_use_cases.view_models.ParamsViewModel
import com.tangem.tangemtest.commons.Action
import com.tangem.tangemtest.commons.ActionType
import ru.dev.gbixahue.eu4d.lib.android.global.log.Log
import ru.dev.gbixahue.eu4d.lib.kotlin.common.LayoutHolder
@ -24,33 +29,21 @@ import ru.dev.gbixahue.eu4d.lib.kotlin.common.LayoutHolder
*/
abstract class BaseCardActionFragment : Fragment(), LayoutHolder {
protected lateinit var mainView: View
protected val mainView: View by lazy { view!! }
protected val incomingParamsContainer: ViewGroup by lazy {
mainView.findViewById<LinearLayout>(R.id.ll_incoming_params_container)
}
protected val viewModel: ParamsViewModel by viewModels { ActionViewModelFactory(paramsManager) }
protected val viewModel: ParamsViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.d(this, "onCreateView")
mainView = inflater.inflate(getLayoutId(), container, false)
return mainView
}
private val paramsManager: ParamsManager by lazy { ParamsManagerFactory.createFactory().get(getAction())!! }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.d(this, "onViewCreated")
val activity = activity ?: return
try {
viewModel.init(activity, getAction())
initFab()
val widgetList = createWidgets()
subscribeToViewModelChanges(widgetList)
} catch (e: Exception) {
Log.e(this, e)
showSnackbarMessage(e.toString())
}
viewModel.setCardManager(CardManager.init(requireActivity()))
initFab()
createWidgets { subscribeToViewModelChanges(it) }
}
private fun initFab() {
@ -61,28 +54,30 @@ abstract class BaseCardActionFragment : Fragment(), LayoutHolder {
fab.setOnClickListener { viewModel.invokeMainAction() }
}
private fun createWidgets(): MutableList<ParameterWidget> {
private fun createWidgets(callback: (List<ParameterWidget>) -> Unit) {
Log.d(this, "createWidgets")
val widgetList = mutableListOf<ParameterWidget>()
viewModel.getInitialParams()?.forEach { param ->
val widget = ParameterWidget(inflateParamView(incomingParamsContainer), param)
widget.onParameterValueChanged = { viewModel.userChangedParameter(it) }
widget.onParameterAction = viewModel.getParameterAction(param.tlvTag)
widgetList.add(widget)
}
return widgetList
viewModel.ldParams.observe(viewLifecycleOwner, Observer { paramsList ->
val widgetList = mutableListOf<ParameterWidget>()
paramsList.forEach { param ->
val widget = ParameterWidget(inflateParamView(incomingParamsContainer), param)
widget.onValueChanged = { tag, value -> viewModel.userChangedParameter(tag, value) }
widget.onActionBtnClickListener = viewModel.getParameterAction(param.tlvTag)
widgetList.add(widget)
}
callback(widgetList)
})
}
private fun subscribeToViewModelChanges(widgetList: MutableList<ParameterWidget>) {
private fun subscribeToViewModelChanges(widgetList: List<ParameterWidget>) {
Log.d(this, "subscribeToViewModelChanges")
val tvResponse by lazy { mainView.findViewById<TextView>(R.id.tv_action_response_json) }
viewModel.ldResponse.observe(viewLifecycleOwner, Observer {
Log.d(this, "action response: ${if (it.length > 50) it.substring(0..50) else it}")
mainView.findViewById<TextView>(R.id.tv_action_response_json).text = it
tvResponse.text = it
})
viewModel.ldError.observe(viewLifecycleOwner, Observer { showSnackbarMessage(it) })
viewModel.ldIncomingParameter.observe(viewLifecycleOwner, Observer { param ->
viewModel.seError.observe(viewLifecycleOwner, Observer { showSnackbarMessage(it) })
viewModel.seIncomingParameter.observe(viewLifecycleOwner, Observer { param ->
Log.d(this, "parameter changed from VM - name: ${param.tlvTag.name}, value:${param.data}")
widgetList.firstOrNull { it.tlvTag == param.tlvTag }?.changeParamValue(param.data)
})
@ -100,5 +95,5 @@ abstract class BaseCardActionFragment : Fragment(), LayoutHolder {
return view
}
abstract fun getAction(): Action
abstract fun getAction(): ActionType
}

View file

@ -1,7 +1,7 @@
package com.tangem.tangemtest.card_use_cases.ui
import com.tangem.tangemtest.R
import com.tangem.tangemtest.commons.Action
import com.tangem.tangemtest.commons.ActionType
/**
[REDACTED_AUTHOR]
@ -10,5 +10,5 @@ class ScanActionFragment : BaseCardActionFragment() {
override fun getLayoutId(): Int = R.layout.fg_action_card_scan
override fun getAction(): Action = Action.Scan
override fun getAction(): ActionType = ActionType.Scan
}

View file

@ -1,7 +1,7 @@
package com.tangem.tangemtest.card_use_cases.ui
import com.tangem.tangemtest.R
import com.tangem.tangemtest.commons.Action
import com.tangem.tangemtest.commons.ActionType
/**
[REDACTED_AUTHOR]
@ -10,5 +10,5 @@ class SignActionFragment : BaseCardActionFragment() {
override fun getLayoutId(): Int = R.layout.fg_action_card_sign
override fun getAction(): Action = Action.Sign
override fun getAction(): ActionType = ActionType.Sign
}

View file

@ -12,60 +12,61 @@ import androidx.transition.TransitionManager
import com.tangem.common.tlv.TlvTag
import com.tangem.tangemtest.R
import com.tangem.tangemtest.card_use_cases.models.params.manager.IncomingParameter
import com.tangem.tangemtest.commons.Action
import com.tangem.tangemtest.commons.ActionType
import ru.dev.gbixahue.eu4d.lib.android.global.log.Log
import ru.dev.gbixahue.eu4d.lib.kotlin.stringOf
/**
[REDACTED_AUTHOR]
*/
class ParameterWidget(parent: View, private val model: IncomingParameter) {
class ParameterWidget(parent: View, model: IncomingParameter) {
val tlvTag: TlvTag = model.tlvTag
var onValueChanged: ((TlvTag, Any?) -> Unit)? = null
var onActionBtnClickListener: (() -> Unit)? = null
set(value) {
field = value
toggleActionBtnVisibility()
}
private val tvName: TextView = parent.findViewById(R.id.tv_param_name)
private val etValue: EditText = parent.findViewById(R.id.tv_param_value)
private val btnAction: Button = parent.findViewById(R.id.btn_action)
private val valueWatcher: TextWatcher by lazy { getWatcher() }
val tlvTag: TlvTag = model.tlvTag
private var parameterActionStorage: (() -> Unit)? = null
var onParameterAction: (() -> Unit)? = null
set(value) {
TransitionManager.beginDelayedTransition(etValue.parent as ViewGroup, AutoTransition())
if (value != null) {
parameterActionStorage = value
setActionButtonTitle()
btnAction.visibility = View.VISIBLE
} else {
btnAction.visibility = View.GONE
}
field = value
}
var onParameterValueChanged: ((IncomingParameter) -> Unit)? = null
private var actionBtnVisibilityState: Int = btnAction.visibility
private var value: Any? = model.data
init {
tvName.text = model.tlvTag.name
etValue.setText(stringOf(model.data))
etValue.addTextChangedListener(valueWatcher)
btnAction.setOnClickListener { onParameterAction?.invoke() }
btnAction.setOnClickListener { onActionBtnClickListener?.invoke() }
btnAction.text = btnAction.context.getString(getActionResName())
toggleActionBtnVisibility()
}
private fun setActionButtonTitle() {
val action = when (model.tlvTag) {
TlvTag.CardId -> Action.Scan
else -> Action.Unknown
fun changeParamValue(data: Any?, silent: Boolean = true) {
Log.d(this, "changeParamValue: tag: $tlvTag, value: $data")
value = data
toggleActionBtnVisibility()
if (silent) {
etValue.removeTextChangedListener(valueWatcher)
etValue.setText(stringOf(value))
etValue.addTextChangedListener(valueWatcher)
} else {
etValue.setText(stringOf(value))
}
btnAction.setText(action.resName)
}
private fun getWatcher(): TextWatcher {
return object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val newValue = stringOf(s)
onParameterAction = if (newValue.isEmpty() && onParameterAction == null) parameterActionStorage
else null
model.data = newValue
onParameterValueChanged?.invoke(model)
override fun afterTextChanged(editable: Editable?) {
Log.d(this, "afterTextChanged $editable")
value = if (editable.isNullOrEmpty()) null else editable.toString()
toggleActionBtnVisibility()
onValueChanged?.invoke(tlvTag, value)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
@ -74,15 +75,27 @@ class ParameterWidget(parent: View, private val model: IncomingParameter) {
}
}
fun changeParamValue(data: Any?, silent: Boolean = true) {
Log.d(this, "changeParamValue: tag: $tlvTag, value: $data")
if (silent) {
etValue.removeTextChangedListener(valueWatcher)
etValue.setText(stringOf(data))
etValue.addTextChangedListener(valueWatcher)
} else {
etValue.setText(stringOf(data))
private fun getActionResName(): Int {
val action = when (tlvTag) {
TlvTag.CardId -> ActionType.Scan
else -> ActionType.Unknown
}
return action.resName
}
private fun toggleActionBtnVisibility() {
fun switchVisibilityState(newState: Int) {
actionBtnVisibilityState = newState
TransitionManager.beginDelayedTransition(etValue.parent as ViewGroup, AutoTransition())
btnAction.visibility = actionBtnVisibilityState
}
when {
onActionBtnClickListener == null -> {
if (actionBtnVisibilityState == View.GONE) return
switchVisibilityState(View.GONE)
}
value == null && actionBtnVisibilityState != View.VISIBLE -> switchVisibilityState(View.VISIBLE)
value != null && actionBtnVisibilityState != View.GONE -> switchVisibilityState(View.GONE)
}
onParameterAction = null
}
}

View file

@ -1,65 +1,57 @@
package com.tangem.tangemtest.card_use_cases.view_models
import androidx.annotation.UiThread
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.google.gson.GsonBuilder
import com.tangem.CardManager
import com.tangem.commands.Card
import com.tangem.common.tlv.TlvTag
import com.tangem.tangemtest.card_use_cases.models.CardContext
import com.tangem.tangemtest._arch.SingleLiveEvent
import com.tangem.tangemtest.card_use_cases.models.params.manager.IncomingParameter
import com.tangem.tangemtest.card_use_cases.models.params.manager.ParamsManager
import com.tangem.tangemtest.card_use_cases.models.params.manager.ParamsManagerFactory
import com.tangem.tangemtest.commons.Action
import com.tangem.tangemtest.commons.performAction
import com.tangem.tasks.ScanEvent
import com.tangem.tasks.TaskError
import com.tangem.tasks.TaskEvent
import ru.dev.gbixahue.eu4d.lib.android.global.threading.postUI
/**
[REDACTED_AUTHOR]
*/
class ParamsViewModel : ViewModel() {
val ldResponse: MutableLiveData<String> = MutableLiveData()
val ldError: MutableLiveData<String> = MutableLiveData()
val ldIncomingParameter: MutableLiveData<IncomingParameter> = MutableLiveData()
lateinit var ldCardContext: MutableLiveData<CardContext>
private set
class ActionViewModelFactory(private val manager: ParamsManager) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = ParamsViewModel(manager) as T
}
private lateinit var ldParamsManager: LiveData<ParamsManager>
private lateinit var notifier: Notifier
class ParamsViewModel(val paramsManager: ParamsManager) : ViewModel() {
// call before any subscriptions
fun init(context: FragmentActivity, action: Action) {
if (::ldCardContext.isInitialized && ::ldParamsManager.isInitialized) return
val ldCard = MutableLiveData<Card>()
val ldIsVerified = MutableLiveData<Boolean>()
val ldResponse = MutableLiveData<String>()
val ldParams = MutableLiveData(paramsManager.getParams())
val factory = ParamsManagerFactory.createFactory()
val manager = factory.get(action) ?: throw NotImplementedError(
"ParamsManager for the ${context.getString(action.resName)} not implemented yet")
val seError: MutableLiveData<String> = SingleLiveEvent()
val seIncomingParameter: MutableLiveData<IncomingParameter> = SingleLiveEvent()
ldParamsManager = MutableLiveData(manager)
ldCardContext = MutableLiveData(CardContext().init(context))
notifier = Notifier(this)
private val notifier: Notifier = Notifier(this)
private lateinit var cardManager: CardManager
fun setCardManager(cardManager: CardManager) {
this.cardManager = cardManager
}
fun getInitialParams(): List<IncomingParameter>? {
return ldParamsManager.value?.getParams()
}
fun userChangedParameter(param: IncomingParameter) {
parameterChanged(param.tlvTag, param.data)
fun userChangedParameter(tlvTag: TlvTag, value: Any?) {
parameterChanged(tlvTag, value)
}
private fun parameterChanged(tlvTag: TlvTag, value: Any?) {
ldParamsManager.value?.parameterChanged(tlvTag, value) { notifier.notifyParameterChanges(it) }
paramsManager.parameterChanged(tlvTag, value) { notifier.notifyParameterChanges(it) }
}
//invokes Scan, Sign etc...
fun invokeMainAction() {
performAction(ldParamsManager.value, ldCardContext.value?.cardManager) { paramsManager, cardManager ->
performAction(paramsManager, cardManager) { paramsManager, cardManager ->
paramsManager.invokeMainAction(cardManager) { response, listOfChangedParams ->
notifier.handleActionResult(response, listOfChangedParams)
}
@ -67,8 +59,6 @@ class ParamsViewModel : ViewModel() {
}
fun getParameterAction(tag: TlvTag): (() -> Unit)? {
val paramsManager = ldParamsManager.value ?: return null
val cardManager = ldCardContext.value?.cardManager ?: return null
val parameterFunction = paramsManager.getActionByTag(tag, cardManager) ?: return null
return {
@ -85,57 +75,46 @@ internal class Notifier(private val vm: ParamsViewModel) {
private var notShowedError: TaskError? = null
fun handleActionResult(response: TaskEvent<*>, list: List<IncomingParameter>) {
postUI { notifyParameterChanges(list) }
notifyParameterChanges(list)
handleResponse(response)
}
@UiThread
fun notifyParameterChanges(list: List<IncomingParameter>) {
list.forEach { vm.ldIncomingParameter.value = it }
list.forEach { vm.seIncomingParameter.postValue(it) }
}
fun handleResponse(response: TaskEvent<*>) {
val taskEvent = response as? TaskEvent<*> ?: return
postUI {
when (taskEvent) {
is TaskEvent.Completion -> handleCompletionEvent(taskEvent)
is TaskEvent.Event -> handleDataEvent(taskEvent.data)
}
when (taskEvent) {
is TaskEvent.Completion -> handleCompletionEvent(taskEvent)
is TaskEvent.Event -> handleDataEvent(taskEvent.data)
}
}
@UiThread
private fun handleDataEvent(event: Any?) {
when (event) {
is ScanEvent.OnReadEvent -> {
val cardContext = vm.ldCardContext.value ?: return
cardContext.card = event.card
vm.ldCardContext.value = cardContext
vm.ldResponse.value = gsonConverter.toJson(event)
vm.ldCard.postValue(event.card)
vm.ldResponse.postValue(gsonConverter.toJson(event))
}
is ScanEvent.OnVerifyEvent -> {
val cardContext = vm.ldCardContext.value ?: return
cardContext.isVerified = true
vm.ldCardContext.value = cardContext
vm.ldIsVerified.postValue(true)
}
else -> vm.ldResponse.value = gsonConverter.toJson(event)
else -> vm.ldResponse.postValue(gsonConverter.toJson(event))
}
}
@UiThread
private fun handleCompletionEvent(taskEvent: TaskEvent.Completion<*>) {
val error: TaskError = taskEvent.error ?: return
when (error) {
is TaskError.UserCancelled -> {
if (notShowedError == null) {
vm.ldError.value = "User was cancelled"
vm.seError.postValue("User was cancelled")
} else {
vm.ldError.value = "Error description not implemented. Code: ${notShowedError!!.code}"
vm.seError.postValue("Error description not implemented. Code: ${notShowedError!!.code}")
notShowedError = null
}
}

View file

@ -20,9 +20,9 @@ fun getDefaultNavigationOptions(): NavOptions {
}
}
data class NavigateOptions(val name: Action, val destinationId: Int)
data class NavigateOptions(val name: ActionType, val destinationId: Int)
enum class Action(@StringRes val resName: Int) {
enum class ActionType(@StringRes val resName: Int) {
Scan(R.string.action_card_scan),
Sign(R.string.action_card_sign),
CreateWallet(R.string.action_wallet_create),