Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AND-8497 improvements in enabling reader mode for nfc #416

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading