From cd1c59dca491600c919a14536d6f2584e97c107f Mon Sep 17 00:00:00 2001 From: Kevin Deus Date: Tue, 19 Nov 2024 15:31:26 -0800 Subject: [PATCH] Replace ScanQrDialog with the KMM ScanQrCodeDialog. There were significant layout problems with the old QR code reader: the camera image was often misaligned, the borders were offset, the image could be squished, and the misalignment was inconsistent. The new QR code scanner looks much better behaved, though we lose the brackets that used to be there. Tested by: - Manual testing of both issuance and identity verification. - ./gradlew check - ./gradlew connectedCheck Signed-off-by: Kevin Deus --- .../addtowallet/AddToWalletScreen.kt | 23 +-- .../ui/destination/reader/ReaderScreen.kt | 13 +- .../wallet/ui/qrscanner/ScanQrDialog.kt | 173 ------------------ 3 files changed, 18 insertions(+), 191 deletions(-) delete mode 100644 wallet/src/main/java/com/android/identity_credential/wallet/ui/qrscanner/ScanQrDialog.kt diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/addtowallet/AddToWalletScreen.kt b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/addtowallet/AddToWalletScreen.kt index c23818016..dfdf59ee0 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/addtowallet/AddToWalletScreen.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/addtowallet/AddToWalletScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.android.identity.appsupport.ui.qrcode.ScanQrCodeDialog import com.android.identity.issuance.IssuingAuthorityConfiguration import com.android.identity.issuance.remote.WalletServerProvider import com.android.identity.util.Logger @@ -38,7 +39,6 @@ import com.android.identity_credential.wallet.WalletApplication import com.android.identity_credential.wallet.credentialoffer.extractCredentialIssuerData import com.android.identity_credential.wallet.navigation.WalletDestination import com.android.identity_credential.wallet.ui.ScreenWithAppBarAndBackButton -import com.android.identity_credential.wallet.ui.qrscanner.ScanQrDialog import com.android.identity_credential.wallet.util.getUrlQueryFromCustomSchemeUrl import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -100,13 +100,6 @@ fun AddToWalletScreen( // to navigate after scanning a Qr code navigateToOnComposable.value?.let { route -> onNavigate(route) } - /** - * Helper function for hiding [ScanQrDialog] from current view. - */ - fun dismissScanQrDialog() { - showQrScannerDialog.value = false - } - ScreenWithAppBarAndBackButton( title = stringResource(R.string.add_screen_title), onBackButtonClick = { onNavigate(WalletDestination.PopBackStack.route) } @@ -121,13 +114,10 @@ fun AddToWalletScreen( } else { // compose ScanQrDialog when user taps on "Scan Credential Offer" if (showQrScannerDialog.value) { - ScanQrDialog( - modifier = Modifier - .fillMaxWidth(0.9f) - .fillMaxHeight(0.6f), + ScanQrCodeDialog( title = stringResource(R.string.credential_offer_scan), description = stringResource(id = R.string.credential_offer_details), - onScannedQrCode = { qrCodeTextUrl -> + onCodeScanned = { qrCodeTextUrl -> // filter only for OID4VCI Url schemes. if (qrCodeTextUrl.startsWith(WalletApplication.OID4VCI_CREDENTIAL_OFFER_URL_SCHEME)) { // scanned text is expected to be an encoded Url @@ -145,8 +135,13 @@ fun AddToWalletScreen( } } } + true }, - onClose = { dismissScanQrDialog() } + dismissButton = stringResource(R.string.reader_screen_scan_qr_dialog_dismiss_button), + onDismiss = { showQrScannerDialog.value = false }, + modifier = Modifier + .fillMaxWidth()//0.9f) + .fillMaxHeight(0.6f), ) } else { // not showing [ScanQrDialog] AddToWalletScreenWithIssuerDisplayDatas( diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/reader/ReaderScreen.kt b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/reader/ReaderScreen.kt index 9ded9e389..4b002b2dd 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/reader/ReaderScreen.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/reader/ReaderScreen.kt @@ -63,6 +63,7 @@ import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import com.android.identity.appsupport.ui.qrcode.ScanQrCodeDialog import com.android.identity.cbor.Cbor import com.android.identity.cbor.DiagnosticOption import com.android.identity.documenttype.DocumentTypeRepository @@ -81,7 +82,6 @@ import com.android.identity_credential.wallet.navigation.WalletDestination import com.android.identity_credential.wallet.ui.KeyValuePairHtml import com.android.identity_credential.wallet.ui.KeyValuePairText import com.android.identity_credential.wallet.ui.ScreenWithAppBarAndBackButton -import com.android.identity_credential.wallet.ui.qrscanner.ScanQrDialog import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import kotlinx.datetime.Instant @@ -173,10 +173,15 @@ private fun WaitForEngagement( var dropdownSelected = remember { mutableStateOf(availableRequests[0]) } if (showQrScannerDialog.value) { - ScanQrDialog(title = stringResource(R.string.reader_screen_scan_qr_dialog_title), + ScanQrCodeDialog( + title = stringResource(R.string.reader_screen_scan_qr_dialog_title), description = stringResource(R.string.reader_screen_scan_qr_dialog_text), - onScannedQrCode = { qrCodeText -> model.setQrCode(qrCodeText) }, - onClose = { showQrScannerDialog.value = false } + onCodeScanned = { qrCodeText -> + model.setQrCode(qrCodeText) + true + }, + dismissButton = stringResource(R.string.reader_screen_scan_qr_dialog_dismiss_button), + onDismiss = { showQrScannerDialog.value = false }, ) } diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/ui/qrscanner/ScanQrDialog.kt b/wallet/src/main/java/com/android/identity_credential/wallet/ui/qrscanner/ScanQrDialog.kt deleted file mode 100644 index e055ff411..000000000 --- a/wallet/src/main/java/com/android/identity_credential/wallet/ui/qrscanner/ScanQrDialog.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.android.identity_credential.wallet.ui.qrscanner - -import android.Manifest -import android.view.ViewGroup -import android.widget.LinearLayout -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.QrCode -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.window.DialogProperties -import com.android.identity.util.Logger -import com.android.identity_credential.wallet.R -import com.budiyev.android.codescanner.CodeScanner -import com.budiyev.android.codescanner.CodeScannerView -import com.budiyev.android.codescanner.DecodeCallback -import com.budiyev.android.codescanner.ErrorCallback -import com.budiyev.android.codescanner.ScanMode -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState - -const val TAG = "ScanQrDialog" - -/** - * Composable Dialog that shows a [title], [description], Qr scanner/camera surface, and close button. - * Issues callbacks to [onClose] for dismiss requests (from dialog or close button). When a Qr code - * is successfully scanned, the callback [onScannedQrCode] is invoked with the decoded text. - * - * A [modifier] can be passed for setting the Dialog's dimensions - */ -@Composable -fun ScanQrDialog( - title: String, - description: String, - onClose: () -> Unit, - onScannedQrCode: (String) -> Unit, - modifier: Modifier? = null -) { - // if provided, use the passed-in modifier and set dialog properties that allow for adjustment of dimensions - val (dialogModier: Modifier, dialogProperties: DialogProperties) = - if (modifier != null) { - Pair( - modifier, DialogProperties( - usePlatformDefaultWidth = false, - decorFitsSystemWindows = false - ) - ) - } else { - Pair(Modifier, DialogProperties()) - } - - AlertDialog( - modifier = dialogModier, - properties = dialogProperties, - icon = { - Icon( - Icons.Filled.QrCode, - contentDescription = stringResource(R.string.reader_screen_qr_icon_content_description) - ) - }, - title = { Text(text = title) }, - text = { QrScanner(description, onScannedQrCode) }, - onDismissRequest = { onClose.invoke() }, - confirmButton = {}, - dismissButton = { - TextButton( - onClick = { - onClose.invoke() - } - ) { - Text(stringResource(R.string.reader_screen_scan_qr_dialog_dismiss_button)) - } - } - ) -} - -@OptIn(ExperimentalPermissionsApi::class) -@Composable -private fun QrScanner( - description: String, - onScannedQrCode: (String) -> Unit, -) { - val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA) - if (!cameraPermissionState.status.isGranted) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - modifier = Modifier.padding(20.dp), - text = stringResource(R.string.reader_screen_scan_qr_dialog_missing_permission_text) - ) - Button( - onClick = { - cameraPermissionState.launchPermissionRequest() - } - ) { - Text(stringResource(R.string.reader_screen_scan_qr_dialog_request_permission_button)) - } - } - } - } else { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - modifier = Modifier.padding(8.dp), - text = description - ) - Row( - modifier = Modifier - .width(300.dp) - .height(300.dp), - horizontalArrangement = Arrangement.Center - ) { - AndroidView( - modifier = Modifier - .fillMaxSize() - .padding(32.dp), - factory = { context -> - CodeScannerView(context).apply { - val codeScanner = CodeScanner(context, this).apply { - layoutParams = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - isAutoFocusEnabled = true - isAutoFocusButtonVisible = false - scanMode = ScanMode.SINGLE - decodeCallback = DecodeCallback { result -> - releaseResources() - onScannedQrCode.invoke(result.text) - } - errorCallback = ErrorCallback { error -> - Logger.w(TAG, "Error scanning QR", error) - releaseResources() - } - camera = CodeScanner.CAMERA_BACK - isFlashEnabled = false - } - codeScanner.startPreview() - } - }, - ) - } - } - } - } -} \ No newline at end of file