diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestDetailedMessage.kt b/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestDetailedMessage.kt new file mode 100644 index 000000000..1ce184602 --- /dev/null +++ b/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestDetailedMessage.kt @@ -0,0 +1,26 @@ +package com.android.identity.issuance.evidence + +import kotlinx.io.bytestring.ByteString + +/** + * Detailed rich-text message building on top of [EvidenceRequestMessage] that additionally contains + * a [messageTitle] and a [messageType] that is used to identify the classification of the detailed + * message (such as "completed", "info", "confirmation_XYZ") that allows showing tailored UI + * pertaining to the message. + * + * [messageType] identifier used for classifying message types (success, error, info, ..) to show + * the appropriate UI pertaining to the detailed message. + * [messageTitle] title to show above the message + * [message] message formatted as markdown + * [assets] images that can be referenced in markdown, type (PNG, JPEG, or SVG) determined by the extension + * [acceptButtonText] button label to continue to the next screen + * [rejectButtonText] optional button label to continue without accepting + */ +data class EvidenceRequestDetailedMessage( + val messageType: String, + val messageTitle: String, + val message: String, + val assets: Map, + val acceptButtonText: String, + val rejectButtonText: String?, +) : EvidenceRequest() \ No newline at end of file diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestMessage.kt b/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestMessage.kt index e02c917a0..91e580f27 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestMessage.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestMessage.kt @@ -9,16 +9,10 @@ import kotlinx.io.bytestring.ByteString * [assets] images that can be referenced in markdown, type (PNG, JPEG, or SVG) determined by the extension * [acceptButtonText] button label to continue to the next screen * [rejectButtonText] optional button label to continue without accepting - * [messageType] optional identifier used for classifying message types (success, error, info, ..) - * for showing a more appropriate UI than a standard basic message - * [messageTitle] optional title to show above the message for a non-standard or non-basic message - * that is filtered/classified via the [messageType] */ data class EvidenceRequestMessage( val message: String, val assets: Map, val acceptButtonText: String, val rejectButtonText: String?, - val messageType: String? = null, - val messageTitle: String? = null ) : EvidenceRequest() \ No newline at end of file diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/proofing/ProofingGraphBuilder.kt b/identity-issuance/src/main/java/com/android/identity/issuance/proofing/ProofingGraphBuilder.kt index 90ea878c2..f2b878925 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/proofing/ProofingGraphBuilder.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/proofing/ProofingGraphBuilder.kt @@ -1,6 +1,7 @@ package com.android.identity.issuance.proofing import com.android.identity.issuance.evidence.EvidenceRequestCreatePassphrase +import com.android.identity.issuance.evidence.EvidenceRequestDetailedMessage import com.android.identity.issuance.evidence.EvidenceRequestGermanEid import com.android.identity.issuance.evidence.EvidenceRequestIcaoPassiveAuthentication import com.android.identity.issuance.evidence.EvidenceRequestMessage @@ -27,23 +28,31 @@ class ProofingGraphBuilder { private val chain = mutableListOf<(Node?) -> Node>() /** Sends [EvidenceRequestMessage]. */ - fun message( + fun message(id: String, message: String, assets: Map, + acceptButtonText: String, rejectButtonText: String?) { + val evidenceRequest = EvidenceRequestMessage(message, assets, acceptButtonText, rejectButtonText) + chain.add { followUp -> ProofingGraph.SimpleNode(id, followUp, evidenceRequest) } + } + + /** Sends [EvidenceRequestMessage]. */ + fun detailedMessage( id: String, + messageType: String, + messageTitle: String, message: String, assets: Map, acceptButtonText: String, rejectButtonText: String?, - messageType: String? = null, - messageTitle: String? = null + ) { val evidenceRequest = - EvidenceRequestMessage( + EvidenceRequestDetailedMessage( + messageType, + messageTitle, message, assets, acceptButtonText, rejectButtonText, - messageType, - messageTitle, ) chain.add { followUp -> ProofingGraph.SimpleNode(id, followUp, evidenceRequest) } } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt b/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt index 8293898cc..e1fd2b76f 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt @@ -199,18 +199,13 @@ fun defaultGraph( } } } - message( - id = "message", - message = resources.getStringResource("R.string.evidence_request_complete_info") ?: """ - Your application is about to be sent the ID issuer for verification. You will - get notified when the application is approved. - """.trimIndent(), + detailedMessage( + id = "detailedMessage", messageType = "evidence_request_complete", - messageTitle = resources.getStringResource("R.string.evidence_request_complete_title") - ?: "Submitted for Verification", + messageTitle = "Document Scanning Complete", + message = "Your application is now under ID issuer verification. This process might take a few hours.", assets = mapOf(), - acceptButtonText = resources.getStringResource("R.string.evidence_request_complete_done_button") - ?: "Continue", + acceptButtonText = "Done", rejectButtonText = null ) requestNotificationPermission( diff --git a/wallet/src/main/java/com/android/identity/issuance/remote/LocalDevelopmentEnvironment.kt b/wallet/src/main/java/com/android/identity/issuance/remote/LocalDevelopmentEnvironment.kt index 6721270ce..94366ebf1 100644 --- a/wallet/src/main/java/com/android/identity/issuance/remote/LocalDevelopmentEnvironment.kt +++ b/wallet/src/main/java/com/android/identity/issuance/remote/LocalDevelopmentEnvironment.kt @@ -5,11 +5,11 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.os.Handler import androidx.annotation.RawRes -import com.android.identity.flow.handler.FlowNotifications import com.android.identity.flow.server.Configuration -import com.android.identity.flow.server.FlowEnvironment import com.android.identity.flow.server.Resources import com.android.identity.flow.server.Storage +import com.android.identity.flow.server.FlowEnvironment +import com.android.identity.flow.handler.FlowNotifications import com.android.identity.issuance.ApplicationSupport import com.android.identity.securearea.SecureArea import com.android.identity_credential.wallet.R @@ -199,28 +199,7 @@ internal class LocalDevelopmentEnvironment( context.resources.getString(R.string.utopia_local_issuing_authority_photoid_tos) "funke/tos.html" -> context.resources.getString(R.string.funke_issuing_authority_tos) - else -> { - // String passthrough allowing upstream code to reference downstream Strings - // there's no need to hard-code/define every possible String in here - if (name.startsWith("R.string.")){ - val stringName = name.split("R.string.")[1] - /** - * Localized function resolving a String resource ID from a String's name. - * Note: This approach uses reflection under the hood and is less efficient - * than retrieving a String directly by its identifier. If this ends - * up being a bottleneck we can change the approach of how downstream - * Strings/resources are shared upstream. - */ - fun getStringResId(stringName: String): Int { - return context.resources.getIdentifier(stringName, "string", context.packageName) - } - // return the requested String's value - context.resources.getString(getStringResId(stringName)) - } else { - // unknown raw file name or String resource, return null - null - } - } + else -> null } } diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/navigation/WalletNavigation.kt b/wallet/src/main/java/com/android/identity_credential/wallet/navigation/WalletNavigation.kt index f59932699..2fbcace04 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/navigation/WalletNavigation.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/navigation/WalletNavigation.kt @@ -189,7 +189,8 @@ fun WalletNavigation( onNavigate = onNavigate, permissionTracker = permissionTracker, walletServerProvider = application.walletServerProvider, - documentStore = application.documentStore + documentStore = application.documentStore, + developerMode = application.settingsModel.developerModeEnabled.value ?: false ) } diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/EvidenceRequest.kt b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/EvidenceRequest.kt index fd21b5c73..abde2ee5e 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/EvidenceRequest.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/EvidenceRequest.kt @@ -18,6 +18,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -72,6 +73,7 @@ import com.android.identity.document.DocumentStore import com.android.identity.issuance.ApplicationSupport import com.android.identity.issuance.LandingUrlUnknownException import com.android.identity.issuance.evidence.EvidenceRequestCreatePassphrase +import com.android.identity.issuance.evidence.EvidenceRequestDetailedMessage import com.android.identity.issuance.evidence.EvidenceRequestGermanEid import com.android.identity.issuance.evidence.EvidenceRequestIcaoNfcTunnel import com.android.identity.issuance.evidence.EvidenceRequestIcaoPassiveAuthentication @@ -102,6 +104,7 @@ import com.android.identity_credential.wallet.ProvisioningViewModel import com.android.identity_credential.wallet.R import com.android.identity_credential.wallet.ui.RichTextSnippet import com.android.identity_credential.wallet.ui.SelfieRecorder +import com.android.identity_credential.wallet.util.inverse import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState @@ -163,12 +166,84 @@ fun EvidenceRequestMessageView( } /** - * Show the "Document Scanning Complete" screen at the end of the Evidence-Request gathering steps - * for Provisioning a Personal ID. + * Show a generic "detailed message" screen whose [detailedMessage.messageType] is not handled. + * Composes a screen containing the "info" icon, message title, message text and the accept button. + */ +@Composable +fun EvidenceRequestGenericDetailedMessageScreen( + detailedMessage: EvidenceRequestDetailedMessage, + provisioningViewModel: ProvisioningViewModel, + walletServerProvider: WalletServerProvider, + documentStore: DocumentStore +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 10.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 100.dp), + horizontalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = android.R.drawable.ic_dialog_info), + modifier = Modifier.size(80.dp), + contentDescription = stringResource( + R.string.accessibility_artwork_for, + detailedMessage.messageTitle + ) + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = detailedMessage.messageTitle, + modifier = Modifier.padding(16.dp), + textAlign = TextAlign.Left, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.headlineLarge + ) + } + Row( + modifier = Modifier.fillMaxWidth().padding(bottom = 200.dp), + horizontalArrangement = Arrangement.Absolute.Left + ) { + Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp), + text = detailedMessage.message, + style = MaterialTheme.typography.bodyLarge + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Button( + modifier = Modifier.padding(8.dp), + onClick = { + provisioningViewModel.provideEvidence( + evidence = EvidenceResponseMessage(true), + walletServerProvider = walletServerProvider, + documentStore = documentStore + ) + }) { + Text(detailedMessage.acceptButtonText) + } + } + } +} + +/** + * Show the "Document Scanning Complete" screen after completing all steps of for evidence gathering + * of the "Provisioning a Personal ID" flow. */ @Composable fun EvidenceRequestCompleteScreen( - evidenceRequest: EvidenceRequestMessage, + evidenceRequest: EvidenceRequestDetailedMessage, provisioningViewModel: ProvisioningViewModel, walletServerProvider: WalletServerProvider, documentStore: DocumentStore @@ -749,7 +824,8 @@ fun EvidenceRequestIcaoPassiveAuthenticationView( fun EvidenceRequestIcaoNfcTunnelView( evidenceRequest: EvidenceRequestIcaoNfcTunnel, provisioningViewModel: ProvisioningViewModel, - permissionTracker: PermissionTracker + permissionTracker: PermissionTracker, + developerMode: Boolean = false ) { var showSuccessfulScanningScreen by remember { mutableStateOf(false) } val tunnelScanner = NfcTunnelScanner(provisioningViewModel) @@ -764,17 +840,26 @@ fun EvidenceRequestIcaoNfcTunnelView( IcaoMrtdCommunicationModel.Route.CAMERA_SCAN } - // show the Evidence Request Camera/NFC Screen - if (!showSuccessfulScanningScreen) { + // if developer mode not enabled, simply show the MRZ or NFC screen without briefly showing + // transitional success screen after successfully scanning an NFC document + if (!developerMode){ EvidenceRequestIcaoView(tunnelScanner, permissionTracker, initialRoute) { - // upon completion of nfc scanning, show a transitional "Success" screen - showSuccessfulScanningScreen = true - } - } else { - // briefly show a transitional success screen then finish the tunneling process - DocumentSuccessfullyScannedScreen(pauseDurationMs = 1000L) { + // upon completion of nfc scanning, finish the tunneling process provisioningViewModel.finishTunnel() } + } else { // developer mode is enabled + // if user has not successfully scanned an NFC document, show the MRZ or NFC screen + if (!showSuccessfulScanningScreen) { + EvidenceRequestIcaoView(tunnelScanner, permissionTracker, initialRoute) { + // upon completion of nfc scanning, show a transitional "Success" screen + showSuccessfulScanningScreen = true + } + } else { // user successfully scanned an NFC document + // briefly (1 sec) show a transitional success screen then finish the tunneling process + DocumentSuccessfullyScannedScreen(pauseDurationMs = 1000L) { + provisioningViewModel.finishTunnel() + } + } } } @@ -904,7 +989,7 @@ fun EvidenceRequestIcaoView( AndroidView( modifier = Modifier .fillMaxSize() - .padding(16.dp) + .padding(bottom = 16.dp) .clip(RoundedCornerShape(32.dp)) .background( MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f) @@ -913,7 +998,7 @@ fun EvidenceRequestIcaoView( PreviewView(context).apply { layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - 100 + ViewGroup.LayoutParams.MATCH_PARENT ) scaleType = PreviewView.ScaleType.FILL_CENTER implementationMode = @@ -986,6 +1071,8 @@ fun EvidenceRequestIcaoView( style = MaterialTheme.typography.bodyLarge, fontFamily = FontFamily.SansSerif, text = when (icaoStatusValue) { + // initially don't show a message while waiting to detect an NFC document + is MrtdNfc.Initial -> "" is MrtdNfc.Connected -> stringResource(R.string.nfc_status_connected) is MrtdNfc.AttemptingPACE -> stringResource(R.string.nfc_status_attempting_pace) is MrtdNfc.PACESucceeded -> stringResource(R.string.nfc_status_pace_succeeded) @@ -999,10 +1086,7 @@ fun EvidenceRequestIcaoView( icaoStatusValue.progressPercent ) is MrtdNfc.TunnelAuthenticating -> - stringResource( - R.string.nfc_status_tunnel_authenticating, - icaoStatusValue.progressPercent - ) + stringResource(R.string.nfc_status_tunnel_authenticating) is MrtdNfc.TunnelReading -> stringResource( R.string.nfc_status_tunnel_reading_data, @@ -1030,7 +1114,6 @@ fun EvidenceRequestIcaoView( modifier = Modifier .size(48.dp) .padding(start = 16.dp) - .clip(RoundedCornerShape(32.dp)) ) Text( modifier = Modifier.padding(start = 16.dp, end = 16.dp), @@ -1104,6 +1187,12 @@ fun NfcHeartbeatAnimation(nfcAnimationStatus: NfcAnimationStatus) { repeatMode = RepeatMode.Restart ), label = "alpha animation" ) + val tintColorThemed = if (!isSystemInDarkTheme()) { + colorResource(id = R.color.contactless_blue) + } else { + colorResource(id = R.color.contactless_blue).inverse() + } + // start composing the "nfc heartbeat" animation Box( contentAlignment = Alignment.Center, @@ -1115,7 +1204,7 @@ fun NfcHeartbeatAnimation(nfcAnimationStatus: NfcAnimationStatus) { .size(iconSize) // heartbeat size .graphicsLayer(scaleX = scale, scaleY = scale) .alpha(alpha) - .background(colorResource(id = R.color.contactless_blue), CircleShape) + .background(tintColorThemed, CircleShape) ) } // Clip the icon to be circular @@ -1127,19 +1216,26 @@ fun NfcHeartbeatAnimation(nfcAnimationStatus: NfcAnimationStatus) { // waiting to begin reading an NFC document // contactless blue color icon with the same color for background having a low alpha NfcAnimationStatus.Initial -> Pair( - colorResource(R.color.contactless_blue), - colorResource(R.color.contactless_blue).copy(alpha = animationBgAlpha) + tintColorThemed, + tintColorThemed.copy(alpha = animationBgAlpha) ) // once an NFC document is detected, inverse colors of "Initial" status NfcAnimationStatus.Connected -> Pair( MaterialTheme.colorScheme.background, - colorResource(R.color.contactless_blue) + tintColorThemed ) // Error while initializing/reading NFC card // white icon with light red background with a lower alpha NfcAnimationStatus.Error -> Pair( MaterialTheme.colorScheme.background, - Color.Red.copy(alpha = 0.3F) + Color.Red.copy( + alpha = + if (isSystemInDarkTheme()) { + 1F + } else { + 0.3F + } + ) ) // NFC scan complete -- colors are not used b/c icon is changed entirely else -> Pair(Color.Transparent, Color.Transparent) diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/ProvisionCredentialScreen.kt b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/ProvisionCredentialScreen.kt index f585e3f3e..19724bfb5 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/ProvisionCredentialScreen.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/ProvisionCredentialScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp import com.android.identity.document.DocumentStore import com.android.identity.issuance.IssuingAuthorityException import com.android.identity.issuance.evidence.EvidenceRequestCreatePassphrase +import com.android.identity.issuance.evidence.EvidenceRequestDetailedMessage import com.android.identity.issuance.evidence.EvidenceRequestGermanEid import com.android.identity.issuance.evidence.EvidenceRequestIcaoNfcTunnel import com.android.identity.issuance.evidence.EvidenceRequestIcaoPassiveAuthentication @@ -50,7 +51,8 @@ fun ProvisionDocumentScreen( onNavigate: (String) -> Unit, permissionTracker: PermissionTracker, walletServerProvider: WalletServerProvider, - documentStore: DocumentStore + documentStore: DocumentStore, + developerMode: Boolean = false ) { ScreenWithAppBar(title = stringResource(R.string.provisioning_title), navigationIcon = { if (provisioningViewModel.state.value != ProvisioningViewModel.State.PROOFING_COMPLETE) { @@ -148,25 +150,30 @@ fun ProvisionDocumentScreen( } is EvidenceRequestMessage -> { - // show a standard message that does not have a [messageType] defined - if (evidenceRequest.messageType==null) { - EvidenceRequestMessageView( + EvidenceRequestMessageView( + evidenceRequest = evidenceRequest, + provisioningViewModel = provisioningViewModel, + walletServerProvider = walletServerProvider, + documentStore = documentStore + ) + } + + is EvidenceRequestDetailedMessage -> { + // show detailed message according to the messageType + if (evidenceRequest.messageType=="evidence_request_complete"){ + EvidenceRequestCompleteScreen( evidenceRequest = evidenceRequest, provisioningViewModel = provisioningViewModel, walletServerProvider = walletServerProvider, documentStore = documentStore ) } else { - // handle showing a non-standard message according to the classification - // of the defined [messageType] - if (evidenceRequest.messageType=="evidence_request_complete"){ - EvidenceRequestCompleteScreen( - evidenceRequest = evidenceRequest, - provisioningViewModel = provisioningViewModel, - walletServerProvider = walletServerProvider, - documentStore = documentStore - ) - } + EvidenceRequestGenericDetailedMessageScreen( + detailedMessage = evidenceRequest, + provisioningViewModel = provisioningViewModel, + walletServerProvider = walletServerProvider, + documentStore = documentStore + ) } } @@ -204,9 +211,11 @@ fun ProvisionDocumentScreen( is EvidenceRequestIcaoNfcTunnel -> { EvidenceRequestIcaoNfcTunnelView( - evidenceRequest, + evidenceRequest = evidenceRequest, provisioningViewModel = provisioningViewModel, - permissionTracker = permissionTracker + permissionTracker = permissionTracker, + developerMode = developerMode + ) } diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml index 109f06aee..5afe874a9 100644 --- a/wallet/src/main/res/values/strings.xml +++ b/wallet/src/main/res/values/strings.xml @@ -327,10 +327,6 @@ The PIN didn\'t match, please try again Document Scanning Successful - Document Scanning Complete - Your application is now under ID issuer verification. This process might take a few hours. - Done - Next Cancel @@ -354,8 +350,8 @@ Failed to initialize Initializing... Success! - Reading data: %1$d%% - Preparing to read... %1$d%% + Reading... %1$d%% + Starting... Reading... %1$d%% Finished