Repository: admin-ch/CovidCertificate-App-Android Branch: main Commit: 2578da419092 Files: 643 Total size: 2.9 MB Directory structure: gitextract_e7z7c8gy/ ├── .github/ │ ├── actions/ │ │ └── gradle_docker/ │ │ ├── action.yml │ │ └── main.sh │ └── workflows/ │ ├── appcenter_verifier_abn.yml │ ├── appcenter_verifier_dev.yml │ ├── appcenter_verifier_prod.yml │ ├── appcenter_verifier_prodfdroid.yml │ ├── appcenter_wallet_abn.yml │ ├── appcenter_wallet_dev.yml │ ├── appcenter_wallet_prod.yml │ ├── appcenter_wallet_prodfdroid.yml │ ├── browserstack_wallet_abn.yml │ ├── build.yml │ └── gradle-wrapper-validation.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── REPRODUCIBLE_BUILDS.md ├── apkdiff.py ├── build.gradle ├── buildAndCompare.sh ├── common/ │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── abn/ │ │ └── java/ │ │ └── ch/ │ │ └── admin/ │ │ └── bag/ │ │ └── covidcertificate/ │ │ └── common/ │ │ └── debug/ │ │ └── DebugFragment.kt │ ├── dev/ │ │ ├── java/ │ │ │ └── ch/ │ │ │ └── admin/ │ │ │ └── bag/ │ │ │ └── covidcertificate/ │ │ │ └── common/ │ │ │ └── debug/ │ │ │ ├── DebugFragment.kt │ │ │ └── DebugSecureStorage.kt │ │ └── res/ │ │ └── xml/ │ │ └── network_security_config.xml │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ └── impressum/ │ │ │ ├── de/ │ │ │ │ ├── impressum.html │ │ │ │ └── licence.html │ │ │ ├── en/ │ │ │ │ ├── impressum.html │ │ │ │ └── licence.html │ │ │ ├── fonts/ │ │ │ │ ├── Inter-Bold.otf │ │ │ │ └── Inter-Light.otf │ │ │ ├── fr/ │ │ │ │ ├── impressum.html │ │ │ │ └── licence.html │ │ │ ├── it/ │ │ │ │ ├── impressum.html │ │ │ │ └── licence.html │ │ │ └── rm/ │ │ │ ├── impressum.html │ │ │ └── licence.html │ │ ├── java/ │ │ │ └── ch/ │ │ │ └── admin/ │ │ │ └── bag/ │ │ │ └── covidcertificate/ │ │ │ └── common/ │ │ │ ├── BaseActivity.kt │ │ │ ├── browserstack/ │ │ │ │ ├── AirplaneMode.kt │ │ │ │ ├── BadNetwork.kt │ │ │ │ ├── Normal.kt │ │ │ │ └── Onboarding.kt │ │ │ ├── config/ │ │ │ │ ├── CertificateRenewalInfoDetailModel.kt │ │ │ │ ├── CertificateRenewalInfoModel.kt │ │ │ │ ├── CertificateRenewalType.kt │ │ │ │ ├── CheckModesInfosModel.kt │ │ │ │ ├── ConfigModel.kt │ │ │ │ ├── ConfigViewModel.kt │ │ │ │ ├── CovidCertificateNewsItem.kt │ │ │ │ ├── EolBannerInfoModel.kt │ │ │ │ ├── FaqEntryModel.kt │ │ │ │ ├── FaqIntroSection.kt │ │ │ │ ├── FaqModel.kt │ │ │ │ ├── ForeignRulesHintModel.kt │ │ │ │ ├── InfoBoxModel.kt │ │ │ │ ├── InfoCovidCertificateNews.kt │ │ │ │ ├── RefreshButtonInfoModel.kt │ │ │ │ ├── VaccinationBookingInfoModel.kt │ │ │ │ └── VaccinationHintModel.kt │ │ │ ├── data/ │ │ │ │ └── ConfigSecureStorage.kt │ │ │ ├── dialog/ │ │ │ │ └── InfoDialogFragment.kt │ │ │ ├── exception/ │ │ │ │ ├── HttpIOException.kt │ │ │ │ └── TimeDeviationException.kt │ │ │ ├── extensions/ │ │ │ │ ├── ContextExtensions.kt │ │ │ │ ├── DccCertExtensions.kt │ │ │ │ ├── LifecycleOwnerExtensions.kt │ │ │ │ ├── OkHttpExtensions.kt │ │ │ │ └── WindowExtensions.kt │ │ │ ├── faq/ │ │ │ │ ├── FaqAdapter.kt │ │ │ │ ├── FaqFragment.kt │ │ │ │ ├── FaqItem.kt │ │ │ │ ├── FaqViewHolder.kt │ │ │ │ └── model/ │ │ │ │ └── Header.kt │ │ │ ├── html/ │ │ │ │ ├── BuildInfo.kt │ │ │ │ └── ImprintFragment.kt │ │ │ ├── net/ │ │ │ │ ├── ConfigRepository.kt │ │ │ │ └── ConfigService.kt │ │ │ ├── onboarding/ │ │ │ │ ├── BaseOnboardingActivity.kt │ │ │ │ └── SimpleOnboardingPagerAdapter.kt │ │ │ ├── qr/ │ │ │ │ ├── CameraPermissionExplanationDialog.kt │ │ │ │ ├── KittlerBinarizer.kt │ │ │ │ ├── QRCodeReaderHelper.kt │ │ │ │ ├── QrScanFragment.kt │ │ │ │ └── QrScannerState.kt │ │ │ ├── settings/ │ │ │ │ └── SettingsFragment.kt │ │ │ ├── util/ │ │ │ │ ├── AssetUtil.kt │ │ │ │ ├── CutOutEdgeTreatment.kt │ │ │ │ ├── EnvironmentUtil.kt │ │ │ │ ├── ErrorCodeUtil.kt │ │ │ │ ├── ErrorHelper.kt │ │ │ │ ├── ErrorState.kt │ │ │ │ ├── HorizontalMarginItemDecoration.kt │ │ │ │ ├── LocaleUtil.kt │ │ │ │ ├── SingleLiveEvent.java │ │ │ │ ├── StringUtil.kt │ │ │ │ ├── UiUtil.kt │ │ │ │ ├── UlTagHandler.java │ │ │ │ └── UrlUtil.java │ │ │ └── views/ │ │ │ ├── MarginItemDecoration.kt │ │ │ ├── ViewExtensions.kt │ │ │ └── WindowInsetsLayout.kt │ │ └── res/ │ │ ├── anim/ │ │ │ ├── fragment_open_enter.xml │ │ │ ├── fragment_open_exit.xml │ │ │ ├── slide_enter.xml │ │ │ ├── slide_exit.xml │ │ │ ├── slide_pop_enter.xml │ │ │ └── slide_pop_exit.xml │ │ ├── color/ │ │ │ ├── selector_black_or_white.xml │ │ │ ├── selector_blue_or_white.xml │ │ │ ├── selector_grey_or_blue.xml │ │ │ ├── selector_grey_or_white.xml │ │ │ ├── selector_transparent_or_blue.xml │ │ │ ├── selector_white_or_black.xml │ │ │ ├── selector_white_or_blue.xml │ │ │ └── text_radio_checkable.xml │ │ ├── drawable/ │ │ │ ├── bg_button_default.xml │ │ │ ├── bg_button_red.xml │ │ │ ├── bg_button_white.xml │ │ │ ├── bg_corners_top_left.xml │ │ │ ├── bg_dialog.xml │ │ │ ├── bg_pill.xml │ │ │ ├── bg_rect_rounded_sheet.xml │ │ │ ├── bg_rect_rounded_small.xml │ │ │ ├── bg_rect_rounded_small_blue_ripple.xml │ │ │ ├── bg_rect_rounded_small_checkable.xml │ │ │ ├── bg_rect_rounded_small_red.xml │ │ │ ├── btn_radio_checkable.xml │ │ │ ├── dot_black.xml │ │ │ ├── dot_grey.xml │ │ │ ├── dot_white.xml │ │ │ ├── header_bottom.xml │ │ │ ├── header_collapsed_shadow.xml │ │ │ ├── ic_1g.xml │ │ │ ├── ic_2g.xml │ │ │ ├── ic_2g_green.xml │ │ │ ├── ic_2g_grey.xml │ │ │ ├── ic_2g_plus.xml │ │ │ ├── ic_3g.xml │ │ │ ├── ic_arrow_contract.xml │ │ │ ├── ic_arrow_expand.xml │ │ │ ├── ic_arrow_forward.xml │ │ │ ├── ic_bund_small.xml │ │ │ ├── ic_bundwappen_big.xml │ │ │ ├── ic_call.xml │ │ │ ├── ic_cam_off.xml │ │ │ ├── ic_camera_switch.xml │ │ │ ├── ic_check_filled.xml │ │ │ ├── ic_check_green.xml │ │ │ ├── ic_check_grey.xml │ │ │ ├── ic_check_large.xml │ │ │ ├── ic_checkbox_empty.xml │ │ │ ├── ic_checkbox_filled.xml │ │ │ ├── ic_close.xml │ │ │ ├── ic_close_red.xml │ │ │ ├── ic_dot.xml │ │ │ ├── ic_double_check.xml │ │ │ ├── ic_error.xml │ │ │ ├── ic_error_blue.xml │ │ │ ├── ic_error_grey.xml │ │ │ ├── ic_error_large.xml │ │ │ ├── ic_error_orange.xml │ │ │ ├── ic_error_triangle.xml │ │ │ ├── ic_expire_i.xml │ │ │ ├── ic_faq.xml │ │ │ ├── ic_header_slim.xml │ │ │ ├── ic_how_it_works_image.xml │ │ │ ├── ic_info.xml │ │ │ ├── ic_info_alert.xml │ │ │ ├── ic_info_blue.xml │ │ │ ├── ic_info_outline.xml │ │ │ ├── ic_invalid_grey.xml │ │ │ ├── ic_invalid_red.xml │ │ │ ├── ic_light_off.xml │ │ │ ├── ic_light_off_blue.xml │ │ │ ├── ic_light_on.xml │ │ │ ├── ic_light_on_black.xml │ │ │ ├── ic_link_external.xml │ │ │ ├── ic_load.xml │ │ │ ├── ic_no1g.xml │ │ │ ├── ic_no2g.xml │ │ │ ├── ic_no3g.xml │ │ │ ├── ic_no_2_g_plus_height.xml │ │ │ ├── ic_no_connection.xml │ │ │ ├── ic_no_connection_large.xml │ │ │ ├── ic_notification.xml │ │ │ ├── ic_notification_filled.xml │ │ │ ├── ic_offline.xml │ │ │ ├── ic_offline_large.xml │ │ │ ├── ic_offline_orange.xml │ │ │ ├── ic_one.xml │ │ │ ├── ic_phone.xml │ │ │ ├── ic_plus.xml │ │ │ ├── ic_plus_green.xml │ │ │ ├── ic_privacy.xml │ │ │ ├── ic_privacy_grey.xml │ │ │ ├── ic_process_error.xml │ │ │ ├── ic_process_error_grey.xml │ │ │ ├── ic_process_error_large.xml │ │ │ ├── ic_qr_certificate_light.xml │ │ │ ├── ic_qr_certificate_light_no.xml │ │ │ ├── ic_question_outline.xml │ │ │ ├── ic_retry.xml │ │ │ ├── ic_scanner_alert.xml │ │ │ ├── ic_scanner_alert_white.xml │ │ │ ├── ic_settings.xml │ │ │ ├── ic_t.xml │ │ │ ├── ic_three.xml │ │ │ ├── ic_timeerror.xml │ │ │ ├── ic_timeerror_large.xml │ │ │ ├── ic_timeerror_orange.xml │ │ │ ├── ic_timelapse.xml │ │ │ ├── ic_timelapse_blue.xml │ │ │ ├── ic_timelapse_red.xml │ │ │ ├── ic_travel.xml │ │ │ ├── ic_two.xml │ │ │ ├── ic_zoom_off.xml │ │ │ ├── ic_zoom_off_white.xml │ │ │ ├── ic_zoom_on.xml │ │ │ ├── ic_zoom_on_black.xml │ │ │ ├── illu_onboarding_data_protection.xml │ │ │ ├── line_dashed_grey.xml │ │ │ ├── qr_scanner_bottom_left.xml │ │ │ ├── qr_scanner_bottom_right.xml │ │ │ ├── qr_scanner_top_left.xml │ │ │ ├── qr_scanner_top_right.xml │ │ │ ├── ripple_rect.xml │ │ │ ├── ripple_rounded.xml │ │ │ ├── ripple_rounded_button.xml │ │ │ ├── ripple_rounded_rect.xml │ │ │ ├── ripple_rounded_rect_banner.xml │ │ │ ├── ripple_rounded_rect_small.xml │ │ │ ├── tab_selector.xml │ │ │ └── tab_selector_white.xml │ │ ├── font/ │ │ │ └── inter.xml │ │ ├── layout/ │ │ │ ├── activity_onboarding.xml │ │ │ ├── dialog_camera_permission_explanation.xml │ │ │ ├── dialog_fragment_info_box.xml │ │ │ ├── fragment_debug.xml │ │ │ ├── fragment_faq.xml │ │ │ ├── fragment_html.xml │ │ │ ├── fragment_settings.xml │ │ │ ├── item_error_status.xml │ │ │ ├── item_faq_header.xml │ │ │ ├── item_faq_intro_section.xml │ │ │ ├── item_faq_question.xml │ │ │ ├── item_header.xml │ │ │ └── item_language_option.xml │ │ ├── menu/ │ │ │ └── imprint.xml │ │ ├── values/ │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ ├── values-de/ │ │ │ └── strings.xml │ │ ├── values-fr/ │ │ │ └── strings.xml │ │ ├── values-it/ │ │ │ └── strings.xml │ │ ├── values-rm/ │ │ │ └── strings.xml │ │ └── xml/ │ │ └── network_security_config.xml │ └── prod/ │ └── java/ │ └── ch/ │ └── admin/ │ └── bag/ │ └── covidcertificate/ │ └── common/ │ └── debug/ │ └── DebugFragment.kt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── loadConfigs.sh ├── settings.gradle ├── verifier/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── src/ │ │ ├── abn/ │ │ │ └── assets/ │ │ │ └── faq/ │ │ │ └── config.json │ │ ├── dev/ │ │ │ └── assets/ │ │ │ └── faq/ │ │ │ └── config.json │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ └── faq/ │ │ │ └── config.json │ │ ├── java/ │ │ │ └── ch/ │ │ │ └── admin/ │ │ │ └── bag/ │ │ │ └── covidcertificate/ │ │ │ └── verifier/ │ │ │ ├── HomeFragment.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainApplication.kt │ │ │ ├── data/ │ │ │ │ └── VerifierSecureStorage.kt │ │ │ ├── extensions/ │ │ │ │ └── ContextExtensions.kt │ │ │ ├── faq/ │ │ │ │ └── VerifierFaqFragment.kt │ │ │ ├── modes/ │ │ │ │ ├── ChooseModeDialogFragment.kt │ │ │ │ └── ModesAndConfigViewModel.kt │ │ │ ├── news/ │ │ │ │ └── InfoCertificateNewsFragment.kt │ │ │ ├── pager/ │ │ │ │ ├── HomescreenPageAdapter.kt │ │ │ │ └── HomescreenPagerFragment.kt │ │ │ ├── qr/ │ │ │ │ └── VerifierQrScanFragment.kt │ │ │ ├── updateboarding/ │ │ │ │ ├── UpdateboardingActivity.kt │ │ │ │ ├── UpdateboardingAgbFragment.kt │ │ │ │ └── UpdateboardingCertificateLightFragment.kt │ │ │ ├── verification/ │ │ │ │ ├── VerificationAdapter.kt │ │ │ │ ├── VerificationFragment.kt │ │ │ │ ├── VerificationItem.kt │ │ │ │ ├── VerificationStateUtil.kt │ │ │ │ ├── VerificationViewHolder.kt │ │ │ │ └── VerificationViewModel.kt │ │ │ └── zebra/ │ │ │ ├── ZebraActionBroadcastReceiver.kt │ │ │ ├── ZebraDataWedgeApiUtil.kt │ │ │ └── ZebraResultActionBroadcastReceiver.kt │ │ ├── play/ │ │ │ ├── listings/ │ │ │ │ ├── de-DE/ │ │ │ │ │ ├── full-description.txt │ │ │ │ │ ├── short-description.txt │ │ │ │ │ └── title.txt │ │ │ │ ├── en-US/ │ │ │ │ │ ├── full-description.txt │ │ │ │ │ ├── short-description.txt │ │ │ │ │ └── title.txt │ │ │ │ ├── fr-FR/ │ │ │ │ │ ├── full-description.txt │ │ │ │ │ ├── short-description.txt │ │ │ │ │ └── title.txt │ │ │ │ └── it-IT/ │ │ │ │ ├── full-description.txt │ │ │ │ ├── short-description.txt │ │ │ │ └── title.txt │ │ │ └── release-notes/ │ │ │ └── en-US/ │ │ │ └── default.txt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── bg_bubble_bottom_left.xml │ │ │ ├── ic_faq_image.xml │ │ │ ├── ic_header_2g_off.xml │ │ │ ├── ic_header_2g_on.xml │ │ │ ├── ic_header_plus_off.xml │ │ │ ├── ic_header_plus_on.xml │ │ │ ├── ic_illu_home_1.xml │ │ │ ├── ic_illu_home_2.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_settings.xml │ │ │ └── illu_updateboarding_certificate_light.xml │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── dialog_fragment_choose_mode.xml │ │ │ ├── dialog_fragment_info_certificate_news.xml │ │ │ ├── fragment_home.xml │ │ │ ├── fragment_home_screen_pager.xml │ │ │ ├── fragment_qr_scan.xml │ │ │ ├── fragment_updateboarding_agb.xml │ │ │ ├── fragment_updateboarding_certificate_light.xml │ │ │ ├── fragment_verification.xml │ │ │ ├── item_certificate_news.xml │ │ │ ├── item_mode_button.xml │ │ │ ├── item_mode_info.xml │ │ │ ├── item_progress_indicator.xml │ │ │ ├── item_verification_header_icon.xml │ │ │ ├── item_verification_status.xml │ │ │ └── item_verification_status_info.xml │ │ └── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ └── testKeystore └── wallet/ ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── src/ │ ├── abn/ │ │ ├── assets/ │ │ │ └── faq/ │ │ │ └── config.json │ │ └── java/ │ │ └── ch/ │ │ └── admin/ │ │ └── bag/ │ │ └── covidcertificate/ │ │ └── wallet/ │ │ └── debug/ │ │ └── WalletDebugFragment.kt │ ├── androidTest/ │ │ └── java/ │ │ └── ch/ │ │ └── admin/ │ │ └── bag/ │ │ └── covidcertificate/ │ │ └── wallet/ │ │ ├── EspressoUtil.kt │ │ ├── LoadconfigTest.kt │ │ ├── NestedScrollViewScrollTo.kt │ │ ├── OnboardingTest.kt │ │ ├── RecyclerViewNotEmptyAssertion.kt │ │ ├── ScanCountTest.kt │ │ ├── TransferCodeTest.kt │ │ └── WaitUntilVisibleAction.kt │ ├── dev/ │ │ ├── assets/ │ │ │ └── faq/ │ │ │ └── config.json │ │ └── java/ │ │ └── ch/ │ │ └── admin/ │ │ └── bag/ │ │ └── covidcertificate/ │ │ └── wallet/ │ │ └── debug/ │ │ ├── DebugCertificateItem.kt │ │ ├── DebugCertificatesListAdapter.kt │ │ ├── DebugCertificatesListViewHolder.kt │ │ └── WalletDebugFragment.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ └── faq/ │ │ │ └── config.json │ │ ├── java/ │ │ │ └── ch/ │ │ │ └── admin/ │ │ │ └── bag/ │ │ │ └── covidcertificate/ │ │ │ └── wallet/ │ │ │ ├── CertificatesAndConfigViewModel.kt │ │ │ ├── DeeplinkViewModel.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainApplication.kt │ │ │ ├── add/ │ │ │ │ └── CertificateAddFragment.kt │ │ │ ├── data/ │ │ │ │ ├── CertificateStorage.kt │ │ │ │ ├── WalletDataItem.kt │ │ │ │ ├── WalletDataSecureStorage.kt │ │ │ │ ├── WalletSecureStorage.kt │ │ │ │ └── adapter/ │ │ │ │ └── InstantJsonAdapter.kt │ │ │ ├── detail/ │ │ │ │ ├── CertificateDetailAdapter.kt │ │ │ │ ├── CertificateDetailFragment.kt │ │ │ │ ├── CertificateDetailItem.kt │ │ │ │ ├── CertificateDetailItemListBuilder.kt │ │ │ │ └── CertificateDetailViewHolder.kt │ │ │ ├── dialog/ │ │ │ │ ├── CertificateBannerInfoDialogFragment.kt │ │ │ │ ├── ModeInfoDialogFragment.kt │ │ │ │ └── RefreshButtonInfoDialogFragment.kt │ │ │ ├── faq/ │ │ │ │ └── WalletFaqFragment.kt │ │ │ ├── homescreen/ │ │ │ │ ├── HomeFragment.kt │ │ │ │ └── pager/ │ │ │ │ ├── CertificatePagerFragment.kt │ │ │ │ ├── CertificatesPagerAdapter.kt │ │ │ │ ├── PagerDiffUtil.kt │ │ │ │ ├── StatefulWalletItem.kt │ │ │ │ ├── TransferCodePagerFragment.kt │ │ │ │ └── WalletItem.kt │ │ │ ├── howto/ │ │ │ │ └── HowToScanFragment.kt │ │ │ ├── light/ │ │ │ │ ├── CertificateLightConversionFragment.kt │ │ │ │ ├── CertificateLightDetailFragment.kt │ │ │ │ ├── CertificateLightErrorCodes.kt │ │ │ │ ├── CertificateLightPagerFragment.kt │ │ │ │ ├── CertificateLightViewModel.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── CertificateLightConversionResponse.kt │ │ │ │ │ └── CertificateLightConversionState.kt │ │ │ │ └── net/ │ │ │ │ ├── CertificateLightRepository.kt │ │ │ │ ├── CertificateLightRequestBody.kt │ │ │ │ ├── CertificateLightResponse.kt │ │ │ │ └── CertificateLightService.kt │ │ │ ├── list/ │ │ │ │ ├── CertificatesListFragment.kt │ │ │ │ ├── CertificatesListTouchHelper.kt │ │ │ │ ├── WalletDataListAdapter.kt │ │ │ │ ├── WalletDataListItem.kt │ │ │ │ └── WalletDataListViewHolder.kt │ │ │ ├── networking/ │ │ │ │ └── interceptor/ │ │ │ │ └── AcceptLanguageHeaderInterceptor.kt │ │ │ ├── onboarding/ │ │ │ │ ├── OnboardingActivity.kt │ │ │ │ ├── OnboardingAgbFragment.kt │ │ │ │ ├── OnboardingContentFragment.kt │ │ │ │ ├── OnboardingIntroFragment.kt │ │ │ │ ├── OnboardingPreInfoFragment.kt │ │ │ │ ├── agbupdate/ │ │ │ │ │ └── UpdateboardingAgbFragment.kt │ │ │ │ ├── certificatelight/ │ │ │ │ │ └── UpdateboardingCertificateLightFragment.kt │ │ │ │ └── validity/ │ │ │ │ ├── UpdateboardingValidity1Fragment.kt │ │ │ │ ├── UpdateboardingValidity2Fragment.kt │ │ │ │ ├── UpdateboardingValidity3Fragment.kt │ │ │ │ └── UpdateboardingValidity4Fragment.kt │ │ │ ├── pdf/ │ │ │ │ ├── PdfViewModel.kt │ │ │ │ ├── export/ │ │ │ │ │ ├── PdfExportFragment.kt │ │ │ │ │ ├── PdfExportShareContract.kt │ │ │ │ │ └── PdfExportState.kt │ │ │ │ └── net/ │ │ │ │ ├── PdfExportRepository.kt │ │ │ │ ├── PdfExportRequestBody.kt │ │ │ │ ├── PdfExportResponse.kt │ │ │ │ └── PdfExportService.kt │ │ │ ├── qr/ │ │ │ │ ├── VerifierInfoDialogFragment.kt │ │ │ │ └── WalletQrScanFragment.kt │ │ │ ├── ratconversion/ │ │ │ │ └── RatConversionFragment.kt │ │ │ ├── renewal/ │ │ │ │ ├── QrCodeRenewalErrorCodes.kt │ │ │ │ ├── QrCodeRenewalFragment.kt │ │ │ │ ├── QrCodeRenewalViewModel.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── QrCodeRenewalResponse.kt │ │ │ │ │ └── QrCodeRenewalViewState.kt │ │ │ │ └── net/ │ │ │ │ ├── QrCodeRenewalBody.kt │ │ │ │ ├── QrCodeRenewalRepository.kt │ │ │ │ └── QrCodeRenewalService.kt │ │ │ ├── transfercode/ │ │ │ │ ├── TransferCodeCreationFragment.kt │ │ │ │ ├── TransferCodeCreationViewModel.kt │ │ │ │ ├── TransferCodeDetailFragment.kt │ │ │ │ ├── TransferCodeErrorCodes.kt │ │ │ │ ├── TransferCodeHowToFragment.kt │ │ │ │ ├── TransferCodeIntroFragment.kt │ │ │ │ ├── TransferCodeViewModel.kt │ │ │ │ ├── logic/ │ │ │ │ │ ├── Luhn.kt │ │ │ │ │ └── TransferCodeCrypto.kt │ │ │ │ ├── model/ │ │ │ │ │ ├── ConvertedCertificate.kt │ │ │ │ │ ├── TransferCodeConversionState.kt │ │ │ │ │ ├── TransferCodeCreationResponse.kt │ │ │ │ │ ├── TransferCodeCreationState.kt │ │ │ │ │ └── TransferCodeModel.kt │ │ │ │ ├── net/ │ │ │ │ │ ├── CovidCert.kt │ │ │ │ │ ├── CovidCertDelivery.kt │ │ │ │ │ ├── DeliveryRegistration.kt │ │ │ │ │ ├── DeliveryRepository.kt │ │ │ │ │ ├── DeliveryService.kt │ │ │ │ │ └── RequestDeliveryPayload.kt │ │ │ │ ├── view/ │ │ │ │ │ ├── TransferCodeBubbleView.kt │ │ │ │ │ ├── TransferCodeView.kt │ │ │ │ │ └── TransferCodeWaitingView.kt │ │ │ │ └── worker/ │ │ │ │ └── TransferWorker.kt │ │ │ ├── travel/ │ │ │ │ ├── ForeignValidityFragment.kt │ │ │ │ ├── ForeignValidityViewModel.kt │ │ │ │ └── ForeignValidityViewState.kt │ │ │ ├── util/ │ │ │ │ ├── BitmapUtil.kt │ │ │ │ ├── ModeValidityStateUtil.kt │ │ │ │ ├── NotificationUtil.kt │ │ │ │ ├── QrCode.kt │ │ │ │ └── VerificationStateUtil.kt │ │ │ └── vaccination/ │ │ │ ├── appointment/ │ │ │ │ └── VaccinationAppointmentFragment.kt │ │ │ └── hint/ │ │ │ └── VaccinationHintViewModel.kt │ │ ├── play/ │ │ │ ├── listings/ │ │ │ │ ├── de-DE/ │ │ │ │ │ ├── full-description.txt │ │ │ │ │ ├── short-description.txt │ │ │ │ │ └── title.txt │ │ │ │ ├── en-US/ │ │ │ │ │ ├── full-description.txt │ │ │ │ │ ├── short-description.txt │ │ │ │ │ └── title.txt │ │ │ │ ├── fr-FR/ │ │ │ │ │ ├── full-description.txt │ │ │ │ │ ├── short-description.txt │ │ │ │ │ └── title.txt │ │ │ │ └── it-IT/ │ │ │ │ ├── full-description.txt │ │ │ │ ├── short-description.txt │ │ │ │ └── title.txt │ │ │ └── release-notes/ │ │ │ └── en-US/ │ │ │ └── default.txt │ │ └── res/ │ │ ├── color/ │ │ │ └── text_color_button.xml │ │ ├── drawable/ │ │ │ ├── bg_add_certificate_option.xml │ │ │ ├── bg_border.xml │ │ │ ├── bg_bordered_button.xml │ │ │ ├── bg_bubble_bottom_left.xml │ │ │ ├── bg_certificate_bottom.xml │ │ │ ├── bg_certificate_bubble.xml │ │ │ ├── bg_certificate_bubble_bundesrot.xml │ │ │ ├── bg_certificate_bubble_ripple.xml │ │ │ ├── bg_certificate_detail_note.xml │ │ │ ├── bg_certificate_top.xml │ │ │ ├── bg_info_banner.xml │ │ │ ├── bg_type_bubble_small.xml │ │ │ ├── circle_white.xml │ │ │ ├── cutout_left.xml │ │ │ ├── cutout_right.xml │ │ │ ├── ic_add_certificate.xml │ │ │ ├── ic_arrow_right.xml │ │ │ ├── ic_calendar.xml │ │ │ ├── ic_certificate_light.xml │ │ │ ├── ic_check_mark.xml │ │ │ ├── ic_cloud.xml │ │ │ ├── ic_corner_offline.xml │ │ │ ├── ic_corner_process_error.xml │ │ │ ├── ic_covid_check_app.xml │ │ │ ├── ic_data_protection.xml │ │ │ ├── ic_drag.xml │ │ │ ├── ic_exchange.xml │ │ │ ├── ic_expire_1.xml │ │ │ ├── ic_expire_2.xml │ │ │ ├── ic_expire_3.xml │ │ │ ├── ic_expire_4.xml │ │ │ ├── ic_expire_5.xml │ │ │ ├── ic_expire_6.xml │ │ │ ├── ic_expire_7.xml │ │ │ ├── ic_faq_image.xml │ │ │ ├── ic_flag_ch.xml │ │ │ ├── ic_header_slim.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ ├── ic_list.xml │ │ │ ├── ic_online.xml │ │ │ ├── ic_pdf.xml │ │ │ ├── ic_pen_write.xml │ │ │ ├── ic_qr_certificate.xml │ │ │ ├── ic_qrcode.xml │ │ │ ├── ic_qrcode_add.xml │ │ │ ├── ic_qrcode_icon_placeholder.xml │ │ │ ├── ic_qrcode_scan.xml │ │ │ ├── ic_scan_code.xml │ │ │ ├── ic_transfer_code_list_failed.xml │ │ │ ├── ic_transfer_code_list_valid.xml │ │ │ ├── ic_transfer_notification.xml │ │ │ ├── ic_validation.xml │ │ │ ├── icon_ag.xml │ │ │ ├── icon_ai.xml │ │ │ ├── icon_ar.xml │ │ │ ├── icon_be.xml │ │ │ ├── icon_bl.xml │ │ │ ├── icon_bs.xml │ │ │ ├── icon_fr.xml │ │ │ ├── icon_ge.xml │ │ │ ├── icon_gl.xml │ │ │ ├── icon_gr.xml │ │ │ ├── icon_ju.xml │ │ │ ├── icon_lu.xml │ │ │ ├── icon_ne.xml │ │ │ ├── icon_nw.xml │ │ │ ├── icon_ow.xml │ │ │ ├── icon_sg.xml │ │ │ ├── icon_sh.xml │ │ │ ├── icon_so.xml │ │ │ ├── icon_sz.xml │ │ │ ├── icon_tg.xml │ │ │ ├── icon_ti.xml │ │ │ ├── icon_ur.xml │ │ │ ├── icon_vd.xml │ │ │ ├── icon_vs.xml │ │ │ ├── icon_zg.xml │ │ │ ├── icon_zh.xml │ │ │ ├── illu_add_certificate.xml │ │ │ ├── illu_faq_transfer_code.xml │ │ │ ├── illu_home_empty_state.xml │ │ │ ├── illu_how_it_works.xml │ │ │ ├── illu_how_to_scan.xml │ │ │ ├── illu_onboarding_covid_certificate.xml │ │ │ ├── illu_onboarding_hero.xml │ │ │ ├── illu_onboarding_privacy.xml │ │ │ ├── illu_transfer_code_failed.xml │ │ │ ├── illu_transfer_code_intro.xml │ │ │ ├── illu_transfer_code_waiting_phone.xml │ │ │ ├── illu_transfer_code_waiting_ripple.xml │ │ │ ├── illu_transfer_code_waiting_shadow.xml │ │ │ ├── illu_updateboarding_certificate_light.xml │ │ │ └── oval_horizontal.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── activity_main.xml │ │ │ ├── dialog_fragment_certificate_banner_info.xml │ │ │ ├── dialog_fragment_mode_info.xml │ │ │ ├── dialog_fragment_refresh_button_info.xml │ │ │ ├── fragment_certificate_add.xml │ │ │ ├── fragment_certificate_detail.xml │ │ │ ├── fragment_certificate_light_conversion.xml │ │ │ ├── fragment_certificate_light_detail.xml │ │ │ ├── fragment_certificate_light_pager.xml │ │ │ ├── fragment_certificate_pager.xml │ │ │ ├── fragment_certificates_list.xml │ │ │ ├── fragment_foreign_validity.xml │ │ │ ├── fragment_home.xml │ │ │ ├── fragment_how_to_scan.xml │ │ │ ├── fragment_onboarding_agb.xml │ │ │ ├── fragment_onboarding_content.xml │ │ │ ├── fragment_onboarding_intro.xml │ │ │ ├── fragment_onboarding_pre_info.xml │ │ │ ├── fragment_pdf_export.xml │ │ │ ├── fragment_qr_code_renewal.xml │ │ │ ├── fragment_qr_scan.xml │ │ │ ├── fragment_rat_conversion.xml │ │ │ ├── fragment_transfer_code_creation.xml │ │ │ ├── fragment_transfer_code_detail.xml │ │ │ ├── fragment_transfer_code_howto.xml │ │ │ ├── fragment_transfer_code_intro.xml │ │ │ ├── fragment_transfer_code_pager.xml │ │ │ ├── fragment_updateboarding_agb.xml │ │ │ ├── fragment_updateboarding_certificate_light.xml │ │ │ ├── fragment_updateboarding_validity_1.xml │ │ │ ├── fragment_updateboarding_validity_2.xml │ │ │ ├── fragment_updateboarding_validity_3.xml │ │ │ ├── fragment_updateboarding_validity_4.xml │ │ │ ├── fragment_vaccination_appointment.xml │ │ │ ├── item_certificate_list.xml │ │ │ ├── item_debug_certificate_list.xml │ │ │ ├── item_detail_divider.xml │ │ │ ├── item_detail_mode.xml │ │ │ ├── item_detail_mode_refresh.xml │ │ │ ├── item_detail_modes_list.xml │ │ │ ├── item_detail_title.xml │ │ │ ├── item_detail_value.xml │ │ │ ├── item_detail_value_without_label.xml │ │ │ ├── item_foreign_rules_check_hint.xml │ │ │ ├── item_header_not_empty.xml │ │ │ ├── item_icon_text_info.xml │ │ │ ├── item_mode_list_info.xml │ │ │ ├── item_transfer_code_list.xml │ │ │ ├── item_vaccination_appointment_canton.xml │ │ │ ├── partial_certificate_detail_banners.xml │ │ │ ├── partial_home_add_certificate_options.xml │ │ │ ├── view_transfer_code.xml │ │ │ ├── view_transfer_code_bubble.xml │ │ │ └── view_transfer_code_waiting.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── xml/ │ │ └── filepaths.xml │ ├── prod/ │ │ └── java/ │ │ └── ch/ │ │ └── admin/ │ │ └── bag/ │ │ └── covidcertificate/ │ │ └── wallet/ │ │ └── debug/ │ │ └── WalletDebugFragment.kt │ └── test/ │ └── java/ │ └── ch/ │ └── admin/ │ └── bag/ │ └── covidcertificate/ │ └── wallet/ │ └── transfercode/ │ ├── LuhnTest.kt │ └── LuhnTestWrongCodes.kt └── testKeystore ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/actions/gradle_docker/action.yml ================================================ name: 'GradleDocker' description: 'Run gradle inside Docker image' inputs: gradle-cmd: # id of input description: 'gradleCmd to run' required: true runs: using: 'docker' image: '../../../Dockerfile' entrypoint: '.github/actions/gradle_docker/main.sh' args: - ${{ inputs.gradle-cmd }} ================================================ FILE: .github/actions/gradle_docker/main.sh ================================================ #!/bin/sh cd $GITHUB_WORKSPACE gradle $1 ================================================ FILE: .github/workflows/appcenter_verifier_abn.yml ================================================ name: Build Verifier ABN on: push: branches: - main - 'release/**' pull_request: branches: - main - 'release/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: verifier:assembleAbnRelease -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} - name: upload artefact to App Center uses: wzieba/AppCenter-Github-Action@8db6b765c4d7ce337bd783ea986f17ce0c9a9e85 with: appName: ${{secrets.APPCENTER_ORGANIZATION}}/${{secrets.APPCENTER_VERIFIER_APP_ABN}} token: ${{secrets.APPCENTER_API_TOKEN}} group: public file: verifier/build/outputs/apk/abn/release/verifier-abn-release.apk ================================================ FILE: .github/workflows/appcenter_verifier_dev.yml ================================================ name: Build Verifier DEV on: push: branches: - main - 'release/**' pull_request: branches: - main - 'release/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: verifier:assembleDevRelease -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} - name: upload artefact to App Center uses: wzieba/AppCenter-Github-Action@8db6b765c4d7ce337bd783ea986f17ce0c9a9e85 with: appName: ${{secrets.APPCENTER_ORGANIZATION}}/${{secrets.APPCENTER_VERIFIER_APP_DEV}} token: ${{secrets.APPCENTER_API_TOKEN}} group: public file: verifier/build/outputs/apk/dev/release/verifier-dev-release.apk ================================================ FILE: .github/workflows/appcenter_verifier_prod.yml ================================================ name: Build Verifier PROD on: push: branches: - main - 'release/**' pull_request: branches: - main - 'release/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: verifier:assembleProdRelease -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} - name: upload artefact to App Center uses: wzieba/AppCenter-Github-Action@8db6b765c4d7ce337bd783ea986f17ce0c9a9e85 with: appName: ${{secrets.APPCENTER_ORGANIZATION}}/${{secrets.APPCENTER_VERIFIER_APP}} token: ${{secrets.APPCENTER_API_TOKEN}} group: public file: verifier/build/outputs/apk/prod/release/verifier-prod-release.apk - name: Upload APK uses: actions/upload-artifact@v1.0.0 with: name: verifier.apk path: verifier/build/outputs/apk/prod/release/verifier-prod-release.apk ================================================ FILE: .github/workflows/appcenter_verifier_prodfdroid.yml ================================================ name: Build Verifier PRODFDROID on: push: branches: - main - 'release/**' pull_request: branches: - main - 'release/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: verifier:assembleProdRelease -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} -PminSdkVersion=24 - name: upload artefact to App Center uses: wzieba/AppCenter-Github-Action@8db6b765c4d7ce337bd783ea986f17ce0c9a9e85 with: appName: ${{secrets.APPCENTER_ORGANIZATION}}/${{secrets.APPCENTER_VERIFIER_APP}} token: ${{secrets.APPCENTER_API_TOKEN}} group: public file: verifier/build/outputs/apk/prod/release/verifier-prod-release.apk - name: Upload APK uses: actions/upload-artifact@v1.0.0 with: name: verifier.apk path: verifier/build/outputs/apk/prod/release/verifier-prod-release.apk ================================================ FILE: .github/workflows/appcenter_wallet_abn.yml ================================================ name: Build Wallet ABN on: push: branches: - main - 'release/**' pull_request: branches: - main - 'release/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: wallet:assembleAbnRelease -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} - name: upload artefact to App Center uses: wzieba/AppCenter-Github-Action@8db6b765c4d7ce337bd783ea986f17ce0c9a9e85 with: appName: ${{secrets.APPCENTER_ORGANIZATION}}/${{secrets.APPCENTER_WALLET_APP_ABN}} token: ${{secrets.APPCENTER_API_TOKEN}} group: public file: wallet/build/outputs/apk/abn/release/wallet-abn-release.apk ================================================ FILE: .github/workflows/appcenter_wallet_dev.yml ================================================ name: Build Wallet DEV on: push: branches: - main - 'release/**' pull_request: branches: - main - 'release/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: wallet:assembleDevRelease -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} - name: upload artefact to App Center uses: wzieba/AppCenter-Github-Action@8db6b765c4d7ce337bd783ea986f17ce0c9a9e85 with: appName: ${{secrets.APPCENTER_ORGANIZATION}}/${{secrets.APPCENTER_WALLET_APP_DEV}} token: ${{secrets.APPCENTER_API_TOKEN}} group: public file: wallet/build/outputs/apk/dev/release/wallet-dev-release.apk ================================================ FILE: .github/workflows/appcenter_wallet_prod.yml ================================================ name: Build Wallet PROD on: push: branches: - main - 'release/**' pull_request: branches: - main - 'release/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: wallet:assembleProdRelease -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} - name: upload artefact to App Center uses: wzieba/AppCenter-Github-Action@8db6b765c4d7ce337bd783ea986f17ce0c9a9e85 with: appName: ${{secrets.APPCENTER_ORGANIZATION}}/${{secrets.APPCENTER_WALLET_APP}} token: ${{secrets.APPCENTER_API_TOKEN}} group: public file: wallet/build/outputs/apk/prod/release/wallet-prod-release.apk - name: Upload APK uses: actions/upload-artifact@v1.0.0 with: name: wallet.apk path: wallet/build/outputs/apk/prod/release/wallet-prod-release.apk ================================================ FILE: .github/workflows/appcenter_wallet_prodfdroid.yml ================================================ name: Build Wallet PRODFDROID on: push: branches: - main - 'release/**' pull_request: branches: - main - 'release/**' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: wallet:assembleProdRelease -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} -PminSdkVersion=24 - name: upload artefact to App Center uses: wzieba/AppCenter-Github-Action@8db6b765c4d7ce337bd783ea986f17ce0c9a9e85 with: appName: ${{secrets.APPCENTER_ORGANIZATION}}/${{secrets.APPCENTER_WALLET_APP}} token: ${{secrets.APPCENTER_API_TOKEN}} group: public file: wallet/build/outputs/apk/prod/release/wallet-prod-release.apk - name: Upload APK uses: actions/upload-artifact@v1.0.0 with: name: wallet.apk path: wallet/build/outputs/apk/prod/release/wallet-prod-release.apk ================================================ FILE: .github/workflows/browserstack_wallet_abn.yml ================================================ name: Build Browserstack UI-Test Wallet on: push: branches: - main - 'release/**' pull_request: branches: - main - 'release/**' jobs: build-app-and-upload: runs-on: ubuntu-latest outputs: app-url: ${{ steps.upload-app.outputs.app-url }} steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: wallet:assembleAbnRelease -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} - name: upload artefact to Browserstack run: curl -u ${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESSKEY }} -X Post "https://api-cloud.browserstack.com/app-automate/espresso/v2/app" -F "file=@wallet/build/outputs/apk/abn/release/wallet-abn-release.apk" >> appResponse.json - id: upload-app run: echo ::set-output name=app-url::$(jq -r '.app_url' appResponse.json) build-test-suite-and-upload: runs-on: ubuntu-latest outputs: test-url: ${{ steps.upload-test-suite.outputs.test-url }} steps: - uses: actions/checkout@v2 with: submodules: true - name: Build with Gradle uses: ./.github/actions/gradle_docker with: gradle-cmd: wallet:assembleAbnDebugAndroidTest -PkeystorePassword=${{secrets.KEYSTORE_PASSWORD}} -PkeyAliasPassword=${{secrets.KEY_ALIAS_PASSWORD}} - name: upload artefact to Browserstack run: curl -u ${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESSKEY }} -X Post "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite" -F "file=@wallet/build/outputs/apk/androidTest/abn/debug/wallet-abn-debug-androidTest.apk" >> testSuiteResponse.json - id: upload-test-suite run: echo ::set-output name=test-url::$(jq -r '.test_suite_url' testSuiteResponse.json) run-tests: runs-on: ubuntu-latest needs: [build-app-and-upload, build-test-suite-and-upload] steps: - name: run normal tests run: curl -u ${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESSKEY }} -X Post "https://api-cloud.browserstack.com/app-automate/espresso/v2/build" -d '{"app":"${{ needs.build-app-and-upload.outputs.app-url }}", "testSuite":"${{ needs.build-test-suite-and-upload.outputs.test-url }}", "devices":["Samsung Galaxy S10e-9.0"] , "language":"de", "annotation":["ch.admin.bag.covidcertificate.common.browserstack.Normal"] }' -H "Content-Type:application/json" - name: run airplane tests run: curl -u ${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESSKEY }} -X Post "https://api-cloud.browserstack.com/app-automate/espresso/v2/build" -d '{"app":"${{ needs.build-app-and-upload.outputs.app-url }}", "testSuite":"${{ needs.build-test-suite-and-upload.outputs.test-url }}", "devices":["Samsung Galaxy S10e-9.0"] , "networkProfile":"airplane-mode", "annotation":["ch.admin.bag.covidcertificate.common.browserstack.AirplaneMode"] }' -H "Content-Type:application/json" - name: run bad network tests run: curl -u ${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESSKEY }} -X Post "https://api-cloud.browserstack.com/app-automate/espresso/v2/build" -d '{"app":"${{ needs.build-app-and-upload.outputs.app-url }}", "testSuite":"${{ needs.build-test-suite-and-upload.outputs.test-url }}", "devices":["Samsung Galaxy S10e-9.0"] , "networkProfile":"2g-gprs-lossy", "annotation":["ch.admin.bag.covidcertificate.common.browserstack.BadNetwork"] }' -H "Content-Type:application/json" - name: run onboarding test run: curl -u ${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESSKEY }} -X Post "https://api-cloud.browserstack.com/app-automate/espresso/v2/build" -d '{"app":"${{ needs.build-app-and-upload.outputs.app-url }}", "testSuite":"${{ needs.build-test-suite-and-upload.outputs.test-url }}", "devices":["Samsung Galaxy S10e-9.0"] , "language":"de", "annotation":["ch.admin.bag.covidcertificate.common.browserstack.Onboarding"] }' -H "Content-Type:application/json" - name: run onboarding test offline run: curl -u ${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESSKEY }} -X Post "https://api-cloud.browserstack.com/app-automate/espresso/v2/build" -d '{"app":"${{ needs.build-app-and-upload.outputs.app-url }}", "testSuite":"${{ needs.build-test-suite-and-upload.outputs.test-url }}", "devices":["Samsung Galaxy S10e-9.0"] , "language":"de", "networkProfile":"airplane-mode", "annotation":["ch.admin.bag.covidcertificate.common.browserstack.Onboarding"] }' -H "Content-Type:application/json" ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: - main - 'release/**' pull_request: jobs: build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 - name: Cache SonarCloud packages uses: actions/cache@v1 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Gradle packages uses: actions/cache@v1 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: ${{ runner.os }}-gradle - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: ./gradlew sonarqube ================================================ FILE: .github/workflows/gradle-wrapper-validation.yml ================================================ name: Validate Gradle Wrapper on: [push, pull_request] jobs: validation: name: Validation runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true - uses: gradle/wrapper-validation-action@84d7e182ae7c7a37f200c184f64038fb0e62dd7d ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.aar *.ap_ *.aab # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Uncomment the following line in case you need and you don't have the release build type files in your app # release/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/assetWizardSettings.xml .idea/dictionaries .idea/libraries # Android Studio 3 in .gitignore file. .idea/caches .idea/modules.xml # Comment next line if keeping position of elements in Navigation Editor is relevant for you .idea/navEditor.xml # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. #*.jks #*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild .cxx/ # Google Services (e.g. APIs or Firebase) # google-services.json # Freeline freeline.py freeline/ freeline_project_description.json # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md # Version control vcs.xml # lint lint/intermediates/ lint/generated/ lint/outputs/ lint/tmp/ # lint/reports/ .idea/misc.xml .idea/ .DS_Store ================================================ FILE: Dockerfile ================================================ FROM gradle:7.2-jdk11 ENV ANDROID_SDK_URL https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip ENV ANDROID_BUILD_TOOLS_VERSION 30.0.1 ENV ANDROID_HOME /usr/local/android-sdk-linux ENV ANDROID_VERSION 30 ENV PATH ${PATH}:${ANDROID_HOME}/cmdline-tools/bin:${ANDROID_HOME}/platform-tools RUN mkdir "$ANDROID_HOME" .android && \ cd "$ANDROID_HOME" && \ curl -o sdk.zip $ANDROID_SDK_URL && \ unzip sdk.zip && \ rm sdk.zip RUN yes | ${ANDROID_HOME}/cmdline-tools/bin/sdkmanager --sdk_root=$ANDROID_HOME --licenses RUN $ANDROID_HOME/cmdline-tools/bin/sdkmanager --sdk_root=$ANDROID_HOME --update RUN $ANDROID_HOME/cmdline-tools/bin/sdkmanager --sdk_root=$ANDROID_HOME "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \ "platforms;android-${ANDROID_VERSION}" \ "platform-tools" RUN apt-get update RUN apt-get install -y imagemagick RUN convert -version ================================================ FILE: LICENSE ================================================ Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================ # COVID Certificate Apps - Android [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://github.com/admin-ch/CovidCertificate-App-Android/blob/main/LICENSE) ![Android Build Wallet](https://github.com/admin-ch/CovidCertificate-App-Android/actions/workflows/appcenter_wallet_dev.yml/badge.svg) ![Android Build Verifier](https://github.com/admin-ch/CovidCertificate-App-Android/actions/workflows/appcenter_verifier_dev.yml/badge.svg) This project is released by the the [Federal Office of Information Technology, Systems and Telecommunication FOITT](https://www.bit.admin.ch/) on behalf of the [Federal Office of Public Health FOPH](https://www.bag.admin.ch/). The app design, UX and implementation was done by [Ubique](https://www.ubique.ch?app=github). ## COVID Certificate App COVID Certificate is the official app for storing and presenting COVID certificates issued in Switzerland. The certificates are kept and checked locally on the user's phone.

