From 776a94282fc6a07a99a13076a884d4737e293926 Mon Sep 17 00:00:00 2001 From: Vlad Kozarez Date: Wed, 30 Oct 2024 17:34:32 +0300 Subject: [PATCH] AND-8497 improvements in enabling reader mode for nfc --- .../java/com/tangem/sdk/nfc/NfcManager.kt | 17 ++--- .../main/java/com/tangem/sdk/nfc/NfcReader.kt | 76 +++++++++++++------ .../com/tangem/common/core/CardSession.kt | 44 ++++++++--- 3 files changed, 91 insertions(+), 46 deletions(-) diff --git a/tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcManager.kt b/tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcManager.kt index 446b26b6..165a33de 100644 --- a/tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcManager.kt +++ b/tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcManager.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.LifecycleOwner import com.tangem.Log import com.tangem.common.extensions.VoidCallback import com.tangem.common.nfc.ReadingActiveListener +import kotlinx.coroutines.plus /** * Helps use NFC, leveraging Android NFC functionality. @@ -25,12 +26,6 @@ class NfcManager : NfcAdapter.ReaderCallback, ReadingActiveListener, DefaultLife override var readingIsActive: Boolean = false set(value) { Log.nfc { "set readingIsActive $value" } - if (value) { - disableReaderMode() - // delay before enableReaderMode because some devices catch ANR or smth without it - Thread.sleep(200) - enableReaderMode() - } field = value } @@ -77,16 +72,16 @@ class NfcManager : NfcAdapter.ReaderCallback, ReadingActiveListener, DefaultLife super.onCreate(owner) val filter = IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED) activity?.registerReceiver(mBroadcastReceiver, filter) - enableReaderModeIfEnabled() } override fun onStart(owner: LifecycleOwner) { + enableReaderModeIfNfcEnabled() reader.listener = this } override fun onStop(owner: LifecycleOwner) { - reader.stopSession(true) disableReaderMode() + reader.stopSession(true) reader.listener = null } @@ -96,7 +91,7 @@ class NfcManager : NfcAdapter.ReaderCallback, ReadingActiveListener, DefaultLife nfcAdapter = null } - private fun enableReaderModeIfEnabled() { + private fun enableReaderModeIfNfcEnabled() { val nfcEnabled = nfcAdapter?.isEnabled == true if (nfcEnabled) { @@ -105,14 +100,14 @@ class NfcManager : NfcAdapter.ReaderCallback, ReadingActiveListener, DefaultLife } private fun enableReaderMode() { - Log.nfc { "enableReaderMode $nfcAdapter" } + Log.nfc { "enableReaderMode" } if (activity?.isDestroyed == false) { nfcAdapter?.enableReaderMode(activity, this, READER_FLAGS, Bundle()) } } private fun disableReaderMode() { - Log.nfc { "disableReaderMode $nfcAdapter" } + Log.nfc { "disableReaderMode" } if (activity?.isDestroyed == false) { nfcAdapter?.disableReaderMode(activity) } diff --git a/tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcReader.kt b/tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcReader.kt index 98a2f9cc..1105785e 100644 --- a/tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcReader.kt +++ b/tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcReader.kt @@ -17,9 +17,12 @@ import com.tangem.common.nfc.ReadingActiveListener import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.ConflatedBroadcastChannel +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -import java.util.concurrent.CancellationException +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume data class NfcTag(val type: TagType, val isoDep: IsoDep?, val nfcV: NfcV? = null) @@ -33,46 +36,55 @@ class NfcReader : CardReader { var listener: ReadingActiveListener? = null + private val readerMutex = Mutex() private var nfcTag: NfcTag? = null set(value) { field = value Log.nfc { "received tag: ${value?.type?.name?.uppercase()}" } - scope?.launch { tag.send(value?.type) } + scope?.launchWithLock(readerMutex) { tag.send(value?.type) } } override fun startSession() { - Log.nfc { "start NFC session" } - nfcTag = null - listener?.readingIsActive = true + scope?.launchWithLock(readerMutex) { + Log.nfc { "start NFC session, thread ${Thread.currentThread().id}" } + nfcTag = null + listener?.readingIsActive = true + } } override fun pauseSession() { - Log.nfc { "pause NFC session" } - listener?.readingIsActive = false + scope?.launchWithLock(readerMutex) { + Log.nfc { "pause NFC session, thread ${Thread.currentThread().id}" } + listener?.readingIsActive = false + } } override fun resumeSession() { - Log.nfc { "resume NFC session" } - listener?.readingIsActive = true + scope?.launchWithLock(readerMutex) { + Log.nfc { "resume NFC session, thread ${Thread.currentThread().id}" } + listener?.readingIsActive = true + } } fun onTagDiscovered(tag: Tag?) { - 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) + scope?.launchWithLock(readerMutex) { + NfcV.get(tag)?.let { + nfcTag = NfcTag(TagType.Slix, null, NfcV.get(tag)) + return@launchWithLock + } + IsoDep.get(tag)?.let { isoDep -> + connect(isoDep) + nfcTag = NfcTag(TagType.Nfc, isoDep) + } } } - private fun connect(isoDep: IsoDep) { + private suspend fun connect(isoDep: IsoDep) { Log.nfc { "connect" } if (isoDep.isConnected) { Log.nfc { "already connected close and reconnect" } isoDep.closeInternal() - Thread.sleep(CONNECTION_DELAY) + delay(CONNECTION_DELAY) isoDep.connectInternal() } else { isoDep.connectInternal() @@ -82,12 +94,20 @@ class NfcReader : CardReader { } override fun stopSession(cancelled: Boolean) { - Log.nfc { "stop NFC session" } - listener?.readingIsActive = false - if (cancelled) { - scope?.cancel(CancellationException(TangemSdkError.UserCancelled().customMessage)) - } else { - nfcTag = null + scope?.launchWithLock(readerMutex) { + Log.nfc { "stop NFC session, thread ${Thread.currentThread().id}" } + listener?.readingIsActive = false + if (cancelled) { + val userCancelledException = TangemSdkError.UserCancelled() + scope?.cancel( + CancellationException( + message = userCancelledException.customMessage, + cause = userCancelledException, + ), + ) + } else { + nfcTag = null + } } } @@ -179,6 +199,14 @@ class NfcReader : CardReader { } } + private fun CoroutineScope.launchWithLock(mutex: Mutex, action: suspend () -> Unit) { + this.launch { + mutex.withLock(null) { + action() + } + } + } + private companion object { const val ISO_DEP_TIMEOUT_MS = 240_000 const val CONNECTION_DELAY = 100L diff --git a/tangem-sdk-core/src/main/java/com/tangem/common/core/CardSession.kt b/tangem-sdk-core/src/main/java/com/tangem/common/core/CardSession.kt index 8b5e2001..8d4e933b 100644 --- a/tangem-sdk-core/src/main/java/com/tangem/common/core/CardSession.kt +++ b/tangem-sdk-core/src/main/java/com/tangem/common/core/CardSession.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.asFlow @@ -49,6 +50,7 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.take +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.plus import java.io.PrintWriter @@ -89,12 +91,8 @@ class CardSession( private var resetCodesController: ResetCodesController? = null - val scope = CoroutineScope(Dispatchers.IO) + CoroutineExceptionHandler { _, throwable -> - val sw = StringWriter() - throwable.printStackTrace(PrintWriter(sw)) - val exceptionAsString: String = sw.toString() - Log.error { exceptionAsString } - throw throwable + val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + CoroutineExceptionHandler { _, throwable -> + handleScopeCoroutineException(throwable) } private var preflightReadMode: PreflightReadMode = PreflightReadMode.FullCardRead @@ -217,10 +215,12 @@ class CardSession( scope.launch { reader.tag.asFlow() .onCompletion { - if (it is CancellationException && it.message == TangemSdkError.UserCancelled().customMessage) { - stopWithError(TangemSdkError.UserCancelled(), SessionErrorMoment.CancellingByUser) + val exception = it as? CancellationException ?: return@onCompletion + val cause = exception.cause ?: return@onCompletion + if (cause is TangemSdkError.UserCancelled) { + stopWithError(cause, SessionErrorMoment.CancellingByUser) viewDelegate.dismiss() - onSessionStarted(this@CardSession, TangemSdkError.UserCancelled()) + onSessionStarted(this@CardSession, cause) } } .collect { @@ -381,11 +381,17 @@ class CardSession( if (state == CardSessionState.Inactive) return Log.session { "stop session" } + onStopSessionFinalize() + reader.stopSession() + if (scope.isActive) { + scope.cancel() + } + } + + private fun onStopSessionFinalize() { state = CardSessionState.Inactive preflightReadMode = PreflightReadMode.FullCardRead saveUserCodeIfNeeded() - reader.stopSession() - scope.cancel() } fun send(apdu: CommandApdu, callback: CompletionCallback) { @@ -561,6 +567,22 @@ class CardSession( } } + private fun handleScopeCoroutineException(throwable: Throwable) { + when (throwable) { + is TangemSdkError.UserCancelled -> { + onStopSessionFinalize() + } + + else -> { + val sw = StringWriter() + throwable.printStackTrace(PrintWriter(sw)) + val exceptionAsString: String = sw.toString() + Log.error { exceptionAsString } + throw throwable + } + } + } + private fun saveUserCodeIfNeeded() { val saveCodeAndLock: suspend (String, UserCode) -> Unit = { cardId, code -> userCodeRepository?.save(cardId, code)