From 05a1e9631b954298e3eda8f1f6c08160c8de64a0 Mon Sep 17 00:00:00 2001 From: Jovche Mitrejchevski Date: Fri, 17 Nov 2023 14:04:38 +0100 Subject: [PATCH] Replace ThreadExecutor with Coroutines (#416) --- .../wallet/transfer/TransferManager.kt | 135 ++++++++---------- .../viewmodel/TransferDocumentViewModel.kt | 77 +++++----- 2 files changed, 107 insertions(+), 105 deletions(-) diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt index 011cfd936..f57797695 100644 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt +++ b/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt @@ -5,8 +5,6 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.Color.BLACK import android.graphics.Color.WHITE -import android.os.Handler -import android.os.Looper import android.view.View import android.widget.ImageView import androidx.lifecycle.LiveData @@ -32,8 +30,9 @@ import com.google.zxing.BarcodeFormat import com.google.zxing.MultiFormatWriter import com.google.zxing.WriterException import com.google.zxing.common.BitMatrix +import kotlinx.coroutines.suspendCancellableCoroutine import java.util.* -import java.util.concurrent.Executors +import kotlin.coroutines.resume class TransferManager private constructor(private val context: Context) { @@ -159,90 +158,82 @@ class TransferManager private constructor(private val context: Context) { } @Throws(IllegalStateException::class) - fun addDocumentToResponse( + suspend fun addDocumentToResponse( credentialName: String, docType: String, issuerSignedEntriesToRequest: MutableMap>, deviceResponseGenerator: DeviceResponseGenerator, authKey: Credential.AuthenticationKey?, authKeyUnlockData: SecureArea.KeyUnlockData?, - onResultReady: (result: AddDocumentToResponseResult) -> Unit - ) { - // Putting together the response involves a bit of work and also potentially - // blocking I/O. Do all this on a worker thread. - Executors.newSingleThreadExecutor().execute(kotlinx.coroutines.Runnable { - var result: AddDocumentToResponseResult - var signingKeyUsageLimitPassed = false - val documentManager = DocumentManager.getInstance(context) - val documentInformation = documentManager.getDocumentInformation(credentialName) - requireValidProperty(documentInformation) { "Document not found!" } + ) = suspendCancellableCoroutine { continuation -> + var result: AddDocumentToResponseResult + var signingKeyUsageLimitPassed = false + val documentManager = DocumentManager.getInstance(context) + val documentInformation = documentManager.getDocumentInformation(credentialName) + requireValidProperty(documentInformation) { "Document not found!" } - val credential = requireNotNull(documentManager.getCredentialByName(credentialName)) - val dataElements = issuerSignedEntriesToRequest.keys.flatMap { key -> - issuerSignedEntriesToRequest.getOrDefault(key, emptyList()).map { value -> - CredentialRequest.DataElement(key, value, false) - } + val credential = requireNotNull(documentManager.getCredentialByName(credentialName)) + val dataElements = issuerSignedEntriesToRequest.keys.flatMap { key -> + issuerSignedEntriesToRequest.getOrDefault(key, emptyList()).map { value -> + CredentialRequest.DataElement(key, value, false) } + } - val request = CredentialRequest(dataElements) + val request = CredentialRequest(dataElements) - val authKeyToUse : Credential.AuthenticationKey - if (authKey != null) { - authKeyToUse = authKey - } else { - authKeyToUse = credential.findAuthenticationKey(Timestamp.now()) - ?: throw IllegalStateException("No auth key available") - } + val authKeyToUse: Credential.AuthenticationKey + if (authKey != null) { + authKeyToUse = authKey + } else { + authKeyToUse = credential.findAuthenticationKey(Timestamp.now()) + ?: throw IllegalStateException("No auth key available") + } - if (authKeyToUse.usageCount >= documentInformation.maxUsagesPerKey) { - logWarning("Using Auth Key previously used ${authKeyToUse.usageCount} times, and maxUsagesPerKey is ${documentInformation.maxUsagesPerKey}") - signingKeyUsageLimitPassed = true - } + if (authKeyToUse.usageCount >= documentInformation.maxUsagesPerKey) { + logWarning("Using Auth Key previously used ${authKeyToUse.usageCount} times, and maxUsagesPerKey is ${documentInformation.maxUsagesPerKey}") + signingKeyUsageLimitPassed = true + } - val staticAuthData = StaticAuthDataParser(authKeyToUse.issuerProvidedData).parse() - val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces( - request, - credential.applicationData.getNameSpacedData("credentialData"), - staticAuthData - ) + val staticAuthData = StaticAuthDataParser(authKeyToUse.issuerProvidedData).parse() + val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces( + request, + credential.applicationData.getNameSpacedData("credentialData"), + staticAuthData + ) - val transcript = communication.getSessionTranscript() ?: byteArrayOf() + val transcript = communication.getSessionTranscript() ?: byteArrayOf() - try { - val generator = DocumentGenerator(docType, staticAuthData.issuerAuth, transcript) - .setIssuerNamespaces(mergedIssuerNamespaces) - val keyInfo = authKeyToUse.secureArea.getKeyInfo(authKeyToUse.alias) - if ((keyInfo.keyPurposes and SecureArea.KEY_PURPOSE_SIGN) != 0) { - generator.setDeviceNamespacesSignature( - NameSpacedData.Builder().build(), - authKeyToUse.secureArea, - authKeyToUse.alias, - authKeyUnlockData, - SecureArea.ALGORITHM_ES256 - ) - } else { - generator.setDeviceNamespacesMac( - NameSpacedData.Builder().build(), - authKeyToUse.secureArea, - authKeyToUse.alias, - authKeyUnlockData, - communication.deviceRetrievalHelper!!.eReaderKey - ) - } - val data = generator.generate() - deviceResponseGenerator.addDocument(data) - log("Increasing usage count on ${authKeyToUse.alias}") - authKeyToUse.increaseUsageCount() - ProvisioningUtil.getInstance(context).trackUsageTimestamp(credential) - result = AddDocumentToResponseResult.DocumentAdded(signingKeyUsageLimitPassed) - } catch (lockedException: SecureArea.KeyLockedException) { - result = AddDocumentToResponseResult.DocumentLocked(authKeyToUse) - } - // Post the result on the UI thread - Handler(Looper.getMainLooper()).post { - onResultReady(result) + try { + val generator = DocumentGenerator(docType, staticAuthData.issuerAuth, transcript) + .setIssuerNamespaces(mergedIssuerNamespaces) + val keyInfo = authKeyToUse.secureArea.getKeyInfo(authKeyToUse.alias) + if ((keyInfo.keyPurposes and SecureArea.KEY_PURPOSE_SIGN) != 0) { + generator.setDeviceNamespacesSignature( + NameSpacedData.Builder().build(), + authKeyToUse.secureArea, + authKeyToUse.alias, + authKeyUnlockData, + SecureArea.ALGORITHM_ES256 + ) + } else { + generator.setDeviceNamespacesMac( + NameSpacedData.Builder().build(), + authKeyToUse.secureArea, + authKeyToUse.alias, + authKeyUnlockData, + communication.deviceRetrievalHelper!!.eReaderKey + ) } - }) + val data = generator.generate() + deviceResponseGenerator.addDocument(data) + log("Increasing usage count on ${authKeyToUse.alias}") + authKeyToUse.increaseUsageCount() + ProvisioningUtil.getInstance(context).trackUsageTimestamp(credential) + result = AddDocumentToResponseResult.DocumentAdded(signingKeyUsageLimitPassed) + } catch (lockedException: SecureArea.KeyLockedException) { + result = AddDocumentToResponseResult.DocumentLocked(authKeyToUse) + } + continuation.resume(result) } fun stopPresentation( diff --git a/appholder/src/main/java/com/android/identity/wallet/viewmodel/TransferDocumentViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/viewmodel/TransferDocumentViewModel.kt index b1d7257e0..85bfa3f4e 100644 --- a/appholder/src/main/java/com/android/identity/wallet/viewmodel/TransferDocumentViewModel.kt +++ b/appholder/src/main/java/com/android/identity/wallet/viewmodel/TransferDocumentViewModel.kt @@ -8,12 +8,13 @@ import androidx.databinding.ObservableInt import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.android.identity.util.Constants.DEVICE_RESPONSE_STATUS_OK +import androidx.lifecycle.viewModelScope import com.android.identity.android.legacy.CredentialInvalidatedException import com.android.identity.credential.Credential -import com.android.identity.mdoc.response.DeviceResponseGenerator import com.android.identity.mdoc.request.DeviceRequestParser +import com.android.identity.mdoc.response.DeviceResponseGenerator import com.android.identity.securearea.SecureArea +import com.android.identity.util.Constants.DEVICE_RESPONSE_STATUS_OK import com.android.identity.wallet.R import com.android.identity.wallet.authconfirmation.RequestedDocumentData import com.android.identity.wallet.authconfirmation.RequestedElement @@ -22,9 +23,12 @@ import com.android.identity.wallet.document.DocumentInformation import com.android.identity.wallet.document.DocumentManager import com.android.identity.wallet.transfer.AddDocumentToResponseResult import com.android.identity.wallet.transfer.TransferManager +import com.android.identity.wallet.util.PreferencesHelper import com.android.identity.wallet.util.TransferStatus import com.android.identity.wallet.util.logWarning -import com.android.identity.wallet.util.PreferencesHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) { @@ -105,37 +109,44 @@ class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) { ) { val elementsToSend = signedElements.collect() val responseGenerator = DeviceResponseGenerator(DEVICE_RESPONSE_STATUS_OK) - elementsToSend.forEach { signedDocument -> - try { - val issuerSignedEntries = signedDocument.issuerSignedEntries() - transferManager.addDocumentToResponse( - signedDocument.identityCredentialName, - signedDocument.documentType, - issuerSignedEntries, - responseGenerator, - authKey, - authKeyUnlockData, - onResultReady = { - if (it !is AddDocumentToResponseResult.DocumentAdded) { - onResultReady(it) - return@addDocumentToResponse - } - transferManager.sendResponse(responseGenerator.generate(), PreferencesHelper.isConnectionAutoCloseEnabled()) - transferManager.setResponseServed() - val documentsCount = elementsToSend.count() - documentsSent.set(app.getString(R.string.txt_documents_sent, documentsCount)) - cleanUp() - onResultReady(it) + viewModelScope.launch { + elementsToSend.forEach { signedDocument -> + try { + val issuerSignedEntries = signedDocument.issuerSignedEntries() + val result = withContext(Dispatchers.IO) { //<- Offload from UI thread + transferManager.addDocumentToResponse( + signedDocument.identityCredentialName, + signedDocument.documentType, + issuerSignedEntries, + responseGenerator, + authKey, + authKeyUnlockData, + ) } - ) - } catch (e: CredentialInvalidatedException) { - logWarning("Credential '${signedDocument.identityCredentialName}' is invalid. Deleting.") - documentManager.deleteCredentialByName(signedDocument.identityCredentialName) - Toast.makeText(app.applicationContext, "Deleting invalid credential " - + signedDocument.identityCredentialName, - Toast.LENGTH_SHORT).show() - } catch (e: NoSuchElementException) { - logWarning("No requestedDocument for " + signedDocument.documentType) + if (result !is AddDocumentToResponseResult.DocumentAdded) { + onResultReady(result) + return@forEach + } + transferManager.sendResponse( + responseGenerator.generate(), + PreferencesHelper.isConnectionAutoCloseEnabled() + ) + transferManager.setResponseServed() + val documentsCount = elementsToSend.count() + documentsSent.set(app.getString(R.string.txt_documents_sent, documentsCount)) + cleanUp() + onResultReady(result) + } catch (e: CredentialInvalidatedException) { + logWarning("Credential '${signedDocument.identityCredentialName}' is invalid. Deleting.") + documentManager.deleteCredentialByName(signedDocument.identityCredentialName) + Toast.makeText( + app.applicationContext, "Deleting invalid credential " + + signedDocument.identityCredentialName, + Toast.LENGTH_SHORT + ).show() + } catch (e: NoSuchElementException) { + logWarning("No requestedDocument for " + signedDocument.documentType) + } } } }