Get it on Google Play Get it on F-Droid Explore it on AppGallery

## COVID Certificate Check App COVID Certificate Check is the official app for checking COVID certificates in Switzerland. The validation is executed locally on the phone and no information of the scanned certificates is saved.

Get it on Google Play Get it on F-Droid Explore it on AppGallery

## Contribution Guide This project is truly open-source and we welcome any feedback on the code regarding both the implementation and security aspects. Bugs or potential problems should be reported using Github issues. We welcome all pull requests that improve the quality of the source code. Please note that the app will be available with approved translations in English, German, French, Italian, Rumantsch. ## Repositories * Android App: [CovidCertificate-App-Android](https://github.com/admin-ch/CovidCertificate-App-Android) * Android SDK: [CovidCertificate-SDK-Android](https://github.com/admin-ch/CovidCertificate-SDK-Android) * iOS App: [CovidCertificate-App-iOS](https://github.com/admin-ch/CovidCertificate-App-iOS) * For all others, see the [Github organisation](https://github.com/admin-ch/) ## Installation and Building The apps require at least Android 6 (Marshmallow). To build the project you need at least Java 11 and Android Studio 2020.3.1. You can also build the apps directly: ```sh $ ./gradlew verifier:assembleProdRelease $ ./gradlew wallet:assembleProdRelease ``` Note that in order for that to work, you must have set up your own keystore. The APK is generated under `app/build/outputs/apk/prod/release/app-prod-release.apk` where `app` is one of: `verifier`, `wallet`. ## Reproducible builds To verify that the app distributed on the Play Store was built by the source code published here, please see the instructions in [REPRODUCIBLE_BUILDS.md](REPRODUCIBLE_BUILDS.md). ## License This project is licensed under the terms of the MPL 2 license. See the [LICENSE](LICENSE) file for details. ================================================ FILE: REPRODUCIBLE_BUILDS.md ================================================ # Reproducible Builds This document outlines how you can reproduce the Android app. The instructions below are for the wallet app. To reproduce the verifier app, change **all** occurences of `wallet` to `verifier`. ## Prerequisites 1. Make sure you have both [Docker](https://www.docker.com/) and `git` installed. 2. Clone the repository 3. Checkout the tag (or branch or commit) that corresponds to the version of your app (e.g., 1.0.0) ```shell git clone https://github.com/admin-ch/CovidCertificate-App-Android.git ~/CovidCertificate-App-Android cd ~/CovidCertificate-App-Android git tag # List all available tags git checkout v2.7.0-2700-wallet ``` ## Verifying the app ### Step 1: Check your app version and build timestamp 1. Open the app 2. Click on the `i` button in the top-right corner 3. Check the app version in the top right corner 4. Check the build timestamp in the bottom right corner, which is the number before the slash (e.g., 1622186583268), and record its value to be used later ### Step 2: Extract the APK from your device 1. Make sure you have `adb` installed 2. Connect your phone to your computer 3. Extract the APK from the phone: ```shell adb pull `adb shell pm path ch.admin.bag.covidcertificate.wallet | cut -d':' -f2` wallet-store.apk ``` If you want to check the version of the APK you are pulling from your device: ```shell adb shell dumpsys package ch.admin.bag.covidcertificate.wallet | grep versionName=| cut -d '=' -f 2 ``` ### Step 3: Reproduce it TLDR: Run the script and follow its instructions: ```shell ./buildAndCompare.sh wallet-store.apk ``` The script will do the following: 1. Build a Docker image with the required Android tools 2. (Optionally) Generate a dummy key store for signing 3. Build the app from source in the Docker container 4. Compare the APK pulled from your phone with the APK built from source To manually compare to files you can run: ```shell python3 apkdiff.py wallet-built.apk wallet-store.apk ``` ================================================ FILE: apkdiff.py ================================================ # Taken from https://github.com/DrKLO/Telegram/blob/master/apkdiff.py on June 4th, 2020 import sys from zipfile import ZipFile def compareFiles(first, second): while True: firstBytes = first.read(4096); secondBytes = second.read(4096); if firstBytes != secondBytes: return False if firstBytes == b"": break return True def compare(first, second): FILES_TO_IGNORE = ["META-INF/MANIFEST.MF", "META-INF/CERT.RSA", "META-INF/CERT.SF"] firstZip = ZipFile(first, 'r') secondZip = ZipFile(second, 'r') firstList = list(filter(lambda firstInfo: firstInfo.filename not in FILES_TO_IGNORE, firstZip.infolist())) secondList = list(filter(lambda secondInfo: secondInfo.filename not in FILES_TO_IGNORE, secondZip.infolist())) apksAreTheSame = True if len(firstList) != len(secondList): print("APKs has different amount of files (%d != %d)" % (len(firstList), len(secondList))) apksAreTheSame = False for firstInfo in firstList: found = False for secondInfo in secondList: if firstInfo.filename == secondInfo.filename: found = True firstFile = firstZip.open(firstInfo, 'r') secondFile = secondZip.open(secondInfo, 'r') if compareFiles(firstFile, secondFile) != True: print("APK file %s does not match" % firstInfo.filename) apksAreTheSame = False secondList.remove(secondInfo) break if found == False: print("file %s not found in second APK" % firstInfo.filename) apksAreTheSame = False if len(secondList) != 0: for secondInfo in secondList: print("file %s not found in first APK" % secondInfo.filename) apksAreTheSame = False return apksAreTheSame if __name__ == '__main__': if len(sys.argv) != 3: print("Usage: apkdiff ") sys.exit(1) if sys.argv[1] == sys.argv[2] or compare(sys.argv[1], sys.argv[2]) == True: print("APKs are the same!") else: print("APKs are different!") ================================================ FILE: build.gradle ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = "1.6.10" repositories { google() mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath 'com.android.tools:r8:3.0.73' classpath 'com.android.tools.build:gradle:7.0.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath 'ch.ubique.gradle:ubdiag-android:7.0.2' } } allprojects { repositories { google() mavenCentral() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: buildAndCompare.sh ================================================ #!/bin/bash # Script to automate the building and comparing of the CovidCertificate apps # # The first and only argument should be the path to the APK to be compared. set -eu if [[ $# -ne 1 ]]; then echo "Pass the path/to/store.apk as an argument!" exit 1 fi referenceApk=$1 echo "Which app would you like to build ('wallet' or 'verifier')?" read appName echo "Do you want to provide a keystore [Yn]?" read willProvideKeystore case "$willProvideKeystore" in # Case 1: Autogenerate a dummy keystore [nN][oO]|[nN]) echo "[WARNING] Auto-generating a dummy keystore with default credentials. Do NOT use the resulting APK!" rm -f "$appName"/insecure.keystore # Generate a keystore with default credentials that is only valid 1 day keytool -genkeypair -storepass password -keypass password -alias keyAlias -keyalg RSA -keystore "$appName"/insecure.keystore -dname 'CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown' -validity 1 keystoreFile="$appName"/insecure.keystore keystorePassword=password keyAlias=keyAlias keyAliasPassword=password ;; # Case 2: Let the user choose a keystore *) echo "Please enter the keystore filename (e.g. wallet/build.keystore):" read keystoreFile echo "Please enter the keystore password:" read -s keystorePassword echo "Please enter the keyAlias:" read keyAlias echo "Please enter the keyAlias password:" read -s keyAliasPassword ;; esac echo "Please enter the build timestamp (e.g. 1622186583268):" read buildTimestamp # This is necessary because Ubique's gradle plugin will automatically set the branch. # Here we want to override it in order to reproduce the build. echo "Please enter the branch off which the release was build (e.g. release/version-1.0.0):" read buildBranch echo "Please enter the git tag (e.g. v2.7.0-2700-wallet) or branch (e.g. release/version-1.0.0) to be reproduced." echo "This is what will be checked out and reproduced." read tree echo "Do you want to build the F-Droid version [yN]?" read fdroid # Set gradle task to be run case "$fdroid" in [yY][eE][sS]|[yY]) overrideMinSdk="-PminSdkVersion=24" ;; *) overrideMinSdk="" ;; esac echo "Building apk from source..." # Clean up any existing images docker images -a | grep "covidcertificate-builder" | awk '{print $3}' | xargs -r docker rmi # Build a fresh container image docker build -t covidcertificate-builder . # Prepare the build command (for readability) currentPath=`pwd` buildCommand=$(cat < * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ plugins { id 'com.android.library' id 'kotlin-android' id 'com.google.devtools.ksp' version '1.6.10-1.0.4' } ext.readPropertyWithDefault = { paramName, defaultValue -> if (project.hasProperty(paramName)) { return project.getProperties().get(paramName) } else { Properties properties = new Properties() if (project.rootProject.file('local.properties').exists()) { properties.load(project.rootProject.file('local.properties').newDataInputStream()) } if (properties.getProperty(paramName) != null) { return properties.getProperty(paramName) } else { return defaultValue } } } android { compileSdkVersion 31 defaultConfig { minSdkVersion readPropertyWithDefault('minSdkVersion', '23').toInteger() targetSdkVersion 31 // Stops the Gradle plugin’s automatic rasterization of vectors generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" } productFlavors { dev {} abn {} prod {} } flavorDimensions "version" buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } buildFeatures { viewBinding true } } dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' api 'ch.admin.bag.covidcertificate:sdk-android:2.3.3' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.1' api 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' implementation 'androidx.fragment:fragment-ktx:1.4.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.security:security-crypto:1.0.0' ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.13.0' // ZXing base QR code scanner api 'ch.ubique.android:qrscanner-zxing:1.0.0-dev-6' api 'com.augustcellars.cose:cose-java:1.1.0' api 'org.bouncycastle:bcprov-jdk15on:1.69' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } ================================================ FILE: common/consumer-rules.pro ================================================ ================================================ FILE: common/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: common/src/abn/java/ch/admin/bag/covidcertificate/common/debug/DebugFragment.kt ================================================ package ch.admin.bag.covidcertificate.common.debug /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ import android.content.Context import androidx.fragment.app.Fragment open class DebugFragment : Fragment() { companion object { fun newInstance(): DebugFragment = DebugFragment() const val EXISTS = false fun initDebug(context: Context) {} } } ================================================ FILE: common/src/dev/java/ch/admin/bag/covidcertificate/common/debug/DebugFragment.kt ================================================ package ch.admin.bag.covidcertificate.common.debug /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import ch.admin.bag.covidcertificate.common.databinding.FragmentDebugBinding import ch.admin.bag.covidcertificate.sdk.android.net.CertificatePinning open class DebugFragment : Fragment() { companion object { fun newInstance(): DebugFragment = DebugFragment() const val EXISTS = true fun initDebug(context: Context) { CertificatePinning.enabled = DebugSecureStorage.getInstance(context).isCertPinningEnabled } } private var _binding: FragmentDebugBinding? = null protected val binding get() = _binding!! private lateinit var debugSecureStorage: DebugSecureStorage override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) debugSecureStorage = DebugSecureStorage.getInstance(requireContext()) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentDebugBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding.toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } binding.buttonToggleCertificatePinning.apply { isChecked = debugSecureStorage.isCertPinningEnabled setOnCheckedChangeListener { _, isChecked -> debugSecureStorage.isCertPinningEnabled = isChecked CertificatePinning.enabled = isChecked } } } override fun onDestroyView() { super.onDestroyView() _binding = null } } ================================================ FILE: common/src/dev/java/ch/admin/bag/covidcertificate/common/debug/DebugSecureStorage.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.debug import android.content.Context import ch.admin.bag.covidcertificate.sdk.android.net.CertificatePinning import ch.admin.bag.covidcertificate.sdk.android.utils.EncryptedSharedPreferencesUtil import ch.admin.bag.covidcertificate.sdk.android.utils.SingletonHolder class DebugSecureStorage private constructor(context: Context) { companion object : SingletonHolder(::DebugSecureStorage) { private const val PREFERENCES = "DebugSecureStorage" private const val KEY_CERT_PINNING_ENABLED = "KEY_CERT_PINNING_ENABLED" } private val prefs = EncryptedSharedPreferencesUtil.initializeSharedPreferences(context, PREFERENCES) var isCertPinningEnabled: Boolean get() = prefs.getBoolean(KEY_CERT_PINNING_ENABLED, CertificatePinning.enabled) set(value) = prefs.edit().putBoolean(KEY_CERT_PINNING_ENABLED, value).apply() } ================================================ FILE: common/src/dev/res/xml/network_security_config.xml ================================================ ================================================ FILE: common/src/main/AndroidManifest.xml ================================================ ================================================ FILE: common/src/main/assets/impressum/de/impressum.html ================================================
{APP_NAME}
Version {VERSION}

Herausgeber

Weitere Informationen

www.bag.admin.ch

Infoline Coronavirus

+41 58 463 00 00

Rechtliches

Datenschutzerklärung & Nutzungsbedingungen

Umsetzung & Betrieb

Technische Hilfe

+41 58 466 07 99

In Zusammenarbeit mit

www.ubique.ch

Quellcode

Der Quellcode des Covid Certificate Systems (inkl. dieser App) ist Open-Source und kann auf GitHub eingesehen werden.

www.github.com/admin-ch

Lizenzen

{BUILD}
================================================ FILE: common/src/main/assets/impressum/de/licence.html ================================================

Lizenzen

  • This app uses COVID Certificate Android SDK licensed under the MPL 2 license
  • This app uses androidx, kotlinx licensed under the Apache 2.0 license
  • This app uses Material Components For Android licensed under the Apache 2.0 license
  • This app uses ZXing licensed under the Apache 2.0 license
  • This app uses Retrofit licensed under the Apache 2.0 license
  • This app uses Moshi licensed under the Apache 2.0 license
  • This app uses Jackson licensed under the Apache 2.0 license
  • This app uses Gson licensed under the Apache 2.0 license
  • This app uses hcert-kotlin licensed under the Apache 2.0 license
  • This app uses dgc-java licensed under the MIT license license
  • This app uses COSE-JAVA licensed under the BSD-3-Clause license
  • This app uses Java JWT licensed under the Apache 2.0 license
  • This app uses Bouncy Castle licensed under the MIT license
  • This app uses Fira Code Font licensed under the OFL-1.1 license
================================================ FILE: common/src/main/assets/impressum/en/impressum.html ================================================
{APP_NAME}
Version {VERSION}

Published by

Further information

www.bag.admin.ch

Infoline Coronavirus

+41 58 463 00 00

Legal

Data Protection Statement & Conditions of Use

Development & operation

Technical support

+41 58 466 07 99

In collaboration with

www.ubique.ch

Source code

The source code of the COVID Certificate System (including this app) is open source and can be viewed on GitHub.

www.github.com/admin-ch

Licences

{BUILD}
================================================ FILE: common/src/main/assets/impressum/en/licence.html ================================================

Licences

  • This app uses COVID Certificate Android SDK licensed under the MPL 2 license
  • This app uses androidx, kotlinx licensed under the Apache 2.0 license
  • This app uses Material Components For Android licensed under the Apache 2.0 license
  • This app uses ZXing licensed under the Apache 2.0 license
  • This app uses Retrofit licensed under the Apache 2.0 license
  • This app uses Moshi licensed under the Apache 2.0 license
  • This app uses Jackson licensed under the Apache 2.0 license
  • This app uses Gson licensed under the Apache 2.0 license
  • This app uses hcert-kotlin licensed under the Apache 2.0 license
  • This app uses dgc-java licensed under the MIT license license
  • This app uses COSE-JAVA licensed under the BSD-3-Clause license
  • This app uses Java JWT licensed under the Apache 2.0 license
  • This app uses Bouncy Castle licensed under the MIT license
  • This app uses Fira Code Font licensed under the OFL-1.1 license
================================================ FILE: common/src/main/assets/impressum/fr/impressum.html ================================================
{APP_NAME}
Version {VERSION}

Éditeur

Informations complémentaires

www.bag.admin.ch

Infoline Coronavirus

+41 58 463 00 00

Indications légales

Déclaration de confidentialité & condition d'utilisation

Mise en application & opération

Assistance technique

+41 58 466 07 99

En collaboration avec

www.ubique.ch

Code source

Le code source du système de Certificat COVID (y compris cette application) est open source et peut être trouvé sur GitHub.

www.github.com/admin-ch

Licences

{BUILD}
================================================ FILE: common/src/main/assets/impressum/fr/licence.html ================================================

Licences

  • This app uses COVID Certificate Android SDK licensed under the MPL 2 license
  • This app uses androidx, kotlinx licensed under the Apache 2.0 license
  • This app uses Material Components For Android licensed under the Apache 2.0 license
  • This app uses ZXing licensed under the Apache 2.0 license
  • This app uses Retrofit licensed under the Apache 2.0 license
  • This app uses Moshi licensed under the Apache 2.0 license
  • This app uses Jackson licensed under the Apache 2.0 license
  • This app uses Gson licensed under the Apache 2.0 license
  • This app uses hcert-kotlin licensed under the Apache 2.0 license
  • This app uses dgc-java licensed under the MIT license license
  • This app uses COSE-JAVA licensed under the BSD-3-Clause license
  • This app uses Java JWT licensed under the Apache 2.0 license
  • This app uses Bouncy Castle licensed under the MIT license
  • This app uses Fira Code Font licensed under the OFL-1.1 license
================================================ FILE: common/src/main/assets/impressum/it/impressum.html ================================================
{APP_NAME}
Version {VERSION}

Editore

Ulteriori informazioni

www.bag.admin.ch

Infoline Coronavirus

+41 58 463 00 00

Note legali

Informativa sulla protezione dei dati & condizioni d'uso

Realizzazione & operazione

Supporto tecnico

+41 58 466 07 99

In collaborazione con

www.ubique.ch

Codice sorgente

Il codice sorgente del Covid Certificate System (compresa questa applicazione) è open source e può essere trovato su GitHub.

www.github.com/admin-ch

Licenze

{BUILD}
================================================ FILE: common/src/main/assets/impressum/it/licence.html ================================================

Licenze

  • This app uses COVID Certificate Android SDK licensed under the MPL 2 license
  • This app uses androidx, kotlinx licensed under the Apache 2.0 license
  • This app uses Material Components For Android licensed under the Apache 2.0 license
  • This app uses ZXing licensed under the Apache 2.0 license
  • This app uses Retrofit licensed under the Apache 2.0 license
  • This app uses Moshi licensed under the Apache 2.0 license
  • This app uses Jackson licensed under the Apache 2.0 license
  • This app uses Gson licensed under the Apache 2.0 license
  • This app uses hcert-kotlin licensed under the Apache 2.0 license
  • This app uses dgc-java licensed under the MIT license license
  • This app uses COSE-JAVA licensed under the BSD-3-Clause license
  • This app uses Java JWT licensed under the Apache 2.0 license
  • This app uses Bouncy Castle licensed under the MIT license
  • This app uses Fira Code Font licensed under the OFL-1.1 license
================================================ FILE: common/src/main/assets/impressum/rm/impressum.html ================================================
{APP_NAME}
Version {VERSION}

Editur

Ulteriuras infurmaziuns

www.bag.admin.ch

Infoline Coronavirus

+41 58 463 00 00

Indicaziuns giuridicas

Basas legalas

Realisaziun & gestiun

Agid tecnic

+41 58 466 07 99

En collavuraziun cun

www.ubique.ch

Code da funtauna

Il code da funtauna dal COVID Certificate System (incl. questa app) è open source e po vegnir consultà/chattà sin GitHub.

www.github.com/admin-ch

Licenzas

{BUILD}
================================================ FILE: common/src/main/assets/impressum/rm/licence.html ================================================

Licenzas

  • This app uses COVID Certificate Android SDK licensed under the MPL 2 license
  • This app uses androidx, kotlinx licensed under the Apache 2.0 license
  • This app uses Material Components For Android licensed under the Apache 2.0 license
  • This app uses ZXing licensed under the Apache 2.0 license
  • This app uses Retrofit licensed under the Apache 2.0 license
  • This app uses Moshi licensed under the Apache 2.0 license
  • This app uses Jackson licensed under the Apache 2.0 license
  • This app uses Gson licensed under the Apache 2.0 license
  • This app uses hcert-kotlin licensed under the Apache 2.0 license
  • This app uses dgc-java licensed under the MIT license license
  • This app uses COSE-JAVA licensed under the BSD-3-Clause license
  • This app uses Java JWT licensed under the Apache 2.0 license
  • This app uses Bouncy Castle licensed under the MIT license
  • This app uses Fira Code Font licensed under the OFL-1.1 license
