Updated on 2026-08-14
This commit is contained in:
parent
eb7f0f0386
commit
0355c34514
20 changed files with 635 additions and 408 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +181,7 @@ class PersonalizationConfig {
|
|||
signingMethodMaskBuilder.add(SigningMethod.SignRawValidateByIssuerWriteIssuerData)
|
||||
}
|
||||
if (from.SigningMethod6) {
|
||||
signingMethodMaskBuilder.add(SigningMethod.SignHash)
|
||||
signingMethodMaskBuilder.add(SigningMethod.SignPos)
|
||||
}
|
||||
return signingMethodMaskBuilder.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
21
tangem-sdk/src/main/res/drawable/pb_simple_circle.xml
Normal file
21
tangem-sdk/src/main/res/drawable/pb_simple_circle.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue