Skip to content

Commit

Permalink
AND-8497 improvements in enabling reader mode for nfc
Browse files Browse the repository at this point in the history
  • Loading branch information
kozarezvlad committed Oct 30, 2024
1 parent 8970394 commit 776a942
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 46 deletions.
17 changes: 6 additions & 11 deletions tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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) {
Expand All @@ -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)
}
Expand Down
76 changes: 52 additions & 24 deletions tangem-sdk-android/src/main/java/com/tangem/sdk/nfc/NfcReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand All @@ -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
}
}
}

Expand Down Expand Up @@ -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
Expand Down
44 changes: 33 additions & 11 deletions tangem-sdk-core/src/main/java/com/tangem/common/core/CardSession.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<ResponseApdu>) {
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 776a942

Please sign in to comment.