From 708e916a7a8cf22f43e2306cde5e1de523e3015f Mon Sep 17 00:00:00 2001 From: oil-bleach-re-el-idited Date: Sun, 22 Dec 2024 02:13:50 -0500 Subject: [PATCH] Delete appholder directory Signed-off-by: oil-bleach-re-el-idited --- appholder/.gitignore | 1 - appholder/build.gradle.kts | 114 ---- appholder/lint.xml | 10 - .../mdl/app/ExampleInstrumentedTest.kt | 26 - .../selfsigned/SelfSignedScreenStateTest.kt | 182 ------ appholder/src/main/AndroidManifest.xml | 138 ----- .../assets/identitycredentialmatcher.wasm | Bin 61513 -> 0 bytes appholder/src/main/ic_launcher-playstore.png | Bin 12260 -> 0 bytes .../identity/wallet/GetCredentialActivity.kt | 359 ----------- .../com/android/identity/wallet/HolderApp.kt | 108 ---- .../android/identity/wallet/MainActivity.kt | 132 ----- .../wallet/adapter/BindingAdapters.kt | 15 - .../wallet/adapter/DocumentAdapter.kt | 99 ---- .../AuthConfirmationFragment.kt | 160 ----- .../authconfirmation/ConfirmationSheet.kt | 370 ------------ .../authconfirmation/ConfirmationSheetData.kt | 17 - .../authconfirmation/PassphraseAuthResult.kt | 6 - .../authconfirmation/PassphrasePrompt.kt | 139 ----- .../PassphrasePromptViewModel.kt | 21 - .../authconfirmation/RequestedDocumentData.kt | 10 - .../authconfirmation/RequestedElement.kt | 6 - .../authconfirmation/SignedDocumentData.kt | 17 - .../SignedElementsCollection.kt | 33 -- .../authprompt/BiometricUserAuthPrompt.kt | 17 - .../authprompt/UserAuthPromptBuilder.kt | 117 ---- .../composables/AndroidSetupContainer.kt | 128 ---- .../AuthenticationKeyCurveAndroid.kt | 76 --- .../AuthenticationKeyCurveSoftware.kt | 74 --- .../wallet/composables/CounterInput.kt | 24 - .../wallet/composables/CurveLabelFor.kt | 26 - .../wallet/composables/DocumentValues.kt | 26 - .../wallet/composables/DropDownIndicator.kt | 20 - .../wallet/composables/LabeledUserInput.kt | 26 - .../wallet/composables/LoadingIndicator.kt | 33 -- .../wallet/composables/MdocAuthentication.kt | 68 --- .../wallet/composables/NumberChanger.kt | 58 -- .../OutlinedContainerHorizontal.kt | 43 -- .../identity/wallet/composables/Preview.kt | 8 - .../composables/SoftwareSetupContainer.kt | 53 -- .../wallet/composables/TextDropDownRow.kt | 24 - .../identity/wallet/composables/Toast.kt | 10 - .../identity/wallet/composables/ValueLabel.kt | 19 - .../composables/mdocAuthOptionLabelFor.kt | 19 - .../wallet/composables/state/AuthTypeState.kt | 10 - .../composables/state/MdocAuthOption.kt | 10 - .../composables/state/MdocAuthStateOption.kt | 9 - .../wallet/credman/IdentityCredentialEntry.kt | 44 -- .../wallet/credman/IdentityCredentialField.kt | 19 - .../credman/IdentityCredentialRegistry.kt | 64 -- .../identity/wallet/document/DocumentColor.kt | 12 - .../wallet/document/DocumentInformation.kt | 31 - .../wallet/document/DocumentManager.kt | 350 ----------- .../wallet/document/KeysAndCertificates.kt | 105 ---- .../wallet/documentdata/DocumentDataReader.kt | 261 -------- .../wallet/documentdata/DocumentElements.kt | 12 - .../wallet/documentdata/DrivingLicense.kt | 225 ------- .../documentdata/MdocComplexTypeDefinition.kt | 11 - .../documentdata/MdocComplexTypeRepository.kt | 19 - .../wallet/documentdata/MdocComplexTypes.kt | 30 - .../documentdata/MdocNamespaceComplexTypes.kt | 25 - .../documentdata/ShowDocumentDataFragment.kt | 62 -- .../documentdata/VaccinationDocument.kt | 236 -------- .../documentdata/VehicleRegistration.kt | 139 ----- .../wallet/documentinfo/DocumentInfoScreen.kt | 458 -------------- .../documentinfo/DocumentInfoScreenState.kt | 35 -- .../documentinfo/DocumentInfoViewModel.kt | 106 ---- .../wallet/fragment/DocumentDetailFragment.kt | 48 -- .../fragment/ReverseEngagementFragment.kt | 140 ----- .../fragment/SelfSignedDetailsFragment.kt | 558 ------------------ .../wallet/fragment/ShareDocumentFragment.kt | 86 --- .../fragment/TransferDocumentFragment.kt | 207 ------- .../selfsigned/AddSelfSignedDocumentScreen.kt | 424 ------------- .../selfsigned/AddSelfSignedFragment.kt | 52 -- .../selfsigned/AddSelfSignedScreenState.kt | 23 - .../selfsigned/AddSelfSignedViewModel.kt | 87 --- .../wallet/selfsigned/DocumentItem.kt | 6 - .../selfsigned/SelfSignedDocumentData.kt | 25 - .../settings/CaCertificateDetailsFragment.kt | 41 -- .../settings/CaCertificateDetailsScreen.kt | 160 ----- .../wallet/settings/CaCertificatesFragment.kt | 145 ----- .../wallet/settings/CaCertificatesScreen.kt | 144 ----- .../settings/CaCertificatesScreenState.kt | 7 - .../settings/CaCertificatesViewModel.kt | 48 -- .../wallet/settings/CertificateItem.kt | 22 - .../identity/wallet/settings/Mappers.kt | 57 -- .../wallet/settings/SettingsFragment.kt | 57 -- .../wallet/settings/SettingsScreen.kt | 296 ---------- .../wallet/settings/SettingsScreenState.kt | 93 --- .../wallet/settings/SettingsViewModel.kt | 96 --- .../AndroidKeystoreSecureAreaSupport.kt | 227 ------- .../AndroidKeystoreSecureAreaSupportState.kt | 28 - .../wallet/support/CurrentSecureArea.kt | 13 - .../wallet/support/MdocAuthStateExtensions.kt | 11 - .../wallet/support/SecureAreaSupport.kt | 108 ---- .../wallet/support/SecureAreaSupportNull.kt | 61 -- .../wallet/support/SecureAreaSupportState.kt | 8 - .../support/SecureAreaSupportStateNull.kt | 11 - .../SoftwareKeystoreSecureAreaSupport.kt | 154 ----- .../SoftwareKeystoreSecureAreaSupportState.kt | 15 - .../AndroidAuthKeyCurveOption.kt | 17 - .../AndroidAuthKeyCurveState.kt | 10 - .../SoftwareAuthKeyCurveOption.kt | 35 -- .../SoftwareAuthKeyCurveState.kt | 10 - .../android/identity/wallet/theme/Color.kt | 30 - .../android/identity/wallet/theme/Shape.kt | 11 - .../android/identity/wallet/theme/Theme.kt | 56 -- .../com/android/identity/wallet/theme/Type.kt | 21 - .../transfer/AddDocumentToResponseResult.kt | 14 - .../identity/wallet/transfer/Communication.kt | 87 --- .../wallet/transfer/ConnectionSetup.kt | 68 --- .../wallet/transfer/QrCommunicationSetup.kt | 106 ---- .../transfer/ReverseQrCommunicationSetup.kt | 88 --- .../wallet/transfer/TransferManager.kt | 288 --------- .../trustmanagement/CountryValidator.kt | 53 -- .../trustmanagement/CustomValidators.kt | 16 - .../X500PrincipalExtensions.kt | 63 -- .../X509CertificateExtensions.kt | 18 - .../identity/wallet/util/BindingAdapters.kt | 22 - .../identity/wallet/util/ContextExtensions.kt | 14 - .../identity/wallet/util/DocumentData.kt | 26 - .../identity/wallet/util/FormatUtil.kt | 95 --- .../identity/wallet/util/LogginExtensions.kt | 43 -- .../wallet/util/NfcDataTransferHandler.kt | 43 -- .../wallet/util/NfcEngagementHandler.kt | 183 ------ .../util/PeriodicKeysRefreshWorkRequest.kt | 17 - .../identity/wallet/util/Preconditions.kt | 19 - .../identity/wallet/util/PreferencesHelper.kt | 102 ---- .../identity/wallet/util/ProvisioningUtil.kt | 344 ----------- .../identity/wallet/util/RefreshKeysWorker.kt | 23 - .../wallet/util/SampleDataProvider.kt | 342 ----------- .../wallet/util/SavedStateHandleExtensions.kt | 11 - .../identity/wallet/util/TransferStatus.kt | 11 - .../identity/wallet/util/ViewHelper.kt | 39 -- .../wallet/viewmodel/SelfSignedViewModel.kt | 286 --------- .../viewmodel/ShareDocumentViewModel.kt | 47 -- .../viewmodel/TransferDocumentViewModel.kt | 182 ------ .../wallet/wallet/DocumentPageTransformer.kt | 19 - .../wallet/DocumentPagerItemDecoration.kt | 26 - .../wallet/wallet/SelectDocumentFragment.kt | 188 ------ .../src/main/res/drawable/blue_gradient.xml | 9 - .../main/res/drawable/bottom_sheet_handle.xml | 9 - .../main/res/drawable/default_page_dot.xml | 8 - .../main/res/drawable/driving_license_bg.png | Bin 14904 -> 0 bytes .../src/main/res/drawable/gradient_red.xml | 9 - .../src/main/res/drawable/green_gradient.xml | 9 - .../src/main/res/drawable/ic_add_document.xml | 11 - .../src/main/res/drawable/ic_add_http.xml | 10 - .../res/drawable/ic_launcher_foreground.xml | 15 - appholder/src/main/res/drawable/ic_nfc.xml | 10 - .../main/res/drawable/ic_outline_info_24.xml | 10 - .../main/res/drawable/ic_present_document.xml | 41 -- .../src/main/res/drawable/ic_settings.xml | 11 - appholder/src/main/res/drawable/ic_wallet.xml | 11 - .../main/res/drawable/img_erika_portrait.jpg | Bin 11581 -> 0 bytes .../main/res/drawable/img_erika_signature.jpg | Bin 5556 -> 0 bytes .../res/drawable/pager_indicator_selector.xml | 5 - .../main/res/drawable/selected_pager_dot.xml | 8 - .../src/main/res/drawable/yellow_gradient.xml | 9 - .../layout-land/fragment_share_document.xml | 57 -- .../src/main/res/layout/activity_main.xml | 46 -- .../layout/fragment_reverse_engagement.xml | 49 -- .../res/layout/fragment_select_document.xml | 140 ----- .../layout/fragment_self_signed_details.xml | 56 -- .../res/layout/fragment_share_document.xml | 57 -- .../layout/fragment_show_document_data.xml | 62 -- .../res/layout/fragment_transfer_document.xml | 115 ---- .../main/res/layout/list_item_document.xml | 84 --- .../main/res/menu/side_navigation_menu.xml | 31 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 7 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 7 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 1477 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 3528 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1035 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2155 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 2015 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 4980 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 3269 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 8111 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 4706 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 11822 -> 0 bytes .../main/res/navigation/navigation_graph.xml | 170 ------ appholder/src/main/res/raw/bdr_iaca_cert.pem | 13 - .../src/main/res/raw/bdr_reader_ca_cert.pem | 14 - .../res/raw/credenceid_mdl_reader_cert.pem | 18 - .../src/main/res/raw/fast_reader_auth_cer.pem | 12 - .../src/main/res/raw/google_mdl_ds_cert.pem | 13 - .../res/raw/google_mdl_ds_cert_iaca_2.pem | 12 - .../raw/google_mdl_ds_cert_iaca_2_privkey.pem | 5 - .../raw/google_mdl_ds_cert_iaca_2_pubkey.pem | 4 - .../main/res/raw/google_mdl_ds_privkey.pem | 4 - .../src/main/res/raw/google_mdl_ds_pubkey.pem | 4 - .../src/main/res/raw/google_mekb_ds_cert.pem | 13 - .../main/res/raw/google_mekb_ds_privkey.pem | 4 - .../main/res/raw/google_mekb_ds_pubkey.pem | 4 - .../src/main/res/raw/google_micov_ds_cert.pem | 13 - .../main/res/raw/google_micov_ds_privkey.pem | 4 - .../main/res/raw/google_micov_ds_pubkey.pem | 4 - .../src/main/res/raw/google_reader_ca.cer | 14 - .../res/raw/hid_test_reader_ca_mdl_cert.pem | 15 - .../src/main/res/raw/hidtestiacamdl_cert.pem | 15 - appholder/src/main/res/raw/iaca_zetes.der | Bin 512 -> 0 bytes .../idemia_brisbane_interop_readerauthca.pem | 21 - ...iana_department_of_motor_vehicles_cert.cer | Bin 623 -> 0 bytes .../src/main/res/raw/nist_reader_ca_cer.pem | 14 - .../owf_identity_credential_reader_cert.pem | 12 - .../res/raw/reader_ca_nec_reader_ca_cer.pem | 14 - .../main/res/raw/samsung_iaca_test_cert.pem | 14 - .../src/main/res/raw/scytales_root_ca.pem | 14 - .../src/main/res/raw/spruce_iaca_cert.pem | 17 - appholder/src/main/res/raw/ul_cert_ca_01.crt | Bin 709 -> 0 bytes .../src/main/res/raw/ul_cert_ca_01_cer.pem | 17 - appholder/src/main/res/raw/ul_cert_ca_02.crt | Bin 705 -> 0 bytes .../src/main/res/raw/ul_cert_ca_02_cer.pem | 17 - .../src/main/res/raw/ul_cert_ca_03_cer.pem | 12 - appholder/src/main/res/raw/utms_reader_ca.der | Bin 371 -> 0 bytes .../src/main/res/raw/utms_reader_ca_cer.pem | 10 - .../src/main/res/raw/zetes_reader_ca.der | Bin 371 -> 0 bytes .../src/main/res/raw/zetes_reader_ca_cer.pem | 10 - .../src/main/res/values-night/themes.xml | 18 - appholder/src/main/res/values/arrays.xml | 13 - appholder/src/main/res/values/colors.xml | 10 - appholder/src/main/res/values/dimen.xml | 5 - appholder/src/main/res/values/strings.xml | 212 ------- appholder/src/main/res/values/styles.xml | 19 - appholder/src/main/res/values/themes.xml | 18 - appholder/src/main/res/xml/file_paths.xml | 4 - .../main/res/xml/network_security_config.xml | 4 - .../xml/nfc_data_transfer_apdu_service.xml | 28 - .../res/xml/nfc_engagement_apdu_service.xml | 28 - .../res/values/ic_launcher_background.xml | 4 - appholder/src/purse/res/values/strings.xml | 4 - .../identity/wallet/ExampleUnitTest.kt | 16 - .../PassphraseValidationTest.kt | 36 -- .../res/values/ic_launcher_background.xml | 4 - appholder/src/wallet/res/values/strings.xml | 4 - 235 files changed, 13486 deletions(-) delete mode 100644 appholder/.gitignore delete mode 100644 appholder/build.gradle.kts delete mode 100644 appholder/lint.xml delete mode 100644 appholder/src/androidTest/java/com/android/mdl/app/ExampleInstrumentedTest.kt delete mode 100644 appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt delete mode 100644 appholder/src/main/AndroidManifest.xml delete mode 100644 appholder/src/main/assets/identitycredentialmatcher.wasm delete mode 100644 appholder/src/main/ic_launcher-playstore.png delete mode 100644 appholder/src/main/java/com/android/identity/wallet/GetCredentialActivity.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/HolderApp.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/MainActivity.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/adapter/BindingAdapters.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/adapter/DocumentAdapter.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheet.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheetData.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphraseAuthResult.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePrompt.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePromptViewModel.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedDocumentData.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedElement.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedDocumentData.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedElementsCollection.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authprompt/BiometricUserAuthPrompt.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/authprompt/UserAuthPromptBuilder.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/CurveLabelFor.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/LoadingIndicator.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/Preview.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/Toast.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialEntry.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialField.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialRegistry.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/document/DocumentColor.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/document/DocumentManager.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/document/KeysAndCertificates.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentDataReader.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentElements.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/DrivingLicense.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeDefinition.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeRepository.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypes.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/MdocNamespaceComplexTypes.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/ShowDocumentDataFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/VaccinationDocument.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentdata/VehicleRegistration.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/fragment/DocumentDetailFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/fragment/ReverseEngagementFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/fragment/SelfSignedDetailsFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/fragment/ShareDocumentFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/selfsigned/DocumentItem.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsScreen.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreen.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreenState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesViewModel.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/CertificateItem.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/Mappers.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/SettingsFragment.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreen.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreenState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/settings/SettingsViewModel.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/theme/Color.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/theme/Shape.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/theme/Theme.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/theme/Type.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/transfer/Communication.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/transfer/ConnectionSetup.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/transfer/QrCommunicationSetup.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/transfer/ReverseQrCommunicationSetup.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/trustmanagement/CountryValidator.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/trustmanagement/CustomValidators.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/trustmanagement/X500PrincipalExtensions.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/trustmanagement/X509CertificateExtensions.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/BindingAdapters.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/ContextExtensions.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/DocumentData.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/FormatUtil.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/LogginExtensions.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/NfcDataTransferHandler.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/NfcEngagementHandler.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/PeriodicKeysRefreshWorkRequest.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/Preconditions.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/PreferencesHelper.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/RefreshKeysWorker.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/SampleDataProvider.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/SavedStateHandleExtensions.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/TransferStatus.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/util/ViewHelper.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/viewmodel/SelfSignedViewModel.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/viewmodel/ShareDocumentViewModel.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/viewmodel/TransferDocumentViewModel.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPageTransformer.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPagerItemDecoration.kt delete mode 100644 appholder/src/main/java/com/android/identity/wallet/wallet/SelectDocumentFragment.kt delete mode 100644 appholder/src/main/res/drawable/blue_gradient.xml delete mode 100644 appholder/src/main/res/drawable/bottom_sheet_handle.xml delete mode 100644 appholder/src/main/res/drawable/default_page_dot.xml delete mode 100644 appholder/src/main/res/drawable/driving_license_bg.png delete mode 100644 appholder/src/main/res/drawable/gradient_red.xml delete mode 100644 appholder/src/main/res/drawable/green_gradient.xml delete mode 100644 appholder/src/main/res/drawable/ic_add_document.xml delete mode 100644 appholder/src/main/res/drawable/ic_add_http.xml delete mode 100644 appholder/src/main/res/drawable/ic_launcher_foreground.xml delete mode 100644 appholder/src/main/res/drawable/ic_nfc.xml delete mode 100644 appholder/src/main/res/drawable/ic_outline_info_24.xml delete mode 100644 appholder/src/main/res/drawable/ic_present_document.xml delete mode 100644 appholder/src/main/res/drawable/ic_settings.xml delete mode 100644 appholder/src/main/res/drawable/ic_wallet.xml delete mode 100644 appholder/src/main/res/drawable/img_erika_portrait.jpg delete mode 100644 appholder/src/main/res/drawable/img_erika_signature.jpg delete mode 100644 appholder/src/main/res/drawable/pager_indicator_selector.xml delete mode 100644 appholder/src/main/res/drawable/selected_pager_dot.xml delete mode 100644 appholder/src/main/res/drawable/yellow_gradient.xml delete mode 100644 appholder/src/main/res/layout-land/fragment_share_document.xml delete mode 100644 appholder/src/main/res/layout/activity_main.xml delete mode 100644 appholder/src/main/res/layout/fragment_reverse_engagement.xml delete mode 100644 appholder/src/main/res/layout/fragment_select_document.xml delete mode 100644 appholder/src/main/res/layout/fragment_self_signed_details.xml delete mode 100644 appholder/src/main/res/layout/fragment_share_document.xml delete mode 100644 appholder/src/main/res/layout/fragment_show_document_data.xml delete mode 100644 appholder/src/main/res/layout/fragment_transfer_document.xml delete mode 100644 appholder/src/main/res/layout/list_item_document.xml delete mode 100644 appholder/src/main/res/menu/side_navigation_menu.xml delete mode 100644 appholder/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 appholder/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 appholder/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 appholder/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 appholder/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 appholder/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 appholder/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 appholder/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 appholder/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 appholder/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 appholder/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 appholder/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 appholder/src/main/res/navigation/navigation_graph.xml delete mode 100644 appholder/src/main/res/raw/bdr_iaca_cert.pem delete mode 100644 appholder/src/main/res/raw/bdr_reader_ca_cert.pem delete mode 100644 appholder/src/main/res/raw/credenceid_mdl_reader_cert.pem delete mode 100644 appholder/src/main/res/raw/fast_reader_auth_cer.pem delete mode 100644 appholder/src/main/res/raw/google_mdl_ds_cert.pem delete mode 100644 appholder/src/main/res/raw/google_mdl_ds_cert_iaca_2.pem delete mode 100644 appholder/src/main/res/raw/google_mdl_ds_cert_iaca_2_privkey.pem delete mode 100644 appholder/src/main/res/raw/google_mdl_ds_cert_iaca_2_pubkey.pem delete mode 100644 appholder/src/main/res/raw/google_mdl_ds_privkey.pem delete mode 100644 appholder/src/main/res/raw/google_mdl_ds_pubkey.pem delete mode 100644 appholder/src/main/res/raw/google_mekb_ds_cert.pem delete mode 100644 appholder/src/main/res/raw/google_mekb_ds_privkey.pem delete mode 100644 appholder/src/main/res/raw/google_mekb_ds_pubkey.pem delete mode 100644 appholder/src/main/res/raw/google_micov_ds_cert.pem delete mode 100644 appholder/src/main/res/raw/google_micov_ds_privkey.pem delete mode 100644 appholder/src/main/res/raw/google_micov_ds_pubkey.pem delete mode 100644 appholder/src/main/res/raw/google_reader_ca.cer delete mode 100644 appholder/src/main/res/raw/hid_test_reader_ca_mdl_cert.pem delete mode 100644 appholder/src/main/res/raw/hidtestiacamdl_cert.pem delete mode 100644 appholder/src/main/res/raw/iaca_zetes.der delete mode 100644 appholder/src/main/res/raw/idemia_brisbane_interop_readerauthca.pem delete mode 100644 appholder/src/main/res/raw/louisiana_department_of_motor_vehicles_cert.cer delete mode 100644 appholder/src/main/res/raw/nist_reader_ca_cer.pem delete mode 100644 appholder/src/main/res/raw/owf_identity_credential_reader_cert.pem delete mode 100644 appholder/src/main/res/raw/reader_ca_nec_reader_ca_cer.pem delete mode 100755 appholder/src/main/res/raw/samsung_iaca_test_cert.pem delete mode 100644 appholder/src/main/res/raw/scytales_root_ca.pem delete mode 100644 appholder/src/main/res/raw/spruce_iaca_cert.pem delete mode 100644 appholder/src/main/res/raw/ul_cert_ca_01.crt delete mode 100644 appholder/src/main/res/raw/ul_cert_ca_01_cer.pem delete mode 100644 appholder/src/main/res/raw/ul_cert_ca_02.crt delete mode 100644 appholder/src/main/res/raw/ul_cert_ca_02_cer.pem delete mode 100644 appholder/src/main/res/raw/ul_cert_ca_03_cer.pem delete mode 100644 appholder/src/main/res/raw/utms_reader_ca.der delete mode 100644 appholder/src/main/res/raw/utms_reader_ca_cer.pem delete mode 100644 appholder/src/main/res/raw/zetes_reader_ca.der delete mode 100644 appholder/src/main/res/raw/zetes_reader_ca_cer.pem delete mode 100644 appholder/src/main/res/values-night/themes.xml delete mode 100644 appholder/src/main/res/values/arrays.xml delete mode 100644 appholder/src/main/res/values/colors.xml delete mode 100644 appholder/src/main/res/values/dimen.xml delete mode 100644 appholder/src/main/res/values/strings.xml delete mode 100644 appholder/src/main/res/values/styles.xml delete mode 100644 appholder/src/main/res/values/themes.xml delete mode 100644 appholder/src/main/res/xml/file_paths.xml delete mode 100644 appholder/src/main/res/xml/network_security_config.xml delete mode 100644 appholder/src/main/res/xml/nfc_data_transfer_apdu_service.xml delete mode 100644 appholder/src/main/res/xml/nfc_engagement_apdu_service.xml delete mode 100644 appholder/src/purse/res/values/ic_launcher_background.xml delete mode 100644 appholder/src/purse/res/values/strings.xml delete mode 100644 appholder/src/test/java/com/android/identity/wallet/ExampleUnitTest.kt delete mode 100644 appholder/src/test/java/com/android/identity/wallet/authconfirmation/PassphraseValidationTest.kt delete mode 100644 appholder/src/wallet/res/values/ic_launcher_background.xml delete mode 100644 appholder/src/wallet/res/values/strings.xml diff --git a/appholder/.gitignore b/appholder/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/appholder/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/appholder/build.gradle.kts b/appholder/build.gradle.kts deleted file mode 100644 index e31b4e9e8..000000000 --- a/appholder/build.gradle.kts +++ /dev/null @@ -1,114 +0,0 @@ -plugins { - alias(libs.plugins.androidApplication) - alias(libs.plugins.compose.compiler) - alias(libs.plugins.jetbrainsCompose) - id("kotlin-android") - alias(libs.plugins.navigation.safe.args) - alias(libs.plugins.parcelable) - alias(libs.plugins.kapt) -} - -val projectVersionCode: Int by rootProject.extra -val projectVersionName: String by rootProject.extra - -kotlin { - jvmToolchain(17) -} - -android { - namespace = "com.android.identity.wallet" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - defaultConfig { - applicationId = "com.android.identity.wallet" - minSdk = 29 - targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = projectVersionCode - versionName = projectVersionName - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - isMinifyEnabled = true - isShrinkResources = true - } - } - - flavorDimensions.addAll(listOf("standard")) - productFlavors { - create("wallet") { - dimension = "standard" - isDefault = true - } - create("purse") { - dimension = "standard" - applicationIdSuffix = ".purse" - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - buildFeatures { - dataBinding = true - viewBinding = true - compose = true - } - - packaging { - resources { - excludes += listOf("/META-INF/{AL2.0,LGPL2.1}") - excludes += listOf("/META-INF/versions/9/OSGI-INF/MANIFEST.MF") - } - } -} - -dependencies { - implementation(project(":identity")) - implementation(project(":identity-mdoc")) - implementation(project(":identity-android")) - implementation(project(":identity-doctypes")) - implementation(project(":jpeg2k")) - - implementation(libs.kotlinx.datetime) - - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(compose.material) - - debugImplementation(compose.uiTooling) - implementation(compose.preview) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.biometrics) - implementation(compose.material3) - implementation(libs.compose.material.icons.extended) - implementation(libs.androidx.navigation.runtime) - implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.activity.compose) - implementation(libs.code.scanner) - implementation(libs.androidx.material) - implementation(libs.androidx.navigation.fragment) - implementation(libs.androidx.navigation.ui) - implementation(libs.androidx.preference) - implementation(libs.kotlinx.io.core) - implementation(libs.cbor) - implementation(libs.exifinterface) - implementation(libs.androidx.work) - - implementation(files("../third-party/play-services-identity-credentials-0.0.1-eap01.aar")) - implementation(libs.bundles.google.play.services) - - implementation(libs.bouncy.castle.bcprov) - implementation(libs.bouncy.castle.bcpkix) - - testImplementation(libs.kotlin.test) - androidTestImplementation(libs.androidx.test.junit) - androidTestImplementation(libs.androidx.espresso.core) -} diff --git a/appholder/lint.xml b/appholder/lint.xml deleted file mode 100644 index e06ca9c94..000000000 --- a/appholder/lint.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/appholder/src/androidTest/java/com/android/mdl/app/ExampleInstrumentedTest.kt b/appholder/src/androidTest/java/com/android/mdl/app/ExampleInstrumentedTest.kt deleted file mode 100644 index 2d948e4c0..000000000 --- a/appholder/src/androidTest/java/com/android/mdl/app/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.mdl.app - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - Assert.assertTrue( - "com.android.identity.wallet" == appContext.packageName || - "com.android.identity.wallet.purse" == appContext.packageName - ) - } -} \ No newline at end of file diff --git a/appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt b/appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt deleted file mode 100644 index b9a1a850f..000000000 --- a/appholder/src/androidTest/java/com/android/mdl/app/selfsigned/SelfSignedScreenStateTest.kt +++ /dev/null @@ -1,182 +0,0 @@ -package com.android.mdl.app.selfsigned - -import androidx.lifecycle.SavedStateHandle -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.android.storage.AndroidStorageEngine -import com.android.identity.documenttype.knowntypes.EUPersonalID -import com.android.identity.documenttype.knowntypes.VehicleRegistration -import com.android.identity.securearea.SecureAreaRepository -import com.android.identity.securearea.software.SoftwareSecureArea -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState -import com.android.identity.wallet.selfsigned.AddSelfSignedViewModel -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.util.ProvisioningUtil -import kotlinx.io.files.Path -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class SelfSignedScreenStateTest { - - private val savedStateHandle = SavedStateHandle() - private lateinit var repository: SecureAreaRepository - - @Before - fun setUp() { - val context = InstrumentationRegistry.getInstrumentation().targetContext - repository = ProvisioningUtil.getInstance(context).secureAreaRepository - val storageFile = Path(PreferencesHelper.getKeystoreBackedStorageLocation(context).path) - val storageEngine = AndroidStorageEngine.Builder(context, storageFile).build() - val androidKeystoreSecureArea = AndroidKeystoreSecureArea(context, storageEngine) - val softwareSecureArea = SoftwareSecureArea(storageEngine) - repository.addImplementation(androidKeystoreSecureArea) - repository.addImplementation(softwareSecureArea) - } - - @Test - fun defaultScreenState() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState()) - } - - @Test - fun updateDocumentType() { - val personalId = EUPersonalID.getDocumentType().mdocDocumentType?.docType!! - val name= EUPersonalID.getDocumentType().displayName - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateDocumentType(personalId, name) - - assertEquals(viewModel.screenState.value, - AddSelfSignedScreenState(documentType = personalId, documentName = "EU Personal ID") - ) - } - - @Test - fun updateDocumentName() { - val newName = ":irrelevant:" - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateDocumentName(newName) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(documentName = newName)) - } - - @Test - fun updateDocumentTypeAfterNameUpdate() { - val registration = VehicleRegistration.getDocumentType().mdocDocumentType?.docType!! - val name = VehicleRegistration.getDocumentType().displayName - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateDocumentName(":irrelevant:") - viewModel.updateDocumentType(registration, name) - - assertEquals(viewModel.screenState.value, - AddSelfSignedScreenState( - documentType = registration, - documentName = "Vehicle Registration" - ) - ) - } - - @Test - fun updateCardArt() { - val blue = DocumentColor.Blue - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateCardArt(blue) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(cardArt = blue)) - } - - @Test - fun updateValidityInDays() { - val newValue = 15 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateValidityInDays(newValue) - - assertEquals(viewModel.screenState.value.validityInDays, newValue) - } - - @Test - fun updateValidityInDaysBelowMinValidityDays() { - val defaultMinValidity = 10 - val belowMinValidity = 9 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateValidityInDays(defaultMinValidity) - viewModel.updateValidityInDays(belowMinValidity) - - assertEquals(viewModel.screenState.value.validityInDays, defaultMinValidity) - } - - @Test - fun updateMinValidityInDays() { - val newMinValidity = 15 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateMinValidityInDays(newMinValidity) - - assertEquals(viewModel.screenState.value.minValidityInDays, newMinValidity) - } - - @Test - fun updateMinValidityInDaysAboveValidityInDays() { - val defaultValidityInDays = 30 - val minValidityInDays = defaultValidityInDays + 5 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateMinValidityInDays(minValidityInDays) - - assertEquals(viewModel.screenState.value.validityInDays, minValidityInDays) - } - - @Test - fun updateNumberOfMso() { - val msoCount = 2 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateNumberOfMso(msoCount) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(numberOfMso = msoCount)) - } - - @Test - fun updateNumberOfMsoInvalidValue() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateNumberOfMso(1) - viewModel.updateNumberOfMso(0) - viewModel.updateNumberOfMso(-1) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(numberOfMso = 1)) - } - - @Test - fun updateMaxUseOfMso() { - val maxMsoUsages = 3 - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateMaxUseOfMso(maxMsoUsages) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(maxUseOfMso = maxMsoUsages)) - } - - @Test - fun updateMaxUseOfMsoInvalidValue() { - val viewModel = AddSelfSignedViewModel(savedStateHandle) - - viewModel.updateMaxUseOfMso(1) - viewModel.updateMaxUseOfMso(0) - viewModel.updateMaxUseOfMso(-1) - - assertEquals(viewModel.screenState.value, AddSelfSignedScreenState(maxUseOfMso = 1)) - } -} \ No newline at end of file diff --git a/appholder/src/main/AndroidManifest.xml b/appholder/src/main/AndroidManifest.xml deleted file mode 100644 index 950ed3ef4..000000000 --- a/appholder/src/main/AndroidManifest.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/appholder/src/main/assets/identitycredentialmatcher.wasm b/appholder/src/main/assets/identitycredentialmatcher.wasm deleted file mode 100644 index 027d559b7b9899e18fcebe4a3ab0b1917c61e0df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61513 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 VfOe@|_ltPow`kFa?-J1N{{S|CQI!Ay diff --git a/appholder/src/main/ic_launcher-playstore.png b/appholder/src/main/ic_launcher-playstore.png deleted file mode 100644 index b01a66a3001caa8e289b98d12a348d9746d5d2cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12260 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 M&KT(yXj|R;A3r+Qm;e9( diff --git a/appholder/src/main/java/com/android/identity/wallet/GetCredentialActivity.kt b/appholder/src/main/java/com/android/identity/wallet/GetCredentialActivity.kt deleted file mode 100644 index f03a27856..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/GetCredentialActivity.kt +++ /dev/null @@ -1,359 +0,0 @@ -package com.android.identity.wallet - -import android.content.Intent -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.Base64 -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.fragment.app.FragmentActivity -import com.android.identity.android.mdoc.util.CredmanUtil -import com.android.identity.android.securearea.AndroidKeystoreKeyUnlockData -import com.android.identity.document.DocumentRequest -import com.android.identity.document.NameSpacedData -import com.android.identity.crypto.Algorithm -import com.android.identity.crypto.Crypto -import com.android.identity.crypto.EcCurve -import com.android.identity.crypto.EcPublicKeyDoubleCoordinate -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.mdoc.mso.StaticAuthDataParser -import com.android.identity.mdoc.response.DeviceResponseGenerator -import com.android.identity.mdoc.response.DocumentGenerator -import com.android.identity.mdoc.util.MdocUtil -import com.android.identity.securearea.KeyLockedException -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.util.Constants -import com.android.identity.util.Logger -import com.android.identity.wallet.util.ProvisioningUtil -import com.android.identity.wallet.util.log -import com.google.android.gms.identitycredentials.GetCredentialResponse -import com.google.android.gms.identitycredentials.IntentHelper -import com.google.android.gms.identitycredentials.IntentHelper.EXTRA_CREDENTIAL_ID -import com.google.android.gms.identitycredentials.IntentHelper.extractGetCredentialRequest -import com.google.android.gms.identitycredentials.IntentHelper.setGetCredentialException -import com.google.android.gms.identitycredentials.IntentHelper.setGetCredentialResponse -import org.json.JSONObject -import java.util.StringTokenizer -import kotlinx.datetime.Clock - -class GetCredentialActivity : FragmentActivity() { - - fun addDeviceNamespaces(documentGenerator : DocumentGenerator, - credential : MdocCredential, - unlockData: KeyUnlockData?) { - documentGenerator.setDeviceNamespacesSignature( - NameSpacedData.Builder().build(), - credential.secureArea, - credential.alias, - unlockData, - Algorithm.ES256) - } - - fun doBiometricAuth(credential : MdocCredential, - forceLskf : Boolean, - onBiometricAuthCompleted: (unlockData: KeyUnlockData?) -> Unit) { - var title = "To share your credential we need to check that it's you." - var unlockData = AndroidKeystoreKeyUnlockData(credential.alias) - var cryptoObject = unlockData.getCryptoObjectForSigning(Algorithm.ES256) - - val promptInfoBuilder = BiometricPrompt.PromptInfo.Builder() - .setTitle("Authentication required") - .setSubtitle(title) - .setConfirmationRequired(false) - if (forceLskf) { - // TODO: this works only on Android 11 or later but for now this is fine - // as this is just a reference/test app and this path is only hit if - // the user actually presses the "Use LSKF" button. Longer term, we should - // fall back to using KeyGuard which will work on all Android versions. - promptInfoBuilder.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) - } else { - val canUseBiometricAuth = BiometricManager - .from(applicationContext) - .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS - if (canUseBiometricAuth) { - promptInfoBuilder.setNegativeButtonText("Use PIN") - } else { - promptInfoBuilder.setDeviceCredentialAllowed(true) - } - } - - val biometricPromptInfo = promptInfoBuilder.build() - val biometricPrompt = BiometricPrompt(this, - object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) { - Logger.d("TAG", "onAuthenticationError $errorCode $errString") - // TODO: "Use LSKF"... without this delay, the prompt won't work correctly - Handler(Looper.getMainLooper()).postDelayed({ - doBiometricAuth(credential, true, onBiometricAuthCompleted) - }, 100) - } - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - Logger.d("TAG", "onAuthenticationSucceeded $result") - - onBiometricAuthCompleted(unlockData) - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - Logger.d("TAG", "onAuthenticationFailed") - } - }) - - if (cryptoObject != null) { - biometricPrompt.authenticate(biometricPromptInfo, cryptoObject) - } else { - biometricPrompt.authenticate(biometricPromptInfo) - } - } - - private fun createMDocDeviceResponse( - credentialId: Int, - dataElements: List, - encodedSessionTranscript: ByteArray, - onComplete: (ByteArray) -> Unit - ) { - val documentRequest = DocumentRequest(dataElements) - val documentStore = ProvisioningUtil.getInstance(applicationContext).documentStore - val documentName = documentStore.listDocuments()[credentialId] - val document = documentStore.lookupDocument(documentName) - val nameSpacedData = document!!.applicationData.getNameSpacedData("documentData") - - val credential = document.findCredential( - ProvisioningUtil.CREDENTIAL_DOMAIN, - Clock.System.now() - ) as MdocCredential? ?: throw IllegalStateException("No credential") - val staticAuthData = StaticAuthDataParser(credential.issuerProvidedData).parse() - val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces( - documentRequest, nameSpacedData, staticAuthData - ) - - val deviceResponseGenerator = DeviceResponseGenerator(Constants.DEVICE_RESPONSE_STATUS_OK) - val documentGenerator = DocumentGenerator( - document.applicationData.getString(ProvisioningUtil.DOCUMENT_TYPE), - staticAuthData.issuerAuth, - encodedSessionTranscript - ) - documentGenerator.setIssuerNamespaces(mergedIssuerNamespaces) - try { - addDeviceNamespaces(documentGenerator, credential, null) - deviceResponseGenerator.addDocument(documentGenerator.generate()) - credential.increaseUsageCount() - onComplete(deviceResponseGenerator.generate()) - } catch (e: KeyLockedException) { - doBiometricAuth(credential, false) { keyUnlockData -> - if (keyUnlockData != null) { - addDeviceNamespaces(documentGenerator, credential, keyUnlockData) - deviceResponseGenerator.addDocument(documentGenerator.generate()) - credential.increaseUsageCount() - onComplete(deviceResponseGenerator.generate()) - } else { - throw RuntimeException("Biometric Auth Failed") - } - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - try { - - - val cmrequest = extractGetCredentialRequest(intent) - val credentialId = intent.getLongExtra(EXTRA_CREDENTIAL_ID, -1).toInt() - - // This call is currently broken, have to extract this info manually for now - //val callingAppInfo = extractCallingAppInfo(intent) - val callingPackageName = - intent.getStringExtra(IntentHelper.EXTRA_CALLING_PACKAGE_NAME)!! - val callingOrigin = intent.getStringExtra(IntentHelper.EXTRA_ORIGIN) - - log("CredId: $credentialId ${cmrequest!!.credentialOptions.get(0).requestMatcher}") - log("Calling app $callingPackageName $callingOrigin") - - val dataElements = mutableListOf() - - val json = JSONObject(cmrequest!!.credentialOptions.get(0).requestMatcher) - val provider = json.getJSONArray("providers").getJSONObject(0) - - val protocol = provider.getString("protocol") - log("Request protocol: $protocol") - val request = provider.getString("request") - log("Request: $request") - if (protocol == "preview") { - // Extract params from the preview protocol request - val previewRequest = JSONObject(request) - val selector = previewRequest.getJSONObject("selector") - val nonceBase64 = previewRequest.getString("nonce") - val readerPublicKeyBase64 = previewRequest.getString("readerPublicKey") - val docType = selector.getString("doctype") - log("DocType: $docType") - log("nonce: $nonceBase64") - log("readerPublicKey: $readerPublicKeyBase64") - - // Covert nonce and publicKey - val nonce = Base64.decode(nonceBase64, Base64.NO_WRAP or Base64.URL_SAFE) - val readerPublicKey = EcPublicKeyDoubleCoordinate.fromUncompressedPointEncoding( - EcCurve.P256, - Base64.decode(readerPublicKeyBase64, Base64.NO_WRAP or Base64.URL_SAFE) - ) - - // Match all the requested fields - val fields = selector.getJSONArray("fields") - for (n in 0 until fields.length()) { - val field = fields.getJSONObject(n) - val name = field.getString("name") - val namespace = field.getString("namespace") - val intentToRetain = field.getBoolean("intentToRetain") - log("Field $namespace $name $intentToRetain") - dataElements.add( - DocumentRequest.DataElement( - namespace, - name, - intentToRetain - ) - ) - } - - // Generate the Session Transcript - val encodedSessionTranscript = if (callingOrigin == null) { - CredmanUtil.generateAndroidSessionTranscript( - nonce, - callingPackageName, - Crypto.digest(Algorithm.SHA256, readerPublicKey.asUncompressedPointEncoding) - ) - } else { - CredmanUtil.generateBrowserSessionTranscript( - nonce, - callingOrigin, - Crypto.digest(Algorithm.SHA256, readerPublicKey.asUncompressedPointEncoding) - ) - } - // Create ISO DeviceResponse - createMDocDeviceResponse(credentialId, dataElements, encodedSessionTranscript) { deviceResponse -> - // The Preview protocol HPKE encrypts the response. - val (cipherText, encapsulatedPublicKey) = Crypto.hpkeEncrypt( - Algorithm.HPKE_BASE_P256_SHA256_AES128GCM, - readerPublicKey, - deviceResponse, - encodedSessionTranscript - ) - val encodedCredentialDocument = - CredmanUtil.generateCredentialDocument(cipherText, encapsulatedPublicKey) - - // Create the preview response - val responseJson = JSONObject() - responseJson.put( - "token", - Base64.encodeToString( - encodedCredentialDocument, - Base64.NO_WRAP or Base64.URL_SAFE - ) - ) - val response = responseJson.toString(2) - - // Send result back to credman - val resultData = Intent() - setGetCredentialResponse(resultData, createGetCredentialResponse(response)) - setResult(RESULT_OK, resultData) - finish() - } - } else if (protocol == "openid4vp") { - val openid4vpRequest = JSONObject(request) - val clientID = openid4vpRequest.getString("client_id") - log("client_id $clientID") - val nonceBase64 = openid4vpRequest.getString("nonce") - log("nonce: $nonceBase64") - val nonce = Base64.decode(nonceBase64, Base64.NO_WRAP or Base64.URL_SAFE) - - val presentationDefinition = openid4vpRequest.getJSONObject("presentation_definition") - val inputDescriptors = presentationDefinition.getJSONArray("input_descriptors") - if (inputDescriptors.length() != 1) { - throw IllegalArgumentException("Only support a single input input_descriptor") - } - val inputDescriptor = inputDescriptors.getJSONObject(0)!! - val docType = inputDescriptor.getString("id") - log("DocType: $docType") - - val constraints = inputDescriptor.getJSONObject("constraints") - val fields = constraints.getJSONArray("fields") - - for (n in 0 until fields.length()) { - val field = fields.getJSONObject(n) - // Only support a single path entry for now - val path = field.getJSONArray("path").getString(0)!! - // JSONPath is horrible, hacky way to parse it for demonstration purposes - val st = StringTokenizer(path, "'", false).asSequence().toList() - val namespace = st[1] as String - val name = st[3] as String - log("namespace $namespace name $name") - val intentToRetain = field.getBoolean("intent_to_retain") - dataElements.add( - DocumentRequest.DataElement( - namespace, - name, - intentToRetain - ) - ) - } - // Generate the Session Transcript - val encodedSessionTranscript = if (callingOrigin == null) { - CredmanUtil.generateAndroidSessionTranscript( - nonce, - callingPackageName, - Crypto.digest(Algorithm.SHA256, clientID.toByteArray()) - ) - } else { - CredmanUtil.generateBrowserSessionTranscript( - nonce, - callingOrigin, - Crypto.digest(Algorithm.SHA256, clientID.toByteArray()) - ) - } - // Create ISO DeviceResponse - createMDocDeviceResponse(credentialId, dataElements, encodedSessionTranscript) { deviceResponse -> - // Create the openid4vp respoinse - val responseJson = JSONObject() - responseJson.put( - "vp_token", - Base64.encodeToString( - deviceResponse, - Base64.NO_WRAP or Base64.URL_SAFE - ) - ) - val response = responseJson.toString(2) - - // Send result back to credman - val resultData = Intent() - setGetCredentialResponse(resultData, createGetCredentialResponse(response)) - setResult(RESULT_OK, resultData) - finish() - } - } else { - // Unknown protocol - throw IllegalArgumentException("Unknown protocol") - } - - } catch (e: Exception) { - log("Exception $e") - val resultData = Intent() - setGetCredentialException(resultData, e.toString(), e.message) - setResult(RESULT_OK, resultData) - finish() - } - } - - private fun createGetCredentialResponse(response: String): GetCredentialResponse { - val bundle = Bundle() - bundle.putByteArray("identityToken", response.toByteArray()) - val credentialResponse = com.google.android.gms.identitycredentials.Credential("type", bundle) - return GetCredentialResponse(credentialResponse) - } - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt b/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt deleted file mode 100644 index 5c0ba6e39..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/HolderApp.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.android.identity.wallet - -import android.app.Application -import android.content.Context -import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.android.storage.AndroidStorageEngine -import com.android.identity.android.util.AndroidLogPrinter -import com.android.identity.credential.CredentialFactory -import com.android.identity.crypto.X509Cert -import com.android.identity.document.DocumentStore -import com.android.identity.documenttype.DocumentTypeRepository -import com.android.identity.documenttype.knowntypes.DrivingLicense -import com.android.identity.documenttype.knowntypes.EUPersonalID -import com.android.identity.documenttype.knowntypes.VaccinationDocument -import com.android.identity.documenttype.knowntypes.VehicleRegistration -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.SecureAreaRepository -import com.android.identity.securearea.software.SoftwareSecureArea -import com.android.identity.storage.GenericStorageEngine -import com.android.identity.storage.StorageEngine -import com.android.identity.trustmanagement.TrustManager -import com.android.identity.trustmanagement.TrustPoint -import com.android.identity.util.Logger -import com.android.identity.wallet.document.KeysAndCertificates -import com.android.identity.wallet.util.PeriodicKeysRefreshWorkRequest -import com.android.identity.wallet.util.PreferencesHelper -import com.google.android.material.color.DynamicColors -import kotlinx.io.files.Path -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.io.ByteArrayInputStream -import java.security.Security -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate - -class HolderApp: Application() { - - private val documentTypeRepository by lazy { - DocumentTypeRepository() - } - - private val trustManager by lazy { - TrustManager() - } - - private val certificateStorageEngine by lazy { - GenericStorageEngine(Path(getDir("Certificates", MODE_PRIVATE).name)) - } - - override fun onCreate() { - super.onCreate() - Logger.setLogPrinter(AndroidLogPrinter()) - // This is needed to prefer BouncyCastle bundled with the app instead of the Conscrypt - // based implementation included in the OS itself. - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) - Security.addProvider(BouncyCastleProvider()) - DynamicColors.applyToActivitiesIfAvailable(this) - PreferencesHelper.initialize(this) - PeriodicKeysRefreshWorkRequest(this).schedulePeriodicKeysRefreshing() - documentTypeRepositoryInstance = documentTypeRepository - documentTypeRepositoryInstance.addDocumentType(DrivingLicense.getDocumentType()) - documentTypeRepositoryInstance.addDocumentType(VehicleRegistration.getDocumentType()) - documentTypeRepositoryInstance.addDocumentType(VaccinationDocument.getDocumentType()) - documentTypeRepositoryInstance.addDocumentType(EUPersonalID.getDocumentType()) - trustManagerInstance = trustManager - certificateStorageEngineInstance = certificateStorageEngine - certificateStorageEngineInstance.enumerate().forEach { - val certificate = parseCertificate(certificateStorageEngineInstance.get(it)!!) - trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded))) - } - KeysAndCertificates.getTrustedReaderCertificates(this).forEach { - trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(it.encoded))) - } - } - - companion object { - - lateinit var documentTypeRepositoryInstance: DocumentTypeRepository - lateinit var trustManagerInstance: TrustManager - lateinit var certificateStorageEngineInstance: StorageEngine - fun createDocumentStore( - context: Context, - secureAreaRepository: SecureAreaRepository - ): DocumentStore { - val storageFile = Path(PreferencesHelper.getKeystoreBackedStorageLocation(context).path) - val storageEngine = AndroidStorageEngine.Builder(context, storageFile).build() - - val androidKeystoreSecureArea = AndroidKeystoreSecureArea(context, storageEngine) - val softwareSecureArea = SoftwareSecureArea(storageEngine) - - secureAreaRepository.addImplementation(androidKeystoreSecureArea) - secureAreaRepository.addImplementation(softwareSecureArea) - - var credentialFactory = CredentialFactory() - credentialFactory.addCredentialImplementation(MdocCredential::class) { - document, dataItem -> MdocCredential(document, dataItem) - } - return DocumentStore(storageEngine, secureAreaRepository, credentialFactory) - } - } - - /** - * Parse a byte array as an X509 certificate - */ - private fun parseCertificate(certificateBytes: ByteArray): X509Certificate { - return CertificateFactory.getInstance("X509") - .generateCertificate(ByteArrayInputStream(certificateBytes)) as X509Certificate - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt b/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt deleted file mode 100644 index f4fa6fb12..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/MainActivity.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.android.identity.wallet - -import android.app.PendingIntent -import android.content.Intent -import android.net.Uri -import android.nfc.NfcAdapter -import android.os.Bundle -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.GravityCompat -import androidx.navigation.Navigation -import androidx.navigation.findNavController -import androidx.navigation.ui.NavigationUI -import androidx.navigation.ui.NavigationUI.setupActionBarWithNavController -import androidx.navigation.ui.setupWithNavController -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.mdoc.origininfo.OriginInfoDomain -import com.android.identity.util.Logger -import com.android.identity.wallet.databinding.ActivityMainBinding -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logError -import com.android.identity.wallet.util.logInfo -import com.android.identity.wallet.util.logWarning -import com.android.identity.wallet.viewmodel.ShareDocumentViewModel -import com.google.android.material.elevation.SurfaceColors - -class MainActivity : AppCompatActivity() { - - private val viewModel: ShareDocumentViewModel by viewModels() - private lateinit var binding: ActivityMainBinding - private lateinit var pendingIntent: PendingIntent - private var nfcAdapter: NfcAdapter? = null - - private val navController by lazy { - Navigation.findNavController(this, R.id.nav_host_fragment) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val color = SurfaceColors.SURFACE_2.getColor(this) - window.statusBarColor = color - window.navigationBarColor = color - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - DocumentManager.getInstance(this).registerDocuments() - setupDrawerLayout() - setupNfc() - onNewIntent(intent) - Logger.isDebugEnabled = PreferencesHelper.isDebugLoggingEnabled() - } - - private fun setupNfc() { - nfcAdapter = NfcAdapter.getDefaultAdapter(this) - // Create a generic PendingIntent that will be deliver to this activity. The NFC stack - // will fill in the intent with the details of the discovered tag before delivering to - // this activity. - val intent = Intent(this, javaClass).apply { - addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) - } - pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) - } - - private fun setupDrawerLayout() { - binding.nvSideDrawer.setupWithNavController(navController) - setupActionBarWithNavController(this, navController, binding.dlMainDrawer) - } - - override fun onResume() { - super.onResume() - nfcAdapter?.enableForegroundDispatch(this, pendingIntent, null, null) - } - - override fun onPause() { - super.onPause() - nfcAdapter?.disableForegroundDispatch(this) - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - log("New intent on Activity $intent") - - if (intent == null) { - return - } - - var mdocUri: String? = null - var mdocReferrerUri: String? = null - if (intent.scheme.equals("mdoc")) { - val uri = Uri.parse(intent.toUri(0)) - mdocUri = "mdoc://" + uri.authority - - mdocReferrerUri = intent.extras?.get(Intent.EXTRA_REFERRER)?.toString() - } - - if (mdocUri == null) { - logError("No mdoc:// URI") - return - } - logInfo("uri: $mdocUri") - - val originInfos = ArrayList() - if (mdocReferrerUri == null) { - logWarning("No referrer URI") - // TODO: maybe bail in the future if this isn't set. - } else { - logInfo("referrer: $mdocReferrerUri") - originInfos.add( - OriginInfoDomain( - mdocReferrerUri - ) - ) - } - - viewModel.startPresentationReverseEngagement(mdocUri, originInfos) - val navController = findNavController(R.id.nav_host_fragment) - navController.navigate(R.id.transferDocumentFragment) - } - - override fun onSupportNavigateUp(): Boolean { - return NavigationUI.navigateUp(navController, binding.dlMainDrawer) - } - - override fun onBackPressed() { - if (binding.dlMainDrawer.isDrawerOpen(GravityCompat.START)) { - binding.dlMainDrawer.closeDrawer(GravityCompat.START) - } else { - super.onBackPressed() - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/adapter/BindingAdapters.kt b/appholder/src/main/java/com/android/identity/wallet/adapter/BindingAdapters.kt deleted file mode 100644 index 36002d533..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/adapter/BindingAdapters.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.identity.wallet.adapter - -import android.icu.text.SimpleDateFormat -import android.widget.TextView -import androidx.databinding.BindingAdapter -import java.util.Calendar -import java.util.Locale - -@BindingAdapter("displayDateTime") -fun bindDisplayDateTime(view: TextView, calendar: Calendar?) { - calendar?.let { - val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSXXX", Locale.getDefault()) - view.text = df.format(it.time) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/adapter/DocumentAdapter.kt b/appholder/src/main/java/com/android/identity/wallet/adapter/DocumentAdapter.kt deleted file mode 100644 index d078b798a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/adapter/DocumentAdapter.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ -package com.android.identity.wallet.adapter - - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.DrawableRes -import androidx.navigation.findNavController -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.android.identity.wallet.R -import com.android.identity.wallet.databinding.ListItemDocumentBinding -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.wallet.SelectDocumentFragmentDirections - -/** - * Adapter for the [RecyclerView]. - */ -class DocumentAdapter : ListAdapter(DocumentDiffCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return DocumentViewHolder( - ListItemDocumentBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val document = getItem(position) - (holder as DocumentViewHolder).bind(document) - } - - class DocumentViewHolder( - private val binding: ListItemDocumentBinding - ) : RecyclerView.ViewHolder(binding.root) { - - init { - binding.setClickDetailListener { - binding.document?.let { doc -> - navigateToDetail(doc, it) - } - } - } - - private fun navigateToDetail(document: DocumentInformation, view: View) { - val direction = SelectDocumentFragmentDirections.toDocumentDetail(document.docName) - if (view.findNavController().currentDestination?.id == R.id.wallet) { - view.findNavController().navigate(direction) - } - } - - fun bind(item: DocumentInformation) { - binding.apply { - val cardArt = cardArtFor(item.documentColor) - binding.llItemContainer.setBackgroundResource(cardArt) - document = item - executePendingBindings() - } - } - - @DrawableRes - private fun cardArtFor(cardArt: Int): Int { - return when (cardArt) { - 1 -> R.drawable.yellow_gradient - 2 -> R.drawable.blue_gradient - 3 -> R.drawable.gradient_red - else -> R.drawable.green_gradient - } - } - } -} - -private class DocumentDiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: DocumentInformation, newItem: DocumentInformation): Boolean { - return oldItem.userVisibleName == newItem.userVisibleName - } - - override fun areContentsTheSame(oldItem: DocumentInformation, newItem: DocumentInformation): Boolean { - return oldItem == newItem - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt deleted file mode 100644 index 624f4f692..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/AuthConfirmationFragment.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import android.content.DialogInterface -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.R -import com.android.identity.wallet.support.SecureAreaSupport -import com.android.identity.wallet.theme.HolderAppTheme -import com.android.identity.wallet.transfer.AddDocumentToResponseResult -import com.android.identity.wallet.viewmodel.TransferDocumentViewModel -import com.google.android.material.bottomsheet.BottomSheetDialogFragment - -class AuthConfirmationFragment : BottomSheetDialogFragment() { - - private val viewModel: TransferDocumentViewModel by activityViewModels() - private val arguments by navArgs() - private var isSendingInProgress = mutableStateOf(false) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val elementsToSign = viewModel.requestedElements() - val sheetData = mapToConfirmationSheetData(elementsToSign) - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed) - setContent { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxWidth(), - title = getSubtitle(), - isTrustedReader = arguments.readerIsTrusted, - isSendingInProgress = isSendingInProgress.value, - sheetData = sheetData, - onElementToggled = { element -> viewModel.toggleSignedElement(element) }, - onConfirm = { sendResponse() }, - onCancel = { - dismiss() - cancelAuthorization() - } - ) - } - } - } - } - - override fun onCancel(dialog: DialogInterface) { - cancelAuthorization() - } - - private fun cancelAuthorization() { - viewModel.onAuthenticationCancelled() - } - - private fun mapToConfirmationSheetData( - elementsToSign: List - ): List { - return elementsToSign.map { documentData -> - viewModel.addDocumentForSigning(documentData) - val elements = documentData.requestedElements.map { element -> - viewModel.toggleSignedElement(element) - val displayName = stringValueFor( - documentData.requestedDocument.docType, - element.namespace, - element.value - ) - ConfirmationSheetData.DocumentElement(displayName, element) - } - ConfirmationSheetData(documentData.userReadableName, elements) - } - } - - private fun stringValueFor(docType: String, namespace: String, element: String): String { - return HolderApp.documentTypeRepositoryInstance - .getDocumentTypeForMdoc(docType) - ?.mdocDocumentType - ?.namespaces?.get( - namespace - )?.dataElements?.get(element)?.attribute?.displayName ?: element - } - - private fun sendResponse() { - isSendingInProgress.value = true - viewModel.sendResponseForSelection( - onResultReady = { - onSendResponseResult(it) - }) - } - - private fun getSubtitle(): String { - val readerCommonName = arguments.readerCommonName - val readerIsTrusted = arguments.readerIsTrusted - return if (readerCommonName != "") { - if (readerIsTrusted) { - getString(R.string.bio_auth_verifier_trusted_with_name, readerCommonName) - } else { - getString(R.string.bio_auth_verifier_untrusted_with_name, readerCommonName) - } - } else { - getString(R.string.bio_auth_verifier_anonymous) - } - } - - private fun onSendResponseResult(result: AddDocumentToResponseResult) { - when (result) { - is AddDocumentToResponseResult.DocumentLocked -> { - - val secureAreaSupport = SecureAreaSupport.getInstance( - requireContext(), - result.credential.secureArea - ) - with(secureAreaSupport) { - unlockKey( - credential = result.credential, - onKeyUnlocked = { keyUnlockData -> - viewModel.sendResponseForSelection( - onResultReady = { - onSendResponseResult(it) - }, - result.credential, - keyUnlockData - ) - }, - onUnlockFailure = { wasCancelled -> - if (wasCancelled) { - cancelAuthorization() - } else { - viewModel.closeConnection() - } - } - ) - } - } - - is AddDocumentToResponseResult.DocumentAdded -> { - if (result.signingKeyUsageLimitPassed) { - toast("Using previously used Auth Key") - } - findNavController().navigateUp() - } - } - } - - private fun toast(message: String, duration: Int = Toast.LENGTH_SHORT) { - Toast.makeText(requireContext(), message, duration).show() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheet.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheet.kt deleted file mode 100644 index f0912438e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheet.kt +++ /dev/null @@ -1,370 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.focusGroup -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -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.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Done -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.R -import com.android.identity.wallet.authconfirmation.ConfirmationSheetData.DocumentElement -import com.android.identity.wallet.theme.HolderAppTheme - -@Composable -fun ConfirmationSheet( - modifier: Modifier = Modifier, - title: String, - isTrustedReader: Boolean = false, - isSendingInProgress: Boolean = false, - sheetData: List = emptyList(), - onElementToggled: (element: RequestedElement) -> Unit = { }, - onConfirm: () -> Unit = {}, - onCancel: () -> Unit = {} -) { - Column(modifier = modifier) { - BottomSheetHandle( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) - if (isTrustedReader) { - TrustedReaderCheck( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) - } - Text( - text = title, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(16.dp)) - Box( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.4f) - ) { - DocumentElements(sheetData, onElementToggled) - if (isSendingInProgress) { - LoadingIndicator( - modifier = Modifier - .matchParentSize() - .padding(horizontal = 8.dp) - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.6f)), - ) - } - } - SheetActions( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - enabled = !isSendingInProgress, - onCancel = onCancel, - onConfirm = onConfirm - ) - } -} - -@Composable -private fun BottomSheetHandle( - modifier: Modifier = Modifier, -) { - Row(modifier = modifier, horizontalArrangement = Arrangement.Center) { - Spacer( - modifier = Modifier - .size(64.dp, 4.dp) - .clip(RoundedCornerShape(4.dp)) - .background(Color.Gray) - ) - } -} - -@Composable -private fun TrustedReaderCheck( - modifier: Modifier = Modifier, -) { - Row(modifier = modifier, horizontalArrangement = Arrangement.Center) { - Box( - modifier = Modifier - .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.primaryContainer) - .size(48.dp), - contentAlignment = Alignment.Center - ) { - Icon( - modifier = Modifier.size(24.dp), - imageVector = Icons.Default.Check, - contentDescription = "", - tint = MaterialTheme.colorScheme.primary - ) - } - } -} - -@Composable -private fun DocumentTitle( - modifier: Modifier = Modifier, - document: ConfirmationSheetData -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Text( - textAlign = TextAlign.Center, - text = document.documentName, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f) - ) - } -} - -@Composable -private fun ChipsRow( - modifier: Modifier = Modifier, - left: DocumentElement, - right: DocumentElement?, - onElementToggled: (element: RequestedElement) -> Unit -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - val chipModifier = if (right != null) Modifier.weight(1f) else Modifier - ElementChip( - modifier = chipModifier, - documentElement = left, - onElementToggled = onElementToggled - ) - right?.let { - Spacer(modifier = Modifier.width(8.dp)) - ElementChip( - modifier = chipModifier, - documentElement = right, - onElementToggled = onElementToggled - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ElementChip( - modifier: Modifier = Modifier, - documentElement: DocumentElement, - onElementToggled: (element: RequestedElement) -> Unit -) { - var isChecked by remember { mutableStateOf(true) } - FilterChip( - modifier = modifier, - selected = isChecked, - onClick = { - isChecked = !isChecked - onElementToggled(documentElement.requestedElement) - }, - label = { Text(text = documentElement.displayName) }, - leadingIcon = { - AnimatedVisibility(visible = isChecked) { - Icon( - imageVector = Icons.Filled.Done, - contentDescription = "", - modifier = Modifier.size(FilterChipDefaults.IconSize) - ) - } - } - ) -} - -@Composable -@OptIn(ExperimentalFoundationApi::class) -private fun DocumentElements( - sheetData: List, - onElementToggled: (element: RequestedElement) -> Unit -) { - LazyColumn(modifier = Modifier.focusGroup()) { - sheetData.forEach { document -> - stickyHeader { - DocumentTitle( - modifier = Modifier - .fillMaxWidth() - .height(42.dp) - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .clip(RoundedCornerShape(20.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant), - document = document - ) - } - val grouped = document.elements.chunked(2).map { pair -> - if (pair.size == 1) Pair(pair.first(), null) - else Pair(pair.first(), pair.last()) - } - items(grouped.size) { index -> - val items = grouped[index] - ChipsRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - left = items.first, - right = items.second, - onElementToggled = onElementToggled - ) - } - } - } -} - -@Composable -private fun LoadingIndicator( - modifier: Modifier = Modifier -) { - Box( - modifier = modifier, - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } -} - -@Composable -private fun SheetActions( - modifier: Modifier = Modifier, - enabled: Boolean, - onCancel: () -> Unit, - onConfirm: () -> Unit -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Button( - modifier = Modifier.weight(1f), - enabled = enabled, - onClick = { - if (enabled) { - onCancel() - } - } - ) { - Text(text = stringResource(id = R.string.bt_cancel)) - } - Spacer(modifier = Modifier.width(8.dp)) - Button( - modifier = Modifier.weight(1f), - enabled = enabled, - onClick = { - if (enabled) { - onConfirm() - } - } - ) { - Text(text = stringResource(id = R.string.btn_send_data)) - } - } -} - -@Composable -@Preview(name = "Default", showBackground = true) -@Preview(name = "Default", showBackground = true, uiMode = UI_MODE_NIGHT_YES) -private fun PreviewConfirmationSheet() { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxSize(), - title = "Title" - ) - } -} - -@Composable -@Preview(name = "Default With Trusted Reader", showBackground = true) -@Preview(name = "Default With Trusted Reader", showBackground = true, uiMode = UI_MODE_NIGHT_YES) -private fun PreviewConfirmationSheetTrustedReader() { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxSize(), - title = "Title", - isTrustedReader = true - ) - } -} - -@Composable -@Preview(name = "Document With Trusted Reader", showBackground = true) -@Preview(name = "Document With Trusted Reader", showBackground = true, uiMode = UI_MODE_NIGHT_YES) -private fun PreviewConfirmationSheetWithDocumentAndTrustedReader() { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxSize(), - title = "Trusted verifier 'Google' is requesting the following information", - isTrustedReader = true, - sheetData = listOf( - ConfirmationSheetData( - documentName = "Driving Licence | mDL", - elements = (1..11).map { DocumentElement("Property $it", RequestedElement("$it", "namespace")) } - ) - ) - ) - } -} - -@Composable -@Preview(name = "Sending progress", showBackground = true) -@Preview(name = "Sending progress", showBackground = true, uiMode = UI_MODE_NIGHT_YES) -private fun PreviewConfirmationSendingProgress() { - HolderAppTheme { - ConfirmationSheet( - modifier = Modifier.fillMaxSize(), - title = "Trusted verifier 'Google' is requesting the following information", - isSendingInProgress = true, - sheetData = listOf( - ConfirmationSheetData( - documentName = "Driving Licence | mDL", - elements = (1..11).map { DocumentElement("Property $it", RequestedElement("$it", "namespace")) } - ) - ) - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheetData.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheetData.kt deleted file mode 100644 index 3031c002c..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/ConfirmationSheetData.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable - -@Stable -@Immutable -data class ConfirmationSheetData( - val documentName: String, - val elements: List -) { - - data class DocumentElement( - val displayName: String, - val requestedElement: RequestedElement - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphraseAuthResult.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphraseAuthResult.kt deleted file mode 100644 index 1488a1108..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphraseAuthResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -sealed class PassphraseAuthResult { - object Idle: PassphraseAuthResult() - data class Success(val userPassphrase: String): PassphraseAuthResult() -} diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePrompt.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePrompt.kt deleted file mode 100644 index d4d09f2bb..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePrompt.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.PreviewLightDark -import com.android.identity.wallet.theme.HolderAppTheme - -class PassphrasePrompt : DialogFragment() { - - private val args by navArgs() - private val viewModel by activityViewModels() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - HolderAppTheme { - PassphrasePromptUI( - showIncorrectPassword = args.showIncorrectPassword, - onDone = { passphrase -> - viewModel.authorize(userPassphrase = passphrase) - findNavController().navigateUp() - } - ) - } - } - } - } -} - -@Composable -private fun PassphrasePromptUI( - showIncorrectPassword: Boolean, - onDone: (passphrase: String) -> Unit -) { - var value by remember { mutableStateOf("") } - Card( - modifier = Modifier.fillMaxWidth(), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = stringResource(id = R.string.passphrase_prompt_title), - style = MaterialTheme.typography.titleLarge - ) - Text( - text = stringResource(id = R.string.passphrase_prompt_message), - style = MaterialTheme.typography.titleSmall - ) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = value, - onValueChange = { value = it }, - textStyle = MaterialTheme.typography.bodyMedium, - visualTransformation = PasswordVisualTransformation(), - placeholder = { - Text( - text = stringResource(id = R.string.passphrase_prompt_hint), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f) - ) - } - ) - if (showIncorrectPassword) { - Text( - text = stringResource(id = R.string.passphrase_prompt_incorrect_passphrase), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.error - ) - } - TextButton( - modifier = Modifier - .align(Alignment.End), - onClick = { onDone(value) }) { - Text(text = stringResource(id = R.string.bt_ok)) - } - } - } -} - -@Composable -@PreviewLightDark -private fun PreviewPassphrasePrompt() { - HolderAppTheme { - PassphrasePromptUI( - showIncorrectPassword = false, - onDone = {} - ) - } -} - -@Composable -@PreviewLightDark -private fun PreviewPassphrasePromptWithIncorrectPassword() { - HolderAppTheme { - PassphrasePromptUI( - showIncorrectPassword = true, - onDone = {} - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePromptViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePromptViewModel.kt deleted file mode 100644 index 03a477134..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/PassphrasePromptViewModel.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import androidx.lifecycle.ViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update - -class PassphrasePromptViewModel : ViewModel() { - - private val _authorizationState = - MutableStateFlow(PassphraseAuthResult.Idle) - val authorizationState: StateFlow = _authorizationState - - fun authorize(userPassphrase: String) { - _authorizationState.update { PassphraseAuthResult.Success(userPassphrase) } - } - - fun reset() { - _authorizationState.update { PassphraseAuthResult.Idle } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedDocumentData.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedDocumentData.kt deleted file mode 100644 index dd0dd15f4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedDocumentData.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -import com.android.identity.mdoc.request.DeviceRequestParser - -data class RequestedDocumentData( - val userReadableName: String, - val identityCredentialName: String, - val requestedElements: ArrayList, - val requestedDocument: DeviceRequestParser.DocRequest -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedElement.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedElement.kt deleted file mode 100644 index e0e444cc9..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/RequestedElement.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -data class RequestedElement( - val namespace: String, - val value: String -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedDocumentData.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedDocumentData.kt deleted file mode 100644 index 786d95304..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedDocumentData.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -class SignedDocumentData( - private val signedElements: List, - val identityCredentialName: String, - val documentType: String, -) { - - fun issuerSignedEntries(): MutableMap> { - val byNamespace = signedElements.groupBy { it.namespace } - val result = mutableMapOf>() - byNamespace.forEach { (namespace, elements) -> - result[namespace] = elements.map { it.value } - } - return result - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedElementsCollection.kt b/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedElementsCollection.kt deleted file mode 100644 index 91bc2d80f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authconfirmation/SignedElementsCollection.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.android.identity.wallet.authconfirmation - -class SignedElementsCollection { - - private val requestedDocuments = mutableMapOf() - private val signedElements = mutableListOf() - - fun addNamespace(requestedData: RequestedDocumentData) { - this.requestedDocuments[requestedData.identityCredentialName] = requestedData - } - - fun toggleProperty(element: RequestedElement) { - if (!signedElements.remove(element)) { - signedElements.add(element) - } - } - - fun collect(): List { - return requestedDocuments.keys.map { namespace -> - val document = requestedDocuments.getValue(namespace) - SignedDocumentData( - signedElements = signedElements, - identityCredentialName = document.identityCredentialName, - documentType = document.requestedDocument.docType, - ) - } - } - - fun clear() { - requestedDocuments.clear() - signedElements.clear() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authprompt/BiometricUserAuthPrompt.kt b/appholder/src/main/java/com/android/identity/wallet/authprompt/BiometricUserAuthPrompt.kt deleted file mode 100644 index 8a0d6ea83..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authprompt/BiometricUserAuthPrompt.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.authprompt - -import androidx.biometric.BiometricPrompt - -class BiometricUserAuthPrompt( - private val prompt: BiometricPrompt, - private val promptInfo: BiometricPrompt.PromptInfo -) { - - fun authenticate(cryptoObject: BiometricPrompt.CryptoObject?) { - if (cryptoObject != null) { - prompt.authenticate(promptInfo, cryptoObject) - } else { - prompt.authenticate(promptInfo) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/authprompt/UserAuthPromptBuilder.kt b/appholder/src/main/java/com/android/identity/wallet/authprompt/UserAuthPromptBuilder.kt deleted file mode 100644 index 10a3fcdd0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/authprompt/UserAuthPromptBuilder.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.android.identity.wallet.authprompt - -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG -import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL -import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS -import androidx.biometric.BiometricPrompt -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import com.android.identity.wallet.util.log - -class UserAuthPromptBuilder private constructor(private val fragment: Fragment) { - - private var title: String = "" - private var subtitle: String = "" - private var description: String = "" - private var negativeButton: String = "" - private var forceLskf: Boolean = false - private var onSuccess: () -> Unit = {} - private var onFailure: () -> Unit = {} - private var onCancelled: () -> Unit = {} - - private val biometricAuthCallback = object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - super.onAuthenticationError(errorCode, errString) - // reached max attempts to authenticate the user, or authentication dialog was cancelled - if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) { - onCancelled.invoke() - } else { - log("User authentication failed $errorCode - $errString") - onFailure.invoke() - } - } - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - log("User authentication succeeded") - onSuccess.invoke() - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - log("User authentication failed") - onFailure.invoke() - } - } - - fun withTitle(title: String) = apply { - this.title = title - } - - fun withSubtitle(subtitle: String) = apply { - this.subtitle = subtitle - } - - fun withDescription(description: String) = apply { - this.description = description - } - - fun withNegativeButton(negativeButton: String) = apply { - this.negativeButton = negativeButton - } - - fun setForceLskf(forceLskf: Boolean) = apply { - this.forceLskf = forceLskf - } - - fun withSuccessCallback(onSuccess: () -> Unit) = apply { - this.onSuccess = onSuccess - } - - fun withFailureCallback(onFailure: () -> Unit) = apply { - this.onFailure = onFailure - } - - fun withCancelledCallback(onCancelled: () -> Unit) = apply { - this.onCancelled = onCancelled - } - - fun build(): BiometricUserAuthPrompt { - val promptInfoBuilder = BiometricPrompt.PromptInfo.Builder() - .setTitle(title) - .setSubtitle(subtitle) - .setDescription(description) - .setConfirmationRequired(false) - - if (forceLskf) { - // TODO: this works only on Android 11 or later but for now this is fine - // as this is just a reference/test app and this path is only hit if - // the user actually presses the "Use PIN" button. Longer term, we should - // fall back to using KeyGuard which will work on all Android versions. - promptInfoBuilder.setAllowedAuthenticators(DEVICE_CREDENTIAL) - } else { - val canUseBiometricAuth = BiometricManager - .from(fragment.requireContext()) - .canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS - if (canUseBiometricAuth) { - promptInfoBuilder.setNegativeButtonText(negativeButton) - } else { - // No biometrics enrolled, force use of LSKF - promptInfoBuilder.setDeviceCredentialAllowed(true) - } - } - - val promptInfo = promptInfoBuilder.build() - val executor = ContextCompat.getMainExecutor(fragment.requireContext()) - val prompt = BiometricPrompt(fragment, executor, biometricAuthCallback) - return BiometricUserAuthPrompt(prompt, promptInfo) - } - - companion object { - fun requestUserAuth(fragment: Fragment): UserAuthPromptBuilder { - return UserAuthPromptBuilder(fragment) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt deleted file mode 100644 index 45473e5af..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/AndroidSetupContainer.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Checkbox -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.state.AuthTypeState -import com.android.identity.wallet.selfsigned.OutlinedContainerVertical - -@Composable -fun AndroidSetupContainer( - modifier: Modifier = Modifier, - isOn: Boolean, - timeoutSeconds: Int, - lskfAuthTypeState: AuthTypeState, - biometricAuthTypeState: AuthTypeState, - useStrongBox: AuthTypeState, - onUserAuthenticationChanged: (isOn: Boolean) -> Unit, - onAuthTimeoutChanged: (authTimeout: Int) -> Unit, - onLskfAuthChanged: (isOn: Boolean) -> Unit, - onBiometricAuthChanged: (isOn: Boolean) -> Unit, - onStrongBoxChanged: (isOn: Boolean) -> Unit -) { - Column(modifier = modifier) { - OutlinedContainerVertical(modifier = Modifier.fillMaxWidth()) { - val labelOn = stringResource(id = R.string.user_authentication_on) - val labelOff = stringResource(id = R.string.user_authentication_off) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = if (isOn) labelOn else labelOff, - ) - Switch( - modifier = Modifier.padding(start = 8.dp), - checked = isOn, - onCheckedChange = onUserAuthenticationChanged - ) - } - AnimatedVisibility( - modifier = Modifier.fillMaxWidth(), - visible = isOn - ) { - Column(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = stringResource(id = R.string.keystore_android_user_auth_timeout) - ) - NumberChanger( - number = timeoutSeconds, - onNumberChanged = onAuthTimeoutChanged, - counterTextStyle = MaterialTheme.typography.titleLarge - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - val alpha = if (lskfAuthTypeState.canBeModified) 1f else .5f - ValueLabel( - modifier = Modifier - .weight(1f) - .alpha(alpha), - label = stringResource(id = R.string.user_auth_type_allow_lskf) - ) - Checkbox( - checked = lskfAuthTypeState.isEnabled, - onCheckedChange = onLskfAuthChanged, - enabled = lskfAuthTypeState.canBeModified - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - val alpha = if (biometricAuthTypeState.canBeModified) 1f else .5f - ValueLabel( - modifier = Modifier - .weight(1f) - .alpha(alpha), - label = stringResource(id = R.string.user_auth_type_allow_biometric) - ) - Checkbox( - checked = biometricAuthTypeState.isEnabled, - onCheckedChange = onBiometricAuthChanged, - enabled = biometricAuthTypeState.canBeModified - ) - } - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - val alpha = if (useStrongBox.canBeModified) 1f else .5f - ValueLabel( - modifier = Modifier - .weight(1f) - .alpha(alpha), - label = stringResource(id = R.string.user_auth_use_strong_box) - ) - Checkbox( - checked = useStrongBox.isEnabled, - onCheckedChange = onStrongBoxChanged, - enabled = useStrongBox.canBeModified - ) - } - } - } - } - } -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt deleted file mode 100644 index 0bed3a7c6..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveAndroid.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.DropdownMenu -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.stringResource -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.state.MdocAuthOption -import com.android.identity.wallet.composables.state.MdocAuthStateOption -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveOption -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState - -@Composable -fun AuthenticationKeyCurveAndroid( - modifier: Modifier = Modifier, - state: AndroidAuthKeyCurveState, - mDocAuthState: MdocAuthOption, - onAndroidAuthKeyCurveChanged: (newValue: AndroidAuthKeyCurveOption) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.authentication_key_curve_label) - ) { - var keyCurveDropDownExpanded by remember { mutableStateOf(false) } - val clickModifier = if (state.isEnabled) { - Modifier.clickable { keyCurveDropDownExpanded = true } - } else { - Modifier - } - val alpha = if (state.isEnabled) 1f else .5f - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha) - .then(clickModifier) - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = curveLabelFor(state.authCurve.toEcCurve()) - ) - DropDownIndicator() - } - DropdownMenu( - expanded = keyCurveDropDownExpanded, - onDismissRequest = { keyCurveDropDownExpanded = false } - ) { - val ecCurveOption = - if (mDocAuthState.mDocAuthentication == MdocAuthStateOption.ECDSA) { - AndroidAuthKeyCurveOption.Ed25519 - } else { - AndroidAuthKeyCurveOption.X25519 - } - TextDropDownRow( - label = curveLabelFor(curveOption = AndroidAuthKeyCurveOption.P_256.toEcCurve()), - onSelected = { - onAndroidAuthKeyCurveChanged(AndroidAuthKeyCurveOption.P_256) - keyCurveDropDownExpanded = false - } - ) - TextDropDownRow( - label = curveLabelFor(curveOption = ecCurveOption.toEcCurve()), - onSelected = { - onAndroidAuthKeyCurveChanged(ecCurveOption) - keyCurveDropDownExpanded = false - } - ) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt b/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt deleted file mode 100644 index 5f9f94be5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/AuthenticationKeyCurveSoftware.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.DropdownMenu -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.stringResource -import com.android.identity.wallet.R -import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveOption -import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveState -import com.android.identity.wallet.composables.state.MdocAuthOption -import com.android.identity.wallet.composables.state.MdocAuthStateOption - -@Composable -fun AuthenticationKeyCurveSoftware( - modifier: Modifier = Modifier, - state: SoftwareAuthKeyCurveState, - mDocAuthState: MdocAuthOption, - onSoftwareAuthKeyCurveChanged: (newValue: SoftwareAuthKeyCurveOption) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.authentication_key_curve_label) - ) { - var keyCurveDropDownExpanded by remember { mutableStateOf(false) } - val clickModifier = if (state.isEnabled) { - Modifier.clickable { keyCurveDropDownExpanded = true } - } else { - Modifier - } - val alpha = if (state.isEnabled) 1f else .5f - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha) - .then(clickModifier) - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = curveLabelFor(state.authCurve.toEcCurve()) - ) - DropDownIndicator() - } - val entries = - SoftwareAuthKeyCurveOption.values().toMutableList() - if (mDocAuthState.mDocAuthentication == MdocAuthStateOption.ECDSA) { - entries.remove(SoftwareAuthKeyCurveOption.X448) - entries.remove(SoftwareAuthKeyCurveOption.X25519) - } else { - entries.remove(SoftwareAuthKeyCurveOption.Ed448) - entries.remove(SoftwareAuthKeyCurveOption.Ed25519) - } - DropdownMenu( - expanded = keyCurveDropDownExpanded, - onDismissRequest = { keyCurveDropDownExpanded = false } - ) { - for (entry in entries) { - TextDropDownRow( - label = curveLabelFor(curveOption = entry.toEcCurve()), - onSelected = { - onSoftwareAuthKeyCurveChanged(entry) - keyCurveDropDownExpanded = false - } - ) - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt b/appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt deleted file mode 100644 index 4b5e48695..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/CounterInput.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun CounterInput( - modifier: Modifier = Modifier, - label: String, - value: Int, - onValueChange: (newValue: Int) -> Unit -) { - Column(modifier = modifier) { - OutlinedContainerHorizontal(modifier = Modifier.fillMaxWidth()) { - ValueLabel( - modifier = Modifier.weight(1f), - label = label - ) - NumberChanger(number = value, onNumberChanged = onValueChange) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/CurveLabelFor.kt b/appholder/src/main/java/com/android/identity/wallet/composables/CurveLabelFor.kt deleted file mode 100644 index 3f9a9e047..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/CurveLabelFor.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.android.identity.crypto.EcCurve -import com.android.identity.wallet.R - -@Composable -fun curveLabelFor( - curveOption: EcCurve -): String { - return when (curveOption) { - EcCurve.P256 -> stringResource(id = R.string.curve_p_256) - EcCurve.P384 -> stringResource(id = R.string.curve_p_384) - EcCurve.P521 -> stringResource(id = R.string.curve_p_521) - EcCurve.BRAINPOOLP256R1 -> stringResource(id = R.string.curve_brain_pool_p_256R1) - EcCurve.BRAINPOOLP320R1 -> stringResource(id = R.string.curve_brain_pool_p_320R1) - EcCurve.BRAINPOOLP384R1 -> stringResource(id = R.string.curve_brain_pool_p_384R1) - EcCurve.BRAINPOOLP512R1 -> stringResource(id = R.string.curve_brain_pool_p_512R1) - EcCurve.ED25519 -> stringResource(id = R.string.curve_ed25519) - EcCurve.X25519 -> stringResource(id = R.string.curve_x25519) - EcCurve.ED448 -> stringResource(id = R.string.curve_ed448) - EcCurve.X448 -> stringResource(id = R.string.curve_X448) - else -> "" - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt b/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt deleted file mode 100644 index 82066891a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/DocumentValues.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.ui.graphics.Brush -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.theme.BlueGradient -import com.android.identity.wallet.theme.GreenGradient -import com.android.identity.wallet.theme.RedGradient -import com.android.identity.wallet.theme.YellowGradient - -fun Int.toCardArt(): DocumentColor { - return when (this) { - 1 -> DocumentColor.Yellow - 2 -> DocumentColor.Blue - 3 -> DocumentColor.Red - else -> DocumentColor.Green - } -} - -fun gradientFor(cardArt: DocumentColor): Brush { - return when (cardArt) { - is DocumentColor.Green -> GreenGradient - is DocumentColor.Yellow -> YellowGradient - is DocumentColor.Blue -> BlueGradient - is DocumentColor.Red -> RedGradient - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt b/appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt deleted file mode 100644 index 06f09b8fe..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/DropDownIndicator.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun DropDownIndicator( - modifier: Modifier = Modifier -) { - Icon( - modifier = modifier, - imageVector = Icons.Default.ArrowDropDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt b/appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt deleted file mode 100644 index b642101d7..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/LabeledUserInput.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun LabeledUserInput( - modifier: Modifier = Modifier, - label: String, - content: @Composable () -> Unit -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row(modifier = Modifier.fillMaxWidth()) { - ValueLabel(label = label) - } - content() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/LoadingIndicator.kt b/appholder/src/main/java/com/android/identity/wallet/composables/LoadingIndicator.kt deleted file mode 100644 index 3732a1217..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/LoadingIndicator.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.android.identity.wallet.theme.HolderAppTheme - -@Composable -fun LoadingIndicator( - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .background(MaterialTheme.colorScheme.surface.copy(alpha = .5f)), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } -} - -@Preview -@Composable -private fun PreviewLoadingIndicator() { - HolderAppTheme { - LoadingIndicator(modifier = Modifier.fillMaxSize()) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt b/appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt deleted file mode 100644 index b52cd286a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/MdocAuthentication.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.DropdownMenu -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.stringResource -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.state.MdocAuthOption -import com.android.identity.wallet.composables.state.MdocAuthStateOption - -@Composable -fun MdocAuthentication( - modifier: Modifier = Modifier, - state: MdocAuthOption, - onMdocAuthOptionChange: (newValue: MdocAuthStateOption) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.mdoc_authentication_label) - ) { - var expanded by remember { mutableStateOf(false) } - val alpha = if (state.isEnabled) 1f else .5f - val clickModifier = if (state.isEnabled) { - Modifier.clickable { expanded = true } - } else { - Modifier - } - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha) - .then(clickModifier) - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = mdocAuthOptionLabelFor(state.mDocAuthentication) - ) - DropDownIndicator() - } - DropdownMenu( - modifier = Modifier.fillMaxWidth(0.8f), - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - TextDropDownRow( - label = stringResource(id = R.string.mdoc_auth_ecdsa), - onSelected = { - onMdocAuthOptionChange(MdocAuthStateOption.ECDSA) - expanded = false - } - ) - TextDropDownRow( - label = stringResource(id = R.string.mdoc_auth_mac), - onSelected = { - onMdocAuthOptionChange(MdocAuthStateOption.MAC) - expanded = false - } - ) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt b/appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt deleted file mode 100644 index e9e4ea904..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/NumberChanger.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.with -import androidx.compose.foundation.layout.Row -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Remove -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun NumberChanger( - modifier: Modifier = Modifier, - number: Int, - onNumberChanged: (newValue: Int) -> Unit, - counterTextStyle: TextStyle = MaterialTheme.typography.bodyLarge -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = { onNumberChanged(number - 1) }) { - Icon(imageVector = Icons.Default.Remove, contentDescription = null) - } - AnimatedContent( - targetState = number, - label = "", - transitionSpec = { - if (targetState > initialState) { - slideInVertically { -it } with slideOutVertically { it } - } else { - slideInVertically { it } with slideOutVertically { -it } - } - } - ) { count -> - Text( - text = "$count", - textAlign = TextAlign.Center, - style = counterTextStyle - ) - } - IconButton(onClick = { onNumberChanged(number + 1) }) { - Icon(imageVector = Icons.Default.Add, contentDescription = null) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt b/appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt deleted file mode 100644 index ceae2ab58..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/OutlinedContainerHorizontal.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -@Composable -fun OutlinedContainerHorizontal( - modifier: Modifier = Modifier, - outlineBorderWidth: Dp = 2.dp, - outlineBrush: Brush? = null, - content: @Composable RowScope.() -> Unit -) { - val brush = outlineBrush ?: SolidColor(MaterialTheme.colorScheme.outline) - Row( - modifier = modifier - .heightIn(48.dp) - .clip(RoundedCornerShape(12.dp)) - .border(outlineBorderWidth, brush, RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.inverseOnSurface), - verticalAlignment = Alignment.CenterVertically - ) { - Row( - modifier = Modifier.padding(horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - content() - } - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/Preview.kt b/appholder/src/main/java/com/android/identity/wallet/composables/Preview.kt deleted file mode 100644 index d5a9167b3..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/Preview.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.android.identity.wallet.composables - -import android.content.res.Configuration -import androidx.compose.ui.tooling.preview.Preview - -@Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO) -@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) -annotation class PreviewLightDark \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt b/appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt deleted file mode 100644 index 914da5c1e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/SoftwareSetupContainer.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.R - -@Composable -fun SoftwareSetupContainer( - modifier: Modifier = Modifier, - passphrase: String, - onPassphraseChanged: (newValue: String) -> Unit -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedContainerHorizontal(modifier = Modifier.fillMaxWidth()) { - Box(contentAlignment = Alignment.CenterStart) { - BasicTextField( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - textStyle = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface, - ), - value = passphrase, - onValueChange = onPassphraseChanged, - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface) - ) - if (passphrase.isEmpty()) { - Text( - text = stringResource(id = R.string.keystore_software_passphrase_hint), - style = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = .5f) - ), - ) - } - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt b/appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt deleted file mode 100644 index e9c9dc1d3..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/TextDropDownRow.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun TextDropDownRow( - modifier: Modifier = Modifier, - label: String, - onSelected: () -> Unit -) { - DropdownMenuItem( - modifier = modifier, - text = { - ValueLabel( - modifier = Modifier.fillMaxWidth(), - label = label - ) - }, - onClick = onSelected - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/Toast.kt b/appholder/src/main/java/com/android/identity/wallet/composables/Toast.kt deleted file mode 100644 index 1b920eba8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/Toast.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.composables - -import android.widget.Toast -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -@Composable -fun ShowToast(message: String, duration: Int = Toast.LENGTH_SHORT) { - Toast.makeText(LocalContext.current, message, duration).show() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt b/appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt deleted file mode 100644 index 2d1ff3c65..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/ValueLabel.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun ValueLabel( - modifier: Modifier = Modifier, - label: String -) { - Text( - modifier = modifier, - text = label, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelMedium - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt b/appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt deleted file mode 100644 index f86eaec29..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/mdocAuthOptionLabelFor.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.identity.wallet.composables - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.state.MdocAuthStateOption - -@Composable -fun mdocAuthOptionLabelFor( - state: MdocAuthStateOption -): String { - return when (state) { - MdocAuthStateOption.ECDSA -> - stringResource(id = R.string.mdoc_auth_ecdsa) - - MdocAuthStateOption.MAC -> - stringResource(id = R.string.mdoc_auth_mac) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt deleted file mode 100644 index 67236282b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/state/AuthTypeState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.composables.state - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AuthTypeState( - val isEnabled: Boolean = true, - val canBeModified: Boolean = false -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt deleted file mode 100644 index 06c424f66..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthOption.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.composables.state - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class MdocAuthOption( - val isEnabled: Boolean = true, - val mDocAuthentication: MdocAuthStateOption = MdocAuthStateOption.ECDSA -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt b/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt deleted file mode 100644 index eb6a7923b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/composables/state/MdocAuthStateOption.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.android.identity.wallet.composables.state - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class MdocAuthStateOption : Parcelable { - ECDSA, MAC -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialEntry.kt b/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialEntry.kt deleted file mode 100644 index 36cda199e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialEntry.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.android.mdl.app.credman - -import android.graphics.Bitmap -import java.io.ByteArrayOutputStream -import org.json.JSONArray -import org.json.JSONObject - -class IdentityCredentialEntry( - val id: Long, - val format: String, - val title: String, - val subtitle: String, - val icon: Bitmap, - val fields: List, - val disclaimer: String?, - val warning: String?, -) { - fun getIconBytes(): ByteArrayOutputStream { - val scaledIcon = Bitmap.createScaledBitmap(icon, 128, 128, true) - val stream = ByteArrayOutputStream() - scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, stream) - return stream - } - - fun toJson(iconIndex: Int?): JSONObject { - val credential = JSONObject() - credential.put("format", format) - val displayInfo = JSONObject() - displayInfo.put("title", title) - displayInfo.putOpt("subtitle", subtitle) - displayInfo.putOpt("disclaimer", disclaimer) - displayInfo.putOpt("warning", warning) - displayInfo.putOpt("icon_id", iconIndex) - credential.put("display_info", displayInfo) - val fieldsJson = JSONArray() - fields.forEach { fieldsJson.put(it.toJson()) } - credential.put("fields", fieldsJson) - - val result = JSONObject() - result.put("id", id) - result.put("credential", credential) - return result - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialField.kt b/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialField.kt deleted file mode 100644 index b3ffd1c05..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialField.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.mdl.app.credman - -import org.json.JSONObject - -class IdentityCredentialField( - val name: String, - val value: Any?, - val displayName: String, - val displayValue: String?, -) { - fun toJson(): JSONObject { - val field = JSONObject() - field.put("name", name) - field.putOpt("value", value) - field.put("display_name", displayName) - field.putOpt("display_value", displayValue) - return field - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialRegistry.kt b/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialRegistry.kt deleted file mode 100644 index 44025da28..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/credman/IdentityCredentialRegistry.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.android.mdl.app.credman - -import android.content.Context -import com.google.android.gms.identitycredentials.RegistrationRequest -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -import java.nio.ByteOrder -import org.json.JSONArray -import org.json.JSONObject - -class IdentityCredentialRegistry( - val entries: List, -) { - fun toRegistrationRequest(context: Context): RegistrationRequest { - - return RegistrationRequest( - credentials = credentialBytes(), - matcher = loadMatcher(context), - type = "com.credman.IdentityCredential", - requestType = "", - protocolTypes = emptyList(), - ) - } - - private fun loadMatcher(context: Context): ByteArray { - val stream = context.assets.open("identitycredentialmatcher.wasm"); - val matcher = ByteArray(stream.available()) - stream.read(matcher) - stream.close() - return matcher - } - - private fun credentialBytes(): ByteArray { - val json = JSONObject() - val credListJson = JSONArray() - val icons = ByteArrayOutputStream() - val iconSizeList = mutableListOf() - entries.forEach { entry -> - val iconBytes = entry.getIconBytes() - credListJson.put(entry.toJson(iconSizeList.size)) - iconSizeList.add(iconBytes.size()) - iconBytes.writeTo(icons) - } - json.put("credentials", credListJson) - val credsBytes = json.toString(0).toByteArray() - val result = ByteArrayOutputStream() - // header_size - result.write(intBytes((3 + iconSizeList.size) * Int.SIZE_BYTES)) - // creds_size - result.write(intBytes(credsBytes.size)) - // icon_size_array_size - result.write(intBytes(iconSizeList.size)) - // icon offsets - iconSizeList.forEach { result.write(intBytes(it)) } - result.write(credsBytes) - icons.writeTo(result) - return result.toByteArray() - } - - companion object { - fun intBytes(num: Int): ByteArray = - ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(num).array() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/document/DocumentColor.kt b/appholder/src/main/java/com/android/identity/wallet/document/DocumentColor.kt deleted file mode 100644 index dde802c79..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/document/DocumentColor.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.android.identity.wallet.document - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -sealed class DocumentColor(val value: Int) : Parcelable { - object Green : DocumentColor(0) - object Yellow : DocumentColor(1) - object Blue : DocumentColor(2) - object Red : DocumentColor(3) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt b/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt deleted file mode 100644 index b78174e00..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/document/DocumentInformation.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.android.identity.wallet.document - -import com.android.identity.crypto.EcCurve -import com.android.identity.securearea.KeyPurpose - -data class DocumentInformation( - val userVisibleName: String, - val docName: String, - val docType: String, - val dateProvisioned: String, - val selfSigned: Boolean, - val documentColor: Int, - val maxUsagesPerKey: Int, - val lastTimeUsed: String, - val authKeys: List -) { - - data class KeyData( - val counter: Int, - val validFrom: String, - val validUntil: String, - val domain: String, - val issuerDataBytesCount: Int, - val usagesCount: Int, - val keyPurposes: KeyPurpose, - val ecCurve: EcCurve, - val isHardwareBacked: Boolean, - val secureAreaDisplayName: String - ) -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/document/DocumentManager.kt b/appholder/src/main/java/com/android/identity/wallet/document/DocumentManager.kt deleted file mode 100644 index 829b9c0b7..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/document/DocumentManager.kt +++ /dev/null @@ -1,350 +0,0 @@ -package com.android.identity.wallet.document - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import co.nstant.`in`.cbor.CborBuilder -import co.nstant.`in`.cbor.model.DataItem -import co.nstant.`in`.cbor.model.UnicodeString -import com.android.identity.cbor.Cbor -import com.android.identity.document.Document -import com.android.identity.document.NameSpacedData -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.wallet.selfsigned.SelfSignedDocumentData -import com.android.identity.wallet.util.Field -import com.android.identity.wallet.util.FormatUtil -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.util.ProvisioningUtil -import com.android.identity.wallet.util.ProvisioningUtil.Companion.toDocumentInformation -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logError -import com.android.identity.wallet.R -import com.android.mdl.app.credman.IdentityCredentialEntry -import com.android.mdl.app.credman.IdentityCredentialField -import com.android.mdl.app.credman.IdentityCredentialRegistry -import com.google.android.gms.identitycredentials.IdentityCredentialManager -import java.io.ByteArrayOutputStream -import java.util.Locale - -class DocumentManager private constructor(private val context: Context) { - val client = IdentityCredentialManager.Companion.getClient(context) - companion object { - - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: DocumentManager? = null - - fun getInstance(context: Context) = - instance ?: synchronized(this) { - instance ?: DocumentManager(context).also { instance = it } - } - } - - fun getDocumentInformation(documentName: String): DocumentInformation? { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - val document = documentStore.lookupDocument(documentName) - return document.toDocumentInformation() - } - - fun getDocumentByName(documentName: String): Document? { - val documentInfo = getDocumentInformation(documentName) - documentInfo?.let { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - return documentStore.lookupDocument(documentName) - } - return null - } - - fun getDataElementDisplayName(docTypeName : String, - nameSpaceName : String, - dataElementName : String): String { - val credType = HolderApp.documentTypeRepositoryInstance.getDocumentTypeForMdoc(docTypeName) - if (credType != null) { - val mdocDataElement = credType.mdocDocumentType!! - .namespaces[nameSpaceName]?.dataElements?.get(dataElementName) - if (mdocDataElement != null) { - return mdocDataElement.attribute.displayName - } - } - return dataElementName - } - - fun registerDocuments() { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - var idCount = 0L - val entries = documentStore.listDocuments().map { documentId -> - val document = documentStore.lookupDocument(documentId)!! - val documentInformation = document.toDocumentInformation()!! - - val fields = mutableListOf() - fields.add(IdentityCredentialField( - name = "doctype", - value = documentInformation.docType, - displayName = "Document Type", - displayValue = documentInformation.docType - )) - - val nameSpacedData = document.applicationData.getNameSpacedData("documentData") - nameSpacedData.nameSpaceNames.map {nameSpaceName -> - nameSpacedData.getDataElementNames(nameSpaceName).map {dataElementName -> - val fieldName = nameSpaceName + "." + dataElementName - val valueCbor = nameSpacedData.getDataElement(nameSpaceName, dataElementName) - var valueString = Cbor.toDiagnostics(valueCbor) - // Workaround for Credman not supporting images yet - if (dataElementName.equals("portrait") || dataElementName.equals("signature_usual_mark")) { - valueString = String.format(Locale.US, "%d bytes", valueCbor.size) - } - val dataElementDisplayName = getDataElementDisplayName(documentInformation.docType, nameSpaceName, dataElementName) - fields.add(IdentityCredentialField( - name = fieldName, - value = valueString, - displayName = dataElementDisplayName, - displayValue = valueString - )) - log("Adding field $fieldName ('$dataElementDisplayName') with value '$valueString'") - } - } - - log("Adding document ${documentInformation.userVisibleName}") - IdentityCredentialEntry( - id = idCount++, - format = "mdoc", - title = documentInformation.userVisibleName, - subtitle = context.getString(R.string.app_name), - icon = BitmapFactory.decodeResource(context.resources, R.drawable.driving_license_bg), - fields = fields.toList(), - disclaimer = null, - warning = null, - ) - } - val registry = IdentityCredentialRegistry(entries) - client.registerCredentials(registry.toRegistrationRequest(context)) - .addOnSuccessListener { log("CredMan registry succeeded") } - .addOnFailureListener { logError("CredMan registry failed $it") } - } - - fun getDocuments(): List { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - return documentStore.listDocuments().mapNotNull { documentName -> - val document = documentStore.lookupDocument(documentName) - document.toDocumentInformation() - } - } - - - fun deleteCredentialByName(documentName: String) { - val document = getDocumentInformation(documentName) - document?.let { - val documentStore = ProvisioningUtil.getInstance(context).documentStore - documentStore.deleteDocument(documentName) - } - registerDocuments() - } - - fun createSelfSignedDocument(documentData: SelfSignedDocumentData) { - val docName = getUniqueDocumentName(documentData) - documentData.provisionInfo.docName = docName - try { - provisionSelfSignedDocument(documentData) - } catch (e: Exception) { - throw IllegalStateException("Error creating self signed document", e) - } - registerDocuments() - } - - private fun getUniqueDocumentName( - documentData: SelfSignedDocumentData, - docName: String = documentData.provisionInfo.docName, - count: Int = 1 - ): String { - val store = ProvisioningUtil.getInstance(context).documentStore - store.listDocuments().forEach { name -> - if (name == docName) { - return getUniqueDocumentName(documentData, "$docName ($count)", count + 1) - } - } - return docName - } - - private fun provisionSelfSignedDocument(documentData: SelfSignedDocumentData) { - val builder = NameSpacedData.Builder() - - for (field in documentData.fields.filter { it.parentId == null }) { - when (field.fieldType) { - is DocumentAttributeType.Date, DocumentAttributeType.DateTime -> { - val date = UnicodeString(field.getValueString()) - date.setTag(1004) - builder.putEntry( - field.namespace!!, - field.name, - FormatUtil.cborEncode(date) - ) - } - - is DocumentAttributeType.Number -> { - builder.putEntryNumber( - field.namespace!!, - field.name, - field.getValueLong() - ) - } - - is DocumentAttributeType.Boolean -> { - builder.putEntryBoolean( - field.namespace!!, - field.name, - field.getValueBoolean() - ) - } - - is DocumentAttributeType.Picture -> { - val bitmap = field.getValueBitmap() - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos) - val bytes = baos.toByteArray() - builder.putEntryByteString(field.namespace!!, field.name, bytes) - } - - is DocumentAttributeType.IntegerOptions -> { - if (field.hasValue()) { - builder.putEntryNumber( - field.namespace!!, - field.name, - field.getValueLong() - ) - } - } - - is DocumentAttributeType.ComplexType -> { - - val dataItem = when (field.isArray) { - true -> { - createArrayDataItem(field, documentData) - } - - false -> { - createMapDataItem(field, documentData) - } - } - builder.putEntry( - field.namespace!!, - field.name, - FormatUtil.cborEncode(dataItem) - ) - } - - else -> { - - builder.putEntryString( - field.namespace!!, - field.name, - field.getValueString() - ) - } - } - } - ProvisioningUtil.getInstance(context) - .provisionSelfSigned(builder.build(), documentData.provisionInfo) - } - - private fun createArrayDataItem(field: Field, documentData: SelfSignedDocumentData): DataItem { - val childFields = documentData.fields.filter { it.parentId == field.id } - val childDataItems = mutableListOf() - - val fieldsPerItem = childFields.distinctBy { it.name }.count() - val itemCount = childFields.count() / fieldsPerItem - - for (i in 0 until itemCount) { - childDataItems.add( - createMapDataItem( - childFields.subList( - i * fieldsPerItem, - (i + 1) * fieldsPerItem - ), documentData - ) - ) - } - - val arrayBuilder = CborBuilder().addArray() - for (childDataItem in childDataItems) { - arrayBuilder.add(childDataItem) - } - return arrayBuilder.end().build()[0] - } - - - private fun createMapDataItem(field: Field, documentData: SelfSignedDocumentData): DataItem { - val childFields = documentData.fields.filter { it.parentId == field.id } - return createMapDataItem(childFields, documentData) - } - - private fun createMapDataItem( - fields: List, - documentData: SelfSignedDocumentData - ): DataItem { - val mapBuilder = CborBuilder().addMap() - for (field in fields) { - when (field.fieldType) { - is DocumentAttributeType.Date, DocumentAttributeType.DateTime -> { - val date = UnicodeString(field.getValueString()) - date.setTag(1004) - mapBuilder.put( - UnicodeString(field.name), - date - ) - } - - is DocumentAttributeType.Boolean -> { - mapBuilder.put( - field.name, - field.getValueBoolean() - ) - } - - is DocumentAttributeType.Picture -> { - val bitmap = field.getValueBitmap() - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos) - val bytes = baos.toByteArray() - mapBuilder.put(field.name, bytes) - } - - is DocumentAttributeType.IntegerOptions, - is DocumentAttributeType.Number -> { - if (field.value != "") { - mapBuilder.put( - field.name, - field.getValueLong() - ) - } - } - - is DocumentAttributeType.ComplexType -> { - val dataItem = when (field.isArray) { - true -> { - createArrayDataItem(field, documentData) - } - - false -> { - createMapDataItem(field, documentData) - } - } - mapBuilder.put(UnicodeString(field.name), dataItem) - } - - else -> { - mapBuilder.put(field.name, field.value as String) - } - } - } - - return mapBuilder.end().build()[0] - } - - fun refreshCredentials(documentName: String) { - val documentInformation = requireNotNull(getDocumentInformation(documentName)) - val document = requireNotNull(getDocumentByName(documentName)) - ProvisioningUtil.getInstance(context).refreshCredentials(document, documentInformation.docType) - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/document/KeysAndCertificates.kt b/appholder/src/main/java/com/android/identity/wallet/document/KeysAndCertificates.kt deleted file mode 100644 index a06a881e5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/document/KeysAndCertificates.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.android.identity.wallet.document - -import android.content.Context -import com.android.identity.wallet.R -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.io.InputStream -import java.nio.charset.StandardCharsets -import java.security.KeyFactory -import java.security.KeyPair -import java.security.PrivateKey -import java.security.PublicKey -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import java.util.Base64 -object KeysAndCertificates { - - private fun getCertificate(context: Context, resourceId: Int): X509Certificate { - val certInputStream = context.resources.openRawResource(resourceId) - val factory: CertificateFactory = CertificateFactory.getInstance("X509") - return factory.generateCertificate(certInputStream) as X509Certificate - } - - private fun getKeyBytes(keyInputStream: InputStream): ByteArray { - val keyBytes = keyInputStream.readBytes() - val publicKeyPEM = String(keyBytes, StandardCharsets.US_ASCII) - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----BEGIN PRIVATE KEY-----", "") - .replace("\r", "") - .replace("\n", "") - .replace("-----END PUBLIC KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - - return Base64.getDecoder().decode(publicKeyPEM) - } - - private fun getPrivateKey(context: Context, resourceId: Int): PrivateKey { - val keyBytes: ByteArray = getKeyBytes(context.resources.openRawResource(resourceId)) - val spec = PKCS8EncodedKeySpec(keyBytes) - val kf = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) - return kf.generatePrivate(spec) - } - - private fun getPublicKey(context: Context, resourceId: Int): PublicKey { - val keyBytes: ByteArray = getKeyBytes(context.resources.openRawResource(resourceId)) - val spec = X509EncodedKeySpec(keyBytes) - val kf = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) - return kf.generatePublic(spec) - } - - fun getMdlDsKeyPair(context: Context) = - KeyPair( - getPublicKey(context, R.raw.google_mdl_ds_cert_iaca_2_pubkey), - getPrivateKey(context, R.raw.google_mdl_ds_cert_iaca_2_privkey) - ) - - fun getMekbDsKeyPair(context: Context) = - KeyPair( - getPublicKey(context, R.raw.google_mekb_ds_pubkey), - getPrivateKey(context, R.raw.google_mekb_ds_privkey) - ) - - fun getMicovDsKeyPair(context: Context) = - KeyPair( - getPublicKey(context, R.raw.google_micov_ds_pubkey), - getPrivateKey(context, R.raw.google_micov_ds_privkey) - ) - - fun getMdlDsCertificate(context: Context) = getCertificate(context, R.raw.google_mdl_ds_cert_iaca_2) - - fun getMekbDsCertificate(context: Context) = getCertificate(context, R.raw.google_mekb_ds_cert) - - fun getMicovDsCertificate(context: Context) = getCertificate(context, R.raw.google_micov_ds_cert) - - fun getTrustedReaderCertificates(context: Context) = - listOf( - getCertificate(context, R.raw.bdr_iaca_cert), - getCertificate(context, R.raw.bdr_reader_ca_cert), - getCertificate(context, R.raw.credenceid_mdl_reader_cert), - getCertificate(context, R.raw.fast_reader_auth_cer), - getCertificate(context, R.raw.google_reader_ca), - getCertificate(context, R.raw.hid_test_reader_ca_mdl_cert), - getCertificate(context, R.raw.hidtestiacamdl_cert), - getCertificate(context, R.raw.iaca_zetes), - getCertificate(context, R.raw.idemia_brisbane_interop_readerauthca), - getCertificate(context, R.raw.louisiana_department_of_motor_vehicles_cert), - getCertificate(context, R.raw.nist_reader_ca_cer), - getCertificate(context, R.raw.reader_ca_nec_reader_ca_cer), - getCertificate(context, R.raw.samsung_iaca_test_cert), - getCertificate(context, R.raw.scytales_root_ca), - getCertificate(context, R.raw.spruce_iaca_cert), - getCertificate(context, R.raw.ul_cert_ca_01), - getCertificate(context, R.raw.ul_cert_ca_01_cer), - getCertificate(context, R.raw.ul_cert_ca_02), - getCertificate(context, R.raw.ul_cert_ca_03_cer), - getCertificate(context, R.raw.ul_cert_ca_02_cer), - getCertificate(context, R.raw.utms_reader_ca), - getCertificate(context, R.raw.utms_reader_ca_cer), - getCertificate(context, R.raw.zetes_reader_ca), - getCertificate(context, R.raw.zetes_reader_ca_cer), - getCertificate(context, R.raw.owf_identity_credential_reader_cert), - ) - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentDataReader.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentDataReader.kt deleted file mode 100644 index 6b3f28727..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentDataReader.kt +++ /dev/null @@ -1,261 +0,0 @@ -package com.android.identity.wallet.documentdata - -import android.graphics.BitmapFactory -import co.nstant.`in`.cbor.CborDecoder -import co.nstant.`in`.cbor.CborException -import co.nstant.`in`.cbor.model.AbstractFloat -import co.nstant.`in`.cbor.model.Array -import co.nstant.`in`.cbor.model.ByteString -import co.nstant.`in`.cbor.model.DataItem -import co.nstant.`in`.cbor.model.DoublePrecisionFloat -import co.nstant.`in`.cbor.model.MajorType -import co.nstant.`in`.cbor.model.Map -import co.nstant.`in`.cbor.model.NegativeInteger -import co.nstant.`in`.cbor.model.SimpleValue -import co.nstant.`in`.cbor.model.SimpleValueType -import co.nstant.`in`.cbor.model.UnicodeString -import co.nstant.`in`.cbor.model.UnsignedInteger -import com.android.identity.document.NameSpacedData -import com.android.identity.wallet.util.DocumentData.EU_PID_DOCTYPE -import com.android.identity.wallet.util.DocumentData.EU_PID_NAMESPACE -import com.android.identity.wallet.util.DocumentData.MDL_DOCTYPE -import com.android.identity.wallet.util.DocumentData.MDL_NAMESPACE -import com.android.identity.wallet.util.DocumentData.MICOV_ATT_NAMESPACE -import com.android.identity.wallet.util.DocumentData.MICOV_DOCTYPE -import java.io.ByteArrayInputStream -import java.math.BigInteger -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols -import java.util.Locale - -class DocumentDataReader(private val docType: String) { - - fun read(nameSpacedData: NameSpacedData): DocumentElements { - val builder = StringBuilder() - var portraitBytes: ByteArray? = null - var signatureBytes: ByteArray? = null - var fingerprintBytes: ByteArray? = null - - nameSpacedData.nameSpaceNames.forEach { namespace -> - val dataElementNames = nameSpacedData.getDataElementNames(namespace) - builder.append("
") - builder.append("
Namespace: $namespace
") - builder.append("

") - dataElementNames.forEach { entryName -> - val byteArray: ByteArray = nameSpacedData.getDataElement(namespace, entryName) - byteArray.let { value -> - val valueStr: String - if (isPortraitElement(docType, namespace, entryName)) { - valueStr = String.format("(%d bytes, shown above)", value.size) - portraitBytes = nameSpacedData.getDataElementByteString(namespace, entryName) - } else if (docType == MICOV_DOCTYPE && namespace == MICOV_ATT_NAMESPACE && entryName == "fac") { - valueStr = String.format("(%d bytes, shown above)", value.size) - portraitBytes = nameSpacedData.getDataElement(namespace, entryName) - } else if (docType == MDL_DOCTYPE && namespace == MDL_NAMESPACE && entryName == "extra") { - valueStr = String.format("%d bytes extra data", value.size) - } else if (docType == MDL_DOCTYPE && namespace == MDL_NAMESPACE && entryName == "signature_usual_mark") { - valueStr = String.format("(%d bytes, shown below)", value.size) - signatureBytes = nameSpacedData.getDataElementByteString(namespace, entryName) - } else if (docType == EU_PID_DOCTYPE && namespace == EU_PID_NAMESPACE && entryName == "biometric_template_finger") { - valueStr = String.format("(%d bytes, not shown)", value.size) - fingerprintBytes = nameSpacedData.getDataElementByteString(namespace, entryName) - } else { - valueStr = cborPrettyPrint(value) - } - builder.append("$entryName -> $valueStr
") - } - builder.append("


") - } - } - val portrait = portraitBytes?.let { bytes -> - BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - } - val signature = signatureBytes?.let { bytes -> - BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - } - val fingerprint = fingerprintBytes?.let { bytes -> - BitmapFactory.decodeByteArray(bytes, 0, bytes.size) - } - - return DocumentElements( - text = builder.toString(), - portrait = portrait, - signature = signature, - fingerprint = fingerprint - ) - } - - private fun isPortraitElement( - docType: String, - namespace: String?, - entryName: String? - ): Boolean { - val hasPortrait = docType == MDL_DOCTYPE || docType == EU_PID_DOCTYPE - val namespaceContainsPortrait = namespace == MDL_NAMESPACE || namespace == EU_PID_NAMESPACE - return hasPortrait && namespaceContainsPortrait && entryName == "portrait" - } - - private fun cborPrettyPrint(encodedBytes: ByteArray): String { - val newLine = "
" - val sb = java.lang.StringBuilder() - val bais = ByteArrayInputStream(encodedBytes) - val dataItems = try { - CborDecoder(bais).decode() - } catch (e: CborException) { - throw java.lang.IllegalStateException(e) - } - for ((count, dataItem) in dataItems.withIndex()) { - if (count > 0) { - sb.append(",$newLine") - } - cborPrettyPrintDataItem(sb, 0, dataItem) - } - return sb.toString() - } - - private fun cborPrettyPrintDataItem( - sb: java.lang.StringBuilder, indent: Int, - dataItem: DataItem - ) { - val space = " " - val newLine = "
" - val indentBuilder = java.lang.StringBuilder() - for (n in 0 until indent) { - indentBuilder.append(space) - } - val indentString = indentBuilder.toString() - if (dataItem.hasTag()) { - sb.append(String.format("tag %d ", dataItem.tag.value)) - } - when (dataItem.majorType) { - MajorType.INVALID -> // TODO: throw - sb.append("**invalid**") - - MajorType.UNSIGNED_INTEGER -> { - // Major type 0: an unsigned integer. - val value: BigInteger = (dataItem as UnsignedInteger).value - sb.append(value) - } - - MajorType.NEGATIVE_INTEGER -> { - // Major type 1: a negative integer. - val value: BigInteger = (dataItem as NegativeInteger).value - sb.append(value) - } - - MajorType.BYTE_STRING -> { - // Major type 2: a byte string. - val value = (dataItem as ByteString).bytes - sb.append("[") - for ((count, b) in value.withIndex()) { - if (count > 0) { - sb.append(", ") - } - sb.append(String.format("0x%02x", b)) - } - sb.append("]") - } - - MajorType.UNICODE_STRING -> { - // Major type 3: string of Unicode characters that is encoded as UTF-8 [RFC3629]. - val value = (dataItem as UnicodeString).string - // TODO: escape ' in |value| - sb.append("'$value'") - } - - MajorType.ARRAY -> { - - // Major type 4: an array of data items. - val items = (dataItem as Array).dataItems - if (items.size == 0) { - sb.append("[]") - } else if (cborAreAllDataItemsNonCompound(items)) { - // The case where everything fits on one line. - sb.append("[") - for ((count, item) in items.withIndex()) { - cborPrettyPrintDataItem(sb, indent, item) - if (count + 1 < items.size) { - sb.append(", ") - } - } - sb.append("]") - } else { - sb.append("[$newLine$indentString") - for ((count, item) in items.withIndex()) { - sb.append("$space$space") - cborPrettyPrintDataItem(sb, indent + 2, item) - if (count + 1 < items.size) { - sb.append(",") - } - sb.append("$newLine $indentString") - } - sb.append("]") - } - } - - MajorType.MAP -> { - // Major type 5: a map of pairs of data items. - val keys = (dataItem as Map).keys - if (keys.isEmpty()) { - sb.append("{}") - } else { - sb.append("{$newLine$indentString") - for ((count, key) in keys.withIndex()) { - sb.append("$space$space") - val value = dataItem[key] - cborPrettyPrintDataItem(sb, indent + 2, key) - sb.append(" : ") - cborPrettyPrintDataItem(sb, indent + 2, value) - if (count + 1 < keys.size) { - sb.append(",") - } - sb.append("$newLine $indentString") - } - sb.append("}") - } - } - - MajorType.TAG -> throw java.lang.IllegalStateException("Semantic tag data item not expected") - MajorType.SPECIAL -> // Major type 7: floating point numbers and simple data types that need no - // content, as well as the "break" stop code. - if (dataItem is SimpleValue) { - when (dataItem.simpleValueType) { - SimpleValueType.FALSE -> sb.append("false") - SimpleValueType.TRUE -> sb.append("true") - SimpleValueType.NULL -> sb.append("null") - SimpleValueType.UNDEFINED -> sb.append("undefined") - SimpleValueType.RESERVED -> sb.append("reserved") - SimpleValueType.UNALLOCATED -> sb.append("unallocated") - } - } else if (dataItem is DoublePrecisionFloat) { - val df = DecimalFormat( - "0", - DecimalFormatSymbols.getInstance(Locale.ENGLISH) - ) - df.maximumFractionDigits = 340 - sb.append(df.format(dataItem.value)) - } else if (dataItem is AbstractFloat) { - val df = DecimalFormat( - "0", - DecimalFormatSymbols.getInstance(Locale.ENGLISH) - ) - df.maximumFractionDigits = 340 - sb.append(df.format(dataItem.value)) - } else { - sb.append("break") - } - } - } - - // Returns true iff all elements in |items| are not compound (e.g. an array or a map). - private fun cborAreAllDataItemsNonCompound(items: List): Boolean { - for (item in items) { - when (item.majorType) { - MajorType.ARRAY, MajorType.MAP -> return false - else -> { - } - } - } - return true - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentElements.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentElements.kt deleted file mode 100644 index 322eff249..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/DocumentElements.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.android.identity.wallet.documentdata - -import android.graphics.Bitmap - -data class DocumentElements( - val text: String = "", - val portrait: Bitmap? = null, - val signature: Bitmap? = null, - val fingerprint: Bitmap? = null, - val requestUserAuthorization: Boolean = false, - val passphrase: String = "" -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/DrivingLicense.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/DrivingLicense.kt deleted file mode 100644 index d0f477a85..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/DrivingLicense.kt +++ /dev/null @@ -1,225 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.StringOption - -object DrivingLicense { - const val MDL_NAMESPACE = "org.iso.18013.5.1" - const val AAMVA_NAMESPACE = "org.iso.18013.5.1.aamva" - fun getMdocComplexTypes() = MdocComplexTypes.Builder("org.iso.18013.5.1.mDL") - .addDefinition( - MDL_NAMESPACE, - hashSetOf("driving_privileges"), - true, - "vehicle_category_code", - "Vehicle Category Code", - DocumentAttributeType.StringOptions( - listOf( - StringOption(null, "(not set)"), - StringOption("A", "Motorcycles (A)"), - StringOption("AEU", "Motorcycles (AEU)"), - StringOption("B", "Light vehicles (B"), - StringOption("C", "Goods vehicles (C)"), - StringOption("D", "Passenger vehicles (D)"), - StringOption("BE", "Light vehicles with trailers (BE)"), - StringOption("CE", "Goods vehicles with trailers (CE)"), - StringOption("DE", "Passenger vehicles with trailers (DE)"), - StringOption("AM", "Mopeds (AM)"), - StringOption("A1", "Light motorcycles (A1)"), - StringOption("A1EU", "Light motorcycles (A1EU)"), - StringOption("A2", "Medium motorcycles (A2)"), - StringOption("B1", "Light vehicles (B1)"), - StringOption("B1EU", "Light vehicles (B1EU)"), - StringOption("C1", "Medium sized goods vehicles (C1)"), - StringOption("D1", "Medium sized passenger vehicles (e.g. minibuses) (D1)"), - ) - ) - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("driving_privileges"), - true, - "issue_date", - "Date of Issue", - DocumentAttributeType.Date - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("driving_privileges"), - true, - "expiry_date", - "Date of Expiry", - DocumentAttributeType.Date - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("driving_privileges"), - true, - "codes", - "Codes of Driving Privileges", - DocumentAttributeType.ComplexType, - ) - // details of DrivingPrivilege.codes - .addDefinition( - MDL_NAMESPACE, - hashSetOf("codes"), - true, - "code", - "Code", - DocumentAttributeType.StringOptions( - listOf( - StringOption(null, "(not set)"), - StringOption( - "01", - "Licence holder requires eye sight correction and/or protection" - ), - StringOption( - "03", - "Licence holder requires prosthetic device for the limbs" - ), - StringOption( - "78", - "Licence holder restricted to vehicles with automatic transmission" - ), - StringOption( - "S01", - "The vehicle's maximum authorized mass (kg) shall be" - ), - StringOption( - "S02", - "The vehicle's authorized passenger seats, excluding the driver's seat, shall be" - ), - StringOption( - "S03", - "The vehicle's cylinder capacity (cm3) shall be" - ), - StringOption( - "S04", - "The vehicle's power (kW) shall be" - ), - StringOption( - "S05", - "Licence holder restricted to vehicles adapted for physically disabled" - ) - ) - ) - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("codes"), - true, - "sign", - "Sign", - DocumentAttributeType.StringOptions( - listOf( - StringOption(null, "(not set)"), - StringOption("=", "Equals (=)"), - StringOption(">", "Greater than (>)"), - StringOption("<", "Less than (<)"), - StringOption(">=", "Greater than or equal to (≥)"), - StringOption("<=", "Less than or equal to (≤)") - ) - ) - ) - .addDefinition( - MDL_NAMESPACE, - hashSetOf("codes"), - true, - "value", - "Value", - DocumentAttributeType.String - ). - // details of domestic_driving_privileges - addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_driving_privileges"), - true, - "domestic_vehicle_class", - "Domestic Vehicle Class", - DocumentAttributeType.ComplexType, - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_driving_privileges"), - true, - "domestic_vehicle_restrictions", - "Domestic Vehicle Restrictions", - DocumentAttributeType.ComplexType - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_driving_privileges"), - true, - "domestic_vehicle_endorsements", - "Domestic Vehicle Endorsements", - DocumentAttributeType.ComplexType - ) - // details of DomesticDrivingPrivilege.domestic_vehicle_class - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_class"), - false, - "domestic_vehicle_class_code", - "Domestic Vehicle Class Code", - DocumentAttributeType.String - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_class"), - false, - "domestic_vehicle_class_description", - "Domestic Vehicle Class Description", - DocumentAttributeType.String - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_class"), - false, - "issue_date", - "Date of Issue", - DocumentAttributeType.Date - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_class"), - false, - "expiry_date", - "Date of Expiry", - DocumentAttributeType.Date - ) - // details of DomesticDrivingPrivilege.domestic_vehicle_restrictions - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_restrictions"), - true, - "domestic_vehicle_restriction_code", - "Restriction Code", - DocumentAttributeType.String - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_restrictions"), - true, - "domestic_vehicle_restriction_description", - "Vehicle Category Description", - DocumentAttributeType.String - ) - // details of DomesticDrivingPrivilege.domestic_vehicle_endorsements - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_endorsements"), - true, - "domestic_vehicle_endorsement_code", - "Endorsement Code", - DocumentAttributeType.String - ) - .addDefinition( - AAMVA_NAMESPACE, - hashSetOf("domestic_vehicle_endorsements"), - true, - "domestic_vehicle_endorsement_description", - "Vehicle Endorsement Description", - DocumentAttributeType.String - ) - .build() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeDefinition.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeDefinition.kt deleted file mode 100644 index d5026e6fd..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeDefinition.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType - -data class MdocComplexTypeDefinition( - val parentIdentifiers: HashSet, - val partOfArray: Boolean, - val identifier: String, - val displayName: String, - val type: DocumentAttributeType -) diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeRepository.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeRepository.kt deleted file mode 100644 index 3e48f8830..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypeRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.identity.wallet.documentdata - -object MdocComplexTypeRepository { - private val allComplexTypes: MutableMap = mutableMapOf() - - init { - addComplexTypes(DrivingLicense.getMdocComplexTypes()) - addComplexTypes(VehicleRegistration.getMdocComplexTypes()) - addComplexTypes(VaccinationDocument.getMdocComplexTypes()) - } - - fun addComplexTypes(complexTypes: MdocComplexTypes) { - allComplexTypes[complexTypes.docType] = complexTypes - } - - fun getComplexTypes(docType: String): MdocComplexTypes? { - return allComplexTypes[docType] - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypes.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypes.kt deleted file mode 100644 index a52e608e9..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocComplexTypes.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType - -class MdocComplexTypes private constructor( - val docType: String, - val namespaces: List -) { - - data class Builder( - val docType: String, - val namespaces: MutableMap = mutableMapOf() - ) { - fun addDefinition( - namespace: String, - parentIdentifiers: HashSet, - partOfArray: Boolean, - identifier: String, - displayName: String, - type: DocumentAttributeType - ) = apply { - if (!namespaces.containsKey(namespace)) { - namespaces[namespace] = MdocNamespaceComplexTypes.Builder(namespace) - } - namespaces[namespace]!!.addDefinition(parentIdentifiers, partOfArray, identifier, displayName, type) - } - - fun build() = MdocComplexTypes(docType, namespaces.values.map { it.build() }) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocNamespaceComplexTypes.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocNamespaceComplexTypes.kt deleted file mode 100644 index ae68f8021..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/MdocNamespaceComplexTypes.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType - -class MdocNamespaceComplexTypes( - val namespace: String, - val dataElements: List -) { - data class Builder( - val namespace: String, - val dataElements: MutableList = mutableListOf() - ) { - fun addDefinition( - parentIdentifiers: HashSet, - partOfArray: Boolean, - identifier: String, - displayName: String, - type: DocumentAttributeType - ) = apply { - dataElements.add(MdocComplexTypeDefinition(parentIdentifiers, partOfArray, identifier, displayName, type)) - } - - fun build() = MdocNamespaceComplexTypes(namespace, dataElements) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/ShowDocumentDataFragment.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/ShowDocumentDataFragment.kt deleted file mode 100644 index 148ad9fa0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/ShowDocumentDataFragment.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.android.identity.wallet.documentdata - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.text.HtmlCompat -import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.navArgs -import com.android.identity.wallet.databinding.FragmentShowDocumentDataBinding -import com.android.identity.wallet.transfer.TransferManager - -class ShowDocumentDataFragment : Fragment() { - - private val arguments by navArgs() - private var _binding: FragmentShowDocumentDataBinding? = null - private val binding get() = _binding!! - private lateinit var transferManager: TransferManager - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - transferManager = TransferManager.getInstance(requireContext()) - } - - override fun onDestroy() { - super.onDestroy() - transferManager.destroy() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentShowDocumentDataBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - loadDocumentElements() - } - - private fun loadDocumentElements() { - transferManager.readDocumentEntries(arguments.documentName).let { result -> - binding.tvResults.text = HtmlCompat.fromHtml(result.text, FROM_HTML_MODE_LEGACY) - result.portrait?.let { portrait -> - binding.ivPortrait.setImageBitmap(portrait) - binding.ivPortrait.visibility = View.VISIBLE - } - result.signature?.let { signature -> - binding.ivSignature.setImageBitmap(signature) - binding.ivSignature.visibility = View.VISIBLE - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/VaccinationDocument.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/VaccinationDocument.kt deleted file mode 100644 index bd4798ee3..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/VaccinationDocument.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.knowntypes.Options - -object VaccinationDocument { - const val MICOV_ATT_NAMESPACE = "org.micov.attestation.1" - const val MICOV_VTR_NAMESPACE = "org.micov.vtr.1" - fun getMdocComplexTypes() = MdocComplexTypes.Builder("org.micov.1") - - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("RA01_test"), - false, - "Result", - "Test Result", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("RA01_test"), - false, - "TypeOfTest", - "Type of Test", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("RA01_test"), - false, - "TimeOfTest", - "Time of Test", - DocumentAttributeType.DateTime - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("safeEntry_Leisure"), - false, - "SeCondFulfilled", - "Second Fulfilled", - DocumentAttributeType.Boolean - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("safeEntry_Leisure"), - false, - "SeCondType", - "Second Type", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_ATT_NAMESPACE, - hashSetOf("safeEntry_Leisure"), - false, - "SeCondExpiry", - "Second Expiry", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "tg", - "Disease or Agent Targeted", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "vp", - "Vaccine or Prophylaxis", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "mp", - "Vaccine Medicinal Product", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "br", - "Vaccine Brand", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "ma", - "Manufacturer", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "bn", - "Batch/Lot Number of the Vaccine", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "dn", - "Dose Number", - DocumentAttributeType.Number - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "sd", - "Total Series of Doses", - DocumentAttributeType.Number - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "dt", - "Date of Vaccination", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "co", - "Country of Vaccination", - DocumentAttributeType.StringOptions(Options.COUNTRY_ISO_3166_1_ALPHA_2) - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "ao", - "Administering Organization", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "ap", - "Administering Professional", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "nx", - "Due Date of Next Dose", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "is", - "Certificate Issuer", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "ci", - "Unique Certificate Identifier (UVCI)", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "pd", - "Protection Duration", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "vf", - "Valid From", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("v_RA01_1", "v_RA01_2"), - false, - "vu", - "Valid Until", - DocumentAttributeType.Date - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("pid_PPN", "pid_DL"), - false, - "pty", - "Type of Person Identifier", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("pid_PPN", "pid_DL"), - false, - "pnr", - "Unique number for the PTY/PIC/(PIA) combination", - DocumentAttributeType.String - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("pid_PPN", "pid_DL"), - false, - "pic", - "Issuing Country of the PTY", - DocumentAttributeType.StringOptions(Options.COUNTRY_ISO_3166_1_ALPHA_2) - ) - .addDefinition( - MICOV_VTR_NAMESPACE, - hashSetOf("pid_PPN", "pid_DL"), - false, - "pia", - "Issuing Authority of the PTY", - DocumentAttributeType.String, - ) - .build() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentdata/VehicleRegistration.kt b/appholder/src/main/java/com/android/identity/wallet/documentdata/VehicleRegistration.kt deleted file mode 100644 index 3b3c64556..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentdata/VehicleRegistration.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.android.identity.wallet.documentdata - -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.knowntypes.Options - -object VehicleRegistration { - const val MVR_NAMESPACE = "nl.rdw.mekb.1" - fun getMdocComplexTypes() = MdocComplexTypes.Builder("nl.rdw.mekb.1") - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "issuingCountry", - "Country Code", - DocumentAttributeType.StringOptions(Options.COUNTRY_ISO_3166_1_ALPHA_2) - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "competentAuthority", - "Competent Authority", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "registrationNumber", - "UN/EU Element A", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "validFrom", - "Custom EKB Element, Valid From", - DocumentAttributeType.Date - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_info"), - false, - "validUntil", - "Custom EKB Element, Valid Until", - DocumentAttributeType.Date - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_holder"), - false, - "holderInfo", - "Personal Data", - DocumentAttributeType.ComplexType - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("registration_holder"), - false, - "ownershipStatus", - "Ownership Status", - DocumentAttributeType.Number - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("holderInfo"), - false, - "name", - "Name of the Vehicle Owner", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("holderInfo"), - false, - "address", - "Address of the Vehicle Owner", - DocumentAttributeType.ComplexType - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "streetName", - "Street Name", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "houseNumber", - "House Number", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "houseNumberSuffix", - "House Number Suffix", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "postalCode", - "Postal Code", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("address"), - false, - "placeOfResidence", - "Place of Residence", - DocumentAttributeType.String - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("basic_vehicle_info"), - false, - "vehicle", - "Vehicle", - DocumentAttributeType.ComplexType - ) - .addDefinition( - MVR_NAMESPACE, - hashSetOf("vehicle"), - false, - "make", - "Make of the Vehicle", - DocumentAttributeType.String, - ) - - .build() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt deleted file mode 100644 index 9581661d0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreen.kt +++ /dev/null @@ -1,458 +0,0 @@ -package com.android.identity.wallet.documentinfo - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -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.size -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Key -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.RemoveRedEye -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.crypto.EcCurve -import com.android.identity.securearea.KeyPurpose -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.LoadingIndicator -import com.android.identity.wallet.composables.ShowToast -import com.android.identity.wallet.composables.gradientFor -import com.android.identity.wallet.theme.HolderAppTheme - -@Composable -fun DocumentInfoScreen( - viewModel: DocumentInfoViewModel, - onNavigateUp: () -> Unit, - onNavigateToDocumentDetails: () -> Unit -) { - val state by viewModel.screenState.collectAsState() - if (state.isDeleted) { - ShowToast(message = stringResource(id = R.string.delete_document_deleted_message)) - onNavigateUp() - } - - DocumentInfoScreenContent( - screenState = state, - onRefreshCredentials = viewModel::refreshCredentials, - onShowDocumentElements = { onNavigateToDocumentDetails() }, - onDeleteDocument = { viewModel.promptDocumentDelete() }, - onConfirmDocumentDelete = viewModel::confirmDocumentDelete, - onCancelDocumentDelete = viewModel::cancelDocumentDelete - ) -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun DocumentInfoScreenContent( - modifier: Modifier = Modifier, - screenState: DocumentInfoScreenState, - onRefreshCredentials: () -> Unit, - onShowDocumentElements: () -> Unit, - onDeleteDocument: () -> Unit, - onConfirmDocumentDelete: () -> Unit, - onCancelDocumentDelete: () -> Unit, -) { - Scaffold( - modifier = modifier - ) { paddingValues -> - Column( - modifier = Modifier.padding(paddingValues), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalAlignment = CenterHorizontally - ) { - if (screenState.isLoading) { - LoadingIndicator( - modifier = Modifier.fillMaxSize() - ) - } else { - Box(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxWidth()) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .border( - 2.dp, - gradientFor(screenState.documentColor), - RoundedCornerShape(12.dp) - ) - ) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - LabeledValue( - label = stringResource(id = R.string.label_document_name), - value = screenState.documentName - ) - LabeledValue( - label = stringResource(id = R.string.label_document_type), - value = screenState.documentType - ) - LabeledValue( - label = stringResource(id = R.string.label_date_provisioned), - value = screenState.provisioningDate - ) - LabeledValue( - label = stringResource(id = R.string.label_last_time_used), - value = screenState.lastTimeUsedDate.ifBlank { "N/A" } - ) - LabeledValue( - label = stringResource(id = R.string.label_issuer), - value = if (screenState.isSelfSigned) "Self-Signed on Device" else "N/A" - ) - } - } - val pagerState = rememberPagerState(pageCount = { screenState.authKeys.size }) - HorizontalPager( - modifier = Modifier - .fillMaxWidth(), - state = pagerState, - ) { page -> - val key = screenState.authKeys[page] - AuthenticationKeyInfo( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.secondaryContainer), - authKeyInfo = key - ) - } - PagerIndicators( - modifier = Modifier - .height(24.dp) - .fillMaxWidth() - .align(CenterHorizontally), - pagerState = pagerState, - itemsCount = screenState.authKeys.size, - ) - Divider(modifier = Modifier.padding(top = 12.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .weight(1f) - .clickable { onRefreshCredentials() }, - horizontalAlignment = CenterHorizontally - ) { - Column( - modifier = Modifier.padding(12.dp), - horizontalAlignment = CenterHorizontally - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = stringResource(id = R.string.bt_refresh_auth_keys), - tint = MaterialTheme.colorScheme.primary - ) - Text( - text = stringResource(id = R.string.bt_refresh_auth_keys), - style = MaterialTheme.typography.bodySmall - ) - } - } - Column( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .weight(1f) - .clickable { onShowDocumentElements() }, - horizontalAlignment = CenterHorizontally - ) { - Column( - modifier = Modifier.padding(12.dp), - horizontalAlignment = CenterHorizontally - ) { - Icon( - imageVector = Icons.Default.RemoveRedEye, - contentDescription = stringResource(id = R.string.bt_show_data), - tint = MaterialTheme.colorScheme.primary - ) - Text( - text = stringResource(id = R.string.bt_show_data), - style = MaterialTheme.typography.bodySmall - ) - } - } - } - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 24.dp) - .align(Alignment.BottomCenter), - onClick = onDeleteDocument, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.error), - colors = ButtonDefaults.outlinedButtonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.error - ) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(id = R.string.bt_delete), - ) - Text(text = stringResource(id = R.string.bt_delete)) - } - } - if (screenState.isDeletingPromptShown) { - DeleteDocumentPrompt( - onConfirm = onConfirmDocumentDelete, - onCancel = onCancelDocumentDelete - ) - } - } - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun PagerIndicators( - modifier: Modifier = Modifier, - pagerState: PagerState, - itemsCount: Int, -) { - Row( - modifier, - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - repeat(itemsCount) { iteration -> - val color = if (pagerState.currentPage == iteration) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = .2f) - } - Box( - modifier = Modifier - .padding(2.dp) - .clip(CircleShape) - .background(color) - .size(12.dp) - ) - } - } -} - -@Composable -private fun AuthenticationKeyInfo( - modifier: Modifier = Modifier, - authKeyInfo: DocumentInfoScreenState.KeyInformation -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - modifier = Modifier - .size(48.dp) - .padding(horizontal = 8.dp), - imageVector = Icons.Default.Key, - contentDescription = "${authKeyInfo.counter}", - tint = MaterialTheme.colorScheme.primary.copy(alpha = .5f) - ) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - LabeledValue( - label = stringResource(id = R.string.txt_keystore_implementation), - value = authKeyInfo.secureAreaDisplayName - ) - LabeledValue( - label = stringResource(id = R.string.document_info_counter), - value = "${authKeyInfo.counter}" - ) - LabeledValue( - label = stringResource(id = R.string.document_info_domain), - value = authKeyInfo.domain - ) - LabeledValue( - label = stringResource(id = R.string.document_info_valid_from), - value = authKeyInfo.validFrom - ) - LabeledValue( - label = stringResource(id = R.string.document_info_valid_until), - value = authKeyInfo.validUntil - ) - LabeledValue( - label = stringResource(id = R.string.document_info_issuer_data), - value = stringResource( - id = R.string.document_info_issuer_data_bytes, - authKeyInfo.issuerDataBytesCount - ) - ) - LabeledValue( - label = stringResource(id = R.string.document_info_usage_count), - value = "${authKeyInfo.usagesCount}" - ) - LabeledValue( - label = stringResource(id = R.string.document_info_key_purposes), - value = authKeyInfo.keyPurposes.toString() - ) - LabeledValue( - label = stringResource(id = R.string.document_info_ec_curve), - value = authKeyInfo.ecCurve.toString() - ) - } - } -} - -@Composable -private fun DeleteDocumentPrompt( - onConfirm: () -> Unit, - onCancel: () -> Unit -) { - AlertDialog( - onDismissRequest = onCancel, - title = { - Text(text = stringResource(id = R.string.delete_document_prompt_title)) - }, - text = { - Text(text = stringResource(id = R.string.delete_document_prompt_message)) - }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(text = stringResource(id = R.string.bt_delete)) - } - }, - dismissButton = { - TextButton(onClick = onCancel) { - Text(text = stringResource(id = R.string.bt_cancel)) - } - } - ) -} - -@Composable -private fun LabeledValue( - modifier: Modifier = Modifier, - label: String, - value: String -) { - val textValue = buildAnnotatedString { - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(label) - append(": ") - } - append(value) - } - Text( - modifier = modifier, - text = textValue, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) -} - -@Composable -@Preview -private fun PreviewDocumentInfoScreenLoading() { - HolderAppTheme { - DocumentInfoScreenContent( - screenState = DocumentInfoScreenState( - isLoading = true - ), - onRefreshCredentials = {}, - onShowDocumentElements = {}, - onDeleteDocument = {}, - onConfirmDocumentDelete = {} - ) {} - } -} - -@Composable -@Preview -private fun PreviewDocumentInfoScreen() { - HolderAppTheme { - DocumentInfoScreenContent( - screenState = DocumentInfoScreenState( - documentName = "Erica's Driving Licence", - documentType = "org.iso.18013.5.1.mDL", - provisioningDate = "16-07-2023", - isSelfSigned = true, - authKeys = listOf( - DocumentInfoScreenState.KeyInformation( - counter = 1, - validFrom = "16-07-2023", - validUntil = "23-07-2023", - domain = "Domain", - usagesCount = 1, - issuerDataBytesCount = "Issuer 1".toByteArray().count(), - keyPurposes = KeyPurpose.AGREE_KEY, - ecCurve = EcCurve.P256, - isHardwareBacked = false, - secureAreaDisplayName = "Secure Area Name" - ), - DocumentInfoScreenState.KeyInformation( - counter = 2, - validFrom = "16-07-2023", - validUntil = "23-07-2023", - domain = "Domain", - usagesCount = 0, - issuerDataBytesCount = "Issuer 2".toByteArray().count(), - keyPurposes = KeyPurpose.SIGN, - ecCurve = EcCurve.ED25519, - isHardwareBacked = true, - secureAreaDisplayName = "Secure Area Name" - - ) - ) - ), - onRefreshCredentials = {}, - onShowDocumentElements = {}, - onDeleteDocument = {}, - onConfirmDocumentDelete = {} - ) {} - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt deleted file mode 100644 index cde989dff..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoScreenState.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.android.identity.wallet.documentinfo - -import androidx.compose.runtime.Immutable -import com.android.identity.crypto.EcCurve -import com.android.identity.securearea.KeyPurpose -import com.android.identity.wallet.document.DocumentColor - -@Immutable -data class DocumentInfoScreenState( - val isLoading: Boolean = false, - val documentName: String = "", - val documentType: String = "", - val documentColor: DocumentColor = DocumentColor.Green, - val provisioningDate: String = "", - val lastTimeUsedDate: String = "", - val isSelfSigned: Boolean = false, - val authKeys: List = emptyList(), - val isDeletingPromptShown: Boolean = false, - val isDeleted: Boolean = false -) { - - @Immutable - data class KeyInformation( - val counter: Int, - val validFrom: String, - val validUntil: String, - val domain: String, - val issuerDataBytesCount: Int, - val usagesCount: Int, - val keyPurposes: KeyPurpose, - val ecCurve: EcCurve, - val isHardwareBacked: Boolean, - val secureAreaDisplayName: String - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt deleted file mode 100644 index b5a27a4de..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/documentinfo/DocumentInfoViewModel.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.android.identity.wallet.documentinfo - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.createSavedStateHandle -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory -import com.android.identity.wallet.composables.toCardArt -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.fragment.DocumentDetailFragmentArgs -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DocumentInfoViewModel( - private val documentManager: DocumentManager, - savedStateHandle: SavedStateHandle -) : ViewModel() { - - private val args = DocumentDetailFragmentArgs.fromSavedStateHandle(savedStateHandle) - private val _state = MutableStateFlow(DocumentInfoScreenState()) - val screenState: StateFlow = _state.asStateFlow() - - fun loadDocument(documentName: String) { - _state.update { it.copy(isLoading = true) } - viewModelScope.launch { - val documentInfo = withContext(Dispatchers.IO) { - documentManager.getDocumentInformation(documentName) - } - onDocumentInfoLoaded(documentInfo) - } - } - - fun promptDocumentDelete() { - _state.update { it.copy(isDeletingPromptShown = true) } - } - - fun confirmDocumentDelete() { - documentManager.deleteCredentialByName(args.documentName) - _state.update { it.copy(isDeleted = true, isDeletingPromptShown = false) } - } - - fun cancelDocumentDelete() { - _state.update { it.copy(isDeletingPromptShown = false) } - } - - fun refreshCredentials() { - _state.update { it.copy(isLoading = true) } - viewModelScope.launch { - withContext(Dispatchers.IO) { - documentManager.refreshCredentials(args.documentName) - } - loadDocument(args.documentName) - } - } - - private fun onDocumentInfoLoaded(documentInformation: DocumentInformation?) { - documentInformation?.let { - _state.update { - it.copy( - isLoading = false, - documentName = documentInformation.userVisibleName, - documentType = documentInformation.docType, - documentColor = documentInformation.documentColor.toCardArt(), - provisioningDate = documentInformation.dateProvisioned, - isSelfSigned = documentInformation.selfSigned, - lastTimeUsedDate = documentInformation.lastTimeUsed, - authKeys = documentInformation.authKeys.asScreenStateKeys() - ) - } - } - } - - private fun List.asScreenStateKeys(): List { - return map { keyData -> - DocumentInfoScreenState.KeyInformation( - counter = keyData.counter, - validFrom = keyData.validFrom, - validUntil = keyData.validUntil, - domain = keyData.domain, - issuerDataBytesCount = keyData.issuerDataBytesCount, - usagesCount = keyData.usagesCount, - keyPurposes = keyData.keyPurposes, - ecCurve = keyData.ecCurve, - isHardwareBacked = keyData.isHardwareBacked, - secureAreaDisplayName = keyData.secureAreaDisplayName - ) - } - } - - companion object { - fun Factory(documentManager: DocumentManager): ViewModelProvider.Factory = - viewModelFactory { - initializer { - DocumentInfoViewModel(documentManager, createSavedStateHandle()) - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/DocumentDetailFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/DocumentDetailFragment.kt deleted file mode 100644 index 36e001cf0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/DocumentDetailFragment.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.documentinfo.DocumentInfoScreen -import com.android.identity.wallet.documentinfo.DocumentInfoViewModel -import com.android.identity.wallet.theme.HolderAppTheme - -class DocumentDetailFragment : Fragment() { - - private val args: DocumentDetailFragmentArgs by navArgs() - private val viewModel by viewModels { - val documentManager = DocumentManager.getInstance(requireContext()) - DocumentInfoViewModel.Factory(documentManager) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - HolderAppTheme { - DocumentInfoScreen( - viewModel = viewModel, - onNavigateUp = { findNavController().navigateUp() }, - onNavigateToDocumentDetails = { onShowData(args.documentName) } - ) - } - } - viewModel.loadDocument(args.documentName) - } - } - - private fun onShowData(documentName: String) { - val direction = DocumentDetailFragmentDirections - .navigateToDocumentData(documentName) - findNavController().navigate(direction) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/ReverseEngagementFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/ReverseEngagementFragment.kt deleted file mode 100644 index 22ce77a2a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/ReverseEngagementFragment.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.Manifest -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Bundle -import android.provider.Settings -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.wallet.databinding.FragmentReverseEngagementBinding -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logWarning -import com.android.identity.wallet.viewmodel.ShareDocumentViewModel -import com.budiyev.android.codescanner.CodeScanner -import com.budiyev.android.codescanner.DecodeCallback - -class ReverseEngagementFragment : Fragment() { - - private var _binding: FragmentReverseEngagementBinding? = null - private var codeScanner: CodeScanner? = null - - private val binding get() = _binding!! - private val vm: ShareDocumentViewModel by viewModels() - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentReverseEngagementBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - binding.btCancel.setOnClickListener { - findNavController().navigate( - ReverseEngagementFragmentDirections.actionReverseEngagementFragmentToSelectDocumentFragment() - ) - } - - codeScanner = CodeScanner(requireContext(), binding.csScanner) - codeScanner?.decodeCallback = DecodeCallback { result -> - requireActivity().runOnUiThread { - val qrText = result.text - log("qrText: $qrText") - val uri = Uri.parse(qrText) - if (uri.scheme.equals("mdoc")) { - vm.startPresentationReverseEngagement(qrText, emptyList()) - findNavController().navigate( - ReverseEngagementFragmentDirections.actionReverseEngagementFragmentToTransferDocumentFragment() - ) - } else { - logWarning("Ignoring QR code with scheme " + uri.scheme) - } - } - } - binding.csScanner.setOnClickListener { codeScanner?.startPreview() } - } - - override fun onResume() { - super.onResume() - enableReader() - } - - override fun onPause() { - super.onPause() - disableReader() - } - - private fun enableReader() { - if (isAllPermissionsGranted()) { - codeScanner?.startPreview() - } else { - shouldRequestPermission() - } - } - - private fun disableReader() { - codeScanner?.releaseResources() - } - - private val appPermissions: List - get() = mutableListOf(Manifest.permission.CAMERA) - - private fun shouldRequestPermission() { - val permissionsNeeded = appPermissions.filter { permission -> - ContextCompat.checkSelfPermission( - requireContext(), - permission - ) != PackageManager.PERMISSION_GRANTED - } - - if (permissionsNeeded.isNotEmpty()) { - permissionsLauncher.launch( - permissionsNeeded.toTypedArray() - ) - } - } - - private fun isAllPermissionsGranted(): Boolean { - // If any permission is not granted return false - return appPermissions.none { permission -> - ContextCompat.checkSelfPermission( - requireContext(), - permission - ) != PackageManager.PERMISSION_GRANTED - } - } - - private val permissionsLauncher = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - permissions.entries.forEach { - log("permissionsLauncher ${it.key} = ${it.value}") - // Open settings if user denied any required permission - if (!it.value && !shouldShowRequestPermissionRationale(it.key)) { - openSettings() - return@registerForActivityResult - } - } - } - - private fun openSettings() { - val intent = Intent() - intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - intent.data = Uri.fromParts("package", requireContext().packageName, null) - startActivity(intent) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/SelfSignedDetailsFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/SelfSignedDetailsFragment.kt deleted file mode 100644 index 0a92ebd7e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/SelfSignedDetailsFragment.kt +++ /dev/null @@ -1,558 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.Manifest -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Matrix -import android.graphics.Typeface -import android.icu.text.SimpleDateFormat -import android.net.Uri -import android.os.Bundle -import android.os.Environment -import android.provider.MediaStore -import android.text.Editable -import android.text.InputType -import android.util.TypedValue -import android.view.LayoutInflater -import android.view.View -import android.view.View.NOT_FOCUSABLE -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT -import android.widget.ArrayAdapter -import android.widget.CheckBox -import android.widget.EditText -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.Spinner -import android.widget.TextView -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts.RequestPermission -import androidx.activity.result.contract.ActivityResultContracts.TakePicture -import androidx.core.content.FileProvider -import androidx.core.widget.addTextChangedListener -import androidx.exifinterface.media.ExifInterface -import androidx.exifinterface.media.ExifInterface.ORIENTATION_UNDEFINED -import androidx.exifinterface.media.ExifInterface.TAG_ORIENTATION -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.IntegerOption -import com.android.identity.documenttype.StringOption -import com.android.identity.wallet.databinding.FragmentSelfSignedDetailsBinding -import com.android.identity.wallet.util.Field -import com.android.identity.wallet.util.FormatUtil.fullDateStringToMilliseconds -import com.android.identity.wallet.util.FormatUtil.millisecondsToFullDateString -import com.android.identity.wallet.selfsigned.ProvisionInfo -import com.android.identity.wallet.selfsigned.SelfSignedDocumentData -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logError -import com.android.identity.wallet.viewmodel.SelfSignedViewModel -import com.google.android.material.datepicker.MaterialDatePicker -import java.io.File -import java.io.IOException -import java.util.Date -import kotlin.math.max -import kotlin.math.min - -class SelfSignedDetailsFragment : Fragment() { - - private val vm: SelfSignedViewModel by viewModels() - private val args: SelfSignedDetailsFragmentArgs by navArgs() - private val nameElements = listOf("given_name", "name", "gn") - - private var _binding: FragmentSelfSignedDetailsBinding? = null - private val binding get() = _binding!! - - private lateinit var provisionInfo: ProvisionInfo - private lateinit var documentNameEditText: EditText - private lateinit var holderNameEditText: EditText - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - provisionInfo = args.provisionInfo - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentSelfSignedDetailsBinding.inflate(inflater) - binding.fragment = this - bindUI() - return binding.root - } - - private fun bindUI() { - // Create all fields in the screen - val documentName = getDocumentNameValue() - addField( - Field( - 0, - "Document Name", - provisionInfo.docName, - DocumentAttributeType.String, - documentName - ) - ) - documentNameEditText = binding.layoutSelfSignedDetails.findViewById(0) - vm.getFields(provisionInfo.docType).forEach { field -> - addField(field) - if (field.name in nameElements) { - holderNameEditText = binding.layoutSelfSignedDetails.findViewById(field.id) - } - } - setupTextChangeListener() - - vm.loading.observe(viewLifecycleOwner) { - binding.loadingProgress.visibility = it - } - - vm.created.observe(viewLifecycleOwner) { - Toast.makeText( - requireContext(), "Document created successfully!", - Toast.LENGTH_SHORT - ).show() - findNavController().navigate( - SelfSignedDetailsFragmentDirections.actionSelfSignedDetailsToSelectDocumentFragment() - ) - } - } - - private fun getDocumentNameValue(): String { - val value = vm.getFields(provisionInfo.docType).find { it.name in nameElements }?.value - val name = value?.toString() ?: "" - val docName = provisionInfo.docName - return if (name.isBlank()) docName else "$name's $docName" - } - - private fun setupTextChangeListener() { - holderNameEditText.addTextChangedListener { newValue -> - documentNameEditText.setText("$newValue's ${provisionInfo.docName}") - } - } - - override fun onDestroyView() { - super.onDestroyView() - updateList() - } - - private fun updateList() { - provisionInfo.docName = documentNameEditText.text.toString() - vm.getFields(provisionInfo.docType).forEachIndexed { index, field -> - vm.getFields(provisionInfo.docType)[index] = getField(field) - } - } - - private fun getField(field: Field): Field { - return when (field.fieldType) { - is DocumentAttributeType.Picture -> { - Field( - field.id, - field.label, - field.name, - field.fieldType, - getImageViewValue(field.id), - namespace = field.namespace, - parentId = field.parentId, - stringOptions = field.stringOptions, - integerOptions = field.integerOptions - ) - } - - is DocumentAttributeType.Boolean -> { - Field( - field.id, - field.label, - field.name, - field.fieldType, - getViewValue(field.id), - namespace = field.namespace, - parentId = field.parentId, - stringOptions = field.stringOptions, - integerOptions = field.integerOptions - ) - } - - else -> { - Field( - field.id, - field.label, - field.name, - field.fieldType, - getViewValue(field.id), - namespace = field.namespace, - isArray = field.isArray, - parentId = field.parentId, - stringOptions = field.stringOptions, - integerOptions = field.integerOptions - ) - } - } - } - - private fun addField(field: Field) { - when (field.fieldType) { - is DocumentAttributeType.Picture -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView(getImageView( - field.id, field.value as Bitmap - ) { dispatchTakePictureIntent(field.id) }) - } - - is DocumentAttributeType.Boolean -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - checkBox(field.id, field.value as Boolean) - ) - } - - is DocumentAttributeType.String, DocumentAttributeType.Number -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - getEditView(field.id, field.value.toString(), null) - ) - } - - is DocumentAttributeType.Date, DocumentAttributeType.DateTime -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - getEditView(field.id, field.value as String, picker(field.id, field.id + 500)) - ) - } - - is DocumentAttributeType.IntegerOptions -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - integerOptionsSpinner( - field.integerOptions!!, field.id, field.value - ) - ) - } - - is DocumentAttributeType.StringOptions -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - stringOptionsSpinner( - field.stringOptions!!, field.id, field.value - ) - ) - } - - is DocumentAttributeType.ComplexType -> { - binding.layoutSelfSignedDetails.addView( - getTitleView(field.id + 500, field.label) - ) - } - - is DocumentAttributeType.Blob -> { - binding.layoutSelfSignedDetails.addView( - getTextView(field.id + 500, field.label) - ) - binding.layoutSelfSignedDetails.addView( - getEditView(field.id, field.value.toString(), null) - ) - } - } - } - - private fun getImageView( - id: Int, bitmap: Bitmap, onClickListener: View.OnClickListener? - ): View { - val imageView = ImageView(requireContext()) - imageView.id = id - imageView.setImageBitmap(bitmap) - - imageView.layoutParams = LinearLayout.LayoutParams(bitmap.width, bitmap.height).also { - it.setMargins(16, 16, 16, 0) - } - onClickListener?.let { - imageView.setOnClickListener(it) - } - return imageView - } - - private fun getTextView(id: Int, value: String): View { - val textView = TextView(requireContext()) - textView.id = id - textView.text = value - textView.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).also { - it.setMargins(16, 16, 16, 0) - } - return textView - } - - private fun getTitleView(id: Int, value: String): View { - val textView = TextView(requireContext()) - textView.id = id - textView.text = value - textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18f) - textView.setTypeface(textView.typeface, Typeface.BOLD) - textView.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).also { - it.setMargins(16, 32, 16, 16) - } - return textView - } - - private fun getEditView(id: Int, value: String, onClickListener: View.OnClickListener?): View { - val editText = EditText(requireContext()) - editText.id = id - editText.text = Editable.Factory.getInstance().newEditable(value) - editText.layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).also { - it.setMargins(16, 0, 16, 0) - } - onClickListener?.let { - editText.setOnClickListener(it) - // avoid open keyboard - editText.inputType = InputType.TYPE_NULL - editText.focusable = NOT_FOCUSABLE - } - return editText - } - - fun onCreateSelfSigned() { - updateList() - val dData = SelfSignedDocumentData( - provisionInfo, vm.getFields(provisionInfo.docType) - ) - vm.createSelfSigned(dData) - binding.loadingProgress.visibility = View.VISIBLE - } - - private fun getImageViewValue(id: Int): Bitmap { - val imageView = binding.layoutSelfSignedDetails.findViewById(id) - val bitmap = Bitmap.createBitmap( - imageView.width, imageView.height, Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(bitmap) - imageView.draw(canvas) - return bitmap - } - - private fun getViewValue(id: Int): Any? { - return when (val view = binding.layoutSelfSignedDetails.findViewById(id)) { - is CheckBox -> { - view.isChecked - } - - is TextView -> { - view.text.toString() - } - - is Spinner -> { - when (view.selectedItem) { - is StringOption -> (view.selectedItem as StringOption).value - is IntegerOption -> (view.selectedItem as IntegerOption).value - else -> view.selectedItem.toString() - } - } - - - else -> { - String() - } - } - } - - private fun setViewValue(id: Int, value: String) { - val view = binding.layoutSelfSignedDetails.findViewById(id) - if (view is TextView) { - view.text = value - } - } - - /** - * OnClickListener for date picker - */ - private fun picker(id: Int, idLabel: Int) = View.OnClickListener { - val titleText = getViewValue(idLabel) as String - val dateText = getViewValue(id) as String - log("$dateText - ${fullDateStringToMilliseconds(dateText)}") - val datePicker = MaterialDatePicker.Builder.datePicker().setTitleText(titleText) - .setSelection(fullDateStringToMilliseconds(dateText)).build() - datePicker.addOnPositiveButtonClickListener { - log("$it - ${millisecondsToFullDateString(it)}") - setViewValue(id, millisecondsToFullDateString(it)) - } - datePicker.show(parentFragmentManager, view?.tag?.toString()) - } - - private fun stringOptionsSpinner( - options: List, id: Int, value: Any? - ): View { - val spinner = Spinner(context) - spinner.id = id - val adapter = - ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, options) - spinner.adapter = adapter - val selected = options.find { (it.value == null && value == null) || it.value == value } - if (selected != null) { - spinner.setSelection(options.indexOf(selected)) - } - return spinner - } - - private fun integerOptionsSpinner( - options: List, id: Int, value: Any? - ): View { - val spinner = Spinner(context) - spinner.id = id - val adapter = - ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, options) - spinner.adapter = adapter - val selected = options.find { (it.value == null && value == null) || it.value == value } - if (selected != null) { - spinner.setSelection(options.indexOf(selected)) - } - return spinner - } - - private fun checkBox(id: Int, value: Boolean): View { - val checkBox = CheckBox(context) - checkBox.id = id - checkBox.isChecked = value - return checkBox - } - - // Following to enable take picture - private lateinit var photoUri: Uri - private lateinit var currentPhotoPath: String - private var imageViewId: Int? = null - private val takePicture = registerForActivityResult(TakePicture()) { isSuccess -> - if (isSuccess) { - val rotation = calculateDegrees() - setPic(rotation) - } - } - private val cameraLauncher = registerForActivityResult(RequestPermission()) { granted -> - if (granted) { - proceedTakingPhoto() - } - } - - private fun dispatchTakePictureIntent(viewId: Int) { - imageViewId = viewId - if (!hasCameraAvailable()) return - if (!canTakePhoto()) return - cameraLauncher.launch(Manifest.permission.CAMERA) - } - - private fun proceedTakingPhoto() { - try { - val imageFile = createImageFile() - photoUri = getUriForFile(imageFile) - takePicture.launch(photoUri) - } catch (exception: IOException) { - log("Error capturing image", exception) - } - } - - private fun hasCameraAvailable(): Boolean { - val packageManager = requireContext().packageManager - if (!packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) { - val errorMessage = "This device does not have a camera." - Toast.makeText(activity, errorMessage, Toast.LENGTH_SHORT).show() - return false - } - return true - } - - private fun canTakePhoto(): Boolean { - val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - // Ensure that there's a camera activity to handle the intent - if (takePictureIntent.resolveActivity(requireContext().packageManager) == null) { - val errorMessage = "Could not find camera activity." - Toast.makeText(activity, errorMessage, Toast.LENGTH_SHORT).show() - return false - } - return true - } - - @Throws(IOException::class) - private fun createImageFile(): File { - // Create an image file name - val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) - val storageDir: File? = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES) - return File.createTempFile( - "JPEG_${timeStamp}_", /* prefix */ - ".jpg", /* suffix */ - storageDir /* directory */ - ).apply { - currentPhotoPath = absolutePath - } - } - - private fun getUriForFile(file: File): Uri { - val authority = "${requireContext().packageName}.fileprovider" - return FileProvider.getUriForFile(requireContext(), authority, file) - } - - private fun calculateDegrees(): Float { - val inputStream = requireContext().contentResolver.openInputStream(photoUri) - val exifInterface = ExifInterface(inputStream!!) - return when (exifInterface.getAttributeInt(TAG_ORIENTATION, ORIENTATION_UNDEFINED)) { - ExifInterface.ORIENTATION_ROTATE_90 -> 90f - ExifInterface.ORIENTATION_ROTATE_180 -> 180f - ExifInterface.ORIENTATION_ROTATE_270 -> 270f - else -> 0f - }.apply { - inputStream.close() - } - } - - private fun setPic(rotation: Float) { - val id = imageViewId - if (id == null) { - logError("No image view id, impossible to set picture") - return - } - - val imageView = binding.layoutSelfSignedDetails.findViewById(id) - - // Get the dimensions of the View - val targetW: Int = imageView.width - val targetH: Int = imageView.height - - val bmOptions = BitmapFactory.Options().apply { - // Get the dimensions of the bitmap - inJustDecodeBounds = true - - val photoW: Int = outWidth - val photoH: Int = outHeight - - // Determine how much to scale down the image - val scaleFactor: Int = max(1, min(photoW / targetW, photoH / targetH)) - - // Decode the image file into a Bitmap sized to fill the View - inJustDecodeBounds = false - inSampleSize = scaleFactor - inPurgeable = true - } - - val original = BitmapFactory.decodeFile(currentPhotoPath, bmOptions) - val rotated = if (rotation != 0f) { - val matrix = Matrix() - matrix.postRotate(rotation) - Bitmap.createBitmap(original, 0, 0, original.width, original.height, matrix, true) - } else original - imageView.setImageBitmap(rotated) - } -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/ShareDocumentFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/ShareDocumentFragment.kt deleted file mode 100644 index 9f45bf667..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/ShareDocumentFragment.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.databinding.FragmentShareDocumentBinding -import com.android.identity.wallet.util.TransferStatus -import com.android.identity.wallet.viewmodel.ShareDocumentViewModel - -class ShareDocumentFragment : Fragment() { - - private val viewModel: ShareDocumentViewModel by viewModels() - - private var _binding: FragmentShareDocumentBinding? = null - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentShareDocumentBinding.inflate(inflater) - binding.vm = viewModel - binding.fragment = this - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, onBackCallback) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewModel.message.set("Scan QR code with mdoc verifier device") - viewModel.getTransferStatus().observe(viewLifecycleOwner) { - when (it) { - TransferStatus.CONNECTED -> { - viewModel.message.set("Connected!") - val destination = ShareDocumentFragmentDirections.toTransferDocumentFragment() - findNavController().navigate(destination) - } - - TransferStatus.REQUEST -> { - viewModel.message.set("Request received!") - } - - TransferStatus.DISCONNECTED -> { - viewModel.message.set("Disconnected!") - findNavController().navigateUp() - } - - TransferStatus.ERROR -> { - viewModel.message.set("Error on presentation!") - } - - TransferStatus.ENGAGEMENT_DETECTED -> { - viewModel.message.set("Engagement detected!") - } - - TransferStatus.CONNECTING -> { - viewModel.message.set("Connecting...") - } - - else -> {} - } - } - } - - override fun onResume() { - super.onResume() - viewModel.triggerQrEngagement() - viewModel.showQrCode() - } - - private val onBackCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - onCancel() - } - } - - fun onCancel() { - viewModel.cancelPresentation() - findNavController().navigateUp() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt b/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt deleted file mode 100644 index e12b6fb29..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/fragment/TransferDocumentFragment.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.android.identity.wallet.fragment - -import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.crypto.javaX509Certificate -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.R -import com.android.identity.wallet.databinding.FragmentTransferDocumentBinding -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.transfer.TransferManager -import com.android.identity.wallet.trustmanagement.CustomValidators -import com.android.identity.wallet.trustmanagement.getCommonName -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.util.TransferStatus -import com.android.identity.wallet.util.log -import com.android.identity.wallet.viewmodel.TransferDocumentViewModel -import java.security.cert.X509Certificate - -class TransferDocumentFragment : Fragment() { - private var _binding: FragmentTransferDocumentBinding? = null - private val binding get() = _binding!! - - private val viewModel: TransferDocumentViewModel by activityViewModels() - - override fun onAttach(context: Context) { - super.onAttach(context) - val backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - onDone() - } - } - requireActivity().onBackPressedDispatcher.addCallback(this, backPressedCallback) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentTransferDocumentBinding.inflate(inflater) - binding.fragment = this - binding.vm = viewModel - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - viewModel.getTransferStatus().observe(viewLifecycleOwner) { transferStatus -> - when (transferStatus) { - TransferStatus.CONNECTED -> log("Connected") - TransferStatus.REQUEST -> onTransferRequested() - TransferStatus.REQUEST_SERVED -> onRequestServed() - TransferStatus.DISCONNECTED -> onTransferDisconnected() - TransferStatus.ERROR -> onTransferError() - else -> {} - } - } - viewModel.connectionClosedLiveData.observe(viewLifecycleOwner) { - onCloseConnection( - sendSessionTerminationMessage = true, - useTransportSpecificSessionTermination = false - ) - } - viewModel.authConfirmationState.observe(viewLifecycleOwner) { cancelled -> - if (cancelled == true) { - viewModel.onAuthenticationCancellationConsumed() - onDone() - findNavController().navigateUp() - } - } - } - - private fun onRequestServed() { - log("Request Served") - } - - private fun onTransferRequested() { - log("Request") - var commonName = "" - var trusted = false - try { - val requestedDocuments = viewModel.getRequestedDocuments() - requestedDocuments.forEach { reqDoc -> - val docs = viewModel.getDocuments().filter { reqDoc.docType == it.docType } - if (!viewModel.getSelectedDocuments().any { reqDoc.docType == it.docType }) { - if (docs.isEmpty()) { - binding.txtDocuments.append("- No document found for ${reqDoc.docType}\n") - return@forEach - } else if (docs.size == 1) { - viewModel.getSelectedDocuments().add(docs[0]) - } else { - showDocumentSelection(docs) - return - } - } - val doc = viewModel.getSelectedDocuments().first { reqDoc.docType == it.docType } - if (reqDoc.readerAuth != null && reqDoc.readerAuthenticated) { - var certChain: List = - reqDoc.readerCertificateChain!!.certificates.map { it.javaX509Certificate } - .toList() - val customValidators = CustomValidators.getByDocType(doc.docType) - val result = HolderApp.trustManagerInstance.verify( - chain = certChain, - customValidators = customValidators - ) - trusted = result.isTrusted - if (result.trustChain.any()) { - certChain = result.trustChain - } - commonName = certChain.last().issuerX500Principal.getCommonName("") - - // Add some information about the reader certificate used - if (result.isTrusted) { - binding.txtDocuments.append("- Trusted reader auth used: ($commonName)\n") - } else { - binding.txtDocuments.append("- Not trusted reader auth used: ($commonName)\n") - if (result.error != null) { - binding.txtDocuments.append("- TrustManager Error: (${result.error})\n") - } - } - } - binding.txtDocuments.append("- ${doc.userVisibleName} (${doc.docType})\n") - } - if (viewModel.getSelectedDocuments().isNotEmpty()) { - viewModel.createSelectedItemsList() - val direction = TransferDocumentFragmentDirections - .navigateToConfirmation(commonName, trusted) - findNavController().navigate(direction) - } else { - // Send response with 0 documents - viewModel.sendResponseForSelection( - onResultReady = { - } - ) - } - // TODO: this is kind of a hack but we really need to move the sending of the - // message to here instead of in the auth confirmation dialog - if (PreferencesHelper.isConnectionAutoCloseEnabled()) { - hideButtons() - } - } catch (e: Exception) { - val message = "On request received error: ${e.message}" - log(message, e) - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() - binding.txtConnectionStatus.append("\n$message") - } - } - - private fun showDocumentSelection(doc: List) { - val alertDialogBuilder = AlertDialog.Builder(requireContext()) - alertDialogBuilder.setTitle("Select which document to share") - val listItems = doc.map { it.userVisibleName }.toTypedArray() - alertDialogBuilder.setSingleChoiceItems(listItems, -1) { dialogInterface, i -> - viewModel.getSelectedDocuments().add(doc[i]) - onTransferRequested() - dialogInterface.dismiss() - } - val mDialog = alertDialogBuilder.create() - mDialog.show() - } - - private fun onTransferDisconnected() { - log("Disconnected") - hideButtons() - TransferManager.getInstance(requireContext()).disconnect() - } - - private fun onTransferError() { - Toast.makeText(requireContext(), "An error occurred.", Toast.LENGTH_SHORT).show() - hideButtons() - TransferManager.getInstance(requireContext()).disconnect() - } - - private fun hideButtons() { - binding.txtConnectionStatus.text = getString(R.string.connection_mdoc_closed) - binding.btCloseConnection.visibility = View.GONE - binding.btCloseTerminationMessage.visibility = View.GONE - binding.btCloseTransportSpecific.visibility = View.GONE - binding.btOk.visibility = View.VISIBLE - } - - fun onCloseConnection( - sendSessionTerminationMessage: Boolean, - useTransportSpecificSessionTermination: Boolean - ) { - viewModel.cancelPresentation( - sendSessionTerminationMessage, - useTransportSpecificSessionTermination - ) - hideButtons() - } - - fun onDone() { - onCloseConnection( - sendSessionTerminationMessage = true, - useTransportSpecificSessionTermination = false - ) - findNavController().navigateUp() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt deleted file mode 100644 index b0588e39b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedDocumentScreen.kt +++ /dev/null @@ -1,424 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import androidx.annotation.StringRes -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.R -import com.android.identity.wallet.composables.CounterInput -import com.android.identity.wallet.composables.DropDownIndicator -import com.android.identity.wallet.composables.LabeledUserInput -import com.android.identity.wallet.composables.OutlinedContainerHorizontal -import com.android.identity.wallet.composables.TextDropDownRow -import com.android.identity.wallet.composables.ValueLabel -import com.android.identity.wallet.composables.gradientFor -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.support.CurrentSecureArea -import com.android.identity.wallet.support.SecureAreaSupport -import com.android.identity.wallet.support.SecureAreaSupportState -import com.android.identity.wallet.support.toSecureAreaState -import com.android.identity.wallet.util.ProvisioningUtil - -@Composable -fun AddSelfSignedDocumentScreen( - viewModel: AddSelfSignedViewModel, - onNext: () -> Unit -) { - val screenState by viewModel.screenState.collectAsState() - - AddSelfSignedDocumentScreenContent( - modifier = Modifier.fillMaxSize(), - screenState = screenState, - documentItems = viewModel.documentItems, - onDocumentTypeChanged = viewModel::updateDocumentType, - onCardArtSelected = viewModel::updateCardArt, - onDocumentNameChanged = viewModel::updateDocumentName, - onKeystoreImplementationChanged = viewModel::updateKeystoreImplementation, - onSecureAreaSupportStateUpdated = viewModel::updateSecureAreaSupportState, - onNumberOfMsoChanged = viewModel::updateNumberOfMso, - onMaxUseOfMsoChanged = viewModel::updateMaxUseOfMso, - onValidityInDaysChanged = viewModel::updateValidityInDays, - onMinValidityInDaysChanged = viewModel::updateMinValidityInDays, - onNext = onNext - ) -} - -@Composable -private fun AddSelfSignedDocumentScreenContent( - modifier: Modifier, - screenState: AddSelfSignedScreenState, - documentItems: List, - onDocumentTypeChanged: (newType: String, newName: String) -> Unit, - onCardArtSelected: (newCardArt: DocumentColor) -> Unit, - onDocumentNameChanged: (newValue: String) -> Unit, - onKeystoreImplementationChanged: (newImplementation: CurrentSecureArea) -> Unit, - onSecureAreaSupportStateUpdated: (newState: SecureAreaSupportState) -> Unit, - onNumberOfMsoChanged: (newValue: Int) -> Unit, - onMaxUseOfMsoChanged: (newValue: Int) -> Unit, - onValidityInDaysChanged: (newValue: Int) -> Unit, - onMinValidityInDaysChanged: (newValue: Int) -> Unit, - onNext: () -> Unit -) { - Scaffold(modifier = modifier) { paddingValues -> - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .padding(paddingValues) - .verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Spacer(modifier = Modifier.fillMaxWidth()) - DocumentTypeChooser( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - documentItems = documentItems, - currentDocumentType = screenState.documentType, - onDocumentTypeSelected = onDocumentTypeChanged - ) - CardArtChooser( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - currentCardArt = screenState.cardArt, - onCardArtSelected = onCardArtSelected - ) - DocumentNameInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - value = screenState.documentName, - onValueChanged = onDocumentNameChanged - ) - KeystoreImplementationChooser( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - currentImplementation = screenState.currentSecureArea, - onKeystoreImplementationChanged = onKeystoreImplementationChanged - ) - SecureAreaSupport.getInstance( - LocalContext.current, - screenState.currentSecureArea - ).SecureAreaAuthUi(onUiStateUpdated = onSecureAreaSupportStateUpdated) - CounterInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.txt_number_mso), - value = screenState.numberOfMso, - onValueChange = onNumberOfMsoChanged - ) - CounterInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.txt_max_use_mso), - value = screenState.maxUseOfMso, - onValueChange = onMaxUseOfMsoChanged - ) - CounterInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.validity_in_days), - value = screenState.validityInDays, - onValueChange = onValidityInDaysChanged - ) - CounterInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - label = stringResource(id = R.string.minimum_validity_in_days), - value = screenState.minValidityInDays, - onValueChange = onMinValidityInDaysChanged - ) - Button( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - onClick = onNext - ) { - Text(text = "Next") - } - } - } -} - -@Composable -private fun DocumentTypeChooser( - modifier: Modifier = Modifier, - documentItems: List, - currentDocumentType: String, - onDocumentTypeSelected: (newType: String, newName: String) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.txt_document_type) - ) { - var expanded by remember { mutableStateOf(false) } - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = true } - ) { - documentItems.find {it.docType == currentDocumentType}?.let { - ValueLabel( - modifier = Modifier.weight(1f), - label = it.displayName - ) - } - DropDownIndicator() - } - DropdownMenu( - modifier = Modifier - .fillMaxWidth(0.8f), - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - documentItems.forEach{ - TextDropDownRow( - label = it.displayName, - onSelected = { - onDocumentTypeSelected(it.docType, it.displayName) - expanded = false - } - ) - } - } - } -} - -@Composable -private fun CardArtChooser( - modifier: Modifier, - currentCardArt: DocumentColor, - onCardArtSelected: (newCardArt: DocumentColor) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.txt_card_art) - ) { - var expanded by remember { mutableStateOf(false) } - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = true }, - outlineBrush = gradientFor(currentCardArt) - ) { - Box( - modifier = Modifier - .size(32.dp) - .clip(RoundedCornerShape(8.dp)) - .background(gradientFor(currentCardArt), RoundedCornerShape(8.dp)), - ) - ValueLabel( - modifier = Modifier - .padding(horizontal = 12.dp) - .weight(1f), - label = stringResource(id = colorNameFor(currentCardArt)) - ) - DropDownIndicator() - } - DropdownMenu( - modifier = Modifier - .fillMaxWidth(0.8f), - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - CardArtDropDownRow( - cardArt = DocumentColor.Green, - onSelected = { - onCardArtSelected(DocumentColor.Green) - expanded = false - } - ) - CardArtDropDownRow( - cardArt = DocumentColor.Yellow, - onSelected = { - onCardArtSelected(DocumentColor.Yellow) - expanded = false - } - ) - CardArtDropDownRow( - cardArt = DocumentColor.Blue, - onSelected = { - onCardArtSelected(DocumentColor.Blue) - expanded = false - } - ) - CardArtDropDownRow( - cardArt = DocumentColor.Red, - onSelected = { - onCardArtSelected(DocumentColor.Red) - expanded = false - } - ) - } - } -} - -@Composable -private fun DocumentNameInput( - modifier: Modifier = Modifier, - value: String, - onValueChanged: (newValue: String) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.txt_document_name) - ) { - OutlinedContainerHorizontal(modifier = Modifier.fillMaxWidth()) { - BasicTextField( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - textStyle = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface, - ), - value = value, - onValueChange = onValueChanged, - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface) - ) - } - } -} - -@Composable -private fun KeystoreImplementationChooser( - modifier: Modifier = Modifier, - currentImplementation: CurrentSecureArea, - onKeystoreImplementationChanged: (newImplementation: CurrentSecureArea) -> Unit -) { - LabeledUserInput( - modifier = modifier, - label = stringResource(id = R.string.txt_keystore_implementation) - ) { - var expanded by remember { mutableStateOf(false) } - OutlinedContainerHorizontal( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = true } - ) { - ValueLabel( - modifier = Modifier.weight(1f), - label = currentImplementation.displayName - ) - DropDownIndicator() - } - DropdownMenu( - modifier = Modifier - .fillMaxWidth(0.8f), - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - ProvisioningUtil.getInstance(LocalContext.current) - .secureAreaRepository.implementations.forEach { implementation -> - TextDropDownRow( - label = implementation.displayName, - onSelected = { - onKeystoreImplementationChanged(implementation.toSecureAreaState()) - expanded = false - } - ) - } - } - } -} - -@Composable -private fun CardArtDropDownRow( - modifier: Modifier = Modifier, - cardArt: DocumentColor, - onSelected: () -> Unit -) { - DropdownMenuItem( - text = { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Box( - modifier = Modifier - .size(32.dp) - .clip(RoundedCornerShape(8.dp)) - .background(gradientFor(cardArt), RoundedCornerShape(8.dp)), - ) - ValueLabel(label = stringResource(id = colorNameFor(cardArt))) - } - }, - onClick = onSelected - ) -} - -@Composable -fun OutlinedContainerVertical( - modifier: Modifier = Modifier, - outlineBorderWidth: Dp = 2.dp, - outlineBrush: Brush? = null, - content: @Composable ColumnScope.() -> Unit -) { - val brush = outlineBrush ?: SolidColor(MaterialTheme.colorScheme.outline) - Row( - modifier = modifier - .heightIn(48.dp) - .clip(RoundedCornerShape(12.dp)) - .border(outlineBorderWidth, brush, RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.inverseOnSurface), - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - content() - } - } -} - -@StringRes -private fun colorNameFor(cardArt: DocumentColor): Int { - return when (cardArt) { - is DocumentColor.Green -> R.string.document_color_green - is DocumentColor.Yellow -> R.string.document_color_yellow - is DocumentColor.Blue -> R.string.document_color_blue - is DocumentColor.Red -> R.string.document_color_red - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt deleted file mode 100644 index b642f0a14..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.theme.HolderAppTheme - -class AddSelfSignedFragment : Fragment() { - - private val viewModel: AddSelfSignedViewModel by viewModels() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - HolderAppTheme { - AddSelfSignedDocumentScreen( - viewModel = viewModel, - onNext = { onNext() } - ) - } - } - } - } - - private fun onNext() { - val state = viewModel.screenState.value - val secureAreaScreenState = requireNotNull(state.secureAreaSupportState) - val provisionInfo = ProvisionInfo( - docType = state.documentType, - docName = state.documentName, - docColor = state.cardArt.value, - currentSecureArea = state.currentSecureArea, - secureAreaSupportState = secureAreaScreenState, - validityInDays = state.validityInDays, - minValidityInDays = state.minValidityInDays, - numberMso = state.numberOfMso, - maxUseMso = state.maxUseOfMso - ) - val destination = AddSelfSignedFragmentDirections - .actionAddSelfSignedToSelfSignedDetails(provisionInfo) - findNavController().navigate(destination) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt deleted file mode 100644 index 84e36cc1b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedScreenState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import android.os.Parcelable -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.support.CurrentSecureArea -import com.android.identity.wallet.support.SecureAreaSupportState -import com.android.identity.wallet.support.toSecureAreaState -import com.android.identity.wallet.util.DocumentData -import com.android.identity.wallet.util.ProvisioningUtil -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AddSelfSignedScreenState( - val documentType: String = DocumentData.MDL_DOCTYPE, - val cardArt: DocumentColor = DocumentColor.Green, - val documentName: String = "Driving License", - val currentSecureArea: CurrentSecureArea = ProvisioningUtil.defaultSecureArea.toSecureAreaState(), - val numberOfMso: Int = 3, - val maxUseOfMso: Int = 1, - val validityInDays: Int = 30, - val minValidityInDays: Int = 10, - val secureAreaSupportState: SecureAreaSupportState? = null, -) : Parcelable diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt deleted file mode 100644 index fdc52d65a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/AddSelfSignedViewModel.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.document.DocumentColor -import com.android.identity.wallet.support.CurrentSecureArea -import com.android.identity.wallet.support.SecureAreaSupportState -import com.android.identity.wallet.util.getState -import com.android.identity.wallet.util.updateState -import kotlinx.coroutines.flow.StateFlow -import java.lang.Integer.max - -class AddSelfSignedViewModel( - private val savedStateHandle: SavedStateHandle -) : ViewModel() { - - val screenState: StateFlow = savedStateHandle.getState( - AddSelfSignedScreenState() - ) - - val documentItems: List = - HolderApp.documentTypeRepositoryInstance.documentTypes.filter { it.mdocDocumentType != null } - .map { DocumentItem(it.mdocDocumentType!!.docType, it.displayName) } - - - fun updateDocumentType(newValue: String, newName: String) { - savedStateHandle.updateState { - it.copy(documentType = newValue, documentName = newName) - } - } - - fun updateCardArt(newValue: DocumentColor) { - savedStateHandle.updateState { - it.copy(cardArt = newValue) - } - } - - fun updateDocumentName(newValue: String) { - savedStateHandle.updateState { - it.copy(documentName = newValue) - } - } - - fun updateKeystoreImplementation(newValue: CurrentSecureArea) { - savedStateHandle.updateState { - it.copy(currentSecureArea = newValue) - } - } - - fun updateSecureAreaSupportState(newValue: SecureAreaSupportState) { - savedStateHandle.updateState { - it.copy(secureAreaSupportState = newValue) - } - } - - fun updateValidityInDays(newValue: Int) { - val state = savedStateHandle.getState(AddSelfSignedScreenState()) - if (newValue < state.value.minValidityInDays) return - savedStateHandle.updateState { - it.copy(validityInDays = newValue) - } - } - - fun updateMinValidityInDays(newValue: Int) { - if (newValue <= 0) return - savedStateHandle.updateState { - val validityDays = max(newValue, it.validityInDays) - it.copy(minValidityInDays = newValue, validityInDays = validityDays) - } - } - - - fun updateNumberOfMso(newValue: Int) { - if (newValue <= 0) return - savedStateHandle.updateState { - it.copy(numberOfMso = newValue) - } - } - - fun updateMaxUseOfMso(newValue: Int) { - if (newValue <= 0) return - savedStateHandle.updateState { - it.copy(maxUseOfMso = newValue) - } - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/DocumentItem.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/DocumentItem.kt deleted file mode 100644 index 1009df77f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/DocumentItem.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.android.identity.wallet.selfsigned - -data class DocumentItem ( - val docType: String, - val displayName: String -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt b/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt deleted file mode 100644 index 80e34352f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/selfsigned/SelfSignedDocumentData.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.android.identity.wallet.selfsigned - -import android.os.Parcelable -import com.android.identity.wallet.support.CurrentSecureArea -import com.android.identity.wallet.util.Field -import com.android.identity.wallet.support.SecureAreaSupportState -import kotlinx.parcelize.Parcelize - -data class SelfSignedDocumentData( - val provisionInfo: ProvisionInfo, - val fields: List -) - -@Parcelize -data class ProvisionInfo( - val docType: String, - var docName: String, - var docColor: Int, - val currentSecureArea: CurrentSecureArea, - val secureAreaSupportState: SecureAreaSupportState, - val validityInDays: Int, - val minValidityInDays: Int, - val numberMso: Int, - val maxUseMso: Int -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsFragment.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsFragment.kt deleted file mode 100644 index 75ab58dc8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsFragment.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.android.identity.wallet.settings - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.theme.HolderAppTheme - -class CaCertificateDetailsFragment : Fragment() { - private val viewModel: CaCertificatesViewModel by activityViewModels { - CaCertificatesViewModel.factory() - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - val state by viewModel.currentCertificateItem.collectAsState() - HolderAppTheme { - CaCertificateDetailsScreen(certificateItem = state, - onDeleteCertificate = { deleteCertificate() }) - } - } - } - } - - private fun deleteCertificate() { - viewModel.deleteCertificate() - viewModel.loadCertificates() - findNavController().popBackStack() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsScreen.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsScreen.kt deleted file mode 100644 index 03ad6e22f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificateDetailsScreen.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.android.identity.wallet.settings - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.theme.HolderAppTheme -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.util.Date - -@Composable -fun CaCertificateDetailsScreen( - certificateItem: CertificateItem?, - onDeleteCertificate: () -> Unit = {} -) { - if (certificateItem == null) { - Title(title = "No certificate provided") - } else { - val scrollState = rememberScrollState() - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Column( - modifier = Modifier - .verticalScroll(scrollState) - .weight(1f) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Title(title = certificateItem.title) - Subtitle(title = "Issued to") - Line( - modifier = Modifier, - text = "Common Name (CN) " + certificateItem.commonNameSubject - ) - Line( - modifier = Modifier, - text = "Organisation (O) " + certificateItem.organisationSubject - ) - Line( - modifier = Modifier, - text = "Organisational Unit (OU) " + certificateItem.organisationalUnitSubject - ) - Subtitle(title = "Issued by") - Line( - modifier = Modifier, - text = "Common Name (CN) " + certificateItem.commonNameIssuer - ) - Line( - modifier = Modifier, - text = "Organisation (O) " + certificateItem.organisationIssuer - ) - Line( - modifier = Modifier, - text = "Organisational Unit (OU) " + certificateItem.organisationalUnitIssuer - ) - Subtitle(title = "Fingerprints") - Line(modifier = Modifier, "SHA-256 fingerprint") - Line(modifier = Modifier.padding(16.dp), certificateItem.sha255Fingerprint) - Line(modifier = Modifier, "SHA-1 fingerprint") - Line(modifier = Modifier.padding(16.dp), certificateItem.sha1Fingerprint) - if (certificateItem.docTypes.isNotEmpty()){ - Subtitle(title = "Supported mdoc types") - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 0.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - items(certificateItem.docTypes) { docType -> - Line(modifier = Modifier, text = docType) - } - } - } - } - if (certificateItem.supportsDelete) { - Button(onClick = onDeleteCertificate) { - Text(text = "Delete") - } - } - } - } -} - -@Composable -fun Title(title: String) { - Text( - modifier = Modifier.fillMaxWidth(), - text = title, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface - ) -} - -@Composable -fun Subtitle(title: String) { - Text( - modifier = Modifier.fillMaxWidth(), - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) -} - -@Composable -fun Line(modifier: Modifier, text: String) { - Text( - modifier = modifier.fillMaxWidth(), - text = text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - ) -} - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewCaCertificatesScreen() { - HolderAppTheme { - CaCertificateDetailsScreen( - certificateItem = CertificateItem( - title = "Test 1", - commonNameSubject = "*.google.com", - organisationSubject = "", - organisationalUnitSubject = "", - commonNameIssuer = "GTS CA 1C3", - organisationIssuer = "Google Trust Services LLC", - organisationalUnitIssuer = "", - notBefore = Date.from(LocalDateTime.now().minusDays(365).toInstant(ZoneOffset.UTC)), - notAfter = Date.from(LocalDateTime.now().plusDays(365).toInstant(ZoneOffset.UTC)), - sha255Fingerprint = "03 5C 31 E7 A9 F3 71 2B 27 1C 5A 8D 82 E5 6C 5B 92 BC FC 28 7F72D7 4A B6 9D 61 BF 53 EF 3E 67", - sha1Fingerprint = "9D 80 9B CF 63 AA86 29 E9 3C 78 9A EA DA 15 56 7E BF 56 D8", - docTypes = listOf("Doc type 1", "Doc type 2"), - supportsDelete = true, - trustPoint = null - ), - onDeleteCertificate = {} - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesFragment.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesFragment.kt deleted file mode 100644 index 06b201530..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesFragment.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.android.identity.wallet.settings - -import android.content.ClipboardManager -import android.content.Context -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.crypto.X509Cert -import com.android.identity.trustmanagement.TrustPoint -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.theme.HolderAppTheme -import com.android.identity.wallet.trustmanagement.getSubjectKeyIdentifier -import com.google.android.material.R -import com.google.android.material.snackbar.Snackbar -import java.io.ByteArrayInputStream -import java.security.cert.CertificateException -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate - - -class CaCertificatesFragment : Fragment() { - - private val viewModel: CaCertificatesViewModel by activityViewModels { - CaCertificatesViewModel.factory() - } - - private val browseCertificateLauncher = - registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> - uris.forEach { uri -> importCertificate(uri) } - viewModel.loadCertificates() - } - - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - val state = viewModel.screenState.collectAsState().value - viewModel.loadCertificates() - HolderAppTheme { - CaCertificatesScreen( - screenState = state, - onSelectCertificate = { - viewModel.setCurrentCertificateItem(it) - openDetails() - }, - onImportCertificate = { fileDialog() }, - onPasteCertificate = { pasteCertificate() } - ) - } - } - } - } - - private fun openDetails() { - val destination = CaCertificatesFragmentDirections.toCaCertificateDetails() - findNavController().navigate(destination) - } - - private fun fileDialog() { - browseCertificateLauncher.launch(arrayOf("*/*")) - } - - private fun importCertificate(uri: Uri) { - try { - this.requireContext().contentResolver.openInputStream(uri).use { inputStream -> - if (inputStream != null) { - val certificate = parseCertificate(inputStream.readBytes()) - HolderApp.trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded))) - HolderApp.certificateStorageEngineInstance.put( - certificate.getSubjectKeyIdentifier(), - certificate.encoded - ) - } - } - } catch (e: Throwable) { - showException(e) - } - } - - private fun pasteCertificate() { - try { - val clipboard = - activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - if (!clipboard.hasPrimaryClip() - || clipboard.primaryClip?.itemCount == 0 - || clipboard.primaryClip?.getItemAt(0)?.text == null - ) { - showMessage("Nothing found to paste") - return - } - val text = clipboard.primaryClip?.getItemAt(0)?.text!! - val certificate = parseCertificate(text.toString().toByteArray()) - HolderApp.trustManagerInstance.addTrustPoint(TrustPoint(X509Cert(certificate.encoded))) - HolderApp.certificateStorageEngineInstance.put( - certificate.getSubjectKeyIdentifier(), - certificate.encoded - ) - } catch (e: Throwable) { - showException(e) - } finally { - viewModel.loadCertificates() - } - } - - private fun showException(exception: Throwable) { - val message = when (exception) { - is FileAlreadyExistsException -> "The certificate is already in the mDoc Issuer Trust Store" - is CertificateException -> "The certificate could not be parsed correctly" - else -> exception.message - } - showMessage(message.toString()) - } - - private fun showMessage(message: String) { - val snackbar = Snackbar.make( - this.requireView(), - message, - Snackbar.LENGTH_LONG - ) - val snackTextView = snackbar.view.findViewById(R.id.snackbar_text) as TextView - snackTextView.maxLines = 4 - snackbar.show() - } - - /** - * Parse a byte array an X509 certificate - */ - private fun parseCertificate(certificateBytes: ByteArray): X509Certificate { - return CertificateFactory.getInstance("X509") - .generateCertificate(ByteArrayInputStream(certificateBytes)) as X509Certificate - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreen.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreen.kt deleted file mode 100644 index 863176bfa..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreen.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.android.identity.wallet.settings - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.theme.HolderAppTheme -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.util.Date - -@Composable -fun CaCertificatesScreen( - screenState: CaCertificatesScreenState, - onSelectCertificate: (item: CertificateItem) -> Unit, - onImportCertificate: () -> Unit, - onPasteCertificate: () -> Unit -) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .verticalScroll(scrollState) - .weight(1f) - ) { - if (screenState.certificates.isEmpty()) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = "No certificates provided", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } - } else { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(screenState.certificates) { certificateItem -> - Text( - modifier = Modifier.clickable { onSelectCertificate(certificateItem) }, - text = certificateItem.title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - } - } - Button(onClick = onImportCertificate) { - Text(text = "Import") - } - Button(onClick = onPasteCertificate) { - Text(text = "Paste") - } - } -} - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewCaCertificatesScreen() { - HolderAppTheme { - CaCertificatesScreen( - screenState = CaCertificatesScreenState( - listOf( - CertificateItem( - title = "Test 1", - commonNameSubject = "*.google.com", - organisationSubject = "", - organisationalUnitSubject = "", - commonNameIssuer = "GTS CA 1C3", - organisationIssuer = "Google Trust Services LLC", - organisationalUnitIssuer = "", - notBefore = Date.from( - LocalDateTime.now().minusDays(365).toInstant(ZoneOffset.UTC) - ), - notAfter = Date.from( - LocalDateTime.now().plusDays(365).toInstant(ZoneOffset.UTC) - ), - sha255Fingerprint = "03 5C 31 E7 A9 F3 71 2B 27 1C 5A 8D 82 E5 6C 5B 92 BC FC 28 7F72D7 4A B6 9D 61 BF 53 EF 3E 67", - sha1Fingerprint = "9D 80 9B CF 63 AA86 29 E9 3C 78 9A EA DA 15 56 7E BF 56 D8", - docTypes = emptyList(), - supportsDelete = false, - trustPoint = null - ), - CertificateItem( - title = "Test 2", - commonNameSubject = "*.google.com", - organisationSubject = "", - organisationalUnitSubject = "", - commonNameIssuer = "GTS CA 1C3", - organisationIssuer = "Google Trust Services LLC", - organisationalUnitIssuer = "", - notBefore = Date.from( - LocalDateTime.now().minusDays(100).toInstant( - ZoneOffset.UTC - ) - ), - notAfter = Date.from( - LocalDateTime.now().plusDays(100).toInstant(ZoneOffset.UTC) - ), - sha255Fingerprint = "03 5C 31 E7 A9 F3 71 2B 27 1C 5A 8D 82 E5 6C 5B 92 BC FC 28 7F72D7 4A B6 9D 61 BF 53 EF 3E 67", - sha1Fingerprint = "9D 80 9B CF 63 AA86 29 E9 3C 78 9A EA DA 15 56 7E BF 56 D8", - docTypes = emptyList(), - supportsDelete = false, - trustPoint = null - ) - ) - ), - onSelectCertificate = {}, - onImportCertificate = {}, - onPasteCertificate = {} - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreenState.kt deleted file mode 100644 index b6b8cc21e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesScreenState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.android.identity.wallet.settings - -data class CaCertificatesScreenState ( - val certificates: List = emptyList() -) { - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesViewModel.kt deleted file mode 100644 index abffacc97..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CaCertificatesViewModel.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.android.identity.wallet.settings - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory -import com.android.identity.crypto.javaX509Certificate -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.trustmanagement.getSubjectKeyIdentifier -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update - -class CaCertificatesViewModel() : ViewModel() { - - private val _screenState = MutableStateFlow(CaCertificatesScreenState()) - val screenState: StateFlow = _screenState.asStateFlow() - - private val _currentCertificateItem = MutableStateFlow(null) - val currentCertificateItem = _currentCertificateItem.asStateFlow() - fun loadCertificates() { - val certificates = - HolderApp.trustManagerInstance.getAllTrustPoints().map { it.toCertificateItem() } - _screenState.update { it.copy(certificates = certificates) } - } - - fun setCurrentCertificateItem(certificateItem: CertificateItem) { - _currentCertificateItem.update { certificateItem } - } - - fun deleteCertificate() { - _currentCertificateItem.value?.trustPoint?.let { - HolderApp.trustManagerInstance.removeTrustPoint(it) - HolderApp.certificateStorageEngineInstance.delete( - it.certificate.javaX509Certificate.getSubjectKeyIdentifier() - ) - } - } - - companion object { - fun factory(): ViewModelProvider.Factory { - return viewModelFactory { - initializer { CaCertificatesViewModel() } - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/CertificateItem.kt b/appholder/src/main/java/com/android/identity/wallet/settings/CertificateItem.kt deleted file mode 100644 index e7d9b1de9..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/CertificateItem.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.android.identity.wallet.settings - -import com.android.identity.trustmanagement.TrustPoint -import java.util.Date - -data class CertificateItem( - val title: String, - val commonNameSubject: String, - val organisationSubject: String, - val organisationalUnitSubject: String, - val commonNameIssuer: String, - val organisationIssuer: String, - val organisationalUnitIssuer: String, - val notBefore: Date, - val notAfter: Date, - val sha255Fingerprint: String, - val sha1Fingerprint: String, - val docTypes: List, - val supportsDelete: Boolean, - val trustPoint: TrustPoint? -) { -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/Mappers.kt b/appholder/src/main/java/com/android/identity/wallet/settings/Mappers.kt deleted file mode 100644 index ed12ad0b5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/Mappers.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.android.identity.wallet.settings - -import com.android.identity.crypto.javaX509Certificate -import com.android.identity.trustmanagement.TrustPoint -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.trustmanagement.getCommonName -import com.android.identity.wallet.trustmanagement.getOrganisation -import com.android.identity.wallet.trustmanagement.getSubjectKeyIdentifier -import com.android.identity.wallet.trustmanagement.organisationalUnit -import java.lang.StringBuilder -import java.security.MessageDigest - -fun TrustPoint.toCertificateItem(docTypes: List = emptyList()): CertificateItem { - val subject = this.certificate.javaX509Certificate.subjectX500Principal - val issuer = this.certificate.javaX509Certificate.issuerX500Principal - val sha255Fingerprint = hexWithSpaces( - MessageDigest.getInstance("SHA-256").digest( - this.certificate.encodedCertificate - ) - ) - val sha1Fingerprint = hexWithSpaces( - MessageDigest.getInstance("SHA-1").digest( - this.certificate.encodedCertificate - ) - ) - val defaultValue = "" - - return CertificateItem( - title = subject.name, - commonNameSubject = subject.getCommonName(defaultValue), - organisationSubject = subject.getOrganisation(defaultValue), - organisationalUnitSubject = subject.organisationalUnit(defaultValue), - commonNameIssuer = issuer.getCommonName(defaultValue), - organisationIssuer = issuer.getOrganisation(defaultValue), - organisationalUnitIssuer = issuer.organisationalUnit(defaultValue), - notBefore = this.certificate.javaX509Certificate.notBefore, - notAfter = this.certificate.javaX509Certificate.notAfter, - sha255Fingerprint = sha255Fingerprint, - sha1Fingerprint = sha1Fingerprint, - docTypes = docTypes, - supportsDelete = HolderApp.certificateStorageEngineInstance.get( - this.certificate.javaX509Certificate.getSubjectKeyIdentifier() - ) != null, - trustPoint = this - ) -} - -private fun hexWithSpaces(byteArray: ByteArray): String { - val stringBuilder = StringBuilder() - byteArray.forEach { - if (stringBuilder.isNotEmpty()) { - stringBuilder.append(" ") - } - stringBuilder.append(String.format("%02X", it)) - } - return stringBuilder.toString() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsFragment.kt b/appholder/src/main/java/com/android/identity/wallet/settings/SettingsFragment.kt deleted file mode 100644 index 1919fe1a4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsFragment.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.android.identity.wallet.settings - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.theme.HolderAppTheme - -class SettingsFragment : Fragment() { - - private val settingsViewModel: SettingsViewModel by viewModels() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setContent { - HolderAppTheme { - val state = settingsViewModel.settingsState.collectAsState().value - SettingsScreen( - modifier = Modifier.fillMaxSize(), - screenState = state, - onAutoCloseChanged = settingsViewModel::onConnectionAutoCloseChanged, - onSessionEncryptionCurveChanged = settingsViewModel::onEphemeralKeyCurveChanged, - onUseStaticHandoverChanged = settingsViewModel::onUseStaticHandoverChanged, - onUseL2CAPChanged = settingsViewModel::onL2CAPChanged, - onBLEDataRetrievalModeChanged = settingsViewModel::onBleDataRetrievalChanged, - onBLEServiceCacheChanged = settingsViewModel::onBleServiceCacheChanged, - onBLEPeripheralDataRetrievalModeChanged = settingsViewModel::onBlePeripheralModeChanged, - onWiFiAwareChanged = settingsViewModel::onWiFiAwareChanged, - onNfcChanged = settingsViewModel::onNFCChanged, - onDebugChanged = settingsViewModel::onDebugLoggingChanged, - onOpenCaCertificates = {openCaCertificates()}, - ) - } - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - settingsViewModel.loadSettings() - } - - private fun openCaCertificates(){ - val destination = SettingsFragmentDirections.toCaCertificates() - findNavController().navigate(destination) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreen.kt b/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreen.kt deleted file mode 100644 index 5de767056..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreen.kt +++ /dev/null @@ -1,296 +0,0 @@ -package com.android.identity.wallet.settings - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -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.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.android.identity.wallet.composables.curveLabelFor -import com.android.identity.wallet.theme.HolderAppTheme - -@Composable -fun SettingsScreen( - modifier: Modifier = Modifier, - screenState: SettingsScreenState, - onAutoCloseChanged: (Boolean) -> Unit, - onSessionEncryptionCurveChanged: (newValue: SettingsScreenState.SessionEncryptionCurveOption) -> Unit, - onUseStaticHandoverChanged: (Boolean) -> Unit, - onUseL2CAPChanged: (Boolean) -> Unit, - onBLEServiceCacheChanged: (Boolean) -> Unit, - onBLEDataRetrievalModeChanged: (Boolean) -> Unit, - onBLEPeripheralDataRetrievalModeChanged: (Boolean) -> Unit, - onWiFiAwareChanged: (Boolean) -> Unit, - onNfcChanged: (Boolean) -> Unit, - onDebugChanged: (Boolean) -> Unit, - onOpenCaCertificates: () -> Unit, -) { - Column(modifier = modifier) { - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .padding(16.dp) - .verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - SettingSectionTitle(title = "General") - SettingToggle( - title = "Auto close connection", - subtitleOn = "Close connection after first response", - subtitleOff = "Don't close connection after first response", - isChecked = screenState.autoCloseEnabled, - onCheckedChange = onAutoCloseChanged - ) - SettingsDropDown( - title = "Session Encryption Curve", - description = curveLabelFor(screenState.sessionEncryptionCurveOption.toEcCurve()), - onCurveChanged = onSessionEncryptionCurveChanged - ) - SettingSectionTitle(title = "NFC Engagement") - SettingToggle( - title = "Use static handover", - subtitleOn = "Use static handover", - subtitleOff = "Use negotiated handover", - isChecked = screenState.useStaticHandover, - onCheckedChange = onUseStaticHandoverChanged - ) - SettingSectionTitle(title = "Data retrieval options") - SettingToggle( - title = "Use L2CAP if available", - subtitleOn = "Use L2CAP", - subtitleOff = "Don't use L2CAP", - isChecked = screenState.isL2CAPEnabled, - enabled = screenState.isBleEnabled(), - onCheckedChange = onUseL2CAPChanged - ) - SettingToggle( - title = "Clear BLE Service Cache", - subtitleOn = "Clean the cache", - subtitleOff = "Don't clean the cache", - isChecked = screenState.isBleClearCacheEnabled, - enabled = screenState.isBleEnabled(), - onCheckedChange = onBLEServiceCacheChanged - ) - SettingSectionTitle(title = "Data retrieval methods") - SettingToggle( - title = "BLE central client mode", - subtitleOn = "BLE central client mode activated", - subtitleOff = "BLE central client mode deactivated", - isChecked = screenState.isBleDataRetrievalEnabled, - onCheckedChange = onBLEDataRetrievalModeChanged - ) - SettingToggle( - title = "BLE peripheral server mode", - subtitleOn = "BLE peripheral server mode activated", - subtitleOff = "BLE peripheral server mode deactivated", - isChecked = screenState.isBlePeripheralModeEnabled, - onCheckedChange = onBLEPeripheralDataRetrievalModeChanged - ) - SettingToggle( - title = "Wifi Aware", - subtitleOn = "Wifi Aware transfer activated", - subtitleOff = "Wifi Aware transfer deactivated", - isChecked = screenState.wifiAwareEnabled, - onCheckedChange = onWiFiAwareChanged - ) - SettingToggle( - title = "NFC", - subtitleOn = "NFC transfer activated", - subtitleOff = "NFC transfer deactivated", - isChecked = screenState.nfcEnabled, - onCheckedChange = onNfcChanged - ) - SettingSectionTitle(title = "Debug logging options") - SettingToggle( - title = "Debug", - subtitleOn = "Debug logging activated", - subtitleOff = "Debug logging deactivated", - isChecked = screenState.debugEnabled, - onCheckedChange = onDebugChanged - ) - SettingSectionTitle( - title = "CA Certificates" - ) - SettingItem( - modifier = Modifier - .clickable { onOpenCaCertificates() }, - title = "Show CA Certificates", - subtitle = "Click here to show the CA Certificates" - ) - } - } -} - -@Composable -private fun SettingSectionTitle( - modifier: Modifier = Modifier, - title: String -) { - Column(modifier = modifier) { - Text( - modifier = Modifier.fillMaxWidth(), - text = title, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface - ) - } -} - -@Composable -private fun SettingItem( - modifier: Modifier = Modifier, - title: String, - subtitle: String -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.fillMaxWidth()) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface - ) - } - } -} - -@Composable -private fun SettingToggle( - modifier: Modifier = Modifier, - title: String, - subtitleOn: String, - subtitleOff: String, - isChecked: Boolean, - onCheckedChange: (Boolean) -> Unit, - enabled: Boolean = true -) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - val subtitle = if (isChecked) subtitleOn else subtitleOff - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface - ) - } - Switch( - checked = isChecked, - enabled = enabled, - onCheckedChange = onCheckedChange - ) - } -} - -@Composable -private fun SettingsDropDown( - modifier: Modifier = Modifier, - title: String, - description: String, - onCurveChanged: (selection: SettingsScreenState.SessionEncryptionCurveOption) -> Unit -) { - var dropDownExpanded by remember { mutableStateOf(false) } - val expandDropDown = { dropDownExpanded = true } - Row( - modifier = modifier.clickable { expandDropDown() }, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface - ) - } - IconButton(onClick = expandDropDown) { - Icon( - imageVector = Icons.Default.ArrowDropDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface - ) - } - val entries = SettingsScreenState.SessionEncryptionCurveOption.values().toList() - DropdownMenu( - expanded = dropDownExpanded, - onDismissRequest = { dropDownExpanded = false } - ) { - for (entry in entries) { - DropdownMenuItem( - modifier = modifier, - text = { - Text( - modifier = Modifier.fillMaxWidth(), - text = curveLabelFor(curveOption = entry.toEcCurve()) - ) - }, - onClick = { - onCurveChanged(entry) - dropDownExpanded = false - } - ) - } - } - } -} - - -@Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SettingsScreenPreview() { - HolderAppTheme { - SettingsScreen( - modifier = Modifier.fillMaxSize(), - screenState = SettingsScreenState(), - onAutoCloseChanged = {}, - onSessionEncryptionCurveChanged = {}, - onUseStaticHandoverChanged = {}, - onUseL2CAPChanged = {}, - onBLEServiceCacheChanged = {}, - onBLEDataRetrievalModeChanged = {}, - onBLEPeripheralDataRetrievalModeChanged = {}, - onWiFiAwareChanged = {}, - onNfcChanged = {}, - onDebugChanged = {}, - onOpenCaCertificates = {} - ) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreenState.kt b/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreenState.kt deleted file mode 100644 index dacbd9a9e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsScreenState.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.android.identity.wallet.settings - -import android.os.Parcelable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import com.android.identity.crypto.EcCurve -import kotlinx.parcelize.Parcelize - -@Stable -@Immutable -data class SettingsScreenState( - val autoCloseEnabled: Boolean = true, - val sessionEncryptionCurveOption: SessionEncryptionCurveOption = SessionEncryptionCurveOption.P256, - val useStaticHandover: Boolean = true, - val isL2CAPEnabled: Boolean = false, - val isBleClearCacheEnabled: Boolean = false, - val isBleDataRetrievalEnabled: Boolean = true, - val isBlePeripheralModeEnabled: Boolean = false, - val wifiAwareEnabled: Boolean = false, - val nfcEnabled: Boolean = false, - val debugEnabled: Boolean = true -) { - - fun isBleEnabled(): Boolean = isBleDataRetrievalEnabled || isBlePeripheralModeEnabled - - fun canToggleBleDataRetrievalMode(newBleCentralMode: Boolean): Boolean { - val updatedState = copy(isBleDataRetrievalEnabled = newBleCentralMode) - return updatedState.hasDataRetrieval() - } - - fun canToggleBlePeripheralMode(newBlePeripheralMode: Boolean): Boolean { - val updatedState = copy(isBlePeripheralModeEnabled = newBlePeripheralMode) - return updatedState.hasDataRetrieval() - } - - fun canToggleWifiAware(newWifiAwareValue: Boolean): Boolean { - val updatedState = copy(wifiAwareEnabled = newWifiAwareValue) - return updatedState.hasDataRetrieval() - } - - fun canToggleNfc(newNfcValue: Boolean): Boolean { - val updatedState = copy(nfcEnabled = newNfcValue) - return updatedState.hasDataRetrieval() - } - - private fun hasDataRetrieval(): Boolean = - isBleDataRetrievalEnabled - || isBlePeripheralModeEnabled - || wifiAwareEnabled - || nfcEnabled - - @Parcelize - enum class SessionEncryptionCurveOption : Parcelable { - P256, - P384, - P521, - BrainPoolP256R1, - BrainPoolP320R1, - BrainPoolP384R1, - BrainPoolP512R1, - X25519, - X448; - - fun toEcCurve(): EcCurve = - when (this) { - P256 -> EcCurve.P256 - P384 -> EcCurve.P384 - P521 -> EcCurve.P521 - BrainPoolP256R1 -> EcCurve.BRAINPOOLP256R1 - BrainPoolP320R1 -> EcCurve.BRAINPOOLP320R1 - BrainPoolP384R1 -> EcCurve.BRAINPOOLP384R1 - BrainPoolP512R1 -> EcCurve.BRAINPOOLP512R1 - X25519 -> EcCurve.X25519 - X448 -> EcCurve.X448 - } - - companion object { - fun fromEcCurve(curve: EcCurve): SessionEncryptionCurveOption = - when (curve) { - EcCurve.P256 -> P256 - EcCurve.P384 -> P384 - EcCurve.P521 -> P521 - EcCurve.BRAINPOOLP256R1 -> BrainPoolP256R1 - EcCurve.BRAINPOOLP320R1 -> BrainPoolP320R1 - EcCurve.BRAINPOOLP384R1 -> BrainPoolP384R1 - EcCurve.BRAINPOOLP512R1 -> BrainPoolP512R1 - EcCurve.X25519 -> X25519 - EcCurve.X448 -> X448 - else -> throw IllegalStateException("Unknown EcCurve") - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/settings/SettingsViewModel.kt deleted file mode 100644 index 0526dd243..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/settings/SettingsViewModel.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.android.identity.wallet.settings - -import androidx.lifecycle.ViewModel -import com.android.identity.wallet.util.PreferencesHelper -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update - -class SettingsViewModel : ViewModel() { - - private val mutableSettingsState = MutableStateFlow(SettingsScreenState()) - val settingsState: StateFlow = mutableSettingsState - - fun loadSettings() { - val settingsState = SettingsScreenState( - autoCloseEnabled = PreferencesHelper.isConnectionAutoCloseEnabled(), - sessionEncryptionCurveOption = SettingsScreenState.SessionEncryptionCurveOption.fromEcCurve( - PreferencesHelper.getEphemeralKeyCurveOption() - ), - useStaticHandover = PreferencesHelper.shouldUseStaticHandover(), - isL2CAPEnabled = PreferencesHelper.isBleL2capEnabled(), - isBleClearCacheEnabled = PreferencesHelper.isBleClearCacheEnabled(), - isBleDataRetrievalEnabled = PreferencesHelper.isBleDataRetrievalEnabled(), - isBlePeripheralModeEnabled = PreferencesHelper.isBleDataRetrievalPeripheralModeEnabled(), - wifiAwareEnabled = PreferencesHelper.isWifiDataRetrievalEnabled(), - nfcEnabled = PreferencesHelper.isNfcDataRetrievalEnabled(), - debugEnabled = PreferencesHelper.isDebugLoggingEnabled() - ) - mutableSettingsState.value = settingsState - } - - fun onConnectionAutoCloseChanged(newValue: Boolean) { - PreferencesHelper.setConnectionAutoCloseEnabled(newValue) - mutableSettingsState.update { it.copy(autoCloseEnabled = newValue) } - } - - fun onEphemeralKeyCurveChanged( - sessionEncryptionCurveOption: SettingsScreenState.SessionEncryptionCurveOption - ) { - PreferencesHelper.setEphemeralKeyCurveOption(sessionEncryptionCurveOption.toEcCurve()) - mutableSettingsState.update { it.copy(sessionEncryptionCurveOption = sessionEncryptionCurveOption) } - } - - fun onUseStaticHandoverChanged(newValue: Boolean) { - PreferencesHelper.setUseStaticHandover(newValue) - mutableSettingsState.update { it.copy(useStaticHandover = newValue) } - } - - fun onL2CAPChanged(newValue: Boolean) { - PreferencesHelper.setBleL2CAPEnabled(newValue) - mutableSettingsState.update { it.copy(isL2CAPEnabled = newValue) } - } - - fun onBleServiceCacheChanged(newValue: Boolean) { - PreferencesHelper.setBleClearCacheEnabled(newValue) - mutableSettingsState.update { it.copy(isBleClearCacheEnabled = newValue) } - } - - fun onBleDataRetrievalChanged(newValue: Boolean) { - val state = mutableSettingsState.value - if (state.canToggleBleDataRetrievalMode(newValue)) { - PreferencesHelper.setBleDataRetrievalEnabled(newValue) - mutableSettingsState.update { it.copy(isBleDataRetrievalEnabled = newValue) } - } - } - - fun onBlePeripheralModeChanged(newValue: Boolean) { - val state = mutableSettingsState.value - if (state.canToggleBlePeripheralMode(newValue)) { - PreferencesHelper.setBlePeripheralDataRetrievalMode(newValue) - mutableSettingsState.update { it.copy(isBlePeripheralModeEnabled = newValue) } - } - } - - fun onWiFiAwareChanged(newValue: Boolean) { - val state = mutableSettingsState.value - if (state.canToggleWifiAware(newValue)) { - PreferencesHelper.setWifiDataRetrievalEnabled(newValue) - mutableSettingsState.update { it.copy(wifiAwareEnabled = newValue) } - } - } - - fun onNFCChanged(newValue: Boolean) { - val state = mutableSettingsState.value - if (state.canToggleNfc(newValue)) { - PreferencesHelper.setNfcDataRetrievalEnabled(newValue) - mutableSettingsState.update { it.copy(nfcEnabled = newValue) } - } - } - - fun onDebugLoggingChanged(newValue: Boolean) { - PreferencesHelper.setDebugLoggingEnabled(newValue) - mutableSettingsState.update { it.copy(debugEnabled = newValue) } - } -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt deleted file mode 100644 index f9d518c7b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupport.kt +++ /dev/null @@ -1,227 +0,0 @@ -package com.android.identity.wallet.support - -import android.os.Handler -import android.os.Looper -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.fragment.app.Fragment -import com.android.identity.android.securearea.AndroidKeystoreCreateKeySettings -import com.android.identity.android.securearea.AndroidKeystoreKeyInfo -import com.android.identity.android.securearea.AndroidKeystoreKeyUnlockData -import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.android.securearea.UserAuthenticationType -import com.android.identity.android.securearea.userAuthenticationTypeSet -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.CborMap -import com.android.identity.crypto.Algorithm -import com.android.identity.securearea.CreateKeySettings -import com.android.identity.crypto.EcCurve -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.KeyPurpose -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.wallet.R -import com.android.identity.wallet.authprompt.UserAuthPromptBuilder -import com.android.identity.wallet.composables.AndroidSetupContainer -import com.android.identity.wallet.composables.AuthenticationKeyCurveAndroid -import com.android.identity.wallet.composables.MdocAuthentication -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveOption -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState -import com.android.identity.wallet.composables.state.AuthTypeState -import com.android.identity.wallet.composables.state.MdocAuthOption -import kotlinx.datetime.Instant - -class AndroidKeystoreSecureAreaSupport( - private val capabilities: AndroidKeystoreSecureArea.Capabilities -) : SecureAreaSupport { - - private val screenState = AndroidKeystoreSecureAreaSupportState( - allowLSKFUnlocking = AuthTypeState(true, capabilities.multipleAuthenticationTypesSupported), - allowBiometricUnlocking = AuthTypeState(true, capabilities.multipleAuthenticationTypesSupported), - useStrongBox = AuthTypeState(false, capabilities.strongBoxSupported), - mDocAuthOption = MdocAuthOption(isEnabled = capabilities.keyAgreementSupported), - authKeyCurveState = AndroidAuthKeyCurveState(isEnabled = capabilities.curve25519Supported) - ) - - override fun Fragment.unlockKey( - credential: MdocCredential, - onKeyUnlocked: (unlockData: KeyUnlockData?) -> Unit, - onUnlockFailure: (wasCancelled: Boolean) -> Unit - ) { - val keyInfo = credential.secureArea.getKeyInfo(credential.alias) as AndroidKeystoreKeyInfo - val unlockData = AndroidKeystoreKeyUnlockData(credential.alias) - - val allowLskf = keyInfo.userAuthenticationTypes.contains(UserAuthenticationType.LSKF) - val allowBiometric = keyInfo.userAuthenticationTypes.contains(UserAuthenticationType.BIOMETRIC) - val allowBoth = keyInfo.userAuthenticationTypes.contains(UserAuthenticationType.LSKF) && - keyInfo.userAuthenticationTypes.contains(UserAuthenticationType.BIOMETRIC) - val allowLskfUnlock = allowLskf || allowBoth - val allowBiometricUnlock = allowBiometric || allowBoth - val forceLskf: Boolean = !allowBiometricUnlock - - val userAuthRequest = UserAuthPromptBuilder.requestUserAuth(this) - .withTitle(getString(R.string.bio_auth_title)) - .withSuccessCallback { - onKeyUnlocked(unlockData) - } - .withCancelledCallback { - if (allowLskfUnlock) { - val runnable = { - unlockKey(credential, onKeyUnlocked, onUnlockFailure) - } - // Without this delay, the prompt won't reshow - Handler(Looper.getMainLooper()).postDelayed(runnable, 100) - } else { - onUnlockFailure(true) - } - } - .withFailureCallback { onUnlockFailure(false) } - .setForceLskf(forceLskf) - if (allowLskfUnlock) { - userAuthRequest.withNegativeButton(getString(R.string.bio_auth_use_pin)) - } else { - userAuthRequest.withNegativeButton("Cancel") - } - val cryptoObject = unlockData.getCryptoObjectForSigning(Algorithm.ES256) - userAuthRequest.build().authenticate(cryptoObject) - } - - @Composable - override fun SecureAreaAuthUi( - onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit - ) { - var compositionState by remember { mutableStateOf(screenState) } - LaunchedEffect(key1 = compositionState) { - onUiStateUpdated(compositionState) - } - AndroidSetupContainer( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - isOn = compositionState.userAuthentication, - timeoutSeconds = compositionState.userAuthenticationTimeoutSeconds, - lskfAuthTypeState = compositionState.allowLSKFUnlocking, - biometricAuthTypeState = compositionState.allowBiometricUnlocking, - useStrongBox = compositionState.useStrongBox, - onUserAuthenticationChanged = { - compositionState = compositionState.copy(userAuthentication = it) - }, - onAuthTimeoutChanged = { seconds -> - if (seconds < 0) return@AndroidSetupContainer - compositionState = compositionState.copy(userAuthenticationTimeoutSeconds = seconds) - }, - onLskfAuthChanged = { - val allowLskfUnlock = - if (compositionState.allowBiometricUnlocking.isEnabled) it else true - val newValue = compositionState.allowLSKFUnlocking.copy(isEnabled = allowLskfUnlock) - compositionState = compositionState.copy(allowLSKFUnlocking = newValue) - }, - onBiometricAuthChanged = { - val allowBiometricUnlock = - if (compositionState.allowLSKFUnlocking.isEnabled) it else true - val newValue = - compositionState.allowBiometricUnlocking.copy(isEnabled = allowBiometricUnlock) - compositionState = compositionState.copy(allowBiometricUnlocking = newValue) - }, - onStrongBoxChanged = { newValue -> - val update = compositionState.copy( - useStrongBox = compositionState.useStrongBox.copy(isEnabled = newValue), - mDocAuthOption = MdocAuthOption( - isEnabled = if (newValue) capabilities.strongBoxKeyAgreementSupported else capabilities.keyAgreementSupported - ), - authKeyCurveState = AndroidAuthKeyCurveState( - isEnabled = if (newValue) capabilities.strongBoxCurve25519Supported else capabilities.curve25519Supported - ) - ) - compositionState = update - } - ) - MdocAuthentication( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = compositionState.mDocAuthOption, - onMdocAuthOptionChange = { newValue -> - val authState = compositionState.mDocAuthOption.copy(mDocAuthentication = newValue) - compositionState = compositionState.copy( - mDocAuthOption = authState, - authKeyCurveState = compositionState.authKeyCurveState.copy( - authCurve = AndroidAuthKeyCurveOption.P_256 - ) - ) - } - ) - AuthenticationKeyCurveAndroid( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = compositionState.authKeyCurveState, - mDocAuthState = compositionState.mDocAuthOption, - onAndroidAuthKeyCurveChanged = { - val newValue = compositionState.authKeyCurveState.copy(authCurve = it) - compositionState = compositionState.copy(authKeyCurveState = newValue) - } - ) - } - - override fun getSecureAreaSupportState(): SecureAreaSupportState { - return screenState - } - - override fun createAuthKeySettingsConfiguration(secureAreaSupportState: SecureAreaSupportState): ByteArray { - val state = secureAreaSupportState as AndroidKeystoreSecureAreaSupportState - - val userAuthSettings = mutableSetOf() - if (state.allowLSKFUnlocking.isEnabled) { - userAuthSettings.add(UserAuthenticationType.LSKF) - } - if (state.allowBiometricUnlocking.isEnabled) { - userAuthSettings.add(UserAuthenticationType.BIOMETRIC) - } - - return Cbor.encode( - CborMap.builder() - .put("curve", state.authKeyCurveState.authCurve.toEcCurve().coseCurveIdentifier.toLong()) - .put("purposes", KeyPurpose.encodeSet(setOf(state.mDocAuthOption.mDocAuthentication.toKeyPurpose()))) - .put("userAuthEnabled", state.userAuthentication) - .put("userAuthTimeoutMillis", state.userAuthenticationTimeoutSeconds.toLong() * 1000L) - .put("userAuthSettings", UserAuthenticationType.encodeSet(userAuthSettings)) - .put("useStrongBox", state.useStrongBox.isEnabled) - .end() - .build() - ) - } - - override fun createAuthKeySettingsFromConfiguration( - encodedConfiguration: ByteArray, - challenge: ByteArray, - validFrom: Instant, - validUntil: Instant - ): CreateKeySettings { - val map = Cbor.decode(encodedConfiguration) - val curve = EcCurve.fromInt(map["curve"].asNumber.toInt()) - val purposes = KeyPurpose.decodeSet(map["purposes"].asNumber) - val userAuthEnabled = map["userAuthEnabled"].asBoolean - val userAuthTimeoutMillis = map["userAuthTimeoutMillis"].asNumber - val userAuthSettings = map["userAuthSettings"].asNumber - val useStrongBox = map["useStrongBox"].asBoolean - return AndroidKeystoreCreateKeySettings.Builder(challenge) - .setEcCurve(curve) - .setKeyPurposes(purposes) - .setValidityPeriod(validFrom, validUntil) - .setUseStrongBox(useStrongBox) - .setUserAuthenticationRequired( - userAuthEnabled, - userAuthTimeoutMillis, - userAuthSettings.userAuthenticationTypeSet - ) - .build() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt deleted file mode 100644 index 8e75167b2..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/AndroidKeystoreSecureAreaSupportState.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.android.identity.wallet.support - -import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState -import com.android.identity.wallet.composables.state.AuthTypeState -import com.android.identity.wallet.composables.state.MdocAuthOption -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AndroidKeystoreSecureAreaSupportState( - override val mDocAuthOption: MdocAuthOption = MdocAuthOption(), - val userAuthentication: Boolean = true, - val userAuthenticationTimeoutSeconds: Int = 0, - val allowLSKFUnlocking: AuthTypeState = AuthTypeState( - isEnabled = true, - canBeModified = false - ), - val allowBiometricUnlocking: AuthTypeState = AuthTypeState( - isEnabled = true, - canBeModified = false - ), - val useStrongBox: AuthTypeState = AuthTypeState( - isEnabled = false, - canBeModified = false - ), - val authKeyCurveState: AndroidAuthKeyCurveState = AndroidAuthKeyCurveState(), -) : SecureAreaSupportState { - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt b/appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt deleted file mode 100644 index 7508e8571..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/CurrentSecureArea.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.android.identity.wallet.support - -import android.os.Parcelable -import com.android.identity.securearea.SecureArea -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class CurrentSecureArea( - @IgnoredOnParcel val secureArea: SecureArea, - val identifier: String = secureArea.identifier, - val displayName: String = secureArea.displayName -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt deleted file mode 100644 index 14088a783..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/MdocAuthStateExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.support - -import com.android.identity.securearea.KeyPurpose -import com.android.identity.wallet.composables.state.MdocAuthStateOption - -fun MdocAuthStateOption.toKeyPurpose(): KeyPurpose = - if (this == MdocAuthStateOption.ECDSA) { - KeyPurpose.SIGN - } else { - KeyPurpose.AGREE_KEY - } \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt deleted file mode 100644 index 35c697d76..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupport.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.android.identity.wallet.support - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.fragment.app.Fragment -import com.android.identity.android.securearea.AndroidKeystoreSecureArea -import com.android.identity.document.Document -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.CreateKeySettings -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.securearea.SecureArea -import com.android.identity.securearea.software.SoftwareSecureArea -import kotlinx.datetime.Instant - -interface SecureAreaSupport { - - /** - * This function should create a composable that will render the portion of the UI - * for the specific [SecureArea] setup inside the [AddSelfSignedDocumentScreen]. - * - * The composable should hold and manage its state internally, and expose it through - * the [onUiStateUpdated] lambda. The state must be an implementation of [SecureAreaSupportState] - */ - @Composable - fun SecureAreaAuthUi(onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit) - - /** - * This function should create [SecureArea.KeyUnlockData] based on the incoming - * [Document.AuthenticationKey]. Its implementation should decide on the mechanism - * that will do the unlocking (i.e. present a biometric prompts or other sort of UI), - * and the way the [SecureArea.KeyUnlockData] is created. - * - * The function is an extension on a [Fragment] due to the nature of Android and the navigation, - * so in case of rendering a UI specific for unlocking (like a Biometric Prompt, or a Dialog), - * there is a provided way to navigate using the [findNavController] function. - */ - fun Fragment.unlockKey( - credential: MdocCredential, - onKeyUnlocked: (unlockData: KeyUnlockData?) -> Unit, - onUnlockFailure: (wasCancelled: Boolean) -> Unit - ) - - /** - * Should return the current [SecureAreaSupportState] which is used by the composition - * when rendering the composable UI. - */ - fun getSecureAreaSupportState(): SecureAreaSupportState - - /** - * Returns a configuration for creating authentication keys which can be persisted to disk - * and passed to [createAuthKeySettingsFromConfiguration] every time new authentication keys - * need to be created. - */ - fun createAuthKeySettingsConfiguration(secureAreaSupportState: SecureAreaSupportState): ByteArray - - /** - * Creates a [SecureArea.CreateKeySettings] instead based on the settings previously created - * with [createAuthKeySettingsConfiguration] and the given challenge and validity period. - */ - fun createAuthKeySettingsFromConfiguration( - encodedConfiguration: ByteArray, - challenge: ByteArray, - validFrom: Instant, - validUntil: Instant - ): CreateKeySettings - - companion object { - fun getInstance( - context: Context, - currentSecureArea: CurrentSecureArea - ): SecureAreaSupport { - return when (currentSecureArea.secureArea) { - is AndroidKeystoreSecureArea -> { - val capabilities = AndroidKeystoreSecureArea.Capabilities(context) - AndroidKeystoreSecureAreaSupport(capabilities) - } - - is SoftwareSecureArea -> SoftwareKeystoreSecureAreaSupport() - - else -> SecureAreaSupportNull() - } - } - - fun getInstance( - context: Context, - secureArea: SecureArea, - ): SecureAreaSupport { - return when (secureArea) { - is AndroidKeystoreSecureArea -> { - val capabilities = AndroidKeystoreSecureArea.Capabilities(context) - AndroidKeystoreSecureAreaSupport(capabilities) - } - - is SoftwareSecureArea -> SoftwareKeystoreSecureAreaSupport() - - else -> SecureAreaSupportNull() - } - } - } -} - -/** - * Utility function to convert a [SecureArea] implementation into a corresponding state - * used by the Jetpack Compose composition when rendering the UI. - */ -fun SecureArea.toSecureAreaState(): CurrentSecureArea { - return CurrentSecureArea(this) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt deleted file mode 100644 index 9467ac0f6..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportNull.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.android.identity.wallet.support - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.fragment.app.Fragment -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.CreateKeySettings -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.wallet.selfsigned.OutlinedContainerVertical -import kotlinx.datetime.Instant - -class SecureAreaSupportNull : SecureAreaSupport { - - private val state = SecureAreaSupportStateNull() - - @Composable - override fun SecureAreaAuthUi(onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit) { - val compositionState by remember { mutableStateOf(state) } - LaunchedEffect(key1 = compositionState) { - onUiStateUpdated(compositionState) - } - OutlinedContainerVertical( - modifier = Modifier.padding(horizontal = 16.dp) - ) { - Text( - modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp), - text = "The selected Secure Area lacks dedicated support so no additional options are available. " + - "Please add a SecureAreaSupport-derived class.", - style = MaterialTheme.typography.bodySmall - ) - } - } - - override fun Fragment.unlockKey( - credential: MdocCredential, - onKeyUnlocked: (unlockData: KeyUnlockData?) -> Unit, - onUnlockFailure: (wasCancelled: Boolean) -> Unit - ) { - throw IllegalStateException("No implementation") - } - - override fun getSecureAreaSupportState(): SecureAreaSupportState = state - - override fun createAuthKeySettingsConfiguration(secureAreaSupportState: SecureAreaSupportState): ByteArray = - ByteArray(0) - - override fun createAuthKeySettingsFromConfiguration( - encodedConfiguration: ByteArray, - challenge: ByteArray, - validFrom: Instant, - validUntil: Instant - ): CreateKeySettings = CreateKeySettings() -} diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt deleted file mode 100644 index 864f32447..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.android.identity.wallet.support - -import android.os.Parcelable -import com.android.identity.wallet.composables.state.MdocAuthOption - -interface SecureAreaSupportState : Parcelable { - val mDocAuthOption: MdocAuthOption -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt b/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt deleted file mode 100644 index 74a419f7e..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SecureAreaSupportStateNull.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.support - -import com.android.identity.wallet.composables.state.MdocAuthOption -import kotlinx.parcelize.Parcelize - -@Parcelize -class SecureAreaSupportStateNull : SecureAreaSupportState { - - override val mDocAuthOption: MdocAuthOption - get() = MdocAuthOption() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt b/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt deleted file mode 100644 index 040ca0d28..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupport.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.android.identity.wallet.support - -import android.os.Handler -import android.os.Looper -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.fragment.findNavController -import co.nstant.`in`.cbor.CborBuilder -import com.android.identity.cbor.Cbor -import com.android.identity.securearea.CreateKeySettings -import com.android.identity.crypto.EcCurve -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.KeyPurpose -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.securearea.software.SoftwareCreateKeySettings -import com.android.identity.securearea.software.SoftwareKeyUnlockData -import com.android.identity.wallet.authconfirmation.AuthConfirmationFragmentDirections -import com.android.identity.wallet.authconfirmation.PassphraseAuthResult -import com.android.identity.wallet.authconfirmation.PassphrasePromptViewModel -import com.android.identity.wallet.composables.AuthenticationKeyCurveSoftware -import com.android.identity.wallet.composables.MdocAuthentication -import com.android.identity.wallet.composables.SoftwareSetupContainer -import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveOption -import com.android.identity.wallet.util.FormatUtil -import kotlinx.coroutines.launch -import kotlinx.datetime.Instant - -class SoftwareKeystoreSecureAreaSupport : SecureAreaSupport { - - private val screenState = SoftwareKeystoreSecureAreaSupportState() - - override fun Fragment.unlockKey( - credential: MdocCredential, - onKeyUnlocked: (unlockData: KeyUnlockData?) -> Unit, - onUnlockFailure: (wasCancelled: Boolean) -> Unit - ) { - val viewModel: PassphrasePromptViewModel by activityViewModels() - var didAttemptToUnlock = false - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) { - viewModel.authorizationState.collect { value -> - if (value is PassphraseAuthResult.Success) { - val keyUnlockData = SoftwareKeyUnlockData(value.userPassphrase) - didAttemptToUnlock = true - onKeyUnlocked(keyUnlockData) - viewModel.reset() - } - } - } - } - val destination = AuthConfirmationFragmentDirections.openPassphrasePrompt( - showIncorrectPassword = didAttemptToUnlock - ) - val runnable = { findNavController().navigate(destination) } - // The system needs a little time to get back to this screen - Handler(Looper.getMainLooper()).postDelayed(runnable, 500) - } - - @Composable - override fun SecureAreaAuthUi( - onUiStateUpdated: (newState: SecureAreaSupportState) -> Unit - ) { - var compositionState by remember { mutableStateOf(screenState) } - LaunchedEffect(key1 = compositionState) { - onUiStateUpdated(compositionState) - } - SoftwareSetupContainer( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - passphrase = compositionState.passphrase, - onPassphraseChanged = { - compositionState = compositionState.copy(passphrase = it) - } - ) - MdocAuthentication( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = compositionState.mDocAuthOption, - onMdocAuthOptionChange = { - val newValue = compositionState.mDocAuthOption.copy(mDocAuthentication = it) - compositionState = compositionState.copy( - mDocAuthOption = newValue, - softwareAuthKeyCurveState = compositionState.softwareAuthKeyCurveState.copy( - authCurve = SoftwareAuthKeyCurveOption.P256 - ) - ) - } - ) - AuthenticationKeyCurveSoftware( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - state = compositionState.softwareAuthKeyCurveState, - mDocAuthState = compositionState.mDocAuthOption, - onSoftwareAuthKeyCurveChanged = { - val newValue = compositionState.authKeyCurve.copy(authCurve = it) - compositionState = compositionState.copy(softwareAuthKeyCurveState = newValue) - } - ) - } - - override fun getSecureAreaSupportState(): SecureAreaSupportState { - return screenState - } - - override fun createAuthKeySettingsConfiguration(secureAreaSupportState: SecureAreaSupportState): ByteArray { - val state = secureAreaSupportState as SoftwareKeystoreSecureAreaSupportState - return FormatUtil.cborEncode( - CborBuilder() - .addMap() - .put("curve", state.softwareAuthKeyCurveState.authCurve.toEcCurve().coseCurveIdentifier.toLong()) - .put("purposes", KeyPurpose.encodeSet( - setOf(state.mDocAuthOption.mDocAuthentication.toKeyPurpose())).toLong()) - .put("passphraseRequired", state.passphrase.isNotEmpty()) - .put("passphrase", state.passphrase) - .end() - .build().get(0)) - } - - override fun createAuthKeySettingsFromConfiguration( - encodedConfiguration: ByteArray, - challenge: ByteArray, - validFrom: Instant, - validUntil: Instant - ): CreateKeySettings { - val map = Cbor.decode(encodedConfiguration) - val curve = EcCurve.fromInt(map["curve"].asNumber.toInt()) - val purposes = KeyPurpose.decodeSet(map["purposes"].asNumber) - val passphraseRequired = map["passphraseRequired"].asBoolean - val passphrase = map["passphrase"].asTstr - return SoftwareCreateKeySettings.Builder() - .setEcCurve(curve) - .setKeyPurposes(purposes) - .setValidityPeriod(validFrom, validUntil) - .setPassphraseRequired(passphraseRequired, passphrase, null) - .build() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt b/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt deleted file mode 100644 index 43b8487a4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/SoftwareKeystoreSecureAreaSupportState.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.android.identity.wallet.support - -import com.android.identity.wallet.support.softwarekeystore.SoftwareAuthKeyCurveState -import com.android.identity.wallet.composables.state.MdocAuthOption -import kotlinx.parcelize.Parcelize - -@Parcelize -data class SoftwareKeystoreSecureAreaSupportState( - override val mDocAuthOption: MdocAuthOption = MdocAuthOption(), - val softwareAuthKeyCurveState: SoftwareAuthKeyCurveState = SoftwareAuthKeyCurveState(), - val passphrase: String = "", - val authKeyCurve: SoftwareAuthKeyCurveState = SoftwareAuthKeyCurveState(), -) : SecureAreaSupportState { - -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt b/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt deleted file mode 100644 index 7f56aefa5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveOption.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.support.androidkeystore - -import android.os.Parcelable -import com.android.identity.crypto.EcCurve -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class AndroidAuthKeyCurveOption : Parcelable { - P_256, Ed25519, X25519; - - fun toEcCurve(): EcCurve = - when (this) { - P_256 -> EcCurve.P256 - Ed25519 -> EcCurve.ED25519 - X25519 -> EcCurve.X25519 - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt b/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt deleted file mode 100644 index 80d8cbde8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/androidkeystore/AndroidAuthKeyCurveState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.support.androidkeystore - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AndroidAuthKeyCurveState( - val isEnabled: Boolean = true, - val authCurve: AndroidAuthKeyCurveOption = AndroidAuthKeyCurveOption.P_256 -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt b/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt deleted file mode 100644 index aea270074..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveOption.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.android.identity.wallet.support.softwarekeystore - -import android.os.Parcelable -import com.android.identity.crypto.EcCurve -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class SoftwareAuthKeyCurveOption : Parcelable { - P256, - P384, - P521, - BrainPoolP256R1, - BrainPoolP320R1, - BrainPoolP384R1, - BrainPoolP512R1, - Ed25519, - Ed448, - X25519, - X448; - - fun toEcCurve(): EcCurve = - when (this) { - P256 -> EcCurve.P256 - P384 -> EcCurve.P384 - P521 -> EcCurve.P521 - BrainPoolP256R1 -> EcCurve.BRAINPOOLP256R1 - BrainPoolP320R1 -> EcCurve.BRAINPOOLP320R1 - BrainPoolP384R1 -> EcCurve.BRAINPOOLP384R1 - BrainPoolP512R1 -> EcCurve.BRAINPOOLP512R1 - Ed25519 -> EcCurve.ED25519 - Ed448 -> EcCurve.ED448 - X25519 -> EcCurve.X25519 - X448 -> EcCurve.X448 - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt b/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt deleted file mode 100644 index f7004ee44..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/support/softwarekeystore/SoftwareAuthKeyCurveState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.android.identity.wallet.support.softwarekeystore - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class SoftwareAuthKeyCurveState( - val isEnabled: Boolean = true, - val authCurve: SoftwareAuthKeyCurveOption = SoftwareAuthKeyCurveOption.P256 -) : Parcelable \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/theme/Color.kt b/appholder/src/main/java/com/android/identity/wallet/theme/Color.kt deleted file mode 100644 index 3cd8bccb9..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/theme/Color.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.android.identity.wallet.theme - -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color - -//Light Theme -val ThemeLightPrimary = Color(0xFF476810) -val ThemeLightOnPrimary = Color(0xFFFFFFFF) -val ThemeLightPrimaryContainer = Color(0xFFC7F089) - -//Dark Theme -val ThemeDarkPrimary = Color(0xFFACD370) -val ThemeDarkOnPrimary = Color(0xFF213600) -val ThemeDarkPrimaryContainer = Color(0xFF324F00) - -val GreenLight = Color(0xFF00E676) -val GreenDark = Color(0xFF34A853) -val GreenGradient = Brush.linearGradient(colors = listOf(GreenDark, GreenLight)) - -val BlueLight = Color(0xFF00B0FF) -val BlueDark = Color(0xFF4285F4) -val BlueGradient = Brush.linearGradient(colors = listOf(BlueDark, BlueLight)) - -val YellowLight = Color(0xFFFFEA00) -val YellowDark = Color(0xFFFBBC05) -val YellowGradient = Brush.linearGradient(colors = listOf(YellowDark, YellowLight)) - -val RedLight = Color(0xFFFF5722) -val RedDark = Color(0xFFF44336) -val RedGradient = Brush.linearGradient(colors = listOf(RedDark, RedLight)) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/theme/Shape.kt b/appholder/src/main/java/com/android/identity/wallet/theme/Shape.kt deleted file mode 100644 index 6edaebc19..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/theme/Shape.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.theme - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Shapes -import androidx.compose.ui.unit.dp - -val Shapes = Shapes( - small = RoundedCornerShape(4.dp), - medium = RoundedCornerShape(4.dp), - large = RoundedCornerShape(0.dp) -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/theme/Theme.kt b/appholder/src/main/java/com/android/identity/wallet/theme/Theme.kt deleted file mode 100644 index d5c9b31d8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/theme/Theme.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.android.identity.wallet.theme - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -private val lightColorPalette = lightColorScheme( - primary = ThemeLightPrimary, - onPrimary = ThemeLightOnPrimary, - primaryContainer = ThemeLightPrimaryContainer, - onPrimaryContainer = ThemeLightPrimaryContainer, -) - -private val darkColorPalette = darkColorScheme( - primary = ThemeDarkPrimary, - onPrimary = ThemeDarkOnPrimary, - primaryContainer = ThemeDarkPrimaryContainer, - onPrimaryContainer = ThemeDarkPrimaryContainer, -) - -@Composable -fun HolderAppTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit -) { - - val darkColorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - dynamicDarkColorScheme(LocalContext.current) - } else { - darkColorPalette - } - - val lightColorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - dynamicLightColorScheme(LocalContext.current) - } else { - lightColorPalette - } - - val colors = when { - darkTheme -> darkColorScheme - else -> lightColorScheme - } - - MaterialTheme( - colorScheme = colors, - typography = Typography, - shapes = Shapes, - content = content - ) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/theme/Type.kt b/appholder/src/main/java/com/android/identity/wallet/theme/Type.kt deleted file mode 100644 index d6341d7b3..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/theme/Type.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.android.identity.wallet.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -val Typography = Typography( - bodyLarge = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 36.sp - ), - bodyMedium = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - ), - bodySmall = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - ) -) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt deleted file mode 100644 index f73ff598f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/AddDocumentToResponseResult.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.android.identity.wallet.transfer - -import com.android.identity.mdoc.credential.MdocCredential - -sealed class AddDocumentToResponseResult { - - data class DocumentAdded( - val signingKeyUsageLimitPassed: Boolean - ) : AddDocumentToResponseResult() - - data class DocumentLocked( - val credential: MdocCredential - ) : AddDocumentToResponseResult() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/Communication.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/Communication.kt deleted file mode 100644 index 908e273e2..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/Communication.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.annotation.SuppressLint -import android.content.Context -import com.android.identity.util.Constants -import com.android.identity.mdoc.request.DeviceRequestParser -import com.android.identity.android.mdoc.deviceretrieval.DeviceRetrievalHelper -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.mainExecutor -import java.util.OptionalLong - -class Communication private constructor( - private val context: Context, -) { - - private var request: DeviceRequest? = null - var deviceRetrievalHelper: DeviceRetrievalHelper? = null - - fun setDeviceRequest(deviceRequest: ByteArray) { - this.request = DeviceRequest(deviceRequest) - } - - fun getDeviceRequest(): DeviceRequestParser.DeviceRequest = - request?.let { requestBytes -> - deviceRetrievalHelper?.let { presentation -> - DeviceRequestParser( - requestBytes.value, - presentation.sessionTranscript - ).run { parse() } - } ?: throw IllegalStateException("Presentation not set") - } ?: throw IllegalStateException("Request not received") - - - fun getSessionTranscript(): ByteArray? = deviceRetrievalHelper?.sessionTranscript - - fun sendResponse(deviceResponse: ByteArray, closeAfterSending: Boolean) { - if (closeAfterSending) { - deviceRetrievalHelper?.sendDeviceResponse( - deviceResponse, - Constants.SESSION_DATA_STATUS_SESSION_TERMINATION) - deviceRetrievalHelper?.disconnect() - } else { - deviceRetrievalHelper?.sendDeviceResponse(deviceResponse, null) - } - } - - fun stopPresentation( - sendSessionTerminationMessage: Boolean, - useTransportSpecificSessionTermination: Boolean - ) { - if (sendSessionTerminationMessage) { - if (useTransportSpecificSessionTermination) { - deviceRetrievalHelper?.sendTransportSpecificTermination() - } else { - deviceRetrievalHelper?.sendDeviceResponse( - null, - Constants.SESSION_DATA_STATUS_SESSION_TERMINATION - ) - } - } - disconnect() - } - - fun disconnect() = - try { - request = null - deviceRetrievalHelper?.disconnect() - } catch (e: RuntimeException) { - log("Error ignored closing presentation", e) - } - - companion object { - - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: Communication? = null - - fun getInstance(context: Context): Communication { - return instance ?: synchronized(this) { - instance ?: Communication(context).also { instance = it } - } - } - } - - @JvmInline - value class DeviceRequest(val value: ByteArray) -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/ConnectionSetup.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/ConnectionSetup.kt deleted file mode 100644 index d15b5da13..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/ConnectionSetup.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.content.Context -import com.android.identity.mdoc.connectionmethod.ConnectionMethod -import com.android.identity.mdoc.connectionmethod.ConnectionMethodBle -import com.android.identity.mdoc.connectionmethod.ConnectionMethodNfc -import com.android.identity.mdoc.connectionmethod.ConnectionMethodWifiAware -import com.android.identity.android.mdoc.transport.DataTransportOptions -import com.android.identity.util.UUID -import com.android.identity.wallet.util.PreferencesHelper -import java.util.ArrayList -import java.util.OptionalLong - -class ConnectionSetup( - private val context: Context -) { - - fun getConnectionOptions(): DataTransportOptions { - val builder = DataTransportOptions.Builder() - .setBleUseL2CAP(PreferencesHelper.isBleL2capEnabled()) - .setBleClearCache(PreferencesHelper.isBleClearCacheEnabled()) - return builder.build() - } - - fun getConnectionMethods(): List { - val connectionMethods = ArrayList() - if (PreferencesHelper.isBleDataRetrievalEnabled()) { - connectionMethods.add( - ConnectionMethodBle( - false, - true, - null, - UUID.randomUUID() - ) - ) - } - if (PreferencesHelper.isBleDataRetrievalPeripheralModeEnabled()) { - connectionMethods.add( - ConnectionMethodBle( - true, - false, - UUID.randomUUID(), - null - ) - ) - } - if (PreferencesHelper.isWifiDataRetrievalEnabled()) { - connectionMethods.add( - ConnectionMethodWifiAware( - null, - null, - null, - null - ) - ) - } - if (PreferencesHelper.isNfcDataRetrievalEnabled()) { - // TODO: Add API to ConnectionMethodNfc to get sizes appropriate for the device - connectionMethods.add( - ConnectionMethodNfc( - 0xffff, - 0x10000 - ) - ) - } - return connectionMethods - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/QrCommunicationSetup.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/QrCommunicationSetup.kt deleted file mode 100644 index 82e1a6402..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/QrCommunicationSetup.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.content.Context -import com.android.identity.android.mdoc.deviceretrieval.DeviceRetrievalHelper -import com.android.identity.android.mdoc.engagement.QrEngagementHelper -import com.android.identity.android.mdoc.transport.DataTransport -import com.android.identity.crypto.Crypto -import com.android.identity.crypto.EcPublicKey -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.mainExecutor - -class QrCommunicationSetup( - private val context: Context, - private val onConnecting: () -> Unit, - private val onDeviceRetrievalHelperReady: (deviceRetrievalHelper: DeviceRetrievalHelper) -> Unit, - private val onNewDeviceRequest: (request: ByteArray) -> Unit, - private val onDisconnected: (transportSpecificTermination: Boolean) -> Unit, - private val onCommunicationError: (error: Throwable) -> Unit, -) { - - private val settings = PreferencesHelper.apply { initialize(context) } - private val connectionSetup = ConnectionSetup(context) - private val eDeviceKey = Crypto.createEcPrivateKey(settings.getEphemeralKeyCurveOption()) - - private var deviceRetrievalHelper: DeviceRetrievalHelper? = null - private lateinit var qrEngagement: QrEngagementHelper - - val deviceEngagementUriEncoded: String - get() = qrEngagement.deviceEngagementUriEncoded - - private val qrEngagementListener = object : QrEngagementHelper.Listener { - - override fun onDeviceConnecting() { - log("QR Engagement: Device Connecting") - onConnecting() - } - - override fun onDeviceConnected(transport: DataTransport) { - if (deviceRetrievalHelper != null) { - log("OnDeviceConnected for QR engagement -> ignoring due to active presentation") - return - } - log("OnDeviceConnected via QR: qrEngagement=$qrEngagement") - val builder = DeviceRetrievalHelper.Builder( - context, - deviceRetrievalHelperListener, - context.mainExecutor(), - eDeviceKey - ) - builder.useForwardEngagement( - transport, - qrEngagement.deviceEngagement, - qrEngagement.handover - ) - deviceRetrievalHelper = builder.build() - qrEngagement.close() - onDeviceRetrievalHelperReady(requireNotNull(deviceRetrievalHelper)) - } - - override fun onError(error: Throwable) { - log("QR onError: ${error.message}") - onCommunicationError(error) - } - } - - private val deviceRetrievalHelperListener = object : DeviceRetrievalHelper.Listener { - override fun onEReaderKeyReceived(eReaderKey: EcPublicKey) { - log("DeviceRetrievalHelper Listener (QR): OnEReaderKeyReceived") - } - - override fun onDeviceRequest(deviceRequestBytes: ByteArray) { - log("DeviceRetrievalHelper Listener (QR): OnDeviceRequest") - onNewDeviceRequest(deviceRequestBytes) - } - - override fun onDeviceDisconnected(transportSpecificTermination: Boolean) { - log("DeviceRetrievalHelper Listener (QR): onDeviceDisconnected") - onDisconnected(transportSpecificTermination) - } - - override fun onError(error: Throwable) { - log("DeviceRetrievalHelper Listener (QR): onError -> ${error.message}") - onCommunicationError(error) - } - } - - fun configure() { - qrEngagement = QrEngagementHelper.Builder( - context, - eDeviceKey.publicKey, - connectionSetup.getConnectionOptions(), - qrEngagementListener, - context.mainExecutor() - ).setConnectionMethods(connectionSetup.getConnectionMethods()) - .build() - } - - fun close() { - try { - qrEngagement.close() - } catch (exception: RuntimeException) { - log("Error closing QR engagement", exception) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/transfer/ReverseQrCommunicationSetup.kt b/appholder/src/main/java/com/android/identity/wallet/transfer/ReverseQrCommunicationSetup.kt deleted file mode 100644 index df51050be..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/ReverseQrCommunicationSetup.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.content.Context -import android.net.Uri -import android.util.Base64 -import com.android.identity.android.mdoc.deviceretrieval.DeviceRetrievalHelper -import com.android.identity.android.mdoc.transport.DataTransport -import com.android.identity.crypto.Crypto -import com.android.identity.crypto.EcPublicKey -import com.android.identity.mdoc.engagement.EngagementParser -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.wallet.util.PreferencesHelper -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.mainExecutor - -class ReverseQrCommunicationSetup( - private val context: Context, - private val onPresentationReady: (presentation: DeviceRetrievalHelper) -> Unit, - private val onNewRequest: (request: ByteArray) -> Unit, - private val onDisconnected: () -> Unit, - private val onCommunicationError: (error: Throwable) -> Unit, -) { - - private val settings = PreferencesHelper.apply { initialize(context) } - private val connectionSetup = ConnectionSetup(context) - private val eDeviceKey = Crypto.createEcPrivateKey(settings.getEphemeralKeyCurveOption()) - - private var presentation: DeviceRetrievalHelper? = null - - private val presentationListener = object : DeviceRetrievalHelper.Listener { - override fun onEReaderKeyReceived(eReaderKey: EcPublicKey) { - log("DeviceRetrievalHelper Listener (QR): OnEReaderKeyReceived") - } - - override fun onDeviceRequest(deviceRequestBytes: ByteArray) { - onNewRequest(deviceRequestBytes) - } - - override fun onDeviceDisconnected(transportSpecificTermination: Boolean) { - onDisconnected() - } - - override fun onError(error: Throwable) { - onCommunicationError(error) - } - } - - fun configure( - reverseEngagementUri: String, - origins: List - ) { - val uri = Uri.parse(reverseEngagementUri) - if (!uri.scheme.equals("mdoc")) { - throw IllegalStateException("Only supports mdoc URIs") - } - val encodedReaderEngagement = Base64.decode( - uri.encodedSchemeSpecificPart, - Base64.URL_SAFE or Base64.NO_PADDING - ) - val engagement = EngagementParser( - encodedReaderEngagement - ).parse() - if (engagement.connectionMethods.size == 0) { - throw IllegalStateException("No connection methods in engagement") - } - - // For now, just pick the first transport - val connectionMethod = engagement.connectionMethods[0] - log("Using connection method $connectionMethod") - - val transport = DataTransport.fromConnectionMethod( - context, - connectionMethod, - DataTransport.Role.MDOC, - connectionSetup.getConnectionOptions() - ) - - val builder = DeviceRetrievalHelper.Builder( - context, - presentationListener, - context.mainExecutor(), - eDeviceKey - ) - builder.useReverseEngagement(transport, encodedReaderEngagement, origins) - presentation = builder.build() - onPresentationReady(requireNotNull(presentation)) - } -} \ No newline at end of file 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 deleted file mode 100644 index 01172abd7..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/transfer/TransferManager.kt +++ /dev/null @@ -1,288 +0,0 @@ -package com.android.identity.wallet.transfer - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Color.BLACK -import android.graphics.Color.WHITE -import android.view.View -import android.widget.ImageView -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.android.identity.document.DocumentRequest -import com.android.identity.document.NameSpacedData -import com.android.identity.mdoc.mso.StaticAuthDataParser -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.mdoc.request.DeviceRequestParser -import com.android.identity.mdoc.response.DeviceResponseGenerator -import com.android.identity.mdoc.response.DocumentGenerator -import com.android.identity.mdoc.util.MdocUtil -import com.android.identity.crypto.Algorithm -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.securearea.KeyLockedException -import com.android.identity.securearea.KeyPurpose -import com.android.identity.securearea.KeyUnlockData -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.documentdata.DocumentDataReader -import com.android.identity.wallet.documentdata.DocumentElements -import com.android.identity.wallet.util.ProvisioningUtil -import com.android.identity.wallet.util.TransferStatus -import com.android.identity.wallet.util.log -import com.android.identity.wallet.util.logWarning -import com.android.identity.wallet.util.requireValidProperty -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 kotlinx.datetime.Clock -import kotlin.coroutines.resume - -class TransferManager private constructor(private val context: Context) { - - companion object { - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: TransferManager? = null - - fun getInstance(context: Context) = - instance ?: synchronized(this) { - instance ?: TransferManager(context).also { instance = it } - } - } - - private var reversedQrCommunicationSetup: ReverseQrCommunicationSetup? = null - private var qrCommunicationSetup: QrCommunicationSetup? = null - private var hasStarted = false - - private lateinit var communication: Communication - - private var transferStatusLd = MutableLiveData() - - fun setCommunication(communication: Communication) { - this.communication = communication - } - - fun getTransferStatus(): LiveData = transferStatusLd - - fun updateStatus(status: TransferStatus) { - transferStatusLd.value = status - } - - fun documentRequests(): Collection { - return communication.getDeviceRequest().docRequests - } - - fun startPresentationReverseEngagement( - reverseEngagementUri: String, - origins: List - ) { - if (hasStarted) { - throw IllegalStateException("Transfer has already started.") - } - communication = Communication.getInstance(context) - reversedQrCommunicationSetup = ReverseQrCommunicationSetup( - context = context, - onPresentationReady = { deviceRetrievalHelper -> - communication.deviceRetrievalHelper = deviceRetrievalHelper - }, - onNewRequest = { request -> - communication.setDeviceRequest(request) - transferStatusLd.value = TransferStatus.REQUEST - }, - onDisconnected = { transferStatusLd.value = TransferStatus.DISCONNECTED }, - onCommunicationError = { error -> - log("onError: ${error.message}") - transferStatusLd.value = TransferStatus.ERROR - } - ).apply { - configure(reverseEngagementUri, origins) - } - hasStarted = true - } - - fun startQrEngagement() { - if (hasStarted) { - throw IllegalStateException("Transfer has already started.") - } - communication = Communication.getInstance(context) - qrCommunicationSetup = QrCommunicationSetup( - context = context, - onConnecting = { transferStatusLd.value = TransferStatus.CONNECTING }, - onDeviceRetrievalHelperReady = { deviceRetrievalHelper -> - communication.deviceRetrievalHelper = deviceRetrievalHelper - transferStatusLd.value = TransferStatus.CONNECTED - }, - onNewDeviceRequest = { deviceRequest -> - communication.setDeviceRequest(deviceRequest) - transferStatusLd.value = TransferStatus.REQUEST - }, - onDisconnected = { transferStatusLd.value = TransferStatus.DISCONNECTED } - ) { error -> - log("onError: ${error.message}") - transferStatusLd.value = TransferStatus.ERROR - }.apply { - configure() - } - hasStarted = true - } - - fun getDeviceEngagementQrCode(): View { - val deviceEngagementForQrCode = qrCommunicationSetup!!.deviceEngagementUriEncoded - val qrCodeBitmap = encodeQRCodeAsBitmap(deviceEngagementForQrCode) - val qrCodeView = ImageView(context) - qrCodeView.setImageBitmap(qrCodeBitmap) - - return qrCodeView - } - - private fun encodeQRCodeAsBitmap(str: String): Bitmap { - val width = 800 - val result: BitMatrix = try { - MultiFormatWriter().encode( - str, - BarcodeFormat.QR_CODE, width, width, null - ) - } catch (e: WriterException) { - throw java.lang.IllegalArgumentException(e) - } - val w = result.width - val h = result.height - val pixels = IntArray(w * h) - for (y in 0 until h) { - val offset = y * w - for (x in 0 until w) { - pixels[offset + x] = if (result[x, y]) BLACK else WHITE - } - } - val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) - bitmap.setPixels(pixels, 0, width, 0, 0, w, h) - return bitmap - } - - @Throws(IllegalStateException::class) - suspend fun addDocumentToResponse( - documentName: String, - docType: String, - issuerSignedEntriesToRequest: MutableMap>, - deviceResponseGenerator: DeviceResponseGenerator, - credential: MdocCredential?, - authKeyUnlockData: KeyUnlockData?, - ) = suspendCancellableCoroutine { continuation -> - var result: AddDocumentToResponseResult - var signingKeyUsageLimitPassed = false - val documentManager = DocumentManager.getInstance(context) - val documentInformation = documentManager.getDocumentInformation(documentName) - requireValidProperty(documentInformation) { "Document not found!" } - - val document = requireNotNull(documentManager.getDocumentByName(documentName)) - val dataElements = issuerSignedEntriesToRequest.keys.flatMap { key -> - issuerSignedEntriesToRequest.getOrDefault(key, emptyList()).map { value -> - DocumentRequest.DataElement(key, value, false) - } - } - - val request = DocumentRequest(dataElements) - - val credentialToUse: MdocCredential - if (credential != null) { - credentialToUse = credential - } else { - credentialToUse = document.findCredential( - ProvisioningUtil.CREDENTIAL_DOMAIN, - Clock.System.now() - ) as MdocCredential? - ?: throw IllegalStateException("No credential available") - } - - if (credentialToUse.usageCount >= documentInformation.maxUsagesPerKey) { - logWarning("Using Credential previously used ${credentialToUse.usageCount} times, and maxUsagesPerKey is ${documentInformation.maxUsagesPerKey}") - signingKeyUsageLimitPassed = true - } - - val staticAuthData = StaticAuthDataParser(credentialToUse.issuerProvidedData).parse() - val mergedIssuerNamespaces = MdocUtil.mergeIssuerNamesSpaces( - request, - document.applicationData.getNameSpacedData("documentData"), - staticAuthData - ) - - val transcript = communication.getSessionTranscript() ?: byteArrayOf() - - try { - val generator = DocumentGenerator(docType, staticAuthData.issuerAuth, transcript) - .setIssuerNamespaces(mergedIssuerNamespaces) - val keyInfo = credentialToUse.secureArea.getKeyInfo(credentialToUse.alias) - if (keyInfo.keyPurposes.contains(KeyPurpose.SIGN)) { - generator.setDeviceNamespacesSignature( - NameSpacedData.Builder().build(), - credentialToUse.secureArea, - credentialToUse.alias, - authKeyUnlockData, - Algorithm.ES256 - ) - } else { - generator.setDeviceNamespacesMac( - NameSpacedData.Builder().build(), - credentialToUse.secureArea, - credentialToUse.alias, - authKeyUnlockData, - communication.deviceRetrievalHelper!!.eReaderKey - ) - } - val data = generator.generate() - deviceResponseGenerator.addDocument(data) - log("Increasing usage count on ${credentialToUse.alias}") - credentialToUse.increaseUsageCount() - ProvisioningUtil.getInstance(context).trackUsageTimestamp(document) - result = AddDocumentToResponseResult.DocumentAdded(signingKeyUsageLimitPassed) - } catch (lockedException: KeyLockedException) { - result = AddDocumentToResponseResult.DocumentLocked(credentialToUse) - } - continuation.resume(result) - } - - fun stopPresentation( - sendSessionTerminationMessage: Boolean, - useTransportSpecificSessionTermination: Boolean - ) { - communication.stopPresentation( - sendSessionTerminationMessage, - useTransportSpecificSessionTermination - ) - disconnect() - } - - fun disconnect() { - communication.disconnect() - qrCommunicationSetup?.close() - transferStatusLd = MutableLiveData() - destroy() - } - - fun destroy() { - qrCommunicationSetup = null - reversedQrCommunicationSetup = null - hasStarted = false - } - - fun sendResponse(deviceResponse: ByteArray, closeAfterSending: Boolean) { - communication.sendResponse(deviceResponse, closeAfterSending) - if (closeAfterSending) { - disconnect() - } - } - - fun readDocumentEntries(documentName: String): DocumentElements { - val documentManager = DocumentManager.getInstance(context) - val documentInformation = documentManager.getDocumentInformation(documentName) - - val document = requireNotNull(documentManager.getDocumentByName(documentName)) - val nameSpacedData = document.applicationData.getNameSpacedData("documentData") - return DocumentDataReader(documentInformation?.docType ?: "").read(nameSpacedData) - } - - fun setResponseServed() { - transferStatusLd.value = TransferStatus.REQUEST_SERVED - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CountryValidator.kt b/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CountryValidator.kt deleted file mode 100644 index a1a1f829a..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CountryValidator.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.android.identity.wallet.trustmanagement - -import java.security.cert.Certificate -import java.security.cert.CertificateException -import java.security.cert.PKIXCertPathChecker -import java.security.cert.X509Certificate - -/** - * Class used to validate that the country code in the whole certificate chain is the same - */ -class CountryValidator : PKIXCertPathChecker() { - private var previousCountryCode: String = "" - - /** - * There is no custom initialisation of this class - */ - override fun init(p0: Boolean) { - // intentionally left empty - } - - - /** - * Forward checking supported: the order of the certificate chain is not relevant for the check - * on country code. - */ - override fun isForwardCheckingSupported(): Boolean { - return true - } - - /** - * Check the country code - */ - override fun check(certificate: Certificate?, state: MutableCollection?) { - if (certificate is X509Certificate) { - val countryCode = certificate.subjectX500Principal.countryCode("") - if (countryCode.isBlank()) { - throw CertificateException("Country code is not present in certificate " + certificate.subjectX500Principal.name) - } - if (previousCountryCode.isNotBlank() && previousCountryCode.uppercase() != countryCode.uppercase()) { - throw CertificateException("There are different country codes in the certificate chain: $previousCountryCode and $countryCode") - } else { - previousCountryCode = countryCode - } - } - } - - /** - * Extensions are not validated on country code - */ - override fun getSupportedExtensions(): MutableSet { - return mutableSetOf() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CustomValidators.kt b/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CustomValidators.kt deleted file mode 100644 index aa5275971..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/CustomValidators.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.android.identity.wallet.trustmanagement - -import java.security.cert.PKIXCertPathChecker - -/** - * Object used to obtain custom validators based on docType - */ -object CustomValidators { - fun getByDocType(docType: String): List - { - when (docType){ - "org.iso.18013.5.1.mDL" -> return listOf(CountryValidator()) - else -> return emptyList() - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X500PrincipalExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X500PrincipalExtensions.kt deleted file mode 100644 index 5b2f73741..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X500PrincipalExtensions.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.identity.wallet.trustmanagement - -import org.bouncycastle.asn1.ASN1ObjectIdentifier -import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.style.BCStyle -import javax.security.auth.x500.X500Principal - -/** - * Extract the common name of a X500Principal (subject or issuer) - */ -fun X500Principal.getCommonName(defaultValue: String): String { - return readRdn(this.name, BCStyle.CN, defaultValue) -} - -/** - * Extract the organisation of a X500Principal (subject or issuer) - */ -fun X500Principal.getOrganisation(defaultValue: String): String { - return readRdn(this.name, BCStyle.O, defaultValue) -} - -/** - * Extract the organisational unit of a X500Principal (subject or issuer) - */ -fun X500Principal.organisationalUnit(defaultValue: String): String { - return readRdn(this.name, BCStyle.OU, defaultValue) -} - -/** - * Extract the country code of a X500Principal (subject or issuer) - */ -fun X500Principal.countryCode(defaultValue: String): String { - return readRdn(this.name, BCStyle.C, defaultValue) -} - -/** - * Read a relative distinguished name from a distinguished name - */ -private fun readRdn(name: String, field: ASN1ObjectIdentifier, defaultValue: String): String { - val x500name = X500Name(name) - for (rdn in x500name.getRDNs(field)) { - val attributes = rdn.typesAndValues - for (attribute in attributes) { - return attribute.value.toString() - } - } - return defaultValue -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X509CertificateExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X509CertificateExtensions.kt deleted file mode 100644 index fe32decec..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/trustmanagement/X509CertificateExtensions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.android.identity.wallet.trustmanagement - -import com.android.identity.util.toHex -import org.bouncycastle.asn1.DEROctetString -import org.bouncycastle.asn1.x509.Extension -import org.bouncycastle.asn1.x509.SubjectKeyIdentifier -import java.security.cert.X509Certificate - -/** - * Get the Subject Key Identifier Extension from the - * X509 certificate in hexadecimal format. - */ -fun X509Certificate.getSubjectKeyIdentifier(): String { - val extensionValue = this.getExtensionValue(Extension.subjectKeyIdentifier.id) - val octets = DEROctetString.getInstance(extensionValue).octets - val subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets) - return subjectKeyIdentifier.keyIdentifier.toHex() -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/BindingAdapters.kt b/appholder/src/main/java/com/android/identity/wallet/util/BindingAdapters.kt deleted file mode 100644 index c61aba37b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/BindingAdapters.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.android.identity.wallet.util - -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import androidx.databinding.BindingAdapter - - -object BindingAdapters { - /** - * A Binding Adapter that is called whenever the value of the attribute `app:engagementView` - * changes. Receives a view with the QR Code for the device engagement. - */ - @BindingAdapter("app:engagementView") - @JvmStatic - fun engagementView(view: LinearLayout, viewEngagement: View?) { - viewEngagement?.let { - (viewEngagement.parent as? ViewGroup)?.removeView(viewEngagement) - view.addView(it) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/ContextExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/util/ContextExtensions.kt deleted file mode 100644 index 76ee921ec..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/ContextExtensions.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import android.os.Build -import androidx.core.content.ContextCompat -import java.util.concurrent.Executor - -fun Context.mainExecutor(): Executor { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - mainExecutor - } else { - ContextCompat.getMainExecutor(applicationContext) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/DocumentData.kt b/appholder/src/main/java/com/android/identity/wallet/util/DocumentData.kt deleted file mode 100644 index 4bb0972c4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/DocumentData.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.util - -object DocumentData { - const val MDL_DOCTYPE = "org.iso.18013.5.1.mDL" - const val MDL_NAMESPACE = "org.iso.18013.5.1" - const val MVR_DOCTYPE = "nl.rdw.mekb.1" - const val MVR_NAMESPACE = "nl.rdw.mekb.1" - const val MICOV_DOCTYPE = "org.micov.1" - const val MICOV_VTR_NAMESPACE = "org.micov.vtr.1" - const val MICOV_ATT_NAMESPACE = "org.micov.attestation.1" - const val AAMVA_NAMESPACE = "org.iso.18013.5.1.aamva" - const val EU_PID_DOCTYPE = "eu.europa.ec.eudi.pid.1" - const val EU_PID_NAMESPACE = "eu.europa.ec.eudi.pid.1" - - enum class ErikaStaticData(val identifier: String, val value: String) { - VISIBLE_NAME("visible_name", "Driving License"), - } - - enum class MekbStaticData(val identifier: String, val value: String) { - VISIBLE_NAME("visible_name", "Vehicle Registration"), - } - - enum class MicovStaticData(val identifier: String, val value: String) { - VISIBLE_NAME("visible_name", "Vaccination Document"), - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/FormatUtil.kt b/appholder/src/main/java/com/android/identity/wallet/util/FormatUtil.kt deleted file mode 100644 index 179d0a179..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/FormatUtil.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.android.identity.wallet.util - -import android.icu.text.SimpleDateFormat -import android.icu.util.TimeZone -import co.nstant.`in`.cbor.CborBuilder -import co.nstant.`in`.cbor.CborEncoder -import co.nstant.`in`.cbor.CborException -import co.nstant.`in`.cbor.model.DataItem -import java.io.ByteArrayOutputStream -import java.security.PublicKey -import java.security.interfaces.ECPublicKey -import java.security.spec.ECPoint -import kotlin.math.min - - -object FormatUtil { - // Helper function to convert a byteArray to HEX string - fun encodeToString(bytes: ByteArray): String { - val sb = StringBuilder(bytes.size * 2) - for (b in bytes) { - sb.append(String.format("%02x", b)) - } - - return sb.toString() - } - - private const val CHUNK_SIZE = 2048 - - private fun debugPrint(message: String) { - var index = 0 - while (index < message.length) { - log(message.substring(index, min(message.length, index + CHUNK_SIZE))) - index += CHUNK_SIZE - } - } - - fun debugPrintEncodeToString(bytes: ByteArray) { - debugPrint(encodeToString(bytes)) - } - - private const val COSE_KEY_KTY = 1 - private const val COSE_KEY_TYPE_EC2 = 2 - private const val COSE_KEY_EC2_CRV = -1 - private const val COSE_KEY_EC2_X = -2 - private const val COSE_KEY_EC2_Y = -3 - private const val COSE_KEY_EC2_CRV_P256 = 1 - - fun cborBuildCoseKey(key: PublicKey): DataItem { - val ecKey: ECPublicKey = key as ECPublicKey - val w: ECPoint = ecKey.w - // X and Y are always positive so for interop we remove any leading zeroes - // inserted by the BigInteger encoder. - val x = stripLeadingZeroes(w.affineX.toByteArray()) - val y = stripLeadingZeroes(w.affineY.toByteArray()) - return CborBuilder() - .addMap() - .put(COSE_KEY_KTY.toLong(), COSE_KEY_TYPE_EC2.toLong()) - .put(COSE_KEY_EC2_CRV.toLong(), COSE_KEY_EC2_CRV_P256.toLong()) - .put(COSE_KEY_EC2_X.toLong(), x) - .put(COSE_KEY_EC2_Y.toLong(), y) - .end() - .build()[0] - } - - fun cborEncode(dataItem: DataItem): ByteArray { - val baos = ByteArrayOutputStream() - try { - CborEncoder(baos).encode(dataItem) - } catch (e: CborException) { - // This should never happen and we don't want cborEncode() to throw since that - // would complicate all callers. Log it instead. - throw IllegalStateException("Unexpected failure encoding data", e) - } - return baos.toByteArray() - } - - private fun stripLeadingZeroes(value: ByteArray): ByteArray { - var n = 0 - while (n < value.size && value[n] == 0x00.toByte()) { - n++ - } - return value.copyOfRange(n, value.size) - } - - fun fullDateStringToMilliseconds(date: String): Long { - val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd") - simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC") - return simpleDateFormat.parse(date).toInstant().toEpochMilli() - } - - fun millisecondsToFullDateString(milliseconds: Long): String { - val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd") - return simpleDateFormat.format(milliseconds) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/LogginExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/util/LogginExtensions.kt deleted file mode 100644 index 67e19216b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/LogginExtensions.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.android.identity.wallet.util - -import android.util.Log - -fun Any.log(message: String, exception: Throwable? = null) { - if (!PreferencesHelper.isDebugLoggingEnabled()) return - val tag: String = tagValue() - if (exception == null) { - Log.d(tag, message) - } else { - Log.e(tag, message, exception) - } -} - -fun Any.logInfo(message: String) { - if (!PreferencesHelper.isDebugLoggingEnabled()) return - val tag: String = tagValue() - Log.i(tag, message) -} - -fun Any.logWarning(message: String) { - if (!PreferencesHelper.isDebugLoggingEnabled()) return - val tag: String = tagValue() - Log.w(tag, message) -} - -fun Any.logError(message: String) { - if (!PreferencesHelper.isDebugLoggingEnabled()) return - val tag: String = tagValue() - Log.e(tag, message) -} - -private fun Any.tagValue(): String { - if (this is String) return this - val fullClassName: String = this::class.qualifiedName ?: this::class.java.typeName - val outerClassName = fullClassName.substringBefore('$') - val simplerOuterClassName = outerClassName.substringAfterLast('.') - return if (simplerOuterClassName.isEmpty()) { - fullClassName - } else { - simplerOuterClassName.removeSuffix("Kt") - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/util/NfcDataTransferHandler.kt b/appholder/src/main/java/com/android/identity/wallet/util/NfcDataTransferHandler.kt deleted file mode 100644 index ec7821753..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/NfcDataTransferHandler.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -package com.android.identity.wallet.util - -import android.nfc.cardemulation.HostApduService -import android.os.Bundle -import com.android.identity.android.mdoc.transport.DataTransportNfc -import com.android.identity.wallet.transfer.TransferManager - -class NfcDataTransferHandler : HostApduService() { - - private lateinit var transferManager: TransferManager - - override fun onCreate() { - super.onCreate() - log("onCreate") - transferManager = TransferManager.getInstance(applicationContext) - } - - override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray? { - log("processCommandApdu: Command-> ${FormatUtil.encodeToString(commandApdu)}") - return DataTransportNfc.processCommandApdu(this, commandApdu) - } - - override fun onDeactivated(reason: Int) { - log("onDeactivated: reason-> $reason") - DataTransportNfc.onDeactivated(reason) - } -} diff --git a/appholder/src/main/java/com/android/identity/wallet/util/NfcEngagementHandler.kt b/appholder/src/main/java/com/android/identity/wallet/util/NfcEngagementHandler.kt deleted file mode 100644 index d52813e74..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/NfcEngagementHandler.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ - -package com.android.identity.wallet.util - -import android.content.Intent -import android.nfc.cardemulation.HostApduService -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import androidx.navigation.NavDeepLinkBuilder -import com.android.identity.android.mdoc.deviceretrieval.DeviceRetrievalHelper -import com.android.identity.android.mdoc.engagement.NfcEngagementHelper -import com.android.identity.android.mdoc.transport.DataTransport -import com.android.identity.crypto.Crypto -import com.android.identity.crypto.EcPublicKey -import com.android.identity.wallet.R -import com.android.identity.wallet.transfer.Communication -import com.android.identity.wallet.transfer.ConnectionSetup -import com.android.identity.wallet.transfer.TransferManager - -class NfcEngagementHandler : HostApduService() { - - private lateinit var engagementHelper: NfcEngagementHelper - private lateinit var communication: Communication - private lateinit var transferManager: TransferManager - - private var deviceRetrievalHelper: DeviceRetrievalHelper? = null - - private val settings by lazy { - PreferencesHelper.apply { initialize(applicationContext) } - } - private val eDeviceKey by lazy { - Crypto.createEcPrivateKey(settings.getEphemeralKeyCurveOption()) - } - private val nfcEngagementListener = object : NfcEngagementHelper.Listener { - - override fun onTwoWayEngagementDetected() { - log("Engagement Listener: Two Way Engagement Detected.") - } - - override fun onHandoverSelectMessageSent() { - log("Engagement Listener: Handover Select Message Sent.") - } - - override fun onDeviceConnecting() { - log("Engagement Listener: Device Connecting. Launching Transfer Screen") - val pendingIntent = NavDeepLinkBuilder(applicationContext) - .setGraph(R.navigation.navigation_graph) - .setDestination(R.id.transferDocumentFragment) - .setComponentName(com.android.identity.wallet.MainActivity::class.java) - .createPendingIntent() - pendingIntent.send(applicationContext, 0, null) - transferManager.updateStatus(TransferStatus.CONNECTING) - } - - override fun onDeviceConnected(transport: DataTransport) { - if (deviceRetrievalHelper != null) { - log("Engagement Listener: Device Connected -> ignored due to active presentation") - return - } - - log("Engagement Listener: Device Connected via NFC") - - val builder = DeviceRetrievalHelper.Builder( - applicationContext, - presentationListener, - applicationContext.mainExecutor(), - eDeviceKey - ) - builder.useForwardEngagement( - transport, - engagementHelper.deviceEngagement, - engagementHelper.handover - ) - deviceRetrievalHelper = builder.build() - communication.deviceRetrievalHelper = deviceRetrievalHelper - engagementHelper.close() - transferManager.updateStatus(TransferStatus.CONNECTED) - } - - override fun onError(error: Throwable) { - log("Engagement Listener: onError -> ${error.message}") - transferManager.updateStatus(TransferStatus.ERROR) - engagementHelper.close() - } - } - - private val presentationListener = object : DeviceRetrievalHelper.Listener { - - override fun onEReaderKeyReceived(eReaderKey: EcPublicKey) { - log("DeviceRetrievalHelper Listener (NFC): OnEReaderKeyReceived") - } - - override fun onDeviceRequest(deviceRequestBytes: ByteArray) { - log("Presentation Listener: OnDeviceRequest") - communication.setDeviceRequest(deviceRequestBytes) - transferManager.updateStatus(TransferStatus.REQUEST) - } - - override fun onDeviceDisconnected(transportSpecificTermination: Boolean) { - log("Presentation Listener: onDeviceDisconnected") - transferManager.updateStatus(TransferStatus.DISCONNECTED) - } - - override fun onError(error: Throwable) { - log("Presentation Listener: onError -> ${error.message}") - transferManager.updateStatus(TransferStatus.ERROR) - } - } - - override fun onCreate() { - super.onCreate() - log("onCreate") - communication = Communication.getInstance(applicationContext) - transferManager = TransferManager.getInstance(applicationContext) - transferManager.setCommunication(communication) - val connectionSetup = ConnectionSetup(applicationContext) - val builder = NfcEngagementHelper.Builder( - applicationContext, - eDeviceKey.publicKey, - connectionSetup.getConnectionOptions(), - nfcEngagementListener, - applicationContext.mainExecutor() - ) - if (PreferencesHelper.shouldUseStaticHandover()) { - builder.useStaticHandover(connectionSetup.getConnectionMethods()) - } else { - builder.useNegotiatedHandover() - } - engagementHelper = builder.build() - - val launchAppIntent = Intent(applicationContext, com.android.identity.wallet.MainActivity::class.java) - launchAppIntent.action = Intent.ACTION_VIEW - launchAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - launchAppIntent.addCategory(Intent.CATEGORY_DEFAULT) - launchAppIntent.addCategory(Intent.CATEGORY_BROWSABLE) - applicationContext.startActivity(launchAppIntent) - } - - override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray? { - log("processCommandApdu: Command-> ${FormatUtil.encodeToString(commandApdu)}") - return engagementHelper.nfcProcessCommandApdu(commandApdu) - } - - override fun onDeactivated(reason: Int) { - log("onDeactivated: reason-> $reason") - engagementHelper.nfcOnDeactivated(reason) - - // We need to close the NfcEngagementHelper but if we're doing it as the reader moves - // out of the field, it's too soon as it may take a couple of seconds to establish - // the connection, triggering onDeviceConnected() callback above. - // - // In fact, the reader _could_ actually take a while to establish the connection... - // for example the UI in the mdoc doc reader might have the operator pick the - // transport if more than one is offered. In fact this is exactly what we do in - // our mdoc reader. - // - // So we give the reader 15 seconds to do this... - // - val timeoutSeconds = 15 - Handler(Looper.getMainLooper()).postDelayed({ - if (deviceRetrievalHelper == null) { - logWarning("reader didn't connect inside $timeoutSeconds seconds, closing") - engagementHelper.close() - } - }, timeoutSeconds * 1000L) - } -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/util/PeriodicKeysRefreshWorkRequest.kt b/appholder/src/main/java/com/android/identity/wallet/util/PeriodicKeysRefreshWorkRequest.kt deleted file mode 100644 index 69e2c04b8..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/PeriodicKeysRefreshWorkRequest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import java.util.concurrent.TimeUnit - -class PeriodicKeysRefreshWorkRequest(context: Context) { - - private val workManager = WorkManager.getInstance(context) - - fun schedulePeriodicKeysRefreshing() { - val workRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) - .build() - workManager.enqueue(workRequest) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/Preconditions.kt b/appholder/src/main/java/com/android/identity/wallet/util/Preconditions.kt deleted file mode 100644 index 096d4f7d5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/Preconditions.kt +++ /dev/null @@ -1,19 +0,0 @@ -@file:OptIn(ExperimentalContracts::class) - -package com.android.identity.wallet.util - -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract - -inline fun requireValidProperty(value: T?, lazyMessage: () -> Any): T { - contract { - returns() implies (value != null) - } - - if (value == null) { - val message = lazyMessage() - throw IllegalStateException(message.toString()) - } else { - return value - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/PreferencesHelper.kt b/appholder/src/main/java/com/android/identity/wallet/util/PreferencesHelper.kt deleted file mode 100644 index 90a36b64f..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/PreferencesHelper.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import androidx.preference.PreferenceManager -import com.android.identity.crypto.EcCurve -import com.android.identity.util.Logger -import java.io.File - -object PreferencesHelper { - private const val BLE_DATA_RETRIEVAL = "ble_transport" - private const val BLE_DATA_RETRIEVAL_PERIPHERAL_MODE = "ble_transport_peripheral_mode" - private const val BLE_DATA_L2CAP = "ble_l2cap" - private const val BLE_CLEAR_CACHE = "ble_clear_cache" - private const val WIFI_DATA_RETRIEVAL = "wifi_transport" - private const val NFC_DATA_RETRIEVAL = "nfc_transport" - private const val DEBUG_LOG = "debug_log" - private const val CONNECTION_AUTO_CLOSE = "connection_auto_close" - private const val STATIC_HANDOVER = "static_handover" - private const val EPHEMERAL_KEY_CURVE_OPTION = "ephemeral_key_curve" - - private lateinit var sharedPreferences: SharedPreferences - - fun initialize(context: Context) { - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - } - - fun getKeystoreBackedStorageLocation(context: Context): File { - // As per the docs, the document data contains reference to Keystore aliases so ensure - // this is stored in a location where it's not automatically backed up and restored by - // Android Backup as per https://developer.android.com/guide/topics/data/autobackup - return File(context.noBackupFilesDir, "identity.bin") - } - - fun isBleDataRetrievalEnabled(): Boolean = - sharedPreferences.getBoolean(BLE_DATA_RETRIEVAL, true) - - fun setBleDataRetrievalEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(BLE_DATA_RETRIEVAL, enabled) } - - fun isBleDataRetrievalPeripheralModeEnabled(): Boolean = - sharedPreferences.getBoolean(BLE_DATA_RETRIEVAL_PERIPHERAL_MODE, false) - - fun setBlePeripheralDataRetrievalMode(enabled: Boolean) = - sharedPreferences.edit { putBoolean(BLE_DATA_RETRIEVAL_PERIPHERAL_MODE, enabled) } - - fun isBleL2capEnabled(): Boolean = - sharedPreferences.getBoolean(BLE_DATA_L2CAP, false) - - fun setBleL2CAPEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(BLE_DATA_L2CAP, enabled) } - - fun isBleClearCacheEnabled(): Boolean = - sharedPreferences.getBoolean(BLE_CLEAR_CACHE, false) - - fun setBleClearCacheEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(BLE_CLEAR_CACHE, enabled) } - - fun isWifiDataRetrievalEnabled(): Boolean = - sharedPreferences.getBoolean(WIFI_DATA_RETRIEVAL, false) - - fun setWifiDataRetrievalEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(WIFI_DATA_RETRIEVAL, enabled) } - - fun isNfcDataRetrievalEnabled(): Boolean = - sharedPreferences.getBoolean(NFC_DATA_RETRIEVAL, false) - - fun setNfcDataRetrievalEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(NFC_DATA_RETRIEVAL, enabled) } - - fun isConnectionAutoCloseEnabled(): Boolean = - sharedPreferences.getBoolean(CONNECTION_AUTO_CLOSE, true) - - fun setConnectionAutoCloseEnabled(enabled: Boolean) = - sharedPreferences.edit { putBoolean(CONNECTION_AUTO_CLOSE, enabled) } - - fun shouldUseStaticHandover(): Boolean = - sharedPreferences.getBoolean(STATIC_HANDOVER, false) - - fun setUseStaticHandover(enabled: Boolean) = - sharedPreferences.edit { putBoolean(STATIC_HANDOVER, enabled) } - - fun isDebugLoggingEnabled(): Boolean = - sharedPreferences.getBoolean(DEBUG_LOG, true) - - fun setDebugLoggingEnabled(enabled: Boolean) = - sharedPreferences - .edit { putBoolean(DEBUG_LOG, enabled) } - .also { Logger.isDebugEnabled = enabled } - - fun getEphemeralKeyCurveOption(): EcCurve = - EcCurve.fromInt( - sharedPreferences.getInt( - EPHEMERAL_KEY_CURVE_OPTION, - EcCurve.P256.coseCurveIdentifier - ) - ) - - fun setEphemeralKeyCurveOption(newValue: EcCurve) = - sharedPreferences.edit { putInt(EPHEMERAL_KEY_CURVE_OPTION, newValue.coseCurveIdentifier) } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt b/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt deleted file mode 100644 index 04f3b55b4..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/ProvisioningUtil.kt +++ /dev/null @@ -1,344 +0,0 @@ -package com.android.identity.wallet.util - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.Rect -import com.android.identity.cbor.Bstr -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.Tagged -import com.android.identity.cbor.toDataItem -import com.android.identity.cose.Cose -import com.android.identity.cose.CoseNumberLabel -import com.android.identity.document.Document -import com.android.identity.document.DocumentUtil -import com.android.identity.document.NameSpacedData -import com.android.identity.crypto.Algorithm -import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.X509CertChain -import com.android.identity.crypto.EcCurve -import com.android.identity.crypto.toEcPrivateKey -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator -import com.android.identity.mdoc.mso.StaticAuthDataGenerator -import com.android.identity.mdoc.util.MdocUtil -import com.android.identity.securearea.SecureArea -import com.android.identity.securearea.SecureAreaRepository -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.document.KeysAndCertificates -import com.android.identity.wallet.selfsigned.ProvisionInfo -import com.android.identity.wallet.support.SecureAreaSupport -import com.android.identity.wallet.util.DocumentData.MICOV_DOCTYPE -import com.android.identity.wallet.util.DocumentData.MVR_DOCTYPE -import java.io.ByteArrayOutputStream -import java.time.Instant as JavaInstant -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import kotlin.random.Random -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant - -class ProvisioningUtil private constructor( - private val context: Context -) { - - val secureAreaRepository = SecureAreaRepository() - val documentStore by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { - HolderApp.createDocumentStore(context, secureAreaRepository) - } - - fun provisionSelfSigned( - nameSpacedData: NameSpacedData, - provisionInfo: ProvisionInfo, - ) { - val document = documentStore.createDocument(provisionInfo.documentName()) - documentStore.addDocument(document) - document.applicationData.setNameSpacedData("documentData", nameSpacedData) - - val authKeySecureArea: SecureArea = provisionInfo.currentSecureArea.secureArea - - // Store all settings for the document that are not SecureArea specific - document.applicationData.setString(USER_VISIBLE_NAME, provisionInfo.docName) - document.applicationData.setString(DOCUMENT_TYPE, provisionInfo.docType) - document.applicationData.setString(DATE_PROVISIONED, dateTimeFormatter.format(ZonedDateTime.now())) - document.applicationData.setNumber(CARD_ART, provisionInfo.docColor.toLong()) - document.applicationData.setBoolean(IS_SELF_SIGNED, true) - document.applicationData.setNumber(MAX_USAGES_PER_KEY, provisionInfo.maxUseMso.toLong()) - document.applicationData.setNumber(VALIDITY_IN_DAYS, provisionInfo.validityInDays.toLong()) - document.applicationData.setNumber(MIN_VALIDITY_IN_DAYS, provisionInfo.minValidityInDays.toLong()) - document.applicationData.setNumber(LAST_TIME_USED, -1) - document.applicationData.setString(AUTH_KEY_SECURE_AREA_IDENTIFIER, authKeySecureArea.identifier) - document.applicationData.setNumber(NUM_CREDENTIALS, provisionInfo.numberMso.toLong()) - - // Store settings for auth-key creation, these are all SecureArea-specific and we store - // them in a single blob at AUTH_KEY_SETTINGS - val support = SecureAreaSupport.getInstance(context, authKeySecureArea) - document.applicationData.setData( - AUTH_KEY_SETTINGS, - support.createAuthKeySettingsConfiguration(provisionInfo.secureAreaSupportState)) - - // Create initial batch of credentials - refreshCredentials(document, provisionInfo.docType) - } - - private fun ProvisionInfo.documentName(): String { - val regex = Regex("[^A-Za-z0-9 ]") - return regex.replace(docName, "").replace(" ", "_").lowercase() - } - - fun trackUsageTimestamp(document: Document) { - val now = Clock.System.now() - document.applicationData.setNumber(LAST_TIME_USED, now.toEpochMilliseconds()) - } - - fun refreshCredentials(document: Document, docType: String) { - val secureAreaIdentifier = document.applicationData.getString(AUTH_KEY_SECURE_AREA_IDENTIFIER) - val minValidTimeDays = document.applicationData.getNumber(MIN_VALIDITY_IN_DAYS) - val maxUsagesPerCred = document.applicationData.getNumber(MAX_USAGES_PER_KEY) - val numCreds = document.applicationData.getNumber(NUM_CREDENTIALS) - val validityInDays = document.applicationData.getNumber(VALIDITY_IN_DAYS).toInt() - - val now = Clock.System.now() - val validFrom = now - val validUntil = Instant.fromEpochMilliseconds( - validFrom.toEpochMilliseconds() + validityInDays*86400*1000L) - - val secureArea = secureAreaRepository.getImplementation(secureAreaIdentifier) - ?: throw IllegalStateException("No Secure Area with id ${secureAreaIdentifier} for document ${document.name}") - - val support = SecureAreaSupport.getInstance(context, secureArea) - val settings = support.createAuthKeySettingsFromConfiguration( - document.applicationData.getData(AUTH_KEY_SETTINGS), - "challenge".toByteArray(), - validFrom, - validUntil - ) - - val pendingCredsCount = DocumentUtil.managedCredentialHelper( - document, - CREDENTIAL_DOMAIN, - {toBeReplaced -> MdocCredential( - document, - toBeReplaced, - CREDENTIAL_DOMAIN, - secureArea, - settings, - docType - )}, - now, - numCreds.toInt(), - maxUsagesPerCred.toInt(), - minValidTimeDays*24*60*60*1000L, - false - ) - if (pendingCredsCount <= 0) { - return - } - - for (pendingCred in document.pendingCredentials.filter { it.domain == CREDENTIAL_DOMAIN }) { - pendingCred as MdocCredential - val msoGenerator = MobileSecurityObjectGenerator( - "SHA-256", - docType, - pendingCred.attestation.publicKey - ) - msoGenerator.setValidityInfo(now, validFrom, validUntil, null) - - // For mDLs, override the portrait with AuthenticationKeyCounter on top - // - var dataElementExceptions: Map>? = null - var dataElementOverrides: Map>? = null - if (docType.equals("org.iso.18013.5.1.mDL")) { - val portrait = document.applicationData.getNameSpacedData("documentData") - .getDataElementByteString("org.iso.18013.5.1", "portrait") - val portrait_override = overridePortrait(portrait, - pendingCred.credentialCounter) - - dataElementExceptions = - mapOf("org.iso.18013.5.1" to listOf("given_name", "portrait")) - dataElementOverrides = - mapOf("org.iso.18013.5.1" to mapOf( - "portrait" to Cbor.encode(Bstr(portrait_override)))) - } - - val issuerNameSpaces = MdocUtil.generateIssuerNameSpaces( - document.applicationData.getNameSpacedData("documentData"), - Random.Default, - 16, - dataElementOverrides - ) - - for (nameSpaceName in issuerNameSpaces.keys) { - val digests = MdocUtil.calculateDigestsForNameSpace( - nameSpaceName, - issuerNameSpaces, - Algorithm.SHA256 - ) - msoGenerator.addDigestIdsForNamespace(nameSpaceName, digests) - } - - val mso = msoGenerator.generate() - val taggedEncodedMso = Cbor.encode(Tagged(Tagged.ENCODED_CBOR, Bstr(mso))) - - val issuerKeyPair = when (docType) { - MVR_DOCTYPE -> KeysAndCertificates.getMekbDsKeyPair(context) - MICOV_DOCTYPE -> KeysAndCertificates.getMicovDsKeyPair(context) - else -> KeysAndCertificates.getMdlDsKeyPair(context) - } - - val issuerCert = when (docType) { - MVR_DOCTYPE -> KeysAndCertificates.getMekbDsCertificate(context) - MICOV_DOCTYPE -> KeysAndCertificates.getMicovDsCertificate(context) - else -> KeysAndCertificates.getMdlDsCertificate(context) - } - - val encodedIssuerAuth = Cbor.encode( - Cose.coseSign1Sign( - issuerKeyPair.private.toEcPrivateKey(issuerKeyPair.public, EcCurve.P256), - taggedEncodedMso, - true, - Algorithm.ES256, - protectedHeaders = mapOf( - Pair( - CoseNumberLabel(Cose.COSE_LABEL_ALG), - Algorithm.ES256.coseAlgorithmIdentifier.toDataItem() - ) - ), - unprotectedHeaders = mapOf( - Pair( - CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN), - X509CertChain( - listOf(X509Cert(issuerCert.encoded)) - ).toDataItem() - ) - ), - ).toDataItem() - ) - - val issuerProvidedAuthenticationData = StaticAuthDataGenerator( - MdocUtil.stripIssuerNameSpaces(issuerNameSpaces, dataElementExceptions), - encodedIssuerAuth - ).generate() - - pendingCred.certify( - issuerProvidedAuthenticationData, - validFrom, - validUntil - ) - } - } - - // Puts the string "MSO ${counter}" on top of the portrait image. - private fun overridePortrait(encodedPortrait: ByteArray, counter: Number): ByteArray { - val options = BitmapFactory.Options() - options.inMutable = true - val bitmap = BitmapFactory.decodeByteArray( - encodedPortrait, - 0, - encodedPortrait.size, - options) - - val text = "MSO ${counter}" - val canvas = Canvas(bitmap) - val paint = Paint(Paint.ANTI_ALIAS_FLAG) - paint.setColor(Color.WHITE) - paint.textSize = bitmap.width / 5.0f - paint.setShadowLayer(2.0f, 1.0f, 1.0f, Color.BLACK) - val bounds = Rect() - paint.getTextBounds(text, 0, text.length, bounds) - val x: Float = (bitmap.width - bounds.width()) / 2.0f - val y: Float = (bitmap.height - bounds.height()) / 4.0f - canvas.drawText(text, x, y, paint) - - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos) - val encodedModifiedPortrait: ByteArray = baos.toByteArray() - - return encodedModifiedPortrait - } - - companion object { - - const val CREDENTIAL_DOMAIN = "mdoc/MSO" - private const val USER_VISIBLE_NAME = "userVisibleName" - const val DOCUMENT_TYPE = "documentType" - private const val DATE_PROVISIONED = "dateProvisioned" - private const val CARD_ART = "cardArt" - private const val IS_SELF_SIGNED = "isSelfSigned" - private const val MAX_USAGES_PER_KEY = "maxUsagesPerCredential" - private const val VALIDITY_IN_DAYS = "validityInDays" - private const val MIN_VALIDITY_IN_DAYS = "minValidityInDays" - private const val LAST_TIME_USED = "lastTimeUsed" - private const val NUM_CREDENTIALS = "numCredentials" - private const val AUTH_KEY_SETTINGS = "authKeySettings" - private const val AUTH_KEY_SECURE_AREA_IDENTIFIER = "authKeySecureAreaIdentifier" - - private val dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME - - @SuppressLint("StaticFieldLeak") - @Volatile - private var instance: ProvisioningUtil? = null - - fun getInstance(context: Context) = instance ?: synchronized(this) { - instance ?: ProvisioningUtil(context).also { instance = it } - } - - val defaultSecureArea: SecureArea - get() = requireNotNull(instance?.secureAreaRepository?.implementations?.first()) - - fun Document?.toDocumentInformation(): DocumentInformation? { - return this?.let { - - val authKeySecureAreaIdentifier = it.applicationData.getString(AUTH_KEY_SECURE_AREA_IDENTIFIER) - val authKeySecureArea = instance!!.secureAreaRepository.getImplementation(authKeySecureAreaIdentifier) - ?: throw IllegalStateException("No Secure Area with id ${authKeySecureAreaIdentifier} for document ${it.name}") - - val credentials = certifiedCredentials.map { key -> - key as MdocCredential - val info = authKeySecureArea.getKeyInfo(key.alias) - DocumentInformation.KeyData( - counter = key.credentialCounter.toInt(), - validFrom = key.validFrom.formatted(), - validUntil = key.validUntil.formatted(), - domain = key.domain, - issuerDataBytesCount = key.issuerProvidedData.size, - usagesCount = key.usageCount, - keyPurposes = info.keyPurposes.first(), - ecCurve = info.publicKey.curve, - isHardwareBacked = false, // TODO: remove - secureAreaDisplayName = authKeySecureArea.displayName - ) - } - val lastTimeUsedMillis = it.applicationData.getNumber(LAST_TIME_USED) - val lastTimeUsed = if (lastTimeUsedMillis == -1L) { - "" - } else { - Instant.fromEpochMilliseconds(lastTimeUsedMillis).formatted() - } - DocumentInformation( - userVisibleName = it.applicationData.getString(USER_VISIBLE_NAME), - docName = it.name, - docType = it.applicationData.getString(DOCUMENT_TYPE), - dateProvisioned = it.applicationData.getString(DATE_PROVISIONED), - documentColor = it.applicationData.getNumber(CARD_ART).toInt(), - selfSigned = it.applicationData.getBoolean(IS_SELF_SIGNED), - maxUsagesPerKey = it.applicationData.getNumber(MAX_USAGES_PER_KEY).toInt(), - lastTimeUsed = lastTimeUsed, - authKeys = credentials - ) - } - } - - private fun Instant.formatted(): String { - val javaInstant = JavaInstant.ofEpochMilli(this.toEpochMilliseconds()) - val dateTime = ZonedDateTime.ofInstant(javaInstant, ZoneId.systemDefault()) - return dateTimeFormatter.format(dateTime) - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/RefreshKeysWorker.kt b/appholder/src/main/java/com/android/identity/wallet/util/RefreshKeysWorker.kt deleted file mode 100644 index c73520336..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/RefreshKeysWorker.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import androidx.work.Worker -import androidx.work.WorkerParameters -import com.android.identity.wallet.document.DocumentManager - -class RefreshKeysWorker( - context: Context, - params: WorkerParameters -) : Worker(context, params) { - - private val documentManager = DocumentManager.getInstance(context) - private val provisioningUtil = ProvisioningUtil.getInstance(context) - - override fun doWork(): Result { - documentManager.getDocuments().forEach { documentInformation -> - val document = documentManager.getDocumentByName(documentInformation.docName) - document?.let { provisioningUtil.refreshCredentials(it, documentInformation.docType) } - } - return Result.success() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/SampleDataProvider.kt b/appholder/src/main/java/com/android/identity/wallet/util/SampleDataProvider.kt deleted file mode 100644 index 81f287011..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/SampleDataProvider.kt +++ /dev/null @@ -1,342 +0,0 @@ -package com.android.identity.wallet.util - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.wallet.R - -object SampleDataProvider { - const val MDL_NAMESPACE = "org.iso.18013.5.1" - const val AAMVA_NAMESPACE = "org.iso.18013.5.1.aamva" - const val MVR_NAMESPACE = "nl.rdw.mekb.1" - const val MICOV_ATT_NAMESPACE = "org.micov.attestation.1" - const val MICOV_VTR_NAMESPACE = "org.micov.vtr.1" - const val EUPID_NAMESPACE = "eu.europa.ec.eudi.pid.1" - - fun getSampleValue( - context: Context, - namespace: String, - identifier: String, - type: DocumentAttributeType, - identifierParent: String? = null - ): Any? { - return when (namespace) { - MDL_NAMESPACE -> when (identifier) { - "family_name" -> "Mustermann" - "given_name" -> "Erika" - "birth_date" -> "1971-09-01" - "issue_date" -> "2021-04-18" - "expiry_date" -> "2026-04-18" - "issuing_country" -> "US" - "issuing_authority" -> "Google" - "document_number" -> "987654321" - "portrait" -> BitmapFactory.decodeResource( - context.resources, - R.drawable.img_erika_portrait - ) - - "un_distinguishing_sign" -> "USA" - "administrative_number" -> "123456789" - "sex" -> 2 - "height" -> 175 - "weight" -> 68 - "eye_colour" -> "blue" - "hair_colour" -> "blond" - "birth_place" -> "Sample City" - "resident_address" -> "Sample address" - "portrait_capture_date" -> "2021-04-18" - "age_in_years" -> 52 - "age_birth_year" -> 1971 - "age_over_18" -> true - "age_over_21" -> true - "age_over_25" -> true - "age_over_62" -> false - "age_over_65" -> false - "issuing_jurisdiction" -> "Sample issuing jurisdiction" - "nationality" -> "US" - "resident_city" -> "Sample City" - "resident_state" -> "Sample State" - "resident_postal_code" -> "18013" - "resident_country" -> "US" - "family_name_national_character" -> "Бабіак" - "given_name_national_character" -> "Ерика" - "signature_usual_mark" -> BitmapFactory.decodeResource( - context.resources, - R.drawable.img_erika_signature - ) - - "biometric_template_face", - "biometric_template_finger", - "biometric_template_signature_sign", - "biometric_template_iris" -> Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888) - - else -> defaultValue(type) - } - - AAMVA_NAMESPACE -> when (identifier) { - "name_suffix" -> "SR" - "organ_donor" -> 1 - "veteran" -> null - "family_name_truncation" -> "N" - "given_name_truncation" -> "N" - "aka_family_name" -> "Muster" - "aka_given_name" -> "Erik" - "aka_suffix" -> "JR" - "weight_range" -> 3 - "race_ethnicity" -> "W" - "DHS_compliance" -> "F" - "DHS_temporary_lawful_status" -> null - "EDL_credential" -> null - "resident_county" -> "123" - "hazmat_endorsement_expiration_date" -> "2026-04-18" - "sex" -> 2 - "audit_information" -> "Sample auditor" - "aamva_version" -> 2 - "domestic_vehicle_class_code" -> "B" - "domestic_vehicle_class_description" -> "Light vehicles" - "issue_date" -> "2021-04-18" - "expiry_date" -> "2026-04-18" - else -> defaultValue(type) - } - - MVR_NAMESPACE -> when (identifier) { - "issue_date" -> "2021-04-18" - "vin" -> "1M8GDM9AXKP042788" - "issuingCountry" -> "NL" - "competentAuthority" -> "RDW" - "registrationNumber" -> "E-01-23" - "validFrom" -> "2021-04-19" - "validUntil" -> "2023-04-20" - "ownershipStatus" -> 2 - "name" -> "Erika" - "streetName" -> "Teststraat" - "houseNumber" -> "86" - "houseNumberSuffix" -> "A" - "postalCode" -> "1234 AA" - "placeOfResidence" -> "Samplecity" - "make" -> "Dummymobile" - else -> defaultValue(type) - } - - MICOV_ATT_NAMESPACE -> when (identifier) { - "1D47_vaccinated" -> true - "RA01_vaccinated" -> true - "fac" -> BitmapFactory.decodeResource( - context.resources, - R.drawable.img_erika_portrait - ) - - "fni" -> "M" - "gni" -> "E" - "by" -> 1964 - "bm" -> 8 - "bd" -> 12 - "Result" -> "260415000" - "TypeOfTest" -> "LP6464-4" - "TimeOfTest" -> "2021-10-12" - "SeCondFulfilled" -> true - "SeCondType" -> "leisure" - "SeCondExpiry" -> "2021-10-13" - else -> defaultValue(type) - } - - MICOV_VTR_NAMESPACE -> when (identifier) { - "fn" -> "Mustermann" - "gn" -> "Erika" - "dob" -> "1964-08-12" - "sex" -> 2 - "tg" -> "840539006" - "vp" -> "1119349007" - "mp" -> "EU/1/20/1528" - "br" -> "Sample brand" - "ma" -> "ORG-100030215" - "bn" -> when (identifierParent != null && identifierParent == "v_RA01_1") { - true -> "B12345/67" - else -> "B67890/12" - } - - "dn" -> when (identifierParent != null && identifierParent == "v_RA01_1") { - true -> 1 - else -> 2 - } - - "sd" -> 2 - "dt" -> when (identifierParent != null && identifierParent == "v_RA01_1") { - true -> "2021-04-08" - else -> "2021-05-18" - } - - "co" -> "US" - "ao" -> "RHI" - "ap" -> "" - "nx" -> "2021-05-20" - "is" -> "SC17" - "ci" -> when (identifierParent != null && identifierParent == "v_RA01_1") { - true -> "URN:UVCI:01:UT:187/37512422923" - else -> "URN:UVCI:01:UT:187/37512533044" - } - - "pd" -> "" - "vf" -> "2021-05-27" - "vu" -> "2022-05-27" - "pty" -> when (identifierParent != null && identifierParent == "pid_PPN") { - true -> "PPN" - else -> "DL" - } - - "pnr" -> when (identifierParent != null && identifierParent == "pid_PPN") { - true -> "476284728" - else -> "987654321" - } - - "pic" -> "US" - "pia" -> "" - else -> defaultValue(type) - } - - EUPID_NAMESPACE -> when (identifier) { - "family_name" -> "Mustermann" - "family_name_national_characters" -> "Бабіак" - "given_name" -> "Erika" - "given_name_national_characters" -> "Ерика" - "birth_date" -> "1986-03-14" - "persistent_id" -> "0128196532" - "family_name_birth" -> "Mustermann" - "family_name_birth_national_characters" -> "Бабіак" - "given_name_birth" -> "Erika" - "given_name_birth_national_characters" -> "Ерика" - "birth_place" -> "Place of birth" - "resident_address" -> "Resident address" - "resident_city" -> "Resident City" - "resident_postal_code" -> "Resident postal code" - "resident_state" -> "Resident state" - "resident_country" -> "Resident country" - "gender" -> "female" - "nationality" -> "NL" - "portrait" -> BitmapFactory.decodeResource( - context.resources, - R.drawable.img_erika_portrait - ) - "portrait_capture_date" -> "2022-11-14" - "biometric_template_finger" -> Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888) - "age_over_13" -> true - "age_over_16" -> true - "age_over_18" -> true - "age_over_21" -> true - "age_over_60" -> false - "age_over_65" -> false - "age_over_68" -> false - "age_in_years" -> 37 - "age_birth_year" -> 1986 - else -> defaultValue(type) - } - - else -> defaultValue(type) - } - } - - fun getSampleValue(namespace: String, identifier: String, type: DocumentAttributeType, index: Int): Any? { - return when (namespace) { - MDL_NAMESPACE -> when (identifier) { - "vehicle_category_code" -> when (index) { - 0 -> "A" - else -> "B" - } - - "issue_date" -> when (index) { - 0 -> "2018-08-09" - else -> "2017-02-23" - } - - "expiry_date" -> when (index) { - 0 -> "2024-10-20" - else -> "2024-10-20" - } - - "code" -> when (index) { - 0 -> "S01" - else -> "S02" - } - - "sign" -> when (index) { - 0 -> "<=" - else -> "=" - } - - "value" -> when (index) { - 0 -> "2500" - else -> "8" - } - - else -> defaultValue(type) - } - - AAMVA_NAMESPACE -> when (identifier) { - "domestic_vehicle_restriction_code" -> when (index) { - 0 -> "B" - else -> "C" - } - - "domestic_vehicle_restriction_description" -> when (index) { - 0 -> "Corrective lenses must be worn" - else -> "Mechanical Aid (special brakes, hand controls, or other adaptive devices)" - } - - "domestic_vehicle_endorsement_code" -> when (index) { - 0 -> "P" - else -> "S" - } - - "domestic_vehicle_endorsement_description" -> when (index) { - 0 -> "Passenger" - else -> "School Bus" - } - - else -> defaultValue(type) - } - - else -> defaultValue(type) - } - } - - fun getArrayLength(namespace: String, identifier: String): Int { - return when (namespace) { - MDL_NAMESPACE -> when (identifier) { - "driving_privileges" -> 2 - "codes" -> 2 - else -> 2 - } - - AAMVA_NAMESPACE -> when (identifier) { - "domestic_driving_privileges" -> 1 - "domestic_vehicle_restrictions" -> 2 - "domestic_vehicle_endorsements" -> 2 - else -> 2 - } - - else -> 2 - } - } - - private fun defaultValue(type: DocumentAttributeType): Any? { - return when (type) { - is DocumentAttributeType.Blob -> byteArrayOf() - is DocumentAttributeType.String -> "-" - is DocumentAttributeType.Number -> 0 - is DocumentAttributeType.Date, - is DocumentAttributeType.DateTime -> "2100-01-01" - - is DocumentAttributeType.Picture -> Bitmap.createBitmap( - 200, - 200, - Bitmap.Config.ARGB_8888 - ) - - is DocumentAttributeType.Boolean -> false - is DocumentAttributeType.StringOptions, - is DocumentAttributeType.IntegerOptions, - is DocumentAttributeType.ComplexType -> null - } - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/SavedStateHandleExtensions.kt b/appholder/src/main/java/com/android/identity/wallet/util/SavedStateHandleExtensions.kt deleted file mode 100644 index 2fc445ce0..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/SavedStateHandleExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.util - -import androidx.lifecycle.SavedStateHandle - -fun SavedStateHandle.updateState(block: (T) -> T) { - val prevValue = get("state")!! - val nextValue = block(prevValue) - set("state", nextValue) -} - -fun SavedStateHandle.getState(initialState: T) = getStateFlow("state", initialState) \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/TransferStatus.kt b/appholder/src/main/java/com/android/identity/wallet/util/TransferStatus.kt deleted file mode 100644 index c41943ef5..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/TransferStatus.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.android.identity.wallet.util - -enum class TransferStatus { - ENGAGEMENT_DETECTED, - CONNECTING, - CONNECTED, - REQUEST, - REQUEST_SERVED, - DISCONNECTED, - ERROR -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/util/ViewHelper.kt b/appholder/src/main/java/com/android/identity/wallet/util/ViewHelper.kt deleted file mode 100644 index a4f72f9ff..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/util/ViewHelper.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.android.identity.wallet.util - -import android.graphics.Bitmap -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.IntegerOption -import com.android.identity.documenttype.StringOption - -data class Field( - val id: Int, - val label: String, - val name: String, - val fieldType: DocumentAttributeType, - val value: Any?, - val namespace: String? = null, - val isArray: Boolean = false, - val parentId: Int? = null, - var stringOptions: List? = null, - var integerOptions: List? = null -) { - fun hasValue(): Boolean { - return value != "" - } - - fun getValueLong(): Long { - return value?.toString()?.toLong() ?: 0 - } - - fun getValueString(): String { - return value as String - } - - fun getValueBoolean(): Boolean { - return value as Boolean - } - - fun getValueBitmap(): Bitmap { - return value as Bitmap - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/viewmodel/SelfSignedViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/viewmodel/SelfSignedViewModel.kt deleted file mode 100644 index afb48c285..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/viewmodel/SelfSignedViewModel.kt +++ /dev/null @@ -1,286 +0,0 @@ -package com.android.identity.wallet.viewmodel - -import android.app.Application -import android.view.View -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.android.identity.documenttype.DocumentAttributeType -import com.android.identity.documenttype.MdocDocumentType -import com.android.identity.documenttype.MdocDataElement -import com.android.identity.wallet.HolderApp -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.documentdata.MdocComplexTypeDefinition -import com.android.identity.wallet.documentdata.MdocComplexTypeRepository -import com.android.identity.wallet.selfsigned.SelfSignedDocumentData -import com.android.identity.wallet.util.Field -import com.android.identity.wallet.util.SampleDataProvider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class SelfSignedViewModel(val app: Application) : - AndroidViewModel(app) { - - companion object { - private const val LOG_TAG = "SelfSignedViewModel" - } - - private val documentManager = DocumentManager.getInstance(app.applicationContext) - val loading = MutableLiveData() - val created = MutableLiveData() - private val fieldsByDocType: MutableMap> = mutableMapOf() - private var id = 1 - - init { - loading.value = View.GONE - for (documentType in HolderApp.documentTypeRepositoryInstance.documentTypes - .filter { it.mdocDocumentType != null }) { - id = 1 // reset the id to 1 - fieldsByDocType[documentType.mdocDocumentType?.docType!!] = - createFields(documentType.mdocDocumentType!!) - } - } - - fun getFields(docType: String): MutableList { - return fieldsByDocType[docType] - ?: throw IllegalArgumentException("No field list valid for $docType") - } - - fun createSelfSigned(documentData: SelfSignedDocumentData) { - loading.value = View.VISIBLE - viewModelScope.launch { - withContext(Dispatchers.IO) { - documentManager.createSelfSignedDocument(documentData) - } - created.value = true - loading.value = View.GONE - } - } - - private fun createFields(mdocDocumentType: MdocDocumentType): MutableList { - val fields: MutableList = mutableListOf() - val complexTypes = MdocComplexTypeRepository.getComplexTypes(mdocDocumentType.docType) - for (namespace in mdocDocumentType.namespaces.values) { - val namespaceComplexTypes = - complexTypes?.namespaces?.find { it.namespace == namespace.namespace } - for (dataElement in namespace.dataElements.values) { - when (dataElement.attribute.type) { - is DocumentAttributeType.ComplexType -> { - val complexTypeDefinitions = namespaceComplexTypes?.dataElements?.filter { - it.parentIdentifiers.contains(dataElement.attribute.identifier) - } - - if (complexTypeDefinitions?.first()?.partOfArray == true) { - val arrayLength = - SampleDataProvider.getArrayLength( - namespace.namespace, - dataElement.attribute.identifier - ) - val parentField = Field( - id++, - "${dataElement.attribute.displayName} ($arrayLength items)", - dataElement.attribute.identifier, - dataElement.attribute.type, - null, - namespace = namespace.namespace, - isArray = true, - ) - fields.add(parentField) - addArrayFields( - parentField, - fields, - namespaceComplexTypes.dataElements) - } else { - val parentField = Field( - id++, - dataElement.attribute.displayName, - dataElement.attribute.identifier, - dataElement.attribute.type, - null, - namespace = namespace.namespace - ) - fields.add(parentField) - addMapFields( - parentField, - fields, - namespaceComplexTypes?.dataElements!!) - } - } - - else -> { - - val sampleValue = SampleDataProvider.getSampleValue( - app, - namespace.namespace, - dataElement.attribute.identifier, - dataElement.attribute.type - ) - val field = Field( - id++, - dataElement.attribute.displayName, - dataElement.attribute.identifier, - dataElement.attribute.type, - sampleValue, - namespace = namespace.namespace - ) - addOptions(field, dataElement) - fields.add(field) - } - } - } - } - return fields - } - - - private fun addArrayFields( - parentField: Field, - fields: MutableList, - dataElements: List, - prefix: String = "" - ) { - val arrayLength = - SampleDataProvider.getArrayLength(parentField.namespace!!, parentField.name) - val childElements = dataElements.filter { it.parentIdentifiers.contains(parentField.name) } - for (i in 0..arrayLength - 1) { - for (childElement in childElements) { - if (childElement.type is DocumentAttributeType.ComplexType) { - - if (dataElements.any { it.parentIdentifiers.contains(childElement.identifier) && it.partOfArray }) { - val childField = Field( - id++, - "$prefix${i + 1} | ${childElement.displayName} (${ - SampleDataProvider.getArrayLength( - parentField.namespace, - childElement.identifier - ) - } items)", - childElement.identifier, - childElement.type, - null, - namespace = parentField.namespace, - isArray = true, - parentId = parentField.id - ) - fields.add(childField) - addArrayFields( - childField, - fields, - dataElements, - "$prefix${i + 1} | " - ) - } else { - val childField = Field( - id++, - "$prefix${i + 1} | ${childElement.displayName}", - childElement.identifier, - childElement.type, - null, - namespace = parentField.namespace, - parentId = parentField.id - ) - fields.add(childField) - addMapFields( - childField, - fields, - dataElements, - "$prefix${i + 1} | " - ) - } - } else { - val sampleValue = - SampleDataProvider.getSampleValue(parentField.namespace, childElement.identifier, childElement.type, i) - val childField = Field( - id++, - "$prefix${i + 1} | ${childElement.displayName}", - childElement.identifier, - childElement.type, - sampleValue, - namespace = parentField.namespace, - parentId = parentField.id - ) - addOptions(childField, childElement) - fields.add(childField) - } - } - } - } - - private fun addMapFields( - parentField: Field, - fields: MutableList, - dataElements: List, - prefix: String = "" - ) { - - val childElements = dataElements.filter { it.parentIdentifiers.contains(parentField.name) } - for (childElement in childElements) { - if (childElement.type is DocumentAttributeType.ComplexType) { - val isArray = dataElements.any { it.parentIdentifiers.contains(childElement.identifier) && it.partOfArray } - val childField = Field( - id++, - "$prefix${childElement.displayName}", - childElement.identifier, - childElement.type, - null, - namespace = parentField.namespace, - isArray = isArray, - parentId = parentField.id - ) - fields.add(childField) - if (isArray){ - addArrayFields(childField, fields, dataElements, prefix) - } else { - addMapFields(childField, fields, dataElements, prefix) - } - } else { - val sampleValue = - SampleDataProvider.getSampleValue( - app, - parentField.namespace!!, - childElement.identifier, - childElement.type - ) - val childField = Field( - id++, - "$prefix${childElement.displayName}", - childElement.identifier, - childElement.type, - sampleValue, - namespace = parentField.namespace, - parentId = parentField.id - ) - addOptions(childField, childElement) - fields.add(childField) - } - - } - } - - fun addOptions(field: Field, dataElement: MdocDataElement) { - when (dataElement.attribute.type) { - is DocumentAttributeType.StringOptions -> field.stringOptions = - (dataElement.attribute.type as DocumentAttributeType.StringOptions).options - - is DocumentAttributeType.IntegerOptions -> field.integerOptions = - (dataElement.attribute.type as DocumentAttributeType.IntegerOptions).options - - else -> {} - } - } - - fun addOptions(field: Field, dataElement: MdocComplexTypeDefinition) { - when (dataElement.type) { - is DocumentAttributeType.StringOptions -> field.stringOptions = - dataElement.type.options - - is DocumentAttributeType.IntegerOptions -> field.integerOptions = - dataElement.type.options - - else -> {} - } - } - -} - diff --git a/appholder/src/main/java/com/android/identity/wallet/viewmodel/ShareDocumentViewModel.kt b/appholder/src/main/java/com/android/identity/wallet/viewmodel/ShareDocumentViewModel.kt deleted file mode 100644 index 8e98d874c..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/viewmodel/ShareDocumentViewModel.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.android.identity.wallet.viewmodel - -import android.app.Application -import android.view.View -import androidx.databinding.ObservableField -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import com.android.identity.mdoc.origininfo.OriginInfo -import com.android.identity.wallet.transfer.TransferManager -import com.android.identity.wallet.util.TransferStatus - -class ShareDocumentViewModel(val app: Application) : AndroidViewModel(app) { - - private val transferManager = TransferManager.getInstance(app.applicationContext) - var deviceEngagementQr = ObservableField() - var message = ObservableField() - private var hasStarted = false - - fun getTransferStatus(): LiveData = transferManager.getTransferStatus() - - fun startPresentationReverseEngagement( - reverseEngagementUri: String, - originInfos: List - ) { - if (!hasStarted) { - transferManager.startPresentationReverseEngagement(reverseEngagementUri, originInfos) - hasStarted = true - } - } - - fun cancelPresentation() { - transferManager.stopPresentation( - sendSessionTerminationMessage = true, - useTransportSpecificSessionTermination = false - ) - hasStarted = false - message.set("Presentation canceled") - } - - fun showQrCode() { - deviceEngagementQr.set(transferManager.getDeviceEngagementQrCode()) - } - - fun triggerQrEngagement() { - transferManager.startQrEngagement() - } -} 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 deleted file mode 100644 index cbe4f224b..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/viewmodel/TransferDocumentViewModel.kt +++ /dev/null @@ -1,182 +0,0 @@ -package com.android.identity.wallet.viewmodel - -import android.app.Application -import android.view.View -import androidx.databinding.ObservableField -import androidx.databinding.ObservableInt -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.android.identity.mdoc.credential.MdocCredential -import com.android.identity.mdoc.request.DeviceRequestParser -import com.android.identity.mdoc.response.DeviceResponseGenerator -import com.android.identity.securearea.KeyUnlockData -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 -import com.android.identity.wallet.authconfirmation.SignedElementsCollection -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 kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class TransferDocumentViewModel(val app: Application) : AndroidViewModel(app) { - - private val transferManager = TransferManager.getInstance(app.applicationContext) - private val documentManager = DocumentManager.getInstance(app.applicationContext) - private val signedElements = SignedElementsCollection() - private val requestedElements = mutableListOf() - private val closeConnectionMutableLiveData = MutableLiveData() - private val selectedDocuments = mutableListOf() - - var inProgress = ObservableInt(View.GONE) - var documentsSent = ObservableField() - val connectionClosedLiveData: LiveData = closeConnectionMutableLiveData - - private val mutableConfirmationState = MutableLiveData() - val authConfirmationState: LiveData = mutableConfirmationState - - fun onAuthenticationCancelled() { - mutableConfirmationState.value = true - } - - fun onAuthenticationCancellationConsumed() { - mutableConfirmationState.value = null - } - - fun getTransferStatus(): LiveData = - transferManager.getTransferStatus() - - fun getRequestedDocuments(): Collection = - transferManager.documentRequests() - - fun getDocuments() = documentManager.getDocuments() - - fun getSelectedDocuments() = selectedDocuments - - fun requestedElements() = requestedElements - - fun closeConnection() { - cleanUp() - closeConnectionMutableLiveData.value = true - } - - fun addDocumentForSigning(document: RequestedDocumentData) { - signedElements.addNamespace(document) - } - - fun toggleSignedElement(element: RequestedElement) { - signedElements.toggleProperty(element) - } - - fun createSelectedItemsList() { - val ownDocuments = getSelectedDocuments() - val requestedDocuments = getRequestedDocuments() - val result = mutableListOf() - requestedDocuments.forEach { requestedDocument -> - try { - val ownDocument = ownDocuments.first { it.docType == requestedDocument.docType } - val issuerSignedEntriesToRequest = requestedElementsFrom(requestedDocument) - result.add( - RequestedDocumentData( - userReadableName = ownDocument.userVisibleName, - identityCredentialName = ownDocument.docName, - requestedElements = issuerSignedEntriesToRequest, - requestedDocument = requestedDocument - ) - ) - } catch (e: NoSuchElementException) { - logWarning("No document for docType " + requestedDocument.docType) - } - } - requestedElements.addAll(result) - } - - fun sendResponseForSelection( - onResultReady: (result: AddDocumentToResponseResult) -> Unit, - credential: MdocCredential? = null, - authKeyUnlockData: KeyUnlockData? = null - ) { - val elementsToSend = signedElements.collect() - val responseGenerator = DeviceResponseGenerator(DEVICE_RESPONSE_STATUS_OK) - 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, - credential, - authKeyUnlockData, - ) - } - 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 as Int)) - cleanUp() - onResultReady(result) - /* - } catch (e: CredentialInvalidatedException) { - logWarning("Credential '${signedDocument.identityCredentialName}' is invalid. Deleting.") - documentManager.deleteCredentialByName(signedDocument.identityCredentialName) - Toast.makeText( - app.applicationContext, "Deleting invalid document " - + signedDocument.identityCredentialName, - Toast.LENGTH_SHORT - ).show() - */ - } catch (e: NoSuchElementException) { - logWarning("No requestedDocument for " + signedDocument.documentType) - } - } - } - } - - fun cancelPresentation( - sendSessionTerminationMessage: Boolean, - useTransportSpecificSessionTermination: Boolean - ) { - transferManager.stopPresentation( - sendSessionTerminationMessage, - useTransportSpecificSessionTermination - ) - } - - private fun requestedElementsFrom( - requestedDocument: DeviceRequestParser.DocRequest - ): ArrayList { - val result = arrayListOf() - requestedDocument.namespaces.forEach { namespace -> - val elements = requestedDocument.getEntryNames(namespace).map { element -> - RequestedElement(namespace, element) - } - result.addAll(elements) - } - return result - } - - private fun cleanUp() { - requestedElements.clear() - signedElements.clear() - selectedDocuments.clear() - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPageTransformer.kt b/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPageTransformer.kt deleted file mode 100644 index ab7f3ac23..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPageTransformer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.android.identity.wallet.wallet - -import android.content.Context -import android.view.View -import androidx.viewpager2.widget.ViewPager2 -import com.android.identity.wallet.R -import kotlin.math.abs - -class DocumentPageTransformer(context: Context) : ViewPager2.PageTransformer { - private val resources = context.resources - private val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible) - private val currentItemHorizontalMarginPx = resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin) - private val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx - - override fun transformPage(page: View, position: Float) { - page.translationX = -pageTranslationX * position - page.scaleY = 1 - (0.25f * abs(position)) - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPagerItemDecoration.kt b/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPagerItemDecoration.kt deleted file mode 100644 index c9903b79d..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/wallet/DocumentPagerItemDecoration.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.android.identity.wallet.wallet - -import android.content.Context -import android.graphics.Rect -import android.view.View -import androidx.annotation.DimenRes -import androidx.recyclerview.widget.RecyclerView - -class DocumentPagerItemDecoration( - context: Context, - @DimenRes horizontalMarginInDp: Int -) : RecyclerView.ItemDecoration() { - - private val horizontalMarginInPx: Int = - context.resources.getDimension(horizontalMarginInDp).toInt() - - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - outRect.right = horizontalMarginInPx - outRect.left = horizontalMarginInPx - } -} \ No newline at end of file diff --git a/appholder/src/main/java/com/android/identity/wallet/wallet/SelectDocumentFragment.kt b/appholder/src/main/java/com/android/identity/wallet/wallet/SelectDocumentFragment.kt deleted file mode 100644 index 14ad18614..000000000 --- a/appholder/src/main/java/com/android/identity/wallet/wallet/SelectDocumentFragment.kt +++ /dev/null @@ -1,188 +0,0 @@ -package com.android.identity.wallet.wallet - -import android.Manifest -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.OnBackPressedCallback -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.navigation.fragment.findNavController -import com.android.identity.wallet.R -import com.android.identity.wallet.adapter.DocumentAdapter -import com.android.identity.wallet.databinding.FragmentSelectDocumentBinding -import com.android.identity.wallet.document.DocumentInformation -import com.android.identity.wallet.document.DocumentManager -import com.android.identity.wallet.util.TransferStatus -import com.android.identity.wallet.util.log -import com.android.identity.wallet.viewmodel.ShareDocumentViewModel -import com.google.android.material.tabs.TabLayoutMediator - -class SelectDocumentFragment : Fragment() { - - private var _binding: FragmentSelectDocumentBinding? = null - private val binding get() = _binding!! - - private val viewModel: ShareDocumentViewModel by activityViewModels() - private val timeInterval = 2000 // # milliseconds passed between two back presses - private var mBackPressed: Long = 0 - - private val appPermissions: Array = - if (android.os.Build.VERSION.SDK_INT >= 31) { - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.BLUETOOTH_ADVERTISE, - Manifest.permission.BLUETOOTH_SCAN, - Manifest.permission.BLUETOOTH_CONNECT, - ) - } else { - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - ) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - // Ask to press twice before leave the app - requireActivity().onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (mBackPressed + timeInterval > System.currentTimeMillis()) { - requireActivity().finish() - return - } else { - Toast.makeText( - requireContext(), - R.string.toast_press_back_twice, - Toast.LENGTH_SHORT - ).show() - } - mBackPressed = System.currentTimeMillis() - } - }) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentSelectDocumentBinding.inflate(inflater) - val adapter = DocumentAdapter() - binding.vpDocuments.adapter = adapter - binding.fragment = this - setupDocumentsPager(binding) - - val documentManager = DocumentManager.getInstance(requireContext()) - setupScreen(binding, adapter, documentManager.getDocuments().toMutableList()) - - val permissionsNeeded = appPermissions.filter { permission -> - ContextCompat.checkSelfPermission( - requireContext(), - permission - ) != PackageManager.PERMISSION_GRANTED - } - - if (permissionsNeeded.isNotEmpty()) { - permissionsLauncher.launch( - permissionsNeeded.toTypedArray() - ) - } - - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - viewModel.getTransferStatus().observe(viewLifecycleOwner) { - when (it) { - TransferStatus.CONNECTED -> { - openTransferScreen() - } - - TransferStatus.ERROR -> { - binding.tvNfcLabel.text = "Error on presentation!" - } - //Shall we update the top label of the screen for each state? - else -> {} - } - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private fun setupDocumentsPager(binding: FragmentSelectDocumentBinding) { - TabLayoutMediator(binding.tlPageIndicator, binding.vpDocuments) { _, _ -> }.attach() - binding.vpDocuments.offscreenPageLimit = 1 - binding.vpDocuments.setPageTransformer(DocumentPageTransformer(requireContext())) - val itemDecoration = DocumentPagerItemDecoration( - requireContext(), - R.dimen.viewpager_current_item_horizontal_margin - ) - binding.vpDocuments.addItemDecoration(itemDecoration) - } - - private fun setupScreen( - binding: FragmentSelectDocumentBinding, - adapter: DocumentAdapter, - documentsList: MutableList - ) { - if (documentsList.isEmpty()) { - showEmptyView(binding) - } else { - adapter.submitList(documentsList) - showDocumentsPager(binding) - } - } - - private fun openTransferScreen() { - val destination = SelectDocumentFragmentDirections.toTransferDocument() - findNavController().navigate(destination) - } - - private fun showEmptyView(binding: FragmentSelectDocumentBinding) { - binding.vpDocuments.visibility = View.GONE - binding.cvEmptyView.visibility = View.VISIBLE - binding.btShowQr.visibility = View.GONE - binding.btAddDocument.setOnClickListener { openAddDocument() } - } - - private fun showDocumentsPager(binding: FragmentSelectDocumentBinding) { - binding.vpDocuments.visibility = View.VISIBLE - binding.cvEmptyView.visibility = View.GONE - binding.btShowQr.visibility = View.VISIBLE - binding.btShowQr.setOnClickListener { displayQRCode() } - } - - private fun displayQRCode() { - val destination = SelectDocumentFragmentDirections.toShowQR() - findNavController().navigate(destination) - } - - private fun openAddDocument() { - val destination = SelectDocumentFragmentDirections.toAddSelfSigned() - findNavController().navigate(destination) - } - - private val permissionsLauncher = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - permissions.entries.forEach { - log("permissionsLauncher ${it.key} = ${it.value}") - if (!it.value) { - Toast.makeText( - activity, - "The ${it.key} permission is required for BLE", - Toast.LENGTH_LONG - ).show() - return@registerForActivityResult - } - } - } -} \ No newline at end of file diff --git a/appholder/src/main/res/drawable/blue_gradient.xml b/appholder/src/main/res/drawable/blue_gradient.xml deleted file mode 100644 index 25fb5fc4c..000000000 --- a/appholder/src/main/res/drawable/blue_gradient.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/bottom_sheet_handle.xml b/appholder/src/main/res/drawable/bottom_sheet_handle.xml deleted file mode 100644 index 3e1969557..000000000 --- a/appholder/src/main/res/drawable/bottom_sheet_handle.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/default_page_dot.xml b/appholder/src/main/res/drawable/default_page_dot.xml deleted file mode 100644 index 03c1edb14..000000000 --- a/appholder/src/main/res/drawable/default_page_dot.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/driving_license_bg.png b/appholder/src/main/res/drawable/driving_license_bg.png deleted file mode 100644 index 180488d127560625b9e61e1b366046f318b9f6ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14904 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 UKBBn?JC}sIiq4Z#CF?i;3+dABqyPW_ diff --git a/appholder/src/main/res/drawable/gradient_red.xml b/appholder/src/main/res/drawable/gradient_red.xml deleted file mode 100644 index 725371076..000000000 --- a/appholder/src/main/res/drawable/gradient_red.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/green_gradient.xml b/appholder/src/main/res/drawable/green_gradient.xml deleted file mode 100644 index 44e42bf07..000000000 --- a/appholder/src/main/res/drawable/green_gradient.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/ic_add_document.xml b/appholder/src/main/res/drawable/ic_add_document.xml deleted file mode 100644 index 7bf1f4a44..000000000 --- a/appholder/src/main/res/drawable/ic_add_document.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/appholder/src/main/res/drawable/ic_add_http.xml b/appholder/src/main/res/drawable/ic_add_http.xml deleted file mode 100644 index 283b25e12..000000000 --- a/appholder/src/main/res/drawable/ic_add_http.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/appholder/src/main/res/drawable/ic_launcher_foreground.xml b/appholder/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 04aff291b..000000000 --- a/appholder/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/appholder/src/main/res/drawable/ic_nfc.xml b/appholder/src/main/res/drawable/ic_nfc.xml deleted file mode 100644 index 075742a4e..000000000 --- a/appholder/src/main/res/drawable/ic_nfc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/appholder/src/main/res/drawable/ic_outline_info_24.xml b/appholder/src/main/res/drawable/ic_outline_info_24.xml deleted file mode 100644 index 35f7f5f61..000000000 --- a/appholder/src/main/res/drawable/ic_outline_info_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/appholder/src/main/res/drawable/ic_present_document.xml b/appholder/src/main/res/drawable/ic_present_document.xml deleted file mode 100644 index 91f8971aa..000000000 --- a/appholder/src/main/res/drawable/ic_present_document.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/appholder/src/main/res/drawable/ic_settings.xml b/appholder/src/main/res/drawable/ic_settings.xml deleted file mode 100644 index 1134c3184..000000000 --- a/appholder/src/main/res/drawable/ic_settings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/appholder/src/main/res/drawable/ic_wallet.xml b/appholder/src/main/res/drawable/ic_wallet.xml deleted file mode 100644 index 94ca58094..000000000 --- a/appholder/src/main/res/drawable/ic_wallet.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/appholder/src/main/res/drawable/img_erika_portrait.jpg b/appholder/src/main/res/drawable/img_erika_portrait.jpg deleted file mode 100644 index 31e356ddc4d3d9c5ae56720bef1202af4fe5f841..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11581 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 B9RdIV diff --git a/appholder/src/main/res/drawable/pager_indicator_selector.xml b/appholder/src/main/res/drawable/pager_indicator_selector.xml deleted file mode 100644 index f970bd233..000000000 --- a/appholder/src/main/res/drawable/pager_indicator_selector.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/selected_pager_dot.xml b/appholder/src/main/res/drawable/selected_pager_dot.xml deleted file mode 100644 index d2408fc64..000000000 --- a/appholder/src/main/res/drawable/selected_pager_dot.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/appholder/src/main/res/drawable/yellow_gradient.xml b/appholder/src/main/res/drawable/yellow_gradient.xml deleted file mode 100644 index 74d41f501..000000000 --- a/appholder/src/main/res/drawable/yellow_gradient.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/appholder/src/main/res/layout-land/fragment_share_document.xml b/appholder/src/main/res/layout-land/fragment_share_document.xml deleted file mode 100644 index eb330fdb5..000000000 --- a/appholder/src/main/res/layout-land/fragment_share_document.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - -