diff --git a/tangem-core/build.gradle b/tangem-core/build.gradle index 220c949038..c64079660f 100644 --- a/tangem-core/build.gradle +++ b/tangem-core/build.gradle @@ -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" diff --git a/tangem-core/src/main/java/com/tangem/CardReader.kt b/tangem-core/src/main/java/com/tangem/CardReader.kt index 2327e13fe6..356b14fd34 100644 --- a/tangem-core/src/main/java/com/tangem/CardReader.kt +++ b/tangem-core/src/main/java/com/tangem/CardReader.kt @@ -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 + 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) -> Unit) + +} + +interface ReadingActiveListener { + var readingIsActive: Boolean } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/CardSession.kt b/tangem-core/src/main/java/com/tangem/CardSession.kt index c7fa4559e7..f3a77df81d 100644 --- a/tangem-core/src/main/java/com/tangem/CardSession.kt +++ b/tangem-core/src/main/java/com/tangem/CardSession.kt @@ -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 { fun run(session: CardSession, callback: (result: CompletionResult) -> 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 , R : CommandResponse> startWithRunnable( runnable: T, callback: (result: CompletionResult) -> 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) -> 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) -> 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) -> 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( diff --git a/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt b/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt index 969b147b8f..71311dcf00 100644 --- a/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt +++ b/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt @@ -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. diff --git a/tangem-core/src/main/java/com/tangem/TangemSdk.kt b/tangem-core/src/main/java/com/tangem/TangemSdk.kt index dc8f8dbe06..c5ac313d34 100644 --- a/tangem-core/src/main/java/com/tangem/TangemSdk.kt +++ b/tangem-core/src/main/java/com/tangem/TangemSdk.kt @@ -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) } } /** diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt index c8bbda44ff..49bf955851 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt @@ -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() { } 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): 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) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/common/CardDeserializer.kt b/tangem-core/src/main/java/com/tangem/commands/common/CardDeserializer.kt new file mode 100644 index 0000000000..ff29df7075 --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/common/CardDeserializer.kt @@ -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): 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) + ) + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/personalization/PersonalizeCommand.kt b/tangem-core/src/main/java/com/tangem/commands/personalization/PersonalizeCommand.kt index 52d85278fa..425d33180d 100644 --- a/tangem-core/src/main/java/com/tangem/commands/personalization/PersonalizeCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/personalization/PersonalizeCommand.kt @@ -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): 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 { diff --git a/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt b/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt index c05384d5a1..1b203b28fe 100644 --- a/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt +++ b/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt @@ -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 { - override val performPreflightRead = true + override val performPreflightRead = false override fun run(session: CardSession, callback: (result: CompletionResult) -> 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) -> 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)) } } } diff --git a/tangem-core/src/test/java/com/tangem/common/apdu/CommandApduTest.kt b/tangem-core/src/test/java/com/tangem/common/apdu/CommandApduTest.kt index f879f65e6f..c03069b6da 100644 --- a/tangem-core/src/test/java/com/tangem/common/apdu/CommandApduTest.kt +++ b/tangem-core/src/test/java/com/tangem/common/apdu/CommandApduTest.kt @@ -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(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(1) + val receiveChannel2 = BroadcastChannel(1) + + scope.launch { + channel.asFlow().collect { println(it) } + channel.asFlow().collect { println(it) } + } + scope.launch { channel.asFlow().collect { println(it) } } + + runBlocking { + delay(1000) + } + + + } } \ No newline at end of file diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/dto/PersonalizationConfig.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/dto/PersonalizationConfig.kt index a833de5d24..c3bdaf8c09 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/dto/PersonalizationConfig.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/dto/PersonalizationConfig.kt @@ -181,7 +181,7 @@ class PersonalizationConfig { signingMethodMaskBuilder.add(SigningMethod.SignRawValidateByIssuerWriteIssuerData) } if (from.SigningMethod6) { - signingMethodMaskBuilder.add(SigningMethod.SignHash) + signingMethodMaskBuilder.add(SigningMethod.SignPos) } return signingMethodMaskBuilder.build() } diff --git a/tangem-sdk/build.gradle b/tangem-sdk/build.gradle index de05139eb5..085db2391b 100644 --- a/tangem-sdk/build.gradle +++ b/tangem-sdk/build.gradle @@ -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' diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt index d8ca2ad645..31144eae96 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt @@ -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) -> 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() { diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/SessionViewDelegateState.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/SessionViewDelegateState.kt new file mode 100644 index 0000000000..04ecbd2557 --- /dev/null +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/SessionViewDelegateState.kt @@ -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) -> Unit) : SessionViewDelegateState() + object TagLost : SessionViewDelegateState() + object TagConnected : SessionViewDelegateState() + object WrongCard : SessionViewDelegateState() +} \ No newline at end of file diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/View.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/View.kt index 008f194709..277b729d24 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/View.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/View.kt @@ -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 + } + } \ No newline at end of file diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcManager.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcManager.kt index 8c2a865be4..860c8d5f55 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcManager.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcManager.kt @@ -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() { diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt index 559adf3f80..26ec450462 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt @@ -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() + 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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))) } } } diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/ui/NfcSessionDialog.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/ui/NfcSessionDialog.kt new file mode 100644 index 0000000000..4e7468f390 --- /dev/null +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/ui/NfcSessionDialog.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/tangem-sdk/src/main/res/drawable/pb_simple_circle.xml b/tangem-sdk/src/main/res/drawable/pb_simple_circle.xml new file mode 100644 index 0000000000..1ae7f2d76a --- /dev/null +++ b/tangem-sdk/src/main/res/drawable/pb_simple_circle.xml @@ -0,0 +1,21 @@ + + + + + + + + + + \ No newline at end of file diff --git a/tangem-sdk/src/main/res/layout/nfc_bottom_sheet.xml b/tangem-sdk/src/main/res/layout/nfc_bottom_sheet.xml index 33bf9ad0a3..55317c459a 100644 --- a/tangem-sdk/src/main/res/layout/nfc_bottom_sheet.xml +++ b/tangem-sdk/src/main/res/layout/nfc_bottom_sheet.xml @@ -157,9 +157,24 @@ android:progressDrawable="@drawable/pb_circle" android:progressTint="#FF9D30" android:secondaryProgress="100" /> - + + + + +