Full Code of covid19cz/erouska-android for AI

develop 23064413869f cached
355 files
1.3 MB
435.7k tokens
4 symbols
1 requests
Download .txt
Showing preview only (1,471K chars total). Download the full file or copy to clipboard to get everything.
Repository: covid19cz/erouska-android
Branch: develop
Commit: 23064413869f
Files: 355
Total size: 1.3 MB

Directory structure:
gitextract_yznxotpf/

├── .github/
│   ├── pull_request_template.txt
│   ├── stale.yml
│   └── workflows/
│       ├── deploy-develop.yml
│       └── deploy-master.yml
├── .gitignore
├── .idea/
│   └── codeStyles/
│       └── codeStyleConfig.xml
├── LICENSE
├── README.md
├── app/
│   ├── build.gradle
│   ├── libs/
│   │   └── play-services-nearby-exposurenotification-1.7.2-eap.aar
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── kotlin/
│       │       └── cz/
│       │           └── covid19cz/
│       │               └── erouska/
│       │                   ├── helpers/
│       │                   │   ├── Actions.kt
│       │                   │   ├── ClickableLink.kt
│       │                   │   └── TextMatchesIgnoringWhitespaceType.kt
│       │                   ├── screens/
│       │                   │   ├── A1Screen.kt
│       │                   │   ├── A2Screen.kt
│       │                   │   ├── A3Screen.kt
│       │                   │   ├── B1Screen.kt
│       │                   │   └── N1Screen.kt
│       │                   ├── testRules/
│       │                   │   └── DisableAnimationsRule.kt
│       │                   └── tests/
│       │                       └── ActivationTest.kt
│       ├── dev/
│       │   ├── google-services.json
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── ic_launcher_background.xml
│       │       │   └── ic_launcher_foreground.xml
│       │       └── values/
│       │           ├── controls.xml
│       │           └── strings.xml
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── kotlin/
│       │   │   └── cz/
│       │   │       └── covid19cz/
│       │   │           └── erouska/
│       │   │               ├── App.kt
│       │   │               ├── AppConfig.kt
│       │   │               ├── DI.kt
│       │   │               ├── db/
│       │   │               │   ├── DailySummariesDb.kt
│       │   │               │   ├── DailySummaryDao.kt
│       │   │               │   ├── DailySummaryEntity.kt
│       │   │               │   └── SharedPrefsRepository.kt
│       │   │               ├── exposurenotifications/
│       │   │               │   ├── ExposureCryptoTools.kt
│       │   │               │   ├── ExposureNotificationsErrorHandling.kt
│       │   │               │   ├── ExposureNotificationsRepository.kt
│       │   │               │   ├── Notifications.kt
│       │   │               │   ├── receiver/
│       │   │               │   │   └── ExposureNotificationBroadcastReceiver.kt
│       │   │               │   ├── service/
│       │   │               │   │   └── PushService.kt
│       │   │               │   └── worker/
│       │   │               │       ├── DownloadKeysWorker.kt
│       │   │               │       └── SelfCheckerWorker.kt
│       │   │               ├── ext/
│       │   │               │   ├── ByteArray.kt
│       │   │               │   ├── Context.kt
│       │   │               │   ├── Int.kt
│       │   │               │   ├── Long.kt
│       │   │               │   ├── Rx.kt
│       │   │               │   ├── String.kt
│       │   │               │   └── View.kt
│       │   │               ├── net/
│       │   │               │   ├── ExposureServerRepository.kt
│       │   │               │   ├── FirebaseFunctionsRepository.kt
│       │   │               │   ├── api/
│       │   │               │   │   ├── KeyServerApi.kt
│       │   │               │   │   └── VerificationServerApi.kt
│       │   │               │   ├── exception/
│       │   │               │   │   └── UnauthrorizedException.kt
│       │   │               │   └── model/
│       │   │               │       ├── CovidDataModel.kt
│       │   │               │       ├── DownloadedKeys.kt
│       │   │               │       ├── KeyServerModel.kt
│       │   │               │       └── VerificationServerModel.kt
│       │   │               ├── ui/
│       │   │               │   ├── about/
│       │   │               │   │   ├── AboutFragment.kt
│       │   │               │   │   ├── AboutVM.kt
│       │   │               │   │   └── entity/
│       │   │               │   │       └── AboutProfileItem.kt
│       │   │               │   ├── activation/
│       │   │               │   │   ├── ActivationFragment.kt
│       │   │               │   │   ├── ActivationNotificationsFragment.kt
│       │   │               │   │   ├── ActivationNotificationsVM.kt
│       │   │               │   │   ├── ActivationState.kt
│       │   │               │   │   └── ActivationVM.kt
│       │   │               │   ├── base/
│       │   │               │   │   ├── BaseActivity.kt
│       │   │               │   │   ├── BaseFragment.kt
│       │   │               │   │   ├── BaseVM.kt
│       │   │               │   │   └── UrlEvent.kt
│       │   │               │   ├── contacts/
│       │   │               │   │   ├── Contact.kt
│       │   │               │   │   ├── ContactsFragment.kt
│       │   │               │   │   ├── ContactsVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── ContactsCommandEvent.kt
│       │   │               │   ├── dashboard/
│       │   │               │   │   ├── DashboardCardView.kt
│       │   │               │   │   ├── DashboardFragment.kt
│       │   │               │   │   ├── DashboardVM.kt
│       │   │               │   │   ├── TravellerDashboardCardView.kt
│       │   │               │   │   └── event/
│       │   │               │   │       ├── DashboardCommandEvent.kt
│       │   │               │   │       ├── DisabledEvent.kt
│       │   │               │   │       └── GmsApiErrorEvent.kt
│       │   │               │   ├── efgs/
│       │   │               │   │   ├── EfgsFragment.kt
│       │   │               │   │   ├── EfgsVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── EfgsCommandEvent.kt
│       │   │               │   ├── efgsagreement/
│       │   │               │   │   ├── EfgsAgreementFragment.kt
│       │   │               │   │   └── EfgsAgreementVM.kt
│       │   │               │   ├── error/
│       │   │               │   │   ├── ErrorFragment.kt
│       │   │               │   │   ├── ErrorVM.kt
│       │   │               │   │   └── entity/
│       │   │               │   │       └── ErrorType.kt
│       │   │               │   ├── exposure/
│       │   │               │   │   ├── ExposureFragment.kt
│       │   │               │   │   ├── ExposureVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── ExposuresEvent.kt
│       │   │               │   ├── exposurehelp/
│       │   │               │   │   ├── ExposureHelpFragment.kt
│       │   │               │   │   ├── ExposureHelpVM.kt
│       │   │               │   │   └── entity/
│       │   │               │   │       ├── ExposureHelpData.kt
│       │   │               │   │       ├── ExposureHelpItem.kt
│       │   │               │   │       ├── ExposureHelpTitle.kt
│       │   │               │   │       └── ExposureHelpType.kt
│       │   │               │   ├── exposureinfo/
│       │   │               │   │   ├── ExposureInfoFragment.kt
│       │   │               │   │   └── ExposureInfoVM.kt
│       │   │               │   ├── help/
│       │   │               │   │   ├── HelpFragment.kt
│       │   │               │   │   ├── HelpVM.kt
│       │   │               │   │   └── data/
│       │   │               │   │       ├── AboutAppCategory.kt
│       │   │               │   │       ├── Category.kt
│       │   │               │   │       ├── FaqCategory.kt
│       │   │               │   │       ├── HowToCategory.kt
│       │   │               │   │       └── Question.kt
│       │   │               │   ├── helpcategory/
│       │   │               │   │   ├── HelpCategoryFragment.kt
│       │   │               │   │   └── HelpCategoryVM.kt
│       │   │               │   ├── helpquestion/
│       │   │               │   │   ├── HelpQuestionFragment.kt
│       │   │               │   │   └── HelpQuestionVM.kt
│       │   │               │   ├── helpsearch/
│       │   │               │   │   ├── HelpSearchFragment.kt
│       │   │               │   │   ├── HelpSearchVM.kt
│       │   │               │   │   ├── data/
│       │   │               │   │   │   └── SearchableQuestion.kt
│       │   │               │   │   └── ui/
│       │   │               │   │       └── SearchableItem.kt
│       │   │               │   ├── how/
│       │   │               │   │   ├── HowItWorksFragment.kt
│       │   │               │   │   ├── HowItWorksVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── HowItWorksEvent.kt
│       │   │               │   ├── main/
│       │   │               │   │   ├── MainActivity.kt
│       │   │               │   │   ├── MainActivityOld.kt
│       │   │               │   │   └── MainVM.kt
│       │   │               │   ├── mydata/
│       │   │               │   │   ├── CaseItemView.kt
│       │   │               │   │   ├── MyDataFragment.kt
│       │   │               │   │   └── MyDataVM.kt
│       │   │               │   ├── noverificationcode/
│       │   │               │   │   ├── NoVerificationCodeFragment.kt
│       │   │               │   │   ├── NoVerificationCodeVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── NoVerificationCodeEvent.kt
│       │   │               │   ├── permissions/
│       │   │               │   │   └── BasePermissionsFragment.kt
│       │   │               │   ├── publishsuccess/
│       │   │               │   │   ├── PublishSuccessFragment.kt
│       │   │               │   │   └── PublishSuccessVM.kt
│       │   │               │   ├── ragnarok/
│       │   │               │   │   └── RagnarokVM.kt
│       │   │               │   ├── recentexposures/
│       │   │               │   │   ├── RecentExposuresFragment.kt
│       │   │               │   │   ├── RecentExposuresVM.kt
│       │   │               │   │   └── entity/
│       │   │               │   │       └── RecentExposureGroupHeaderItem.kt
│       │   │               │   ├── sandbox/
│       │   │               │   │   ├── SandboxConfigFragment.kt
│       │   │               │   │   ├── SandboxConfigVM.kt
│       │   │               │   │   ├── SandboxConfigValues.kt
│       │   │               │   │   ├── SandboxDataFragment.kt
│       │   │               │   │   ├── SandboxDataVM.kt
│       │   │               │   │   ├── SandboxFragment.kt
│       │   │               │   │   ├── SandboxVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── SnackbarEvent.kt
│       │   │               │   ├── symptomdate/
│       │   │               │   │   ├── SymptomDateFragment.kt
│       │   │               │   │   ├── SymptomDateVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       ├── DatePickerEvent.kt
│       │   │               │   │       └── SymptomDateCommandEvent.kt
│       │   │               │   ├── traveller/
│       │   │               │   │   ├── TravellerFragment.kt
│       │   │               │   │   └── TravellerVM.kt
│       │   │               │   ├── update/
│       │   │               │   │   ├── efgs/
│       │   │               │   │   │   ├── EfgsUpdateFragment.kt
│       │   │               │   │   │   └── EfgsUpdateVM.kt
│       │   │               │   │   └── playservices/
│       │   │               │   │       ├── UpdatePlayServicesFragment.kt
│       │   │               │   │       ├── UpdatePlayServicesVM.kt
│       │   │               │   │       └── event/
│       │   │               │   │           └── UpdatePlayServicesEvent.kt
│       │   │               │   ├── verification/
│       │   │               │   │   ├── InvalidTokenException.kt
│       │   │               │   │   ├── NoKeysException.kt
│       │   │               │   │   ├── ReportExposureException.kt
│       │   │               │   │   ├── VerificationFragment.kt
│       │   │               │   │   ├── VerificationVM.kt
│       │   │               │   │   ├── VerifyException.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── VerificationCommandEvent.kt
│       │   │               │   └── welcome/
│       │   │               │       ├── WelcomeFragment.kt
│       │   │               │       ├── WelcomeVM.kt
│       │   │               │       └── event/
│       │   │               │           └── WelcomeCommandEvent.kt
│       │   │               └── utils/
│       │   │                   ├── Analytics.kt
│       │   │                   ├── CustomTabHelper.kt
│       │   │                   ├── DeviceInfo.kt
│       │   │                   ├── L.kt
│       │   │                   ├── LocaleUtils.kt
│       │   │                   ├── Markdown.kt
│       │   │                   ├── MiscUtils.kt
│       │   │                   ├── SharedPrefsLiveData.kt
│       │   │                   ├── SupportEmailGenerator.kt
│       │   │                   ├── Text.kt
│       │   │                   └── ViewUtils.kt
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── highlight_selector.xml
│       │       │   ├── ic_about.xml
│       │       │   ├── ic_ack_case.xml
│       │       │   ├── ic_act_case.xml
│       │       │   ├── ic_action_close.xml
│       │       │   ├── ic_action_up.xml
│       │       │   ├── ic_active.xml
│       │       │   ├── ic_antigen.xml
│       │       │   ├── ic_arrow_right.xml
│       │       │   ├── ic_balloon.xml
│       │       │   ├── ic_bluetooth_onboard.xml
│       │       │   ├── ic_calendar.xml
│       │       │   ├── ic_chat.xml
│       │       │   ├── ic_confirm.xml
│       │       │   ├── ic_contacts.xml
│       │       │   ├── ic_control.xml
│       │       │   ├── ic_cured.xml
│       │       │   ├── ic_data.xml
│       │       │   ├── ic_death_toll.xml
│       │       │   ├── ic_encounter.xml
│       │       │   ├── ic_error.xml
│       │       │   ├── ic_eval.xml
│       │       │   ├── ic_exposure_info.xml
│       │       │   ├── ic_face_mask.xml
│       │       │   ├── ic_google_play_services.xml
│       │       │   ├── ic_help.xml
│       │       │   ├── ic_home.xml
│       │       │   ├── ic_hospitalized.xml
│       │       │   ├── ic_how_it_works_banner.xml
│       │       │   ├── ic_injection_complete.xml
│       │       │   ├── ic_injection_first.xml
│       │       │   ├── ic_item_empty.xml
│       │       │   ├── ic_launcher_background.xml
│       │       │   ├── ic_launcher_foreground.xml
│       │       │   ├── ic_mask.xml
│       │       │   ├── ic_mzcr.xml
│       │       │   ├── ic_no_risky_encounter.xml
│       │       │   ├── ic_notif.xml
│       │       │   ├── ic_notifications_sent.xml
│       │       │   ├── ic_notifications_shown.xml
│       │       │   ├── ic_off_bluetooth.xml
│       │       │   ├── ic_off_location.xml
│       │       │   ├── ic_pause.xml
│       │       │   ├── ic_positive.xml
│       │       │   ├── ic_prevention.xml
│       │       │   ├── ic_privacy.xml
│       │       │   ├── ic_restriction.xml
│       │       │   ├── ic_risky_encounter.xml
│       │       │   ├── ic_shortcut_resume.xml
│       │       │   ├── ic_splashscreen_hands.xml
│       │       │   ├── ic_splashscreen_logo.xml
│       │       │   ├── ic_symptoms.xml
│       │       │   ├── ic_test.xml
│       │       │   ├── ic_travel.xml
│       │       │   ├── ic_update_expansion.xml
│       │       │   ├── ic_vacc.xml
│       │       │   ├── ic_warn.xml
│       │       │   └── launchscreen.xml
│       │       ├── drawable-anydpi-v24/
│       │       │   └── ic_notification_normal.xml
│       │       ├── drawable-night/
│       │       │   └── ic_splashscreen_hands.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── activity_ragnarok.xml
│       │       │   ├── dashboard_card_view.xml
│       │       │   ├── fragment_about.xml
│       │       │   ├── fragment_activation.xml
│       │       │   ├── fragment_activation_notifications.xml
│       │       │   ├── fragment_contacts.xml
│       │       │   ├── fragment_dashboard_plus.xml
│       │       │   ├── fragment_efgs.xml
│       │       │   ├── fragment_efgs_agreement.xml
│       │       │   ├── fragment_efgs_update.xml
│       │       │   ├── fragment_error.xml
│       │       │   ├── fragment_exposure.xml
│       │       │   ├── fragment_exposure_help.xml
│       │       │   ├── fragment_exposure_info.xml
│       │       │   ├── fragment_help.xml
│       │       │   ├── fragment_help_category.xml
│       │       │   ├── fragment_help_question.xml
│       │       │   ├── fragment_help_search.xml
│       │       │   ├── fragment_how_it_works.xml
│       │       │   ├── fragment_my_data.xml
│       │       │   ├── fragment_no_verification_code.xml
│       │       │   ├── fragment_play_services_update.xml
│       │       │   ├── fragment_publish_success.xml
│       │       │   ├── fragment_recent_exposures.xml
│       │       │   ├── fragment_sandbox.xml
│       │       │   ├── fragment_sandbox_config.xml
│       │       │   ├── fragment_sandbox_data.xml
│       │       │   ├── fragment_symptom_date.xml
│       │       │   ├── fragment_traveller.xml
│       │       │   ├── fragment_verification.xml
│       │       │   ├── fragment_welcome.xml
│       │       │   ├── item_contacts.xml
│       │       │   ├── item_daily_summary.xml
│       │       │   ├── item_exposure_help.xml
│       │       │   ├── item_exposure_help_title.xml
│       │       │   ├── item_exposure_window.xml
│       │       │   ├── item_help_about_category.xml
│       │       │   ├── item_help_faq_category.xml
│       │       │   ├── item_help_how_category.xml
│       │       │   ├── item_help_question.xml
│       │       │   ├── item_recent_exposure.xml
│       │       │   ├── item_recent_exposure_group_header.xml
│       │       │   ├── item_scan_instance.xml
│       │       │   ├── item_scan_instance_header.xml
│       │       │   ├── item_search.xml
│       │       │   ├── item_search_layout.xml
│       │       │   ├── item_tek.xml
│       │       │   ├── layout_sandbox_config_values.xml
│       │       │   ├── search_toolbar.xml
│       │       │   ├── traveller_dashboard_card_view.xml
│       │       │   └── view_data_item.xml
│       │       ├── menu/
│       │       │   ├── bottom_nav.xml
│       │       │   ├── dashboard.xml
│       │       │   ├── exposure.xml
│       │       │   └── onboarding.xml
│       │       ├── mipmap-anydpi-v26/
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── navigation/
│       │       │   └── nav_graph.xml
│       │       ├── values/
│       │       │   ├── colors.xml
│       │       │   ├── controls.xml
│       │       │   ├── dimens.xml
│       │       │   ├── ids.xml
│       │       │   ├── strings-notranslate.xml
│       │       │   ├── strings.xml
│       │       │   ├── styles.xml
│       │       │   └── themes.xml
│       │       ├── values-cs/
│       │       │   └── strings.xml
│       │       ├── values-night/
│       │       │   └── colors.xml
│       │       ├── values-sk/
│       │       │   └── strings.xml
│       │       ├── xml/
│       │       │   ├── file_paths.xml
│       │       │   └── remote_config_defaults.xml
│       │       ├── xml-cs/
│       │       │   └── remote_config_defaults.xml
│       │       └── xml-sk/
│       │           └── remote_config_defaults.xml
│       ├── prod/
│       │   ├── google-services.json
│       │   └── res/
│       │       └── values/
│       │           └── controls.xml
│       └── test/
│           └── kotlin/
│               └── com/
│                   └── covid19cz/
│                       └── bt_tracing/
│                           └── ExampleUnitTest.kt
├── arch/
│   ├── build.gradle
│   ├── gradle.properties
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── arch/
│           │       ├── BaseApp.kt
│           │       ├── adapter/
│           │       │   ├── BaseRecyclerAdapter.kt
│           │       │   ├── RecyclerLayoutStrategy.kt
│           │       │   ├── SingleTypeRecyclerAdapter.kt
│           │       │   └── StrategyRecyclerAdapter.kt
│           │       ├── binding/
│           │       │   ├── EditTextBindings.kt
│           │       │   ├── ImageViewBindings.kt
│           │       │   ├── ProgressbarBindings.kt
│           │       │   ├── RecyclerViewBindings.kt
│           │       │   ├── TextViewBindings.kt
│           │       │   ├── ViewBindings.kt
│           │       │   └── WebViewBindings.kt
│           │       ├── event/
│           │       │   ├── LiveEvent.kt
│           │       │   ├── LiveEventMap.kt
│           │       │   ├── NavigationEvent.kt
│           │       │   ├── NavigationGraphEvent.kt
│           │       │   └── SingleLiveEvent.java
│           │       ├── extensions/
│           │       │   └── navExtensions.kt
│           │       ├── livedata/
│           │       │   └── SafeMutableLiveData.kt
│           │       ├── utils/
│           │       │   └── NullableUtils.kt
│           │       ├── view/
│           │       │   ├── BaseArchActivity.kt
│           │       │   ├── BaseArchDialogFragment.kt
│           │       │   ├── BaseArchFragment.kt
│           │       │   └── BaseDialogFragment.kt
│           │       └── viewmodel/
│           │           ├── BaseArchViewModel.kt
│           │           └── BaseDialogViewModel.kt
│           └── res/
│               ├── layout/
│               │   ├── base_dialog.xml
│               │   ├── base_view.xml
│               │   └── item_recycler.xml
│               └── values/
│                   ├── ids.xml
│                   └── strings.xml
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── meta/
│   └── debug.keystore
├── release.sh
└── settings.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/pull_request_template.txt
================================================
# Description

<# Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. #>

## Screenshots 📸

<details open><summary>Show/Hide content</summary>

<# Please provide screenshots for any visual change. The best way is to take screenshots directly from the app with production-like data. #>

</details> 

# How Has This Been Tested? 👨‍🔬

<# Please describe the tests that you ran to verify your changes. #>

## What Has Not Been Tested? 🙅🏻‍♂️

<# Please describe what has not been possible to test neither automatically nor manually. #>

# Checklist ✅

- [ ] I have performed a self-review of my own code.
- [ ] I have commented my code, particularly in hard-to-understand areas.
- [ ] I have checked all visual changes in both light and dark mode.

================================================
FILE: .github/stale.yml
================================================
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
  - pinned
  - security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
  This issue has been automatically marked as stale because it has not had
  recent activity. It will be closed if no further activity occurs. Thank you
  for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

================================================
FILE: .github/workflows/deploy-develop.yml
================================================
name: Deploy production app

on:
  push:
    branches:
    - develop

jobs:
  build:

    runs-on: ubuntu-18.04

    steps:
    - name: Check out code
      uses: actions/checkout@v1
    - name: Set up JDK 11
      uses: actions/setup-java@v1
      with:
        java-version: '11'
    - name: Recover Gradle cache
      uses: actions/cache@v1
      with:
        path: ~/.gradle/caches
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
    - name: Download release certificate
      id: release_cert
      uses: timheuer/base64-to-file@v1.0.3
      with:
        fileName: 'erouska_release.jks'
        encodedString: ${{ secrets.RELEASE_KEYSTORE_BASE64 }}
    - name: Build release apps
      run: ./gradlew assembleDevRelease
      env:
        EROUSKA_RELEASE_KEYSTORE_PATH: ${{ steps.release_cert.outputs.filePath }}
        EROUSKA_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }}
        EROUSKA_RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
    - name: Publish artefact
      uses: actions/upload-artifact@v1
      with:
        name: app-releases
        path: app/build/outputs
    - name: Upload DEV app to Firebase App Distribution
      uses: wzieba/Firebase-Distribution-Github-Action@v1.2.1
      with:
        appId: 1:1077972356575:android:75624eb96818aa0d61886a
        token: ${{ secrets.FIREBASE_TOKEN }}
        groups: internal-test
        file: app/build/outputs/apk/dev/release/covid19-cz-dev-release.apk


================================================
FILE: .github/workflows/deploy-master.yml
================================================
name: Deploy production app

on:
  push:
    branches:
    - master

jobs:
  build:

    runs-on: ubuntu-18.04

    steps:
    - name: Check out code
      uses: actions/checkout@v1
    - name: Set up JDK 11
      uses: actions/setup-java@v1
      with:
        java-version: '11'
    - name: Recover Gradle cache
      uses: actions/cache@v1
      with:
        path: ~/.gradle/caches
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
    - name: Download release certificate
      id: release_cert
      uses: timheuer/base64-to-file@v1.0.3
      with:
        fileName: 'erouska_release.jks'
        encodedString: ${{ secrets.RELEASE_KEYSTORE_BASE64 }}
    - name: Build release apps
      run: ./gradlew assembleProdRelease bundleProdRelease
      env:
        EROUSKA_RELEASE_KEYSTORE_PATH: ${{ steps.release_cert.outputs.filePath }}
        EROUSKA_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }}
        EROUSKA_RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
    - name: Publish artefact
      uses: actions/upload-artifact@v1
      with:
        name: app-releases
        path: app/build/outputs
    - name: Upload PROD app to Firebase App Distribution
      uses: wzieba/Firebase-Distribution-Github-Action@v1.2.1
      with:
        appId: 1:941144972907:android:937903c1584d72a673db2e
        token: ${{ secrets.FIREBASE_TOKEN }}
        groups: internal-test
        file: app/build/outputs/apk/prod/release/covid19-cz-prod-release.apk


================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/intellij,android,gradle,windows,osx

### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio

*.iml

## Directory-based project format:
#.idea/
# if you remove the above rule, at least ignore the following:

# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries

# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
.idea/jarRepositories.xml
.idea/misc.xml
.idea/modules.xml
.idea/runConfigurations.xml
.idea/vcs.xml
.idea/compiler.xml
.idea/caches
.idea/.name
.idea/navEditor.xml
.idea/assetWizardSettings.xml
.idea/codeStyles/Project.xml

# Gradle:
.idea/gradle.xml
.idea/libraries

# Mongo Explorer plugin:
.idea/mongoSettings.xml

## File-based project format:
*.ipr
*.iws

## Plugin-specific files:

# IntelliJ
/out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties


### Android ###
# Built application files
*.apk
*.ap_

# Files for the Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/

# Gradle files
.gradle
.gradle/
build/
/build

# Local configuration file (sdk path, etc)
local.properties
# signing.properties
# *.keystore

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

### Android Patch ###
gen-external-apklibs

# Ignore Gradle GUI config
gradle-app.setting

# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar


### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msm
*.msp

# Windows shortcuts
*.lnk


### OSX ###
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.idea/inspectionProfiles/Project_Default.xml


================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
  <state>
    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
  </state>
</component>

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Ministry of Health of the Czech Republic

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# erouska-android

[<img src="https://lh3.googleusercontent.com/cjsqrWQKJQp9RFO7-hJ9AfpKzbUb_Y84vXfjlP0iRHBvladwAfXih984olktDhPnFqyZ0nu9A5jvFwOEQPXzv7hr3ce3QVsLN8kQ2Ao=s0">](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska)