================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/BaseActivity.kt ================================================ package ch.admin.bag.covidcertificate.common import android.content.Context import androidx.appcompat.app.AppCompatActivity import ch.admin.bag.covidcertificate.common.data.ConfigSecureStorage import ch.admin.bag.covidcertificate.common.util.LocaleUtil open class BaseActivity : AppCompatActivity() { override fun attachBaseContext(newBase: Context) { val language = ConfigSecureStorage.getInstance(newBase).getUserLanguage() super.attachBaseContext(LocaleUtil.updateLanguage(newBase, language)) } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/browserstack/AirplaneMode.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.browserstack annotation class AirplaneMode ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/browserstack/BadNetwork.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.browserstack annotation class BadNetwork ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/browserstack/Normal.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.browserstack annotation class Normal ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/browserstack/Onboarding.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.browserstack annotation class Onboarding ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/CertificateRenewalInfoDetailModel.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CertificateRenewalInfoDetailModel( val iconAndroid: String, val text: String ) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/CertificateRenewalInfoModel.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CertificateRenewalInfoModel( val heading: String, val infos: List, val faqLinkText: String, val faqLinkUrl: String, ) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/CertificateRenewalType.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = false) enum class CertificateRenewalType { INFO, EXPIRED, RENEWED } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/CheckModesInfosModel.kt ================================================ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass //for verifier @JsonClass(generateAdapter = true) data class CheckModesInfosModel( val infos: Map?, val unselected: VerifierInfos? ) @JsonClass(generateAdapter = true) data class VerifierInfos( val infos: List? ) @JsonClass(generateAdapter = true) data class VerifierInfoDetails( val iconAndroid: String, val iconIos: String, val text: String ) //for wallet @JsonClass(generateAdapter = true) data class CheckModesInfoModel( val title: String?, val modes: Map?, ) @JsonClass(generateAdapter = true) data class WalletModeModel( val ok: WalletModeDetails, val notOk: WalletModeDetails ) @JsonClass(generateAdapter = true) data class WalletModeDetails( val iconAndroid: String, val iconIos: String, val text: String ) @JsonClass(generateAdapter = true) data class CheckModeInfoModel( val title: String, val hexColor: String, val infos: List ) data class CheckModeInfoModelWithId( val id: String, val title: String, val hexColor: String, val infos: List ) @JsonClass(generateAdapter = true) data class CheckModeInfoEntry( val iconAndroid: String, val text: String ) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/ConfigModel.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import ch.admin.bag.covidcertificate.common.faq.model.Faq import ch.admin.bag.covidcertificate.common.faq.model.Header import ch.admin.bag.covidcertificate.common.faq.model.Question import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ConfigModel( val forceUpdate: Boolean, val infoBox: Map?, val questions: Map?, val works: Map?, val transferQuestions: Map?, val transferWorks: Map?, val androidTransferCheckIntervalMs: Long?, val androidTransferCheckBackoffMs: Long?, val lightCertificateActive: Boolean?, val pdfGenerationActive: Boolean?, val vaccinationHints: Map>?, val vaccinationBookingInfo: Map?, val showVaccinationHintHomescreen: Boolean?, val showVaccinationHintDetail: Boolean?, val showVaccinationHintTransfer: Boolean?, val timeshiftDetectionEnabled: Boolean?, val checkModesInfos: Map?, val checkModesInfo: Map?, val checkModeReselectAfterHours: Int?, val lightCertDurationInHours: Int?, val refreshButtonDisabled: Boolean?, val refreshButtonInfo: Map?, val eolBannerInfo: Map>?, val foreignRulesCheckEnabled: Boolean?, val foreignRulesLinkText: Map?, val foreignRulesLinkUrl: Map?, val foreignRulesHints: Map>?, val showRatConversionForm: Boolean?, val ratConversionFormUrl: String?, val certRenewalInfo: Map>?, val showValidityState: Boolean?, val covidCertificateNewsText: Map?, val infoCovidCertificateNews: Map?, ) { fun getInfoBox(languageKey: String?): InfoBoxModel? = infoBox?.get(languageKey) fun getQuestionsFaqs(languageKey: String): FaqModel? = questions?.get(languageKey) fun getWorksFaqs(languageKey: String): FaqModel? = works?.get(languageKey) fun getTransferQuestionsFaqs(languageKey: String): FaqModel? = transferQuestions?.get(languageKey) fun getTransferWorksFaqs(languageKey: String): FaqModel? = transferWorks?.get(languageKey) fun getVaccinationHints(languageKey: String): List? = vaccinationHints?.get(languageKey) fun getCheckModesInfos(languageKey: String): Map? = checkModesInfos?.get(languageKey)?.infos fun getUnselectedModesInfos(languageKey: String): VerifierInfos? = checkModesInfos?.get(languageKey)?.unselected fun getRefreshButtonInfo(languageKey: String): RefreshButtonInfoModel? = refreshButtonInfo?.get(languageKey) fun getEolBannerInfo(languageKey: String): Map? = eolBannerInfo?.get(languageKey) fun getForeignRulesLinkText(languageKey: String): String? = foreignRulesLinkText?.get(languageKey) fun getForeignRulesLinkUrl(languageKey: String): String? = foreignRulesLinkUrl?.get(languageKey) fun getForeignRulesHints(languageKey: String): List? = foreignRulesHints?.get(languageKey) fun getCertRenewalInfo(languageKey: String): Map? = certRenewalInfo?.get(languageKey) fun getCheckModes(languageKey: String): Map? = checkModesInfo?.get(languageKey)?.modes fun getInfoModeTitle(languageKey: String): String? = checkModesInfo?.get(languageKey)?.title fun getVaccinationBookingInfo(languageKey: String): VaccinationBookingInfoModel? = vaccinationBookingInfo?.get(languageKey) fun getCovidCertificateNewsText(languageKey: String): String? = covidCertificateNewsText?.get(languageKey) fun getInfoCovidCertificateNews(languageKey: String): InfoCovidCertificateNews? = infoCovidCertificateNews?.get(languageKey) fun generateFaqItems(languageKey: String): List { val itemsList = mutableListOf() getQuestionsFaqs(languageKey)?.let { questionModel -> val questionItems = questionModel.faqEntries itemsList.add(Header(questionModel.faqIconAndroid, questionModel.faqTitle, questionModel.faqSubTitle)) questionItems?.let { itemsList.addAll(it.map { faqEntry -> Question( faqEntry.title, faqEntry.text, linkTitle = faqEntry.linkTitle, linkUrl = faqEntry.linkUrl ) }) } } getWorksFaqs(languageKey)?.let { worksModel -> val questionItems = worksModel.faqEntries itemsList.add(Header(worksModel.faqIconAndroid, worksModel.faqTitle, worksModel.faqSubTitle)) questionItems?.let { itemsList.addAll(it.map { faqEntry -> Question( faqEntry.title, faqEntry.text, linkTitle = faqEntry.linkTitle, linkUrl = faqEntry.linkUrl ) }) } } return itemsList } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/ConfigViewModel.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import ch.admin.bag.covidcertificate.common.config.ConfigModel import ch.admin.bag.covidcertificate.common.net.ConfigRepository import ch.admin.bag.covidcertificate.common.net.ConfigSpec import kotlinx.coroutines.launch abstract class ConfigViewModel(application: Application) : AndroidViewModel(application) { private val configMutableLiveData = MutableLiveData(ConfigRepository.getCurrentConfig(application)) val configLiveData: LiveData = configMutableLiveData fun loadConfig(baseUrl: String, versionName: String, buildTime: String) { val configSpec = ConfigSpec(getApplication(), baseUrl, versionName, buildTime) val configRepository = ConfigRepository.getInstance(configSpec) viewModelScope.launch { configRepository.loadConfig(getApplication())?.let { config -> configMutableLiveData.postValue(config) } } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/CovidCertificateNewsItem.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass import java.io.Serializable @JsonClass(generateAdapter = true) data class CovidCertificateNewsItem( val iconAndroid: String?, val text: String?, ) : Serializable ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/EolBannerInfoModel.kt ================================================ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass import java.io.Serializable @JsonClass(generateAdapter = true) data class EolBannerInfoModel( val homescreenHexColor: String, val homescreenTitle: String, val detailHexColor: String, val detailTitle: String, val detailText: String?, val detailMoreInfo: String, val popupTitle: String, val popupText1: String?, val popupBoldText: String?, val popupText2: String?, val popupLinkText: String, val popupLinkUrl: String, ) : Serializable ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/FaqEntryModel.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class FaqEntryModel( val title: String, val text: String, val iconAndroid: String?, val linkTitle: String?, val linkUrl: String? ) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/FaqIntroSection.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class FaqIntroSection( val iconAndroid: String, val text: String ) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/FaqModel.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class FaqModel( val faqTitle: String, val faqSubTitle: String?, val faqIntroSections: List?, val faqIconAndroid: String?, val faqEntries: List? ) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/ForeignRulesHintModel.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ForeignRulesHintModel( val iconAndroid: String, val text: String, ) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/InfoBoxModel.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import android.os.Parcel import android.os.Parcelable import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class InfoBoxModel( val title: String, val msg: String, val url: String?, val urlTitle: String?, val isDismissible: Boolean, val infoId: Long ) : Parcelable { constructor(parcel: Parcel) : this( parcel.readString() ?: throw IllegalArgumentException("No title specified for InfoDialog"), parcel.readString() ?: throw IllegalArgumentException("No message specified for InfoDialog"), parcel.readString(), parcel.readString(), parcel.readByte() != 0.toByte(), parcel.readLong() ) { } override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(title) parcel.writeString(msg) parcel.writeString(url) parcel.writeString(urlTitle) parcel.writeByte(if (isDismissible) 1 else 0) parcel.writeLong(infoId) } override fun describeContents(): Int { return 0 } companion object CREATOR : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): InfoBoxModel { return InfoBoxModel(parcel) } override fun newArray(size: Int): Array { return arrayOfNulls(size) } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/InfoCovidCertificateNews.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass import java.io.Serializable @JsonClass(generateAdapter = true) data class InfoCovidCertificateNews( val title: String?, val newsItems: List, ) : Serializable ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/RefreshButtonInfoModel.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass import java.io.Serializable @JsonClass(generateAdapter = true) data class RefreshButtonInfoModel( val title: String, val text1: String, val text2: String, val fatTitle: String, val text3: String, val linkText: String, val linkUrl: String, ) : Serializable ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/VaccinationBookingInfoModel.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VaccinationBookingInfoModel( val title: String, val text: String, val info: String, val impfcheckTitle: String?, val impfcheckText: String?, val impfcheckButton: String?, val impfcheckUrl: String? ) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/config/VaccinationHintModel.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.config import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class VaccinationHintModel( val title: String, val text: String, ) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/data/ConfigSecureStorage.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.data import android.content.Context import androidx.core.content.edit import ch.admin.bag.covidcertificate.common.config.ConfigModel import ch.admin.bag.covidcertificate.sdk.android.utils.EncryptedSharedPreferencesUtil import ch.admin.bag.covidcertificate.sdk.android.utils.SingletonHolder import com.squareup.moshi.Moshi class ConfigSecureStorage private constructor(context: Context) { companion object : SingletonHolder(::ConfigSecureStorage) { private const val PREFERENCES = "ConfigSecureStorage" private const val KEY_CONFIG = "ConfigKey" private const val KEY_CONFIG_LAST_SUCCESS = "LastSuccessTimestampKey" private const val KEY_CONFIG_LAST_SUCCESS_APP_AND_OS_VERSION = "LastSuccessVersionKey" private const val KEY_CONFIG_SHOWN_INFO_BOX_ID = "LastShownInfoBoxId" private const val KEY_USER_LANGUAGE = "UserLanguage" private const val KEY_ZOOM_ACTIVE = "KEY_ZOOM_ACTIVE" private const val KEY_REFRESH_BUTTON_DISABLED_TIMESTAMP = "KEY_REFRESH_BUTTON_DISABLED_TIMESTAMP" private val moshi = Moshi.Builder().build() private val configModelAdapter = moshi.adapter(ConfigModel::class.java) } private val prefs = EncryptedSharedPreferencesUtil.initializeSharedPreferences(context, PREFERENCES) fun updateConfigData(config: ConfigModel, timestamp: Long, appVersion: String) { val editor = prefs.edit() editor.putLong(KEY_CONFIG_LAST_SUCCESS, timestamp) editor.putString(KEY_CONFIG_LAST_SUCCESS_APP_AND_OS_VERSION, appVersion) editor.putString(KEY_CONFIG, configModelAdapter.toJson(config)) editor.apply() } fun getConfig(): ConfigModel? = prefs.getString(KEY_CONFIG, null) ?.let { configModelAdapter.fromJson(it) } fun getConfigLastSuccessTimestamp(): Long = prefs.getLong(KEY_CONFIG_LAST_SUCCESS, 0) fun getConfigLastSuccessAppAndOSVersion(): String? = prefs.getString(KEY_CONFIG_LAST_SUCCESS_APP_AND_OS_VERSION, null) fun setLastShownInfoBoxId(infoBoxId: Long) = prefs.edit().putLong(KEY_CONFIG_SHOWN_INFO_BOX_ID, infoBoxId).apply() fun getLastShownInfoBoxId(): Long = prefs.getLong(KEY_CONFIG_SHOWN_INFO_BOX_ID, 0L) fun setUserLanguage(language: String?) = prefs.edit { putString(KEY_USER_LANGUAGE, language) } fun getUserLanguage(): String? = prefs.getString(KEY_USER_LANGUAGE, null) fun getZoomOn(): Boolean = prefs.getBoolean(KEY_ZOOM_ACTIVE, false) fun setZoomOn(isZoomActive: Boolean) = prefs.edit().putBoolean(KEY_ZOOM_ACTIVE, isZoomActive).apply() fun getRefreshButtonDisabledTimestamp(): Long = prefs.getLong(KEY_REFRESH_BUTTON_DISABLED_TIMESTAMP, -1L) fun setRefreshButtonDisabledTimestamp(timestamp: Long) = prefs.edit { putLong(KEY_REFRESH_BUTTON_DISABLED_TIMESTAMP, timestamp) } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/dialog/InfoDialogFragment.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import ch.admin.bag.covidcertificate.common.R import ch.admin.bag.covidcertificate.common.config.InfoBoxModel import ch.admin.bag.covidcertificate.common.databinding.DialogFragmentInfoBoxBinding import ch.admin.bag.covidcertificate.common.util.UrlUtil class InfoDialogFragment : DialogFragment() { companion object { private const val ARG_INFO_BOX_MODEL = "ARG_INFO_BOX_MODEL" fun newInstance(infoBoxModel: InfoBoxModel): InfoDialogFragment = InfoDialogFragment().apply { arguments = bundleOf(ARG_INFO_BOX_MODEL to infoBoxModel) } } private lateinit var infoBoxModel: InfoBoxModel private var _binding: DialogFragmentInfoBoxBinding? = null private val binding get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NO_TITLE, R.style.CovidCertificate_InfoDialog) infoBoxModel = requireArguments().getParcelable(ARG_INFO_BOX_MODEL) ?: throw IllegalStateException("No infoBox information supplied to DialogFragment!") } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { _binding = DialogFragmentInfoBoxBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { setupInfo() } override fun onDestroyView() { super.onDestroyView() _binding = null } private fun setupInfo() { binding.infoDialogTitle.text = infoBoxModel.title binding.infoDialogText.text = infoBoxModel.msg val hasCustomButton = !infoBoxModel.urlTitle.isNullOrEmpty() binding.infoDialogUrlButton.apply { text = infoBoxModel.urlTitle isVisible = hasCustomButton infoBoxModel.url?.let { url -> setOnClickListener { UrlUtil.openUrl(context, url) } } } binding.infoDialogCloseButton.apply { text = getString(if (hasCustomButton) R.string.accessibility_close_button else R.string.ok_button) setOnClickListener { if (this@InfoDialogFragment.isVisible) dismiss() } } } fun getInfoId(): Long = infoBoxModel.infoId } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/exception/HttpIOException.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.exception import retrofit2.Response import java.io.IOException /** * A replacement for the retrofit2.HttpException that inherits from IOException instead of RuntimeException. * Since other request related exceptions (e.g. UnknownHost or SocketTimeout) extend IOException, this makes it easier to catch all * networking related exceptions in a single catch-clause. */ class HttpIOException(val response: Response<*>) : IOException(response.message()) { val code = response.code() } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/exception/TimeDeviationException.kt ================================================ package ch.admin.bag.covidcertificate.common.exception class TimeDeviationException : IllegalStateException() ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/extensions/ContextExtensions.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.extensions import android.content.Context import android.content.res.Configuration import java.util.* fun Context.getDrawableIdentifier(drawableName: String) = resources.getIdentifier(drawableName, "drawable", packageName) fun Context.updateLocale(languageKey: String? = null): Context { val config = Configuration() val locale = languageKey?.let { Locale(languageKey, "CH") } ?: Locale.getDefault() config.setLocale(locale) return createConfigurationContext(config) } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/extensions/DccCertExtensions.kt ================================================ package ch.admin.bag.covidcertificate.common.extensions import ch.admin.bag.covidcertificate.sdk.core.extensions.firstPositiveResult import ch.admin.bag.covidcertificate.sdk.core.extensions.vaccineDate import ch.admin.bag.covidcertificate.sdk.core.extensions.validFromDate import ch.admin.bag.covidcertificate.sdk.core.models.healthcert.eu.DccCert import java.time.LocalDateTime fun DccCert.getDate(): LocalDateTime? { var date = this.vaccinations?.firstOrNull()?.vaccineDate() if (date == null) { date = this.pastInfections?.firstOrNull()?.firstPositiveResult() } if (date == null) { date = this.tests?.firstOrNull()?.validFromDate() } return date } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/extensions/LifecycleOwnerExtensions.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.extensions import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch /** * Collect a flow, starting and cancelling the collection automatically when the lifecycle changes from or to STARTED */ fun LifecycleOwner.collectWhenStarted(flow: Flow, block: suspend (T) -> Unit) { lifecycleScope.launch { flow.flowWithLifecycle(lifecycle).collect { block(it) } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/extensions/OkHttpExtensions.kt ================================================ package ch.admin.bag.covidcertificate.common.extensions import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit fun OkHttpClient.Builder.setTimeouts(seconds: Long = 10): OkHttpClient.Builder { connectTimeout(seconds, TimeUnit.SECONDS) writeTimeout(seconds, TimeUnit.SECONDS) readTimeout(seconds, TimeUnit.SECONDS) return this } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/extensions/WindowExtensions.kt ================================================ package ch.admin.bag.covidcertificate.common.extensions import android.view.Window import android.view.WindowManager fun Window.overrideScreenBrightness(override: Boolean) { if (override) { addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } else { clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } val layoutParams: WindowManager.LayoutParams = attributes layoutParams.screenBrightness = if (override) 1f else -1f attributes = layoutParams } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/faq/FaqAdapter.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.faq import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import ch.admin.bag.covidcertificate.common.faq.model.Faq import ch.admin.bag.covidcertificate.common.faq.model.Header import ch.admin.bag.covidcertificate.common.faq.model.IntroSection import ch.admin.bag.covidcertificate.common.faq.model.Question class FaqAdapter(val onLinkClickListener: ((String) -> Unit)? = null) : RecyclerView.Adapter() { private val items = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FaqViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { 0 -> FaqViewHolder(inflater.inflate(HeaderItem.layoutResource, parent, false)) 1 -> FaqViewHolder(inflater.inflate(QuestionItem.layoutResource, parent, false)) 2 -> FaqViewHolder(inflater.inflate(IntroSectionItem.layoutResource, parent, false)) else -> throw IllegalStateException("Unknown viewType $viewType in FaqAdapter") } } override fun onBindViewHolder(holder: FaqViewHolder, position: Int) { holder.bindItem(items[position]) { notifyItemChanged(position) } } override fun getItemCount(): Int = items.size override fun getItemViewType(position: Int): Int { return when (items[position]) { is HeaderItem -> 0 is QuestionItem -> 1 is IntroSectionItem -> 2 } } fun setItems(items: List) { this.items.clear() val newItems = items.mapNotNull { when (it) { is Header -> HeaderItem(it) is Question -> QuestionItem(it, onLinkClickListener) is IntroSection -> IntroSectionItem(it) else -> null } } this.items.addAll(newItems) notifyDataSetChanged() } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/faq/FaqFragment.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.faq import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.SimpleItemAnimator import ch.admin.bag.covidcertificate.common.databinding.FragmentFaqBinding import ch.admin.bag.covidcertificate.common.faq.model.Faq import ch.admin.bag.covidcertificate.common.util.UrlUtil import ch.admin.bag.covidcertificate.common.views.hideAnimated abstract class FaqFragment : Fragment() { private var _binding: FragmentFaqBinding? = null private val binding get() = _binding!! lateinit var toolbar: Toolbar override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { _binding = FragmentFaqBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) toolbar = binding.certificatesOverviewToolbar toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } setupFaqProvider() } override fun onDestroyView() { super.onDestroyView() _binding = null } protected abstract fun setupFaqProvider() protected fun setupFaqList(items: List) { binding.faqLoadingView.hideAnimated() val recyclerView = binding.faqRecyclerView (recyclerView.itemAnimator as SimpleItemAnimator?)?.supportsChangeAnimations = false val layoutManager = LinearLayoutManager(recyclerView.context, LinearLayoutManager.VERTICAL, false) recyclerView.layoutManager = layoutManager val adapter = FaqAdapter { url: String -> context?.let { UrlUtil.openUrl(it, url) } } recyclerView.adapter = adapter adapter.setItems(items) } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/faq/FaqItem.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.faq import android.view.View import androidx.core.view.doOnPreDraw import androidx.core.view.isVisible import ch.admin.bag.covidcertificate.common.R import ch.admin.bag.covidcertificate.common.databinding.ItemFaqHeaderBinding import ch.admin.bag.covidcertificate.common.databinding.ItemFaqIntroSectionBinding import ch.admin.bag.covidcertificate.common.databinding.ItemFaqQuestionBinding import ch.admin.bag.covidcertificate.common.extensions.getDrawableIdentifier import ch.admin.bag.covidcertificate.common.faq.model.Header import ch.admin.bag.covidcertificate.common.faq.model.IntroSection import ch.admin.bag.covidcertificate.common.faq.model.Question sealed class FaqItem { abstract fun bindView(view: View, onItemClickListener: (() -> Unit)? = null) } data class HeaderItem(val header: Header) : FaqItem() { companion object { val layoutResource = R.layout.item_faq_header } override fun bindView(view: View, onItemClickListener: (() -> Unit)?) { val binding = ItemFaqHeaderBinding.bind(view) binding.itemFaqHeaderTitle.text = header.title binding.itemFaqHeaderText.apply { text = header.subtitle isVisible = header.subtitle != null } val drawableId = header.iconName?.let { iconName -> view.context.getDrawableIdentifier(iconName) } ?: 0 binding.itemFaqHeaderIllu.apply { setImageResource(drawableId) isVisible = drawableId != 0 } } } data class QuestionItem( val question: Question, val onLinkClickListener: ((String) -> Unit)? = null, ) : FaqItem() { companion object { val layoutResource = R.layout.item_faq_question } override fun bindView(view: View, onItemClickListener: (() -> Unit)?) { val binding = ItemFaqQuestionBinding.bind(view) binding.root.setOnClickListener { question.isSelected = !question.isSelected view.doOnPreDraw { onItemClickListener?.invoke() } } binding.itemFaqQuestionTitle.text = question.question binding.itemFaqQuestionAnswer.apply { text = question.answer isVisible = question.isSelected } val hasLink = !question.linkTitle.isNullOrEmpty() && !question.linkUrl.isNullOrEmpty() (hasLink && question.isSelected).let { visible -> binding.itemFaqQuestionLinkLabel.isVisible = visible binding.itemFaqQuestionLink.isVisible = visible } if (hasLink) { binding.itemFaqQuestionLinkLabel.text = question.linkTitle binding.itemFaqQuestionLink.setOnClickListener { onLinkClickListener?.invoke(question.linkUrl!!) } } else { binding.itemFaqQuestionLink.setOnClickListener(null) } binding.itemFaqQuestionChevron.setImageResource(if (question.isSelected) R.drawable.ic_arrow_contract else R.drawable.ic_arrow_expand) } } data class IntroSectionItem(val introSection: IntroSection) : FaqItem() { companion object { val layoutResource = R.layout.item_faq_intro_section } override fun bindView(view: View, onItemClickListener: (() -> Unit)?) { val binding = ItemFaqIntroSectionBinding.bind(view) val drawableId = view.context.getDrawableIdentifier(introSection.iconName) binding.faqIntroSectionIcon.setImageResource(drawableId) binding.faqIntroSectionText.text = introSection.text } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/faq/FaqViewHolder.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.faq import android.view.View import androidx.recyclerview.widget.RecyclerView class FaqViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bindItem(item: FaqItem, onItemClickListener: (() -> Unit)? = null) = item.bindView(itemView, onItemClickListener) } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/faq/model/Header.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.faq.model import java.io.Serializable interface Faq data class Header(val iconName: String?, val title: String, val subtitle: String?) : Faq, Serializable data class Question( val question: String, val answer: String, var isSelected: Boolean = false, val linkTitle: String? = null, val linkUrl: String? = null, ) : Faq, Serializable data class IntroSection(val iconName: String, val text: String) : Faq, Serializable ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/html/BuildInfo.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.html import java.io.Serializable data class BuildInfo( val appName: String, val versionName: String, val buildTime: Long, val flavor: String, val agbUrl: String, val appIdentifier: String, ) : Serializable ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/html/ImprintFragment.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.html import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.webkit.WebView import android.webkit.WebViewClient import androidx.annotation.StringRes import androidx.fragment.app.Fragment import androidx.fragment.app.commit import ch.admin.bag.covidcertificate.common.R import ch.admin.bag.covidcertificate.common.databinding.FragmentHtmlBinding import ch.admin.bag.covidcertificate.common.settings.SettingsFragment import ch.admin.bag.covidcertificate.common.util.AssetUtil import ch.admin.bag.covidcertificate.common.util.UrlUtil import ch.admin.bag.covidcertificate.common.views.hideAnimated import java.util.* class ImprintFragment : Fragment() { companion object { private const val COVID_CERT_IMPRESSUM_PREFIX = "ccert://" private const val ARG_BASE_URL = "ARG_BASE_URL" private const val ARG_BUILD_INFO = "ARG_BUILD_INFO" private const val ARG_DATA = "ARG_DATA" private const val ARG_TITLE = "ARG_TITLE" private const val ARG_SETTINGS = "ARG_SETTINGS" fun newInstance( titleRes: Int, buildInfo: BuildInfo, baseUrl: String? = null, data: String? = null, showSettings: Boolean = true ): ImprintFragment { val fragment = ImprintFragment() fragment.arguments = Bundle().apply { putString(ARG_BASE_URL, baseUrl) putSerializable(ARG_BUILD_INFO, buildInfo) putString(ARG_DATA, data) putInt(ARG_TITLE, titleRes) putBoolean(ARG_SETTINGS, showSettings) } return fragment } } private var _binding: FragmentHtmlBinding? = null private val binding get() = _binding!! private lateinit var baseUrl: String private var data: String? = null private var buildInfo: BuildInfo? = null @StringRes private var titleRes = 0 private var showSettings = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requireArguments().apply { baseUrl = getString(ARG_BASE_URL) ?: AssetUtil.getImpressumBaseUrl(requireContext()) buildInfo = getSerializable(ARG_BUILD_INFO) as? BuildInfo? data = getString(ARG_DATA) ?: buildInfo?.let { AssetUtil.getImpressumHtml(requireContext(), it) } titleRes = getInt(ARG_TITLE) showSettings = getBoolean(ARG_SETTINGS) } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { _binding = FragmentHtmlBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val toolbar = binding.htmlToolbar toolbar.setTitle(titleRes) toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } if (showSettings) { toolbar.inflateMenu(R.menu.imprint) toolbar.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.menu_settings -> parentFragmentManager.commit { setCustomAnimations(R.anim.slide_enter, R.anim.slide_exit, R.anim.slide_pop_enter, R.anim.slide_pop_exit) replace(id, SettingsFragment.newInstance()) addToBackStack(SettingsFragment::class.java.canonicalName) } else -> throw UnsupportedOperationException() } true } } val web = binding.htmlWebview val loadingSpinner = binding.loadingSpinner web.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView, url: String) { loadingSpinner.hideAnimated() super.onPageFinished(view, url) } override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { if (baseUrl == url) return true if (url.toLowerCase(Locale.ENGLISH).startsWith(COVID_CERT_IMPRESSUM_PREFIX)) { val buildInfo = buildInfo ?: throw IllegalStateException("No BuildInfo supplied for imprint") val strippedUrl = url.substring(COVID_CERT_IMPRESSUM_PREFIX.length) val htmlFragment = newInstance( R.string.impressum_title, buildInfo, baseUrl, AssetUtil.loadImpressumHtmlFile(view.context, strippedUrl, buildInfo), false ) parentFragmentManager.beginTransaction() .setCustomAnimations( R.anim.slide_enter, R.anim.slide_exit, R.anim.slide_pop_enter, R.anim.slide_pop_exit ) .replace(id, htmlFragment) .addToBackStack(ImprintFragment::class.java.canonicalName) .commit() return true } UrlUtil.openUrl(context, url) return true } } if (data != null) { data?.let { web.loadDataWithBaseURL(baseUrl, it, "text/html", "UTF-8", null) } } else { web.loadUrl(baseUrl) } } override fun onDestroyView() { super.onDestroyView() _binding = null } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/net/ConfigRepository.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.net import android.content.Context import android.os.Build import ch.admin.bag.covidcertificate.common.BuildConfig import ch.admin.bag.covidcertificate.common.config.ConfigModel import ch.admin.bag.covidcertificate.common.data.ConfigSecureStorage import ch.admin.bag.covidcertificate.common.extensions.setTimeouts import ch.admin.bag.covidcertificate.common.util.AssetUtil import ch.admin.bag.covidcertificate.sdk.android.CovidCertificateSdk import ch.admin.bag.covidcertificate.sdk.android.data.Config import ch.admin.bag.covidcertificate.sdk.android.net.CertificatePinning import ch.admin.bag.covidcertificate.sdk.android.net.interceptor.JwsInterceptor import ch.admin.bag.covidcertificate.sdk.android.net.interceptor.UserAgentInterceptor import ch.admin.bag.covidcertificate.sdk.android.utils.SingletonHolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.HttpException import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory class ConfigRepository private constructor(private val configSpec: ConfigSpec) { companion object : SingletonHolder(::ConfigRepository) { private const val APP_VERSION_PREFIX_ANDROID = "android-" private const val OS_VERSION_PREFIX_ANDROID = "android" private const val MIN_LOAD_WAIT_TIME = 60 * 60 * 1000L // 1h private const val MAX_AGE_STATUS_VALID_CACHED_CONFIG = 48 * 60 * 60 * 1000L // 48h fun getCurrentConfig(context: Context) = ConfigSecureStorage.getInstance(context).getConfig() ?: AssetUtil.loadDefaultConfig(context) } private val configService: ConfigService private val storage = ConfigSecureStorage.getInstance(configSpec.context) init { val rootCa = CovidCertificateSdk.getRootCa(configSpec.context) val expectedCommonName = CovidCertificateSdk.getExpectedCommonName() val okHttpBuilder = OkHttpClient.Builder() .certificatePinner(CertificatePinning.pinner) .addInterceptor(JwsInterceptor(rootCa, expectedCommonName)) .addInterceptor(UserAgentInterceptor(Config.userAgent)) .setTimeouts() val cacheSize = 5 * 1024 * 1024 // 5 MB val cache = Cache(configSpec.context.cacheDir, cacheSize.toLong()) okHttpBuilder.cache(cache) if (BuildConfig.DEBUG) { val httpInterceptor = HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) okHttpBuilder.addInterceptor(httpInterceptor) } configService = Retrofit.Builder() .baseUrl(configSpec.baseUrl) .client(okHttpBuilder.build()) .addConverterFactory(MoshiConverterFactory.create()) .build() .create(ConfigService::class.java) } suspend fun loadConfig(context: Context): ConfigModel? { val requestTimeStamp = System.currentTimeMillis() val appVersion = APP_VERSION_PREFIX_ANDROID + configSpec.versionName val osVersion = OS_VERSION_PREFIX_ANDROID + Build.VERSION.SDK_INT val buildNumber = configSpec.buildTime val versionString = "appversion=$appVersion&osversion=$osVersion&buildnr=$buildNumber" var config = if (storage.getConfigLastSuccessTimestamp() + MIN_LOAD_WAIT_TIME <= System.currentTimeMillis() || versionString != storage.getConfigLastSuccessAppAndOSVersion()) { try { val response = withContext(Dispatchers.IO) { configService.getConfig(appVersion, osVersion, buildNumber) } if (!response.isSuccessful) throw HttpException(response) response.body()?.let { storage.updateConfigData(it, requestTimeStamp, versionString) } response.body() } catch (e: Exception) { e.printStackTrace() null } } else null if (config == null) config = storage.getConfig() if (config == null) config = AssetUtil.loadDefaultConfig(context) // If the config has disabled the refresh button check if it's the first time and if so, store the timestamp if (config?.refreshButtonDisabled == true) { if (storage.getRefreshButtonDisabledTimestamp() < 0L) { storage.setRefreshButtonDisabledTimestamp(System.currentTimeMillis()) } } else { storage.setRefreshButtonDisabledTimestamp(-1L) } return config } } class ConfigSpec(val context: Context, val baseUrl: String, val versionName: String, val buildTime: String) ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/net/ConfigService.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.net import ch.admin.bag.covidcertificate.common.config.ConfigModel import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Headers import retrofit2.http.Query interface ConfigService { @Headers("Accept: application/json+jws") @GET("config") suspend fun getConfig( @Query("appversion") appVersion: String, @Query("osversion") osVersion: String, @Query("buildnr") buildNumber: String ): Response } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/onboarding/BaseOnboardingActivity.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.onboarding import android.os.Bundle import androidx.viewpager2.adapter.FragmentStateAdapter import ch.admin.bag.covidcertificate.common.BaseActivity import ch.admin.bag.covidcertificate.common.R import ch.admin.bag.covidcertificate.common.databinding.ActivityOnboardingBinding abstract class BaseOnboardingActivity : BaseActivity() { companion object { const val EXTRA_ONBOARDING_TYPE = "EXTRA_ONBOARDING_TYPE" } private lateinit var binding: ActivityOnboardingBinding private lateinit var pagerAdapter: FragmentStateAdapter protected abstract fun getPagerAdapter(): FragmentStateAdapter? override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val pagerAdapter = getPagerAdapter() if (pagerAdapter == null) { finish() return } binding = ActivityOnboardingBinding.inflate(layoutInflater) setContentView(binding.root) binding.viewPager.isUserInputEnabled = false this.pagerAdapter = pagerAdapter binding.viewPager.adapter = pagerAdapter } fun continueToNextPage() { val currentItem: Int = binding.viewPager.currentItem if (currentItem < pagerAdapter.itemCount - 1) { binding.viewPager.setCurrentItem(currentItem + 1, true) } else { setResult(RESULT_OK) finish() overridePendingTransition(R.anim.fragment_open_enter, R.anim.fragment_open_exit) } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/onboarding/SimpleOnboardingPagerAdapter.kt ================================================ /* * Copyright (c) 2022 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.onboarding import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.viewpager2.adapter.FragmentStateAdapter class SimpleOnboardingPagerAdapter( fragmentActivity: FragmentActivity, private vararg val fragmentProviders: FragmentProvider ) : FragmentStateAdapter(fragmentActivity) { override fun createFragment(position: Int) = fragmentProviders[position].provide() override fun getItemCount() = fragmentProviders.size fun interface FragmentProvider { fun provide(): Fragment } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/qr/CameraPermissionExplanationDialog.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.qr import android.content.Context import android.graphics.Paint import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AlertDialog import ch.admin.bag.covidcertificate.common.R class CameraPermissionExplanationDialog(context: Context) : AlertDialog(context) { private var grantCameraAccessClickListener: View.OnClickListener? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.dialog_camera_permission_explanation) window?.apply { setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) setBackgroundDrawableResource(R.drawable.bg_dialog) } findViewById(R.id.camera_permission_dialog_ok_button)?.apply { paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG setOnClickListener { v: View? -> dismiss() grantCameraAccessClickListener?.onClick(v) } } findViewById(R.id.camera_permission_dialog_close_button)?.setOnClickListener { _ -> cancel() } } fun setGrantCameraAccessClickListener(listener: View.OnClickListener?) { grantCameraAccessClickListener = listener } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/qr/KittlerBinarizer.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.qr import com.google.zxing.LuminanceSource import com.google.zxing.common.BitMatrix import com.google.zxing.common.GlobalHistogramBinarizer import okhttp3.internal.and import kotlin.math.abs /* *Use a Kittler Binarizer described here //http://www.iztok-jr-fister.eu/static/publications/39.pdf * * */ class KittlerBinarizer(luminanceSource: LuminanceSource) : GlobalHistogramBinarizer(luminanceSource) { override fun getBlackMatrix(): BitMatrix { val source = luminanceSource val width = source.width val height = source.height var count = 0 var totalcount = 0 val matrix = BitMatrix(width, height) val localLuminanceSource = source.matrix val threshold = estimateThreshold(localLuminanceSource, width, height) (0 until height).forEach { y -> val offset = y * width (0 until width).forEach { x -> totalcount++ val pixel = localLuminanceSource[offset + x] and 0xFF if (pixel < threshold) { count++ matrix.set(x, y) } } } return matrix } private fun estimateThreshold(localLuminanceSource: ByteArray, width: Int, height: Int): Int { var E = 0L var EF = 0L (1 until height - 1).forEach { y -> val offset = y * width (1 until width - 1).forEach { x -> val grey = localLuminanceSource[offset + x] and 0xFF val grey1 = localLuminanceSource[offset + x - 1] and 0xFF val grey2 = localLuminanceSource[offset + x + 1] and 0xFF val grey3 = localLuminanceSource[offset + x - width] and 0xFF val grey4 = localLuminanceSource[offset + x + width] and 0xFF val Ex = abs(grey1 - grey2) val Ey = abs(grey3 - grey4) val exy = Math.max(Ex, Ey) E += exy EF += exy * grey } } if (E == 0L) { return 128 } return ((EF / E) - 1L).toInt() } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/qr/QRCodeReaderHelper.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.qr import android.app.ActivityManager import android.content.Context import android.graphics.Bitmap import android.graphics.Color import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor import com.google.zxing.* import com.google.zxing.common.GlobalHistogramBinarizer import com.google.zxing.common.HybridBinarizer import java.io.File import java.lang.Integer.min import kotlin.math.roundToInt object QRCodeReaderHelper { private val hints = mapOf( DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE), DecodeHintType.TRY_HARDER to true, ) private const val PDF_PAGE_LIMIT = 5 private val reader = MultiFormatReader().apply { setHints(hints) } fun decodeQrCode(bitmap: Bitmap): String? { val intArray = IntArray(bitmap.width * bitmap.height) bitmap.getPixels(intArray, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) val source: LuminanceSource = RGBLuminanceSource(bitmap.width, bitmap.height, intArray) // First try with hybrid binarizer, then with global histogram binarizer. Same as in the camera scanner return decodeQrCodeWithBinarizer(HybridBinarizer(source)) ?: decodeQrCodeWithBinarizer(GlobalHistogramBinarizer(source)) } fun pdfToBitmaps(context: Context, pdfFile: File): Sequence = sequence { try { PdfRenderer(ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)).use { renderer -> val pageCount = renderer.pageCount val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val memoryInfo = ActivityManager.MemoryInfo().also { activityManager.getMemoryInfo(it) } for (scale in listOf(1f, context.resources.displayMetrics.densityDpi / 72f, 3f, 5f, 7f, 9f)) { for (i in 0 until min(pageCount, PDF_PAGE_LIMIT)) { renderer.openPage(i).use { page -> // PDF width/height are given in "points pt" such that 1 pt = 1/72 inch // => 72 "dots-per-inch dpi" <==> scale = 1 val pixelWidth = (scale * page.width).roundToInt() val pixelHeight = (scale * page.height).roundToInt() if (scale == 1f || !memoryInfo.lowMemory) { // Only yield the scaled bitmap if the system is not considered to be in low memory mode // On some devices, this bitmap can get up to 50MB due to the large scale factor page.renderToBitmap(pixelWidth, pixelHeight).use { yield(it) } } } } } } } catch (ex: Exception) { ex.printStackTrace() } } private fun decodeQrCodeWithBinarizer(binarizer: Binarizer): String? { val binaryBitmap = BinaryBitmap(binarizer) try { val result: Result = reader.decodeWithState(binaryBitmap) return result.text } catch (e: NotFoundException) { e.printStackTrace() } catch (e: ChecksumException) { e.printStackTrace() } catch (e: FormatException) { e.printStackTrace() } return null } private fun PdfRenderer.Page.renderToBitmap(pixelWidth: Int, pixelHeight: Int): Bitmap { val bitmap = Bitmap.createBitmap(pixelWidth, pixelHeight, Bitmap.Config.ARGB_8888) // Make sure the bitmap's background is not transparent (which can cause issues for QR code detection) bitmap.eraseColor(Color.WHITE) // Draw the page onto the bitmap. Internally, this will scale the page to fit the bitmap (unless transform != null). this.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT) // reduce pixel size to avoid memory issues down the line val bitmapR = bitmap.copy(Bitmap.Config.RGB_565, false) bitmap.recycle() return bitmapR } private inline fun Bitmap.use(block: (Bitmap) -> Unit) { try { block(this) } finally { this.recycle() } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/qr/QrScanFragment.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.qr import android.Manifest import android.annotation.SuppressLint import android.content.pm.PackageManager import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable import android.os.Bundle import android.view.View import android.widget.ImageButton import android.widget.TextView import androidx.annotation.ColorRes import androidx.appcompat.widget.Toolbar import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.liveData import ch.admin.bag.covidcertificate.common.R import ch.admin.bag.covidcertificate.common.data.ConfigSecureStorage import ch.admin.bag.covidcertificate.common.util.ErrorHelper import ch.admin.bag.covidcertificate.common.util.ErrorState import ch.admin.bag.covidcertificate.sdk.core.models.state.StateError import ch.ubique.qrscanner.scanner.ErrorCodes import ch.ubique.qrscanner.scanner.ScanningMode import ch.ubique.qrscanner.state.DecodingState import ch.ubique.qrscanner.util.CameraUtil import ch.ubique.qrscanner.view.QrScannerView import ch.ubique.qrscanner.zxing.decoder.GlobalHistogramImageDecoder import ch.ubique.qrscanner.zxing.decoder.HybridImageDecoder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.isActive abstract class QrScanFragment : Fragment() { companion object { private const val STATE_IS_TORCH_ON = "STATE_IS_TORCH_ON" private const val PERMISSION_REQUEST_CAMERA = 13 private const val MIN_ERROR_VISIBILITY = 1000L private const val QR_CODE_ERROR_WRONG_FORMAT = "Q|YWF" private const val QR_CODE_ERROR_READ_FAILED = "Q|IRF" } // These need to be set by implementing classes during onCreateView. That's why they are not private. protected lateinit var toolbar: Toolbar protected lateinit var flashButton: ImageButton protected lateinit var errorView: View protected lateinit var errorCodeView: TextView protected lateinit var zoomButton: ImageButton protected lateinit var invalidCodeText: TextView protected lateinit var viewFinderTopLeftIndicator: View protected lateinit var viewFinderTopRightIndicator: View protected lateinit var viewFinderBottomLeftIndicator: View protected lateinit var viewFinderBottomRightIndicator: View protected lateinit var qrCodeScanner: QrScannerView protected lateinit var cutOut: View abstract val viewFinderColor: Int abstract val viewFinderErrorColor: Int abstract val torchOnDrawable: Int abstract val torchOffDrawable: Int abstract val zoomOnDrawable: Int abstract val zoomOffDrawable: Int private var lastUIErrorUpdate = 0L private var cameraPermissionState = CameraPermissionState.REQUESTING private val secureStorage by lazy { ConfigSecureStorage.getInstance(requireContext()) } private var cameraPermissionExplanationDialog: CameraPermissionExplanationDialog? = null private var isTorchOn: Boolean = false private val autoFocusClockLiveData = liveData(Dispatchers.IO) { while (currentCoroutineContext().isActive) { emit(Unit) delay(3 * 1000L) } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) isTorchOn = savedInstanceState?.getBoolean(STATE_IS_TORCH_ON, isTorchOn) ?: isTorchOn toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } // Wait for the views to be properly laid out qrCodeScanner.post { initializeCamera() } } override fun onResume() { super.onResume() // Check permission in onResume to automatically handle the user returning from the system settings. // Be careful to avoid popup loops, since our fragment is resumed whenever the user returns from the dialog! checkCameraPermission() setFlashAndButtonStyle() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putBoolean(STATE_IS_TORCH_ON, isTorchOn) } abstract fun decodeQrCodeData(qrCodeData: String, onDecodeSuccess: () -> Unit, onDecodeError: (StateError) -> Unit) override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (requestCode == PERMISSION_REQUEST_CAMERA) { val isGranted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED cameraPermissionState = if (isGranted) CameraPermissionState.GRANTED else CameraPermissionState.DENIED refreshView() } } protected fun activateCamera() { if (!isAdded || !qrCodeScanner.isAttachedToWindow) return autoFocusClockLiveData.observe(viewLifecycleOwner) { autoFocus() } qrCodeScanner.activateCamera() } protected fun deactivateCamera() { if (!isAdded || !qrCodeScanner.isAttachedToWindow) return autoFocusClockLiveData.removeObservers(viewLifecycleOwner) qrCodeScanner.deactivateCamera() } @SuppressLint("ClickableViewAccessibility") private fun initializeCamera() { if (!isAdded || !qrCodeScanner.isAttachedToWindow) return setScannerCallback() qrCodeScanner.setImageDecoders(GlobalHistogramImageDecoder(), HybridImageDecoder()) qrCodeScanner.setScanningMode(ScanningMode.PARALLEL) qrCodeScanner.setFocusOnTap(true) qrCodeScanner.setCameraStateCallback { isActive -> if (isActive) { qrCodeScanner.isVisible = true setupZoomButton() setupFlashButton() } else { qrCodeScanner.isVisible = false } } } protected fun setScannerCallback() { qrCodeScanner.setScannerCallback { state -> when (state) { is DecodingState.NotFound -> view?.post { updateQrCodeScannerState(QrScannerState.NO_CODE_FOUND) } is DecodingState.Decoded -> { val qrCodeData = state.content decodeQrCodeData( qrCodeData, onDecodeSuccess = { // Once successfully decoded, clear the analyzer from stopping more frames being // analyzed and possibly decoded successfully qrCodeScanner.setScannerCallback(null) view?.post { updateQrCodeScannerState(QrScannerState.VALID) } }, onDecodeError = { error -> view?.post { handleInvalidQRCodeExceptions(error) } } ) } is DecodingState.Error -> { val stateError = if (state.errorCode == ErrorCodes.INPUT_WRONG_FORMAT) { StateError(QR_CODE_ERROR_WRONG_FORMAT) } else { StateError(QR_CODE_ERROR_READ_FAILED) } handleInvalidQRCodeExceptions(stateError) } } } } private fun setZoom() { if (secureStorage.getZoomOn()) { qrCodeScanner.setLinearZoom(1f) } else { qrCodeScanner.setLinearZoom(0f) } } private fun autoFocus() { val centerX = cutOut.left + cutOut.width / 2.0f val centerY = cutOut.top + cutOut.height / 2.0f qrCodeScanner.startAutofocus(centerX, centerY) } private fun checkCameraPermission() { val isGranted = CameraUtil.hasCameraPermission(requireContext()) if (isGranted) { cameraPermissionState = CameraPermissionState.GRANTED } // Do not request the permission again if the last time we tried the user denied it. // I.e. don't show the popup but the error view else if (cameraPermissionState != CameraPermissionState.DENIED) { cameraPermissionState = CameraPermissionState.REQUESTING } refreshView() } private fun refreshView() { when (cameraPermissionState) { CameraPermissionState.GRANTED -> { errorView.isVisible = false } CameraPermissionState.REQUESTING -> { errorView.isVisible = false showCameraPermissionExplanationDialog() } CameraPermissionState.CANCELLED, CameraPermissionState.DENIED -> { errorView.isVisible = true ErrorHelper.updateErrorView(errorView, ErrorState.CAMERA_ACCESS_DENIED, null, context) } } updateQrCodeScannerState(QrScannerState.NO_CODE_FOUND) } private fun showCameraPermissionExplanationDialog() { if (cameraPermissionExplanationDialog?.isShowing == true) { return } cameraPermissionExplanationDialog = CameraPermissionExplanationDialog(requireContext()).apply { setOnCancelListener { cameraPermissionState = CameraPermissionState.CANCELLED refreshView() } setGrantCameraAccessClickListener { requestPermissions(arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CAMERA) } setOnDismissListener { cameraPermissionExplanationDialog = null } show() } } private fun setupFlashButton() { val cameraInfo = qrCodeScanner.getCameraInfo() ?: return if (cameraInfo.hasFlashUnit()) { flashButton.isVisible = true setFlashAndButtonStyle() } else { flashButton.isVisible = false } flashButton.setOnClickListener { isTorchOn = !flashButton.isSelected setFlashAndButtonStyle() } zoomButton.setOnClickListener { secureStorage.setZoomOn(!secureStorage.getZoomOn()) setupZoomButton() } } private fun setFlashAndButtonStyle() { qrCodeScanner.setFlash(isTorchOn) val drawableId = if (isTorchOn) torchOnDrawable else torchOffDrawable flashButton.isSelected = isTorchOn flashButton.setImageResource(drawableId) } private fun setupZoomButton() { if (requireContext().packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_AUTOFOCUS)) { zoomButton.isVisible = false } else { val isZoomOn = secureStorage.getZoomOn() zoomButton.isVisible = true val drawableId = if (isZoomOn) zoomOnDrawable else zoomOffDrawable zoomButton.isSelected = isZoomOn zoomButton.setImageResource(drawableId) setZoom() } } private fun handleInvalidQRCodeExceptions(error: StateError?) { updateQrCodeScannerState(QrScannerState.INVALID_FORMAT, error?.code) } private fun updateQrCodeScannerState(qrScannerState: QrScannerState, errorCode: String? = null) { if (!isAdded) return val currentTime = System.currentTimeMillis() if (lastUIErrorUpdate > currentTime - MIN_ERROR_VISIBILITY && qrScannerState == QrScannerState.NO_CODE_FOUND) { return } lastUIErrorUpdate = currentTime var color: Int = viewFinderColor when (qrScannerState) { QrScannerState.VALID, QrScannerState.NO_CODE_FOUND -> { invalidCodeText.isVisible = false errorCodeView.isVisible = false } QrScannerState.INVALID_FORMAT -> { invalidCodeText.isVisible = true errorCodeView.isVisible = errorCode != null errorCodeView.text = errorCode color = viewFinderErrorColor } } setIndicatorColor(viewFinderTopLeftIndicator, color) setIndicatorColor(viewFinderTopRightIndicator, color) setIndicatorColor(viewFinderBottomLeftIndicator, color) setIndicatorColor(viewFinderBottomRightIndicator, color) } private fun setIndicatorColor(indicator: View, @ColorRes color: Int) { val drawable = indicator.background as LayerDrawable val stroke = drawable.findDrawableByLayerId(R.id.indicator) as GradientDrawable stroke.setStroke( resources.getDimensionPixelSize(R.dimen.qr_scanner_indicator_stroke_width), resources.getColor(color, null) ) } enum class CameraPermissionState { GRANTED, REQUESTING, CANCELLED, DENIED, } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/qr/QrScannerState.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.qr enum class QrScannerState { NO_CODE_FOUND, VALID, INVALID_FORMAT, } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/settings/SettingsFragment.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.settings import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import ch.admin.bag.covidcertificate.common.R import ch.admin.bag.covidcertificate.common.data.ConfigSecureStorage import ch.admin.bag.covidcertificate.common.databinding.FragmentSettingsBinding import ch.admin.bag.covidcertificate.common.databinding.ItemLanguageOptionBinding import ch.admin.bag.covidcertificate.common.util.LocaleUtil.DEFAULT_COUNTRY import java.util.* class SettingsFragment : Fragment() { companion object { fun newInstance(): SettingsFragment { return SettingsFragment() } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return FragmentSettingsBinding.inflate(inflater, container, false).apply { settingsToolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } val currentLanguage = getString(R.string.language_key) listOf("de", "fr", "it", "rm", "en").forEach { language -> ItemLanguageOptionBinding.inflate(inflater, languageList, true).apply { val locale = Locale(language, DEFAULT_COUNTRY) radiobutton.text = locale.getDisplayLanguage(locale).capitalize(locale) radiobutton.isChecked = locale.language == currentLanguage radiobutton.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { updateLanguage(language) } } } } }.root } private fun updateLanguage(language: String) { ConfigSecureStorage.getInstance(requireContext()).setUserLanguage(language) requireActivity().recreate() } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/AssetUtil.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util import android.content.Context import ch.admin.bag.covidcertificate.common.R import ch.admin.bag.covidcertificate.common.config.ConfigModel import ch.admin.bag.covidcertificate.common.html.BuildInfo import com.squareup.moshi.Moshi import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader import java.text.SimpleDateFormat import java.util.* object AssetUtil { private const val PREFIX_ASSET_FILE = "file:///android_asset/" private const val FOLDER_NAME_IMPRESSUM = "impressum/" private const val FILE_NAME_IMPRESSUM = "impressum.html" private const val FOLDER_NAME_DISCLAIMER = "disclaimer/" private const val DISCLAIMER_FALLBACK_LANGUAGE = "de" private const val FILE_NAME_DATA_PROTECTION_STATEMENT = "data_protection_statement.html" private const val FILE_NAME_TERMS_OF_USE = "terms_of_use.html" private const val REPLACE_STRING_APP_NAME = "{APP_NAME}" private const val REPLACE_STRING_VERSION = "{VERSION}" private const val REPLACE_STRING_APPVERSION = "{APPVERSION}" private const val REPLACE_STRING_RELEASEDATE = "{RELEASEDATE}" private const val REPLACE_STRING_BUILDNR = "{BUILD}" private const val REPLACE_STRING_LAW_LINK = "{LAW_LINK}" private const val REPLACE_STRING_APP_IDENTIFIER = "{PARAM_APP_IDENTIFIER}" private val RELEASE_DATE_FORMAT = SimpleDateFormat("dd.MM.yyyy").apply { timeZone = TimeZone.getTimeZone("Europe/Zurich") } private const val ASSET_FILENAME_DEFAULT_CONFIG = "faq/config.json" private fun loadAssetJson(context: Context, filename: String): String? { return try { val inputStream = context.assets.open(filename) val json = inputStream.bufferedReader().use { it.readText() } inputStream.close() json } catch (ex: IOException) { ex.printStackTrace() null } } fun loadDefaultConfig(context: Context): ConfigModel? = loadAssetJson(context, ASSET_FILENAME_DEFAULT_CONFIG)?.let { Moshi.Builder().build().adapter(ConfigModel::class.java).fromJson(it) } fun getImpressumBaseUrl(context: Context): String { return PREFIX_ASSET_FILE + getFolderNameImpressum(context) } private fun getFolderNameImpressum(context: Context): String { return FOLDER_NAME_IMPRESSUM + context.getString(R.string.language_key) + "/" } fun getImpressumHtml(context: Context, buildInfo: BuildInfo): String? { return loadImpressumHtmlFile(context, FILE_NAME_IMPRESSUM, buildInfo) } fun loadImpressumHtmlFile( context: Context, filename: String, buildInfo: BuildInfo, ): String? { return try { val html = StringBuilder() BufferedReader(InputStreamReader(context.assets.open(getFolderNameImpressum(context) + filename))).use { reader -> var line: String? while (reader.readLine().also { line = it } != null) { html.append(line) } } var impressum = html.toString() val buildString = "${buildInfo.buildTime} / ${buildInfo.flavor}" impressum = impressum.replace(REPLACE_STRING_VERSION, buildInfo.versionName) impressum = impressum.replace(REPLACE_STRING_APPVERSION, buildInfo.versionName) impressum = impressum.replace(REPLACE_STRING_RELEASEDATE, RELEASE_DATE_FORMAT.format(buildInfo.buildTime)) impressum = impressum.replace(REPLACE_STRING_BUILDNR, buildString) impressum = impressum.replace(REPLACE_STRING_APP_NAME, buildInfo.appName) impressum = impressum.replace(REPLACE_STRING_LAW_LINK, buildInfo.agbUrl) impressum = impressum.replace(REPLACE_STRING_APP_IDENTIFIER, buildInfo.appIdentifier) impressum } catch (e: IOException) { e.printStackTrace() "" } } private fun getFolderNameDisclaimer(context: Context): String { return FOLDER_NAME_DISCLAIMER + context.getString(R.string.language_key) + "/" } private fun getDefaultLanguageFolderNameDisclaimer(): String { return "$FOLDER_NAME_DISCLAIMER$DISCLAIMER_FALLBACK_LANGUAGE/" } fun getTermsOfUse(context: Context): String { var htmlString = loadHtml( context, getFolderNameDisclaimer(context) + FILE_NAME_TERMS_OF_USE ) if (htmlString == null) htmlString = loadHtml( context, getDefaultLanguageFolderNameDisclaimer() + FILE_NAME_TERMS_OF_USE ) if (htmlString == null) htmlString = "" return replaceUlTags(htmlString) } fun getDataProtection(context: Context): String { var htmlString = loadHtml( context, getFolderNameDisclaimer(context) + FILE_NAME_DATA_PROTECTION_STATEMENT ) if (htmlString == null) htmlString = loadHtml( context, getDefaultLanguageFolderNameDisclaimer() + FILE_NAME_DATA_PROTECTION_STATEMENT ) if (htmlString == null) htmlString = "" return replaceUlTags(htmlString) } private fun replaceUlTags(htmlString: String): String { return htmlString.replace("
    ", "").replace("
", "").replace("
  • ", "") .replace("
  • ", "") } private fun loadHtml(context: Context, path: String): String? { return try { val html = StringBuilder() BufferedReader(InputStreamReader(context.assets.open(path))).use { reader -> var line: String? while (reader.readLine().also { line = it } != null) { html.append(line) } } html.toString() } catch (e: IOException) { e.printStackTrace() null } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/CutOutEdgeTreatment.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util import com.google.android.material.shape.EdgeTreatment import com.google.android.material.shape.ShapePath import kotlin.math.atan import kotlin.math.sqrt class CutOutEdgeTreatment( private val radius: Float, private val positionPercentage: Float ) : EdgeTreatment() { override fun getEdgePath(length: Float, center: Float, interpolation: Float, shapePath: ShapePath) { val cutOutCenter = length * positionPercentage // Calculate the vertical offset of the cut out. When interpolating the edge, the offset is the perpendicular distance // from the cut out center to the edge. When fully interpolated (1.0), the offset will be zero, while at zero interpolation // the offset will be equal to the radius val verticalOffset = (1.0f - interpolation) * radius val verticalOffsetRatio = verticalOffset / radius if (verticalOffsetRatio >= 1.0f) { // The vertical offset is so high that there would be no cut out drawn in the edge, so just draw a straight line along the edge shapePath.lineTo(length, 0.0f) return } // Calculate the X distance between the center and edge of the cut out, taking the interpolation value into consideration // When fully interpolated (1.0) the radiusX is the same as the radius val squaredRadius = radius * radius val verticalOffsetSquared = verticalOffset * verticalOffset val radiusX = sqrt((squaredRadius - verticalOffsetSquared).toDouble()).toFloat() // Draw a line from the start of the edge to the start of the cut out val cutOutStartX = cutOutCenter - radiusX shapePath.lineTo(cutOutStartX, 0.0f) // Calculate the arc of the cut out val cornerRadiusArcLength = Math.toDegrees(atan((radiusX / verticalOffset).toDouble())).toFloat() val cutoutArcOffset = 90.0f - cornerRadiusArcLength // Draw the cut out arc shapePath.addArc( cutOutCenter - radius, -radius - verticalOffset, cutOutCenter + radius, radius - verticalOffset, 180.0f - cutoutArcOffset, cutoutArcOffset * 2.0f - 180.0f ) // Draw a line from the end of the cut out to the end of the edge shapePath.lineTo(length, 0.0f) } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/EnvironmentUtil.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util import ch.admin.bag.covidcertificate.common.BuildConfig import ch.admin.bag.covidcertificate.sdk.android.SdkEnvironment object EnvironmentUtil { fun getSdkEnvironment(flavor: String = BuildConfig.FLAVOR) = when (flavor) { "dev" -> SdkEnvironment.DEV "abn" -> SdkEnvironment.ABN "prod" -> SdkEnvironment.PROD else -> throw IllegalArgumentException("Unknown environment $flavor") } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/ErrorCodeUtil.kt ================================================ package ch.admin.bag.covidcertificate.common.util import ch.admin.bag.covidcertificate.sdk.core.models.state.CheckNationalRulesState import ch.admin.bag.covidcertificate.sdk.core.models.state.CheckRevocationState import ch.admin.bag.covidcertificate.sdk.core.models.state.CheckSignatureState import ch.admin.bag.covidcertificate.sdk.core.models.state.VerificationState fun VerificationState.getInvalidErrorCode(errorDelimiter: String = ", ", showNationalErrors: Boolean = false): String { val errorCodes = mutableListOf() if (this !is VerificationState.INVALID) return "" val signatureState = signatureState if (signatureState is CheckSignatureState.INVALID) { errorCodes.add(signatureState.signatureErrorCode) } val revocationState = revocationState if (revocationState is CheckRevocationState.INVALID) { errorCodes.add(revocationState.revocationErrorCode) } val nationalRulesState = nationalRulesState if (showNationalErrors && nationalRulesState is CheckNationalRulesState.INVALID) { nationalRulesState.nationalRulesError?.errorCode?.let { errorCodes.add(it) } } return errorCodes.joinToString(errorDelimiter) } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/ErrorHelper.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.graphics.Paint import android.net.Uri import android.provider.Settings import android.view.View import android.widget.ImageView import android.widget.TextView import android.widget.Toast import androidx.core.content.ContextCompat import ch.admin.bag.covidcertificate.common.R object ErrorHelper { @JvmOverloads fun updateErrorView( errorView: View, errorState: ErrorState, customButtonClickAction: Runnable?, context: Context?, showButton: Boolean = true ) { errorView.findViewById(R.id.error_status_title).setText(errorState.titleResId) errorView.findViewById(R.id.error_status_text).setText(errorState.textResId) errorView.findViewById(R.id.error_status_image) .setImageDrawable(ContextCompat.getDrawable(errorView.context, errorState.imageResId)) val buttonView = errorView.findViewById(R.id.error_status_button) if (showButton) { buttonView.visibility = View.VISIBLE buttonView.setText(errorState.actionResId) buttonView.paintFlags = buttonView.paintFlags or Paint.UNDERLINE_TEXT_FLAG buttonView.setOnClickListener { executeErrorAction(errorState, customButtonClickAction, context) } } else { buttonView.visibility = View.GONE } } private fun executeErrorAction(errorState: ErrorState, customButtonClickAction: Runnable?, context: Context?) { customButtonClickAction?.run() when (errorState) { ErrorState.CAMERA_ACCESS_DENIED -> openApplicationSettings(context) else -> {} } } private fun openApplicationSettings(context: Context?) { val context = context ?: return val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri = Uri.fromParts("package", context.packageName, null) intent.data = uri try { context.startActivity(intent) } catch (e: ActivityNotFoundException) { Toast.makeText(context, "Could not open settings", Toast.LENGTH_LONG).show() } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/ErrorState.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util import androidx.annotation.DrawableRes import androidx.annotation.StringRes import ch.admin.bag.covidcertificate.common.R enum class ErrorState( @field:StringRes @param:StringRes val titleResId: Int, @field:StringRes @param:StringRes val textResId: Int, @field:StringRes @param:StringRes val actionResId: Int, @field:DrawableRes @param:DrawableRes val imageResId: Int ) { NETWORK( R.string.error_network_title, R.string.error_network_text, R.string.error_action_retry, R.drawable.ic_error_triangle ), CAMERA_ACCESS_DENIED( R.string.error_camera_permission_title, R.string.error_camera_permission_text, R.string.error_action_change_settings, R.drawable.ic_cam_off ), NO_VALID_QR_CODE( R.string.error_title, R.string.qr_scanner_error, R.string.ok_button, R.drawable.ic_error_triangle ); } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/HorizontalMarginItemDecoration.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util import android.content.Context import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView class HorizontalMarginItemDecoration(context: Context, val horizontalMarginInPx: Int) : RecyclerView.ItemDecoration() { override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { outRect.right = horizontalMarginInPx outRect.left = horizontalMarginInPx } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/LocaleUtil.kt ================================================ package ch.admin.bag.covidcertificate.common.util import android.content.Context import android.content.res.Configuration import ch.admin.bag.covidcertificate.common.R import java.util.* object LocaleUtil { const val DEFAULT_COUNTRY = "CH" fun isSystemLangNotEnglish(context: Context): Boolean { return context.getString(R.string.language_key) != "en" } fun updateLanguage(context: Context, language: String?): Context { if (!language.isNullOrEmpty()) { val config = Configuration() config.setLocale(Locale(language, DEFAULT_COUNTRY)) return context.createConfigurationContext(config) } return context } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/SingleLiveEvent.java ================================================ /* * Copyright 2017 Google Inc. * * 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 ch.admin.bag.covidcertificate.common.util; import android.util.Log; import androidx.annotation.MainThread; import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import java.util.concurrent.atomic.AtomicBoolean; /** * A lifecycle-aware observable that sends only new updates after subscription, used for events like * navigation and Snackbar messages. *

    * This avoids a common problem with events: on configuration change (like rotation) an update * can be emitted if the observer is active. This LiveData only calls the observable if there's an * explicit call to setValue() or call(). *

    * Note that only one observer is going to be notified of changes. */ public class SingleLiveEvent extends MutableLiveData { private static final String TAG = "SingleLiveEvent"; private final AtomicBoolean mPending = new AtomicBoolean(false); @MainThread public void observe(LifecycleOwner owner, final Observer observer) { if (hasActiveObservers()) { Log.w(TAG, "Multiple observers registered but only one will be notified of changes."); } // Observe the internal MutableLiveData super.observe(owner, new Observer() { @Override public void onChanged(@Nullable T t) { if (mPending.compareAndSet(true, false)) { observer.onChanged(t); } } }); } @MainThread public void setValue(@Nullable T t) { mPending.set(true); super.setValue(t); } /** * Used for cases where T is Void, to make calls cleaner. */ @MainThread public void call() { setValue(null); } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/StringUtil.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util import android.graphics.Typeface import android.text.SpannableString import android.text.style.StyleSpan import ch.admin.bag.covidcertificate.sdk.android.extensions.DEFAULT_DISPLAY_DATE_FORMATTER import ch.admin.bag.covidcertificate.sdk.android.extensions.DEFAULT_DISPLAY_DATE_TIME_FORMATTER import java.time.LocalDateTime fun String.makeBold(): SpannableString = SpannableString(this).apply { setSpan(StyleSpan(Typeface.BOLD), 0, this@makeBold.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE) } fun String.makeSubStringBold( subString: String, startIndex: Int = 0, ignoreCase: Boolean = true ): SpannableString = SpannableString(this).apply { val indexSubString = this@makeSubStringBold.indexOf(subString, startIndex, ignoreCase) if (indexSubString >= 0) { setSpan( StyleSpan(Typeface.BOLD), indexSubString, indexSubString + subString.length, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE ) } } fun String.makeSubStringsBold( subStrings: List, startIndex: Int = 0, ignoreCase: Boolean = true ): SpannableString = SpannableString(this).apply { subStrings.forEach { val indexSubString = this@makeSubStringsBold.indexOf(it, startIndex, ignoreCase) if (indexSubString >= 0) { setSpan( StyleSpan(Typeface.BOLD), indexSubString, indexSubString + it.length, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE ) } } } fun String.addBoldDate(dateReplacementString: String, date: LocalDateTime): SpannableString { val dateString = date.format(DEFAULT_DISPLAY_DATE_FORMATTER) return SpannableString(this.replace(dateReplacementString, dateString)).apply { val indexSubString = this.indexOf(dateString) if (indexSubString >= 0) { setSpan( StyleSpan(Typeface.BOLD), indexSubString, indexSubString + dateString.length, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE ) } } } fun String.addBoldDateTime(dateTimeReplacementString: String, dateTime: LocalDateTime): SpannableString { val dateTimeString = dateTime.format(DEFAULT_DISPLAY_DATE_TIME_FORMATTER) return SpannableString(this.replace(dateTimeReplacementString, dateTimeString)).apply { val indexSubString = this.indexOf(dateTimeString) if (indexSubString >= 0) { setSpan( StyleSpan(Typeface.BOLD), indexSubString, indexSubString + dateTimeString.length, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE ) } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/UiUtil.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util import android.view.Window import android.view.WindowManager private val allowlistedFlavours = listOf("abn", "dev") fun Window.setSecureFlagToBlockScreenshots(flavor: String) { if (!allowlistedFlavours.contains(flavor)) { setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/UlTagHandler.java ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util; import android.text.Editable; import android.text.Html; import android.text.Spanned; import android.text.style.BulletSpan; import org.xml.sax.XMLReader; public class UlTagHandler implements Html.TagHandler { private static final int indent = 30; public static String LI = "myli"; @Override public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { if (tag.equals(LI)) { if (opening) { if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') { output.append("\n"); } start(output, new Ul()); } else { if (output.charAt(output.length() - 1) != '\n') { output.append("\n"); } BulletSpan newBullet = new BulletSpan(indent); end(output, Ul.class, newBullet); } } } private static void start(Editable text, Object mark) { int len = text.length(); text.setSpan(mark, len, len, Spanned.SPAN_MARK_MARK); } private static void end(Editable text, Class kind, Object... replaces) { int len = text.length(); Object obj = getLast(text, kind); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { for (Object replace : replaces) { text.setSpan(replace, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private static Object getLast(Spanned text, Class kind) { Object[] objs = text.getSpans(0, text.length(), kind); if (objs.length == 0) { return null; } return objs[objs.length - 1]; } private static class Ul { } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/util/UrlUtil.java ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.util; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.widget.Toast; public class UrlUtil { public static void openUrl(Context context, String url) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); try { context.startActivity(intent); } catch (ActivityNotFoundException e) { Toast.makeText(context, "No browser installed", Toast.LENGTH_LONG).show(); } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/views/MarginItemDecoration.kt ================================================ package ch.admin.bag.covidcertificate.common.views import android.content.Context import android.graphics.Rect import android.view.View import androidx.annotation.DimenRes import androidx.recyclerview.widget.RecyclerView class VerticalMarginItemDecoration(context: Context, @DimenRes marginRes: Int) : RecyclerView.ItemDecoration() { private val marginPx = context.resources.getDimensionPixelSize(marginRes) override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { with(outRect) { if (parent.getChildAdapterPosition(view) != 0) { top = marginPx } } } } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/views/ViewExtensions.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.views import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.res.ColorStateList import android.view.View import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateInterpolator import android.view.animation.DecelerateInterpolator import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import ch.admin.bag.covidcertificate.common.R import ch.admin.bag.covidcertificate.common.util.CutOutEdgeTreatment import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel fun View.showAnimated( duration: Long = resources.getInteger(android.R.integer.config_shortAnimTime).toLong(), fade: Boolean = true ) { animation?.cancel() if (visibility == View.VISIBLE) return visibility = View.VISIBLE alpha = if (fade) 0f else 1f animate() .setDuration(duration) .alpha(1f) .setInterpolator(DecelerateInterpolator()) .setListener(null) } fun View.hideAnimated( duration: Long = resources.getInteger(android.R.integer.config_shortAnimTime).toLong(), fade: Boolean = true ) { animation?.cancel() animate() .setDuration((duration * alpha).toLong()) .alpha(if (fade) 0f else 1f) .setInterpolator(AccelerateInterpolator()) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { visibility = View.GONE } }) } fun View.rotate( toDegrees: Float, duration: Long = resources.getInteger(android.R.integer.config_shortAnimTime).toLong(), resetToDegrees: Float? = null ) { animation?.cancel() animate() .setDuration(duration) .rotation(toDegrees) .setInterpolator(AccelerateDecelerateInterpolator()) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { resetToDegrees?.let { rotation = it } } }) } fun List.animateBackgroundTintColor( @ColorInt targetColor: Int, duration: Long = get(0).resources.getInteger(android.R.integer.config_shortAnimTime).toLong() ) { val startColor = get(0).backgroundTintList?.defaultColor ?: 0 val colorAnim = ValueAnimator.ofArgb(startColor, targetColor).apply { this.duration = duration addUpdateListener { animator -> val backgroundTintList = ColorStateList.valueOf(animator.animatedValue as Int) this@animateBackgroundTintColor.forEach { it.backgroundTintList = backgroundTintList } } } colorAnim.start() } fun View.animateBackgroundTintColor( @ColorInt targetColor: Int, duration: Long = resources.getInteger(android.R.integer.config_shortAnimTime).toLong() ) { val startColor = backgroundTintList?.defaultColor ?: 0 val colorAnim = ValueAnimator.ofArgb(startColor, targetColor).apply { this.duration = duration addUpdateListener { animator -> backgroundTintList = ColorStateList.valueOf(animator.animatedValue as Int) } } colorAnim.start() } fun View.setCutOutCardBackground() { val cutOutPositionPercentage = 0.6f val cutOutRadius = context.resources.getDimension(R.dimen.card_cutout_radius) // The left edge measures from the top to the bottom while the right edge measures from the bottom to the top val backgroundShape = ShapeAppearanceModel.builder() .setAllCornerSizes(context.resources.getDimension(R.dimen.corner_radius_sheet)) .setLeftEdge(CutOutEdgeTreatment(cutOutRadius, 1 - cutOutPositionPercentage)) .setRightEdge(CutOutEdgeTreatment(cutOutRadius, cutOutPositionPercentage)) .build() val backgroundDrawable = MaterialShapeDrawable(backgroundShape).apply { fillColor = ContextCompat.getColorStateList(context, R.color.white) elevation = context.resources.getDimension(R.dimen.certificates_elevation) shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS } background = backgroundDrawable } ================================================ FILE: common/src/main/java/ch/admin/bag/covidcertificate/common/views/WindowInsetsLayout.kt ================================================ /* * Copyright (c) 2021 Ubique Innovation AG * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ package ch.admin.bag.covidcertificate.common.views import android.content.Context import android.graphics.Rect import android.util.AttributeSet import android.view.WindowInsets import android.widget.FrameLayout import androidx.core.view.children import ch.admin.bag.covidcertificate.common.R /** * A FrameLayout that selectively applies the system window insets to itself. * * `applyWindowInsets` defines on which edges the insets are applied to this layout. * Can be one or more of `left`, `top`, `right` or `bottom`, * or `all` to include all four edges; default is `none`. * * Insets that have not been consumed are passed down to all child views * (independent of each other, not just the first one that consumes it, * compared to the standard behaviour of `fitsSystemWindows`). */ class WindowInsetsLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { private var applyInsetEdges: Int init { val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.WindowInsetsLayout, 0, 0) applyInsetEdges = typedArray.getInteger(R.styleable.WindowInsetsLayout_applyWindowInsets, INSETS_NOWHERE) typedArray.recycle() } override fun onAttachedToWindow() { super.onAttachedToWindow() requestApplyInsets() } override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { val appliedInset = Rect( if (applyInsetEdges and INSETS_LEFT != 0) insets.systemWindowInsetLeft else 0, if (applyInsetEdges and INSETS_TOP != 0) insets.systemWindowInsetTop else 0, if (applyInsetEdges and INSETS_RIGHT != 0) insets.systemWindowInsetRight else 0, if (applyInsetEdges and INSETS_BOTTOM != 0) insets.systemWindowInsetBottom else 0 ) setPadding(appliedInset.left, appliedInset.top, appliedInset.right, appliedInset.bottom) if (applyInsetEdges == INSETS_EVERYWHERE) { return insets } val remainingInsets = insets.replaceSystemWindowInsets( Rect( insets.systemWindowInsetLeft - appliedInset.left, insets.systemWindowInsetTop - appliedInset.top, insets.systemWindowInsetRight - appliedInset.right, insets.systemWindowInsetBottom - appliedInset.bottom ) ) children.forEach { it.dispatchApplyWindowInsets(remainingInsets) } return insets } fun setInsets(insetEdges: Int) { applyInsetEdges = insetEdges requestApplyInsets() } companion object { const val INSETS_NOWHERE = 0x0 const val INSETS_EVERYWHERE = 0xF const val INSETS_LEFT = 0x1 const val INSETS_TOP = 0x2 const val INSETS_RIGHT = 0x4 const val INSETS_BOTTOM = 0x8 } } ================================================ FILE: common/src/main/res/anim/fragment_open_enter.xml ================================================ ================================================ FILE: common/src/main/res/anim/fragment_open_exit.xml ================================================ ================================================ FILE: common/src/main/res/anim/slide_enter.xml ================================================ ================================================ FILE: common/src/main/res/anim/slide_exit.xml ================================================ ================================================ FILE: common/src/main/res/anim/slide_pop_enter.xml ================================================ ================================================ FILE: common/src/main/res/anim/slide_pop_exit.xml ================================================ ================================================ FILE: common/src/main/res/color/selector_black_or_white.xml ================================================ ================================================ FILE: common/src/main/res/color/selector_blue_or_white.xml ================================================ ================================================ FILE: common/src/main/res/color/selector_grey_or_blue.xml ================================================ ================================================ FILE: common/src/main/res/color/selector_grey_or_white.xml ================================================ ================================================ FILE: common/src/main/res/color/selector_transparent_or_blue.xml ================================================ ================================================ FILE: common/src/main/res/color/selector_white_or_black.xml ================================================ ================================================ FILE: common/src/main/res/color/selector_white_or_blue.xml ================================================ ================================================ FILE: common/src/main/res/color/text_radio_checkable.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_button_default.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_button_red.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_button_white.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_corners_top_left.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_dialog.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_pill.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_rect_rounded_sheet.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_rect_rounded_small.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_rect_rounded_small_blue_ripple.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_rect_rounded_small_checkable.xml ================================================ ================================================ FILE: common/src/main/res/drawable/bg_rect_rounded_small_red.xml ================================================ ================================================ FILE: common/src/main/res/drawable/btn_radio_checkable.xml ================================================ ================================================ FILE: common/src/main/res/drawable/dot_black.xml ================================================ ================================================ FILE: common/src/main/res/drawable/dot_grey.xml ================================================ ================================================ FILE: common/src/main/res/drawable/dot_white.xml ================================================ ================================================ FILE: common/src/main/res/drawable/header_bottom.xml ================================================ ================================================ FILE: common/src/main/res/drawable/header_collapsed_shadow.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_1g.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_2g.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_2g_green.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_2g_grey.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_2g_plus.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_3g.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_arrow_contract.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_arrow_expand.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_arrow_forward.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_bund_small.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_bundwappen_big.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_call.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_cam_off.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_camera_switch.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_check_filled.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_check_green.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_check_grey.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_check_large.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_checkbox_empty.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_checkbox_filled.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_close.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_close_red.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_dot.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_double_check.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_error.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_error_blue.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_error_grey.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_error_large.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_error_orange.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_error_triangle.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_expire_i.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_faq.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_header_slim.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_how_it_works_image.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_info.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_info_alert.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_info_blue.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_info_outline.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_invalid_grey.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_invalid_red.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_light_off.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_light_off_blue.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_light_on.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_light_on_black.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_link_external.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_load.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_no1g.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_no2g.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_no3g.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_no_2_g_plus_height.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_no_connection.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_no_connection_large.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_notification.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_notification_filled.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_offline.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_offline_large.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_offline_orange.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_one.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_phone.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_plus.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_plus_green.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_privacy.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_privacy_grey.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_process_error.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_process_error_grey.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_process_error_large.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_qr_certificate_light.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_qr_certificate_light_no.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_question_outline.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_retry.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_scanner_alert.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_scanner_alert_white.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_settings.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_t.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_three.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_timeerror.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_timeerror_large.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_timeerror_orange.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_timelapse.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_timelapse_blue.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_timelapse_red.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_travel.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_two.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_zoom_off.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_zoom_off_white.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_zoom_on.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ic_zoom_on_black.xml ================================================ ================================================ FILE: common/src/main/res/drawable/illu_onboarding_data_protection.xml ================================================ ================================================ FILE: common/src/main/res/drawable/line_dashed_grey.xml ================================================ ================================================ FILE: common/src/main/res/drawable/qr_scanner_bottom_left.xml ================================================ ================================================ FILE: common/src/main/res/drawable/qr_scanner_bottom_right.xml ================================================ ================================================ FILE: common/src/main/res/drawable/qr_scanner_top_left.xml ================================================ ================================================ FILE: common/src/main/res/drawable/qr_scanner_top_right.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ripple_rect.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ripple_rounded.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ripple_rounded_button.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ripple_rounded_rect.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ripple_rounded_rect_banner.xml ================================================ ================================================ FILE: common/src/main/res/drawable/ripple_rounded_rect_small.xml ================================================ ================================================ FILE: common/src/main/res/drawable/tab_selector.xml ================================================ ================================================ FILE: common/src/main/res/drawable/tab_selector_white.xml ================================================ ================================================ FILE: common/src/main/res/font/inter.xml ================================================ ================================================ FILE: common/src/main/res/layout/activity_onboarding.xml ================================================ ================================================ FILE: common/src/main/res/layout/dialog_camera_permission_explanation.xml ================================================ ================================================ FILE: common/src/main/res/layout/dialog_fragment_info_box.xml ================================================