Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-01 23:30:52 +03:00
parent eb7f0f0386
commit 0355c34514
20 changed files with 635 additions and 408 deletions

View file

@ -11,6 +11,8 @@ dependencies {
// kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7"
// crypto
implementation "com.madgag.spongycastle:core:1.58.0.0"

View file

@ -3,6 +3,8 @@ package com.tangem
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BroadcastChannel
/**
* Allows interaction between the phone or any other terminal and Tangem card.
@ -11,6 +13,9 @@ import com.tangem.common.apdu.ResponseApdu
*/
interface CardReader {
val tag: BroadcastChannel<TagType?>
var scope: CoroutineScope?
/**
* Sends data to the card and receives the reply.
*
@ -23,10 +28,17 @@ interface CardReader {
/**
* Signals to [CardReader] to become ready to transceive data.
*/
fun openSession()
fun startSession()
/**
* Signals to [CardReader] that no further NFC transition is expected.
*/
fun closeSession()
fun stopSession(cancelled: Boolean = false)
fun readSlixTag(callback: (result: CompletionResult<ResponseApdu>) -> Unit)
}
interface ReadingActiveListener {
var readingIsActive: Boolean
}

View file

@ -1,6 +1,5 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.CommandResponse
import com.tangem.commands.OpenSessionCommand
import com.tangem.commands.ReadCommand
@ -13,6 +12,14 @@ import com.tangem.crypto.EncryptionHelper
import com.tangem.crypto.FastEncryptionHelper
import com.tangem.crypto.StrongEncryptionHelper
import com.tangem.crypto.pbkdf2Hash
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
/**
* Basic interface for running tasks and [com.tangem.commands.Command] in a [CardSession]
@ -30,6 +37,16 @@ interface CardSessionRunnable<T : CommandResponse> {
fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit)
}
enum class CardSessionState {
Inactive,
Active
}
enum class TagType {
Nfc,
Slix
}
/**
* Allows interaction with Tangem cards. Should be opened before sending commands.
*
@ -51,13 +68,16 @@ class CardSession(
private val initialMessage: Message? = null
) {
private val tag = this.javaClass.simpleName
var connectedTag: TagType? = null
/**
* True if some operation is still in progress.
*/
private var isBusy = false
private var state = CardSessionState.Inactive
private var performPreflightRead = true
private val scope = CoroutineScope(Dispatchers.IO)
private val tag = this.javaClass.simpleName
/**
* This metod starts a card session, performs preflight [ReadCommand],
@ -68,9 +88,7 @@ class CardSession(
fun <T : CardSessionRunnable<R>, R : CommandResponse> startWithRunnable(
runnable: T, callback: (result: CompletionResult<R>) -> Unit) {
performPreflightRead = runnable.performPreflightRead
start { session, error ->
start(runnable.performPreflightRead) { session, error ->
if (error != null) {
callback(CompletionResult.Failure(error))
return@start
@ -103,49 +121,55 @@ class CardSession(
* Starts a card session and performs preflight [ReadCommand].
* @param callback: callback with the card session. Can contain [TangemSdkError] if something goes wrong.
*/
fun start(callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
try {
startSession()
} catch (error: TangemSdkError) {
callback(this, error)
}
fun start(performPreflightRead: Boolean = true,
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
if (!performPreflightRead) {
callback(this, null)
if (state != CardSessionState.Inactive) {
callback(this, TangemSdkError.Busy())
return
}
state = CardSessionState.Active
viewDelegate.onSessionStarted(cardId)
preflightRead() { result ->
when (result) {
is CompletionResult.Failure -> {
callback(this, result.error)
stopWithError(result.error)
}
is CompletionResult.Success -> {
callback(this, null)
}
}
scope.launch {
reader.tag
.asFlow()
.collect { tagType ->
if (tagType == null && connectedTag != null) {
handleTagLost()
} else if (tagType != null) {
connectedTag = tagType
viewDelegate.onTagConnected()
if (tagType == TagType.Nfc && performPreflightRead) {
preflightCheck(callback)
} else {
callback(this@CardSession, null)
}
}
}
}
reader.scope = scope
reader.startSession()
}
private fun startSession() {
if (isBusy) throw TangemSdkError.Busy()
isBusy = true
viewDelegate.onNfcSessionStarted(cardId, initialMessage)
reader.openSession()
private fun handleTagLost() {
connectedTag = null
environment.encryptionKey = null
viewDelegate.onTagLost()
}
private fun preflightRead(callback: (result: CompletionResult<Card>) -> Unit) {
private fun preflightCheck(callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val readCommand = ReadCommand()
readCommand.run(this) { result ->
when (result) {
is CompletionResult.Failure -> {
tryHandleError(result.error) { handleErrorResult ->
when (handleErrorResult) {
is CompletionResult.Success -> preflightRead(callback)
is CompletionResult.Success -> preflightCheck(callback)
is CompletionResult.Failure -> {
stopWithError(result.error)
callback(CompletionResult.Failure(result.error))
callback(this, result.error)
}
}
}
@ -153,32 +177,35 @@ class CardSession(
is CompletionResult.Success -> {
val receivedCardId = result.data.cardId
if (cardId != null && receivedCardId != cardId) {
stopWithError(TangemSdkError.WrongCardNumber())
callback(CompletionResult.Failure(TangemSdkError.WrongCardNumber()))
viewDelegate.onWrongCard()
preflightCheck(callback)
return@run
}
val allowedCardTypes = environment.cardFilter.allowedCardTypes
if (!allowedCardTypes.contains(result.data.getType())) {
stopWithError(TangemSdkError.WrongCardType())
callback(CompletionResult.Failure(TangemSdkError.WrongCardType()))
callback(this, TangemSdkError.WrongCardType())
return@run
}
environment.card = result.data
cardId = receivedCardId
callback(CompletionResult.Success(result.data))
callback(this, null)
}
}
}
}
fun readSlixTag(callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
reader.readSlixTag(callback)
}
/**
* Stops the current session with the text message.
* @param message If null, the default message will be shown.
*/
private fun stop(message: Message? = null) {
reader.closeSession()
viewDelegate.onNfcSessionCompleted(message)
isBusy = false
stopSession()
viewDelegate.onSessionStopped(message)
}
/**
@ -186,27 +213,34 @@ class CardSession(
* @param error An error that will be shown.
*/
private fun stopWithError(error: TangemSdkError) {
if (!isBusy) return
reader.closeSession()
isBusy = false
val errorMessage = if (error is TangemSdkError) {
"${error::class.simpleName}: ${error.code}"
} else {
error.localizedMessage
}
stopSession()
if (error !is TangemSdkError.UserCancelled) {
Log.e(tag, "Finishing with error: $errorMessage")
Log.e(tag, "Finishing with error: ${error::class.simpleName}: ${error.code}")
viewDelegate.onError(error)
} else {
Log.i(tag, "User cancelled NFC session")
}
}
private fun stopSession() {
reader.stopSession()
state = CardSessionState.Inactive
scope.cancel()
}
fun send(apdu: CommandApdu, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
reader.transceiveApdu(apdu, callback)
val subscription = reader.tag.openSubscription()
scope.launch {
subscription.consumeAsFlow()
.filterNotNull()
.collect {
reader.transceiveApdu(apdu) { result ->
subscription.cancel()
callback(result)
}
}
}
}
private fun tryHandleError(

View file

@ -12,7 +12,7 @@ interface SessionViewDelegate {
/**
* It is called when user is expected to scan a Tangem Card with an Android device.
*/
fun onNfcSessionStarted(cardId: String?, message: Message? = null)
fun onSessionStarted(cardId: String?, message: Message? = null)
/**
* It is called when security delay is triggered by the card.
@ -32,10 +32,14 @@ interface SessionViewDelegate {
*/
fun onTagLost()
fun onTagConnected()
fun onWrongCard()
/**
* It is called when NFC session was completed and a user can take the card away from the Android device.
*/
fun onNfcSessionCompleted(message: Message? = null)
fun onSessionStopped(message: Message? = null)
/**
* It is called when some error occur during NFC session.

View file

@ -374,7 +374,7 @@ class TangemSdk(
fun startSession(cardId: String? = null, initialMessage: Message? = null,
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.start(callback) }
Thread().run { cardSession.start(callback = callback) }
}
/**

View file

@ -3,13 +3,12 @@ package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.CardDeserializer
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import java.util.*
@ -398,57 +397,6 @@ class ReadCommand : Command<Card>() {
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
return CardDeserializer.deserialize(apdu, environment)
}
}

View file

@ -0,0 +1,69 @@
package com.tangem.commands.common
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.Card
import com.tangem.commands.CardData
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class CardDeserializer() {
companion object {
fun deserialize(apdu: ResponseApdu, environment: SessionEnvironment): Card {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
}
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.commands.Card
import com.tangem.commands.CardData
import com.tangem.commands.CardStatus
import com.tangem.commands.Command
import com.tangem.commands.common.CardDeserializer
import com.tangem.commands.personalization.entities.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
@ -14,9 +15,7 @@ import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
@ -56,57 +55,7 @@ class PersonalizeCommand(
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData(devPersonalizationKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
return CardDeserializer.deserialize(apdu, environment)
}
private fun serializePersonalizationData(config: CardConfig): ByteArray {

View file

@ -2,8 +2,10 @@ package com.tangem.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.TagType
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.commands.common.CardDeserializer
import com.tangem.common.CompletionResult
/**
@ -13,31 +15,58 @@ import com.tangem.common.CompletionResult
*/
internal class ScanTask : CardSessionRunnable<Card> {
override val performPreflightRead = true
override val performPreflightRead = false
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
if (session.connectedTag == TagType.Slix) {
readSlixTag(session, callback)
return
}
} else if (card.cardData?.productMask?.contains(Product.Tag) != false) {
callback(CompletionResult.Success(card))
ReadCommand().run(session) { readResult ->
when(readResult) {
is CompletionResult.Failure -> callback(readResult)
is CompletionResult.Success -> {
val card = readResult.data
session.environment.card = card
if (card.cardData?.productMask?.contains(Product.Tag) != false) {
callback(CompletionResult.Success(card))
} else if (card.status != CardStatus.Loaded) {
callback(CompletionResult.Success(card))
} else if (card.status != CardStatus.Loaded) {
callback(CompletionResult.Success(card))
} else if (card.curve == null || card.walletPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else if (card.curve == null || card.walletPublicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else {
val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey)
} else {
val checkWalletCommand = CheckWalletCommand(card.curve, card.walletPublicKey)
checkWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(card))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
}
}
}
checkWalletCommand.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(card))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
private fun readSlixTag(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
session.readSlixTag { result ->
when (result) {
is CompletionResult.Success -> {
try {
val card = CardDeserializer.deserialize(result.data, session.environment)
callback(CompletionResult.Success(card))
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}

View file

@ -4,6 +4,10 @@ import com.google.common.truth.Truth.assertThat
import com.tangem.SessionEnvironment
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvTag
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import org.junit.Test
@ -19,11 +23,12 @@ class CommandApduTest {
tlvBuilder.serialize()
)
val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 34, 16, 32, -111, -76, -47, 66, -126, 63, 125,
32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10,
-45, 19, -124, -123, -55, -94, 3)
32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25, -10,
-45, 19, -124, -123, -55, -94, 3)
assertThat(commandApdu.apduData)
.isEqualTo(expected)
assertThat(listOf(1, 2)).containsExactlyElementsIn(listOf(1,2))
}
@Test
@ -42,13 +47,42 @@ class CommandApduTest {
)
val expected = byteArrayOf(0, -14, 0, 0, 0, 0, 101, 16, 32, -111, -76, -47, 66, -126, 63,
125, 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25,
-10, -45, 19, -124, -123, -55, -94, 3, 92, 65, 4, 80, -122, 58, -42, 74, -121, -82, -118,
47, -24, 60, 26, -15, -88, 64, 60, -75, 63, 83, -28, -122, -40, 81, 29, -83, -118, 4, -120,
126, 91, 35, 82, 44, -44, 112, 36, 52, 83, -94, -103, -6, -98, 119, 35, 119, 22, 16, 58,
-68, 17, -95, -33, 56, -123, 94, -42, -14, -18, 24, 126, -100, 88, 43, -90)
125, 32, -59, -16, -115, -10, -111, 34, -34, 67, -13, 95, 5, 122, -104, -115, -106, 25,
-10, -45, 19, -124, -123, -55, -94, 3, 92, 65, 4, 80, -122, 58, -42, 74, -121, -82, -118,
47, -24, 60, 26, -15, -88, 64, 60, -75, 63, 83, -28, -122, -40, 81, 29, -83, -118, 4, -120,
126, 91, 35, 82, 44, -44, 112, 36, 52, 83, -94, -103, -6, -98, 119, 35, 119, 22, 16, 58,
-68, 17, -95, -33, 56, -123, 94, -42, -14, -18, 24, 126, -100, 88, 43, -90)
assertThat(commandApdu.apduData)
.isEqualTo(expected)
}
@Test
fun testFlow() {
val channel = BroadcastChannel<Int>(1)
val flow = GlobalScope.launch {
delay(100)
channel.send(1)
delay(200)
channel.send(2)
}
val scope = CoroutineScope(Dispatchers.IO)
val scope2 = CoroutineScope(Dispatchers.IO)
val receiveChannel1 = BroadcastChannel<Int>(1)
val receiveChannel2 = BroadcastChannel<Int>(1)
scope.launch {
channel.asFlow().collect { println(it) }
channel.asFlow().collect { println(it) }
}
scope.launch { channel.asFlow().collect { println(it) } }
runBlocking {
delay(1000)
}
}
}

View file

@ -181,7 +181,7 @@ class PersonalizationConfig {
signingMethodMaskBuilder.add(SigningMethod.SignRawValidateByIssuerWriteIssuerData)
}
if (from.SigningMethod6) {
signingMethodMaskBuilder.add(SigningMethod.SignHash)
signingMethodMaskBuilder.add(SigningMethod.SignPos)
}
return signingMethodMaskBuilder.build()
}

View file

@ -39,22 +39,31 @@ android {
}
dependencies {
// internal
implementation project(':tangem-core')
implementation fileTree(dir: 'libs', include: ['*.jar'])
// kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
// coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7"
// android
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
// misc
implementation 'com.skyfishjy.ripplebackground:library:1.0.1'
implementation 'at.favre.lib:armadillo:0.9.0'
// testing
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View file

@ -1,20 +1,10 @@
package com.tangem.tangem_sdk_new
import android.animation.ObjectAnimator
import android.app.Activity
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.animation.DecelerateInterpolator
import com.google.android.material.bottomsheet.BottomSheetDialog
import androidx.fragment.app.FragmentActivity
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.tangem_sdk_new.extensions.hide
import com.tangem.tangem_sdk_new.extensions.localizedDescription
import com.tangem.tangem_sdk_new.extensions.show
import com.tangem.tangem_sdk_new.nfc.NfcReader
import com.tangem.tangem_sdk_new.ui.TouchCardAnimation
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.main.nfc_bottom_sheet.*
import com.tangem.tangem_sdk_new.ui.NfcSessionDialog
/**
* Default implementation of [SessionViewDelegate].
@ -22,148 +12,65 @@ import kotlinx.android.synthetic.main.nfc_bottom_sheet.*
*/
class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDelegate {
lateinit var activity: Activity
private var readingDialog: BottomSheetDialog? = null
lateinit var activity: FragmentActivity
private var readingDialog: NfcSessionDialog? = null
init {
setLogger()
}
override fun onNfcSessionStarted(cardId: String?, message: Message?) {
reader.readingCancelled = false
override fun onSessionStarted(cardId: String?, message: Message?) {
postUI { showReadingDialog(activity, cardId, message) }
}
private fun showReadingDialog(activity: Activity, cardId: String?, message: Message?) {
private fun showReadingDialog(activity: FragmentActivity, cardId: String?, message: Message?) {
val dialogView = activity.layoutInflater.inflate(R.layout.nfc_bottom_sheet, null)
readingDialog = BottomSheetDialog(activity)
readingDialog = NfcSessionDialog(activity)
readingDialog?.setContentView(dialogView)
readingDialog?.dismissWithAnimation = true
readingDialog?.create()
readingDialog?.setOnShowListener {
readingDialog?.rippleBackgroundNfc?.startRippleAnimation()
val nfcDeviceAntenna = TouchCardAnimation(
activity, readingDialog!!.ivHandCardHorizontal,
readingDialog!!.ivHandCardVertical, readingDialog!!.llHand, readingDialog!!.llNfc)
nfcDeviceAntenna.init()
if (cardId != null) {
readingDialog?.tvCard?.visibility = View.VISIBLE
readingDialog?.tvCardId?.visibility = View.VISIBLE
readingDialog?.tvCardId?.text = cardId
}
if (message != null) {
if (message.body != null) readingDialog?.tvTaskText?.text = message.body
if (message.header != null) readingDialog?.tvTaskTitle?.text = message.header
}
}
readingDialog?.setOnCancelListener {
reader.readingCancelled = true
reader.closeSession()
readingDialog?.show(SessionViewDelegateState.Ready(cardId, message))
}
readingDialog?.setOnCancelListener { reader.stopSession(true) }
readingDialog?.show()
}
override fun onSecurityDelay(ms: Int, totalDurationSeconds: Int) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.tvRemainingTime?.text = ms.div(100).toString()
readingDialog?.flSecurityDelay?.show()
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_security_delay)
readingDialog?.tvTaskText?.text =
activity.getText(R.string.dialog_security_delay_description)
performHapticFeedback()
if (readingDialog?.pbSecurityDelay?.max != totalDurationSeconds) {
readingDialog?.pbSecurityDelay?.max = totalDurationSeconds
}
readingDialog?.pbSecurityDelay?.progress = totalDurationSeconds - ms + 100
val animation = ObjectAnimator.ofInt(
readingDialog?.pbSecurityDelay,
"progress",
totalDurationSeconds - ms,
totalDurationSeconds - ms + 100)
animation.duration = 500
animation.interpolator = DecelerateInterpolator()
animation.start()
readingDialog?.show(SessionViewDelegateState.SecurityDelay(ms, totalDurationSeconds))
}
}
override fun onDelay(total: Int, current: Int, step: Int) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.flSecurityDelay?.show()
readingDialog?.tvRemainingTime?.text = (((total - current) / step) + 1).toString()
readingDialog?.tvTaskTitle?.text = "Operation in process"
readingDialog?.tvTaskText?.text = "Please hold the card firmly until the operation is completed…"
performHapticFeedback()
if (readingDialog?.pbSecurityDelay?.max != total) {
readingDialog?.pbSecurityDelay?.max = total
}
readingDialog?.pbSecurityDelay?.progress = current
val animation = ObjectAnimator.ofInt(
readingDialog?.pbSecurityDelay,
"progress",
current,
current + step)
animation.duration = 300
animation.interpolator = DecelerateInterpolator()
animation.start()
readingDialog?.show(SessionViewDelegateState.Delay(total, current, step))
}
}
override fun onTagLost() {
postUI {
readingDialog?.lTouchCard?.show()
readingDialog?.flSecurityDelay?.hide()
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
readingDialog?.tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
}
postUI { readingDialog?.show(SessionViewDelegateState.TagLost) }
}
override fun onNfcSessionCompleted(message: Message?) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.flSecurityDelay?.hide()
readingDialog?.flCompletion?.show()
readingDialog?.ivCompletion?.setImageDrawable(activity.getDrawable(R.drawable.ic_done_135dp))
if (message != null) {
if (message.body != null) readingDialog?.tvTaskText?.text = message.body
if (message.header != null) readingDialog?.tvTaskTitle?.text = message.header
}
performHapticFeedback()
}
postUI(300) { readingDialog?.dismiss() }
override fun onTagConnected() {
postUI { readingDialog?.show(SessionViewDelegateState.TagConnected) }
}
override fun onWrongCard() {
postUI { readingDialog?.show(SessionViewDelegateState.WrongCard) }
}
override fun onSessionStopped(message: Message?) {
postUI { readingDialog?.show(SessionViewDelegateState.Success(message)) }
}
override fun onError(error: TangemSdkError) {
postUI {
readingDialog?.lTouchCard?.hide()
readingDialog?.flSecurityDelay?.hide()
readingDialog?.flCompletion?.hide()
readingDialog?.flError?.show()
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_error)
readingDialog?.tvTaskText?.text = activity.getString(
R.string.error_message,
error.code.toString(), activity.getString(error.localizedDescription())
)
performHapticFeedback()
}
postUI { readingDialog?.show(SessionViewDelegateState.Error(error)) }
}
override fun onPinRequested(callback: (result: CompletionResult<String>) -> Unit) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
private fun performHapticFeedback() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
readingDialog?.llHeader?.isHapticFeedbackEnabled = true
readingDialog?.llHeader?.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
postUI { readingDialog?.show(SessionViewDelegateState.PinRequested(callback)) }
}
private fun setLogger() {

View file

@ -0,0 +1,17 @@
package com.tangem.tangem_sdk_new
import com.tangem.Message
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
sealed class SessionViewDelegateState() {
data class Error(val error: TangemSdkError) : SessionViewDelegateState()
data class Success(val message: Message?) : SessionViewDelegateState()
data class SecurityDelay(val ms: Int, val totalDurationSeconds: Int) : SessionViewDelegateState()
data class Delay(val total: Int, val current: Int, val step: Int) : SessionViewDelegateState()
data class Ready(val cardId: String?, val message: Message?) : SessionViewDelegateState()
data class PinRequested(val callback: (result: CompletionResult<String>) -> Unit) : SessionViewDelegateState()
object TagLost : SessionViewDelegateState()
object TagConnected : SessionViewDelegateState()
object WrongCard : SessionViewDelegateState()
}

View file

@ -8,4 +8,13 @@ internal fun View.show() {
internal fun View.hide() {
this.visibility = View.GONE
}
internal fun View.show(show: Boolean) {
if (show) {
this.visibility = View.VISIBLE
} else {
this.visibility = View.GONE
}
}

View file

@ -11,6 +11,7 @@ import android.nfc.tech.IsoDep
import android.os.Build
import android.os.Bundle
import com.tangem.Log
import com.tangem.ReadingActiveListener
import com.tangem.tangem_sdk_new.ui.NfcEnableDialog
/**
@ -18,7 +19,16 @@ import com.tangem.tangem_sdk_new.ui.NfcEnableDialog
* Launches [NfcAdapter], manages it with [Activity] lifecycle,
* enables and disables Nfc Reading Mode, receives NFC [Tag].
*/
class NfcManager : NfcAdapter.ReaderCallback {
class NfcManager : NfcAdapter.ReaderCallback, ReadingActiveListener {
override var readingIsActive: Boolean = false
set(value) {
if (value) {
disableReaderMode()
enableReaderMode()
}
field = value
}
val reader = NfcReader()
private var activity: Activity? = null
@ -42,7 +52,7 @@ class NfcManager : NfcAdapter.ReaderCallback {
override fun onTagDiscovered(tag: Tag?) {
Log.i(this::class.simpleName!!, "Nfc tag is discovered")
if (reader.readingActive) reader.onTagDiscovered(tag) else ignoreTag(tag)
if (readingIsActive) reader.onTagDiscovered(tag) else ignoreTag(tag)
}
@ -50,11 +60,10 @@ class NfcManager : NfcAdapter.ReaderCallback {
val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)
activity?.registerReceiver(mBroadcastReceiver, filter)
handleNfcEnabled(nfcAdapter?.isEnabled == true)
reader.manager = this
reader.listener = this
}
private fun handleNfcEnabled(nfcEnabled: Boolean) {
reader.nfcEnabled = nfcEnabled
if (nfcEnabled) {
enableReaderMode()
nfcEnableDialog?.cancel()
@ -67,7 +76,7 @@ class NfcManager : NfcAdapter.ReaderCallback {
fun onPause() {
activity?.unregisterReceiver(mBroadcastReceiver)
disableReaderMode()
reader.manager = null
reader.listener = null
}
fun onDestroy() {

View file

@ -4,127 +4,115 @@ import android.nfc.Tag
import android.nfc.TagLostException
import android.nfc.tech.IsoDep
import android.nfc.tech.NfcV
import com.tangem.CardReader
import com.tangem.Log
import com.tangem.TangemSdkError
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.toHexString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.launch
data class NfcTag(val type: TagType, val isoDep: IsoDep?, val nfcV: NfcV? = null)
/**
* Provides NFC communication between an Android application and Tangem card.
*/
class NfcReader : CardReader {
override val tag = ConflatedBroadcastChannel<TagType?>()
override var scope: CoroutineScope? = null
var readingActive = false
private set
var nfcEnabled = false
var manager: NfcManager? = null
private var isoDep: IsoDep? = null
set(value) {
// don't reassign when there's an active tag already
if (field == null) {
field = value
// if tag is received, call connect first before transceiving data
if (value != null) connect()
}
if (value == null) field = value
}
var listener: ReadingActiveListener? = null
var readingCancelled = false
private var nfcTag: NfcTag? = null
set(value) {
field = value
if (value) {
// Stops reading and sends failure callback to a task
// if reading is cancelled (when user closes nfc bottom sheet dialog).
closeSession()
callback?.invoke(CompletionResult.Failure(TangemSdkError.UserCancelled()))
}
scope?.launch { tag.send(value?.type) }
}
private var data: ByteArray? = null
private var callback: ((response: CompletionResult<ResponseApdu>) -> Unit)? = null
override fun openSession() {
override fun startSession() {
Log.i(this::class.simpleName!!, "NFC reader is starting NFC session")
readingActive = true
readingCancelled = false
manager?.disableReaderMode()
manager?.enableReaderMode()
}
override fun closeSession() {
isoDep = null
readingActive = false
}
override fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit) {
data = apdu.apduData
this.callback = callback
if (isoDep != null) {
transceiveData()
}
}
private fun transceiveData() {
if (readingCancelled) {
callback?.invoke(CompletionResult.Failure(TangemSdkError.UserCancelled()))
return
}
if (data == null) return
val rawResponse: ByteArray?
try {
Log.i(this::class.simpleName!!, "Sending data to the card, size is ${data?.size}")
Log.v(this::class.simpleName!!, "Raw data that is to be sent to the card: ${data?.toHexString()}")
rawResponse = isoDep?.transceive(data)
Log.v(this::class.simpleName!!, "Raw data that was received from the card: ${rawResponse?.toHexString()}")
} catch (exception: TagLostException) {
callback?.invoke(CompletionResult.Failure(TangemSdkError.TagLost()))
isoDep = null
return
} catch (exception: Exception) {
Log.i(this::class.simpleName!!, exception.localizedMessage ?: "Error tranceiving data")
// The messages of errors can vary on different Android devices,
// but we try to identify it by parsing the message.
if (exception.message?.contains("length") == true) {
callback?.invoke(CompletionResult.Failure(TangemSdkError.ExtendedLengthNotSupported()))
}
isoDep = null
return
}
if (rawResponse != null) {
Log.i(this::class.simpleName!!, "Data from the card was received")
data = null
}
rawResponse?.let { callback?.invoke(CompletionResult.Success(ResponseApdu(it))) }
listener?.readingIsActive = true
}
fun onTagDiscovered(tag: Tag?) {
NfcV.get(tag)?.let { onNfcVDiscovered(it) }
isoDep = IsoDep.get(tag)
transceiveData()
NfcV.get(tag)?.let {
nfcTag = NfcTag(TagType.Slix, null, NfcV.get(tag))
return
}
IsoDep.get(tag)?.let { isoDep ->
connect(isoDep)
nfcTag = NfcTag(TagType.Nfc, isoDep)
}
}
private fun connect() {
isoDep?.connect()
isoDep?.close()
isoDep?.connect()
isoDep?.timeout = 240000
private fun connect(isoDep: IsoDep) {
isoDep.connect()
isoDep.close()
isoDep.connect()
isoDep.timeout = 240000
Log.i(this::class.simpleName!!, "NFC tag is connected")
}
private fun onNfcVDiscovered(nfcV: NfcV) {
override fun stopSession(cancelled: Boolean) {
nfcTag = null
listener?.readingIsActive = false
//TODO: send user cancelled if (cancelled)
}
override fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit) {
val data = apdu.apduData
val rawResponse: ByteArray? = try {
transcieveAndLog(data, callback)
} catch (exception: TagLostException) {
callback.invoke(CompletionResult.Failure(TangemSdkError.TagLost()))
nfcTag = null
return
} catch (exception: Exception) {
tryHandleNfcError(exception, callback)
nfcTag = null
return
}
if (rawResponse != null) {
Log.i(this::class.simpleName!!, "Data from the card was received")
}
rawResponse?.let { callback.invoke(CompletionResult.Success(ResponseApdu(it))) }
}
private fun transcieveAndLog(data: ByteArray, callback: (response: CompletionResult<ResponseApdu>) -> Unit): ByteArray? {
Log.i(this::class.simpleName!!, "Sending data to the card, size is ${data.size}")
Log.v(this::class.simpleName!!, "Raw data that is to be sent to the card: ${data.toHexString()}")
val rawResponse = nfcTag?.isoDep?.transceive(data)
Log.v(this::class.simpleName!!, "Raw data that was received from the card: ${rawResponse?.toHexString()}")
return rawResponse
}
private fun tryHandleNfcError(exception: Exception, callback: (response: CompletionResult<ResponseApdu>) -> Unit) {
Log.i(this::class.simpleName!!, exception.localizedMessage ?: "Error tranceiving data")
// The messages of errors can vary on different Android devices,
// but we try to identify it by parsing the message.
if (exception.message?.contains("length") == true) {
callback.invoke(CompletionResult.Failure(TangemSdkError.ExtendedLengthNotSupported()))
}
}
override fun readSlixTag(callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
val nfcV = nfcTag?.nfcV
if (nfcV == null) {
callback.invoke(CompletionResult.Failure(TangemSdkError.ErrorProcessingCommand()))
return
}
val response = SlixTagReader().transceive(nfcV)
when (response) {
is SlixReadResult.Failure -> {
Log.e(this::class.simpleName!!, "${response.exception.message}")
callback?.invoke(CompletionResult.Failure(TangemSdkError.ErrorProcessingCommand()))
callback.invoke(CompletionResult.Failure(TangemSdkError.ErrorProcessingCommand()))
}
is SlixReadResult.Success -> {
callback?.invoke(CompletionResult.Success(ResponseApdu(response.data)))
callback.invoke(CompletionResult.Success(ResponseApdu(response.data)))
}
}
}

View file

@ -0,0 +1,171 @@
package com.tangem.tangem_sdk_new.ui
import android.animation.ObjectAnimator
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.fragment.app.FragmentActivity
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tangem_sdk_new.R
import com.tangem.tangem_sdk_new.SessionViewDelegateState
import com.tangem.tangem_sdk_new.extensions.localizedDescription
import com.tangem.tangem_sdk_new.extensions.show
import com.tangem.tangem_sdk_new.postUI
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.main.nfc_bottom_sheet.*
class NfcSessionDialog(val activity: FragmentActivity) : BottomSheetDialog(activity) {
private var currentState: SessionViewDelegateState? = null
fun show(state: SessionViewDelegateState) {
when (state) {
is SessionViewDelegateState.Ready -> onReady(state)
is SessionViewDelegateState.Success -> onSuccess(state)
is SessionViewDelegateState.Error -> onError(state)
is SessionViewDelegateState.SecurityDelay -> onSecurityDelay(state)
is SessionViewDelegateState.Delay -> onDelay(state)
is SessionViewDelegateState.PinRequested -> onPinRequested(state)
is SessionViewDelegateState.TagLost -> onTagLost()
is SessionViewDelegateState.TagConnected -> onTagConnected()
is SessionViewDelegateState.WrongCard -> onWrongCard()
}
currentState = state
}
private fun onReady(state: SessionViewDelegateState.Ready) {
show(lTouchCard)
rippleBackgroundNfc?.startRippleAnimation()
val nfcDeviceAntenna = TouchCardAnimation(
activity, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc
)
nfcDeviceAntenna.init()
state.cardId?.let { cardId ->
tvCard?.show()
tvCardId?.show()
tvCardId?.text = cardId
}
state.message?.let { message ->
if (message.body != null) tvTaskText?.text = message.body
if (message.header != null) tvTaskTitle?.text = message.header
}
}
private fun onSuccess(state: SessionViewDelegateState.Success) {
show(flCompletion)
ivCompletion?.setImageDrawable(activity.getDrawable(R.drawable.ic_done_135dp))
state.message?.let { message ->
if (message.body != null) tvTaskText?.text = message.body
if (message.header != null) tvTaskTitle?.text = message.header
}
performHapticFeedback()
postUI(300) { dismiss() }
}
private fun onError(state: SessionViewDelegateState.Error) {
show(flError)
tvTaskTitle?.text = activity.getText(R.string.dialog_error)
tvTaskText?.text = activity.getString(
R.string.error_message,
state.error.code.toString(), activity.getString(state.error.localizedDescription())
)
performHapticFeedback()
}
private fun onSecurityDelay(state: SessionViewDelegateState.SecurityDelay) {
show(flSecurityDelay)
tvRemainingTime?.text = state.ms.div(100).toString()
tvTaskTitle?.text = activity.getText(R.string.dialog_security_delay)
tvTaskText?.text =
activity.getText(R.string.dialog_security_delay_description)
performHapticFeedback()
if (pbSecurityDelay?.max != state.totalDurationSeconds) {
pbSecurityDelay?.max = state.totalDurationSeconds
}
pbSecurityDelay?.progress = state.totalDurationSeconds - state.ms + 100
val animation = ObjectAnimator.ofInt(
pbSecurityDelay,
"progress",
state.totalDurationSeconds - state.ms,
state.totalDurationSeconds - state.ms + 100)
animation.duration = 500
animation.interpolator = DecelerateInterpolator()
animation.start()
}
private fun onDelay(state: SessionViewDelegateState.Delay) {
show(flSecurityDelay)
tvRemainingTime?.text = (((state.total - state.current) / state.step) + 1).toString()
tvTaskTitle?.text = "Operation in process"
tvTaskText?.text = "Please hold the card firmly until the operation is completed…"
performHapticFeedback()
if (pbSecurityDelay?.max != state.total) {
pbSecurityDelay?.max = state.total
}
pbSecurityDelay?.progress = state.current
val animation = ObjectAnimator.ofInt(
pbSecurityDelay,
"progress",
state.current,
state.current + state.step)
animation.duration = 300
animation.interpolator = DecelerateInterpolator()
animation.start()
}
private fun onPinRequested(state: SessionViewDelegateState.PinRequested) {
TODO("To be implemented")
}
private fun onTagLost() {
if (currentState is SessionViewDelegateState.Success) return
show(lTouchCard)
tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
}
private fun onTagConnected() {
show(flReading)
}
private fun onWrongCard() {
if (currentState !is SessionViewDelegateState.WrongCard) {
show(flError)
tvTaskTitle?.text = activity.getText(R.string.dialog_error)
tvTaskText?.text = activity.getString(
R.string.error_message,
"Wrong Card", activity.getString(R.string.error_wrong_card_number)
)
performHapticFeedback()
postUI (2000){
show(lTouchCard)
tvTaskTitle?.text = activity.getText(R.string.dialog_ready_to_scan)
tvTaskText?.text = activity.getText(R.string.dialog_scan_text)
}
}
}
private fun show(view: View) {
lTouchCard?.show(view.id == lTouchCard.id)
flSecurityDelay?.show(view.id == flSecurityDelay.id)
flReading?.show(view.id == flReading.id)
flError?.show(view.id == flError.id)
flCompletion?.show(view.id == flCompletion.id)
}
private fun performHapticFeedback() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
llHeader?.isHapticFeedbackEnabled = true
llHeader?.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
}
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<rotate android:pivotX="50%" android:pivotY="50%" android:fromDegrees="0"
android:toDegrees="360"
xmlns:android="http://schemas.android.com/apk/res/android">
<shape
android:shape="ring"
android:thickness="5dp"
android:useLevel="true">
<gradient
android:angle="360"
android:endColor="@color/card_sdk_accent"
android:centerColor="@color/card_sdk_accent"
android:startColor="@color/card_sdk_accent"
android:type="sweep" />
</shape>
</rotate>

View file

@ -157,9 +157,24 @@
android:progressDrawable="@drawable/pb_circle"
android:progressTint="#FF9D30"
android:secondaryProgress="100" />
</FrameLayout>
<FrameLayout
android:id="@+id/flReading"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_gravity="center"
android:visibility="gone">
<ProgressBar
android:id="@+id/pbReading"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:indeterminateDrawable="@drawable/pb_simple_circle"
android:progressTint="#FF9D30"/>
</FrameLayout>
<TextView
android:id="@+id/tvTaskText"