Read our **FAQ**: [Czech](https://erouska.cz/caste-dotazy), [English](https://erouska.cz/en/caste-dotazy)

eRouška (_rouška_ = _face mask_ in Czech) helps to fight against COVID-19.

eRouška uses Bluetooth to scan the area around the device for other eRouška users and saves the data of these encounters.

It's the only app in Czechia authorized to use Exposure Notifications API from Apple/Google.

## Who is developing eRouška?

Starting with version 2.0, the eRouška application is developed by the Ministry of Health in collaboration with the National Agency for Communication and Information Technologies ([NAKIT](https://nakit.cz/)). Earlier versions of eRouška application were developed by a team of volunteers from the [COVID19CZ](https://covid19cz.cz) community. Most of original eRouška developers continue to work on newer versions in the NAKIT team.

## International cooperation

We are open-source from day one and we will be happy to work with people in other countries if they want to develop a similar app. Contact [David Vávra](mailto:david.vavra@erouska.cz) for technical details.

## Building the App from the source code

Clone this repository and import the project into Android Studio. Make sure you have JDK 8.

Run:
`./gradlew assembleDevDebug`

## Contributing
We are happy to accept pull requests! See [Git Workflow](#git-workflow).

If you want to become a more permanent part of the team, join [our Slack](https://covid19cz.slack.com), channel _#erouska_.

## Translations

Help us translate to your language or if you see a problem with translation, fix it. Our translation is open to volunteers [at OneSky](https://covid19cz.oneskyapp.com/).

## <a name="git-workflow"></a>Git workflow

- Work in a fork then send a pull request to the `develop` branch. 
- Pull requests are merged with `squash commits`.
- Admins rebase `develop` to `master` using the script below. This triggers a release build.

## eRouška release process

eRouška uses GitHub Actions. A push to master branch triggers an App build. Then the App is published to [Firebase App Distribution](https://firebase.google.com/docs/app-distribution). 

There are two variants of the App: **DEV** and **PROD**. **PROD** is also built as an App Bundle artefact, that needs to be manually uploaded to Google Play.

Versioning is automatic: major and minor version is in Git, patch is _versionCode_ (a number of commits from the start).

Release is done by executing the release.sh script. Right click it on Android Studio and hit Run 'release.sh' or execute via command line.
If it fails, it fails. Most likely your master has different history from origin. That should never be the case, so you should fix it.

Make sure to update translations & RC defaults before release (next section).

## Updating translations
- Update only Czech and English file in a PR, don't upload anything to OneSky
- Don't put Czech strings into English file, add there either your English translation or `TODO: translate`
- Before every release, the person who is preparing the release will upload Czech file into OneSky and notify translators
- After it's translated, push English and Slovak file from OneSky to develop. Don't update Czech file.

## Updating RC defaults
- Don't add any RC defaults to a pull request, change it only in Dev RC for testing ([follow this guide](https://github.com/covid19cz/erouska-remote-config#how-to-add-new-localizable-rc-key))
- Before every release, the person who is preparing the release should update RC defaults and push them to develop



================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.gms.google-services'
apply plugin: "androidx.navigation.safeargs.kotlin"
apply plugin: 'com.google.firebase.crashlytics'

android {

    compileSdkVersion rootProject.ext.compileSdkVersion
    defaultConfig {
        applicationId "cz.covid19cz.erouska"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.commitCount()
        versionName rootProject.ext.versionName
        archivesBaseName = "covid19-cz"
        multiDexEnabled true
        // If we support another language, add it here
        def supportedLanguages = ["en", "cs", "sk"]
        resConfigs supportedLanguages
        buildConfigField "String[]", "SUPPORTED_LANGUAGES", "{\"" + supportedLanguages.join("\",\"") + "\"}"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        testInstrumentationRunnerArguments clearPackageData: 'true'
    }

    testOptions {
        execution 'ANDROIDX_TEST_ORCHESTRATOR'
    }

    flavorDimensions "environment"
    testBuildType "debug"

    productFlavors {
        dev {
            dimension "environment"
            applicationIdSuffix ".dev"
        }
        prod {
            dimension "environment"
        }
    }

    signingConfigs {
        debug {
            storeFile file("../meta/debug.keystore")
        }
        release {
            storeFile file(System.getenv("EROUSKA_RELEASE_KEYSTORE_PATH") ?: "No CI")
            storePassword System.getenv("EROUSKA_RELEASE_KEYSTORE_PASSWORD")
            keyAlias "covid19cz"
            keyPassword System.getenv("EROUSKA_RELEASE_KEY_PASSWORD")
        }
    }

    buildTypes {
        release {
            debuggable false
            minifyEnabled true
            shrinkResources true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
        debug.java.srcDirs += 'src/debug/kotlin'
        release.java.srcDirs += 'src/release/kotlin'
        androidTest.java.srcDirs += 'src/androidTest/kotlin'
    }

    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }

    buildFeatures {
        dataBinding = true
    }

    lintOptions {
        disable 'MissingTranslation'
    }

    packagingOptions {
        exclude 'META-INF/main.kotlin_module'
    }
}

androidExtensions {
    experimental = true
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])

    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.32"

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.3.5"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.7'

    // Android Basics
    implementation 'androidx.multidex:multidex:2.0.1'
    implementation 'androidx.core:core-ktx:1.6.0'
    implementation "androidx.appcompat:appcompat:1.3.1"
    implementation 'androidx.fragment:fragment-ktx:1.3.6'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
    implementation "com.google.android.material:material:1.4.0"
    implementation "androidx.recyclerview:recyclerview:1.2.1"
    implementation "androidx.browser:browser:1.3.0"
    implementation "com.google.android.play:core-ktx:1.8.1"
    implementation "androidx.work:work-runtime-ktx:2.7.0"

    // Arch
    implementation project(':arch')

    // Navigation
    implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
    implementation "androidx.navigation:navigation-ui-ktx:2.3.5"

    // Dagger-Hilt
    implementation "com.google.dagger:hilt-android:2.38.1"
    kapt "com.google.dagger:hilt-android-compiler:2.38.1"
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03'
    implementation 'androidx.hilt:hilt-work:1.0.0'
    kapt 'androidx.hilt:hilt-compiler:1.0.0'

    //RxJava
    implementation "io.reactivex.rxjava2:rxjava:2.2.17"
    implementation "io.reactivex.rxjava2:rxandroid:2.1.1"

    //RxPermisssions
    implementation 'com.github.tbruyelle:rxpermissions:0.10.2'

    // ViewModel and LiveData
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"

    //Room
    implementation "androidx.room:room-runtime:2.3.0"
    kapt "androidx.room:room-compiler:2.3.0"
    implementation "androidx.room:room-ktx:2.3.0"

    // Gson
    implementation 'com.google.code.gson:gson:2.8.8'

    // Firebase
    implementation platform('com.google.firebase:firebase-bom:26.0.0')

    implementation 'com.google.firebase:firebase-analytics'
    implementation 'com.google.firebase:firebase-auth'
    implementation 'com.google.firebase:firebase-config-ktx'
    implementation 'com.google.firebase:firebase-functions-ktx'
    implementation 'com.google.firebase:firebase-storage-ktx'
    implementation 'com.google.firebase:firebase-messaging-ktx'
    implementation 'com.google.firebase:firebase-crashlytics'

    // Play Services
    implementation 'com.google.android.play:core:1.10.2'
    implementation 'com.google.android.gms:play-services-base:17.6.0'
    implementation 'com.google.android.gms:play-services-basement:17.6.0'
    implementation 'com.google.android.gms:play-services-safetynet:17.0.1'
    implementation 'com.google.android.gms:play-services-tasks:17.2.1'

    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

    // Markdown
    implementation "io.noties.markwon:core:4.3.1"
    implementation "io.noties.markwon:html:4.3.1"
    implementation "io.noties.markwon:inline-parser:4.3.1"
    implementation 'io.noties.markwon:image-glide:4.3.1'
    implementation 'com.atlassian.commonmark:commonmark-ext-autolink:0.12.1'
    implementation 'org.apache.commons:commons-lang3:3.11'

    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'

    // Others
    implementation 'com.android.support:customtabs:28.0.0'
    implementation 'com.jaredrummler:android-device-names:2.0.0'
    implementation 'com.jakewharton.threetenabp:threetenabp:1.2.4'

    // JWT
    implementation 'com.auth0.android:jwtdecode:2.0.0'

    // Tests
    testImplementation 'junit:junit:4.13'
    androidTestImplementation 'org.awaitility:awaitility:3.1.6'

    androidTestImplementation 'junit:junit:4.13'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    androidTestImplementation 'androidx.test:rules:1.2.0'
    androidTestImplementation "org.koin:koin-test:2.0.1"
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
    androidTestUtil 'androidx.test:orchestrator:1.2.0'

    androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}


================================================
FILE: app/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 *;
#}

# Makes debugging easier
-dontobfuscate
-keepattributes SourceFile,LineNumberTable


================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/helpers/Actions.kt
================================================
package cz.covid19cz.erouska.helpers

import android.app.Instrumentation
import android.content.Intent
import android.view.View
import androidx.annotation.StringRes
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher
import org.hamcrest.core.AllOf

const val RETRY_TIMEOUT = 10L

private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

fun click(element: Matcher<View>): ViewInteraction = onView(element).perform(ViewActions.click())

fun click(id: Int): ViewInteraction =  click(withId(id))

fun scrollTo(id: Int): ViewInteraction = onView(withId(id)).perform(ViewActions.scrollTo())

fun clickUiAutomator(buttonText: String) = device.findObject(UiSelector().clickable(true).textStartsWith(buttonText)).click() // startsWith because it is case insensitive

fun clickUiAutomatorByResourceId(resourceId: String) = device.findObject(UiSelector().resourceId(resourceId)).click()

fun checkMatchesString(id: Int, @StringRes stringId: String): ViewInteraction = onView(withId(id)).check(
    ViewAssertions.matches(withText(stringId))
)

fun checkMatchesSubString(id: Int, @StringRes stringId: String): ViewInteraction = onView(withId(id)).check(
    ViewAssertions.matches(withSubstring(stringId))
)

fun checkMatchesContainsString(id: Int, @StringRes stringId: String): ViewInteraction = onView(withId(id)).check(
    ViewAssertions.matches(withText(containsString(stringId)))
)

fun checkDisplayed(id: Int): ViewInteraction = onView(withId(id)).check(
    ViewAssertions.matches(
        isDisplayed()
    )
)

fun checkDisplayed(text: String): ViewInteraction = onView(withText(text)).check(
    ViewAssertions.matches(
        isDisplayed()
    )
)

fun typeText(id: Int, @StringRes text: String): ViewInteraction = onView(withId(id)).perform(ViewActions.typeText(text),
    ViewActions.closeSoftKeyboard()
)


/**
 * verify link
 * @param element element that has link
 * @param url link that should be open
 * @param clickableText optional pass if only part of the element is clickable
 */
fun verifyLink(element: Matcher<View>, url: String, clickableText: String? = null) {

    Intents.init()
    val expectedIntent = AllOf.allOf(
        IntentMatchers.hasAction(Intent.ACTION_VIEW),
        IntentMatchers.hasData(url)
    )
    Intents.intending(expectedIntent).respondWith(Instrumentation.ActivityResult(0, null))

    if(clickableText.isNullOrBlank()) {
        onView(element).perform(ViewActions.click())
    } else {
        onView(element).perform(ViewActions.openLinkWithText(TextMatchesIgnoringWhitespaceType(clickableText)))
    }

    Intents.intended(expectedIntent)
    Intents.release()
}

fun verifyMultipleLinks(element: Matcher<View>, urlTextPairs: ArrayList<ClickableLink>) {
    urlTextPairs.map {clickableLink -> verifyLink(element, clickableLink.url, clickableLink.clickableText)}
}

================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/helpers/ClickableLink.kt
================================================
package cz.covid19cz.erouska.helpers

class ClickableLink(var url: String, var clickableText: String)

================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/helpers/TextMatchesIgnoringWhitespaceType.kt
================================================
package cz.covid19cz.erouska.helpers

import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher

class TextMatchesIgnoringWhitespaceType(string: String?) :
    TypeSafeMatcher<String>() {
    private val string: String
    override fun matchesSafely(item: String): Boolean {
        return normalizeWhitespaces(string).equals(normalizeWhitespaces(item), ignoreCase = true)
    }

    override fun describeTo(description: Description) {
        description.appendText("Expected same strings")
    }

    private fun normalizeWhitespaces(string: String): String {
        return string.replace("\\s".toRegex(), "")
    }

    init {
        requireNotNull(string) { "Non-null value required by TextMatchesIgnoringWhitespaceType()" }
        this.string = string
    }
}

================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/A1Screen.kt
================================================
package cz.covid19cz.erouska.screens

import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.helpers.checkDisplayed
import cz.covid19cz.erouska.helpers.click

object A1Screen {

    fun startActivation() {
        click(R.id.welcome_continue_btn)
    }

    fun checkAllPartsDisplayed() {
        checkDisplayed(R.id.welcome_title)
        checkDisplayed(R.id.welcome_desc)
        checkDisplayed(R.id.welcome_help_btn)
        checkDisplayed(R.id.mzcr_icon)
        checkDisplayed(R.id.welcome_continue_btn)
    }

    fun goToHelp() {
        click(R.id.welcome_help_btn)
    }
}

================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/A2Screen.kt
================================================
package cz.covid19cz.erouska.screens

import android.bluetooth.BluetoothAdapter
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.helpers.checkDisplayed
import cz.covid19cz.erouska.helpers.click
import cz.covid19cz.erouska.helpers.clickUiAutomatorByResourceId

object A2Screen {


    fun checkAllPartsDisplayed() {
        checkDisplayed(R.id.notifications_img)
        checkDisplayed(R.id.notifications_title)
        checkDisplayed(R.id.notifications_body_1)
        checkDisplayed(R.id.notifications_body_2)
        checkDisplayed(R.id.enable_btn)
    }

    /**
     * Check if bluetooth is turn on and if not turn it on using the app
     */
    fun enableBt() {
        val mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        if (!mBluetoothAdapter.isEnabled) {
            click(R.id.enable_btn)
            clickUiAutomatorByResourceId("android:id/button1")
        }
    }

    fun turnOnNotifications() {
        click(R.id.enable_btn)
    }

    fun acceptCovidActivation() {
        clickUiAutomatorByResourceId("android:id/button1")
    }
}

================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/A3Screen.kt
================================================
package cz.covid19cz.erouska.screens

import androidx.test.espresso.matcher.ViewMatchers.withId
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.helpers.checkDisplayed
import cz.covid19cz.erouska.helpers.click
import cz.covid19cz.erouska.helpers.verifyLink
import cz.covid19cz.erouska.screens.N1Screen.TERMS_OF_USE_URL

object A3Screen {

    fun checkAllPartsDisplayed() {
        checkDisplayed(R.id.img_privacy)
        checkDisplayed(R.id.privacy_header)
        checkDisplayed(R.id.privacy_body_1)
        checkDisplayed(R.id.privacy_body_2)
        checkDisplayed(R.id.activate_btn)
    }

    fun checkTermsOfUseLink() {
        verifyLink(withId(R.id.privacy_body_2), TERMS_OF_USE_URL, "podmínkách používání")
    }

    fun finishActivation() {
        click(R.id.activate_btn)
    }
}

================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/B1Screen.kt
================================================
package cz.covid19cz.erouska.screens

import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.helpers.RETRY_TIMEOUT
import cz.covid19cz.erouska.helpers.checkDisplayed
import org.awaitility.Awaitility.await
import java.util.concurrent.TimeUnit

object B1Screen {

    fun checkActiveScreen() {
        await().ignoreExceptions().atMost(RETRY_TIMEOUT, TimeUnit.SECONDS).untilAsserted {
            checkDisplayed(R.id.app_running_image)
        }
        checkDisplayed(R.id.app_running_title)
        checkDisplayed(R.id.app_running_body)
        checkDisplayed(R.id.app_running_body_secondary)
        checkDisplayed(R.id.buttonStop)
    }
}

================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/N1Screen.kt
================================================
package cz.covid19cz.erouska.screens

import androidx.test.espresso.matcher.ViewMatchers.withId
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.helpers.ClickableLink
import cz.covid19cz.erouska.helpers.verifyMultipleLinks

object N1Screen {
    private const val EROUSKA_BASE_URL = "https://erouska.cz/"
    private const val AUDIT_URL = "${EROUSKA_BASE_URL}audit-kod"
    private const val COVIDCZ_GITHUB_URL = "https://github.com/covid19cz/"
    private const val IOS_GITHUB_URL = "${COVIDCZ_GITHUB_URL}erouska-ios"
    private const val ANDROID_GITHUB_URL = "${COVIDCZ_GITHUB_URL}erouska-android"
    private const val APPSTORE_URL = "https://apps.apple.com/cz/app/erou%C5%A1ka/id1509210215"
    private const val GOOGLE_PLAY_URL = "https://play.google.com/store/apps/details?id=cz.covid19cz.erouska"
    private const val APPLE_TRACKING_URL = "https://www.apple.com/covid19/contacttracing"
    private const val GOOGLE_TRACKING_URL = "https://www.google.com/covid19/exposurenotifications/"
    private const val CHYTRA_KARANTENA_URL = "https://koronavirus.mzcr.cz/chytra-karantena/"
    private const val COVID_MZCR_URL = "https://koronavirus.mzcr.cz/"

    const val TERMS_OF_USE_URL = "${EROUSKA_BASE_URL}podminky-pouzivani"

    fun checkScreenAndLink() {
        val descriptionElement = withId(R.id.help_desc)
        val helpClickableLinks = arrayListOf(
            ClickableLink("${EROUSKA_BASE_URL}vyhodnoceni-rizika","Spolehlivost vyhodnocení rizikového kontaktu"),
            ClickableLink("${TERMS_OF_USE_URL}#technicke","Technické podmínky v Podmínkách zpracování"),
            ClickableLink(GOOGLE_PLAY_URL,"Google Play (Android)"),
            ClickableLink(APPSTORE_URL, "App Store (iOS)"),
            ClickableLink(TERMS_OF_USE_URL,"Informacích o zpracování osobních údajů v aplikaci eRouška 2.0"),
            ClickableLink(COVID_MZCR_URL,"na webu Ministerstva zdravotnictví ČR"),
            ClickableLink(CHYTRA_KARANTENA_URL,"chytré karantény"),
            ClickableLink(APPLE_TRACKING_URL,"Apple (anglicky)"),
            ClickableLink(GOOGLE_TRACKING_URL,"Google (česky)"),
            ClickableLink(TERMS_OF_USE_URL,"Informace o zpracování osobních údajů v aplikaci eRouška 2.0"),
            ClickableLink(AUDIT_URL,"Audit zdrojového kódu aplikace"),
            ClickableLink(TERMS_OF_USE_URL,"Informacích o zpracování osobních údajů v rámci aplikace eRouška 2.0"),
            ClickableLink(ANDROID_GITHUB_URL,"Android"),
            ClickableLink(IOS_GITHUB_URL,"iOS"),
            ClickableLink(AUDIT_URL,"prověřují nezávislé autority"),
            ClickableLink(TERMS_OF_USE_URL,"nepracuje s osobními údaji"),
            ClickableLink(TERMS_OF_USE_URL,"Informacích o zpracování osobních údajů v rámci aplikace eRouška")
        )

        verifyMultipleLinks(descriptionElement, helpClickableLinks)
    }
}

================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/testRules/DisableAnimationsRule.kt
================================================
package cz.covid19cz.erouska.testRules

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

/**
 * Test rule for disabling animations before test start.
 * Animations are re-enabled after test finish.
 *
 * @author Michal Kubele (michal.kubele@gmail.com)
 */
class DisableAnimationsRule : TestRule {

    private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())

    companion object {
        private const val DISABLED = 0
        private const val ENABLED = 1
        private const val TRANSITION_ANIMATION_SCALE = "settings put global transition_animation_scale %d"
        private const val WINDOW_ANIMATION_SCALE = "settings put global window_animation_scale %d"
        private const val ANIMATOR_DURATION_SCALE = "settings put global animator_duration_scale %d"
    }

    override fun apply(base: Statement, description: Description): Statement {
        return object : Statement() {
            @Throws(Throwable::class)
            override fun evaluate() {
                disableAnimations()
                try {
                    base.evaluate()
                } finally {
                    enableAnimations()
                }
            }
        }
    }

    internal fun enableAnimations() {
        device.run {
            executeCommand(TRANSITION_ANIMATION_SCALE, ENABLED)
            executeCommand(WINDOW_ANIMATION_SCALE, ENABLED)
            executeCommand(ANIMATOR_DURATION_SCALE, ENABLED)
        }
    }

    internal fun disableAnimations() {
        device.run {
            executeCommand(TRANSITION_ANIMATION_SCALE, DISABLED)
            executeCommand(WINDOW_ANIMATION_SCALE, DISABLED)
            executeCommand(ANIMATOR_DURATION_SCALE, DISABLED)
        }
    }
}

/**
 * Executes provided shell [command] with arguments [args] on the device.
 *
 * @param command command to run
 * @param args arguments for command
 */
fun UiDevice.executeCommand(command: String, vararg args: Any) {
    this.executeShellCommand(command.format(*args))
}

================================================
FILE: app/src/androidTest/kotlin/cz/covid19cz/erouska/tests/ActivationTest.kt
================================================
package cz.covid19cz.erouska.tests

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import cz.covid19cz.erouska.screens.*
import cz.covid19cz.erouska.testRules.DisableAnimationsRule
import cz.covid19cz.erouska.ui.main.MainActivityOld
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith


@RunWith(AndroidJUnit4::class)
class ActivationTest {
    @get:Rule
    val disableAnimationsRule = DisableAnimationsRule()

    @get:Rule
    val activityRule: ActivityTestRule<MainActivityOld> = ActivityTestRule(MainActivityOld::class.java)

    @Test
    fun activationTest() {
        A1Screen.run {
            checkAllPartsDisplayed()
            startActivation()
        }
        A2Screen.run {
            checkAllPartsDisplayed()
            enableBt()
            turnOnNotifications()
            acceptCovidActivation()
        }
        A3Screen.run {
            checkTermsOfUseLink()
            checkAllPartsDisplayed()
            finishActivation()
        }
        B1Screen.checkActiveScreen()
    }

    @Test
    fun checkHelpScreenTest() {
        A1Screen.goToHelp()
        N1Screen.checkScreenAndLink()
    }
}


================================================
FILE: app/src/dev/google-services.json
================================================
{
  "project_info": {
    "project_number": "382369682317",
    "firebase_url": "https://erouska-key-server-dev.firebaseio.com",
    "project_id": "erouska-key-server-dev",
    "storage_bucket": "erouska-key-server-dev.appspot.com"
  },
  "client": [
    {
      "client_info": {
        "mobilesdk_app_id": "1:382369682317:android:eae01d4d3e32686d0f23c4",
        "android_client_info": {
          "package_name": "cz.covid19cz.erouska.dev"
        }
      },
      "oauth_client": [
        {
          "client_id": "382369682317-55si8a5km4pp4s3af88ehcn6sb5ol0ph.apps.googleusercontent.com",
          "client_type": 3
        }
      ],
      "api_key": [
        {
          "current_key": "AIzaSyCvhX7EQRKWpX1XYmU5wiyW2PIetK1EX6U"
        }
      ],
      "services": {
        "appinvite_service": {
          "other_platform_oauth_client": [
            {
              "client_id": "382369682317-55si8a5km4pp4s3af88ehcn6sb5ol0ph.apps.googleusercontent.com",
              "client_type": 3
            },
            {
              "client_id": "382369682317-oa2dc4siamp24t9tk4u9gfvbs6g4of3h.apps.googleusercontent.com",
              "client_type": 2,
              "ios_info": {
                "bundle_id": "cz.covid19cz.erouska.dev"
              }
            }
          ]
        }
      }
    }
  ],
  "configuration_version": "1"
}

================================================
FILE: app/src/dev/res/drawable/ic_launcher_background.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">

  <group android:scaleX="0.2109375"
      android:scaleY="0.2109375">
    <path
        android:pathData="M0,0h512v512h-512z"
        android:fillColor="#EE225B"/>
  </group>

  <group android:scaleX="0.14132813"
      android:scaleY="0.14132813"
      android:translateX="17.82"
      android:translateY="17.82">
    <group>
      <clip-path
          android:pathData="M0,0h512v512h-512z"/>
      <path
          android:pathData="M96,360.4l151.6,151.6l264.4,0l0,-240.7l-96,-95.6l-320,184.7"
          android:fillColor="#E01A53"/>
    </group>
    <group>
      <clip-path
          android:pathData="M0,0h512v512h-512z"/>
      <path
          android:pathData="M405.8,352.5c1.2,4.7 53.1,-31.5 106.2,-35.2"
          android:strokeWidth="23"
          android:fillColor="#00000000"
          android:strokeColor="#FFFFFF"
          android:strokeLineCap="round"/>
      <path
          android:pathData="M106.2,352.5C105,357.2 53.1,321 0,317.3"
          android:strokeWidth="23"
          android:fillColor="#00000000"
          android:strokeColor="#FFFFFF"
          android:strokeLineCap="round"/>
      <path
          android:pathData="M405.8,159.7c1.2,-4.7 53.1,31.5 106.2,35.2"
          android:strokeWidth="23"
          android:fillColor="#00000000"
          android:strokeColor="#FFFFFF"
          android:strokeLineCap="round"/>
      <path
          android:pathData="M106.2,159.7C105,155 53.1,191.2 0,194.9"
          android:strokeWidth="23"
          android:fillColor="#00000000"
          android:strokeColor="#FFFFFF"
          android:strokeLineCap="round"/>
    </group>
    <path
        android:pathData="M401,154.6v55.7L236,191l20,-75C333.2,116 400.3,154.3 401,154.6z"
        android:fillColor="#A4DBDD"/>
    <path
        android:pathData="M401,301.7v55.7L256,401l-20,-90C236,311 400,302 401,301.7z"
        android:fillColor="#C0E5E7"/>
    <path
        android:pathData="M236,246l20,75c77.2,0 144,-19 145,-19.3l0,-45.7C400.3,256 236,246 236,246z"
        android:fillColor="#A4DBDD"/>
    <path
        android:pathData="M401,210.3V256c-0.7,0 -87.7,0 -165,0l20,-65C333.3,191 400.3,210.1 401,210.3z"
        android:fillColor="#C0E5E7"/>
    <path
        android:pathData="M256,321v75c-77.2,0 -144.3,-38.3 -145,-38.6v-55.7L256,321z"
        android:fillColor="#DFF2F3"/>
    <path
        android:pathData="M256,116v75l-145,19.3v-55.7C111.7,154.3 178.6,116 256,116z"
        android:fillColor="#C0E5E7"/>
    <path
        android:pathData="M256,191v65l-72.5,20L111,256v-45.7C112,210 178.8,191 256,191z"
        android:fillColor="#DFF2F3"/>
    <path
        android:pathData="M111,256c0,0 0,45.7 0,45.7c0.7,0.2 67.7,19.3 145,19.3v-65C256,256 111,256 111,256z"
        android:fillColor="#C0E5E7"/>
    <path
        android:pathData="M416,150.2v211.5l-5,2.9c-2.9,1.7 -72.3,41.3 -155,41.3v-20c65.3,0 123.1,-27.2 140,-36V162c-17,-8.8 -74.9,-36 -140,-36v-20c82.7,0 152.1,39.7 155,41.4L416,150.2z"
        android:fillColor="#FFFFFF"/>
    <path
        android:pathData="M256,386v20c-82.7,0 -152.1,-39.7 -155,-41.4l-5,-2.9V150.2l5,-2.9c2.9,-1.7 72.3,-41.4 155,-41.4v20c-65.3,0 -123.1,27.2 -140,36V350C132.9,358.8 190.9,386 256,386z"
        android:fillColor="#FFFFFF"/>
  </group>
</vector>


================================================
FILE: app/src/dev/res/drawable/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
  <path
      android:pathData="M0,106l108,-64l0,66l-108,0z"
      android:strokeWidth="1"
      android:fillColor="#DF2A24"
      android:fillType="nonZero"
      android:strokeColor="#00000000"/>
  <path
      android:pathData="M60.846,84.634L57.317,78.521L59.482,77.271C61.653,76.018 63.312,76.384 64.459,78.37C65.009,79.322 65.151,80.254 64.887,81.164C64.623,82.074 63.997,82.814 63.011,83.383L60.846,84.634ZM59.341,78.847L61.578,82.722L62.26,82.328C62.857,81.984 63.222,81.534 63.354,80.98C63.487,80.426 63.376,79.841 63.02,79.224C62.683,78.642 62.25,78.281 61.721,78.141C61.191,78.002 60.623,78.107 60.015,78.459L59.341,78.847ZM71.414,78.532L67.748,80.649L64.219,74.536L67.744,72.501L68.391,73.622L66.243,74.863L67.028,76.222L69.027,75.068L69.672,76.185L67.673,77.339L68.48,78.737L70.769,77.416L71.414,78.532ZM74.463,68.622L75.886,75.95L74.326,76.851L68.717,71.94L70.2,71.083L73.931,74.601C74.132,74.792 74.29,74.971 74.406,75.139L74.432,75.125C74.342,74.923 74.265,74.689 74.202,74.422L73.022,69.454L74.463,68.622Z"
      android:strokeWidth="1"
      android:fillColor="#FFFFFF"
      android:fillType="nonZero"
      android:strokeColor="#00000000"/>
</vector>


================================================
FILE: app/src/dev/res/values/controls.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="uri_scheme">erouska-dev</string>
    <string name="key_server_base_url">https://europe-west1-erouska-key-server-dev.cloudfunctions.net/</string>
    <string name="verification_server_base_url">https://apiserver-eyrqoibmxa-ew.a.run.app/</string>
    <string name="covid_data_server_base_url">https://europe-west1-erouska-key-server-dev.cloudfunctions.net</string>
    <string name="fileprovider_authorities" translatable="false">cz.covid19cz.erouska.dev.fileprovider</string>
</resources>

================================================
FILE: app/src/dev/res/values/strings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">eRouška DEV</string>
</resources>

================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="cz.covid19cz.erouska">

    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
    <uses-feature android:name="android.hardware.bluetooth" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:name=".App"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity
            android:name=".ui.main.MainActivity"
            android:launchMode="singleTop"
            android:screenOrientation="portrait"
            android:exported="true"
            android:theme="@style/AppTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".ui.main.MainActivityOld"
            android:launchMode="singleTop"
            android:screenOrientation="portrait"
            android:exported="false"
            android:theme="@style/AppTheme.Launchscreen">
        </activity>

        <service
            android:name=".exposurenotifications.service.PushService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service>

        <!-- Receivers -->
        <receiver
            android:name=".exposurenotifications.receiver.ExposureNotificationBroadcastReceiver"
            android:permission="com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK"
            android:exported="true">
            <intent-filter>
                <action android:name="com.google.android.gms.exposurenotification.ACTION_EXPOSURE_STATE_UPDATED" />
            </intent-filter>
        </receiver>

        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="remove">
        </provider>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="@string/fileprovider_authorities"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@drawable/ic_notification_normal" />
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/colorSecondary" />
        <meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
    </application>

    <queries>
        <intent>
            <action android:name="android.intent.action.SENDTO" />
            <data android:scheme="mailto" />
        </intent>
    </queries>

</manifest>

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/App.kt
================================================
package cz.covid19cz.erouska

import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import androidx.work.WorkManager
import arch.BaseApp
import com.jakewharton.threetenabp.AndroidThreeTen
import cz.covid19cz.erouska.exposurenotifications.Notifications
import cz.covid19cz.erouska.exposurenotifications.worker.DownloadKeysWorker
import dagger.hilt.android.HiltAndroidApp
import java.io.File
import javax.inject.Inject

@HiltAndroidApp
class App : BaseApp(), Configuration.Provider {

    @Inject
    lateinit var workerFactory: HiltWorkerFactory

    @Inject
    lateinit var notifications: Notifications

    override fun onCreate() {
        super.onCreate()
        AppConfig.fetchRemoteConfig()
        AndroidThreeTen.init(this)
        notifications.init()
        removeObsoleteData()

        // Init WorkManager with app context, battery saver prevention
        WorkManager.getInstance(this)

        //TODO: Remove if eRouška gets resurrected
        unscheduleWorkers()
    }

    private fun unscheduleWorkers(){
        WorkManager.getInstance(this).cancelAllWork()
    }

    override fun getWorkManagerConfiguration() =
        Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .build()

    private fun removeObsoleteData() {
        val obsoleteDb = File(filesDir.parent + "/databases/android-devices.db")
        if (obsoleteDb.exists()) {
            obsoleteDb.delete()
        }
    }
}


================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/AppConfig.kt
================================================
package cz.covid19cz.erouska

import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
import cz.covid19cz.erouska.utils.L

object AppConfig {
    const val FIREBASE_REGION = "europe-west1"

    private val firebaseRemoteConfig = FirebaseRemoteConfig.getInstance()

    // Exposure Notifications Settings
    val reportTypeWeights
        get() = firebaseRemoteConfig.getString("v2_reportTypeWeights").split(";").map { it.toDouble() }
    val infectiousnessWeights
        get() = firebaseRemoteConfig.getString("v2_infectiousnessWeights").split(";")
            .map { it.toDouble() }
    val attenuationBucketThresholdDb
        get() = firebaseRemoteConfig.getString("v2_attenuationBucketThresholdDb").split(";")
            .map { it.toInt() }
    val attenuationBucketWeights
        get() = firebaseRemoteConfig.getString("v2_attenuationBucketWeights").split(";")
            .map { it.toDouble() }
    val minimumWindowScore
        get() = firebaseRemoteConfig.getDouble("v2_minimumWindowScore")
    val daysSinceOnsetToInfectiousness
        get() = firebaseRemoteConfig.getString("v2_daysSinceOnsetToInfectiousness").split(";")
            .map { it.toInt() }
    val diagnosisKeysDataMappingLimitDays
        get() = firebaseRemoteConfig.getLong("v2_diagnosisKeysDataMappingLimitDays").toInt()
    val dbCleanupDays
        get() = firebaseRemoteConfig.getLong("v2_dbCleanupDays").toInt()
    val supportEmail
        get() = firebaseRemoteConfig.getString("v2_supportEmail")
    val reportTypeWhenMissing
        get() = firebaseRemoteConfig.getLong("v2_reportTypeWhenMissing").toInt()
    val infectiousnessWhenDaysSinceOnsetMissing
        get() = firebaseRemoteConfig.getLong("v2_infectiousnessWhenDaysSinceOnsetMissing").toInt()
    val shareAppDynamicLink
        get() = firebaseRemoteConfig.getString("v2_shareAppDynamicLink")
    val minSupportedVersionCodeAndroid
        get() = firebaseRemoteConfig.getLong("v2_minSupportedVersionCodeAndroid")
    val riskyEncountersTitle
        get() = firebaseRemoteConfig.getString("v2_riskyEncountersTitleAn")
    val noEncounterHeader
        get() = firebaseRemoteConfig.getString("v2_noEncounterHeader")
    val noEncounterCardTitle
        get() = firebaseRemoteConfig.getString("v2_noEncounterCardTitle")
    val noEncounterBody
        get() = firebaseRemoteConfig.getString("v2_noEncounterBody")
    val encounterUpdateFrequency
        get() = String.format(firebaseRemoteConfig.getString("v2_encounterUpdateFrequency"), keyImportPeriodHours)
    val exposureUITitle
        get() = firebaseRemoteConfig.getString("v2_exposureUITitle")
    val symptomsUITitle
        get() = firebaseRemoteConfig.getString("v2_symptomsUITitle")
    val spreadPreventionUITitle
        get() = firebaseRemoteConfig.getString("v2_spreadPreventionUITitle")
    val exposureHelpUITitle
        get() = firebaseRemoteConfig.getString("v2_exposureHelpUITitle")
    val recentExposuresUITitle
        get() = firebaseRemoteConfig.getString("v2_recentExposuresUITitle")
    val symptomsContentJson
        get() = firebaseRemoteConfig.getString("v2_symptomsContentJson")
    val preventionContentJson
        get() = firebaseRemoteConfig.getString("v2_preventionContentJson")
    val exposureHelpContentJson
        get() = firebaseRemoteConfig.getString("v2_exposureHelpContentJson")
    val encounterWarning
        get() = firebaseRemoteConfig.getString("v2_encounterWarning")
    val selfCheckerPeriodHours
        get() = firebaseRemoteConfig.getLong("v2_selfCheckerPeriodHours")
    val keyExportUrl
        get() = firebaseRemoteConfig.getString("v2_keyExportUrl")
    val keyImportPeriodHours
        get() = firebaseRemoteConfig.getLong("v2_keyImportPeriodHours")
    val keyImportDataOutdatedHours
        get() = firebaseRemoteConfig.getLong("v2_keyImportDataOutdatedHours")
    val contactsContentJson
        get() = firebaseRemoteConfig.getString("v2_contactsContentJson")
    val riskyEncountersWithSymptoms
        get() = firebaseRemoteConfig.getString("v2_riskyEncountersWithSymptoms")
    val riskyEncountersWithoutSymptoms
        get() = firebaseRemoteConfig.getString("v2_riskyEncountersWithoutSymptoms")
    val currentMeasuresUrl
        get() = firebaseRemoteConfig.getString("v2_currentMeasuresUrl")
    val minGmsVersionCode
        get() = firebaseRemoteConfig.getLong("v2_minGmsVersionCode")
    val conditionsOfUseUrl
        get() = firebaseRemoteConfig.getString("v2_conditionsOfUseUrl")
    val verificationServerApiKey
        get() = firebaseRemoteConfig.getString("v2_verificationServerApiKey")
    val showChatBotLink
        get() = firebaseRemoteConfig.getBoolean("v2_showChatBotLink")
    val handleError500AsInvalidCode
        get() = firebaseRemoteConfig.getBoolean("v2_handleError500AsInvalidCode")
    val handleError400AsExpiredOrUsedCode
        get() = firebaseRemoteConfig.getBoolean("v2_handleError400AsExpiredOrUsedCode")
    val keyExportNonTravellerUrls
        get() = firebaseRemoteConfig.getString("v2_keyExportNonTravellerUrls")
    val keyExportEuTravellerUrls
        get() = firebaseRemoteConfig.getString("v2_keyExportEuTravellerUrls")
    val recentExposureNotificationTitle
        get() = firebaseRemoteConfig.getString("v2_recentExposureNotificationTitle")
    val updateNewsOnRequest
        get() = firebaseRemoteConfig.getBoolean("v2_updateNewsOnRequest")
    val efgsDays
        get() = firebaseRemoteConfig.getLong("v2_efgsDays").toInt()
    val efgsSupportedCountries
        get() = firebaseRemoteConfig.getString("v2_efgsCountries")
    val efgsVisitedCountries
        get() = firebaseRemoteConfig.getString("v2_efgsVisitedCountries").split(";")
    val efgsReportType
        get() = firebaseRemoteConfig.getString("v2_efgsReportType")
    val efgsConsentToFederation
        get() = firebaseRemoteConfig.getBoolean("v2_efgsConsentToFederation")
    val efgsTravellerDefault
        get() = firebaseRemoteConfig.getBoolean("v2_efgsTravellerDefault")
    val howItWorksUITitle
        get() = firebaseRemoteConfig.getString("v2_howItWorksUITitle")
    val howItWorksEvalContent
        get() = firebaseRemoteConfig.getString("v2_howItWorksEvalContent")
    val helpJson
        get() = firebaseRemoteConfig.getString("v2_helpJson")
    val validationTokenExpirationLeewayMinutes
        get() = firebaseRemoteConfig.getLong("v2_validationTokenExpirationLeewayMinutes")
    val ragnarokHeadline
        get() = firebaseRemoteConfig.getString("v2_ragnarokHeadline")
    val ragnarokBody
        get() = firebaseRemoteConfig.getString("v2_ragnarokBody")
    val ragnarokMoreInfo
        get() = firebaseRemoteConfig.getString("v2_ragnarokMoreInfo")

    init {
        val configSettings: FirebaseRemoteConfigSettings = FirebaseRemoteConfigSettings.Builder()
            .setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 3600)
            .build()
        firebaseRemoteConfig.setConfigSettingsAsync(configSettings)

        firebaseRemoteConfig.setDefaultsAsync(R.xml.remote_config_defaults).addOnCompleteListener {
            print()
        }
    }

    fun fetchRemoteConfig() {
        firebaseRemoteConfig.fetchAndActivate().addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val updated = task.result
                L.d("Config params updated: $updated")
                print()
            } else {
                L.e("Config params update failed")
                task.exception?.printStackTrace()
            }
        }
    }

    private fun print() {
        for (item in firebaseRemoteConfig.all) {
            L.d("${item.key}: ${item.value.asString()}")
        }
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/DI.kt
================================================
package cz.covid19cz.erouska

import android.content.Context
import androidx.room.Room
import com.google.android.gms.nearby.Nearby
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
import cz.covid19cz.erouska.db.DailySummariesDb
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideExposureNotificationClient(@ApplicationContext context: Context): ExposureNotificationClient {
        return Nearby.getExposureNotificationClient(context)
    }

    @Provides
    @Singleton
    fun provideDailySummariesDb(@ApplicationContext context: Context): DailySummariesDb {
        return Room.databaseBuilder(
            context.applicationContext,
            DailySummariesDb::class.java, "daily_summaries"
        ).build()
    }


}







================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/db/DailySummariesDb.kt
================================================
package cz.covid19cz.erouska.db

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(version = 1, entities = [DailySummaryEntity::class], exportSchema = false)
abstract class DailySummariesDb : RoomDatabase(){
    abstract fun dao(): DailySummaryDao
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/db/DailySummaryDao.kt
================================================
package cz.covid19cz.erouska.db

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import cz.covid19cz.erouska.AppConfig
import java.util.concurrent.TimeUnit

@Dao
interface DailySummaryDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(entity : List<DailySummaryEntity>)

    @Query("SELECT * FROM daily_summaries ORDER BY days_since_epoch DESC LIMIT 1")
    suspend fun getLatest() : List<DailySummaryEntity>

    @Query("SELECT * FROM daily_summaries WHERE notified == 1 ORDER BY days_since_epoch DESC LIMIT 1")
    suspend fun getLastNotified() : List<DailySummaryEntity>

    @Query("SELECT * FROM daily_summaries ORDER BY days_since_epoch DESC")
    suspend fun getAllByExposureDate() : List<DailySummaryEntity>

    @Query("SELECT * FROM daily_summaries ORDER BY import_timestamp DESC, days_since_epoch DESC")
    suspend fun getAllByImportDate() : List<DailySummaryEntity>

    @Query("UPDATE daily_summaries SET notified = 1")
    suspend fun markAsNotified()

    @Query("UPDATE daily_summaries SET accepted = 1")
    suspend fun markAsAccepted()

    @Query("DELETE FROM daily_summaries WHERE days_since_epoch < :beforeDaysSinceEpoch")
    suspend fun deleteOld(beforeDaysSinceEpoch : Long = TimeUnit.MILLISECONDS.toDays (System.currentTimeMillis()) - AppConfig.dbCleanupDays)
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/db/DailySummaryEntity.kt
================================================
package cz.covid19cz.erouska.db

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import cz.covid19cz.erouska.ext.daysSinceEpochToDateString

@Entity(tableName = "daily_summaries")
data class DailySummaryEntity(
    @ColumnInfo(name = "days_since_epoch")
    @PrimaryKey val daysSinceEpoch: Int,
    @ColumnInfo(name = "maximum_score")
    val maximumScore: Double,
    @ColumnInfo(name = "score_sum")
    val scoreSum: Double,
    @ColumnInfo(name = "weightened_duration_sum")
    val weightenedDurationSum: Double,
    @ColumnInfo(name = "import_timestamp")
    val importTimestamp: Long,
    @ColumnInfo(name = "notified")
    val notified: Boolean,
    @ColumnInfo(name = "accepted")
    val accepted: Boolean
){

    fun getDateString() : String{
        return daysSinceEpoch.daysSinceEpochToDateString()
    }

    fun getLongDateString() : String{
        return daysSinceEpoch.daysSinceEpochToDateString("d. MMMM yyyy")
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/db/SharedPrefsRepository.kt
================================================
package cz.covid19cz.erouska.db

import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import arch.livedata.SafeMutableLiveData
import com.auth0.android.jwt.JWT
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.ext.timestampToDate
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SharedPrefsRepository @Inject constructor(@ApplicationContext c: Context) {

    companion object {
        const val LAST_KEY_IMPORT = "preference.last_import"
        const val LAST_KEY_IMPORT_TIME = "preference.last_import_time"
        const val LAST_SHOWN_EXPOSURE_INFO = "lastShownExposureInfo"
        const val EXPOSURE_NOTIFICATIONS_ENABLED = "exposureNotificationsEnabled"
        const val LAST_SET_DIAGNOSIS_KEYS_DATA_MAPPING = "lastSetDiagnosisKeysDataMapping"
        const val EFGS_INTRODUCED = "efgsIntroduced"
        const val APP_OPEN_TIMESTAMP = "lastTimeAppOpened"
        const val SUPPRESS_UPDATE_SCREENS = "suppressUpdateScreens"

        const val REPORT_TYPE_WEIGHTS = "reportTypeWeights"
        const val INFECTIOUSNESS_WEIGHTS = "infectiousnessWeights"
        const val ATTENUATION_BUCKET_THRESHOLD_DB = "attenuationBucketThresholdDb"
        const val ATTENUATION_BUCKET_WEIGHTS = "attenuationBucketWeights"
        const val MINIMUM_WINDOW_SCORE = "minimumWindowScore"

        const val LAST_STATS_UPDATE = "lastStatsUpdate"
        const val LAST_METRICS_UPDATE = "lastMetricsUpdate"

        // stats
        const val TESTS_TOTAL = "testsTotal"
        const val TESTS_INCREASE = "testsIncrease"
        const val TESTS_INCREASE_DATE = "testsIncreaseDate"

        const val ANTIGEN_TESTS_TOTAL = "antigenTestsTotal"
        const val ANTIGEN_TESTS_INCREASE = "antigenTestsIncrease"
        const val ANTIGEN_TESTS_INCREASE_DATE = "antigenTestsIncreaseDate"

        const val VACCINATIONS_TOTAL = "vaccinationsTotal"
        const val VACCINATIONS_INCREASE = "vaccinationsIncrease"
        const val VACCINATIONS_INCREASE_DATE = "vaccinationsIncreaseDate"

        const val DAILY_DOSES_DATE = "dailyDosesDate"

        const val FIRST_DOSE_TOTAL = "firstDoseTotal"
        const val FIRST_DOSE_INCREASE = "firstDoseIncrease"

        const val SECOND_DOSE_TOTAL = "secondDoseTotal"
        const val SECOND_DOSE_INCREASE = "secondDoseIncrease"

        const val CONFIRMED_CASES_TOTAL = "confirmedCasesTotal"
        const val CONFIRMED_CASES_INCREASE = "confirmedCasesIncrease"
        const val CONFIRMED_CASES_INCREASE_DATE = "confirmedCasesIncreaseDate"

        const val ACTIVE_CASES_TOTAL = "activeCasesTotal"
        const val CURED_TOTAL = "curedTotal"
        const val DECEASED_TOTAL = "deceasedTotal"
        const val CURRENTLY_HOSPITALIZED_TOTAL = "currentlyHospitalizedTotal"

        // metrics
        const val ACTIVATIONS_TOTAL = "activationsTotal"
        const val ACTIVATIONS_YESTERDAY = "activationsIncrease"
        const val KEY_PUBLISHERS_TOTAL = "keyPublishersTotal"
        const val KEY_PUBLISHERS_YESTERDAY = "keyPublishersYesterday"
        const val NOTIFICATIONS_TOTAL = "notificationsTotal"
        const val NOTIFICATIONS_YESTERDAY = "notificationsTotal"
        const val TRAVELLER = "traveller"
        const val CONSENT_TO_FEDERATION = "consentToFederation"
        const val PUSH_TOKEN_REGISTERED = "pushTokenRegistered"
        const val PUSH_TOPIC_REGISTERED = "pushTopicRegistered"

        const val HOW_IT_WORKS_SHOWN = "howItWorksShown"

        const val LAST_DATA_SENT_TIME = "lastDataSentTime"
        const val VALIDATION_CODE = "validationCode"
        const val VALIDATION_TOKEN = "validationToken"
        const val SYMPTOM_DATE = "symptomDate"
    }

    private val prefs: SharedPreferences = c.getSharedPreferences("prefs", MODE_PRIVATE)
    val lastKeyImportLive = SafeMutableLiveData(getLastKeyImport())

    fun lastKeyExportFileName(indexUrl: String): String {
        return prefs.getString(LAST_KEY_IMPORT + indexUrl, "") ?: ""
    }

    fun setLastKeyExportFileName(indexUrl: String, filename: String) {
        prefs.edit().putString(LAST_KEY_IMPORT + indexUrl, filename).apply()
    }

    fun setLastKeyImport() {
        val timestamp = System.currentTimeMillis()
        prefs.edit().putLong(LAST_KEY_IMPORT_TIME, timestamp).apply()
        lastKeyImportLive.postValue(timestamp)
    }

    fun getLastKeyImport(): Long {
        return prefs.getLong(LAST_KEY_IMPORT_TIME, 0L)
    }

    fun setLastSetDiagnosisKeysDataMapping() {
        prefs.edit().putLong(LAST_SET_DIAGNOSIS_KEYS_DATA_MAPPING, System.currentTimeMillis())
            .apply()
    }

    fun getLastSetDiagnosisKeysDataMapping(): Long {
        return prefs.getLong(LAST_SET_DIAGNOSIS_KEYS_DATA_MAPPING, 0L)
    }

    fun setLastShownExposureInfo(daysSinceEpoch: Int) {
        prefs.edit().putInt(LAST_SHOWN_EXPOSURE_INFO, daysSinceEpoch).apply()
    }

    fun getLastShownExposureInfo(): Int {
        return prefs.getInt(LAST_SHOWN_EXPOSURE_INFO, 0)
    }

    fun isTraveller(): Boolean {
        return prefs.getBoolean(TRAVELLER, true)
    }

    fun setTraveller(traveller: Boolean) {
        prefs.edit().putBoolean(TRAVELLER, traveller).apply()
    }

    fun isConsentToFederation(): Boolean {
        return prefs.getBoolean(CONSENT_TO_FEDERATION, false)
    }

    fun setConsentToFederation(consentToFederation: Boolean) {
        prefs.edit().putBoolean(CONSENT_TO_FEDERATION, consentToFederation).apply()
    }

    fun isPushTokenRegistered(): Boolean {
        return prefs.getBoolean(PUSH_TOKEN_REGISTERED, false)
    }

    fun setPushTokenRegistered() {
        prefs.edit().putBoolean(PUSH_TOKEN_REGISTERED, true).apply()
    }

    fun isPushTopicRegistered(): Boolean {
        return prefs.getBoolean(PUSH_TOPIC_REGISTERED, false)
    }

    fun setPushTopicRegistered() {
        prefs.edit().putBoolean(PUSH_TOPIC_REGISTERED, true).apply()
    }

    fun hasOutdatedKeyData(): Boolean {
        val lastTimestamp = getLastKeyImport()
        return lastTimestamp != 0L && (System.currentTimeMillis() - lastTimestamp) / (1000 * 60 * 60) > AppConfig.keyImportDataOutdatedHours
    }

    fun clearLastKeyExportFileName() {
        prefs.edit().remove(LAST_KEY_IMPORT).apply()
    }

    fun clearLastKeyImportTime() {
        prefs.edit().remove(LAST_KEY_IMPORT_TIME).apply()
    }

    fun isExposureNotificationsEnabled(): Boolean {
        return prefs.getBoolean(EXPOSURE_NOTIFICATIONS_ENABLED, false)
    }

    fun setExposureNotificationsEnabled(enabled: Boolean) {
        prefs.edit().putBoolean(EXPOSURE_NOTIFICATIONS_ENABLED, enabled).apply()
    }

    fun setAppVisitedTimestamp() {
        prefs.edit().putLong(APP_OPEN_TIMESTAMP, System.currentTimeMillis()).apply()
    }

    fun getLastTimeAppVisited(): Long {
        return prefs.getLong(APP_OPEN_TIMESTAMP, 0L)
    }

    fun setSuppressUpdateScreens(suppress: Boolean) {
        prefs.edit().putBoolean(SUPPRESS_UPDATE_SCREENS, suppress).apply()
    }

    fun shouldSuppressUpdateScreens(): Boolean {
        return prefs.getBoolean(SUPPRESS_UPDATE_SCREENS, false)
    }

    fun clearCustomConfig() {
        prefs.edit().apply {
            remove(REPORT_TYPE_WEIGHTS)
            remove(ATTENUATION_BUCKET_THRESHOLD_DB)
            remove(ATTENUATION_BUCKET_WEIGHTS)
            remove(MINIMUM_WINDOW_SCORE)
        }.apply()
    }

    fun setReportTypeWeights(value: String) {
        prefs.edit().putString(REPORT_TYPE_WEIGHTS, value).apply()
    }

    fun setInfectiousnessWeights(value: String) {
        prefs.edit().putString(INFECTIOUSNESS_WEIGHTS, value).apply()
    }

    fun setAttenuationBucketThresholdDb(value: String) {
        prefs.edit().putString(ATTENUATION_BUCKET_THRESHOLD_DB, value).apply()
    }

    fun setAttenuationBucketWeights(value: String) {
        prefs.edit().putString(ATTENUATION_BUCKET_WEIGHTS, value).apply()
    }

    fun setMinimumWindowScore(value: String) {
        prefs.edit().putString(MINIMUM_WINDOW_SCORE, value).apply()
    }

    fun getReportTypeWeights(): List<Double>? {
        return prefs.getString(REPORT_TYPE_WEIGHTS, null)?.let {
            it.split(";").mapNotNull { it.toDoubleOrNull() }
        }
    }

    fun getInfectiousnessWeights(): List<Double>? {
        return prefs.getString(INFECTIOUSNESS_WEIGHTS, null)?.let {
            it.split(";").mapNotNull { it.toDoubleOrNull() }
        }
    }

    fun getAttenuationBucketThresholdDb(): List<Int>? {
        return prefs.getString(ATTENUATION_BUCKET_THRESHOLD_DB, null)?.let {
            it.split(";").mapNotNull { it.toIntOrNull() }
        }
    }

    fun getAttenuationBucketWeights(): List<Double>? {
        return prefs.getString(ATTENUATION_BUCKET_WEIGHTS, null)?.let {
            it.split(";").mapNotNull { it.toDoubleOrNull() }
        }
    }

    fun getMinimumWindowScore(): Double? {
        return prefs.getString(MINIMUM_WINDOW_SCORE, null)?.toDoubleOrNull()
    }

    fun getLastStatsUpdate(): Long {
        return prefs.getLong(LAST_STATS_UPDATE, 0)
    }

    fun setLastStatsUpdate(modified: Long) {
        return prefs.edit().putLong(LAST_STATS_UPDATE, modified).apply()
    }

    fun getLastMetricsUpdate(): Long {
        return prefs.getLong(LAST_METRICS_UPDATE, 0)
    }

    fun setLastMetricsUpdate(modified: Long) {
        return prefs.edit().putLong(LAST_METRICS_UPDATE, modified).apply()
    }

    fun getTestsTotal(): Int {
        return prefs.getInt(TESTS_TOTAL, 0)
    }

    fun setTestsTotal(value: Int) {
        return prefs.edit().putInt(TESTS_TOTAL, value).apply()
    }

    fun getTestsIncrease(): Int {
        return prefs.getInt(TESTS_INCREASE, 0)
    }

    fun setTestsIncrease(value: Int) {
        return prefs.edit().putInt(TESTS_INCREASE, value).apply()
    }

    fun getTestsIncreaseDate(): Long {
        return prefs.getLong(TESTS_INCREASE_DATE, 0)
    }

    fun setTestsIncreaseDate(value: Long) {
        return prefs.edit().putLong(TESTS_INCREASE_DATE, value).apply()
    }

    fun getAntigenTestsTotal(): Int {
        return prefs.getInt(ANTIGEN_TESTS_TOTAL, 0)
    }

    fun setAntigenTestsTotal(value: Int) {
        return prefs.edit().putInt(ANTIGEN_TESTS_TOTAL, value).apply()
    }

    fun getAntigenTestsIncrease(): Int {
        return prefs.getInt(ANTIGEN_TESTS_INCREASE, 0)
    }

    fun setAntigenTestsIncrease(value: Int) {
        return prefs.edit().putInt(ANTIGEN_TESTS_INCREASE, value).apply()
    }

    fun getAntigenTestsIncreaseDate(): Long {
        return prefs.getLong(ANTIGEN_TESTS_INCREASE_DATE, 0)
    }

    fun setAntigenTestsIncreaseDate(value: Long) {
        return prefs.edit().putLong(ANTIGEN_TESTS_INCREASE_DATE, value).apply()
    }

    fun getVaccinationsTotal(): Int {
        return prefs.getInt(VACCINATIONS_TOTAL, 0)
    }

    fun setVaccinationsTotal(value: Int) {
        return prefs.edit().putInt(VACCINATIONS_TOTAL, value).apply()
    }

    fun getVaccinationsIncrease(): Int {
        return prefs.getInt(VACCINATIONS_INCREASE, 0)
    }

    fun setVaccinationsIncrease(value: Int) {
        return prefs.edit().putInt(VACCINATIONS_INCREASE, value).apply()
    }

    fun getVaccinationsIncreaseDate(): Long {
        return prefs.getLong(VACCINATIONS_INCREASE_DATE, 0)
    }

    fun setVaccinationsIncreaseDate(value: Long) {
        return prefs.edit().putLong(VACCINATIONS_INCREASE_DATE, value).apply()
    }

    fun getFirstDoseTotal(): Int {
        return prefs.getInt(FIRST_DOSE_TOTAL, 0)
    }

    fun setFirstDoseTotal(value: Int) {
        return prefs.edit().putInt(FIRST_DOSE_TOTAL, value).apply()
    }

    fun getFirstDoseIncrease(): Int {
        return prefs.getInt(FIRST_DOSE_INCREASE, 0)
    }

    fun setFirstDoseIncrease(value: Int) {
        return prefs.edit().putInt(FIRST_DOSE_INCREASE, value).apply()
    }

    fun getSecondDoseTotal(): Int {
        return prefs.getInt(SECOND_DOSE_TOTAL, 0)
    }

    fun setSecondDoseTotal(value: Int) {
        return prefs.edit().putInt(SECOND_DOSE_TOTAL, value).apply()
    }

    fun getSecondDoseIncrease(): Int {
        return prefs.getInt(SECOND_DOSE_INCREASE, 0)
    }

    fun setSecondDoseIncrease(value: Int) {
        return prefs.edit().putInt(SECOND_DOSE_INCREASE, value).apply()
    }

    fun getDailyDosesDate(): Long {
        return prefs.getLong(DAILY_DOSES_DATE, 0)
    }

    fun setDailyDosesDate(value: Long) {
        return prefs.edit().putLong(DAILY_DOSES_DATE, value).apply()
    }

    fun getConfirmedCasesTotal(): Int {
        return prefs.getInt(CONFIRMED_CASES_TOTAL, 0)
    }

    fun setConfirmedCasesTotal(value: Int) {
        return prefs.edit().putInt(CONFIRMED_CASES_TOTAL, value).apply()
    }

    fun getConfirmedCasesIncrease(): Int {
        return prefs.getInt(CONFIRMED_CASES_INCREASE, 0)
    }

    fun setConfirmedCasesIncrease(value: Int) {
        return prefs.edit().putInt(CONFIRMED_CASES_INCREASE, value).apply()
    }

    fun getConfirmedCasesIncreaseDate(): Long {
        return prefs.getLong(CONFIRMED_CASES_INCREASE_DATE, 0)
    }

    fun setConfirmedCasesIncreaseDate(value: Long) {
        return prefs.edit().putLong(CONFIRMED_CASES_INCREASE_DATE, value).apply()
    }

    fun getActiveCasesTotal(): Int {
        return prefs.getInt(ACTIVE_CASES_TOTAL, 0)
    }

    fun setActiveCasesTotal(value: Int) {
        return prefs.edit().putInt(ACTIVE_CASES_TOTAL, value).apply()
    }

    fun getCuredTotal(): Int {
        return prefs.getInt(CURED_TOTAL, 0)
    }

    fun setCuredTotal(value: Int) {
        return prefs.edit().putInt(CURED_TOTAL, value).apply()
    }

    fun getDeceasedTotal(): Int {
        return prefs.getInt(DECEASED_TOTAL, 0)
    }

    fun setDeceasedTotal(value: Int) {
        return prefs.edit().putInt(DECEASED_TOTAL, value).apply()
    }

    fun getCurrentlyHospitalizedTotal(): Int {
        return prefs.getInt(CURRENTLY_HOSPITALIZED_TOTAL, 0)
    }

    fun setCurrentlyHospitalizedTotal(value: Int) {
        return prefs.edit().putInt(CURRENTLY_HOSPITALIZED_TOTAL, value).apply()
    }

    fun getActivationsTotal(): Int {
        return prefs.getInt(ACTIVATIONS_TOTAL, 0)
    }

    fun setActivationsTotal(value: Int) {
        return prefs.edit().putInt(ACTIVATIONS_TOTAL, value).apply()
    }

    fun getActivationsYesterday(): Int {
        return prefs.getInt(ACTIVATIONS_YESTERDAY, 0)
    }

    fun setActivationsYesterday(value: Int) {
        return prefs.edit().putInt(ACTIVATIONS_YESTERDAY, value).apply()
    }

    fun getKeyPublishersTotal(): Int {
        return prefs.getInt(KEY_PUBLISHERS_TOTAL, 0)
    }

    fun setKeyPublishersTotal(value: Int) {
        return prefs.edit().putInt(KEY_PUBLISHERS_TOTAL, value).apply()
    }

    fun getKeyPublishersYesterday(): Int {
        return prefs.getInt(KEY_PUBLISHERS_YESTERDAY, 0)
    }

    fun setKeyPublishersYesterday(value: Int) {
        return prefs.edit().putInt(KEY_PUBLISHERS_YESTERDAY, value).apply()
    }

    fun getNotificationsTotal(): Int {
        return prefs.getInt(NOTIFICATIONS_TOTAL, 0)
    }

    fun setNotificationsTotal(value: Int) {
        return prefs.edit().putInt(NOTIFICATIONS_TOTAL, value).apply()
    }

    fun getNotificationsYesterday(): Int {
        return prefs.getInt(NOTIFICATIONS_YESTERDAY, 0)
    }

    fun setNotificationsYesterday(value: Int) {
        return prefs.edit().putInt(NOTIFICATIONS_YESTERDAY, value).apply()
    }

    fun wasEFGSIntroduced(): Boolean {
        return prefs.getBoolean(EFGS_INTRODUCED, false)
    }

    fun setEFGSIntroduced(value: Boolean) {
        return prefs.edit().putBoolean(EFGS_INTRODUCED, value).apply()
    }

    fun wasHowItWorksShown(): Boolean {
        return prefs.getBoolean(HOW_IT_WORKS_SHOWN, false)
    }

    fun setHowItWorksShown() {
        prefs.edit().putBoolean(HOW_IT_WORKS_SHOWN, true).apply()
    }

    fun setVerificationData(code: String, token: String) {
        prefs.edit().putString(VALIDATION_CODE, code)
            .putString(VALIDATION_TOKEN, token).apply()
    }

    fun getVerificationCode(): String? {
        return prefs.getString(VALIDATION_CODE, null)
    }

    fun getVerificationToken(): String? {
        return prefs.getString(VALIDATION_TOKEN, null)
    }

    fun deletePublishKeysTemporaryData() {
        prefs.edit().remove(VALIDATION_CODE)
            .remove(VALIDATION_TOKEN)
            .remove(CONSENT_TO_FEDERATION)
            .remove(SYMPTOM_DATE)
            .apply()
    }

    fun isCodeValidated(code: String?): Boolean {
        val savedCode = prefs.getString(VALIDATION_CODE, null)
        return if (savedCode == code) {
            hasValidationToken(useLeeway = true)
        } else {
            false
        }
    }

    fun hasValidationToken(useLeeway: Boolean): Boolean {
        val token = prefs.getString(VALIDATION_TOKEN, null)
        return if (token != null) {
            //Leeway is time, which is subtracted from expiration, to be sure, user has enough time to complete the process before expiration
            !JWT(token).isExpired(if (useLeeway) AppConfig.validationTokenExpirationLeewayMinutes * 60 else 60)
        } else {
            false
        }
    }

    fun setSymptomDate(timestamp: Long?) {
        if (timestamp == null) {
            prefs.edit().remove(SYMPTOM_DATE).apply()
        } else {
            prefs.edit().putLong(SYMPTOM_DATE, timestamp).apply()
        }
    }

    fun getSymptomOnsetInterval(): Long? {
        return prefs.getLong(SYMPTOM_DATE, 0L).let {
            //Unix timestamp / 600
            if (it != 0L) TimeUnit.SECONDS.convert(it, TimeUnit.MILLISECONDS) / 600 else null
        }
    }

    fun setLastDataSentDate() {
        prefs.edit().putLong(LAST_DATA_SENT_TIME, System.currentTimeMillis()).apply()
    }

    fun getLastDataSentDateString(): String? {
        return prefs.getLong(LAST_DATA_SENT_TIME, 0L).let {
            if (it != 0L) it.timestampToDate() else null
        }
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/ExposureCryptoTools.kt
================================================
package cz.covid19cz.erouska.exposurenotifications

import android.util.Base64
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import cz.covid19cz.erouska.utils.L
import java.nio.charset.StandardCharsets
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.ArrayList
import kotlin.random.Random

@Singleton
class ExposureCryptoTools @Inject constructor() {

    fun hashedKeys(keys: List<TemporaryExposureKey>, hmacKey: String): String {
        val cleartextSegments = ArrayList<HashedKeyData>()
        for (k in keys) {
            val base64key = k.keyData.encodeBase64()
            cleartextSegments.add(
                HashedKeyData(
                    base64key, String.format(
                        Locale.ENGLISH,
                        "%s.%d.%d",
                        base64key,
                        k.rollingStartIntervalNumber,
                        k.rollingPeriod
                    )
                )
            )
        }
        val cleartext = cleartextSegments.sortedBy { it.base64key }.joinToString(",") { it.data }
        L.i("Hashing ${keys.size} keys")
        val mac = Mac.getInstance("HmacSHA256")
        mac.init(SecretKeySpec(hmacKey.decodeBase64(), "HmacSHA256"))
        return mac.doFinal(cleartext.toByteArray(StandardCharsets.UTF_8)).encodeBase64()
    }

    fun newHmacKey(): String {
        val bytes = ByteArray(16)
        Random.nextBytes(bytes)
        return bytes.encodeBase64()
    }

    private class HashedKeyData(val base64key: String, val data: String)
}

fun ByteArray.encodeBase64(): String {
    return Base64.encodeToString(this, Base64.NO_WRAP)
}

fun String.decodeBase64(): ByteArray {
    return Base64.decode(this, Base64.NO_WRAP)
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/ExposureNotificationsErrorHandling.kt
================================================
package cz.covid19cz.erouska.exposurenotifications

import android.content.Context
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ui.dashboard.event.GmsApiErrorEvent
import cz.covid19cz.erouska.utils.DeviceInfo
import cz.covid19cz.erouska.utils.SupportEmailGenerator
import java.util.regex.Matcher
import java.util.regex.Pattern
import javax.inject.Inject
import javax.inject.Singleton


/**
 * This class is heavily inspired by Swiss COVID app:
 * https://github.com/DP-3T/dp3t-app-android-ch/blob/1cc2f7cef39206a09ad74ddbdcce69dd7af7d03b/app/src/main/java/ch/admin/bag/dp3t/util/ENExceptionHelper.java
 */
@Singleton
class ExposureNotificationsErrorHandling @Inject constructor(
    private val deviceInfo: DeviceInfo,
    private val supportEmailGenerator: SupportEmailGenerator
) {

    companion object {
        const val REQUEST_GMS_ERROR_RESOLUTION = 42
        private const val ERROR_CODE_UNKNOWN = -2
    }

    private val CONNECTION_RESULT_PATTERN: Pattern =
        Pattern.compile("ConnectionResult\\{[^}]*statusCode=[a-zA-Z0-9_]+\\((\\d+)\\)")

    fun handle(gmsApiErrorEvent: GmsApiErrorEvent, fragment: Fragment, screen: String) {
        if (gmsApiErrorEvent.throwable is ApiException) {
            try {
                fragment.startIntentSenderForResult(
                    gmsApiErrorEvent.throwable.status.resolution?.intentSender,
                    REQUEST_GMS_ERROR_RESOLUTION,
                    null,
                    0,
                    0,
                    0,
                    null
                )
            } catch (t: Throwable) {
                showErrorDialog(fragment, gmsApiErrorEvent.throwable, screen)
            }
        } else {
            showErrorDialog(fragment, gmsApiErrorEvent.throwable, screen)
        }
    }

    private fun showErrorDialog(fragment: Fragment, throwable: Throwable, screen: String) {
        val errorMessage = getErrorMessage(throwable, fragment.requireContext())
        AlertDialog.Builder(fragment.requireContext())
            .setTitle(fragment.getString(R.string.activation_error))
            .setMessage(
                fragment.getString(
                    R.string.activation_error_reason,
                    errorMessage
                )
            )
            .setPositiveButton(R.string.support_request_button) { _, _ ->
                supportEmailGenerator.sendSupportEmail(
                    fragment.requireActivity(),
                    fragment.lifecycleScope,
                    errorCode = errorMessage,
                    isError = true,
                    screenOrigin = screen
                )
            }.setNegativeButton(R.string.send_data_close) { _, _ -> }.show()
    }

    private fun getErrorMessage(exception: Throwable, context: Context): String {
        var errorDetailMessage: String? = null
        var attachExceptionMessage = true
        if (exception is ApiException) {
            val status = exception.status
            if (status.statusCode == CommonStatusCodes.API_NOT_CONNECTED && status.statusMessage != null) {
                when (val connectionStatusCode: Int = getConnectionStatusCode(status)) {
                    ExposureNotificationStatusCodes.FAILED_NOT_SUPPORTED -> if (!deviceInfo.supportsBLE()) {
                        errorDetailMessage =
                            context.getString(R.string.activation_error_reason_bluetooth_le)
                        attachExceptionMessage = false
                    } else if (!deviceInfo.isUserDeviceOwner()) {
                        errorDetailMessage =
                            context.getString(R.string.activation_error_reason_admin)
                        attachExceptionMessage = false
                    } else if (!deviceInfo.supportsMultiAds()) {
                        errorDetailMessage =
                            context.getString(R.string.activation_error_reason_bluetooth_ad)
                    } else {
                        errorDetailMessage =
                            context.getString(R.string.activation_error_reason_en_api)
                    }
                    ExposureNotificationStatusCodes.FAILED_UNAUTHORIZED -> {
                        errorDetailMessage =
                            context.getString(R.string.activation_error_reason_unauthorized)
                    }
                    else -> errorDetailMessage =
                        ExposureNotificationStatusCodes.getStatusCodeString(connectionStatusCode)
                }
            }
        }
        return if (errorDetailMessage != null) {
            if (attachExceptionMessage) {
                "$errorDetailMessage\n${exception.message}"
            } else {
                errorDetailMessage
            }
        } else {
            exception.message ?: ""
        }
    }

    private fun getConnectionStatusCode(status: Status): Int {
        val statusMessage = status.statusMessage
        if (statusMessage != null) {
            val matcher: Matcher = CONNECTION_RESULT_PATTERN.matcher(statusMessage)
            if (matcher.find()) {
                val connectionStatusCode: String? = matcher.group(1)
                return connectionStatusCode?.toInt() ?: ERROR_CODE_UNKNOWN
            }
        }
        return ERROR_CODE_UNKNOWN
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/ExposureNotificationsRepository.kt
================================================
package cz.covid19cz.erouska.exposurenotifications

import android.content.Context
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.google.android.gms.nearby.exposurenotification.*
import com.google.gson.Gson
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.db.DailySummariesDb
import cz.covid19cz.erouska.db.DailySummaryEntity
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.exposurenotifications.worker.SelfCheckerWorker
import cz.covid19cz.erouska.net.ExposureServerRepository
import cz.covid19cz.erouska.net.FirebaseFunctionsRepository
import cz.covid19cz.erouska.net.model.*
import cz.covid19cz.erouska.ui.verification.InvalidTokenException
import cz.covid19cz.erouska.ui.verification.NoKeysException
import cz.covid19cz.erouska.ui.verification.ReportExposureException
import cz.covid19cz.erouska.ui.verification.VerifyException
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.qualifiers.ApplicationContext
import org.threeten.bp.LocalDate
import retrofit2.HttpException
import java.io.File
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

@Singleton
class ExposureNotificationsRepository @Inject constructor(
    @ApplicationContext private val context: Context,
    private val client: ExposureNotificationClient,
    private val server: ExposureServerRepository,
    private val cryptoTools: ExposureCryptoTools,
    private val prefs: SharedPrefsRepository,
    private val firebaseFunctionsRepository: FirebaseFunctionsRepository,
    private val db: DailySummariesDb,
    private val notifications: Notifications
) {

    suspend fun start() = suspendCoroutine<Void> { cont ->
        client.start()
            .addOnSuccessListener {
                prefs.setExposureNotificationsEnabled(true)
                cont.resume(it)
            }.addOnFailureListener {
                cont.resumeWithException(it)
            }
    }

    suspend fun stop() = suspendCoroutine<Void> { cont ->
        client.stop()
            .addOnSuccessListener {
                prefs.setExposureNotificationsEnabled(false)
                cont.resume(it)
            }.addOnFailureListener {
                cont.resumeWithException(it)
            }
    }

    suspend fun isEnabled(): Boolean = suspendCoroutine { cont ->
        client.isEnabled
            .addOnSuccessListener {
                cont.resume(it)
            }.addOnFailureListener {
                cont.resumeWithException(it)
            }
    }

    suspend fun getStatus(): Set<ExposureNotificationStatus> = suspendCoroutine { cont ->
        client.status
            .addOnSuccessListener {
                cont.resume(it)
            }.addOnFailureListener {
                cont.resumeWithException(it)
            }
    }

    suspend fun provideDiagnosisKeys(
        keyList: List<DownloadedKeys>
    ): Boolean = suspendCoroutine { cont ->

        setDiagnosisKeysMapping()

        val filesToImport = mutableListOf<File>()
        keyList.forEach { keys ->
            if (keys.isValid()) {
                if (keys.files.isNotEmpty()) {
                    L.i("Importing keys ${keys.indexUrl}")
                    filesToImport.addAll(keys.files)
                } else {
                    L.i("Import skipped (no new data) ${keys.indexUrl}")
                }
            } else {
                L.i("Import skipped (invalid data) ${keys.indexUrl}")
            }
        }

        if (filesToImport.isEmpty()) {
            L.i("All skipped (empty)")
            prefs.setLastKeyImport()
        } else {
            client.provideDiagnosisKeys(filesToImport)
                .addOnSuccessListener {
                    L.i("Import success of ${filesToImport.size} files")
                    prefs.setLastKeyImport()
                    keyList.forEach { keys ->
                        if (keys.isValid() && keys.files.isNotEmpty()) {
                            L.d("Last successful import for ${keys.indexUrl} is ${keys.getLastUrl()}")
                            prefs.setLastKeyExportFileName(keys.indexUrl, keys.getLastUrl())
                        }
                    }
                    cont.resume(true)
                }.addOnFailureListener {
                    cont.resumeWithException(it)
                }
        }
    }

    private fun setDiagnosisKeysMapping() {
        if (System.currentTimeMillis() - prefs.getLastSetDiagnosisKeysDataMapping() > AppConfig.diagnosisKeysDataMappingLimitDays * 24 * 60 * 60 * 1000) {
            val daysList = AppConfig.daysSinceOnsetToInfectiousness
            val daysToInfectiousness = mutableMapOf<Int, Int>()
            for (i in -14..14) {
                daysToInfectiousness[i] = daysList[i + 14]
            }
            val mapping = DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder()
                .setDaysSinceOnsetToInfectiousness(daysToInfectiousness)
                .setInfectiousnessWhenDaysSinceOnsetMissing(AppConfig.infectiousnessWhenDaysSinceOnsetMissing)
                .setReportTypeWhenMissing(AppConfig.reportTypeWhenMissing)
                .build()
            try {
                client.setDiagnosisKeysDataMapping(mapping)
            } catch (t: Throwable) {
                L.e(t)
            } finally {
                prefs.setLastSetDiagnosisKeysDataMapping()
            }
        }
    }

    suspend fun getDailySummariesFromApi(filter: Boolean = true): List<DailySummary> =
        suspendCoroutine { cont ->

            val reportTypeWeights = prefs.getReportTypeWeights() ?: AppConfig.reportTypeWeights
            val attenuationBucketThresholdDb =
                prefs.getAttenuationBucketThresholdDb()
                    ?: AppConfig.attenuationBucketThresholdDb
            val attenuationBucketWeights =
                prefs.getAttenuationBucketWeights() ?: AppConfig.attenuationBucketWeights
            val infectiousnessWeights =
                prefs.getInfectiousnessWeights() ?: AppConfig.infectiousnessWeights

            client.getDailySummaries(
                DailySummariesConfig.DailySummariesConfigBuilder().apply {

                    setReportTypeWeight(ReportType.CONFIRMED_TEST, reportTypeWeights[1])
                    setReportTypeWeight(
                        ReportType.CONFIRMED_CLINICAL_DIAGNOSIS,
                        reportTypeWeights[2]
                    )
                    setReportTypeWeight(ReportType.SELF_REPORT, reportTypeWeights[3])
                    setReportTypeWeight(ReportType.RECURSIVE, reportTypeWeights[4])

                    setInfectiousnessWeight(Infectiousness.STANDARD, infectiousnessWeights[1])
                    setInfectiousnessWeight(Infectiousness.HIGH, infectiousnessWeights[2])

                    setAttenuationBuckets(attenuationBucketThresholdDb, attenuationBucketWeights)
                    setMinimumWindowScore(AppConfig.minimumWindowScore)
                }.build()
            ).addOnSuccessListener {
                if (filter) {
                    cont.resume(it.filter {
                        it.summaryData.maximumScore >= AppConfig.minimumWindowScore
                    })
                } else {
                    cont.resume(it)
                }

            }.addOnFailureListener {
                cont.resumeWithException(it)
            }
        }


    suspend fun getDailySummariesFromDbByExposureDate(): List<DailySummaryEntity> {
        return db.dao().getAllByExposureDate()
    }

    suspend fun getDailySummariesFromDbByImportDate(): List<DailySummaryEntity> {
        return db.dao().getAllByImportDate()
    }

    suspend fun getLastRiskyExposure(demo: Boolean? = false): DailySummaryEntity? {
        val lastExposure = db.dao().getLatest().firstOrNull()
        return if (lastExposure == null && demo == true) {
            getLastRiskyExposureForDemo()
        } else {
            lastExposure
        }
    }

    private fun getLastRiskyExposureForDemo(): DailySummaryEntity {
        return DailySummaryEntity(
            LocalDate.now().minusDays(1).toEpochDay().toInt(),
            1000.0,
            1000.0,
            1000.0,
            0,
            notified = false,
            accepted = false
        )
    }

    suspend fun markAsAccepted() {
        db.dao().markAsAccepted()
    }

    suspend fun getTemporaryExposureKeyHistory(): List<TemporaryExposureKey> =
        suspendCoroutine { cont ->
            client.temporaryExposureKeyHistory.addOnSuccessListener {
                cont.resume(it)
            }.addOnFailureListener {
                cont.resumeWithException(it)
            }
        }

    suspend fun getExposureWindows(): List<ExposureWindow> = suspendCoroutine { cont ->
        client.exposureWindows
            .addOnSuccessListener {
                cont.resume(it)
            }.addOnFailureListener {
                cont.resumeWithException(it)
            }
    }

    suspend fun verifyCode(code: String) {
        try {
            val verifyResponse = server.verifyCode(VerifyCodeRequest(code))
            if (verifyResponse.token != null) {
                L.i("Verify code success")
                prefs.setVerificationData(code, verifyResponse.token)
            } else {
                throw VerifyException(verifyResponse.error, verifyResponse.errorCode)
            }
        } catch (e: HttpException) {
            var errorResponse: VerifyCodeResponse? = null
            try {
                val errorBody = e.response()?.errorBody()?.string()
                errorResponse =
                    Gson().fromJson<VerifyCodeResponse>(errorBody, VerifyCodeResponse::class.java)
            } catch (e: Throwable) {
                L.e(e)
            }
            // called when we have HTTP not 200
            if (e.code() == 500 && AppConfig.handleError500AsInvalidCode) {
                // This should be enabled only on the old prod server
                throw VerifyException("Invalid code", VerifyCodeResponse.ERROR_CODE_INVALID_CODE)
            } else if (e.code() == 400) {
                if (errorResponse?.errorCode == VerifyCodeResponse.ERROR_CODE_INVALID_CODE || errorResponse?.errorCode == VerifyCodeResponse.ERROR_CODE_EXPIRED_CODE) {
                    throw VerifyException(errorResponse.error, errorResponse.errorCode)
                } else if (AppConfig.handleError400AsExpiredOrUsedCode) {
                    throw VerifyException(
                        errorResponse?.error,
                        VerifyCodeResponse.ERROR_CODE_EXPIRED_USED_CODE
                    )
                } else {
                    throw VerifyException(errorResponse?.error, errorResponse?.errorCode)
                }
            } else {
                throw VerifyException(errorResponse?.error, errorResponse?.errorCode)
            }
        }
    }

    suspend fun publishKeys(): Int {
        val keys = getTemporaryExposureKeyHistory()
        if (keys.isEmpty()) {
            L.e("No keys found, upload cancelled")
            throw NoKeysException()
        }
        if (prefs.hasValidationToken(useLeeway = false)) {
            val token = prefs.getVerificationToken()!!
            val hmackey = cryptoTools.newHmacKey()
            val keyHash = cryptoTools.hashedKeys(keys, hmackey)

            val certificateResponse = server.verifyCertificate(
                VerifyCertificateRequest(token, keyHash)
            )
            if (certificateResponse.error != null) {
                // We ignore error in certificate verification, only log it. It was causing error in production builds with older server.
                L.e("Error in certificate verification: " + certificateResponse.error + " (" + certificateResponse.errorCode + ")")
            } else {
                L.i("Verify certificate success")
            }

            val dtos = keys.map {
                TemporaryExposureKeyDto(
                    it.keyData.encodeBase64(),
                    it.rollingStartIntervalNumber,
                    it.rollingPeriod
                )
            }
            L.i("Uploading ${dtos.size} keys")
            val response = server.reportExposure(dtos, certificateResponse.certificate, hmackey)
            response.errorMessage?.let {
                L.e("Report exposure failed: $it")
                throw ReportExposureException(it, response.code)
            }
            L.i("Report exposure success, ${response.insertedExposures} keys inserted")
            return response.insertedExposures ?: 0
        } else {
            throw InvalidTokenException()
        }
    }

    suspend fun checkExposure() {
        db.dao().deleteOld()
        val timestamp = System.currentTimeMillis()
        db.dao().insert(getDailySummariesFromApi().map {
            DailySummaryEntity(
                daysSinceEpoch = it.daysSinceEpoch,
                maximumScore = it.summaryData.maximumScore,
                scoreSum = it.summaryData.scoreSum,
                weightenedDurationSum = it.summaryData.weightedDurationSum,
                importTimestamp = timestamp,
                notified = false,
                accepted = false
            )
        })

        // latest exposure found in the database
        val latestExposure = db.dao().getLatest().firstOrNull()
        val latestExposureTime = latestExposure?.daysSinceEpoch

        // latest exposure that the user was not notified about yet
        val lastNotifiedExposureTime = db.dao().getLastNotified().firstOrNull()?.daysSinceEpoch

        // the app should show a notification if there is a new exposure the user was not notified
        // about, yet, or if there is an exposure, but the app has not been opened since the last
        // last notification
        val userNotNotifiedAboutLatest = latestExposureTime != null
                && latestExposureTime != lastNotifiedExposureTime
        val lastAppUsedTimestamp = prefs.getLastTimeAppVisited()
        // Suppress showing update screen after launching the app for first time with a new exposure.
        prefs.setSuppressUpdateScreens(true)
        // We can use the import timestamp of the exposure as we are interested in comparing whether
        // the user visited the app after being notified. The notification can take place only when
        // the exposure is imported.
        // In case there is no exposure, the timestamp will default to 0.
        // It won't cause a false positive as the app timestamp will always be greater than 0.
        val lastExposureTimestamp = latestExposure?.importTimestamp ?: 0L
        // If the app visit timestamp is not saved yet, it acts as if the user has not opened the app.
        // To reduce false positives, we should check the timestamp is non-zero.
        val appNotOpenedSinceLastNotification = (lastAppUsedTimestamp > 0)
                && (lastExposureTimestamp > lastAppUsedTimestamp)

        val shouldNotify = userNotNotifiedAboutLatest || appNotOpenedSinceLastNotification

        if (shouldNotify) {
            notifications.showRiskyExposureNotification()
            db.dao().markAsNotified()
            firebaseFunctionsRepository.registerNotification()
        } else {
            L.i(
                "Not showing notification, lastExposure=$latestExposureTime, " +
                        "lastNotifiedExposure=$lastNotifiedExposureTime"
            )
        }
    }

    fun scheduleSelfChecker() {
        //TODO: Uncomment if eRouška gets resurrected
        /*val constraints = Constraints.Builder().build()
        val worker = PeriodicWorkRequestBuilder<SelfCheckerWorker>(
            AppConfig.selfCheckerPeriodHours,
            TimeUnit.HOURS
        ).setConstraints(constraints)
            .addTag(SelfCheckerWorker.TAG)
            .build()

        WorkManager.getInstance(context)
            .enqueueUniquePeriodicWork(
                SelfCheckerWorker.TAG,
                ExistingPeriodicWorkPolicy.REPLACE,
                worker
            )*/
    }

    suspend fun isEligibleToDownloadKeys(): Boolean {
        return isEnabled() && System.currentTimeMillis() - prefs.getLastKeyImport() >= AppConfig.keyImportPeriodHours * 60 * 60 * 1000
    }

    fun isLocationlessScanSupported() = client.deviceSupportsLocationlessScanning()

}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/Notifications.kt
================================================
package cz.covid19cz.erouska.exposurenotifications

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_MAX
import androidx.work.WorkManager
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.ktx.Firebase
import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.ktx.messaging
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.ext.isNetworkAvailable
import cz.covid19cz.erouska.net.FirebaseFunctionsRepository
import cz.covid19cz.erouska.ui.main.MainActivity
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class Notifications @Inject constructor(
    @ApplicationContext private val context: Context,
    private val prefs: SharedPrefsRepository,
    private val firebaseFunctionsRepository: FirebaseFunctionsRepository
) {

    companion object {
        const val CHANNEL_ID_EXPOSURE = "EXPOSURE"
        const val CHANNEL_ID_OUTDATED_DATA = "OUTDATED_DATA"
        const val CHANNEL_ID_NOT_RUNNING = "NOT_RUNNING"
        const val CHANNEL_ID_DOWNLOADING = "DOWNLOADING"

        const val REQ_ID_EXPOSURE = 100
        const val REQ_ID_OUTDATED_DATA = 101
        const val REQ_ID_NOT_RUNNING = 102
        const val REQ_ID_DOWNLOADING = 103
    }

    fun showErouskaPausedNotification() {
        showNotification(
            R.string.dashboard_title_paused,
            R.string.notification_exposure_notifications_off_text,
            CHANNEL_ID_NOT_RUNNING
        )
    }

    fun showRiskyExposureNotification() {
        showNotification(
            R.string.notification_exposure_title,
            R.string.notification_exposure_text,
            CHANNEL_ID_EXPOSURE,
            autoCancel = true,
            color = Color.RED,
            priority = PRIORITY_MAX
        )
    }

    fun showOutdatedDataNotification() {
        showNotification(
            context.getString(R.string.notification_data_outdated_title),
            AppConfig.recentExposureNotificationTitle,
            CHANNEL_ID_OUTDATED_DATA
        )

    }

    private fun showNotification(
        @StringRes title: Int,
        @StringRes text: Int,
        channelId: String,
        autoCancel: Boolean = false,
        color: Int? = null,
        priority: Int? = null
    ) {
        showNotification(
            context.getString(title),
            context.getString(text),
            channelId,
            autoCancel,
            color,
            priority
        )
    }

    private fun showNotification(
        title: String,
        text: String,
        channelId: String,
        autoCancel: Boolean = false,
        color: Int? = null,
        priority: Int? = null
    ) {
        val builder = NotificationCompat.Builder(context, channelId)
            .setContentTitle(title)
            .setContentText(text)
            .setSmallIcon(R.drawable.ic_notification_normal)
            .setContentIntent(getContentIntent())
            .setAutoCancel(autoCancel)
            .setStyle(
                NotificationCompat.BigTextStyle()
                    .bigText(text)
                    .setBigContentTitle(title)
            )

        color?.let {
            builder.setColorized(true)
            builder.color = color
        }
        priority?.let {
            builder.priority = priority
        }

        (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(
            when (channelId) {
                CHANNEL_ID_EXPOSURE -> REQ_ID_EXPOSURE
                CHANNEL_ID_OUTDATED_DATA -> REQ_ID_OUTDATED_DATA
                CHANNEL_ID_NOT_RUNNING -> REQ_ID_NOT_RUNNING
                else -> 0
            }, builder.build()
        )
    }

    fun dismissNotRunningNotification() {
        dismissNotification(REQ_ID_NOT_RUNNING)
    }

    fun dismissOudatedDataNotification() {
        dismissNotification(REQ_ID_OUTDATED_DATA)
    }

    fun getDownloadingNotification(workId: UUID): Notification {
        val cancelIntent = WorkManager.getInstance(context)
            .createCancelPendingIntent(workId)
        return NotificationCompat.Builder(context, CHANNEL_ID_DOWNLOADING)
            .setContentTitle(context.getString(R.string.notification_downloading_title))
            .setContentText(context.getString(R.string.notification_downloading_description))
            .setContentIntent(getContentIntent())
            .setSmallIcon(R.drawable.ic_notification_normal)
            .addAction(
                android.R.drawable.ic_delete,
                context.getString(android.R.string.cancel),
                cancelIntent
            )
            .setOngoing(true)
            .build()
    }

    suspend fun getCurrentPushToken(): String {
        val pushToken = FirebaseMessaging.getInstance().token.await()
        L.d("Push token=$pushToken")
        return pushToken
    }

    private fun dismissNotification(id: Int) {
        (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(
            id
        )
    }

    private fun getContentIntent(): PendingIntent {
        //TODO: Uncomment if eRouška gets resurrected
        //val notificationIntent = Intent(context, MainActivity::class.java)
        //TODO: Remove if eRouška gets resurrected
        val notificationIntent = Intent(context, MainActivity::class.java)

        notificationIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP

        return PendingIntent.getActivity(
            context,
            0,
            notificationIntent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
    }

    fun init() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel(
                CHANNEL_ID_EXPOSURE,
                context.getString(R.string.notification_channel_exposure),
                NotificationManager.IMPORTANCE_MAX,
                context
            )
            createNotificationChannel(
                CHANNEL_ID_NOT_RUNNING,
                context.getString(R.string.notification_channel_exposure_notifications_off),
                NotificationManager.IMPORTANCE_DEFAULT,
                context
            )
            createNotificationChannel(
                CHANNEL_ID_OUTDATED_DATA,
                context.getString(R.string.notification_channel_outdated_data),
                NotificationManager.IMPORTANCE_DEFAULT,
                context
            )
            createNotificationChannel(
                CHANNEL_ID_DOWNLOADING,
                context.getString(R.string.notification_channel_downloading),
                NotificationManager.IMPORTANCE_MIN,
                context
            )
        }
        if (!prefs.isPushTokenRegistered() && context.isNetworkAvailable() && FirebaseAuth.getInstance().currentUser != null) {
            GlobalScope.launch {
                try {
                    firebaseFunctionsRepository.changePushToken(getCurrentPushToken())
                } catch (e: Throwable) {
                    L.e(e)
                }
            }
        }
        if (!prefs.isPushTopicRegistered() && context.isNetworkAvailable()) {
            GlobalScope.launch(Dispatchers.IO) {
                try {
                    Firebase.messaging.subscribeToTopic("budicek").await()
                    prefs.setPushTopicRegistered()
                    L.d("Topic 'budicek' registered")
                } catch (e: Throwable) {
                    L.e(e)
                }
            }
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun createNotificationChannel(
        id: String,
        name: String,
        importance: Int,
        context: Context
    ): NotificationChannel {
        val channel = NotificationChannel(id, name, importance)
        (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(
            channel
        )
        return channel
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/receiver/ExposureNotificationBroadcastReceiver.kt
================================================
/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://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 cz.covid19cz.erouska.exposurenotifications.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject

/**
 * Broadcast receiver for callbacks from exposure notification API.
 */
@AndroidEntryPoint
class ExposureNotificationBroadcastReceiver : BroadcastReceiver() {

    @Inject
    internal lateinit var exposureNotificationsRepository : ExposureNotificationsRepository

    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == ExposureNotificationClient.ACTION_EXPOSURE_STATE_UPDATED) {
            GlobalScope.launch {
                try {
                    exposureNotificationsRepository.checkExposure()
                } catch (e: Throwable) {
                    L.e(e)
                }
            }
        }
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/service/PushService.kt
================================================
package cz.covid19cz.erouska.exposurenotifications.service

import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import cz.covid19cz.erouska.net.ExposureServerRepository
import cz.covid19cz.erouska.net.FirebaseFunctionsRepository
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
class PushService : FirebaseMessagingService() {

    @Inject
    lateinit var firebaseFunctionsRepository: FirebaseFunctionsRepository

    @Inject
    lateinit var exposureNotificationsServerRepository: ExposureServerRepository

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)
        L.d("Push message received: ${message.data}")
        if (message.data.containsKey("downloadKeyExport")) {
            exposureNotificationsServerRepository.scheduleKeyDownload()
        }
    }

    override fun onNewToken(newToken: String) {
        super.onNewToken(newToken)
        L.d("New push token: $newToken")
        if (FirebaseAuth.getInstance().currentUser != null) {
            GlobalScope.launch {
                try {
                    firebaseFunctionsRepository.changePushToken(newToken)
                } catch (e: Throwable) {
                    L.e(e)
                }
            }
        }
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/worker/DownloadKeysWorker.kt
================================================
package cz.covid19cz.erouska.exposurenotifications.worker

import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.exposurenotifications.Notifications
import cz.covid19cz.erouska.net.ExposureServerRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject

@HiltWorker
class DownloadKeysWorker @AssistedInject constructor(
    @Assisted val context: Context,
    @Assisted workerParams: WorkerParameters,
    private val exposureNotificationsRepository: ExposureNotificationsRepository,
    private val serverRepository: ExposureServerRepository,
    private val notifications: Notifications
) : CoroutineWorker(context, workerParams) {

    companion object {
        const val TAG = "DOWNLOAD_KEYS"
    }

    override suspend fun doWork(): Result {
        //TODO: Remove if eRouška gets resurrected
        /**
        try {
            setForeground(ForegroundInfo(Notifications.REQ_ID_DOWNLOADING, notifications.getDownloadingNotification(id)))
            if (exposureNotificationsRepository.isEligibleToDownloadKeys()) {
                L.i("Starting download keys worker")
                Analytics.logEvent(context, Analytics.KEY_EXPORT_DOWNLOAD_STARTED)
                val result = serverRepository.downloadKeyExport()
                exposureNotificationsRepository.provideDiagnosisKeys(result)
                exposureNotificationsRepository.checkExposure()
                Analytics.logEvent(context, Analytics.KEY_EXPORT_DOWNLOAD_FINISHED)
            } else {
                L.i("Skipping download keys worker")
            }
            return Result.success()
        } catch (t: Throwable) {
            L.e(t)
            return if (runAttemptCount < 3) {
                Result.retry()
            } else {
                Result.failure()
            }
        }**/
        return Result.success()
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/worker/SelfCheckerWorker.kt
================================================
package cz.covid19cz.erouska.exposurenotifications.worker

import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.exposurenotifications.Notifications
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject

@HiltWorker
class SelfCheckerWorker @AssistedInject constructor(
    @Assisted val context: Context,
    @Assisted workerParams: WorkerParameters,
    private val prefs: SharedPrefsRepository,
    private val exposureNotificationsRepository: ExposureNotificationsRepository,
    private val notifications: Notifications
) : CoroutineWorker(context, workerParams) {

    companion object {
        const val TAG = "SELF_CHECKER"
    }

    override suspend fun doWork(): Result {
        //TODO: Remove if eRouška gets resurrected
        /**
        val hour = Calendar.getInstance(Locale.getDefault()).get(Calendar.HOUR_OF_DAY)
        if (hour in 9..19) {
            if (!exposureNotificationsRepository.isEnabled()) {
                notifications.showErouskaPausedNotification()
            }
            if (prefs.hasOutdatedKeyData()) {
                notifications.showOutdatedDataNotification()
            }
        }**/
        return Result.success()
    }

}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ext/ByteArray.kt
================================================
package cz.covid19cz.erouska.ext

val ByteArray.asHexLower inline get() = this.joinToString(separator = ""){ String.format("%02x",(it.toInt() and 0xFF))}
val String.hexAsByteArray inline get() = this.chunked(2).map { it.toUpperCase().toInt(16).toByte() }.toByteArray()

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ext/Context.kt
================================================
package cz.covid19cz.erouska.ext

import android.app.Activity
import android.bluetooth.BluetoothManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.location.LocationManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.annotation.StringRes
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.location.LocationManagerCompat
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.utils.CustomTabHelper
import cz.covid19cz.erouska.utils.L

fun Context.isBtEnabled(): Boolean {
    val btManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    return btManager.adapter.isEnabled
}

fun Context?.isLocationEnabled(): Boolean {
    val locationManager = this?.getSystemService(LocationManager::class.java) ?: return false
    return LocationManagerCompat.isLocationEnabled(locationManager)
}

fun Context.openPermissionsScreen() {
    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    val uri: Uri = Uri.fromParts("package", packageName, null)
    intent.data = uri
    startActivity(intent)
}

@Suppress("DEPRECATION")
fun Context.isNetworkAvailable(): Boolean {
    val connectivityManager =
        getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    with(connectivityManager) {
        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            activeNetworkInfo?.isConnected
        } else {
            getNetworkCapabilities(activeNetwork)?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        } ?: false
    }
}

fun Context.shareApp() {
    val text = getString(R.string.share_app_text, AppConfig.shareAppDynamicLink)
    val intent = Intent(Intent.ACTION_SEND)
    intent.type = "text/plain"
    intent.putExtra(Intent.EXTRA_TEXT, text)
    startActivity(Intent.createChooser(intent, getString(R.string.share_app_title)))
}

fun BaseFragment<*, *>.showWeb(url: String, customTabHelper: CustomTabHelper) {
    val intent = CustomTabsIntent.Builder()
        .setShowTitle(true)
        .setToolbarColor(ContextCompat.getColor(requireContext(), R.color.colorPrimary))
        .setCloseButtonIcon(
            BitmapFactory.decodeResource(
                resources,
                R.drawable.ic_action_up
            )
        )
        .build()
    if (customTabHelper.chromePackageName != null) {
        intent.launchUrl(requireContext(), Uri.parse(url))
    } else {
        // Custom Tabs not available
        try {
            startActivity(
                Intent(
                    Intent.ACTION_VIEW,
                    Uri.parse(url)
                )
            )
        } catch (e: ActivityNotFoundException) {
            L.e(e)
        }
    }
}

fun Activity.sendEmail(
    recipient: String,
    subject: Int,
    file: Uri? = null,
    @StringRes emailBody: Int
) {
    val originalIntent = createEmailShareIntent(recipient, subject, file, emailBody)
    val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
    val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
    val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0)
    val targetedIntents = originalIntentResults
        .filter { originalResult -> emailFilterIntentResults.any { originalResult.activityInfo.packageName == it.activityInfo.packageName } }
        .map {
            createEmailShareIntent(recipient, subject, file, emailBody).apply {
                `package` = it.activityInfo.packageName
            }
        }
        .toMutableList()
    if (targetedIntents.size > 0) {
        val finalIntent = Intent.createChooser(
            targetedIntents.removeAt(0),
            getString(R.string.support_email_chooser)
        )
        finalIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
        startActivity(finalIntent)
    }
}

private fun Activity.createEmailShareIntent(
    recipient: String,
    subject: Int,
    file: Uri?,
    @StringRes emailBody: Int
): Intent {
    val builder = ShareCompat.IntentBuilder.from(this)
        .setType("message/rfc822")
        .setEmailTo(arrayOf(recipient))
        .setSubject(getString(subject))
        .setText(getString(emailBody))
    if (file != null) {
        builder.setStream(file)
    }
    return builder.intent
}

fun Context.getInstallDay(): String {
    val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
    val installTimestamp = packageInfo.firstInstallTime
    return if (installTimestamp > 0) {
        installTimestamp.timestampToDate()
    } else {
        "N/A"
    }
}


================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ext/Int.kt
================================================
package cz.covid19cz.erouska.ext

import java.text.SimpleDateFormat
import java.util.*

fun Int.daysSinceEpochToDateString(pattern: String = "d. M. yyyy"): String {
    val formatter = SimpleDateFormat(pattern, Locale.getDefault())
    formatter.timeZone = TimeZone.getTimeZone("UTC")
    val dateTime = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
        timeInMillis = (toLong() * 24 * 60 * 60 * 1000)
    }
    return formatter.format(dateTime.time)
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ext/Long.kt
================================================
package cz.covid19cz.erouska.ext

import java.text.SimpleDateFormat
import java.util.*

fun Long.timestampToDate(): String {
    return SimpleDateFormat("d. M. yyyy", Locale.getDefault()).format(Date(this))
}

fun Long.timestampToTime(): String {
    return SimpleDateFormat("H:mm", Locale.getDefault()).format(Date(this))
}

fun Long.timestampToDateTime(): String {
    return SimpleDateFormat("d.M.yyyy H:mm", Locale.getDefault()).format(Date(this))
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ext/Rx.kt
================================================
package cz.covid19cz.erouska.ext

import io.reactivex.Flowable
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers

fun <T> Observable<T>.execute(onNext : (t :T) -> Unit, onError : (t :Throwable) -> Unit): Disposable {
    return inBackground().subscribe(onNext, onError)
}

fun <T> Single<T>.execute(onNext : (t :T) -> Unit, onError : (t :Throwable) -> Unit): Disposable {
    return inBackground().subscribe(onNext, onError)
}

fun <T> Maybe<T>.execute(onSuccess : (t :T) -> Unit, onError : (t :Throwable) -> Unit): Disposable {
    return inBackground().subscribe(onSuccess, onError)
}

fun <T> Flowable<T>.execute(onNext : (t :T) -> Unit, onError : (t :Throwable) -> Unit): Disposable {
    return inBackground().subscribe(onNext, onError)
}

fun <T> Observable<T>.inBackground(): Observable<T> {
    return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}

fun <T> Single<T>.inBackground(): Single<T> {
    return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}

fun <T> Maybe<T>.inBackground(): Maybe<T> {
    return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}

fun <T> Flowable<T>.inBackground(): Flowable<T> {
    return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ext/String.kt
================================================
package cz.covid19cz.erouska.ext

fun String.toIntList() : List<Int>{
    return this.split(",").map { it.toInt() }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ext/View.kt
================================================
package cz.covid19cz.erouska.ext

import android.content.Context
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText

fun EditText.setOnDoneListener(onDone: () -> Unit) {
    setOnEditorActionListener { _, actionId, _ ->
        if (actionId == EditorInfo.IME_ACTION_DONE) {
            onDone()
            return@setOnEditorActionListener true
        }
        return@setOnEditorActionListener false
    }
}

fun View.attachKeyboardController() {
    setOnFocusChangeListener { _, hasFocus ->
        if (hasFocus) {
            showKeyboard()
        } else {
            hideKeyboard()
        }
    }
}

fun View.showKeyboard() {
    post {
        val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
    }
}

fun View.hideKeyboard() {
    post {
        val im = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        im.hideSoftInputFromWindow(this.windowToken, 0)
    }
}

fun View.hide() {
    visibility = View.GONE
}

fun View.show() {
    visibility = View.VISIBLE
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/net/ExposureServerRepository.kt
================================================
package cz.covid19cz.erouska.net

import android.content.Context
import androidx.work.*
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.BuildConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.exposurenotifications.worker.DownloadKeysWorker
import cz.covid19cz.erouska.net.api.KeyServerApi
import cz.covid19cz.erouska.net.api.VerificationServerApi
import cz.covid19cz.erouska.net.model.*
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.*
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.DataInputStream
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.lang.reflect.Type
import java.net.URL
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ExposureServerRepository @Inject constructor(
    @ApplicationContext private val context: Context,
    private val prefs: SharedPrefsRepository
) {

    private val okhttpBuilder by lazy {
        val builder = OkHttpClient.Builder()
        builder.addInterceptor(UserAgentInterceptor())
        if (BuildConfig.DEBUG) {
            builder.addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            })
        }
        builder
    }

    private val keyServerClient by lazy {
        Retrofit.Builder()
            .baseUrl(context.getString(R.string.key_server_base_url))
            .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
            .client(okhttpBuilder.build())
            .build().create(KeyServerApi::class.java)
    }

    private val verificationServerClient by lazy {
        Retrofit.Builder()
            .baseUrl(context.getString(R.string.verification_server_base_url))
            .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
            .client(okhttpBuilder.addInterceptor {
                val request = it.request().newBuilder()
                    .addHeader("X-API-Key", AppConfig.verificationServerApiKey)
                    .build()
                it.proceed(request)
            }.build())
            .build().create(VerificationServerApi::class.java)
    }

    suspend fun reportExposure(
        temporaryExposureKeyDto: List<TemporaryExposureKeyDto>,
        certificate: String,
        hmackey: String
    ): ExposureResponse {
        return withContext(Dispatchers.IO) {
            keyServerClient.reportExposure(
                ExposureRequest(
                    temporaryExposureKeys = temporaryExposureKeyDto,
                    verificationPayload = certificate,
                    hmackey = hmackey,
                    revisionToken = null,
                    traveler = prefs.isTraveller(),
                    consentToFederation = prefs.isConsentToFederation(),
                    reportType = AppConfig.efgsReportType,
                    visitedCountries = if (prefs.isTraveller()) AppConfig.efgsVisitedCountries else emptyList(),
                    symptomOnsetInterval = prefs.getSymptomOnsetInterval()
                )
            )
        }
    }

    suspend fun verifyCode(request: VerifyCodeRequest): VerifyCodeResponse {
        return withContext(Dispatchers.IO) {
            verificationServerClient.verifyCode(request)
        }
    }

    suspend fun verifyCertificate(request: VerifyCertificateRequest): VerifyCertificateResponse {
        return withContext(Dispatchers.IO) {
            verificationServerClient.verifyCertificate(request)
        }
    }

    suspend fun cover(request: CoverRequest): CoverResponse {
        return withContext(Dispatchers.IO) {
            verificationServerClient.cover(request)
        }
    }

    suspend fun downloadKeyExport(): List<DownloadedKeys> {
        return withContext(Dispatchers.IO) {
            val countryUrls = parseCountryUrls(
                if (prefs.isTraveller()) {
                    AppConfig.keyExportEuTravellerUrls
                } else {
                    AppConfig.keyExportNonTravellerUrls
                }
            )
            val keysList = mutableListOf<DownloadedKeys>()
            val keysListTasks = mutableListOf<Deferred<DownloadedKeys?>>()
            countryUrls.forEach {
                keysListTasks.add(async { downloadIndex(it) })
            }
            keysList.addAll(keysListTasks.awaitAll().filterNotNull())
            return@withContext keysList
        }
    }

    private fun parseCountryUrls(json: String): List<String> {
        val countryUrlListType: Type = object : TypeToken<ArrayList<CountryUrl>?>() {}.type
        val countryUrls: ArrayList<CountryUrl> = Gson().fromJson(json, countryUrlListType)
        return countryUrls.map { it.url }
    }

    private suspend fun downloadIndex(url: String): DownloadedKeys? {
        return withContext(Dispatchers.IO) {
            val indexContent = readURLContent(url)
            if (indexContent != null) {
                val lastDownloadedFile = prefs.lastKeyExportFileName(url)
                var fileNames = indexContent.split('\n')

                // Find index of last downloaded file and get everything after it
                val indexOfLastDownload = fileNames.indexOf(lastDownloadedFile)
                if (indexOfLastDownload != -1) {
                    fileNames = fileNames.subList(indexOfLastDownload + 1, fileNames.size)
                }

                val extractedFiles = mutableListOf<File>()
                val zipUrls = fileNames.map { AppConfig.keyExportUrl + it }

                val downloads = mutableListOf<Deferred<File?>>()
                zipUrls.forEach {
                    downloads.add(async { downloadFile(it) })
                }
                extractedFiles.addAll(downloads.awaitAll().filterNotNull())
                return@withContext DownloadedKeys(url, extractedFiles, fileNames)
            } else {
                return@withContext null
            }
        }
    }

    private fun downloadFile(zipfile: String): File? {
        try {
            val dir = File(context.cacheDir.path + "/export/")
            val fileName = if (zipfile.contains("efgs")) {
                // EFGS files; e.g. efgs_de/1607061600-1607068800-00001.zip
                zipfile.split("/").takeLast(2).joinToString("/")
            } else {
                // CZ files; e.g. 1607061600-1607068800-00001.zip
                zipfile.substring(zipfile.lastIndexOf("/") + 1)
            }
            val file = File(dir.absolutePath + "/" + fileName)

            checkNotNull(file.parentFile).mkdirs()

            file.createNewFile()
            val u = URL(zipfile)
            val inputStream: InputStream = u.openStream()
            val dis = DataInputStream(inputStream)
            val buffer = ByteArray(1024)
            var length: Int
            val fos = FileOutputStream(file)
            while (dis.read(buffer).also { length = it } > 0) {
                fos.write(buffer, 0, length)
            }
            return file
        } catch (t: Throwable) {
            L.e(t)
        }
        return null
    }

    private fun readURLContent(url: String): String? {
        return try {
            val indexConnection = URL(url).openConnection()
            val indexInputStream = indexConnection.getInputStream()

            indexInputStream.readBytes().toString(Charsets.UTF_8)
        } catch (e: Throwable) {
            L.w("Skipping index download due to $e")
            null
        }
    }

    fun scheduleKeyDownload() {
        //TODO: Uncomment if eRouška gets resurrected
        /*val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
        val worker = PeriodicWorkRequestBuilder<DownloadKeysWorker>(
            AppConfig.keyImportPeriodHours,
            TimeUnit.HOURS
        ).setConstraints(constraints)
            .addTag(DownloadKeysWorker.TAG)
            .build()

        WorkManager.getInstance(context)
            .enqueueUniquePeriodicWork(
                DownloadKeysWorker.TAG,
                ExistingPeriodicWorkPolicy.REPLACE,
                worker
            )*/
    }

    fun deleteFiles() {
        val extractedDir = File(context.cacheDir.path + "/export/")
        extractedDir.deleteRecursively()
        prefs.clearLastKeyExportFileName()
        prefs.clearLastKeyImportTime()
    }

    class UserAgentInterceptor : Interceptor {

        companion object {
            const val USER_AGENT = "User-agent"
        }

        private val userAgent =
            "eRouska-Android-${BuildConfig.BUILD_TYPE}/${BuildConfig.VERSION_NAME}"

        override fun intercept(chain: Interceptor.Chain): Response {
            val requestBuilder = chain.request().newBuilder()
            requestBuilder.addHeader(USER_AGENT, userAgent)
            return chain.proceed(requestBuilder.build())
        }

    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/net/FirebaseFunctionsRepository.kt
================================================
package cz.covid19cz.erouska.net

import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.functions.ktx.functions
import com.google.firebase.ktx.Firebase
import com.google.gson.Gson
import cz.covid19cz.erouska.AppConfig.FIREBASE_REGION
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.net.exception.UnauthrorizedException
import cz.covid19cz.erouska.net.model.CovidStatsResponse
import cz.covid19cz.erouska.net.model.DownloadMetricsResponse
import cz.covid19cz.erouska.utils.DeviceInfo
import cz.covid19cz.erouska.utils.L
import cz.covid19cz.erouska.utils.LocaleUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.suspendCoroutine

@Singleton
class FirebaseFunctionsRepository @Inject constructor(
    private val deviceInfo: DeviceInfo,
    private val prefs: SharedPrefsRepository
) {

    /**
     * Creates a new registration, saved to registrations collection.
     */
    suspend fun register(pushRegistrationToken: String) {
        withContext(Dispatchers.IO) {
            val data = hashMapOf(
                "platform" to "android",
                "platformVersion" to deviceInfo.getAndroidVersion(),
                "manufacturer" to deviceInfo.getManufacturer(),
                "model" to deviceInfo.getDeviceName(),
                "locale" to LocaleUtils.getLocale(),
                "pushRegistrationToken" to pushRegistrationToken
            )
            val token = checkNotNull(callFunction("RegisterEhrid", data)["customToken"])
            FirebaseAuth.getInstance().signInWithCustomToken(token).await()
            if (FirebaseAuth.getInstance().currentUser == null) {
                throw RuntimeException("Sign in failed")
            }
            prefs.setPushTokenRegistered()
        }
    }

    /**
     * Returns data from collections covidDataIncrease and covidDataTotal for the given input date (if date is missing, TODAY is used)
     */
    suspend fun getStats(date: String? = null): CovidStatsResponse {
        return withContext(Dispatchers.IO) {
            val data = hashMapOf(
                "idToken" to getIdToken(),
                "date" to date
            )
            val covidStats = callFunction("GetCovidData", data)
            Gson().fromJson(covidStats.toString(), CovidStatsResponse::class.java)
        }
    }

    /**
     * Returns data from collections DownloadMetrics
     */
    suspend fun getDownloadMetrics(): DownloadMetricsResponse {
        val covidStats = callFunction("DownloadMetrics")
        return Gson().fromJson(covidStats.toString(), DownloadMetricsResponse::class.java)
    }

    /**
     * In the registrations collection, changes the value of lastNotificationStatus attribute to sent and the value of lastNotificationUpdatedAt attribute to CURRENT_TIMESTAMP for a given idToken.
     */
    suspend fun registerNotification() {
        withContext(Dispatchers.IO) {
            val data = hashMapOf(
                "idToken" to getIdToken()
            )
            callFunction("RegisterNotification", data)
        }
    }

    /**
     * Changes push token
     */
    suspend fun changePushToken(pushRegistrationToken: String) {
        withContext(Dispatchers.IO) {
            val data = hashMapOf(
                "idToken" to getIdToken(),
                "pushRegistrationToken" to pushRegistrationToken
            )
            callFunction("ChangePushToken", data)
            prefs.setPushTokenRegistered()
        }
    }

    private suspend fun getIdToken(): String = suspendCoroutine { cont ->
        if (FirebaseAuth.getInstance().currentUser != null) {
            FirebaseAuth.getInstance().currentUser?.getIdToken(false)?.addOnSuccessListener {
                cont.resumeWith(Result.success(checkNotNull(it.token)))
            }?.addOnFailureListener {
                cont.resumeWith(Result.failure(it))
            }
        } else {
            cont.resumeWith(Result.failure(UnauthrorizedException()))
        }
    }

    /**
     * Generic function for calling Firebase Function.
     */
    private suspend fun callFunction(
        name: String,
        data: Map<String, String?>? = null
    ): Map<String, String> {
        L.d("Calling function $name with data: $data")
        @Suppress("UNCHECKED_CAST")
        val response = Firebase.functions(FIREBASE_REGION).getHttpsCallable(name).call(data)
            .continueWith { task ->
                (task.result?.data as HashMap<String, String>)
            }.await()
        L.d("Function $name response: $response")
        return response
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/net/api/KeyServerApi.kt
================================================
package cz.covid19cz.erouska.net.api

import cz.covid19cz.erouska.net.model.ExposureRequest
import cz.covid19cz.erouska.net.model.ExposureResponse
import retrofit2.http.Body
import retrofit2.http.POST

interface KeyServerApi {

    @POST("PublishKeys")
    suspend fun reportExposure(@Body exposureRequest: ExposureRequest) : ExposureResponse
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/net/api/VerificationServerApi.kt
================================================
package cz.covid19cz.erouska.net.api

import cz.covid19cz.erouska.net.model.*
import retrofit2.http.Body
import retrofit2.http.POST

interface VerificationServerApi {

    @POST("api/verify")
    suspend fun verifyCode(@Body request: VerifyCodeRequest): VerifyCodeResponse

    @POST("api/certificate")
    suspend fun verifyCertificate(@Body request: VerifyCertificateRequest): VerifyCertificateResponse

    @POST("api/cover")
    suspend fun cover(@Body request: CoverRequest): CoverResponse
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/net/exception/UnauthrorizedException.kt
================================================
package cz.covid19cz.erouska.net.exception

class UnauthrorizedException : Throwable()

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/net/model/CovidDataModel.kt
================================================
package cz.covid19cz.erouska.net.model

import com.google.gson.annotations.SerializedName

data class CovidStatsResponse(
    @SerializedName("date") val date: String?,
    @SerializedName("pcrTestsTotal") val testsTotal: Int?,
    @SerializedName("pcrTestsIncrease") val testsIncrease: Int?,
    @SerializedName("pcrTestsIncreaseDate") val testsIncreaseDate: String?,
    @SerializedName("antigenTestsTotal") val antigenTestsTotal: Int?,
    @SerializedName("antigenTestsIncrease") val antigenTestsIncrease: Int?,
    @SerializedName("antigenTestsIncreaseDate") val antigenTestsIncreaseDate: String?,
    @SerializedName("vaccinationsTotal") val vaccinationsTotal: Int?,
    @SerializedName("vaccinationsIncrease") val vaccinationsIncrease: Int?,
    @SerializedName("vaccinationsIncreaseDate") val vaccinationsIncreaseDate: String?,
    @SerializedName("confirmedCasesTotal") val confirmedCasesTotal: Int?,
    @SerializedName("confirmedCasesIncrease") val confirmedCasesIncrease: Int?,
    @SerializedName("confirmedCasesIncreaseDate") val confirmedCasesIncreaseDate: String?,
    @SerializedName("activeCasesTotal") val activeCasesTotal: Int?,
    @SerializedName("activeCasesIncrease") val activeCasesIncrease: Int?,
    @SerializedName("curedTotal") val curedTotal: Int?,
    @SerializedName("curedIncrease") val curedIncrease: Int?,
    @SerializedName("deceasedTotal") val deceasedTotal: Int?,
    @SerializedName("deceasedIncrease") val deceasedIncrease: Int?,
    @SerializedName("currentlyHospitalizedTotal") val currentlyHospitalizedTotal: Int?,
    @SerializedName("currentlyHospitalizedIncrease") val currentlyHospitalizedIncrease: Int?,
    @SerializedName("vaccinationsTotalFirstDose") val vaccinationsTotalFirstDose: Int?,
    @SerializedName("vaccinationsDailyFirstDose") val vaccinationsDailyFirstDose: Int?,
    @SerializedName("vaccinationsTotalSecondDose") val vaccinationsTotalSecondDose: Int?,
    @SerializedName("vaccinationsDailySecondDose") val vaccinationsDailySecondDose: Int?,
    @SerializedName("vaccinationsDailyDosesDate") val vaccinationsDailyDosesDate: String?,
)

data class DownloadMetricsResponse(
    @SerializedName("modified") val modified: String?,
    @SerializedName("date") val date: String?,
    @SerializedName("activations_yesterday") val activationsYesterday: Int?,
    @SerializedName("activations_total") val activationsTotal: Int?,
    @SerializedName("key_publishers_yesterday") val keyPublishersYesterday: Int?,
    @SerializedName("key_publishers_total") val keyPublishersTotal: Int?,
    @SerializedName("notifications_yesterday") val notificationsYesterday: Int?,
    @SerializedName("notifications_total") val notificationsTotal: Int?
)

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/net/model/DownloadedKeys.kt
================================================
package cz.covid19cz.erouska.net.model

import cz.covid19cz.erouska.utils.L
import java.io.File

data class DownloadedKeys(
    val indexUrl: String,
    val files : List<File>,
    val urls : List<String>
) {
    fun getLastUrl() : String{
        return urls.last()
    }

    fun isValid(): Boolean {
        if (files.size != urls.size){
            L.w("Inconsistent download (${files.size}/${urls.size})")
        }
        return files.size == urls.size
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/net/model/KeyServerModel.kt
================================================
package cz.covid19cz.erouska.net.model

import android.util.Base64
import java.util.*

data class ExposureRequest(
    val temporaryExposureKeys: List<TemporaryExposureKeyDto>,
    val verificationPayload: String?,
    val hmackey: String?,
    val revisionToken: String?,
    val traveler: Boolean,
    val reportType: String,
    val visitedCountries: List<String>,
    val consentToFederation: Boolean = true,
    val symptomOnsetInterval: Long? = null,
    val padding: String = Base64.encodeToString(UUID.randomUUID().toString().toByteArray(), Base64.NO_WRAP),
    val healthAuthorityID: String = "cz.covid19cz.erouska",
)

data class TemporaryExposureKeyDto(
    val key: String,
    val rollingStartNumber: Int,
    val rollingPeriod: Int
)

data class ExposureResponse(
    val revisionToken: String?,
    val insertedExposures: Int?,
    val errorMessage: String?,
    val code: String?
)

data class CountryUrl(
    val country: String,
    val url: String
)

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/net/model/VerificationServerModel.kt
================================================
package cz.covid19cz.erouska.net.model

// Taken from https://github.com/google/exposure-notifications-verification-server#api-guide-for-app-developers

data class VerifyCodeRequest(
    val code: String
)

data class VerifyCodeResponse(
    val testType: String?,
    val symptomDate: String?,
    val token: String?,
    val error: String?,
    val errorCode: String?
) {
    companion object {
        const val ERROR_CODE_INVALID_CODE = "code_invalid"
        const val ERROR_CODE_EXPIRED_CODE = "code_expired"
        const val ERROR_CODE_EXPIRED_USED_CODE = "code_expired_or_used"
    }
}

data class VerifyCertificateRequest(
    val token: String,
    val ekeyhmac: String
)

data class VerifyCertificateResponse(
    val certificate: String,
    val error: String?,
    val errorCode: String?
)

data class CoverRequest(
    val data: String
)

data class CoverResponse(
    val data: String,
    val error: String?
)

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/about/AboutFragment.kt
================================================
package cz.covid19cz.erouska.ui.about

import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.text.HtmlCompat
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.BuildConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentAboutBinding
import cz.covid19cz.erouska.ext.showWeb
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.base.UrlEvent
import cz.covid19cz.erouska.utils.CustomTabHelper
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_about.*
import javax.inject.Inject

@AndroidEntryPoint
class AboutFragment :
    BaseFragment<FragmentAboutBinding, AboutVM>(R.layout.fragment_about, AboutVM::class) {

    private var easterEggShown = false

    @Inject
    internal lateinit var customTabHelper: CustomTabHelper

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        subscribe(UrlEvent::class) {
            showWeb(it.url, customTabHelper)
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        enableUpInToolbar(true, IconType.UP)

        about_tos_content.text = HtmlCompat.fromHtml(
            getString(R.string.about_tos_content, AppConfig.conditionsOfUseUrl),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )

        about_version.setOnLongClickListener {
            if (easterEggShown) {
                throw RuntimeException("Crashlytics exception test for version ${BuildConfig.VERSION_NAME}.")
            } else {
                Toast.makeText(context, "Ještě jeden long-press a asi něco vypustíme", Toast.LENGTH_LONG).show()
                about_content.text = about_content.text.toString().replace("eRouška", "eRespirátor")
                easterEggShown = true
                true
            }
        }
    }
}


================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/about/AboutVM.kt
================================================
package cz.covid19cz.erouska.ui.about

import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.base.UrlEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class AboutVM @Inject constructor() : BaseVM() {

    fun tosLinkClicked() {
        publish(UrlEvent(AppConfig.conditionsOfUseUrl))
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/about/entity/AboutProfileItem.kt
================================================
package cz.covid19cz.erouska.ui.about.entity

import com.google.gson.annotations.SerializedName

class AboutProfileItem(
    @SerializedName("name")
    val name: String?
)

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationFragment.kt
================================================
package cz.covid19cz.erouska.ui.activation

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import androidx.navigation.NavOptions.Builder
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentActivationBinding
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsErrorHandling
import cz.covid19cz.erouska.ext.hide
import cz.covid19cz.erouska.ext.hideKeyboard
import cz.covid19cz.erouska.ext.show
import cz.covid19cz.erouska.ext.showWeb
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.dashboard.event.GmsApiErrorEvent
import cz.covid19cz.erouska.utils.CustomTabHelper
import cz.covid19cz.erouska.utils.SupportEmailGenerator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_activation.*
import javax.inject.Inject

@AndroidEntryPoint
class ActivationFragment :
    BaseFragment<FragmentActivationBinding, ActivationVM>(
        R.layout.fragment_activation,
        ActivationVM::class
    ) {

    companion object {
        private const val SCREEN_NAME = "Activation"
    }

    @Inject
    internal lateinit var customTabHelper: CustomTabHelper

    @Inject
    internal lateinit var exposureNotificationsErrorHandling: ExposureNotificationsErrorHandling

    @Inject
    internal lateinit var supportEmailGenerator: SupportEmailGenerator

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        subscribe(GmsApiErrorEvent::class) {
            exposureNotificationsErrorHandling.handle(it, this, SCREEN_NAME)
        }

    }

    override fun onStart() {
        super.onStart()
        viewModel.state.observe(this) {
            updateState(it)
        }
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.onboarding, menu)
        super.onCreateOptionsMenu(menu, inflater)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            android.R.id.home -> {
                val handled = onBackPressed()
                return if (handled) {
                    true
                } else {
                    super.onOptionsItemSelected(item)
                }
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupListeners()

        enableUpInToolbar(true)

        privacy_body_2.text = HtmlCompat.fromHtml(
            getString(R.string.privacy_body_text_2, AppConfig.conditionsOfUseUrl),
            HtmlCompat.FROM_HTML_MODE_LEGACY
        )
    }

    private fun setupListeners() {
        privacy_body_2.setOnClickListener { showWeb(AppConfig.conditionsOfUseUrl, customTabHelper) }
        activate_btn.setOnClickListener { viewModel.activate() }
    }

    private fun updateState(state: ActivationState) {
        when (state) {
            is ActivationStart -> onActivationStart()
            is ActivationFinished -> onActivationSuccess()
            is ActivationFailed -> onActivationFailed(state.errorMessage)
            is ActivationInit -> onActivationInit()
            is NoInternet -> onNoInternet()
        }
    }

    private fun showSignedIn() {
        if (navController().currentDestination?.id == R.id.nav_activation) {
            navigate(
                R.id.action_nav_activation_to_nav_dashboard,
                null,
                Builder().setPopUpTo(R.id.nav_graph, true).build()
            )
        }
    }

    private fun onActivationStart() {
        login_progress.show()

        activate_btn.hide()
        privacy_group.hide()
        error_group.hide()
    }

    private fun onActivationSuccess() {
        showSignedIn()
    }

    private fun onNoInternet() {
        showSnackBar(R.string.no_internet)
    }

    private fun onActivationInit() {
        activity?.setTitle(R.string.privacy_toolbar_title)

        login_progress.hide()

        activate_btn.show()
        privacy_group.show()
        error_group.hide()
    }

    private fun onActivationFailed(errorMessage: String?) {
        activity?.setTitle(R.string.error_title)
        error_body.text =
            getString(R.string.send_data_failure_body, AppConfig.supportEmail, errorMessage)
        email_button.setOnClickListener {
            supportEmailGenerator.sendSupportEmail(
                requireActivity(),
                lifecycleScope,
                errorCode = errorMessage,
                isError = true,
                screenOrigin = SCREEN_NAME
            )
        }
        login_progress.hide()

        privacy_group.hide()
        activate_btn.hide()
        error_group.show()
    }

    private fun goBack() {
        view?.hideKeyboard()
        viewModel.backPressed()
    }

    override fun onBackPressed(): Boolean {
        return if (viewModel.state.value is ActivationFailed) {
            goBack()
            true
        } else false
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when (requestCode) {
            ExposureNotificationsErrorHandling.REQUEST_GMS_ERROR_RESOLUTION -> {
                if (resultCode == Activity.RESULT_OK) {
                    viewModel.activate()
                }
            }
        }
    }
}


================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationNotificationsFragment.kt
================================================
package cz.covid19cz.erouska.ui.activation

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentActivationNotificationsBinding
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsErrorHandling
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.dashboard.event.BluetoothDisabledEvent
import cz.covid19cz.erouska.ui.dashboard.event.GmsApiErrorEvent
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class ActivationNotificationsFragment :
    BaseFragment<FragmentActivationNotificationsBinding, ActivationNotificationsVM>(
        R.layout.fragment_activation_notifications,
        ActivationNotificationsVM::class
    ) {

    companion object{
        private const val SCREEN_NAME = "Activation Notifications"
    }

    @Inject
    internal lateinit var exposureNotificationsErrorHandling: ExposureNotificationsErrorHandling

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        subscribe(NotificationsVerifiedEvent::class) {
            toEfgs()
        }

        subscribe(GmsApiErrorEvent::class) {
            exposureNotificationsErrorHandling.handle(it, this, SCREEN_NAME)
        }

        subscribe(BluetoothDisabledEvent::class) {
            requestEnableBt()
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        enableUpInToolbar(true)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when (requestCode) {
            ExposureNotificationsErrorHandling.REQUEST_GMS_ERROR_RESOLUTION -> {
                if (resultCode == Activity.RESULT_OK) {
                    viewModel.enableNotifications()
                }
            }
        }
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.onboarding, menu)
        super.onCreateOptionsMenu(menu, inflater)
    }

    private fun toEfgs() {
        navigate(ActivationNotificationsFragmentDirections.actionNavActivationNotificationsToEfgsUpdate(onboarding = true, fullscreen = true))
    }

}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationNotificationsVM.kt
================================================
package cz.covid19cz.erouska.ui.activation

import androidx.lifecycle.viewModelScope
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.dashboard.event.GmsApiErrorEvent
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class ActivationNotificationsVM @Inject constructor(
    private val exposureNotificationsRepository: ExposureNotificationsRepository
) : BaseVM() {

    fun enableNotifications() {
            viewModelScope.launch {
                runCatching {
                    if (!exposureNotificationsRepository.isEnabled()){
                        exposureNotificationsRepository.start()
                        L.d("Exposure Notifications started")
                    }
                }.onSuccess {
                    publish(NotificationsVerifiedEvent)
                }.onFailure {
                    publish(GmsApiErrorEvent(it))
                }
            }
        }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationState.kt
================================================
package cz.covid19cz.erouska.ui.activation

import arch.event.LiveEvent

sealed class ActivationState
object ActivationStart : ActivationState()
object ActivationFinished : ActivationState()
data class ActivationFailed(val errorMessage: String?) : ActivationState()
object ActivationInit : ActivationState()
object NoInternet : ActivationState()
object NotificationsVerifiedEvent : LiveEvent()

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationVM.kt
================================================
package cz.covid19cz.erouska.ui.activation

import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.android.gms.common.api.ApiException
import com.google.firebase.auth.FirebaseAuth
import cz.covid19cz.erouska.exposurenotifications.Notifications
import cz.covid19cz.erouska.ext.isNetworkAvailable
import cz.covid19cz.erouska.net.FirebaseFunctionsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.dashboard.event.GmsApiErrorEvent
import cz.covid19cz.erouska.utils.L
import cz.covid19cz.erouska.utils.LocaleUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class ActivationVM @Inject constructor(
    private val firebaseFunctionsRepository: FirebaseFunctionsRepository,
    @ApplicationContext
    private val context: Context,
    private val notifications: Notifications
) : BaseVM() {

    private val mutableState = MutableLiveData<ActivationState>()
    val state = mutableState as LiveData<ActivationState>

    private val auth: FirebaseAuth = FirebaseAuth.getInstance()

    init {
        auth.setLanguageCode(LocaleUtils.getSupportedLanguage())
        if (auth.currentUser != null) {
            mutableState.postValue(ActivationFinished)
        }
    }

    fun activate() {
        if (context.isNetworkAvailable()) {
            viewModelScope.launch(Dispatchers.IO) {
                mutableState.postValue(ActivationStart)
                try {
                    firebaseFunctionsRepository.register(notifications.getCurrentPushToken())
                    mutableState.postValue(ActivationFinished)
                } catch (e: Exception) {
                    if (e is ApiException) {
                        publish(GmsApiErrorEvent(e))
                        return@launch
                    }
                    L.e(e)
                    mutableState.postValue(ActivationFailed(e.message))
                }
            }
        } else {
            mutableState.postValue(NoInternet)
        }
    }

    fun backPressed() {
        mutableState.postValue(ActivationInit)
    }

}


================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/base/BaseActivity.kt
================================================
package cz.covid19cz.erouska.ui.base

import android.app.Activity
import android.content.Intent
import android.content.IntentSender
import androidx.databinding.ViewDataBinding
import arch.view.BaseArchActivity
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.BuildConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.utils.L
import kotlin.reflect.KClass


open class BaseActivity<B : ViewDataBinding, VM : BaseVM>(
    layoutId: Int,
    viewModelClass: KClass<VM>
) :
    BaseArchActivity<B, VM>(layoutId, viewModelClass) {
    override fun onBackPressed() {
        val childFragmentManager =
            supportFragmentManager.findFragmentById(R.id.nav_host_fragment)?.childFragmentManager

        if ((childFragmentManager?.fragments?.getOrNull(0) as? BaseFragment<*, *>)?.onBackPressed() != true) {
            super.onBackPressed()
        }
    }

    override fun onResume() {
        super.onResume()

        updateIfNeeded()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode == APP_UPDATE_REQUEST_CODE) {
            if (resultCode != Activity.RESULT_OK) {
                updateIfNeeded()
            }
        }

        super.onActivityResult(requestCode, resultCode, data)
    }

    private fun updateIfNeeded() {
        if (isObsolete()) {
            checkForAppUpdate()
        }
    }

    private fun isObsolete(): Boolean {
        return BuildConfig.VERSION_CODE < AppConfig.minSupportedVersionCodeAndroid
    }

    private fun checkForAppUpdate() {
        val appUpdateManager = AppUpdateManagerFactory.create(this)

        appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo ->
            if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
                try {
                    appUpdateManager.startUpdateFlowForResult(
                        appUpdateInfo,
                        AppUpdateType.IMMEDIATE,
                        this,
                        APP_UPDATE_REQUEST_CODE
                    )
                } catch (e: IntentSender.SendIntentException) {
                    L.e(e)
                }
            }
        }
    }

    companion object {
        const val APP_UPDATE_REQUEST_CODE = 1777
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/base/BaseFragment.kt
================================================
package cz.covid19cz.erouska.ui.base

import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.view.MenuItem
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.PackageInfoCompat
import androidx.databinding.ViewDataBinding
import arch.view.BaseArchFragment
import arch.viewmodel.BaseArchViewModel
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.snackbar.Snackbar
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.utils.L
import kotlin.reflect.KClass


abstract class BaseFragment<B : ViewDataBinding, VM : BaseArchViewModel>(
    layoutId: Int,
    viewModelClass: KClass<VM>
) : BaseArchFragment<B, VM>(layoutId, viewModelClass) {

    companion object {
        const val REQUEST_BT_ENABLE = 1000
    }

    var snackBar: Snackbar? = null

    protected open fun showSnackBar(@StringRes stringRes: Int) {
        showSnackBar(getString(stringRes))
    }

    protected open fun showSnackBarForever(@StringRes stringRes: Int) {
        showSnackBarForever(getString(stringRes))
    }

    protected open fun showSnackBar(@StringRes stringRes: Int, vararg args: Any) {
        showSnackBar(getString(stringRes, args))
    }

    protected open fun showSnackBar(text: String) {
        showSnackBarWithDuration(text, Snackbar.LENGTH_LONG)
    }

    protected open fun showSnackBarForever(text: String) {
        showSnackBarWithDuration(text, Snackbar.LENGTH_INDEFINITE)
    }

    protected open fun hideSnackBar() {
        snackBar?.dismiss()
        snackBar = null
    }

    private fun showSnackBarWithDuration(text: String, length: Int) {
        view?.let {
            if (snackBar == null) {
                snackBar = Snackbar.make(it, text, length)
            } else {
                snackBar?.setText(text)
            }
            snackBar?.show()
        }
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            android.R.id.home -> {
                return navController().navigateUp()
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    open fun onBluetoothEnabled() {
        // stub
    }

    fun enableUpInToolbar(enable: Boolean, iconType: IconType = IconType.UP) {
        (activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(enable)
        if (enable) {
            when (iconType) {
                IconType.CLOSE -> R.drawable.ic_action_close
                IconType.UP -> R.drawable.ic_action_up
            }.run {
                (activity as AppCompatActivity).supportActionBar?.setHomeAsUpIndicator(this)
            }
        }
    }

    fun setTitle(@StringRes res: Int) {
        setTitle(getString(res))
    }

    fun setTitle(title: String) {
        (activity as AppCompatActivity).supportActionBar?.title = title
    }

    fun requestEnableBt() {
        val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(enableBtIntent, REQUEST_BT_ENABLE)
    }

    fun requestLocationEnable() {
        val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
        startActivity(intent)
    }

    fun isPlayServicesObsolete(): Boolean {
        return try {
            val current = PackageInfoCompat.getLongVersionCode(
                requireContext().packageManager.getPackageInfo(
                    GoogleApiAvailability.GOOGLE_PLAY_SERVICES_PACKAGE,
                    0
                )
            )

            current < AppConfig.minGmsVersionCode
        } catch (e: Exception) {
            L.e(e)
            true
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when (requestCode) {
            REQUEST_BT_ENABLE -> {
                if (resultCode == Activity.RESULT_OK) {
                    onBluetoothEnabled()
                }
            }
        }

        super.onActivityResult(requestCode, resultCode, data)
    }

    open fun onBackPressed(): Boolean {
        return false
    }

    enum class IconType {
        UP, CLOSE
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/base/BaseVM.kt
================================================
package cz.covid19cz.erouska.ui.base

import arch.viewmodel.BaseArchViewModel
import cz.covid19cz.erouska.ext.execute
import io.reactivex.Flowable
import io.reactivex.Maybe
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable

open class BaseVM : BaseArchViewModel() {

    private val disposables = CompositeDisposable()

    fun <T> subscribe(observable: Observable<T>, onError: (Throwable) -> Unit, onNext: (T) -> Unit) : Disposable {
        val disposable = observable.execute(onNext, onError)
        disposables.add(disposable)
        return disposable
    }

    fun <T> subscribe(observable: Flowable<T>, onError: (Throwable) -> Unit, onNext: (T) -> Unit) : Disposable {
        val disposable = observable.execute(onNext, onError)
        disposables.add(disposable)
        return disposable
    }

    fun <T> subscribe(single: Single<T>, onError: (Throwable) -> Unit, onNext: (T) -> Unit) : Disposable {
        val disposable = single.execute(onNext, onError)
        disposables.add(disposable)
        return disposable
    }

    fun <T> subscribe(maybe: Maybe<T>, onError: (Throwable) -> Unit, onSuccess: (T) -> Unit) : Disposable {
        val disposable = maybe.execute(onSuccess, onError)
        disposables.add(disposable)
        return disposable
    }

    override fun onCleared() {
        disposables.dispose()
        super.onCleared()
    }
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/base/UrlEvent.kt
================================================
package cz.covid19cz.erouska.ui.base

import arch.event.LiveEvent

class UrlEvent(val url : String) : LiveEvent()

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/contacts/Contact.kt
================================================
package cz.covid19cz.erouska.ui.contacts

data class Contact(
    val title: String,
    val text: String,
    val linkTitle: String,
    val link: String
)

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/contacts/ContactsFragment.kt
================================================
package cz.covid19cz.erouska.ui.contacts

import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.observe
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentContactsBinding
import cz.covid19cz.erouska.ext.showWeb
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.contacts.event.ContactsEvent
import cz.covid19cz.erouska.utils.CustomTabHelper
import cz.covid19cz.erouska.utils.SupportEmailGenerator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class ContactsFragment : BaseFragment<FragmentContactsBinding, ContactsVM>(
    R.layout.fragment_contacts,
    ContactsVM::class
) {

    companion object {
        private const val SCREEN_NAME = "Contacts"
    }

    @Inject
    internal lateinit var customTabHelper: CustomTabHelper

    @Inject
    internal lateinit var supportEmailGenerator: SupportEmailGenerator

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewModel.state.observe(this) { processEvent(it) }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        enableUpInToolbar(false)
    }

    private fun processEvent(event: ContactsEvent) {
        if (event is ContactsEvent.ContactLinkClicked) {
            val link = event.link
            if (link.startsWith("mailto:")) {
                supportEmailGenerator.sendSupportEmail(
                    requireActivity(),
                    lifecycleScope,
                    recipient = link.split("mailto:")[1],
                    isError = false,
                    screenOrigin = SCREEN_NAME
                )
            } else {
                showWeb(link, customTabHelper)
            }
        }
    }

}


================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/contacts/ContactsVM.kt
================================================
package cz.covid19cz.erouska.ui.contacts

import androidx.databinding.ObservableArrayList
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.contacts.event.ContactsEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import java.lang.reflect.Type
import javax.inject.Inject

@HiltViewModel
class ContactsVM @Inject constructor() : BaseVM() {

    val state = MutableLiveData<ContactsEvent>()
    val items = ObservableArrayList<Contact>()

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun onCreate() {

        val contactsListType: Type = object : TypeToken<ArrayList<Contact>?>() {}.type

        val contacts: ArrayList<Contact> = Gson().fromJson(AppConfig.contactsContentJson, contactsListType)

        items.clear()
        items.addAll(contacts)

    }

    fun onLinkClick(link: String) {
        state.value = ContactsEvent.ContactLinkClicked(link)
    }

}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/contacts/event/ContactsCommandEvent.kt
================================================
package cz.covid19cz.erouska.ui.contacts.event

sealed class ContactsEvent {
    data class ContactLinkClicked(val link: String) : ContactsEvent()
}

================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/DashboardCardView.kt
================================================
package cz.covid19cz.erouska.ui.dashboard

import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ext.hide
import cz.covid19cz.erouska.ext.show
import kotlinx.android.synthetic.main.dashboard_card_view.view.*
import kotlinx.android.synthetic.main.view_data_item.view.subtitle_text
import kotlinx.android.synthetic.main.view_data_item.view.title_text


open class DashboardCardView : ConstraintLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, attributeSetId: Int) : super(
        context,
        attrs,
        attributeSetId
    )

    init {
        View.inflate(context, getLayoutId(), this)
    }

    open fun getLayoutId() = R.layout.dashboard_card_view

    var card_title: String? = null
        set(value) {
            field = value
            title_text.text = value
        }


    var card_subtitle: String? = null
        set(value) {
            field = value
            subtitle_text.text = value
            if (!value.isNullOrBlank()) {
                subtitle_text.show()
            } else {
                subtitle_text.hide()
            }
        }

    var card_button_text: String? = null
        set(value) {
            field = value
            button.text = value
            button.show()
        }

    open var card_show_right_arrow: Boolean? = false
        set(value) {
            field = value
            if (value == true) {
                title_text.setCompoundDrawablesWithIntrinsicBounds(card_icon, null, ContextCompat.getDrawable(context, R.drawable.ic_arrow_right), null)
            } else {
                title_text.setCompoundDrawablesWithIntrinsicBounds(card_icon, null, null, null)
            }
        }

    var card_on_content_click: OnClickListener? = null
        set(value) {
            field = value
            content_container.setOnClickListener(value)
            content_container.isFocusable = true
            content_container.isClickable = true
        }

    var card_actionable_button: Boolean? = false
        set(value) {
            field = value
            if (value == true) {
                button.show()
            } else {
                button.hide()
            }
        }

    var card_on_button_click: OnClickListener? = null
        set(value) {
            field = value
            card_actionable_button = value != null
            button.setOnClickListener(value)
        }

    var card_has_content: Boolean? = true
        set(value) {
            field = value
            if (value == true) {
                subtitle_text.show()
            } else {
                subtitle_text.hide()
            }
        }

    var card_alert: Boolean? = false
        set(value) {
            field = value
            if (value == true) {
                title_text.setTextColor(ContextCompat.getColor(context, R.color.exposure_notification_red))
            } else {
                title_text.setTextColor(ContextCompat.getColor(context, R.color.textColorPrimary))
            }
        }

    open var card_icon: Drawable? = null
        set(value) {
            field = value
            value?.let {
                title_text.setCompoundDrawablesWithIntrinsicBounds(it, null, if (card_show_right_arrow == true) ContextCompat.getDrawable(context, R.drawable.ic_arrow_right) else null , null)
            }
        }
}






================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/DashboardFragment.kt
================================================
package cz.covid19cz.erouska.ui.dashboard

import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.location.LocationManager
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.observe
import com.tbruyelle.rxpermissions2.RxPermissions
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.BuildConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentDashboardPlusBinding
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsErrorHandling
import cz.covid19cz.erouska.exposurenotifications.Notifications
import cz.covid19cz.erouska.ext.*
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.dashboard.event.DashboardCommandEvent
import cz.covid19cz.erouska.ui.dashboard.event.GmsApiErrorEvent
import cz.covid19cz.erouska.ui.exposure.event.ExposuresCommandEvent
import cz.covid19cz.erouska.ui.main.MainVM
import cz.covid19cz.erouska.utils.Analytics
import cz.covid19cz.erouska.utils.Analytics.KEY_PAUSE_APP
import cz.covid19cz.erouska.utils.Analytics.KEY_RESUME_APP
import cz.covid19cz.erouska.utils.Analytics.KEY_SHARE_APP
import cz.covid19cz.erouska.utils.showOrHide
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_dashboard_plus.*
import javax.inject.Inject

@AndroidEntryPoint
class DashboardFragment : BaseFragment<FragmentDashboardPlusBinding, DashboardVM>(
    R.layout.fragment_dashboard_plus,
    DashboardVM::class
) {

    companion object {
        private const val SCREEN_NAME = "Dashboard"
    }

    private val mainViewModel: MainVM by activityViewModels()

    @Inject
    lateinit var notifications: Notifications

    @Inject
    internal lateinit var exposureNotificationsErrorHandling: ExposureNotificationsErrorHandling

    private lateinit var rxPermissions: RxPermissions
    private var demoMode = false

    private val btAndLocationReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            context?.let {
                viewModel.checkStatus()
                refreshDotIndicator(context)
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        activity?.setTitle(R.string.app_name)
        rxPermissions = RxPermissions(this)
        subscribeToViewModel()

        viewModel.exposureNotificationsEnabled.observe(this, Observer { isEnabled ->
            refreshDotIndicator(requireContext())
            if (isEnabled) {
                notifications.dismissNotRunningNotification()
            }
            checkAppActive()
        })

        viewModel.bluetoothState.observe(
            this,
            Observer { isEnabled -> onBluetoothStateChanged(isEnabled) })

        viewModel.locationState.observe(
            this,
            Observer { isEnabled -> onLocationStateChanged(isEnabled) })

        viewModel.lastUpdateTimestamp.observe(this, { updateLastUpdateDateAndTime() })
        viewModel.lastExposureDate.observe(this, { updateLastUpdateDateAndTime() })
    }

    override fun onStart() {
        super.onStart()

        val intentFilter = IntentFilter().apply {
            addAction(BluetoothAdapter.ACTION_STATE_CHANGED)

            // don't register Location Receiver on devices with Android 11+ (it's not mandatory to have location services turned on)
            // https://developer.android.com/about/versions/11/behavior-changes-all#exposure-notifications
            // https://developers.google.com/android/exposure-notifications/implementation-guide#locationless_scanning_in_android_11
            if (!viewModel.isLocationlessScanSupported()) {
                addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
            }
        }

        context?.registerReceiver(
            btAndLocationReceiver,
            intentFilter
        )
    }

    override fun onStop() {
        context?.unregisterReceiver(btAndLocationReceiver)
        super.onStop()
    }

    private fun refreshDotIndicator(context: Context) {
        mainViewModel.serviceRunning.value =
            viewModel.exposureNotificationsEnabled.value &&
                    context.isBtEnabled() &&
                    (viewModel.isLocationlessScanSupported() || context.isLocationEnabled())
    }

    private fun subscribeToViewModel() {
        subscribe(DashboardCommandEvent::class) { commandEvent ->
            when (commandEvent.command) {
                DashboardCommandEvent.Command.DATA_UP_TO_DATE -> {
                    notifications.dismissOudatedDataNotification()
                    showOrHideDataNotification(false)
                }
                DashboardCommandEvent.Command.SHOW_HOW_IT_WORKS -> updateOnboardingNotif(true)
                DashboardCommandEvent.Command.HIDE_HOW_IT_WORKS -> updateOnboardingNotif(false)
                DashboardCommandEvent.Command.DATA_OBSOLETE -> showOrHideDataNotification(true)
                DashboardCommandEvent.Command.RECENT_EXPOSURE -> showOrHideExposureNotification(true)
                DashboardCommandEvent.Command.NOT_ACTIVATED -> showWelcomeScreen()
                DashboardCommandEvent.Command.EFGS -> showEfgs()
                DashboardCommandEvent.Command.TURN_OFF -> notifications.showErouskaPausedNotification()
            }
        }
        subscribe(GmsApiErrorEvent::class) {
            exposureNotificationsErrorHandling.handle(it, this, SCREEN_NAME)
        }

        subscribe(ExposuresCommandEvent::class) {
            when (it.command) {
                ExposuresCommandEvent.Command.RECENT_EXPOSURE -> onRecentExposureDiscovered()
                ExposuresCommandEvent.Command.NO_RECENT_EXPOSURES -> onNoExposureDiscovered()
            }
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        if (viewModel.shouldIntroduceEFGS()) {
            navigate(DashboardFragmentDirections.actionNavDashboardToNavEfgsUpdate(fullscreen = true))
        }

        exposure_notification_content.text = AppConfig.encounterWarning
        exposure_notification_close.setOnClickListener {
            viewModel.acceptExposure()
            showOrHideExposureNotification(false)
        }
        exposure_notification_more_info.setOnClickListener { viewModel.showExposureDetail() }

        data_notification_close.setOnClickListener { showOrHideDataNotification(false) }
        how_it_works_more.setOnClickListener { viewModel.showHowItWorksPage() }
        how_it_works_close.setOnClickListener { viewModel.dismissHowItWorksNotification() }

        enableUpInToolbar(false)

        data_notification_content.text = AppConfig.recentExposureNotificationTitle

        dash_card_no_risky_encounter.card_title = AppConfig.noEncounterCardTitle

        dash_bluetooth_off.card_on_content_click = View.OnClickListener { requestEnableBt() }
        dash_location_off.card_on_content_click = View.OnClickListener { requestLocationEnable() }

        dash_card_risky_encounter.card_on_content_click =
            View.OnClickListener { viewModel.showExposureDetail() }
        dash_card_no_risky_encounter.card_on_content_click =
            View.OnClickListener { viewModel.showExposureDetail() }

        dash_card_positive_test.card_on_content_click =
            View.OnClickListener { viewModel.sendData() }

        dash_travel.card_on_content_click =
            View.OnClickListener { viewModel.showEfgs() }

        exposure_notification_content.text = AppConfig.encounterWarning
        exposure_notification_more_info.setOnClickListener { viewModel.showExposureDetail() }
        exposure_notification_close.setOnClickListener {
            viewModel.acceptExposure()
            exposure_notification_container.hide()
        }

        dash_card_active.setOnClickListener {
            viewModel.stop()
            Analytics.logEvent(requireContext(), KEY_PAUSE_APP)
        }
        dash_card_inactive.setOnClickListener {
            viewModel.start()
            Analytics.logEvent(requireContext(), KEY_RESUME_APP)
        }

        updateLastUpdateDateAndTime()
        viewModel.cancelSuppression()
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.dashboard, menu)
        if (BuildConfig.FLAVOR == "dev") {
            menu.add(0, R.id.action_activation, 11, "Test Aktivace")
            menu.add(0, R.id.action_exposure_demo, 12, "Test Riz. Notifikace")
            menu.add(0, R.id.action_play_services, 13, "Test PlayServices")
            menu.add(0, R.id.action_sandbox, 14, "Test Sandbox")
            menu.add(0, R.id.action_efgs, 15, "Test EFGS")
            menu.add(0, R.id.action_exposure_screen, 17, "Test Exposure screen")
            menu.add(0, R.id.action_exposure_info, 18, "Test Rizikové setkání")
            menu.add(0, R.id.action_efgs_control, 19, "Test EFGS Control")
        }
        super.onCreateOptionsMenu(menu, inflater)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.menu_share -> {
                requireContext().shareApp()
                Analytics.logEvent(requireContext(), KEY_SHARE_APP)
                true
            }
            R.id.nav_about -> {
                navigate(R.id.nav_about)
                true
            }
            R.id.action_sandbox -> {
                navigate(R.id.nav_sandbox)
                true
            }
            R.id.action_efgs -> {
                navigate(DashboardFragmentDirections.actionNavDashboardToNavEfgsUpdate(fullscreen = true))
                true
            }
            R.id.action_activation -> {
                viewModel.unregister()
                showWelcomeScreen()
                true
            }
            R.id.action_exposure_demo -> {
                demoMode = true
                showOrHideExposureNotification(true)
                true
            }
            R.id.action_exposure_screen -> {
                navigate(DashboardFragmentDirections.actionNavDashboardToNavExposure(demo = true))
                true
            }
            R.id.action_play_services -> {
                showPlayServicesUpdate()
                true
            }
            R.id.action_efgs_control -> {
                navigate(DashboardFragmentDirections.actionNavDashboardToNavEfgs())
                true
            }
            R.id.action_exposure_info -> {
                navigate(DashboardFragmentDirections.actionNavDashboardToNavExposureInfo(demo = true))
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when (requestCode) {
            ExposureNotificationsErrorHandling.REQUEST_GMS_ERROR_RESOLUTION -> {
                if (resultCode == Activity.RESULT_OK) {
                    viewModel.start()
                }
            }
        }
    }

    private fun updateLastUpdateDateAndTime() {
        val lastUpdateTimestamp = viewModel.lastUpdateTimestamp.value
        val lastUpdateString = lastUpdateTimestamp?.let {
            if (it != 0L) {
                resources.getString(
                    R.string.dashboard_body_no_contact,
                    it.timestampToDate(), it.timestampToTime()
                )
            } else null
        } ?: resources.getString(R.string.dashboard_loading_data)

        val text = lastUpdateString + "\n${AppConfig.encounterUpdateFrequency}"
        dash_card_no_risky_encounter.card_subtitle = text
        updateLastUpdateOnExposureCard(lastUpdateString)
    }

    private fun updateLastUpdateOnExposureCard(lastUpdateString: String?) {
        val lastExposureString = resources.getString(
            R.string.dashboard_risky_encounter_subtitle_bad,
            viewModel.lastExposureDate.value
        )

        val text = lastUpdateString?.let {
            "${lastExposureString}\n\n${lastUpdateString}"
        } ?: lastExposureString

        dash_card_risky_encounter.card_subtitle = text
    }

    private fun onRecentExposureDiscovered() {
        dash_card_no_risky_encounter.hide()
        dash_card_risky_encounter.show()
    }

    private fun onNoExposureDiscovered() {
        dash_card_no_risky_encounter.show()
        dash_card_risky_encounter.hide()
    }

    private fun onBluetoothStateChanged(isEnabled: Boolean) {
        dash_bluetooth_off.showOrHide(!isEnabled)
        checkAppActive()
    }

    private fun onLocationStateChanged(isEnabled: Boolean) {
        // Location services don't need to be turned on on devices with Android 11+
        if (viewModel.isLocationlessScanSupported()) {
            dash_location_off.hide()
        } else {
            dash_location_off.showOrHide(!isEnabled)
        }
        checkAppActive()
    }

    private fun checkAppActive() {
        val enEnabled = viewModel.exposureNotificationsEnabled.value
        // Location services don't need to be turned on on devices with 
Download .txt
gitextract_yznxotpf/

├── .github/
│   ├── pull_request_template.txt
│   ├── stale.yml
│   └── workflows/
│       ├── deploy-develop.yml
│       └── deploy-master.yml
├── .gitignore
├── .idea/
│   └── codeStyles/
│       └── codeStyleConfig.xml
├── LICENSE
├── README.md
├── app/
│   ├── build.gradle
│   ├── libs/
│   │   └── play-services-nearby-exposurenotification-1.7.2-eap.aar
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── kotlin/
│       │       └── cz/
│       │           └── covid19cz/
│       │               └── erouska/
│       │                   ├── helpers/
│       │                   │   ├── Actions.kt
│       │                   │   ├── ClickableLink.kt
│       │                   │   └── TextMatchesIgnoringWhitespaceType.kt
│       │                   ├── screens/
│       │                   │   ├── A1Screen.kt
│       │                   │   ├── A2Screen.kt
│       │                   │   ├── A3Screen.kt
│       │                   │   ├── B1Screen.kt
│       │                   │   └── N1Screen.kt
│       │                   ├── testRules/
│       │                   │   └── DisableAnimationsRule.kt
│       │                   └── tests/
│       │                       └── ActivationTest.kt
│       ├── dev/
│       │   ├── google-services.json
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── ic_launcher_background.xml
│       │       │   └── ic_launcher_foreground.xml
│       │       └── values/
│       │           ├── controls.xml
│       │           └── strings.xml
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── kotlin/
│       │   │   └── cz/
│       │   │       └── covid19cz/
│       │   │           └── erouska/
│       │   │               ├── App.kt
│       │   │               ├── AppConfig.kt
│       │   │               ├── DI.kt
│       │   │               ├── db/
│       │   │               │   ├── DailySummariesDb.kt
│       │   │               │   ├── DailySummaryDao.kt
│       │   │               │   ├── DailySummaryEntity.kt
│       │   │               │   └── SharedPrefsRepository.kt
│       │   │               ├── exposurenotifications/
│       │   │               │   ├── ExposureCryptoTools.kt
│       │   │               │   ├── ExposureNotificationsErrorHandling.kt
│       │   │               │   ├── ExposureNotificationsRepository.kt
│       │   │               │   ├── Notifications.kt
│       │   │               │   ├── receiver/
│       │   │               │   │   └── ExposureNotificationBroadcastReceiver.kt
│       │   │               │   ├── service/
│       │   │               │   │   └── PushService.kt
│       │   │               │   └── worker/
│       │   │               │       ├── DownloadKeysWorker.kt
│       │   │               │       └── SelfCheckerWorker.kt
│       │   │               ├── ext/
│       │   │               │   ├── ByteArray.kt
│       │   │               │   ├── Context.kt
│       │   │               │   ├── Int.kt
│       │   │               │   ├── Long.kt
│       │   │               │   ├── Rx.kt
│       │   │               │   ├── String.kt
│       │   │               │   └── View.kt
│       │   │               ├── net/
│       │   │               │   ├── ExposureServerRepository.kt
│       │   │               │   ├── FirebaseFunctionsRepository.kt
│       │   │               │   ├── api/
│       │   │               │   │   ├── KeyServerApi.kt
│       │   │               │   │   └── VerificationServerApi.kt
│       │   │               │   ├── exception/
│       │   │               │   │   └── UnauthrorizedException.kt
│       │   │               │   └── model/
│       │   │               │       ├── CovidDataModel.kt
│       │   │               │       ├── DownloadedKeys.kt
│       │   │               │       ├── KeyServerModel.kt
│       │   │               │       └── VerificationServerModel.kt
│       │   │               ├── ui/
│       │   │               │   ├── about/
│       │   │               │   │   ├── AboutFragment.kt
│       │   │               │   │   ├── AboutVM.kt
│       │   │               │   │   └── entity/
│       │   │               │   │       └── AboutProfileItem.kt
│       │   │               │   ├── activation/
│       │   │               │   │   ├── ActivationFragment.kt
│       │   │               │   │   ├── ActivationNotificationsFragment.kt
│       │   │               │   │   ├── ActivationNotificationsVM.kt
│       │   │               │   │   ├── ActivationState.kt
│       │   │               │   │   └── ActivationVM.kt
│       │   │               │   ├── base/
│       │   │               │   │   ├── BaseActivity.kt
│       │   │               │   │   ├── BaseFragment.kt
│       │   │               │   │   ├── BaseVM.kt
│       │   │               │   │   └── UrlEvent.kt
│       │   │               │   ├── contacts/
│       │   │               │   │   ├── Contact.kt
│       │   │               │   │   ├── ContactsFragment.kt
│       │   │               │   │   ├── ContactsVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── ContactsCommandEvent.kt
│       │   │               │   ├── dashboard/
│       │   │               │   │   ├── DashboardCardView.kt
│       │   │               │   │   ├── DashboardFragment.kt
│       │   │               │   │   ├── DashboardVM.kt
│       │   │               │   │   ├── TravellerDashboardCardView.kt
│       │   │               │   │   └── event/
│       │   │               │   │       ├── DashboardCommandEvent.kt
│       │   │               │   │       ├── DisabledEvent.kt
│       │   │               │   │       └── GmsApiErrorEvent.kt
│       │   │               │   ├── efgs/
│       │   │               │   │   ├── EfgsFragment.kt
│       │   │               │   │   ├── EfgsVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── EfgsCommandEvent.kt
│       │   │               │   ├── efgsagreement/
│       │   │               │   │   ├── EfgsAgreementFragment.kt
│       │   │               │   │   └── EfgsAgreementVM.kt
│       │   │               │   ├── error/
│       │   │               │   │   ├── ErrorFragment.kt
│       │   │               │   │   ├── ErrorVM.kt
│       │   │               │   │   └── entity/
│       │   │               │   │       └── ErrorType.kt
│       │   │               │   ├── exposure/
│       │   │               │   │   ├── ExposureFragment.kt
│       │   │               │   │   ├── ExposureVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── ExposuresEvent.kt
│       │   │               │   ├── exposurehelp/
│       │   │               │   │   ├── ExposureHelpFragment.kt
│       │   │               │   │   ├── ExposureHelpVM.kt
│       │   │               │   │   └── entity/
│       │   │               │   │       ├── ExposureHelpData.kt
│       │   │               │   │       ├── ExposureHelpItem.kt
│       │   │               │   │       ├── ExposureHelpTitle.kt
│       │   │               │   │       └── ExposureHelpType.kt
│       │   │               │   ├── exposureinfo/
│       │   │               │   │   ├── ExposureInfoFragment.kt
│       │   │               │   │   └── ExposureInfoVM.kt
│       │   │               │   ├── help/
│       │   │               │   │   ├── HelpFragment.kt
│       │   │               │   │   ├── HelpVM.kt
│       │   │               │   │   └── data/
│       │   │               │   │       ├── AboutAppCategory.kt
│       │   │               │   │       ├── Category.kt
│       │   │               │   │       ├── FaqCategory.kt
│       │   │               │   │       ├── HowToCategory.kt
│       │   │               │   │       └── Question.kt
│       │   │               │   ├── helpcategory/
│       │   │               │   │   ├── HelpCategoryFragment.kt
│       │   │               │   │   └── HelpCategoryVM.kt
│       │   │               │   ├── helpquestion/
│       │   │               │   │   ├── HelpQuestionFragment.kt
│       │   │               │   │   └── HelpQuestionVM.kt
│       │   │               │   ├── helpsearch/
│       │   │               │   │   ├── HelpSearchFragment.kt
│       │   │               │   │   ├── HelpSearchVM.kt
│       │   │               │   │   ├── data/
│       │   │               │   │   │   └── SearchableQuestion.kt
│       │   │               │   │   └── ui/
│       │   │               │   │       └── SearchableItem.kt
│       │   │               │   ├── how/
│       │   │               │   │   ├── HowItWorksFragment.kt
│       │   │               │   │   ├── HowItWorksVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── HowItWorksEvent.kt
│       │   │               │   ├── main/
│       │   │               │   │   ├── MainActivity.kt
│       │   │               │   │   ├── MainActivityOld.kt
│       │   │               │   │   └── MainVM.kt
│       │   │               │   ├── mydata/
│       │   │               │   │   ├── CaseItemView.kt
│       │   │               │   │   ├── MyDataFragment.kt
│       │   │               │   │   └── MyDataVM.kt
│       │   │               │   ├── noverificationcode/
│       │   │               │   │   ├── NoVerificationCodeFragment.kt
│       │   │               │   │   ├── NoVerificationCodeVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── NoVerificationCodeEvent.kt
│       │   │               │   ├── permissions/
│       │   │               │   │   └── BasePermissionsFragment.kt
│       │   │               │   ├── publishsuccess/
│       │   │               │   │   ├── PublishSuccessFragment.kt
│       │   │               │   │   └── PublishSuccessVM.kt
│       │   │               │   ├── ragnarok/
│       │   │               │   │   └── RagnarokVM.kt
│       │   │               │   ├── recentexposures/
│       │   │               │   │   ├── RecentExposuresFragment.kt
│       │   │               │   │   ├── RecentExposuresVM.kt
│       │   │               │   │   └── entity/
│       │   │               │   │       └── RecentExposureGroupHeaderItem.kt
│       │   │               │   ├── sandbox/
│       │   │               │   │   ├── SandboxConfigFragment.kt
│       │   │               │   │   ├── SandboxConfigVM.kt
│       │   │               │   │   ├── SandboxConfigValues.kt
│       │   │               │   │   ├── SandboxDataFragment.kt
│       │   │               │   │   ├── SandboxDataVM.kt
│       │   │               │   │   ├── SandboxFragment.kt
│       │   │               │   │   ├── SandboxVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── SnackbarEvent.kt
│       │   │               │   ├── symptomdate/
│       │   │               │   │   ├── SymptomDateFragment.kt
│       │   │               │   │   ├── SymptomDateVM.kt
│       │   │               │   │   └── event/
│       │   │               │   │       ├── DatePickerEvent.kt
│       │   │               │   │       └── SymptomDateCommandEvent.kt
│       │   │               │   ├── traveller/
│       │   │               │   │   ├── TravellerFragment.kt
│       │   │               │   │   └── TravellerVM.kt
│       │   │               │   ├── update/
│       │   │               │   │   ├── efgs/
│       │   │               │   │   │   ├── EfgsUpdateFragment.kt
│       │   │               │   │   │   └── EfgsUpdateVM.kt
│       │   │               │   │   └── playservices/
│       │   │               │   │       ├── UpdatePlayServicesFragment.kt
│       │   │               │   │       ├── UpdatePlayServicesVM.kt
│       │   │               │   │       └── event/
│       │   │               │   │           └── UpdatePlayServicesEvent.kt
│       │   │               │   ├── verification/
│       │   │               │   │   ├── InvalidTokenException.kt
│       │   │               │   │   ├── NoKeysException.kt
│       │   │               │   │   ├── ReportExposureException.kt
│       │   │               │   │   ├── VerificationFragment.kt
│       │   │               │   │   ├── VerificationVM.kt
│       │   │               │   │   ├── VerifyException.kt
│       │   │               │   │   └── event/
│       │   │               │   │       └── VerificationCommandEvent.kt
│       │   │               │   └── welcome/
│       │   │               │       ├── WelcomeFragment.kt
│       │   │               │       ├── WelcomeVM.kt
│       │   │               │       └── event/
│       │   │               │           └── WelcomeCommandEvent.kt
│       │   │               └── utils/
│       │   │                   ├── Analytics.kt
│       │   │                   ├── CustomTabHelper.kt
│       │   │                   ├── DeviceInfo.kt
│       │   │                   ├── L.kt
│       │   │                   ├── LocaleUtils.kt
│       │   │                   ├── Markdown.kt
│       │   │                   ├── MiscUtils.kt
│       │   │                   ├── SharedPrefsLiveData.kt
│       │   │                   ├── SupportEmailGenerator.kt
│       │   │                   ├── Text.kt
│       │   │                   └── ViewUtils.kt
│       │   └── res/
│       │       ├── drawable/
│       │       │   ├── highlight_selector.xml
│       │       │   ├── ic_about.xml
│       │       │   ├── ic_ack_case.xml
│       │       │   ├── ic_act_case.xml
│       │       │   ├── ic_action_close.xml
│       │       │   ├── ic_action_up.xml
│       │       │   ├── ic_active.xml
│       │       │   ├── ic_antigen.xml
│       │       │   ├── ic_arrow_right.xml
│       │       │   ├── ic_balloon.xml
│       │       │   ├── ic_bluetooth_onboard.xml
│       │       │   ├── ic_calendar.xml
│       │       │   ├── ic_chat.xml
│       │       │   ├── ic_confirm.xml
│       │       │   ├── ic_contacts.xml
│       │       │   ├── ic_control.xml
│       │       │   ├── ic_cured.xml
│       │       │   ├── ic_data.xml
│       │       │   ├── ic_death_toll.xml
│       │       │   ├── ic_encounter.xml
│       │       │   ├── ic_error.xml
│       │       │   ├── ic_eval.xml
│       │       │   ├── ic_exposure_info.xml
│       │       │   ├── ic_face_mask.xml
│       │       │   ├── ic_google_play_services.xml
│       │       │   ├── ic_help.xml
│       │       │   ├── ic_home.xml
│       │       │   ├── ic_hospitalized.xml
│       │       │   ├── ic_how_it_works_banner.xml
│       │       │   ├── ic_injection_complete.xml
│       │       │   ├── ic_injection_first.xml
│       │       │   ├── ic_item_empty.xml
│       │       │   ├── ic_launcher_background.xml
│       │       │   ├── ic_launcher_foreground.xml
│       │       │   ├── ic_mask.xml
│       │       │   ├── ic_mzcr.xml
│       │       │   ├── ic_no_risky_encounter.xml
│       │       │   ├── ic_notif.xml
│       │       │   ├── ic_notifications_sent.xml
│       │       │   ├── ic_notifications_shown.xml
│       │       │   ├── ic_off_bluetooth.xml
│       │       │   ├── ic_off_location.xml
│       │       │   ├── ic_pause.xml
│       │       │   ├── ic_positive.xml
│       │       │   ├── ic_prevention.xml
│       │       │   ├── ic_privacy.xml
│       │       │   ├── ic_restriction.xml
│       │       │   ├── ic_risky_encounter.xml
│       │       │   ├── ic_shortcut_resume.xml
│       │       │   ├── ic_splashscreen_hands.xml
│       │       │   ├── ic_splashscreen_logo.xml
│       │       │   ├── ic_symptoms.xml
│       │       │   ├── ic_test.xml
│       │       │   ├── ic_travel.xml
│       │       │   ├── ic_update_expansion.xml
│       │       │   ├── ic_vacc.xml
│       │       │   ├── ic_warn.xml
│       │       │   └── launchscreen.xml
│       │       ├── drawable-anydpi-v24/
│       │       │   └── ic_notification_normal.xml
│       │       ├── drawable-night/
│       │       │   └── ic_splashscreen_hands.xml
│       │       ├── layout/
│       │       │   ├── activity_main.xml
│       │       │   ├── activity_ragnarok.xml
│       │       │   ├── dashboard_card_view.xml
│       │       │   ├── fragment_about.xml
│       │       │   ├── fragment_activation.xml
│       │       │   ├── fragment_activation_notifications.xml
│       │       │   ├── fragment_contacts.xml
│       │       │   ├── fragment_dashboard_plus.xml
│       │       │   ├── fragment_efgs.xml
│       │       │   ├── fragment_efgs_agreement.xml
│       │       │   ├── fragment_efgs_update.xml
│       │       │   ├── fragment_error.xml
│       │       │   ├── fragment_exposure.xml
│       │       │   ├── fragment_exposure_help.xml
│       │       │   ├── fragment_exposure_info.xml
│       │       │   ├── fragment_help.xml
│       │       │   ├── fragment_help_category.xml
│       │       │   ├── fragment_help_question.xml
│       │       │   ├── fragment_help_search.xml
│       │       │   ├── fragment_how_it_works.xml
│       │       │   ├── fragment_my_data.xml
│       │       │   ├── fragment_no_verification_code.xml
│       │       │   ├── fragment_play_services_update.xml
│       │       │   ├── fragment_publish_success.xml
│       │       │   ├── fragment_recent_exposures.xml
│       │       │   ├── fragment_sandbox.xml
│       │       │   ├── fragment_sandbox_config.xml
│       │       │   ├── fragment_sandbox_data.xml
│       │       │   ├── fragment_symptom_date.xml
│       │       │   ├── fragment_traveller.xml
│       │       │   ├── fragment_verification.xml
│       │       │   ├── fragment_welcome.xml
│       │       │   ├── item_contacts.xml
│       │       │   ├── item_daily_summary.xml
│       │       │   ├── item_exposure_help.xml
│       │       │   ├── item_exposure_help_title.xml
│       │       │   ├── item_exposure_window.xml
│       │       │   ├── item_help_about_category.xml
│       │       │   ├── item_help_faq_category.xml
│       │       │   ├── item_help_how_category.xml
│       │       │   ├── item_help_question.xml
│       │       │   ├── item_recent_exposure.xml
│       │       │   ├── item_recent_exposure_group_header.xml
│       │       │   ├── item_scan_instance.xml
│       │       │   ├── item_scan_instance_header.xml
│       │       │   ├── item_search.xml
│       │       │   ├── item_search_layout.xml
│       │       │   ├── item_tek.xml
│       │       │   ├── layout_sandbox_config_values.xml
│       │       │   ├── search_toolbar.xml
│       │       │   ├── traveller_dashboard_card_view.xml
│       │       │   └── view_data_item.xml
│       │       ├── menu/
│       │       │   ├── bottom_nav.xml
│       │       │   ├── dashboard.xml
│       │       │   ├── exposure.xml
│       │       │   └── onboarding.xml
│       │       ├── mipmap-anydpi-v26/
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── navigation/
│       │       │   └── nav_graph.xml
│       │       ├── values/
│       │       │   ├── colors.xml
│       │       │   ├── controls.xml
│       │       │   ├── dimens.xml
│       │       │   ├── ids.xml
│       │       │   ├── strings-notranslate.xml
│       │       │   ├── strings.xml
│       │       │   ├── styles.xml
│       │       │   └── themes.xml
│       │       ├── values-cs/
│       │       │   └── strings.xml
│       │       ├── values-night/
│       │       │   └── colors.xml
│       │       ├── values-sk/
│       │       │   └── strings.xml
│       │       ├── xml/
│       │       │   ├── file_paths.xml
│       │       │   └── remote_config_defaults.xml
│       │       ├── xml-cs/
│       │       │   └── remote_config_defaults.xml
│       │       └── xml-sk/
│       │           └── remote_config_defaults.xml
│       ├── prod/
│       │   ├── google-services.json
│       │   └── res/
│       │       └── values/
│       │           └── controls.xml
│       └── test/
│           └── kotlin/
│               └── com/
│                   └── covid19cz/
│                       └── bt_tracing/
│                           └── ExampleUnitTest.kt
├── arch/
│   ├── build.gradle
│   ├── gradle.properties
│   ├── proguard-rules.pro
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           ├── java/
│           │   └── arch/
│           │       ├── BaseApp.kt
│           │       ├── adapter/
│           │       │   ├── BaseRecyclerAdapter.kt
│           │       │   ├── RecyclerLayoutStrategy.kt
│           │       │   ├── SingleTypeRecyclerAdapter.kt
│           │       │   └── StrategyRecyclerAdapter.kt
│           │       ├── binding/
│           │       │   ├── EditTextBindings.kt
│           │       │   ├── ImageViewBindings.kt
│           │       │   ├── ProgressbarBindings.kt
│           │       │   ├── RecyclerViewBindings.kt
│           │       │   ├── TextViewBindings.kt
│           │       │   ├── ViewBindings.kt
│           │       │   └── WebViewBindings.kt
│           │       ├── event/
│           │       │   ├── LiveEvent.kt
│           │       │   ├── LiveEventMap.kt
│           │       │   ├── NavigationEvent.kt
│           │       │   ├── NavigationGraphEvent.kt
│           │       │   └── SingleLiveEvent.java
│           │       ├── extensions/
│           │       │   └── navExtensions.kt
│           │       ├── livedata/
│           │       │   └── SafeMutableLiveData.kt
│           │       ├── utils/
│           │       │   └── NullableUtils.kt
│           │       ├── view/
│           │       │   ├── BaseArchActivity.kt
│           │       │   ├── BaseArchDialogFragment.kt
│           │       │   ├── BaseArchFragment.kt
│           │       │   └── BaseDialogFragment.kt
│           │       └── viewmodel/
│           │           ├── BaseArchViewModel.kt
│           │           └── BaseDialogViewModel.kt
│           └── res/
│               ├── layout/
│               │   ├── base_dialog.xml
│               │   ├── base_view.xml
│               │   └── item_recycler.xml
│               └── values/
│                   ├── ids.xml
│                   └── strings.xml
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── meta/
│   └── debug.keystore
├── release.sh
└── settings.gradle
Download .txt
SYMBOL INDEX (4 symbols across 1 files)

FILE: arch/src/main/java/arch/event/SingleLiveEvent.java
  class SingleLiveEvent (line 23) | public class SingleLiveEvent<T> extends MutableLiveData<T> {
    method observe (line 29) | @MainThread
    method setValue (line 48) | @MainThread
    method call (line 57) | @MainThread
Condensed preview — 355 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,460K chars).
[
  {
    "path": ".github/pull_request_template.txt",
    "chars": 865,
    "preview": "# Description\n\n<# Please include a summary of the change and which issue is fixed. Please also include relevant motivati"
  },
  {
    "path": ".github/stale.yml",
    "chars": 681,
    "preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a "
  },
  {
    "path": ".github/workflows/deploy-develop.yml",
    "chars": 1480,
    "preview": "name: Deploy production app\n\non:\n  push:\n    branches:\n    - develop\n\njobs:\n  build:\n\n    runs-on: ubuntu-18.04\n\n    ste"
  },
  {
    "path": ".github/workflows/deploy-master.yml",
    "chars": 1500,
    "preview": "name: Deploy production app\n\non:\n  push:\n    branches:\n    - master\n\njobs:\n  build:\n\n    runs-on: ubuntu-18.04\n\n    step"
  },
  {
    "path": ".gitignore",
    "chars": 2405,
    "preview": "# Created by https://www.gitignore.io/api/intellij,android,gradle,windows,osx\n\n### Intellij ###\n# Covers JetBrains IDEs:"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "chars": 209,
    "preview": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n "
  },
  {
    "path": "LICENSE",
    "chars": 1097,
    "preview": "MIT License\n\nCopyright (c) 2020 Ministry of Health of the Czech Republic\n\nPermission is hereby granted, free of charge, "
  },
  {
    "path": "README.md",
    "chars": 3793,
    "preview": "# erouska-android\n\n[<img src=\"https://lh3.googleusercontent.com/cjsqrWQKJQp9RFO7-hJ9AfpKzbUb_Y84vXfjlP0iRHBvladwAfXih984"
  },
  {
    "path": "app/build.gradle",
    "chars": 7552,
    "preview": "apply plugin: 'com.android.application'\napply plugin: 'dagger.hilt.android.plugin'\napply plugin: 'kotlin-android'\napply "
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 564,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/helpers/Actions.kt",
    "chars": 3364,
    "preview": "package cz.covid19cz.erouska.helpers\n\nimport android.app.Instrumentation\nimport android.content.Intent\nimport android.vi"
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/helpers/ClickableLink.kt",
    "chars": 101,
    "preview": "package cz.covid19cz.erouska.helpers\n\nclass ClickableLink(var url: String, var clickableText: String)"
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/helpers/TextMatchesIgnoringWhitespaceType.kt",
    "chars": 777,
    "preview": "package cz.covid19cz.erouska.helpers\n\nimport org.hamcrest.Description\nimport org.hamcrest.TypeSafeMatcher\n\nclass TextMat"
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/A1Screen.kt",
    "chars": 584,
    "preview": "package cz.covid19cz.erouska.screens\n\nimport cz.covid19cz.erouska.R\nimport cz.covid19cz.erouska.helpers.checkDisplayed\ni"
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/A2Screen.kt",
    "chars": 1075,
    "preview": "package cz.covid19cz.erouska.screens\n\nimport android.bluetooth.BluetoothAdapter\nimport cz.covid19cz.erouska.R\nimport cz."
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/A3Screen.kt",
    "chars": 803,
    "preview": "package cz.covid19cz.erouska.screens\n\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport cz.covid19cz.erou"
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/B1Screen.kt",
    "chars": 643,
    "preview": "package cz.covid19cz.erouska.screens\n\nimport cz.covid19cz.erouska.R\nimport cz.covid19cz.erouska.helpers.RETRY_TIMEOUT\nim"
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/screens/N1Screen.kt",
    "chars": 2856,
    "preview": "package cz.covid19cz.erouska.screens\n\nimport androidx.test.espresso.matcher.ViewMatchers.withId\nimport cz.covid19cz.erou"
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/testRules/DisableAnimationsRule.kt",
    "chars": 2170,
    "preview": "package cz.covid19cz.erouska.testRules\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.u"
  },
  {
    "path": "app/src/androidTest/kotlin/cz/covid19cz/erouska/tests/ActivationTest.kt",
    "chars": 1199,
    "preview": "package cz.covid19cz.erouska.tests\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4\nimport androidx.test.rule.Activ"
  },
  {
    "path": "app/src/dev/google-services.json",
    "chars": 1349,
    "preview": "{\n  \"project_info\": {\n    \"project_number\": \"382369682317\",\n    \"firebase_url\": \"https://erouska-key-server-dev.firebase"
  },
  {
    "path": "app/src/dev/res/drawable/ic_launcher_background.xml",
    "chars": 3414,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n"
  },
  {
    "path": "app/src/dev/res/drawable/ic_launcher_foreground.xml",
    "chars": 1356,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n"
  },
  {
    "path": "app/src/dev/res/values/controls.xml",
    "chars": 556,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"uri_scheme\">erouska-dev</string>\n    <string name=\""
  },
  {
    "path": "app/src/dev/res/values/strings.xml",
    "chars": 112,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\">eRouška DEV</string>\n</resources>"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 3635,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:to"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/App.kt",
    "chars": 1464,
    "preview": "package cz.covid19cz.erouska\n\nimport androidx.hilt.work.HiltWorkerFactory\nimport androidx.work.Configuration\nimport andr"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/AppConfig.kt",
    "chars": 7729,
    "preview": "package cz.covid19cz.erouska\n\nimport com.google.firebase.remoteconfig.FirebaseRemoteConfig\nimport com.google.firebase.re"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/DI.kt",
    "chars": 1036,
    "preview": "package cz.covid19cz.erouska\n\nimport android.content.Context\nimport androidx.room.Room\nimport com.google.android.gms.nea"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/db/DailySummariesDb.kt",
    "chars": 274,
    "preview": "package cz.covid19cz.erouska.db\n\nimport androidx.room.Database\nimport androidx.room.RoomDatabase\n\n@Database(version = 1,"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/db/DailySummaryDao.kt",
    "chars": 1390,
    "preview": "package cz.covid19cz.erouska.db\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStr"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/db/DailySummaryEntity.kt",
    "chars": 979,
    "preview": "package cz.covid19cz.erouska.db\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.Primar"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/db/SharedPrefsRepository.kt",
    "chars": 18056,
    "preview": "package cz.covid19cz.erouska.db\n\nimport android.content.Context\nimport android.content.Context.MODE_PRIVATE\nimport andro"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/ExposureCryptoTools.kt",
    "chars": 1853,
    "preview": "package cz.covid19cz.erouska.exposurenotifications\n\nimport android.util.Base64\nimport com.google.android.gms.nearby.expo"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/ExposureNotificationsErrorHandling.kt",
    "chars": 5677,
    "preview": "package cz.covid19cz.erouska.exposurenotifications\n\nimport android.content.Context\nimport androidx.appcompat.app.AlertDi"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/ExposureNotificationsRepository.kt",
    "chars": 16579,
    "preview": "package cz.covid19cz.erouska.exposurenotifications\n\nimport android.content.Context\nimport androidx.work.Constraints\nimpo"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/Notifications.kt",
    "chars": 8672,
    "preview": "package cz.covid19cz.erouska.exposurenotifications\n\nimport android.app.Notification\nimport android.app.NotificationChann"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/receiver/ExposureNotificationBroadcastReceiver.kt",
    "chars": 1770,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/service/PushService.kt",
    "chars": 1503,
    "preview": "package cz.covid19cz.erouska.exposurenotifications.service\n\nimport com.google.firebase.auth.FirebaseAuth\nimport com.goog"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/worker/DownloadKeysWorker.kt",
    "chars": 2048,
    "preview": "package cz.covid19cz.erouska.exposurenotifications.worker\n\nimport android.content.Context\nimport androidx.hilt.work.Hilt"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/exposurenotifications/worker/SelfCheckerWorker.kt",
    "chars": 1444,
    "preview": "package cz.covid19cz.erouska.exposurenotifications.worker\n\nimport android.content.Context\nimport androidx.hilt.work.Hilt"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ext/ByteArray.kt",
    "chars": 268,
    "preview": "package cz.covid19cz.erouska.ext\n\nval ByteArray.asHexLower inline get() = this.joinToString(separator = \"\"){ String.form"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ext/Context.kt",
    "chars": 5125,
    "preview": "package cz.covid19cz.erouska.ext\n\nimport android.app.Activity\nimport android.bluetooth.BluetoothManager\nimport android.c"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ext/Int.kt",
    "chars": 468,
    "preview": "package cz.covid19cz.erouska.ext\n\nimport java.text.SimpleDateFormat\nimport java.util.*\n\nfun Int.daysSinceEpochToDateStri"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ext/Long.kt",
    "chars": 453,
    "preview": "package cz.covid19cz.erouska.ext\n\nimport java.text.SimpleDateFormat\nimport java.util.*\n\nfun Long.timestampToDate(): Stri"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ext/Rx.kt",
    "chars": 1453,
    "preview": "package cz.covid19cz.erouska.ext\n\nimport io.reactivex.Flowable\nimport io.reactivex.Maybe\nimport io.reactivex.Observable\n"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ext/String.kt",
    "chars": 117,
    "preview": "package cz.covid19cz.erouska.ext\n\nfun String.toIntList() : List<Int>{\n    return this.split(\",\").map { it.toInt() }\n}"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ext/View.kt",
    "chars": 1208,
    "preview": "package cz.covid19cz.erouska.ext\n\nimport android.content.Context\nimport android.view.View\nimport android.view.inputmetho"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/net/ExposureServerRepository.kt",
    "chars": 9308,
    "preview": "package cz.covid19cz.erouska.net\n\nimport android.content.Context\nimport androidx.work.*\nimport com.google.gson.Gson\nimpo"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/net/FirebaseFunctionsRepository.kt",
    "chars": 4728,
    "preview": "package cz.covid19cz.erouska.net\n\nimport com.google.firebase.auth.FirebaseAuth\nimport com.google.firebase.functions.ktx."
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/net/api/KeyServerApi.kt",
    "chars": 344,
    "preview": "package cz.covid19cz.erouska.net.api\n\nimport cz.covid19cz.erouska.net.model.ExposureRequest\nimport cz.covid19cz.erouska."
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/net/api/VerificationServerApi.kt",
    "chars": 496,
    "preview": "package cz.covid19cz.erouska.net.api\n\nimport cz.covid19cz.erouska.net.model.*\nimport retrofit2.http.Body\nimport retrofit"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/net/exception/UnauthrorizedException.kt",
    "chars": 86,
    "preview": "package cz.covid19cz.erouska.net.exception\n\nclass UnauthrorizedException : Throwable()"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/net/model/CovidDataModel.kt",
    "chars": 2696,
    "preview": "package cz.covid19cz.erouska.net.model\n\nimport com.google.gson.annotations.SerializedName\n\ndata class CovidStatsResponse"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/net/model/DownloadedKeys.kt",
    "chars": 468,
    "preview": "package cz.covid19cz.erouska.net.model\n\nimport cz.covid19cz.erouska.utils.L\nimport java.io.File\n\ndata class DownloadedKe"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/net/model/KeyServerModel.kt",
    "chars": 968,
    "preview": "package cz.covid19cz.erouska.net.model\n\nimport android.util.Base64\nimport java.util.*\n\ndata class ExposureRequest(\n    v"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/net/model/VerificationServerModel.kt",
    "chars": 926,
    "preview": "package cz.covid19cz.erouska.net.model\n\n// Taken from https://github.com/google/exposure-notifications-verification-serv"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/about/AboutFragment.kt",
    "chars": 1961,
    "preview": "package cz.covid19cz.erouska.ui.about\n\nimport android.os.Bundle\nimport android.view.View\nimport android.widget.Toast\nimp"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/about/AboutVM.kt",
    "chars": 399,
    "preview": "package cz.covid19cz.erouska.ui.about\n\nimport cz.covid19cz.erouska.AppConfig\nimport cz.covid19cz.erouska.ui.base.BaseVM\n"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/about/entity/AboutProfileItem.kt",
    "chars": 172,
    "preview": "package cz.covid19cz.erouska.ui.about.entity\n\nimport com.google.gson.annotations.SerializedName\n\nclass AboutProfileItem("
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationFragment.kt",
    "chars": 5723,
    "preview": "package cz.covid19cz.erouska.ui.activation\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.os."
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationNotificationsFragment.kt",
    "chars": 2411,
    "preview": "package cz.covid19cz.erouska.ui.activation\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.os."
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationNotificationsVM.kt",
    "chars": 1113,
    "preview": "package cz.covid19cz.erouska.ui.activation\n\nimport androidx.lifecycle.viewModelScope\nimport cz.covid19cz.erouska.exposur"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationState.kt",
    "chars": 393,
    "preview": "package cz.covid19cz.erouska.ui.activation\n\nimport arch.event.LiveEvent\n\nsealed class ActivationState\nobject ActivationS"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/activation/ActivationVM.kt",
    "chars": 2332,
    "preview": "package cz.covid19cz.erouska.ui.activation\n\nimport android.content.Context\nimport androidx.lifecycle.LiveData\nimport and"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/base/BaseActivity.kt",
    "chars": 2508,
    "preview": "package cz.covid19cz.erouska.ui.base\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.content.I"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/base/BaseFragment.kt",
    "chars": 4457,
    "preview": "package cz.covid19cz.erouska.ui.base\n\nimport android.app.Activity\nimport android.bluetooth.BluetoothAdapter\nimport andro"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/base/BaseVM.kt",
    "chars": 1474,
    "preview": "package cz.covid19cz.erouska.ui.base\n\nimport arch.viewmodel.BaseArchViewModel\nimport cz.covid19cz.erouska.ext.execute\nim"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/base/UrlEvent.kt",
    "chars": 113,
    "preview": "package cz.covid19cz.erouska.ui.base\n\nimport arch.event.LiveEvent\n\nclass UrlEvent(val url : String) : LiveEvent()"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/contacts/Contact.kt",
    "chars": 156,
    "preview": "package cz.covid19cz.erouska.ui.contacts\n\ndata class Contact(\n    val title: String,\n    val text: String,\n    val linkT"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/contacts/ContactsFragment.kt",
    "chars": 1898,
    "preview": "package cz.covid19cz.erouska.ui.contacts\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.lifecycle.li"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/contacts/ContactsVM.kt",
    "chars": 1129,
    "preview": "package cz.covid19cz.erouska.ui.contacts\n\nimport androidx.databinding.ObservableArrayList\nimport androidx.lifecycle.Life"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/contacts/event/ContactsCommandEvent.kt",
    "chars": 148,
    "preview": "package cz.covid19cz.erouska.ui.contacts.event\n\nsealed class ContactsEvent {\n    data class ContactLinkClicked(val link:"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/DashboardCardView.kt",
    "chars": 3701,
    "preview": "package cz.covid19cz.erouska.ui.dashboard\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimpo"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/DashboardFragment.kt",
    "chars": 15189,
    "preview": "package cz.covid19cz.erouska.ui.dashboard\n\nimport android.app.Activity\nimport android.bluetooth.BluetoothAdapter\nimport "
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/DashboardVM.kt",
    "chars": 8963,
    "preview": "package cz.covid19cz.erouska.ui.dashboard\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.MutableLiveData"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/TravellerDashboardCardView.kt",
    "chars": 838,
    "preview": "package cz.covid19cz.erouska.ui.dashboard\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimpo"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/event/DashboardCommandEvent.kt",
    "chars": 360,
    "preview": "package cz.covid19cz.erouska.ui.dashboard.event\n\nimport arch.event.LiveEvent\n\nclass DashboardCommandEvent(val command: C"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/event/DisabledEvent.kt",
    "chars": 120,
    "preview": "package cz.covid19cz.erouska.ui.dashboard.event\n\nimport arch.event.LiveEvent\n\nclass BluetoothDisabledEvent : LiveEvent()"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/event/GmsApiErrorEvent.kt",
    "chars": 140,
    "preview": "package cz.covid19cz.erouska.ui.dashboard.event\n\nimport arch.event.LiveEvent\n\nclass GmsApiErrorEvent(val throwable: Thro"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/efgs/EfgsFragment.kt",
    "chars": 1678,
    "preview": "package cz.covid19cz.erouska.ui.efgs\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.appcompat.app.Al"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/efgs/EfgsVM.kt",
    "chars": 797,
    "preview": "package cz.covid19cz.erouska.ui.efgs\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.OnLifecycleEvent\nimp"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/efgs/event/EfgsCommandEvent.kt",
    "chars": 202,
    "preview": "package cz.covid19cz.erouska.ui.efgs.event\n\nimport arch.event.LiveEvent\n\nclass EfgsCommandEvent(val command: Command) : "
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/efgsagreement/EfgsAgreementFragment.kt",
    "chars": 2209,
    "preview": "package cz.covid19cz.erouska.ui.efgsagreement\n\nimport android.app.Activity\nimport android.content.Intent\nimport android."
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/efgsagreement/EfgsAgreementVM.kt",
    "chars": 3043,
    "preview": "package cz.covid19cz.erouska.ui.efgsagreement\n\nimport androidx.lifecycle.viewModelScope\nimport arch.livedata.SafeMutable"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/error/ErrorFragment.kt",
    "chars": 4446,
    "preview": "package cz.covid19cz.erouska.ui.error\n\nimport android.os.Bundle\nimport android.view.MenuItem\nimport android.view.View\nim"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/error/ErrorVM.kt",
    "chars": 226,
    "preview": "package cz.covid19cz.erouska.ui.error\n\nimport cz.covid19cz.erouska.ui.base.BaseVM\nimport dagger.hilt.android.lifecycle.H"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/error/entity/ErrorType.kt",
    "chars": 137,
    "preview": "package cz.covid19cz.erouska.ui.error.entity\n\nenum class ErrorType {\n    NO_INTERNET, GENERAL_ERROR, INVALID_CODE, EXPIR"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposure/ExposureFragment.kt",
    "chars": 2952,
    "preview": "package cz.covid19cz.erouska.ui.exposure\n\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.MenuInfl"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposure/ExposureVM.kt",
    "chars": 2202,
    "preview": "package cz.covid19cz.erouska.ui.exposure\n\nimport androidx.lifecycle.MutableLiveData\nimport androidx.lifecycle.viewModelS"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposure/event/ExposuresEvent.kt",
    "chars": 231,
    "preview": "package cz.covid19cz.erouska.ui.exposure.event\n\nimport arch.event.LiveEvent\n\nclass ExposuresCommandEvent(val command: Co"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/ExposureHelpFragment.kt",
    "chars": 1422,
    "preview": "package cz.covid19cz.erouska.ui.exposurehelp\n\nimport android.os.Bundle\nimport androidx.navigation.fragment.navArgs\nimpor"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/ExposureHelpVM.kt",
    "chars": 1403,
    "preview": "package cz.covid19cz.erouska.ui.exposurehelp\n\nimport androidx.databinding.ObservableArrayList\nimport arch.adapter.Recycl"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/entity/ExposureHelpData.kt",
    "chars": 150,
    "preview": "package cz.covid19cz.erouska.ui.exposurehelp.entity\n\ndata class ExposureHelpData(\n    val title: String?,\n    val items:"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/entity/ExposureHelpItem.kt",
    "chars": 130,
    "preview": "package cz.covid19cz.erouska.ui.exposurehelp.entity\n\ndata class ExposureHelpItem(\n    val iconUrl: String,\n    val label"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/entity/ExposureHelpTitle.kt",
    "chars": 96,
    "preview": "package cz.covid19cz.erouska.ui.exposurehelp.entity\n\nclass ExposureHelpTitle(val title : String)"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/entity/ExposureHelpType.kt",
    "chars": 119,
    "preview": "package cz.covid19cz.erouska.ui.exposurehelp.entity\n\nenum class ExposureHelpType {\n    SYMPTOMS, PREVENTION, EXPOSURE\n}"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposureinfo/ExposureInfoFragment.kt",
    "chars": 1236,
    "preview": "package cz.covid19cz.erouska.ui.exposureinfo\n\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.Menu"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/exposureinfo/ExposureInfoVM.kt",
    "chars": 1227,
    "preview": "package cz.covid19cz.erouska.ui.exposureinfo\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.MutableLiveD"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/help/HelpFragment.kt",
    "chars": 1931,
    "preview": "package cz.covid19cz.erouska.ui.help\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.lifecycle.lifecy"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/help/HelpVM.kt",
    "chars": 1857,
    "preview": "package cz.covid19cz.erouska.ui.help\n\nimport androidx.databinding.ObservableArrayList\nimport arch.adapter.RecyclerLayout"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/AboutAppCategory.kt",
    "chars": 77,
    "preview": "package cz.covid19cz.erouska.ui.help.data\n\nclass AboutAppCategory : Category\n"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/Category.kt",
    "chars": 62,
    "preview": "package cz.covid19cz.erouska.ui.help.data\n\ninterface Category\n"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/FaqCategory.kt",
    "chars": 842,
    "preview": "package cz.covid19cz.erouska.ui.help.data\n\nimport android.os.Parcelable\nimport com.google.gson.Gson\nimport com.google.gs"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/HowToCategory.kt",
    "chars": 79,
    "preview": "package cz.covid19cz.erouska.ui.help.data\n\nclass HowItWorksCategory : Category\n"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/Question.kt",
    "chars": 220,
    "preview": "package cz.covid19cz.erouska.ui.help.data\n\nimport android.os.Parcelable\nimport kotlinx.android.parcel.Parcelize\n\n@Parcel"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/helpcategory/HelpCategoryFragment.kt",
    "chars": 1298,
    "preview": "package cz.covid19cz.erouska.ui.helpcategory\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.navigati"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/helpcategory/HelpCategoryVM.kt",
    "chars": 1072,
    "preview": "package cz.covid19cz.erouska.ui.helpcategory\n\nimport androidx.databinding.ObservableArrayList\nimport arch.adapter.Recycl"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/helpquestion/HelpQuestionFragment.kt",
    "chars": 1083,
    "preview": "package cz.covid19cz.erouska.ui.helpquestion\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.navigati"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/helpquestion/HelpQuestionVM.kt",
    "chars": 636,
    "preview": "package cz.covid19cz.erouska.ui.helpquestion\n\nimport androidx.databinding.ObservableArrayList\nimport arch.adapter.Recycl"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/helpsearch/HelpSearchFragment.kt",
    "chars": 3642,
    "preview": "package cz.covid19cz.erouska.ui.helpsearch\n\nimport android.app.SearchManager\nimport android.content.Context\nimport andro"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/helpsearch/HelpSearchVM.kt",
    "chars": 4263,
    "preview": "package cz.covid19cz.erouska.ui.helpsearch\n\nimport androidx.databinding.ObservableArrayList\nimport androidx.lifecycle.vi"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/helpsearch/data/SearchableQuestion.kt",
    "chars": 166,
    "preview": "package cz.covid19cz.erouska.ui.helpsearch.data\n\ndata class SearchableQuestion(\n    val category: String,\n    var questi"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/helpsearch/ui/SearchableItem.kt",
    "chars": 1278,
    "preview": "package cz.covid19cz.erouska.ui.helpsearch.ui\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport an"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/how/HowItWorksFragment.kt",
    "chars": 1915,
    "preview": "package cz.covid19cz.erouska.ui.how\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.lifecycle.lifecyc"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/how/HowItWorksVM.kt",
    "chars": 763,
    "preview": "package cz.covid19cz.erouska.ui.how\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.OnLifecycleEvent\nimpo"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/how/event/HowItWorksEvent.kt",
    "chars": 200,
    "preview": "package cz.covid19cz.erouska.ui.how.event\n\nimport arch.event.LiveEvent\n\nclass HowItWorksEvent(val command: Command) : Li"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/main/MainActivity.kt",
    "chars": 999,
    "preview": "package cz.covid19cz.erouska.ui.main\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimpo"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/main/MainActivityOld.kt",
    "chars": 7215,
    "preview": "package cz.covid19cz.erouska.ui.main\n\nimport android.content.ComponentName\nimport android.content.Intent\nimport android."
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/main/MainVM.kt",
    "chars": 837,
    "preview": "package cz.covid19cz.erouska.ui.main\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.OnLifecycleEvent\nimp"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/mydata/CaseItemView.kt",
    "chars": 1232,
    "preview": "package cz.covid19cz.erouska.ui.mydata\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport "
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/mydata/MyDataFragment.kt",
    "chars": 1303,
    "preview": "package cz.covid19cz.erouska.ui.mydata\n\nimport android.os.Bundle\nimport android.view.View\nimport cz.covid19cz.erouska.Ap"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/mydata/MyDataVM.kt",
    "chars": 15823,
    "preview": "package cz.covid19cz.erouska.ui.mydata\n\nimport android.text.format.DateUtils\nimport androidx.lifecycle.Lifecycle\nimport "
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/noverificationcode/NoVerificationCodeFragment.kt",
    "chars": 1479,
    "preview": "package cz.covid19cz.erouska.ui.noverificationcode\n\nimport android.os.Bundle\nimport android.view.View\nimport cz.covid19c"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/noverificationcode/NoVerificationCodeVM.kt",
    "chars": 393,
    "preview": "package cz.covid19cz.erouska.ui.noverificationcode\n\nimport cz.covid19cz.erouska.ui.base.BaseVM\nimport cz.covid19cz.erous"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/noverificationcode/event/NoVerificationCodeEvent.kt",
    "chars": 122,
    "preview": "package cz.covid19cz.erouska.ui.noverificationcode.event\n\nimport arch.event.LiveEvent\n\nclass WriteEmailEvent : LiveEvent"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/permissions/BasePermissionsFragment.kt",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/publishsuccess/PublishSuccessFragment.kt",
    "chars": 350,
    "preview": "package cz.covid19cz.erouska.ui.publishsuccess\n\nimport cz.covid19cz.erouska.R\nimport cz.covid19cz.erouska.databinding.Fr"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/publishsuccess/PublishSuccessVM.kt",
    "chars": 281,
    "preview": "package cz.covid19cz.erouska.ui.publishsuccess\n\nimport cz.covid19cz.erouska.ui.base.BaseVM\n\nclass PublishSuccessVM : Bas"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/ragnarok/RagnarokVM.kt",
    "chars": 1055,
    "preview": "package cz.covid19cz.erouska.ui.ragnarok\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.MutableLiveData\n"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/recentexposures/RecentExposuresFragment.kt",
    "chars": 783,
    "preview": "package cz.covid19cz.erouska.ui.recentexposures\n\nimport android.os.Bundle\nimport android.view.View\nimport cz.covid19cz.e"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/recentexposures/RecentExposuresVM.kt",
    "chars": 4743,
    "preview": "package cz.covid19cz.erouska.ui.recentexposures\n\nimport androidx.databinding.ObservableArrayList\nimport androidx.lifecyc"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/recentexposures/entity/RecentExposureGroupHeaderItem.kt",
    "chars": 382,
    "preview": "package cz.covid19cz.erouska.ui.recentexposures.entity\n\nimport cz.covid19cz.erouska.ext.timestampToDate\nimport cz.covid1"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxConfigFragment.kt",
    "chars": 692,
    "preview": "package cz.covid19cz.erouska.ui.sandbox\n\nimport android.os.Bundle\nimport cz.covid19cz.erouska.R\nimport cz.covid19cz.erou"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxConfigVM.kt",
    "chars": 2217,
    "preview": "package cz.covid19cz.erouska.ui.sandbox\n\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.OnLifecycleEvent\n"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxConfigValues.kt",
    "chars": 1636,
    "preview": "package cz.covid19cz.erouska.ui.sandbox\n\nimport androidx.lifecycle.MutableLiveData\n\nclass SandboxConfigValues(val title:"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxDataFragment.kt",
    "chars": 1319,
    "preview": "package cz.covid19cz.erouska.ui.sandbox\n\nimport android.os.Bundle\nimport android.view.View\nimport arch.adapter.RecyclerL"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxDataVM.kt",
    "chars": 2274,
    "preview": "package cz.covid19cz.erouska.ui.sandbox\n\nimport androidx.databinding.ObservableArrayList\nimport androidx.lifecycle.Lifec"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxFragment.kt",
    "chars": 1539,
    "preview": "package cz.covid19cz.erouska.ui.sandbox\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.os.Bun"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxVM.kt",
    "chars": 4948,
    "preview": "package cz.covid19cz.erouska.ui.sandbox\n\nimport android.util.Base64\nimport androidx.databinding.ObservableArrayList\nimpo"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/event/SnackbarEvent.kt",
    "chars": 128,
    "preview": "package cz.covid19cz.erouska.ui.sandbox.event\n\nimport arch.event.LiveEvent\n\nclass SnackbarEvent(val text : String) : Liv"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/symptomdate/SymptomDateFragment.kt",
    "chars": 2448,
    "preview": "package cz.covid19cz.erouska.ui.symptomdate\n\nimport android.app.DatePickerDialog\nimport android.os.Bundle\nimport android"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/symptomdate/SymptomDateVM.kt",
    "chars": 1654,
    "preview": "package cz.covid19cz.erouska.ui.symptomdate\n\nimport androidx.lifecycle.MutableLiveData\nimport arch.livedata.SafeMutableL"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/symptomdate/event/DatePickerEvent.kt",
    "chars": 157,
    "preview": "package cz.covid19cz.erouska.ui.symptomdate.event\n\nimport arch.event.LiveEvent\nimport java.util.*\n\nclass DatePickerEvent"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/symptomdate/event/SymptomDateCommandEvent.kt",
    "chars": 252,
    "preview": "package cz.covid19cz.erouska.ui.symptomdate.event\n\nimport arch.event.LiveEvent\nimport java.util.*\n\nclass SymptomDateComm"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/traveller/TravellerFragment.kt",
    "chars": 382,
    "preview": "package cz.covid19cz.erouska.ui.traveller\n\nimport cz.covid19cz.erouska.R\nimport cz.covid19cz.erouska.databinding.Fragmen"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/traveller/TravellerVM.kt",
    "chars": 492,
    "preview": "package cz.covid19cz.erouska.ui.traveller\n\nimport cz.covid19cz.erouska.db.SharedPrefsRepository\nimport cz.covid19cz.erou"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/update/efgs/EfgsUpdateFragment.kt",
    "chars": 2205,
    "preview": "package cz.covid19cz.erouska.ui.update.efgs\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.appcompat"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/update/efgs/EfgsUpdateVM.kt",
    "chars": 502,
    "preview": "package cz.covid19cz.erouska.ui.update.efgs\n\nimport arch.viewmodel.BaseArchViewModel\nimport cz.covid19cz.erouska.AppConf"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/update/playservices/UpdatePlayServicesFragment.kt",
    "chars": 2010,
    "preview": "package cz.covid19cz.erouska.ui.update.playservices\n\nimport android.content.Intent\nimport android.net.Uri\nimport android"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/update/playservices/UpdatePlayServicesVM.kt",
    "chars": 453,
    "preview": "package cz.covid19cz.erouska.ui.update.playservices\n\nimport cz.covid19cz.erouska.ui.base.BaseVM\nimport cz.covid19cz.erou"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/update/playservices/event/UpdatePlayServicesEvent.kt",
    "chars": 209,
    "preview": "package cz.covid19cz.erouska.ui.update.playservices.event\n\nimport arch.event.LiveEvent\n\nclass UpdatePlayServicesEvent(va"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/InvalidTokenException.kt",
    "chars": 87,
    "preview": "package cz.covid19cz.erouska.ui.verification\n\nclass InvalidTokenException : Throwable()"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/NoKeysException.kt",
    "chars": 81,
    "preview": "package cz.covid19cz.erouska.ui.verification\n\nclass NoKeysException : Throwable()"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/ReportExposureException.kt",
    "chars": 127,
    "preview": "package cz.covid19cz.erouska.ui.verification\n\nclass ReportExposureException(val error: String, val code: String?) : Thro"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/VerificationFragment.kt",
    "chars": 1849,
    "preview": "package cz.covid19cz.erouska.ui.verification\n\nimport android.os.Bundle\nimport android.view.View\nimport cz.covid19cz.erou"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/VerificationVM.kt",
    "chars": 4153,
    "preview": "package cz.covid19cz.erouska.ui.verification\n\nimport androidx.core.text.isDigitsOnly\nimport androidx.lifecycle.MutableLi"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/VerifyException.kt",
    "chars": 126,
    "preview": "package cz.covid19cz.erouska.ui.verification\n\nclass VerifyException(message : String?, val code: String?) : Throwable(me"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/event/VerificationCommandEvent.kt",
    "chars": 246,
    "preview": "package cz.covid19cz.erouska.ui.verification.event\n\nimport arch.event.LiveEvent\nimport java.util.*\n\nclass VerificationCo"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/welcome/WelcomeFragment.kt",
    "chars": 1911,
    "preview": "package cz.covid19cz.erouska.ui.welcome\n\nimport android.os.Bundle\nimport android.text.method.LinkMovementMethod\nimport a"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/welcome/WelcomeVM.kt",
    "chars": 589,
    "preview": "package cz.covid19cz.erouska.ui.welcome\n\nimport cz.covid19cz.erouska.db.SharedPrefsRepository\nimport cz.covid19cz.erousk"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/ui/welcome/event/WelcomeCommandEvent.kt",
    "chars": 206,
    "preview": "package cz.covid19cz.erouska.ui.welcome.event\n\nimport arch.event.LiveEvent\n\nclass WelcomeCommandEvent(val command: Comma"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/Analytics.kt",
    "chars": 855,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport android.content.Context\nimport android.os.Bundle\nimport com.google.firebase.a"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/CustomTabHelper.kt",
    "chars": 3733,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport android.content.Context\nimport android.content.Intent\nimport android.content."
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/DeviceInfo.kt",
    "chars": 2197,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport android.annotation.SuppressLint\nimport android.bluetooth.BluetoothAdapter\nimp"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/L.kt",
    "chars": 1702,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport android.util.Log\nimport com.google.firebase.crashlytics.FirebaseCrashlytics\ni"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/LocaleUtils.kt",
    "chars": 460,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport cz.covid19cz.erouska.BuildConfig\nimport java.util.*\n\nobject LocaleUtils {\n\n  "
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/Markdown.kt",
    "chars": 4107,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport android.content.Context\nimport android.text.style.BackgroundColorSpan\nimport "
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/MiscUtils.kt",
    "chars": 372,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport java.text.DecimalFormat\nimport java.text.NumberFormat\n\nobject SignNumberForma"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/SharedPrefsLiveData.kt",
    "chars": 7620,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport android.content.SharedPreferences\nimport android.content.SharedPreferences.Ed"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/SupportEmailGenerator.kt",
    "chars": 7204,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport android.app.Activity\nimport android.content.Context\nimport android.net.Uri\nim"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/Text.kt",
    "chars": 2167,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport android.content.Context\nimport android.widget.TextView\nimport androidx.annota"
  },
  {
    "path": "app/src/main/kotlin/cz/covid19cz/erouska/utils/ViewUtils.kt",
    "chars": 239,
    "preview": "package cz.covid19cz.erouska.utils\n\nimport android.view.View\nimport cz.covid19cz.erouska.ext.hide\nimport cz.covid19cz.er"
  },
  {
    "path": "app/src/main/res/drawable/highlight_selector.xml",
    "chars": 342,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:co"
  },
  {
    "path": "app/src/main/res/drawable/ic_about.xml",
    "chars": 386,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"20dp\"\n    android:height=\"20dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_ack_case.xml",
    "chars": 3520,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_act_case.xml",
    "chars": 4602,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_action_close.xml",
    "chars": 381,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_action_up.xml",
    "chars": 349,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_active.xml",
    "chars": 1621,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"42dp\"\n    android:height=\"42dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_antigen.xml",
    "chars": 1979,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_right.xml",
    "chars": 356,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_balloon.xml",
    "chars": 9827,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"180dp\"\n    android:height=\"180dp\"\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_bluetooth_onboard.xml",
    "chars": 20408,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"130dp\"\n    android:height=\"130dp\"\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_calendar.xml",
    "chars": 445,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_chat.xml",
    "chars": 422,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#757575\"\n    android:viewportHeight=\"24.0\" android:viewportWidth=\"24.0\"\n    "
  },
  {
    "path": "app/src/main/res/drawable/ic_confirm.xml",
    "chars": 14179,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    "
  },
  {
    "path": "app/src/main/res/drawable/ic_contacts.xml",
    "chars": 609,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_control.xml",
    "chars": 10386,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    "
  },
  {
    "path": "app/src/main/res/drawable/ic_cured.xml",
    "chars": 3616,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_data.xml",
    "chars": 590,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_death_toll.xml",
    "chars": 701,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_encounter.xml",
    "chars": 19146,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    "
  },
  {
    "path": "app/src/main/res/drawable/ic_error.xml",
    "chars": 1043,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"130dp\"\n    android:height=\"130dp\"\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_eval.xml",
    "chars": 29381,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    "
  },
  {
    "path": "app/src/main/res/drawable/ic_exposure_info.xml",
    "chars": 11351,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"130dp\"\n    android:height=\"130dp\"\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_face_mask.xml",
    "chars": 1916,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_google_play_services.xml",
    "chars": 9160,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"200dp\"\n    android:height=\"200dp\"\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_help.xml",
    "chars": 626,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_home.xml",
    "chars": 354,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  }
]

// ... and 155 more files (download for full content)

About this extraction

This page contains the full source code of the covid19cz/erouska-android GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 355 files (1.3 MB), approximately 435.7k tokens, and a symbol index with 4 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!