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 📸
Show/Hide content
<# Please provide screenshots for any visual change. The best way is to take screenshots directly from the app with production-like data. #>
# 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
================================================
================================================
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
[](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/).
## 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): 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, 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, urlTextPairs: ArrayList) {
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() {
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 = 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
================================================
================================================
FILE: app/src/dev/res/drawable/ic_launcher_foreground.xml
================================================
================================================
FILE: app/src/dev/res/values/controls.xml
================================================
erouska-devhttps://europe-west1-erouska-key-server-dev.cloudfunctions.net/https://apiserver-eyrqoibmxa-ew.a.run.app/https://europe-west1-erouska-key-server-dev.cloudfunctions.netcz.covid19cz.erouska.dev.fileprovider
================================================
FILE: app/src/dev/res/values/strings.xml
================================================
eRouška DEV
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
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)
@Query("SELECT * FROM daily_summaries ORDER BY days_since_epoch DESC LIMIT 1")
suspend fun getLatest() : List
@Query("SELECT * FROM daily_summaries WHERE notified == 1 ORDER BY days_since_epoch DESC LIMIT 1")
suspend fun getLastNotified() : List
@Query("SELECT * FROM daily_summaries ORDER BY days_since_epoch DESC")
suspend fun getAllByExposureDate() : List
@Query("SELECT * FROM daily_summaries ORDER BY import_timestamp DESC, days_since_epoch DESC")
suspend fun getAllByImportDate() : List
@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? {
return prefs.getString(REPORT_TYPE_WEIGHTS, null)?.let {
it.split(";").mapNotNull { it.toDoubleOrNull() }
}
}
fun getInfectiousnessWeights(): List? {
return prefs.getString(INFECTIOUSNESS_WEIGHTS, null)?.let {
it.split(";").mapNotNull { it.toDoubleOrNull() }
}
}
fun getAttenuationBucketThresholdDb(): List? {
return prefs.getString(ATTENUATION_BUCKET_THRESHOLD_DB, null)?.let {
it.split(";").mapNotNull { it.toIntOrNull() }
}
}
fun getAttenuationBucketWeights(): List? {
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, hmacKey: String): String {
val cleartextSegments = ArrayList()
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 { cont ->
client.start()
.addOnSuccessListener {
prefs.setExposureNotificationsEnabled(true)
cont.resume(it)
}.addOnFailureListener {
cont.resumeWithException(it)
}
}
suspend fun stop() = suspendCoroutine { 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 = suspendCoroutine { cont ->
client.status
.addOnSuccessListener {
cont.resume(it)
}.addOnFailureListener {
cont.resumeWithException(it)
}
}
suspend fun provideDiagnosisKeys(
keyList: List
): Boolean = suspendCoroutine { cont ->
setDiagnosisKeysMapping()
val filesToImport = mutableListOf()
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()
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 =
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 {
return db.dao().getAllByExposureDate()
}
suspend fun getDailySummariesFromDbByImportDate(): List {
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 =
suspendCoroutine { cont ->
client.temporaryExposureKeyHistory.addOnSuccessListener {
cont.resume(it)
}.addOnFailureListener {
cont.resumeWithException(it)
}
}
suspend fun getExposureWindows(): List = 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(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(
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 Observable.execute(onNext : (t :T) -> Unit, onError : (t :Throwable) -> Unit): Disposable {
return inBackground().subscribe(onNext, onError)
}
fun Single.execute(onNext : (t :T) -> Unit, onError : (t :Throwable) -> Unit): Disposable {
return inBackground().subscribe(onNext, onError)
}
fun Maybe.execute(onSuccess : (t :T) -> Unit, onError : (t :Throwable) -> Unit): Disposable {
return inBackground().subscribe(onSuccess, onError)
}
fun Flowable.execute(onNext : (t :T) -> Unit, onError : (t :Throwable) -> Unit): Disposable {
return inBackground().subscribe(onNext, onError)
}
fun Observable.inBackground(): Observable {
return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}
fun Single.inBackground(): Single {
return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}
fun Maybe.inBackground(): Maybe {
return subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}
fun Flowable.inBackground(): Flowable {
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{
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,
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 {
return withContext(Dispatchers.IO) {
val countryUrls = parseCountryUrls(
if (prefs.isTraveller()) {
AppConfig.keyExportEuTravellerUrls
} else {
AppConfig.keyExportNonTravellerUrls
}
)
val keysList = mutableListOf()
val keysListTasks = mutableListOf>()
countryUrls.forEach {
keysListTasks.add(async { downloadIndex(it) })
}
keysList.addAll(keysListTasks.awaitAll().filterNotNull())
return@withContext keysList
}
}
private fun parseCountryUrls(json: String): List {
val countryUrlListType: Type = object : TypeToken?>() {}.type
val countryUrls: ArrayList = 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()
val zipUrls = fileNames.map { AppConfig.keyExportUrl + it }
val downloads = mutableListOf>()
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(
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? = null
): Map {
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)
}.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,
val urls : List
) {
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,
val verificationPayload: String?,
val hmackey: String?,
val revisionToken: String?,
val traveler: Boolean,
val reportType: String,
val visitedCountries: List,
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(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(
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(
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()
val state = mutableState as LiveData
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(
layoutId: Int,
viewModelClass: KClass
) :
BaseArchActivity(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(
layoutId: Int,
viewModelClass: KClass
) : BaseArchFragment(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 subscribe(observable: Observable, onError: (Throwable) -> Unit, onNext: (T) -> Unit) : Disposable {
val disposable = observable.execute(onNext, onError)
disposables.add(disposable)
return disposable
}
fun subscribe(observable: Flowable, onError: (Throwable) -> Unit, onNext: (T) -> Unit) : Disposable {
val disposable = observable.execute(onNext, onError)
disposables.add(disposable)
return disposable
}
fun subscribe(single: Single, onError: (Throwable) -> Unit, onNext: (T) -> Unit) : Disposable {
val disposable = single.execute(onNext, onError)
disposables.add(disposable)
return disposable
}
fun subscribe(maybe: Maybe, 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(
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()
val items = ObservableArrayList()
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
val contactsListType: Type = object : TypeToken?>() {}.type
val contacts: ArrayList = 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(
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 Android 11+
val lsEnabled = viewModel.locationState.value
val btEnabled = viewModel.bluetoothState.value
dash_card_active.showOrHide(enEnabled && (viewModel.isLocationlessScanSupported() || lsEnabled) && btEnabled)
dash_card_inactive.showOrHide(!enEnabled && (viewModel.isLocationlessScanSupported() || lsEnabled) && btEnabled)
}
private fun updateOnboardingNotif(show: Boolean) {
how_it_works_container.showOrHide(
show &&
!data_notification_container.isVisible &&
!exposure_notification_container.isVisible
)
}
private fun showOrHideDataNotification(show: Boolean) {
data_notification_container.showOrHide(show)
// TODO: It's weird to call ViewModel after ViewModel calls View - this loop is dangerous. It should be refactored.
viewModel.checkAndShowOrHideHowItWorksNotification() // check if How It Works in-app notification should be shown
}
private fun showOrHideExposureNotification(show: Boolean) {
exposure_notification_container.showOrHide(show)
viewModel.checkAndShowOrHideHowItWorksNotification() // check if How It Works in-app notification should be shown
}
private fun showWelcomeScreen() {
navigate(R.id.action_nav_dashboard_to_nav_welcome_fragment)
}
private fun showPlayServicesUpdate() {
navigate(R.id.action_nav_dashboard_to_nav_play_services_update,
Bundle().apply { putBoolean("demo", true) })
}
private fun showEfgs() {
navigate(R.id.action_nav_dashboard_to_nav_efgs)
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/DashboardVM.kt
================================================
package cz.covid19cz.erouska.ui.dashboard
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.viewModelScope
import arch.livedata.SafeMutableLiveData
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatus
import com.google.firebase.auth.FirebaseAuth
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.ext.daysSinceEpochToDateString
import cz.covid19cz.erouska.net.ExposureServerRepository
import cz.covid19cz.erouska.ui.base.BaseVM
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.utils.L
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DashboardVM
@Inject constructor(
private val exposureNotificationsRepository: ExposureNotificationsRepository,
private val exposureNotificationsServerRepository: ExposureServerRepository,
private val prefs: SharedPrefsRepository
) : BaseVM() {
private val auth: FirebaseAuth = FirebaseAuth.getInstance()
val exposureNotificationsEnabled = SafeMutableLiveData(prefs.isExposureNotificationsEnabled())
val bluetoothState = SafeMutableLiveData(true)
val locationState = SafeMutableLiveData(true)
val efgsState = SafeMutableLiveData(prefs.isTraveller())
val lastUpdateTimestamp = MutableLiveData(prefs.getLastKeyImport())
val lastExposureDate = MutableLiveData()
val exposuresCount = MutableLiveData(0)
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
prefs.lastKeyImportLive.observeForever {
updateLastKeyImport(it)
checkForObsoleteData()
checkAndShowOrHideHowItWorksNotification()
}
exposureNotificationsEnabled.observeForever { enabled ->
if (enabled) {
checkForRiskyExposure()
}
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
if (auth.currentUser == null) {
publish(DashboardCommandEvent(DashboardCommandEvent.Command.NOT_ACTIVATED))
return
}
efgsState.value = prefs.isTraveller()
checkStatus()
checkRiskyExposures()
exposureNotificationsServerRepository.scheduleKeyDownload()
exposureNotificationsRepository.scheduleSelfChecker()
checkForObsoleteData()
checkAndShowOrHideHowItWorksNotification()
}
private fun updateLastKeyImport(lastKeyImport: Long) {
if (lastKeyImport != 0L) {
lastUpdateTimestamp.value = lastKeyImport
}
}
fun checkStatus() {
viewModelScope.launch {
kotlin.runCatching {
if (!exposureNotificationsRepository.isEnabled()) {
return@runCatching setOf(ExposureNotificationStatus.INACTIVATED)
}
return@runCatching exposureNotificationsRepository.getStatus()
}.onSuccess { status ->
L.i("EN API Status: ${status.joinToString { it.name }}")
onExposureNotificationsStatusChanged(status)
}.onFailure {
publish(GmsApiErrorEvent(it))
}
}
}
/**
* Stop the EN API.
*/
fun stop() {
viewModelScope.launch {
kotlin.runCatching {
if (exposureNotificationsRepository.isEnabled()) {
exposureNotificationsRepository.stop()
}
}.onSuccess {
L.i("EN API Stopped")
checkStatus()
publish(DashboardCommandEvent(DashboardCommandEvent.Command.TURN_OFF))
}.onFailure {
L.e(it)
publish(GmsApiErrorEvent(it)) // handle API error
}
}
}
/**
* Start the EN API.
*/
fun start() {
viewModelScope.launch {
kotlin.runCatching {
exposureNotificationsRepository.start()
}.onSuccess {
L.i("EN API Started")
checkStatus()
}.onFailure {
L.e(it)
publish(GmsApiErrorEvent(it)) // handle API error
}
}
}
/**
* EN API state has changed.
*/
private fun onExposureNotificationsStatusChanged(status: Set) {
bluetoothState.value = !status.contains(ExposureNotificationStatus.BLUETOOTH_DISABLED)
locationState.value = !status.contains(ExposureNotificationStatus.LOCATION_DISABLED)
exposureNotificationsEnabled.value = status.contains(ExposureNotificationStatus.ACTIVATED)
}
private fun checkForRiskyExposure() {
viewModelScope.launch {
runCatching {
exposureNotificationsRepository.getLastRiskyExposure()
}.onSuccess {
it?.let {
if (!it.accepted) {
showExposure()
}
}
}.onFailure {
L.e(it)
}
}
}
private fun checkForObsoleteData() {
if (prefs.hasOutdatedKeyData()) {
publish(DashboardCommandEvent(DashboardCommandEvent.Command.DATA_OBSOLETE))
} else {
publish(DashboardCommandEvent(DashboardCommandEvent.Command.DATA_UP_TO_DATE))
}
}
/**
* Check the database for recent risky exposures.
*/
private fun checkRiskyExposures() {
viewModelScope.launch {
kotlin.runCatching {
exposureNotificationsRepository.getDailySummariesFromDbByExposureDate()
}.onSuccess { riskyExposureList ->
if (!riskyExposureList.isNullOrEmpty()) {
val lastExposureDate =
riskyExposureList.first().daysSinceEpoch.daysSinceEpochToDateString()
onRiskyExposuresFound(riskyExposureList.size, lastExposureDate)
} else {
onNoRiskyExposuresFound()
}
}.onFailure {
L.e(it)
}
}
}
private fun onRiskyExposuresFound(count: Int, lastExposureDate: String) {
this.lastExposureDate.value = lastExposureDate
this.exposuresCount.value = count
publish(ExposuresCommandEvent(ExposuresCommandEvent.Command.RECENT_EXPOSURE))
}
private fun onNoRiskyExposuresFound() {
publish(ExposuresCommandEvent(ExposuresCommandEvent.Command.NO_RECENT_EXPOSURES))
}
private fun showExposure() {
publish(DashboardCommandEvent(DashboardCommandEvent.Command.RECENT_EXPOSURE))
}
fun showExposureDetail() {
viewModelScope.launch {
val lastRiskyExposure = exposureNotificationsRepository.getLastRiskyExposure()
if (lastRiskyExposure != null && lastRiskyExposure.daysSinceEpoch > prefs.getLastShownExposureInfo()) {
navigate(DashboardFragmentDirections.actionNavDashboardToNavExposureInfo())
} else {
navigate(DashboardFragmentDirections.actionNavDashboardToNavExposure())
}
}
}
fun showEfgs() {
publish(DashboardCommandEvent(DashboardCommandEvent.Command.EFGS))
}
fun acceptExposure() {
viewModelScope.launch {
runCatching {
exposureNotificationsRepository.markAsAccepted()
}.onFailure {
L.e(it)
}
}
}
fun unregister() {
FirebaseAuth.getInstance().signOut()
}
fun sendData() {
navigate(R.id.action_nav_dashboard_to_nav_verification)
}
fun shouldIntroduceEFGS(): Boolean {
return !prefs.wasEFGSIntroduced() && !prefs.shouldSuppressUpdateScreens()
}
fun cancelSuppression() {
return prefs.setSuppressUpdateScreens(false)
}
fun isLocationlessScanSupported() = exposureNotificationsRepository.isLocationlessScanSupported()
fun checkAndShowOrHideHowItWorksNotification() {
if (prefs.wasHowItWorksShown()) {
publish(DashboardCommandEvent(DashboardCommandEvent.Command.HIDE_HOW_IT_WORKS))
} else {
publish(DashboardCommandEvent(DashboardCommandEvent.Command.SHOW_HOW_IT_WORKS))
}
}
fun dismissHowItWorksNotification() {
prefs.setHowItWorksShown()
publish(DashboardCommandEvent(DashboardCommandEvent.Command.HIDE_HOW_IT_WORKS))
}
fun showHowItWorksPage() {
navigate(DashboardFragmentDirections.actionNavDashboardToNavHowItWorks())
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/TravellerDashboardCardView.kt
================================================
package cz.covid19cz.erouska.ui.dashboard
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import cz.covid19cz.erouska.R
import kotlinx.android.synthetic.main.view_data_item.view.*
class TravellerDashboardCardView : DashboardCardView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, attributeSetId: Int) : super(
context,
attrs,
attributeSetId
)
override fun getLayoutId(): Int = R.layout.traveller_dashboard_card_view
override var card_icon: Drawable? = null
set(value) {
field = value
value?.let {
icon.setImageDrawable(it)
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/event/DashboardCommandEvent.kt
================================================
package cz.covid19cz.erouska.ui.dashboard.event
import arch.event.LiveEvent
class DashboardCommandEvent(val command: Command) : LiveEvent() {
enum class Command {
TURN_OFF,
DATA_OBSOLETE,
DATA_UP_TO_DATE,
RECENT_EXPOSURE,
EFGS,
NOT_ACTIVATED,
SHOW_HOW_IT_WORKS,
HIDE_HOW_IT_WORKS
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/event/DisabledEvent.kt
================================================
package cz.covid19cz.erouska.ui.dashboard.event
import arch.event.LiveEvent
class BluetoothDisabledEvent : LiveEvent()
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/dashboard/event/GmsApiErrorEvent.kt
================================================
package cz.covid19cz.erouska.ui.dashboard.event
import arch.event.LiveEvent
class GmsApiErrorEvent(val throwable: Throwable) : LiveEvent()
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/efgs/EfgsFragment.kt
================================================
package cz.covid19cz.erouska.ui.efgs
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentEfgsBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class EfgsFragment :
BaseFragment(R.layout.fragment_efgs, EfgsVM::class) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true, IconType.CLOSE)
viewModel.efgsState.observe(viewLifecycleOwner){ checked ->
if (binding.efgsCheckbox.tag != null) {
if (!checked) {
showEfgsDisableConfirmationDialog()
}
} else {
// using tag as init flag to prevent dialog onCreate
binding.efgsCheckbox.tag = true
}
}
}
private fun showEfgsDisableConfirmationDialog() {
AlertDialog.Builder(requireContext())
.setMessage(
HtmlCompat.fromHtml(
getString(R.string.efgs_disable_confirmation, AppConfig.efgsDays),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
)
.setPositiveButton(R.string.efgs_disable_confirmation_on) { _, _ ->
viewModel.efgsState.value = true
}
.setNegativeButton(R.string.efgs_disable_confirmation_off) { _, _ ->
}.show()
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/efgs/EfgsVM.kt
================================================
package cz.covid19cz.erouska.ui.efgs
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.OnLifecycleEvent
import arch.livedata.SafeMutableLiveData
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class EfgsVM @Inject constructor(val prefs : SharedPrefsRepository) : BaseVM() {
val efgsState = SafeMutableLiveData(prefs.isTraveller())
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate(){
efgsState.observeForever {
prefs.setTraveller(it)
}
}
fun efgsDays() = AppConfig.efgsDays
fun efgsSupportedCountries() = AppConfig.efgsSupportedCountries
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/efgs/event/EfgsCommandEvent.kt
================================================
package cz.covid19cz.erouska.ui.efgs.event
import arch.event.LiveEvent
class EfgsCommandEvent(val command: Command) : LiveEvent() {
enum class Command{
TURN_ON,
TURN_OFF,
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/efgsagreement/EfgsAgreementFragment.kt
================================================
package cz.covid19cz.erouska.ui.efgsagreement
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.text.HtmlCompat
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentEfgsAgreementBinding
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsErrorHandling
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.dashboard.event.GmsApiErrorEvent
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class EfgsAgreementFragment : BaseFragment(R.layout.fragment_efgs_agreement, EfgsAgreementVM::class) {
companion object {
private const val SCREEN_NAME = "EFGS Agreement"
}
@Inject
internal lateinit var exposureNotificationsErrorHandling: ExposureNotificationsErrorHandling
private var gmsDialogShown = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
subscribe(GmsApiErrorEvent::class) {
if (!gmsDialogShown) {
gmsDialogShown = true
exposureNotificationsErrorHandling.handle(it, this, SCREEN_NAME)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textDescription.text = HtmlCompat.fromHtml(
getString(R.string.efgs_agreement_description, AppConfig.conditionsOfUseUrl),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.textDescription.movementMethod = LinkMovementMethod.getInstance()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == ExposureNotificationsErrorHandling.REQUEST_GMS_ERROR_RESOLUTION) {
gmsDialogShown = false
when (resultCode) {
Activity.RESULT_OK -> viewModel.publishKeys()
}
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/efgsagreement/EfgsAgreementVM.kt
================================================
package cz.covid19cz.erouska.ui.efgsagreement
import androidx.lifecycle.viewModelScope
import arch.livedata.SafeMutableLiveData
import com.google.android.gms.common.api.ApiException
import cz.covid19cz.erouska.db.SharedPrefsRepository
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.ui.error.entity.ErrorType
import cz.covid19cz.erouska.ui.verification.NoKeysException
import cz.covid19cz.erouska.ui.verification.ReportExposureException
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.net.UnknownHostException
import javax.inject.Inject
@HiltViewModel
class EfgsAgreementVM @Inject constructor(val prefs: SharedPrefsRepository, val exposureNotificationsRepo: ExposureNotificationsRepository) :
BaseVM() {
val traveller = prefs.isTraveller()
val loading = SafeMutableLiveData(false)
fun agree() {
prefs.setConsentToFederation(true)
publishKeys()
}
fun disagree() {
prefs.setConsentToFederation(false)
publishKeys()
}
fun publishKeys() {
loading.value = true
viewModelScope.launch {
kotlin.runCatching {
if (!exposureNotificationsRepo.isEnabled()) {
exposureNotificationsRepo.start()
}
exposureNotificationsRepo.publishKeys()
}.onSuccess {
loading.value = false
prefs.deletePublishKeysTemporaryData()
prefs.setLastDataSentDate()
navigate(EfgsAgreementFragmentDirections.actionNavEfgsAgreementToNavPublishSuccess(it > 1))
}.onFailure {
loading.value = false
handleSendDataErrors(it)
}
}
}
private fun handleSendDataErrors(exception: Throwable) {
when (exception) {
is ApiException -> publish(GmsApiErrorEvent(exception))
is NoKeysException -> navigate(EfgsAgreementFragmentDirections.actionNavEfgsAgreementToNavPublishSuccess(false))
is ReportExposureException -> {
navigate(EfgsAgreementFragmentDirections.actionNavEfgsAgreementToNavError(
type = ErrorType.GENERAL_ERROR ,
errorCode = "${exception.message} ${exception.code}"
))
}
is UnknownHostException -> {
navigate(EfgsAgreementFragmentDirections.actionNavEfgsAgreementToNavError(
type = ErrorType.NO_INTERNET
))
}
else -> {
L.e(exception)
navigate(EfgsAgreementFragmentDirections.actionNavEfgsAgreementToNavError(
type = ErrorType.GENERAL_ERROR ,
errorCode = "${exception.message}"
))
}
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/error/ErrorFragment.kt
================================================
package cz.covid19cz.erouska.ui.error
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentErrorBinding
import cz.covid19cz.erouska.ext.*
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.error.entity.ErrorType
import cz.covid19cz.erouska.ui.verification.VerificationFragment
import cz.covid19cz.erouska.utils.SupportEmailGenerator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class ErrorFragment : BaseFragment(
R.layout.fragment_error,
ErrorVM::class
) {
private val args: ErrorFragmentArgs by navArgs()
@Inject
internal lateinit var supportEmailGenerator: SupportEmailGenerator
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true, IconType.CLOSE)
when (args.type) {
ErrorType.EXPIRED_OR_USED_CODE -> {
onCodeExpiredOrUsed()
}
ErrorType.INVALID_CODE -> {
onCodeInvalid()
}
ErrorType.GENERAL_ERROR -> {
onGeneralError(args.errorCode)
}
ErrorType.NO_INTERNET -> {
onNoInternet()
}
}
}
private fun onCodeExpiredOrUsed() {
with(binding) {
emailButton.show()
closeButton.hide()
tryAgainButton.hide()
errorHeader.text = getString(R.string.send_data_failure_header)
errorBody.text =
getString(R.string.send_data_code_expired_or_used, AppConfig.supportEmail)
emailButton.setOnClickListener {
supportEmailGenerator.sendVerificationEmail(requireActivity())
}
}
}
private fun onCodeInvalid() {
with(binding){
tryAgainButton.show()
emailButton.show()
closeButton.hide()
emailButton.text = getString(R.string.support_request_button)
errorHeader.text = getString(R.string.send_data_code_invalid_header)
errorBody.text = getString(R.string.send_data_code_invalid_body, AppConfig.supportEmail)
tryAgainButton.setOnClickListener { navController().navigateUp() }
emailButton.setOnClickListener {
supportEmailGenerator.sendInvalidCodeEmail(requireActivity())
}
}
}
private fun onGeneralError(errorMessage: String) {
with(binding) {
emailButton.show()
closeButton.hide()
tryAgainButton.hide()
errorDesc.show()
errorDesc.text = getString(R.string.general_desc)
emailButton.text = getString(R.string.support_request_button)
errorHeader.text = getString(R.string.send_data_failure_header)
errorBody.text = getString(R.string.general_error, AppConfig.supportEmail, errorMessage)
emailButton.setOnClickListener {
supportEmailGenerator.sendSupportEmail(
requireActivity(),
lifecycleScope,
errorCode = errorMessage,
isError = true,
screenOrigin = VerificationFragment.SCREEN_NAME
)
}
}
}
private fun onNoInternet() {
with(binding) {
emailButton.hide()
closeButton.show()
tryAgainButton.show()
tryAgainButton.setOnClickListener { navController().navigateUp() }
closeButton.setOnClickListener {
navigate(ErrorFragmentDirections.actionNavErrorToNavDashboard())
}
errorHeader.text = getString(R.string.send_data_failure_header)
errorBody.text = getString(R.string.no_internet_error)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
navigate(ErrorFragmentDirections.actionNavErrorToNavDashboard())
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/error/ErrorVM.kt
================================================
package cz.covid19cz.erouska.ui.error
import cz.covid19cz.erouska.ui.base.BaseVM
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ErrorVM @Inject constructor() :
BaseVM()
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/error/entity/ErrorType.kt
================================================
package cz.covid19cz.erouska.ui.error.entity
enum class ErrorType {
NO_INTERNET, GENERAL_ERROR, INVALID_CODE, EXPIRED_OR_USED_CODE
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposure/ExposureFragment.kt
================================================
package cz.covid19cz.erouska.ui.exposure
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import androidx.navigation.fragment.navArgs
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentExposureBinding
import cz.covid19cz.erouska.ext.hide
import cz.covid19cz.erouska.ext.show
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.exposure.event.ExposuresCommandEvent
import cz.covid19cz.erouska.ui.exposurehelp.entity.ExposureHelpType
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_exposure.*
@AndroidEntryPoint
class ExposureFragment : BaseFragment(
R.layout.fragment_exposure,
ExposureVM::class
) {
val args: ExposureFragmentArgs by navArgs()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true, IconType.CLOSE)
activity?.title = AppConfig.exposureUITitle
symptoms_text.text = AppConfig.symptomsUITitle
spread_text.text = AppConfig.spreadPreventionUITitle
earlier_exposures_text.text = AppConfig.recentExposuresUITitle
symptoms_content.text = AppConfig.riskyEncountersWithSymptoms
no_symptoms_content.text = AppConfig.riskyEncountersWithoutSymptoms
viewModel.checkExposures(args.demo)
subscribe(ExposuresCommandEvent::class) {
when (it.command) {
ExposuresCommandEvent.Command.NO_RECENT_EXPOSURES -> onNoRecentExposures()
ExposuresCommandEvent.Command.RECENT_EXPOSURE -> onRecentExposures()
}
}
setupListeners()
}
private fun setupListeners() {
symptoms_container.setOnClickListener { navigate(ExposureFragmentDirections.actionNavExposureToNavExposureHelp(ExposureHelpType.SYMPTOMS)) }
spread_prevention_container.setOnClickListener { navigate(ExposureFragmentDirections.actionNavExposureToNavExposureHelp(ExposureHelpType.PREVENTION)) }
earlier_exposures_container.setOnClickListener { navigate(R.id.action_nav_dashboard_to_nav_recent_exposures) }
}
private fun onNoRecentExposures() {
no_exposures_header.text = AppConfig.noEncounterHeader
no_exposures_body.text = AppConfig.noEncounterBody
no_exposures_group.show()
exposures_group.hide()
}
private fun onRecentExposures() {
exposures_group.show()
no_exposures_group.hide()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.exposure, menu)
super.onCreateOptionsMenu(menu, inflater)
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposure/ExposureVM.kt
================================================
package cz.covid19cz.erouska.ui.exposure
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.ext.daysSinceEpochToDateString
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.exposure.event.ExposuresCommandEvent
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ExposureVM @Inject constructor(private val exposureNotificationsRepo: ExposureNotificationsRepository, private val prefs : SharedPrefsRepository) :
BaseVM() {
val lastExposureDate = MutableLiveData()
fun checkExposures(demo: Boolean) {
if (!demo) {
viewModelScope.launch {
kotlin.runCatching {
exposureNotificationsRepo.getLastRiskyExposure()
}.onSuccess {
publish(
ExposuresCommandEvent(
if (it != null) {
lastExposureDate.value = it.daysSinceEpoch.daysSinceEpochToDateString()
ExposuresCommandEvent.Command.RECENT_EXPOSURE
} else {
ExposuresCommandEvent.Command.NO_RECENT_EXPOSURES
}
)
)
}.onFailure {
L.e(it)
}
}
} else {
lastExposureDate.value =
((System.currentTimeMillis() / 1000 / 60 / 60 / 24) - 3).toInt()
.daysSinceEpochToDateString()
publish(
ExposuresCommandEvent(ExposuresCommandEvent.Command.RECENT_EXPOSURE)
)
}
}
fun debugRecentExp() {
publish(ExposuresCommandEvent(ExposuresCommandEvent.Command.RECENT_EXPOSURE))
}
fun debugExp() {
publish(ExposuresCommandEvent(ExposuresCommandEvent.Command.NO_RECENT_EXPOSURES))
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposure/event/ExposuresEvent.kt
================================================
package cz.covid19cz.erouska.ui.exposure.event
import arch.event.LiveEvent
class ExposuresCommandEvent(val command: Command) : LiveEvent() {
enum class Command {
NO_RECENT_EXPOSURES,
RECENT_EXPOSURE
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/ExposureHelpFragment.kt
================================================
package cz.covid19cz.erouska.ui.exposurehelp
import android.os.Bundle
import androidx.navigation.fragment.navArgs
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentExposureHelpBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.exposurehelp.entity.ExposureHelpType
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ExposureHelpFragment : BaseFragment(
R.layout.fragment_exposure_help,
ExposureHelpVM::class
) {
val args: ExposureHelpFragmentArgs by navArgs()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableUpInToolbar(true, IconType.CLOSE)
when (args.type) {
ExposureHelpType.SYMPTOMS -> {
activity?.title = AppConfig.symptomsUITitle
viewModel.setData(AppConfig.symptomsContentJson)
}
ExposureHelpType.PREVENTION -> {
activity?.title = AppConfig.spreadPreventionUITitle
viewModel.setData(AppConfig.preventionContentJson, bottomTitle = true)
}
ExposureHelpType.EXPOSURE -> {
activity?.title = AppConfig.exposureHelpUITitle
viewModel.setData(AppConfig.exposureHelpContentJson)
}
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/ExposureHelpVM.kt
================================================
package cz.covid19cz.erouska.ui.exposurehelp
import androidx.databinding.ObservableArrayList
import arch.adapter.RecyclerLayoutStrategy
import com.google.gson.Gson
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.exposurehelp.entity.ExposureHelpData
import cz.covid19cz.erouska.ui.exposurehelp.entity.ExposureHelpItem
import cz.covid19cz.erouska.ui.exposurehelp.entity.ExposureHelpTitle
import java.lang.IllegalArgumentException
class ExposureHelpVM : BaseVM(){
val layoutStrategy = object: RecyclerLayoutStrategy {
override fun getLayoutId(item: Any): Int {
return when(item){
is ExposureHelpItem -> R.layout.item_exposure_help
is ExposureHelpTitle -> R.layout.item_exposure_help_title
else -> throw IllegalArgumentException("Missing layout mapping")
}
}
}
val items = ObservableArrayList()
fun setData(jsonData : String, bottomTitle : Boolean = false){
val data: ExposureHelpData = Gson().fromJson(jsonData, ExposureHelpData::class.java)
items.clear()
if (data.title != null && !bottomTitle){
items.add(ExposureHelpTitle(data.title))
}
items.addAll(data.items)
if (data.title != null && bottomTitle){
items.add(ExposureHelpTitle(data.title))
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/entity/ExposureHelpData.kt
================================================
package cz.covid19cz.erouska.ui.exposurehelp.entity
data class ExposureHelpData(
val title: String?,
val items: ArrayList
)
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/entity/ExposureHelpItem.kt
================================================
package cz.covid19cz.erouska.ui.exposurehelp.entity
data class ExposureHelpItem(
val iconUrl: String,
val label: String
)
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/entity/ExposureHelpTitle.kt
================================================
package cz.covid19cz.erouska.ui.exposurehelp.entity
class ExposureHelpTitle(val title : String)
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposurehelp/entity/ExposureHelpType.kt
================================================
package cz.covid19cz.erouska.ui.exposurehelp.entity
enum class ExposureHelpType {
SYMPTOMS, PREVENTION, EXPOSURE
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposureinfo/ExposureInfoFragment.kt
================================================
package cz.covid19cz.erouska.ui.exposureinfo
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import androidx.navigation.fragment.navArgs
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentExposureInfoBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ExposureInfoFragment : BaseFragment(R.layout.fragment_exposure_info, ExposureInfoVM::class) {
val args: ExposureInfoFragmentArgs by navArgs()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
viewModel.demo = args.demo
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true, IconType.CLOSE)
activity?.title = AppConfig.exposureUITitle
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.exposure, menu)
super.onCreateOptionsMenu(menu, inflater)
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/exposureinfo/ExposureInfoVM.kt
================================================
package cz.covid19cz.erouska.ui.exposureinfo
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.viewModelScope
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ExposureInfoVM @Inject constructor(private val exposureNotificationsRepo : ExposureNotificationsRepository, private val prefs : SharedPrefsRepository) : BaseVM(){
val date = MutableLiveData()
var demo = false
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate(){
viewModelScope.launch {
exposureNotificationsRepo.getLastRiskyExposure(demo)?.let {
date.value = exposureNotificationsRepo.getLastRiskyExposure(demo)?.getDateString()
prefs.setLastShownExposureInfo(it.daysSinceEpoch)
}
}
}
fun dismiss(){
navigate(ExposureInfoFragmentDirections.actionNavExposureInfoToNavExposure())
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/help/HelpFragment.kt
================================================
package cz.covid19cz.erouska.ui.help
import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentHelpBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.utils.SupportEmailGenerator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_help.*
import kotlinx.android.synthetic.main.search_toolbar.*
import javax.inject.Inject
@AndroidEntryPoint
class HelpFragment : BaseFragment(
R.layout.fragment_help,
HelpVM::class
) {
companion object {
private const val SCREEN_NAME = "Help"
}
private val args: HelpFragmentArgs by navArgs()
@Inject
internal lateinit var supportEmailGenerator: SupportEmailGenerator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.fillInHelp()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
args.fullscreen.let { fullscreen ->
enableUpInToolbar(
fullscreen,
if (fullscreen) {
IconType.CLOSE
} else {
IconType.UP
}
)
}
activity?.toolbar_search_view?.apply {
setQuery("", false)
setOnSearchClickListener {
viewModel.onSearchTapped()
}
isIconified = true
}
support_button.setOnClickListener {
supportEmailGenerator.sendSupportEmail(
requireActivity(),
lifecycleScope,
isError = false,
screenOrigin = SCREEN_NAME
)
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/help/HelpVM.kt
================================================
package cz.covid19cz.erouska.ui.help
import androidx.databinding.ObservableArrayList
import arch.adapter.RecyclerLayoutStrategy
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ui.about.AboutFragmentArgs
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.help.data.*
import cz.covid19cz.erouska.ui.helpcategory.HelpCategoryFragmentArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class HelpVM @Inject constructor() : BaseVM() {
val layoutStrategy = object : RecyclerLayoutStrategy {
override fun getLayoutId(item: Any): Int {
return when (item) {
is FaqCategory -> R.layout.item_help_faq_category
is AboutAppCategory -> R.layout.item_help_about_category
else -> R.layout.item_help_how_category
}
}
}
var items = ObservableArrayList()
fun fillInHelp() {
items.clear()
items.add(HowItWorksCategory())
items.addAll(AppConfig.helpJson.toFaqCategories())
items.add(AboutAppCategory())
}
fun onSearchTapped() = navigate(R.id.nav_help_search)
fun onItemClicked(category: Category) {
when (category) {
is FaqCategory -> {
navigate(
R.id.nav_help_category,
HelpCategoryFragmentArgs(category = category).toBundle()
)
}
is AboutAppCategory -> {
navigate(
R.id.nav_about,
AboutFragmentArgs(fullscreen = true).toBundle()
)
}
is HowItWorksCategory -> {
navigate(
R.id.nav_how_it_works
)
}
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/AboutAppCategory.kt
================================================
package cz.covid19cz.erouska.ui.help.data
class AboutAppCategory : Category
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/Category.kt
================================================
package cz.covid19cz.erouska.ui.help.data
interface Category
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/FaqCategory.kt
================================================
package cz.covid19cz.erouska.ui.help.data
import android.os.Parcelable
import com.google.gson.Gson
import com.google.gson.JsonParseException
import com.google.gson.reflect.TypeToken
import cz.covid19cz.erouska.utils.L
import kotlinx.android.parcel.Parcelize
import java.lang.reflect.Type
import java.util.*
@Parcelize
data class FaqCategory(
val title: String,
val subtitle: String,
val icon: String,
val questions: List
) : Category, Parcelable
fun String?.toFaqCategories(): List {
val categoryType: Type = object : TypeToken>() {}.type
return try {
val structuredQs: ArrayList? = Gson().fromJson(this, categoryType)
structuredQs.orEmpty()
} catch (ex1: JsonParseException) {
L.e(ex1)
Collections.emptyList()
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/HowToCategory.kt
================================================
package cz.covid19cz.erouska.ui.help.data
class HowItWorksCategory : Category
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/help/data/Question.kt
================================================
package cz.covid19cz.erouska.ui.help.data
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Question
constructor(
var question: String,
var answer: String
) : Parcelable
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/helpcategory/HelpCategoryFragment.kt
================================================
package cz.covid19cz.erouska.ui.helpcategory
import android.os.Bundle
import android.view.View
import androidx.navigation.fragment.navArgs
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentHelpCategoryBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.search_toolbar.*
@AndroidEntryPoint
class HelpCategoryFragment : BaseFragment(
R.layout.fragment_help_category,
HelpCategoryVM::class
) {
private val args: HelpCategoryFragmentArgs by navArgs()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.fillInQuestions(args.category.questions)
viewModel.categoryTitle = args.category.title
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true, IconType.UP)
activity?.title = viewModel.categoryTitle
activity?.toolbar_search_view?.apply {
setQuery("", false)
setOnSearchClickListener {
viewModel.onSearchTapped()
}
isIconified = true
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/helpcategory/HelpCategoryVM.kt
================================================
package cz.covid19cz.erouska.ui.helpcategory
import androidx.databinding.ObservableArrayList
import arch.adapter.RecyclerLayoutStrategy
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.help.data.Question
import cz.covid19cz.erouska.ui.helpquestion.HelpQuestionFragmentArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class HelpCategoryVM @Inject constructor() : BaseVM() {
lateinit var categoryTitle: String
val layoutStrategy = object : RecyclerLayoutStrategy {
override fun getLayoutId(item: Any): Int {
return R.layout.item_help_question
}
}
var items = ObservableArrayList()
fun onItemClicked(question: Question) {
navigate(R.id.nav_help_question, HelpQuestionFragmentArgs(question = question, category = categoryTitle).toBundle())
}
fun fillInQuestions(questions: List) {
items.addAll(questions)
}
fun onSearchTapped() = navigate(R.id.nav_help_search)
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/helpquestion/HelpQuestionFragment.kt
================================================
package cz.covid19cz.erouska.ui.helpquestion
import android.os.Bundle
import android.view.View
import androidx.navigation.fragment.navArgs
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentHelpQuestionBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.utils.Markdown
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_help_question.*
import javax.inject.Inject
@AndroidEntryPoint
class HelpQuestionFragment : BaseFragment(
R.layout.fragment_help_question,
HelpQuestionVM::class
) {
@Inject
lateinit var markdown: Markdown
private val args: HelpQuestionFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true, IconType.UP)
activity?.title = args.category
markdown.show(question, args.question.question)
markdown.show(answer, args.question.answer)
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/helpquestion/HelpQuestionVM.kt
================================================
package cz.covid19cz.erouska.ui.helpquestion
import androidx.databinding.ObservableArrayList
import arch.adapter.RecyclerLayoutStrategy
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.help.data.Question
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class HelpQuestionVM @Inject constructor() : BaseVM() {
val layoutStrategy = object : RecyclerLayoutStrategy {
override fun getLayoutId(item: Any): Int {
return R.layout.item_help_question
}
}
var items = ObservableArrayList()
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/helpsearch/HelpSearchFragment.kt
================================================
package cz.covid19cz.erouska.ui.helpsearch
import android.app.SearchManager
import android.content.Context
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.SearchView
import androidx.lifecycle.Observer
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentHelpSearchBinding
import cz.covid19cz.erouska.ext.attachKeyboardController
import cz.covid19cz.erouska.ext.show
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.utils.Markdown
import cz.covid19cz.erouska.utils.showOrHide
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_help_search.*
import kotlinx.android.synthetic.main.search_toolbar.*
import javax.inject.Inject
@AndroidEntryPoint
class HelpSearchFragment : BaseFragment(
R.layout.fragment_help_search,
HelpSearchVM::class
) {
@Inject
internal lateinit var markdown: Markdown
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
goBack()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.fillQuestions()
viewModel.queryData.observe(this,
Observer {
when {
it.isEmpty() -> empty_text_view.text = ""
it.length < viewModel.minQueryLength -> empty_text_view.setText(R.string.help_type_more)
else -> empty_text_view.setText(R.string.help_no_results)
}
})
viewModel.searchEmpty.observe(this,
Observer { isEmpty ->
help_categories.showOrHide(!isEmpty)
empty_text_view.showOrHide(isEmpty)
empty_image_view.showOrHide(isEmpty)
})
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true, IconType.UP)
// Associate searchable configuration with the SearchView
val searchManager = activity?.getSystemService(Context.SEARCH_SERVICE) as SearchManager
activity?.toolbar_search_view?.apply {
attachKeyboardController()
setSearchableInfo(searchManager.getSearchableInfo(activity?.componentName))
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(query: String?): Boolean {
viewModel.searchQuery(query)
return true
}
})
setOnCloseListener {
goBack()
true
}
show()
requestFocus()
}
}
override fun onBackPressed(): Boolean {
goBack()
return true
}
private fun removeSearchViewCallbacks() {
activity?.toolbar_search_view?.apply {
setOnCloseListener(null)
setOnQueryTextListener(null)
}
}
private fun goBack() {
removeSearchViewCallbacks()
collapseSearchView()
navController().navigateUp()
}
private fun collapseSearchView() {
activity?.toolbar_search_view?.apply {
setQuery("", false)
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/helpsearch/HelpSearchVM.kt
================================================
package cz.covid19cz.erouska.ui.helpsearch
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.viewModelScope
import arch.adapter.RecyclerLayoutStrategy
import arch.livedata.SafeMutableLiveData
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.help.data.FaqCategory
import cz.covid19cz.erouska.ui.help.data.toFaqCategories
import cz.covid19cz.erouska.ui.helpsearch.data.SearchableQuestion
import cz.covid19cz.erouska.utils.L
import cz.covid19cz.erouska.utils.Markdown
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import org.apache.commons.lang3.StringUtils
import java.util.regex.Pattern
import javax.inject.Inject
@HiltViewModel
class HelpSearchVM @Inject constructor(
val markdown: Markdown
) : BaseVM() {
val layoutStrategy = object : RecyclerLayoutStrategy {
override fun getLayoutId(item: Any): Int {
return R.layout.item_search
}
}
val searchResult = ObservableArrayList()
val searchEmpty = SafeMutableLiveData(searchResult.isEmpty())
val content = ArrayList()
val queryData = SafeMutableLiveData("")
val minQueryLength = 2
private var searchJob: Job? = null
fun fillQuestions() {
val structuredQs: List = AppConfig.helpJson.toFaqCategories()
val allQuestions = structuredQs.map {
it.questions.map { question ->
SearchableQuestion(it.title, question.question, question.answer)
}
}.flatten()
content.clear()
content.addAll(allQuestions)
}
fun searchQuery(query: String?) {
searchJob?.cancel()
this.queryData.value = query?.trim() ?: ""
if (queryData.value.length >= minQueryLength) {
searchJob = viewModelScope.launch {
startSearch()
}
} else {
resetSearchResult()
}
}
private suspend fun updateSearchResult(searchResults: List) =
withContext(Dispatchers.Main) {
searchResult.clear()
searchResult.addAll(searchResults)
updateSearchResultCount()
}
private fun resetSearchResult() {
searchResult.clear()
updateSearchResultCount()
}
private fun updateSearchResultCount() {
searchEmpty.postValue(searchResult.isEmpty())
}
private suspend fun startSearch() = withContext(Dispatchers.Default) {
val result = searchQueryInText()
if (isActive) {
updateSearchResult(result)
} else {
L.d("Job was already cancelled, not going to display results")
}
}
private fun searchQueryInText(): List {
val tempSearchResult = mutableListOf()
content.forEach { question ->
val newQ = highlightSearchedText(question.question)
val newA = highlightSearchedText(question.answer)
if (newQ.second || newA.second) {
val q = SearchableQuestion(question.category)
q.answer = newA.first
q.question = newQ.first
tempSearchResult.add(q)
}
}
return tempSearchResult
}
private fun highlightSearchedText(originalText: String): Pair {
val pattern = StringUtils.stripAccents(queryData.value)
val r = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE)
val searchedText = StringUtils.stripAccents(originalText)
var printedText = originalText
val m = r.matcher(searchedText)
val replaceList = arrayListOf()
while (m.find()) {
replaceList.add(printedText.substring(m.start(0), m.end(0)))
}
val searchMatches = replaceList.distinct()
for (replaceString in searchMatches) {
printedText = printedText.replace(
replaceString,
"${Markdown.doubleSearchChar}${replaceString}${Markdown.doubleSearchChar}"
)
}
return Pair(printedText, searchMatches.isNotEmpty())
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/helpsearch/data/SearchableQuestion.kt
================================================
package cz.covid19cz.erouska.ui.helpsearch.data
data class SearchableQuestion(
val category: String,
var question: String = "",
var answer: String = ""
)
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/helpsearch/ui/SearchableItem.kt
================================================
package cz.covid19cz.erouska.ui.helpsearch.ui
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.utils.Markdown
import kotlinx.android.synthetic.main.item_search_layout.view.*
import javax.inject.Inject
class SearchableItem : LinearLayout {
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, R.layout.item_search_layout, this)
}
@Inject
lateinit var markdown: Markdown
var search_category: String? = null
set(value) {
field = value
category.text = value
}
var search_question: String? = null
set(value) {
value?.let {
markdown.show(question, it)
}
field = value
}
var search_answer: String? = null
set(value) {
value?.let {
markdown.show(answer, it)
}
field = value
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/how/HowItWorksFragment.kt
================================================
package cz.covid19cz.erouska.ui.how
import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentExposureBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.how.event.HowItWorksEvent
import cz.covid19cz.erouska.utils.SupportEmailGenerator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_how_it_works.*
import javax.inject.Inject
@AndroidEntryPoint
class HowItWorksFragment : BaseFragment(
R.layout.fragment_how_it_works,
HowItWorksVM::class
) {
companion object {
private const val SCREEN_NAME = "How It Works"
}
@Inject
internal lateinit var supportEmailGenerator: SupportEmailGenerator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true, IconType.CLOSE)
activity?.title = AppConfig.howItWorksUITitle
how_it_works_eval_content.text = AppConfig.howItWorksEvalContent
subscribe(HowItWorksEvent::class) {
when (it.command) {
HowItWorksEvent.Command.WRITE_EMAIL -> onWriteEmail()
HowItWorksEvent.Command.CLOSE -> onClose()
}
}
}
private fun onWriteEmail() {
supportEmailGenerator.sendSupportEmail(
requireActivity(),
lifecycleScope,
recipient = AppConfig.supportEmail,
isError = false,
screenOrigin = SCREEN_NAME
)
}
private fun onClose() {
navController().navigateUp()
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/how/HowItWorksVM.kt
================================================
package cz.covid19cz.erouska.ui.how
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.OnLifecycleEvent
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.how.event.HowItWorksEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class HowItWorksVM @Inject constructor(private val prefs: SharedPrefsRepository) :
BaseVM() {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
prefs.setHowItWorksShown()
}
fun writeEmail() {
publish(HowItWorksEvent(HowItWorksEvent.Command.WRITE_EMAIL))
}
fun close() {
publish(HowItWorksEvent(HowItWorksEvent.Command.CLOSE))
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/how/event/HowItWorksEvent.kt
================================================
package cz.covid19cz.erouska.ui.how.event
import arch.event.LiveEvent
class HowItWorksEvent(val command: Command) : LiveEvent() {
enum class Command {
WRITE_EMAIL,
CLOSE
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/main/MainActivity.kt
================================================
package cz.covid19cz.erouska.ui.main
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.ActivityRagnarokBinding
import cz.covid19cz.erouska.ui.base.BaseActivity
import cz.covid19cz.erouska.ui.base.UrlEvent
import cz.covid19cz.erouska.ui.ragnarok.RagnarokVM
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_ragnarok.*
@AndroidEntryPoint
class MainActivity : BaseActivity(R.layout.activity_ragnarok, RagnarokVM::class) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
subscribe(UrlEvent::class){
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it.url)))
}
buttonMoreInfo.setOnLongClickListener {
Toast.makeText(this,"eRagnarök", Toast.LENGTH_LONG).show()
true
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/main/MainActivityOld.kt
================================================
package cz.covid19cz.erouska.ui.main
import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.findNavController
import androidx.navigation.ui.NavigationUI
import com.google.android.play.core.review.ReviewInfo
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.ActivityMainBinding
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.ui.base.BaseActivity
import cz.covid19cz.erouska.ui.exposurehelp.ExposureHelpFragmentArgs
import cz.covid19cz.erouska.ui.exposurehelp.entity.ExposureHelpType
import cz.covid19cz.erouska.utils.Analytics
import cz.covid19cz.erouska.utils.Analytics.KEY_CONTACTS
import cz.covid19cz.erouska.utils.Analytics.KEY_HELP
import cz.covid19cz.erouska.utils.Analytics.KEY_HOME
import cz.covid19cz.erouska.utils.Analytics.KEY_NEWS
import cz.covid19cz.erouska.utils.CustomTabHelper
import cz.covid19cz.erouska.utils.L
import cz.covid19cz.erouska.utils.showOrHide
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.search_toolbar.*
import javax.inject.Inject
@AndroidEntryPoint
class MainActivityOld :
BaseActivity(R.layout.activity_main, MainVM::class) {
@Inject
internal lateinit var customTabHelper: CustomTabHelper
@Inject
internal lateinit var prefs : SharedPrefsRepository
private lateinit var reviewManager: ReviewManager
var reviewInfo: ReviewInfo? = null
private val fragmentsWithSearch = arrayListOf(
R.id.nav_help,
R.id.nav_help_search,
R.id.nav_help_category
)
private val customTabsConnection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(
name: ComponentName,
client: CustomTabsClient
) {
connectedToCustomTabsService = true
client.warmup(0)
}
override fun onServiceDisconnected(name: ComponentName?) {
connectedToCustomTabsService = false
}
}
private var connectedToCustomTabsService = false
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.AppTheme)
super.onCreate(savedInstanceState)
setSupportActionBar(toolbar)
findNavController(R.id.nav_host_fragment).let {
bottom_navigation.setOnNavigationItemSelectedListener { item ->
val options = NavOptions.Builder().setPopUpTo(R.id.nav_graph, false).build()
navigate(item.itemId, navOptions = options)
logTabClickEventToAnalytics(item)
true
}
it.addOnDestinationChangedListener { _, destination, arguments ->
updateTitle(destination)
toolbar_search_view.showOrHide(fragmentsWithSearch.contains(destination.id))
updateBottomNavigation(destination, arguments)
}
}
viewModel.serviceRunning.observe(this, Observer { isRunning ->
ContextCompat.getColor(
this,
if (isRunning) R.color.green else R.color.red
).let {
bottom_navigation.getOrCreateBadge(R.id.nav_dashboard).backgroundColor = it
}
})
}
private fun logTabClickEventToAnalytics(item: MenuItem) {
val event = when (item.itemId) {
R.id.nav_dashboard -> KEY_HOME
R.id.nav_my_data -> KEY_NEWS
R.id.nav_contacts -> KEY_CONTACTS
R.id.nav_help -> KEY_HELP
else -> throw IllegalStateException("analytics event for ${item.title} is not mapped")
}
Analytics.logEvent(this, event)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.nav_about -> {
false
}
R.id.nav_help -> {
navigate(R.id.nav_help, Bundle().apply { putBoolean("fullscreen", true) })
true
}
R.id.nav_exposure_help -> {
navigate(
R.id.nav_exposure_help,
ExposureHelpFragmentArgs(ExposureHelpType.EXPOSURE).toBundle()
)
true
}
else -> {
NavigationUI.onNavDestinationSelected(
item,
findNavController(R.id.nav_host_fragment)
) || super.onOptionsItemSelected(item)
}
}
}
override fun onStart() {
super.onStart()
customTabHelper.chromePackageName?.let {
CustomTabsClient.bindCustomTabsService(this, it, customTabsConnection)
}
prefs.setAppVisitedTimestamp()
}
override fun onStop() {
if (connectedToCustomTabsService) {
unbindService(customTabsConnection)
connectedToCustomTabsService = false
}
// saving timestamp in onStop so that it eliminates cases when the feature in question
// took place while being in the app
prefs.setAppVisitedTimestamp()
super.onStop()
}
/** Call this on a Fragment which might show reviews, ideally in onViewCreated **/
fun initReviews() {
reviewManager = ReviewManagerFactory.create(this)
reviewManager.requestReviewFlow().addOnCompleteListener { request ->
if (request.isSuccessful) {
reviewInfo = request.result
} else {
L.e(request.exception)
}
}
}
/** Call this when asking for review **/
fun askForReview() {
if (reviewInfo != null) {
reviewManager.launchReviewFlow(this, reviewInfo!!).addOnFailureListener {
L.e(it)
}.addOnCompleteListener { L.i("Review success") }
}
}
private fun updateTitle(destination: NavDestination) {
if (destination.label != null) {
title = destination.label
} else {
setTitle(R.string.app_name)
}
}
private fun updateBottomNavigation(
destination: NavDestination,
arguments: Bundle?
) {
bottom_navigation.visibility =
if (destination.arguments["fullscreen"]?.defaultValue == true
|| arguments?.getBoolean("fullscreen") == true
) {
GONE
} else {
VISIBLE
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
L.d("$requestCode")
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/main/MainVM.kt
================================================
package cz.covid19cz.erouska.ui.main
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.OnLifecycleEvent
import arch.livedata.SafeMutableLiveData
import com.google.firebase.auth.FirebaseAuth
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ui.base.BaseVM
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MainVM @Inject constructor(): BaseVM() {
val serviceRunning = SafeMutableLiveData(false)
private val auth: FirebaseAuth = FirebaseAuth.getInstance()
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
if (auth.currentUser == null) {
setNavigationGraph(R.navigation.nav_graph, R.id.nav_welcome_fragment)
} else {
setNavigationGraph(R.navigation.nav_graph, R.id.nav_dashboard)
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/mydata/CaseItemView.kt
================================================
package cz.covid19cz.erouska.ui.mydata
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.ext.show
import kotlinx.android.synthetic.main.view_data_item.view.*
class CaseItemView : 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, R.layout.view_data_item, this)
}
var case_title: String? = null
set(value) {
field = value
title_text.text = value
}
var case_subtitle: String? = null
set(value) {
field = value
subtitle_text.text = value
subtitle_text.show()
}
var case_icon: Drawable? = null
set(value) {
field = value
value?.let {
icon.setImageDrawable(it)
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/mydata/MyDataFragment.kt
================================================
package cz.covid19cz.erouska.ui.mydata
import android.os.Bundle
import android.view.View
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentMyDataBinding
import cz.covid19cz.erouska.ext.showWeb
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.utils.Analytics
import cz.covid19cz.erouska.utils.Analytics.KEY_CURRENT_MEASURES
import cz.covid19cz.erouska.utils.CustomTabHelper
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_my_data.*
import javax.inject.Inject
@AndroidEntryPoint
class MyDataFragment :
BaseFragment(R.layout.fragment_my_data, MyDataVM::class) {
@Inject
internal lateinit var customTabHelper: CustomTabHelper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!AppConfig.updateNewsOnRequest) {
refresh_container.isEnabled = false
}
measures_text.setOnClickListener {
openMeasures()
Analytics.logEvent(requireContext(), KEY_CURRENT_MEASURES)
}
}
private fun openMeasures() {
showWeb(viewModel.getMeasuresUrl(), customTabHelper)
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/mydata/MyDataVM.kt
================================================
package cz.covid19cz.erouska.ui.mydata
import android.text.format.DateUtils
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.viewModelScope
import arch.livedata.SafeMutableLiveData
import arch.utils.safeLet
import com.google.android.gms.common.api.ApiException
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.net.FirebaseFunctionsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltViewModel
class MyDataVM @Inject constructor(
private val firebaseFunctionsRepository: FirebaseFunctionsRepository,
val prefs: SharedPrefsRepository
) : BaseVM() {
companion object {
const val LAST_UPDATE_UI_FORMAT = "d. M. yyyy" // date format used in UI
const val LAST_UPDATE_API_FORMAT = "yyyyMMdd" // date format returned from API
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
if (AppConfig.updateNewsOnRequest || (!AppConfig.updateNewsOnRequest && !DateUtils.isToday(prefs.getLastStatsUpdate()))) {
getStats()
}
if (AppConfig.updateNewsOnRequest || (!AppConfig.updateNewsOnRequest && !DateUtils.isToday(prefs.getLastMetricsUpdate()))) {
getMetrics()
}
}
fun onRefresh() {
getMetrics()
getStats()
}
val isLoading = SafeMutableLiveData(false)
// vaccinations
val vaccinationsTotal = SafeMutableLiveData(prefs.getVaccinationsTotal())
val vaccinationsIncrease = SafeMutableLiveData(prefs.getVaccinationsIncrease())
val vaccinationsIncreaseDate = if (prefs.getVaccinationsIncreaseDate() == 0L) {
SafeMutableLiveData("-")
} else {
SafeMutableLiveData(
SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(Date(prefs.getVaccinationsIncreaseDate()))
)
}
val firstDoseTotal = SafeMutableLiveData(prefs.getFirstDoseTotal())
val firstDoseIncrease = SafeMutableLiveData(prefs.getFirstDoseIncrease())
val secondDoseTotal = SafeMutableLiveData(prefs.getSecondDoseTotal())
val secondDoseIncrease = SafeMutableLiveData(prefs.getSecondDoseIncrease())
val dailyDosesDate = if (prefs.getDailyDosesDate() == 0L) {
SafeMutableLiveData("-")
} else {
SafeMutableLiveData(
SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(Date(prefs.getDailyDosesDate()))
)
}
// stats
val testsTotal = SafeMutableLiveData(prefs.getTestsTotal())
val testsIncrease = SafeMutableLiveData(prefs.getTestsIncrease())
val testsIncreaseDate = if (prefs.getTestsIncreaseDate() == 0L) {
SafeMutableLiveData("-")
} else {
SafeMutableLiveData(
SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(Date(prefs.getTestsIncreaseDate()))
)
}
val antigenTestsTotal = SafeMutableLiveData(prefs.getAntigenTestsTotal())
val antigenTestsIncrease = SafeMutableLiveData(prefs.getAntigenTestsIncrease())
val antigenTestsIncreaseDate = if (prefs.getAntigenTestsIncreaseDate() == 0L) {
SafeMutableLiveData("-")
} else {
SafeMutableLiveData(
SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(Date(prefs.getAntigenTestsIncreaseDate()))
)
}
val confirmedCasesTotal = SafeMutableLiveData(prefs.getConfirmedCasesTotal())
val confirmedCasesIncrease = SafeMutableLiveData(prefs.getConfirmedCasesIncrease())
val confirmedCasesIncreaseDate= if (prefs.getConfirmedCasesIncreaseDate() == 0L) {
SafeMutableLiveData("-")
} else {
SafeMutableLiveData(
SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(Date(prefs.getConfirmedCasesIncreaseDate()))
)
}
val activeCasesTotal = SafeMutableLiveData(prefs.getActiveCasesTotal())
val curedTotal = SafeMutableLiveData(prefs.getCuredTotal())
val deceasedTotal = SafeMutableLiveData(prefs.getDeceasedTotal())
val currentlyHospitalizedTotal = SafeMutableLiveData(prefs.getCurrentlyHospitalizedTotal())
// metrics
val activationsTotal = SafeMutableLiveData(prefs.getActivationsTotal())
val activationsYesterday = SafeMutableLiveData(prefs.getActivationsYesterday())
val keyPublishersTotal = SafeMutableLiveData(prefs.getKeyPublishersTotal())
val keyPublishersYesterday = SafeMutableLiveData(prefs.getKeyPublishersYesterday())
val notificationsTotal = SafeMutableLiveData(prefs.getNotificationsTotal())
val notificationsYesterday = SafeMutableLiveData(prefs.getNotificationsYesterday())
var lastMetricsIncreaseDate = if (prefs.getLastMetricsUpdate() == 0L) {
SafeMutableLiveData("-")
} else {
SafeMutableLiveData(
SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(Date(prefs.getLastMetricsUpdate() - TimeUnit.DAYS.toMillis(1))) // increase day = day before day of last update
)
}
fun getMeasuresUrl() = AppConfig.currentMeasuresUrl
private fun getStats(date: String? = null) {
isLoading.value = true
viewModelScope.launch {
kotlin.runCatching {
return@runCatching firebaseFunctionsRepository.getStats(date)
}.onSuccess { response ->
L.d(response.toString())
isLoading.value = false
safeLet(response.testsTotal,
response.testsIncrease,
response.testsIncreaseDate) { total, increase, increaseDate ->
testsTotal.value = total
testsIncrease.value = increase
prefs.setTestsTotal(total)
prefs.setTestsIncrease(increase)
val lastUpdateDate = SimpleDateFormat(
LAST_UPDATE_API_FORMAT,
Locale.getDefault()
).parse(increaseDate)
lastUpdateDate?.time?.let { lastUpdateMillis ->
prefs.setTestsIncreaseDate(lastUpdateMillis)
testsIncreaseDate.value = SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(
Date(lastUpdateMillis)
)
}
}
safeLet(response.antigenTestsTotal,
response.antigenTestsIncrease,
response.antigenTestsIncreaseDate) { total, increase, increaseDate ->
antigenTestsTotal.value = total
antigenTestsIncrease.value = increase
prefs.setAntigenTestsTotal(total)
prefs.setAntigenTestsIncrease(increase)
val lastUpdateDate = SimpleDateFormat(
LAST_UPDATE_API_FORMAT,
Locale.getDefault()
).parse(increaseDate)
lastUpdateDate?.time?.let { lastUpdateMillis ->
prefs.setAntigenTestsIncreaseDate(lastUpdateMillis)
antigenTestsIncreaseDate.value = SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(
Date(lastUpdateMillis)
)
}
}
safeLet(response.vaccinationsTotal,
response.vaccinationsIncrease,
response.vaccinationsIncreaseDate) { total, increase, increaseDate ->
vaccinationsTotal.value = total
vaccinationsIncrease.value = increase
prefs.setVaccinationsTotal(total)
prefs.setVaccinationsIncrease(increase)
val lastUpdateDate = SimpleDateFormat(
LAST_UPDATE_API_FORMAT,
Locale.getDefault()
).parse(increaseDate)
lastUpdateDate?.time?.let { lastUpdateMillis ->
prefs.setVaccinationsIncreaseDate(lastUpdateMillis)
vaccinationsIncreaseDate.value = SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(
Date(lastUpdateMillis)
)
}
}
safeLet(response.vaccinationsTotalFirstDose,
response.vaccinationsDailyFirstDose) { total, increase ->
firstDoseTotal.value = total
firstDoseIncrease.value = increase
prefs.setFirstDoseTotal(total)
prefs.setFirstDoseIncrease(increase)
}
safeLet(response.vaccinationsTotalSecondDose,
response.vaccinationsDailySecondDose) { total, increase ->
secondDoseTotal.value = total
secondDoseIncrease.value = increase
prefs.setSecondDoseTotal(total)
prefs.setSecondDoseIncrease(increase)
}
response.vaccinationsDailyDosesDate?.let { increaseDate ->
val lastUpdateDate = SimpleDateFormat(
LAST_UPDATE_API_FORMAT,
Locale.getDefault()
).parse(increaseDate)
lastUpdateDate?.time?.let { lastUpdateMillis ->
prefs.setDailyDosesDate(lastUpdateMillis)
dailyDosesDate.value = SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(
Date(lastUpdateMillis)
)
}
}
safeLet(
response.confirmedCasesTotal,
response.confirmedCasesIncrease,
response.confirmedCasesIncreaseDate,
) { total, increase, increaseDate ->
confirmedCasesTotal.value = total
confirmedCasesIncrease.value = increase
prefs.setConfirmedCasesTotal(total)
prefs.setConfirmedCasesIncrease(increase)
val lastUpdateDate = SimpleDateFormat(
LAST_UPDATE_API_FORMAT,
Locale.getDefault()
).parse(increaseDate)
lastUpdateDate?.time?.let { lastUpdateMillis ->
prefs.setConfirmedCasesIncreaseDate(lastUpdateMillis)
confirmedCasesIncreaseDate.value = SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(
Date(lastUpdateMillis)
)
}
}
response.activeCasesTotal?.let { total ->
activeCasesTotal.value = total
prefs.setActiveCasesTotal(total)
}
response.curedTotal?.let { total ->
curedTotal.value = total
prefs.setCuredTotal(total)
}
response.deceasedTotal?.let { total ->
deceasedTotal.value = total
prefs.setDeceasedTotal(total)
}
response.currentlyHospitalizedTotal?.let { total ->
currentlyHospitalizedTotal.value = total
prefs.setCurrentlyHospitalizedTotal(total)
}
response.date?.let {
val lastStatsUpdate = SimpleDateFormat(
LAST_UPDATE_API_FORMAT,
Locale.getDefault()
).parse(response.date)
lastStatsUpdate?.time?.let { lastUpdateMillis ->
prefs.setLastStatsUpdate(lastUpdateMillis)
}
}
}.onFailure {
isLoading.value = false
if (it is ApiException) {
L.e(it.status.toString() + " " + it.message)
} else {
L.e(it)
}
}
}
}
private fun getMetrics() {
isLoading.value = true
viewModelScope.launch {
kotlin.runCatching {
return@runCatching firebaseFunctionsRepository.getDownloadMetrics()
}.onSuccess { response ->
L.d(response.toString())
isLoading.value = false
safeLet(
response.activationsTotal,
response.activationsYesterday
) { total, yesterday ->
activationsTotal.value = total
activationsYesterday.value = yesterday
prefs.setActivationsTotal(total)
prefs.setActivationsYesterday(yesterday)
}
safeLet(
response.keyPublishersTotal,
response.keyPublishersYesterday
) { total, yesterday ->
keyPublishersTotal.value = total
keyPublishersYesterday.value = yesterday
prefs.setKeyPublishersTotal(total)
prefs.setKeyPublishersYesterday(yesterday)
}
safeLet(
response.notificationsTotal,
response.notificationsYesterday
) { total, yesterday ->
notificationsTotal.value = total
notificationsYesterday.value = yesterday
prefs.setNotificationsTotal(total)
prefs.setNotificationsYesterday(yesterday)
}
response.date?.let {
val lastMetricsUpdate = SimpleDateFormat(
LAST_UPDATE_API_FORMAT,
Locale.getDefault()
).parse(response.date)
lastMetricsUpdate?.time?.let { lastUpdateMillis ->
prefs.setLastMetricsUpdate(lastUpdateMillis)
val lastMetricsIncreaseMillis = lastUpdateMillis - TimeUnit.DAYS.toMillis(1) // increase day = day before day of last update
lastMetricsIncreaseDate.value = SimpleDateFormat(
LAST_UPDATE_UI_FORMAT,
Locale.getDefault()
).format(
Date(lastMetricsIncreaseMillis)
)
}
}
}.onFailure {
isLoading.value = false
if (it is ApiException) {
L.e(it.status.toString() + " " + it.message)
} else {
L.e(it)
}
}
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/noverificationcode/NoVerificationCodeFragment.kt
================================================
package cz.covid19cz.erouska.ui.noverificationcode
import android.os.Bundle
import android.view.View
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentNoVerificationCodeBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.noverificationcode.event.WriteEmailEvent
import cz.covid19cz.erouska.utils.SupportEmailGenerator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class NoVerificationCodeFragment : BaseFragment(
R.layout.fragment_no_verification_code,
NoVerificationCodeVM::class
) {
@Inject
internal lateinit var supportEmailGenerator: SupportEmailGenerator
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true, IconType.CLOSE)
subscribe(WriteEmailEvent::class) {
onWriteEmail()
}
with(binding) {
noVerificationCodeBody.text = getString(R.string.no_verification_body, AppConfig.supportEmail)
noVerificationCodeCaption.text = getString(R.string.no_verification_caption, AppConfig.supportEmail)
}
}
private fun onWriteEmail() {
binding.emailButton.setOnClickListener {
supportEmailGenerator.sendVerificationEmail(requireActivity())
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/noverificationcode/NoVerificationCodeVM.kt
================================================
package cz.covid19cz.erouska.ui.noverificationcode
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.noverificationcode.event.WriteEmailEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class NoVerificationCodeVM @Inject constructor() :
BaseVM() {
fun writeEmail() {
publish(WriteEmailEvent())
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/noverificationcode/event/NoVerificationCodeEvent.kt
================================================
package cz.covid19cz.erouska.ui.noverificationcode.event
import arch.event.LiveEvent
class WriteEmailEvent : LiveEvent()
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/permissions/BasePermissionsFragment.kt
================================================
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/publishsuccess/PublishSuccessFragment.kt
================================================
package cz.covid19cz.erouska.ui.publishsuccess
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentPublishSuccessBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
class PublishSuccessFragment : BaseFragment(R.layout.fragment_publish_success, PublishSuccessVM::class)
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/publishsuccess/PublishSuccessVM.kt
================================================
package cz.covid19cz.erouska.ui.publishsuccess
import cz.covid19cz.erouska.ui.base.BaseVM
class PublishSuccessVM : BaseVM() {
val enoughKeys : Boolean = true
fun close(){
navigate(PublishSuccessFragmentDirections.actionNavPublishSuccessToNavDashboard())
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/ragnarok/RagnarokVM.kt
================================================
package cz.covid19cz.erouska.ui.ragnarok
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.viewModelScope
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.base.UrlEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class RagnarokVM @Inject constructor(private val enRepository : ExposureNotificationsRepository) : BaseVM() {
val headline = MutableLiveData(AppConfig.ragnarokHeadline)
val body = MutableLiveData(AppConfig.ragnarokBody)
fun showMoreInfo(){
publish(UrlEvent(AppConfig.ragnarokMoreInfo))
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate(){
viewModelScope.launch {
kotlin.runCatching {
enRepository.stop()
}
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/recentexposures/RecentExposuresFragment.kt
================================================
package cz.covid19cz.erouska.ui.recentexposures
import android.os.Bundle
import android.view.View
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentRecentExposuresBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class RecentExposuresFragment : BaseFragment(
R.layout.fragment_recent_exposures,
RecentExposuresVM::class
) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(true)
activity?.title = AppConfig.recentExposuresUITitle
viewModel.loadExposures()
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/recentexposures/RecentExposuresVM.kt
================================================
package cz.covid19cz.erouska.ui.recentexposures
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.viewModelScope
import arch.adapter.RecyclerLayoutStrategy
import arch.viewmodel.BaseArchViewModel
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.db.DailySummaryEntity
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.ui.recentexposures.entity.RecentExposureGroupHeaderItem
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.threeten.bp.LocalDate
import java.lang.IllegalArgumentException
import javax.inject.Inject
@HiltViewModel
class RecentExposuresVM @Inject constructor(
private val exposureNotificationsRepo: ExposureNotificationsRepository
) : BaseArchViewModel() {
val layoutStrategy = object: RecyclerLayoutStrategy{
override fun getLayoutId(item: Any): Int {
return when(item){
is RecentExposureGroupHeaderItem -> R.layout.item_recent_exposure_group_header
is DailySummaryEntity -> R.layout.item_recent_exposure
else -> throw IllegalArgumentException("Missing layout mapping")
}
}
}
val items = ObservableArrayList()
fun loadExposures(demo: Boolean = false) {
items.clear()
if (!demo) {
viewModelScope.launch(Dispatchers.IO) {
kotlin.runCatching {
exposureNotificationsRepo.getDailySummariesFromDbByImportDate()
}.onSuccess { dailySummaries ->
viewModelScope.launch(Dispatchers.Main) {
var previousTimestamp = -1L
dailySummaries.forEachIndexed { _, item ->
if (previousTimestamp != item.importTimestamp){
items.add(RecentExposureGroupHeaderItem(item.importTimestamp))
previousTimestamp = item.importTimestamp
}
items.add(item)
}
}
}.onFailure {
L.e(it)
}
}
} else {
val now = LocalDate.now()
items.addAll(
listOf(
// old exposures
DailySummaryEntity(
LocalDate.of(2019, 12, 28).toEpochDay().toInt(),
1000.0,
1000.0,
1000.0,
0,
false,
false
),
DailySummaryEntity(
LocalDate.of(2019, 3, 20).toEpochDay().toInt(),
1000.0,
1000.0,
1000.0,
0,
false,
false
),
DailySummaryEntity(
LocalDate.of(2019, 5, 14).toEpochDay().toInt(),
1000.0,
1000.0,
1000.0,
0,
false,
false
),
DailySummaryEntity(
LocalDate.of(2019, 8, 5).toEpochDay().toInt(),
1000.0,
1000.0,
1000.0,
0,
false,
false
),
// very recent exposures
DailySummaryEntity(
now.minusDays(10).toEpochDay().toInt(),
1000.0,
1000.0,
1000.0,
0,
false,
false
),
DailySummaryEntity(
now.minusDays(5).toEpochDay().toInt(),
1000.0,
1000.0,
1000.0,
0,
false,
false
),
DailySummaryEntity(
now.minusDays(12).toEpochDay().toInt(),
1000.0,
1000.0,
1000.0,
0,
false,
false
)
)
)
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/recentexposures/entity/RecentExposureGroupHeaderItem.kt
================================================
package cz.covid19cz.erouska.ui.recentexposures.entity
import cz.covid19cz.erouska.ext.timestampToDate
import cz.covid19cz.erouska.ext.timestampToTime
class RecentExposureGroupHeaderItem(val timestamp : Long) {
fun getDateString() : String{
return timestamp.timestampToDate()
}
fun getTimeString() : String{
return timestamp.timestampToTime()
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxConfigFragment.kt
================================================
package cz.covid19cz.erouska.ui.sandbox
import android.os.Bundle
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentSandboxConfigBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.sandbox.event.SnackbarEvent
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SandboxConfigFragment : BaseFragment(
R.layout.fragment_sandbox_config,
SandboxConfigVM::class
) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
subscribe(SnackbarEvent::class) {
showSnackBar(it.text)
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxConfigVM.kt
================================================
package cz.covid19cz.erouska.ui.sandbox
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.OnLifecycleEvent
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.sandbox.event.SnackbarEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class SandboxConfigVM @Inject constructor(val prefs : SharedPrefsRepository) : BaseVM() {
val reportTypeWeights = SandboxConfigValues("reportTypeWeights", 6)
val infectiousnessWeights = SandboxConfigValues("infectiousnessWeights", 3)
val attenuationBucketThresholdDb = SandboxConfigValues("attenuationBucketThresholdDb", 3)
val attenuationBucketWeights = SandboxConfigValues("attenuationBucketWeights", 4)
val minimumWindowScore = SandboxConfigValues("minimumWindowScore", 1)
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate(){
load()
}
private fun load(){
reportTypeWeights.setDoubleValues(prefs.getReportTypeWeights() ?: AppConfig.reportTypeWeights)
infectiousnessWeights.setDoubleValues(prefs.getInfectiousnessWeights() ?: AppConfig.infectiousnessWeights)
attenuationBucketThresholdDb.setIntValues(prefs.getAttenuationBucketThresholdDb() ?: AppConfig.attenuationBucketThresholdDb)
attenuationBucketWeights.setDoubleValues(prefs.getAttenuationBucketWeights() ?: AppConfig.attenuationBucketWeights)
minimumWindowScore.setDoubleValue(0, prefs.getMinimumWindowScore() ?: AppConfig.minimumWindowScore)
}
fun save(){
prefs.setReportTypeWeights(reportTypeWeights.joinToString())
prefs.setInfectiousnessWeights(infectiousnessWeights.joinToString())
prefs.setAttenuationBucketThresholdDb(attenuationBucketThresholdDb.joinToString())
prefs.setAttenuationBucketWeights(attenuationBucketWeights.joinToString())
prefs.setMinimumWindowScore(minimumWindowScore.stringValues[0].value!!)
publish(SnackbarEvent("Config saved"))
}
fun useDefaults(){
prefs.clearCustomConfig()
load()
publish(SnackbarEvent("Using remote config"))
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxConfigValues.kt
================================================
package cz.covid19cz.erouska.ui.sandbox
import androidx.lifecycle.MutableLiveData
class SandboxConfigValues(val title: String, val size : Int) {
val stringValues = Array>(size) { MutableLiveData() }
fun setDoubleValues(newValues : List){
if (newValues.size == stringValues.size){
for (i in newValues.indices){
setDoubleValue(i, newValues[i])
}
}
}
fun setIntValues(newValues : List){
if (newValues.size == stringValues.size){
for (i in newValues.indices){
setIntValue(i, newValues[i])
}
}
}
fun setDoubleValue(index : Int, value : Double){
stringValues[index].value = value.toString()
}
fun setIntValue(index : Int, value : Int){
stringValues[index].value = value.toString()
}
fun getIntValues() : List{
return stringValues.map { it.value?.toIntOrNull() ?: 0 }
}
fun getIntValue(index : Int) : Int{
return stringValues[0].value?.toIntOrNull() ?: 0
}
fun getDoubleValues() : List{
return stringValues.map { it.value?.toDoubleOrNull() ?: 0.0 }
}
fun getDoubleValue(index : Int) : Double{
return stringValues[0].value?.toDoubleOrNull() ?: 0.0
}
fun setValues(values : String){
values.split(";").forEachIndexed { i, s ->
if (i < stringValues.size) {
stringValues[i].value = s
}
}
}
fun joinToString() : String{
return stringValues.map { it.value }.joinToString(";")
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxDataFragment.kt
================================================
package cz.covid19cz.erouska.ui.sandbox
import android.os.Bundle
import android.view.View
import arch.adapter.RecyclerLayoutStrategy
import com.google.android.gms.nearby.exposurenotification.ExposureWindow
import com.google.android.gms.nearby.exposurenotification.ScanInstance
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentSandboxDataBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SandboxDataFragment : BaseFragment(
R.layout.fragment_sandbox_data,
SandboxDataVM::class
) {
private val exposureWindowsLayoutStrategy = object : RecyclerLayoutStrategy {
override fun getLayoutId(item: Any): Int {
return when (item) {
is ExposureWindow -> R.layout.item_exposure_window
is String -> R.layout.item_scan_instance_header
is ScanInstance -> R.layout.item_scan_instance
else -> throw RuntimeException("Layout mapping not found")
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.exposureWindowsLayoutStrategy = exposureWindowsLayoutStrategy
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxDataVM.kt
================================================
package cz.covid19cz.erouska.ui.sandbox
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.viewModelScope
import com.google.android.gms.nearby.exposurenotification.DailySummary
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.ext.daysSinceEpochToDateString
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SandboxDataVM @Inject constructor(private val exposureNotificationsRepository: ExposureNotificationsRepository, val prefs : SharedPrefsRepository) : BaseVM() {
val dailySummaries = ObservableArrayList()
val exposureWindows = ObservableArrayList()
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume(){
getDailySummaries()
getExposureWindows()
}
private fun getExposureWindows() {
exposureWindows.clear()
viewModelScope.launch {
kotlin.runCatching {
exposureNotificationsRepository.getExposureWindows().sortedByDescending { it.dateMillisSinceEpoch }
}.onSuccess {
it.forEach {
exposureWindows.add(it)
exposureWindows.add("HEADER")
it.scanInstances.forEach {
exposureWindows.add(it)
}
}
exposureWindows.addAll(it)
}.onFailure {
L.e(it)
}
}
}
private fun getDailySummaries() {
dailySummaries.clear()
viewModelScope.launch {
kotlin.runCatching {
exposureNotificationsRepository.getDailySummariesFromApi().sortedByDescending { it.daysSinceEpoch }
}.onSuccess {
dailySummaries.addAll(it)
}.onFailure {
L.e(it)
}
}
}
fun daysToString(daysSinceEpoch: Int): String {
return daysSinceEpoch.daysSinceEpochToDateString()
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxFragment.kt
================================================
package cz.covid19cz.erouska.ui.sandbox
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentSandboxBinding
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsErrorHandling
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.dashboard.event.GmsApiErrorEvent
import cz.covid19cz.erouska.ui.sandbox.event.SnackbarEvent
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class SandboxFragment :
BaseFragment(R.layout.fragment_sandbox, SandboxVM::class) {
companion object {
private const val SCREEN_NAME = "Sandbox"
}
@Inject
internal lateinit var exposureNotificationsErrorHandling: ExposureNotificationsErrorHandling
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
subscribe(GmsApiErrorEvent::class) {
exposureNotificationsErrorHandling.handle(it, this, SCREEN_NAME)
}
subscribe(SnackbarEvent::class) {
showSnackBar(it.text)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == ExposureNotificationsErrorHandling.REQUEST_GMS_ERROR_RESOLUTION && resultCode == Activity.RESULT_OK) {
viewModel.refreshTeks()
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/SandboxVM.kt
================================================
package cz.covid19cz.erouska.ui.sandbox
import android.util.Base64
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.viewModelScope
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.ext.timestampToDateTime
import cz.covid19cz.erouska.net.ExposureServerRepository
import cz.covid19cz.erouska.net.model.DownloadedKeys
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.dashboard.event.GmsApiErrorEvent
import cz.covid19cz.erouska.ui.sandbox.event.SnackbarEvent
import cz.covid19cz.erouska.ui.verification.ReportExposureException
import cz.covid19cz.erouska.ui.verification.VerifyException
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SandboxVM @Inject constructor(
private val exposureNotificationsRepository: ExposureNotificationsRepository,
private val serverRepository: ExposureServerRepository
) : BaseVM() {
val filesString = MutableLiveData()
val teks = ObservableArrayList()
var downloadResult: List? = null
val code = MutableLiveData("")
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
refreshTeks()
}
fun tekToString(tek: TemporaryExposureKey): String {
return Base64.encodeToString(tek.keyData, Base64.NO_WRAP)
}
fun reportTypeToString(reportType: Int): String {
return when (reportType) {
0 -> "UNKNOWN"
1 -> "CONFIRMED_TEST"
2 -> "CONFIRMED_CLINICAL_DIAGNOSIS"
3 -> "SELF_REPORT"
4 -> "RECURSIVE"
5 -> "REVOKED"
else -> reportType.toString()
}
}
fun rollingStartToString(rollingStart: Int): String {
val timeInMillis = (rollingStart.toLong() * 10 * 60 * 1000)
return timeInMillis.timestampToDateTime()
}
fun rollingIntervalToString(rollingInterval: Int): String {
return "${(rollingInterval * 10) / 60}h"
}
fun refreshTeks() {
teks.clear()
viewModelScope.launch {
kotlin.runCatching {
exposureNotificationsRepository.getTemporaryExposureKeyHistory()
}.onSuccess {
teks.addAll(it.sortedByDescending { it.rollingStartIntervalNumber })
}.onFailure {
publish(GmsApiErrorEvent(it))
L.e(it)
}
}
}
fun downloadKeyExport() {
viewModelScope.launch {
kotlin.runCatching {
downloadResult = serverRepository.downloadKeyExport()
L.d("files=${downloadResult}")
return@runCatching downloadResult
}.onSuccess {
showSnackbar("Download success")
}.onFailure {
showSnackbar("Download failed: ${it.message}")
}
}
}
fun deleteKeys() {
serverRepository.deleteFiles()
filesString.value = null
}
fun provideDiagnosisKeys() {
if (downloadResult != null) {
viewModelScope.launch {
runCatching {
exposureNotificationsRepository.provideDiagnosisKeys(downloadResult!!)
}.onSuccess {
showSnackbar("Import success")
}.onFailure {
showSnackbar("Import error: ${it.message}")
L.e(it)
}
}
} else {
showSnackbar("Download keys first")
}
}
fun reportExposureWithVerification(code: String) {
viewModelScope.launch {
runCatching {
exposureNotificationsRepository.verifyCode(code)
exposureNotificationsRepository.publishKeys()
}.onSuccess {
showSnackbar("Upload success: $it keys")
}.onFailure {
when (it) {
is ApiException -> publish(GmsApiErrorEvent(it))
is VerifyException -> showSnackbar("Verification error: ${it.message}")
is ReportExposureException -> showSnackbar("Upload error: ${it.message}")
else -> showSnackbar("${it.message}")
}
L.e(it)
}
}
}
private fun showSnackbar(text: String) {
publish(SnackbarEvent(text))
}
fun navigateToData() {
navigate(R.id.nav_sandbox_data)
}
fun navigateToConfig() {
navigate(R.id.nav_sandbox_config)
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/sandbox/event/SnackbarEvent.kt
================================================
package cz.covid19cz.erouska.ui.sandbox.event
import arch.event.LiveEvent
class SnackbarEvent(val text : String) : LiveEvent()
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/symptomdate/SymptomDateFragment.kt
================================================
package cz.covid19cz.erouska.ui.symptomdate
import android.app.DatePickerDialog
import android.os.Bundle
import android.view.View
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentSymptomDateBinding
import cz.covid19cz.erouska.ext.hideKeyboard
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.symptomdate.event.DatePickerEvent
import cz.covid19cz.erouska.ui.symptomdate.event.SymptomDateCommandEvent
import cz.covid19cz.erouska.ui.symptomdate.event.SymptomDateCommandEvent.Command.NAV_EFGS_AGREEMENT
import cz.covid19cz.erouska.ui.symptomdate.event.SymptomDateCommandEvent.Command.NAV_TRAVELLER
import dagger.hilt.android.AndroidEntryPoint
import java.util.*
@AndroidEntryPoint
class SymptomDateFragment : BaseFragment(
R.layout.fragment_symptom_date,
SymptomDateVM::class
) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.hideKeyboard()
subscribe(DatePickerEvent::class) {
showDatePickerDialog(it.preselect)
}
subscribe(SymptomDateCommandEvent::class) {
when (it.command) {
NAV_TRAVELLER -> navigate(R.id.action_nav_symptom_date_to_nav_traveller)
NAV_EFGS_AGREEMENT -> navigate(R.id.action_nav_symptom_date_to_efgsAgreementFragment)
}
}
}
private fun showDatePickerDialog(preselect: Date?) {
val preselectCalendar = Calendar.getInstance()
if (preselect != null) {
preselectCalendar.time = preselect
}
val datePickerDialog = DatePickerDialog(
requireContext(),
{ view, year, month, day ->
viewModel.symptomDate.value = Calendar.getInstance().apply {
set(Calendar.YEAR, year)
set(Calendar.MONTH, month)
set(Calendar.DAY_OF_MONTH, day)
}.time
},
preselectCalendar.get(Calendar.YEAR),
preselectCalendar.get(Calendar.MONTH),
preselectCalendar.get(Calendar.DAY_OF_MONTH)
)
datePickerDialog.datePicker.minDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -14) }.timeInMillis
datePickerDialog.datePicker.maxDate = Calendar.getInstance().timeInMillis
datePickerDialog.show()
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/symptomdate/SymptomDateVM.kt
================================================
package cz.covid19cz.erouska.ui.symptomdate
import androidx.lifecycle.MutableLiveData
import arch.livedata.SafeMutableLiveData
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.ext.timestampToDate
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.symptomdate.event.DatePickerEvent
import cz.covid19cz.erouska.ui.symptomdate.event.SymptomDateCommandEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.*
import javax.inject.Inject
@HiltViewModel
class SymptomDateVM @Inject constructor(val prefs: SharedPrefsRepository) : BaseVM() {
val hasSymptoms = SafeMutableLiveData(true)
val symptomDate = MutableLiveData()
val symptomDateString = MutableLiveData()
init {
symptomDate.observeForever {
setSymptomDateString(it?.time?.timestampToDate())
}
hasSymptoms.observeForever {
if (!it) {
clearSymptomFields()
}
}
}
private fun clearSymptomFields() {
symptomDate.value = null
setSymptomDateString(null)
}
private fun setSymptomDateString(symptomDate: String?) {
symptomDateString.value = symptomDate
}
fun showDatePicker() {
publish(DatePickerEvent(symptomDate.value))
}
fun next() {
prefs.setSymptomDate(symptomDate.value?.time)
if (prefs.isTraveller()) {
publish(SymptomDateCommandEvent(SymptomDateCommandEvent.Command.NAV_EFGS_AGREEMENT))
} else {
publish(SymptomDateCommandEvent(SymptomDateCommandEvent.Command.NAV_TRAVELLER))
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/symptomdate/event/DatePickerEvent.kt
================================================
package cz.covid19cz.erouska.ui.symptomdate.event
import arch.event.LiveEvent
import java.util.*
class DatePickerEvent(val preselect : Date?) : LiveEvent()
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/symptomdate/event/SymptomDateCommandEvent.kt
================================================
package cz.covid19cz.erouska.ui.symptomdate.event
import arch.event.LiveEvent
import java.util.*
class SymptomDateCommandEvent(val command: Command) : LiveEvent() {
enum class Command {
NAV_TRAVELLER,
NAV_EFGS_AGREEMENT
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/traveller/TravellerFragment.kt
================================================
package cz.covid19cz.erouska.ui.traveller
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentSymptomDateBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class TravellerFragment : BaseFragment(R.layout.fragment_traveller, TravellerVM::class)
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/traveller/TravellerVM.kt
================================================
package cz.covid19cz.erouska.ui.traveller
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class TravellerVM @Inject constructor(val prefs : SharedPrefsRepository) : BaseVM() {
fun next(traveller : Boolean){
prefs.setTraveller(traveller)
navigate(TravellerFragmentDirections.actionNavTravellerToEfgsAgreementFragment())
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/update/efgs/EfgsUpdateFragment.kt
================================================
package cz.covid19cz.erouska.ui.update.efgs
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentEfgsUpdateBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.utils.CustomTabHelper
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_efgs_update.*
import javax.inject.Inject
@AndroidEntryPoint
class EfgsUpdateFragment : BaseFragment(
R.layout.fragment_efgs_update,
EfgsUpdateVM::class
) {
@Inject
internal lateinit var customTabHelper: CustomTabHelper
private var isFullscreen: Boolean = false
private var isOnboarding: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isOnboarding = arguments?.let {
EfgsUpdateFragmentArgs.fromBundle(it).onboarding
} ?: false
isFullscreen = arguments?.let {
EfgsUpdateFragmentArgs.fromBundle(it).fullscreen
} ?: false
(activity as? AppCompatActivity)?.supportActionBar?.setDisplayShowTitleEnabled(!isFullscreen || isOnboarding)
enableUpInToolbar(!isFullscreen || isOnboarding)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showEFGSNews()
}
override fun onDestroyView() {
super.onDestroyView()
(activity as? AppCompatActivity)?.supportActionBar?.setDisplayShowTitleEnabled(true)
}
private fun showEFGSNews() {
viewModel.sharedPrefsRepository.setEFGSIntroduced(true)
legacy_update_button.setOnClickListener {
if (isOnboarding){
navigate(R.id.action_nav_efgs_update_fragment_to_activation_fragment)
} else {
navController().navigateUp()
}
}
legacy_update_checkbox.setOnCheckedChangeListener { _, isChecked ->
viewModel.sharedPrefsRepository.setTraveller(isChecked)
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/update/efgs/EfgsUpdateVM.kt
================================================
package cz.covid19cz.erouska.ui.update.efgs
import arch.viewmodel.BaseArchViewModel
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.db.SharedPrefsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class EfgsUpdateVM @Inject constructor(
val sharedPrefsRepository: SharedPrefsRepository
) : BaseArchViewModel() {
fun efgsDays() = AppConfig.efgsDays
fun efgsSupportedCountries() = AppConfig.efgsSupportedCountries
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/update/playservices/UpdatePlayServicesFragment.kt
================================================
package cz.covid19cz.erouska.ui.update.playservices
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentPlayServicesUpdateBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.update.playservices.event.UpdatePlayServicesEvent
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class UpdatePlayServicesFragment :
BaseFragment(
R.layout.fragment_play_services_update,
UpdatePlayServicesVM::class
) {
companion object {
const val GMS_STORE_URL = "https://play.google.com/store/apps/details?id=com.google.android.gms"
}
private var isDemoMode: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
subscribe(UpdatePlayServicesEvent::class) {
when (it.command) {
UpdatePlayServicesEvent.Command.PLAY_STORE -> openPlayStore()
}
}
isDemoMode = arguments?.let {
UpdatePlayServicesFragmentArgs.fromBundle(it).demo
} ?: false
}
override fun onResume() {
super.onResume()
if (!isDemoMode && !isPlayServicesObsolete()) {
navController().navigateUp()
}
}
private fun openPlayStore() {
if (isDemoMode) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(GMS_STORE_URL)))
} else {
if (isPlayServicesObsolete()){
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(GMS_STORE_URL)))
} else {
navController().navigateUp()
}
}
}
override fun onBackPressed(): Boolean {
return if (isPlayServicesObsolete()) {
activity?.finish()
true
} else {
navController().navigateUp()
true
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/update/playservices/UpdatePlayServicesVM.kt
================================================
package cz.covid19cz.erouska.ui.update.playservices
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.update.playservices.event.UpdatePlayServicesEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class UpdatePlayServicesVM @Inject constructor() : BaseVM() {
fun openPlayStore() {
publish(UpdatePlayServicesEvent(UpdatePlayServicesEvent.Command.PLAY_STORE))
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/update/playservices/event/UpdatePlayServicesEvent.kt
================================================
package cz.covid19cz.erouska.ui.update.playservices.event
import arch.event.LiveEvent
class UpdatePlayServicesEvent(val command: Command) : LiveEvent() {
enum class Command {
PLAY_STORE
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/InvalidTokenException.kt
================================================
package cz.covid19cz.erouska.ui.verification
class InvalidTokenException : Throwable()
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/NoKeysException.kt
================================================
package cz.covid19cz.erouska.ui.verification
class NoKeysException : Throwable()
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/ReportExposureException.kt
================================================
package cz.covid19cz.erouska.ui.verification
class ReportExposureException(val error: String, val code: String?) : Throwable()
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/VerificationFragment.kt
================================================
package cz.covid19cz.erouska.ui.verification
import android.os.Bundle
import android.view.View
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentVerificationBinding
import cz.covid19cz.erouska.ext.attachKeyboardController
import cz.covid19cz.erouska.ext.setOnDoneListener
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.main.MainActivityOld
import cz.covid19cz.erouska.ui.verification.event.VerificationCommandEvent
import cz.covid19cz.erouska.ui.verification.event.VerificationCommandEvent.Command
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class VerificationFragment : BaseFragment(
R.layout.fragment_verification,
VerificationVM::class
) {
companion object {
const val SCREEN_NAME = "Verification"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribe(VerificationCommandEvent::class) {
when (it.command) {
Command.NAV_SYMPTOMS -> navigate(R.id.action_nav_verification_to_nav_symptom_date)
Command.NAV_NO_CODE -> navigate(R.id.action_nav_error_to_nav_no_verification_code)
}
}
enableUpInToolbar(true, IconType.CLOSE)
setupListeners()
activity?.let {
(it as MainActivityOld).initReviews()
}
with(binding) {
codeInput.attachKeyboardController()
codeInput.requestFocus()
codeInput.setOnDoneListener { viewModel.verifyAndConfirm() }
}
}
private fun setupListeners() {
with(binding) {
codeInput.attachKeyboardController()
codeInput.setOnDoneListener { viewModel.verifyAndConfirm() }
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/VerificationVM.kt
================================================
package cz.covid19cz.erouska.ui.verification
import androidx.core.text.isDigitsOnly
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import arch.livedata.SafeMutableLiveData
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.net.model.VerifyCodeResponse
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.error.entity.ErrorType
import cz.covid19cz.erouska.ui.verification.event.VerificationCommandEvent
import cz.covid19cz.erouska.utils.L
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import java.net.UnknownHostException
import javax.inject.Inject
@HiltViewModel
class VerificationVM @Inject constructor(
private val exposureNotificationRepo: ExposureNotificationsRepository,
private val prefs: SharedPrefsRepository
) :
BaseVM() {
val code = SafeMutableLiveData("")
val error = MutableLiveData(null)
val lastDataSentDate = MutableLiveData(prefs.getLastDataSentDateString())
val loading = SafeMutableLiveData(false)
init {
code.observeForever {
if (error.value != null) {
error.value = null
}
}
}
fun verifyAndConfirm() {
if (!isCodeValid(code.value)) {
error.value = R.string.send_data_code_invalid
return
}
validate()
}
fun navigateToVerificationCode() {
publish(VerificationCommandEvent(VerificationCommandEvent.Command.NAV_NO_CODE))
}
private fun validate() {
if (prefs.isCodeValidated(code.value)) {
navigateToSymptomsScreen()
} else {
startLoading()
viewModelScope.launch {
runCatching {
exposureNotificationRepo.verifyCode(code.value)
}.onSuccess {
stopLoading()
navigateToSymptomsScreen()
}.onFailure {
stopLoading()
handleSendDataErrors(it)
}
}
}
}
private fun startLoading() {
loading.value = true
}
private fun stopLoading() {
loading.value = false
}
private fun handleSendDataErrors(exception: Throwable) {
val errorAndCode: Pair = when (exception) {
is VerifyException -> mapExceptionToErrorType(exception)
is UnknownHostException -> createNoInternetErrorType()
else -> {
L.e(exception)
createNoInternetErrorType()
}
}
showError(errorAndCode.first, errorAndCode.second)
}
private fun mapExceptionToErrorType(exception: VerifyException): Pair {
if (exception.code == null) {
return createNoInternetErrorType()
}
val errorCodeMap: Map = mutableMapOf(
VerifyCodeResponse.ERROR_CODE_EXPIRED_CODE to ErrorType.EXPIRED_OR_USED_CODE,
VerifyCodeResponse.ERROR_CODE_EXPIRED_USED_CODE to ErrorType.EXPIRED_OR_USED_CODE,
VerifyCodeResponse.ERROR_CODE_INVALID_CODE to ErrorType.INVALID_CODE,
)
val type = errorCodeMap.getOrElse(exception.code, { ErrorType.GENERAL_ERROR })
val message = "${exception.message} ${exception.code}"
return Pair(type, message)
}
private fun createNoInternetErrorType() = Pair(ErrorType.NO_INTERNET, "")
private fun showError(errorType: ErrorType, errorMessage: String) {
navigate(
VerificationFragmentDirections.actionNavVerificationToNavError(
type = errorType,
errorCode = errorMessage
)
)
}
private fun navigateToSymptomsScreen() {
publish(VerificationCommandEvent(VerificationCommandEvent.Command.NAV_SYMPTOMS))
}
private fun isCodeValid(code: String): Boolean {
return code.length == 8 && code.isDigitsOnly()
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/VerifyException.kt
================================================
package cz.covid19cz.erouska.ui.verification
class VerifyException(message : String?, val code: String?) : Throwable(message)
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/verification/event/VerificationCommandEvent.kt
================================================
package cz.covid19cz.erouska.ui.verification.event
import arch.event.LiveEvent
import java.util.*
class VerificationCommandEvent(val command: Command) : LiveEvent() {
enum class Command {
NAV_SYMPTOMS,
NAV_NO_CODE
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/welcome/WelcomeFragment.kt
================================================
package cz.covid19cz.erouska.ui.welcome
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.text.HtmlCompat
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.databinding.FragmentWelcomeBinding
import cz.covid19cz.erouska.ui.base.BaseFragment
import cz.covid19cz.erouska.ui.welcome.event.WelcomeCommandEvent
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.fragment_welcome.*
@AndroidEntryPoint
class WelcomeFragment :
BaseFragment(R.layout.fragment_welcome, WelcomeVM::class) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
subscribe(WelcomeCommandEvent::class) {
when (it.command) {
WelcomeCommandEvent.Command.VERIFY_APP -> openAppActivation()
WelcomeCommandEvent.Command.HELP -> openHelpPage()
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
enableUpInToolbar(false)
val welcomeDescription: String = String.format(
getString(R.string.welcome_description)
)
welcome_desc.text = HtmlCompat.fromHtml(welcomeDescription, HtmlCompat.FROM_HTML_MODE_LEGACY)
welcome_desc.movementMethod = LinkMovementMethod.getInstance()
}
private fun openAppActivation() {
if (isPlayServicesObsolete()) {
showPlayServicesUpdate()
return
}
navigate(R.id.action_nav_welcome_fragment_to_nav_activation_notifications)
}
private fun openHelpPage() {
navigate(R.id.action_nav_welcome_fragment_to_nav_how_it_works)
}
private fun showPlayServicesUpdate() {
navigate(R.id.action_nav_welcome_to_nav_play_services_update)
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/welcome/WelcomeVM.kt
================================================
package cz.covid19cz.erouska.ui.welcome
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.ui.base.BaseVM
import cz.covid19cz.erouska.ui.welcome.event.WelcomeCommandEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class WelcomeVM @Inject constructor(private val prefs: SharedPrefsRepository) : BaseVM() {
fun nextStep() {
publish(WelcomeCommandEvent(WelcomeCommandEvent.Command.VERIFY_APP))
}
fun help() {
publish(WelcomeCommandEvent(WelcomeCommandEvent.Command.HELP))
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/ui/welcome/event/WelcomeCommandEvent.kt
================================================
package cz.covid19cz.erouska.ui.welcome.event
import arch.event.LiveEvent
class WelcomeCommandEvent(val command: Command) : LiveEvent() {
enum class Command{
VERIFY_APP,
HELP
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/Analytics.kt
================================================
package cz.covid19cz.erouska.utils
import android.content.Context
import android.os.Bundle
import com.google.firebase.analytics.FirebaseAnalytics
object Analytics {
const val KEY_EXPORT_DOWNLOAD_STARTED = "key_export_download_started"
const val KEY_EXPORT_DOWNLOAD_FINISHED = "key_export_download_finished"
const val KEY_SHARE_APP = "tap_share_app"
const val KEY_PAUSE_APP = "tap_pause_app"
const val KEY_RESUME_APP = "tap_resume_app"
const val KEY_HOME = "tap_tab_home"
const val KEY_NEWS = "tap_tab_news"
const val KEY_CONTACTS = "tap_tab_contacts"
const val KEY_HELP = "tap_tab_help"
const val KEY_CURRENT_MEASURES = "tap_current_measures"
fun logEvent(context: Context, key: String) {
val analytics = FirebaseAnalytics.getInstance(context)
analytics.logEvent(key, Bundle())
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/CustomTabHelper.kt
================================================
package cz.covid19cz.erouska.utils
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.text.TextUtils
import androidx.browser.customtabs.CustomTabsService
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CustomTabHelper @Inject constructor(
@ApplicationContext private val context: Context
) {
companion object {
private const val STABLE_PACKAGE = "com.android.chrome"
private const val BETA_PACKAGE = "com.chrome.beta"
private const val DEV_PACKAGE = "com.chrome.dev"
private const val LOCAL_PACKAGE = "com.google.android.apps.chrome"
}
private var sPackageNameToUse: String? = null
val chromePackageName by lazy {
return@lazy getPackageNameToUse(context, "https://erouska.cz")
}
private fun getPackageNameToUse(context: Context, url: String): String? {
sPackageNameToUse?.let {
return it
}
val pm = context.packageManager
val activityIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0)
var defaultViewHandlerPackageName: String? = null
defaultViewHandlerInfo?.let {
defaultViewHandlerPackageName = it.activityInfo.packageName
}
val resolvedActivityList = pm.queryIntentActivities(activityIntent, 0)
val packagesSupportingCustomTabs = ArrayList()
for (info in resolvedActivityList) {
val serviceIntent = Intent()
serviceIntent.action = CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION
serviceIntent.setPackage(info.activityInfo.packageName)
pm.resolveService(serviceIntent, 0)?.let {
packagesSupportingCustomTabs.add(info.activityInfo.packageName)
}
}
when {
packagesSupportingCustomTabs.isEmpty() -> sPackageNameToUse = null
packagesSupportingCustomTabs.size == 1 -> sPackageNameToUse =
packagesSupportingCustomTabs[0]
!TextUtils.isEmpty(defaultViewHandlerPackageName)
&& !hasSpecializedHandlerIntents(context, activityIntent)
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName) ->
sPackageNameToUse = defaultViewHandlerPackageName
packagesSupportingCustomTabs.contains(STABLE_PACKAGE) -> sPackageNameToUse =
STABLE_PACKAGE
packagesSupportingCustomTabs.contains(BETA_PACKAGE) -> sPackageNameToUse = BETA_PACKAGE
packagesSupportingCustomTabs.contains(DEV_PACKAGE) -> sPackageNameToUse = DEV_PACKAGE
packagesSupportingCustomTabs.contains(LOCAL_PACKAGE) -> sPackageNameToUse =
LOCAL_PACKAGE
}
return sPackageNameToUse
}
private fun hasSpecializedHandlerIntents(context: Context, intent: Intent): Boolean {
try {
val pm = context.packageManager
val handlers = pm.queryIntentActivities(
intent,
PackageManager.GET_RESOLVED_FILTER
)
if (handlers.size == 0) {
return false
}
for (resolveInfo in handlers) {
val filter = resolveInfo.filter ?: continue
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue
if (resolveInfo.activityInfo == null) continue
return true
}
} catch (e: RuntimeException) {
}
return false
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/DeviceInfo.kt
================================================
package cz.covid19cz.erouska.utils
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.PowerManager
import android.os.UserManager
import com.jaredrummler.android.device.DeviceName
import cz.covid19cz.erouska.ext.isBtEnabled
import cz.covid19cz.erouska.ext.isLocationEnabled
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DeviceInfo @Inject constructor(
@ApplicationContext private val context: Context
) {
@SuppressLint("DefaultLocale")
fun getManufacturer(): String {
return Build.MANUFACTURER.capitalize()
}
fun getDeviceName(): String {
return try {
DeviceName.getDeviceInfo(context).marketName
} catch (t: Throwable) {
Build.MODEL
}
}
fun getAndroidVersion(): String {
return Build.VERSION.RELEASE
}
fun isBtEnabled(): Boolean {
return context.isBtEnabled()
}
fun isLocationEnabled(): Boolean {
return context.isLocationEnabled()
}
fun isIgnoringBatteryOptimizations(): Boolean {
val pwrm =
context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
val name = context.applicationContext.packageName
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return pwrm.isIgnoringBatteryOptimizations(name)
}
return true
}
fun isBatterySaverOn(): Boolean {
val pwrm =
context.applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
return pwrm.isPowerSaveMode
}
fun supportsBLE(): Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)
}
fun isUserDeviceOwner(): Boolean {
val um = context.getSystemService(Context.USER_SERVICE) as UserManager
return um.isSystemUser
}
fun supportsMultiAds(): Boolean {
return BluetoothAdapter.getDefaultAdapter().isMultipleAdvertisementSupported
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/L.kt
================================================
package cz.covid19cz.erouska.utils
import android.util.Log
import com.google.firebase.crashlytics.FirebaseCrashlytics
import cz.covid19cz.erouska.BuildConfig
/**
* Created by stepansonsky on 07/01/16.
*/
object L {
fun d(text: String) {
val logStrings = createLogStrings(text)
if (BuildConfig.DEBUG) {
Log.d(logStrings[0], logStrings[1])
}
}
fun i(text: String) {
val logStrings = createLogStrings(text)
if (BuildConfig.DEBUG) {
Log.i(logStrings[0], logStrings[1])
}
FirebaseCrashlytics.getInstance().log("I/${logStrings[0]}: ${logStrings[1]}")
}
fun w(text: String) {
val logStrings = createLogStrings(text)
if (BuildConfig.DEBUG) {
Log.w(logStrings[0], logStrings[1])
}
FirebaseCrashlytics.getInstance().log("W/${logStrings[0]}: ${logStrings[1]}")
}
fun e(text: String) {
val logStrings = createLogStrings(text)
if (BuildConfig.DEBUG) {
Log.e(logStrings[0], logStrings[1])
}
FirebaseCrashlytics.getInstance().log("E/${logStrings[0]}: ${logStrings[1]}")
}
fun e(throwable: Throwable?) {
throwable?.let {
if (BuildConfig.DEBUG) {
Log.e("L", throwable.message, throwable)
throwable.printStackTrace()
}
FirebaseCrashlytics.getInstance().recordException(throwable)
}
}
private fun createLogStrings(text: String): Array {
val ste = Thread.currentThread().stackTrace
val line = "(" + (ste[4].fileName + ":" + ste[4].lineNumber + ")")
return arrayOf(line, text)
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/LocaleUtils.kt
================================================
package cz.covid19cz.erouska.utils
import cz.covid19cz.erouska.BuildConfig
import java.util.*
object LocaleUtils {
fun getLocale(): String {
return Locale.getDefault().toString()
}
fun getSupportedLanguage(): String {
val currentLanguage = Locale.getDefault().language
return if (BuildConfig.SUPPORTED_LANGUAGES.contains(currentLanguage)) {
currentLanguage
} else {
"en"
}
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/Markdown.kt
================================================
package cz.covid19cz.erouska.utils
import android.content.Context
import android.text.style.BackgroundColorSpan
import android.widget.TextView
import androidx.annotation.Nullable
import cz.covid19cz.erouska.R
import dagger.hilt.android.qualifiers.ApplicationContext
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonSpansFactory
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.core.MarkwonTheme
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.glide.GlideImagesPlugin
import io.noties.markwon.inlineparser.InlineProcessor
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin
import org.commonmark.ext.autolink.AutolinkExtension
import org.commonmark.node.CustomNode
import org.commonmark.node.Node
import org.commonmark.parser.Parser
import java.util.regex.Pattern
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class Markdown @Inject constructor(
@ApplicationContext val context: Context
) {
private val markwon by lazy {
Markwon.builder(context)
.usePlugin(HtmlPlugin.create { plugin -> plugin.excludeDefaults(false) })
.usePlugin(GlideImagesPlugin.create(context))
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureParser(builder: Parser.Builder) {
builder.extensions(listOf(AutolinkExtension.create()))
}
})
.usePlugin(MarkwonInlineParserPlugin.create { factoryBuilder ->
factoryBuilder
.addInlineProcessor(SearchedTextInlineProcessor())
})
.usePlugin(object : AbstractMarkwonPlugin() {
// general formatting
override fun configureTheme(builder: MarkwonTheme.Builder) {
builder.headingBreakHeight(0)
builder.headingTextSizeMultipliers(
floatArrayOf(1.15f, 1.10f, 1.05f, 1f, .83f, .67f)
)
}
// searched text highlighting
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
builder.on(SearchedTextNode::class.java, GenericInlineNodeVisitor())
}
// searched text highlighting
override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder
.setFactory(
SearchedTextNode::class.java
) { _, _ ->
arrayOf(BackgroundColorSpan(context.getColor(R.color.highlight)))
}
}
})
.build()
}
fun show(textView: TextView, markdown: String?) {
markdown?.replace("\\n", "\n")?.let {
markwon.setMarkdown(textView, it)
}
}
class GenericInlineNodeVisitor : MarkwonVisitor.NodeVisitor {
override fun visit(visitor: MarkwonVisitor, n: Node) {
val length = visitor.length()
visitor.visitChildren(n)
visitor.setSpansForNodeOptional(n, length)
}
}
class SearchedTextInlineProcessor : InlineProcessor() {
override fun specialCharacter(): Char {
return searchChar
}
@Nullable
override fun parse(): Node? {
val match = match(RE)
if (match != null) {
// consume syntax
val text = match.substring(2, match.length - 2)
val node = SearchedTextNode()
node.appendChild(text(text))
return node
}
return null
}
companion object {
private val RE: Pattern = Pattern.compile("${doubleSearchChar}(.+?)${doubleSearchChar}")
}
}
private class SearchedTextNode : CustomNode()
companion object {
const val searchChar = '\u200B'
const val doubleSearchChar = "${searchChar}${searchChar}"
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/MiscUtils.kt
================================================
package cz.covid19cz.erouska.utils
import java.text.DecimalFormat
import java.text.NumberFormat
object SignNumberFormat {
@JvmStatic
fun format(value: Number): String {
return NumberFormat.getInstance().also { it.showSign() }.format(value)
}
}
fun NumberFormat.showSign() {
if (this is DecimalFormat) {
this.positivePrefix = "+ "
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/SharedPrefsLiveData.kt
================================================
package cz.covid19cz.erouska.utils
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
import androidx.lifecycle.MutableLiveData
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
// Simple SharedPreferences delegates - supports getter/setter
// Note: When not specified explicitly, the name of the property is used as a preference key
// Example: `val userName by sharedPrefs().string()`
fun SharedPreferencesProvider.int(def: Int = 0, key: String? = null): ReadWriteProperty = delegatePrimitive(def, key, SharedPreferences::getInt, Editor::putInt)
fun SharedPreferencesProvider.long(def: Long = 0, key: String? = null): ReadWriteProperty = delegatePrimitive(def, key, SharedPreferences::getLong, Editor::putLong)
fun SharedPreferencesProvider.float(def: Float = 0f, key: String? = null): ReadWriteProperty = delegatePrimitive(def, key, SharedPreferences::getFloat, Editor::putFloat)
fun SharedPreferencesProvider.boolean(def: Boolean = false, key: String? = null): ReadWriteProperty = delegatePrimitive(def, key, SharedPreferences::getBoolean, Editor::putBoolean)
fun SharedPreferencesProvider.stringSet(def: Set = emptySet(), key: String? = null): ReadWriteProperty?> = delegate(def, key, SharedPreferences::getStringSet, Editor::putStringSet)
fun SharedPreferencesProvider.string(def: String? = null, key: String? = null): ReadWriteProperty = delegate(def, key, SharedPreferences::getString, Editor::putString)
// LiveData SharedPreferences delegates - provides LiveData access to prefs with sync across app on changes
// Note: When not specified explicitly, the name of the property is used as a preference key
// Example: `val userName by sharedPrefs().stringLiveData()`
fun SharedPreferencesProvider.intLiveData(def: Int, key: String? = null): ReadOnlyProperty> = liveDataDelegatePrimitive(def, key, SharedPreferences::getInt, Editor::putInt)
fun SharedPreferencesProvider.longLiveData(def: Long, key: String? = null): ReadOnlyProperty> = liveDataDelegatePrimitive(def, key, SharedPreferences::getLong, Editor::putLong)
fun SharedPreferencesProvider.floatLiveData(def: Float, key: String? = null): ReadOnlyProperty> = liveDataDelegatePrimitive(def, key, SharedPreferences::getFloat, Editor::putFloat)
fun SharedPreferencesProvider.booleanLiveData(def: Boolean, key: String? = null): ReadOnlyProperty> = liveDataDelegatePrimitive(def, key, SharedPreferences::getBoolean, Editor::putBoolean)
fun SharedPreferencesProvider.stringLiveData(def: String? = null, key: String? = null): ReadOnlyProperty> = liveDataDelegate(def, key, SharedPreferences::getString, Editor::putString)
fun SharedPreferencesProvider.stringSetLiveData(def: Set? = null, key: String? = null): ReadOnlyProperty?>> = liveDataDelegate(def, key, SharedPreferences::getStringSet, Editor::putStringSet)
// -- internal
private inline fun SharedPreferencesProvider.delegate(
defaultValue: T?,
key: String? = null,
crossinline getter: SharedPreferences.(String, T?) -> T?,
crossinline setter: Editor.(String, T?) -> Editor
) =
object : ReadWriteProperty {
override fun getValue(thisRef: Any?, property: KProperty<*>): T? =
provide().getter(key
?: property.name, defaultValue)
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) =
provide().edit().setter(key
?: property.name, value).apply()
}
private inline fun SharedPreferencesProvider.delegatePrimitive(
defaultValue: T,
key: String? = null,
crossinline getter: SharedPreferences.(String, T) -> T,
crossinline setter: Editor.(String, T) -> Editor
) =
object : ReadWriteProperty {
override fun getValue(thisRef: Any?, property: KProperty<*>): T =
provide().getter(key
?: property.name, defaultValue)!!
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) =
provide().edit().setter(key
?: property.name, value).apply()
}
private inline fun SharedPreferencesProvider.liveDataDelegate(
defaultValue: T? = null,
key: String? = null,
crossinline getter: SharedPreferences.(String, T?) -> T?,
crossinline setter: Editor.(String, T?) -> Editor
): ReadOnlyProperty> = object : MutableLiveData(), ReadOnlyProperty>, SharedPreferences.OnSharedPreferenceChangeListener {
var originalProperty: KProperty<*>? = null
lateinit var prefKey: String
override fun getValue(thisRef: Any?, property: KProperty<*>): MutableLiveData {
originalProperty = property
prefKey = key ?: originalProperty!!.name
return this
}
override fun getValue(): T? {
val value = provide().getter(prefKey, defaultValue)
return super.getValue()
?: value
?: defaultValue
}
override fun setValue(value: T?) {
super.setValue(value)
provide().edit().setter(prefKey, value).apply()
}
override fun onActive() {
super.onActive()
value = provide().getter(prefKey, defaultValue)
provide().registerOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, changedKey: String) {
if (changedKey == prefKey) {
value = sharedPreferences.getter(changedKey, defaultValue)
}
}
override fun onInactive() {
super.onInactive()
provide().unregisterOnSharedPreferenceChangeListener(this)
}
}
private inline fun SharedPreferencesProvider.liveDataDelegatePrimitive(
defaultValue: T,
key: String? = null,
crossinline getter: SharedPreferences.(String, T) -> T,
crossinline setter: Editor.(String, T) -> Editor
): ReadOnlyProperty> = object : MutableLiveData(), ReadOnlyProperty>, SharedPreferences.OnSharedPreferenceChangeListener {
var originalProperty: KProperty<*>? = null
lateinit var prefKey: String
override fun getValue(thisRef: Any?, property: KProperty<*>): MutableLiveData {
originalProperty = property
prefKey = key ?: originalProperty!!.name
return this
}
override fun getValue(): T {
val value = provide().getter(prefKey, defaultValue)
return super.getValue()
?: value
?: defaultValue
}
override fun setValue(value: T) {
super.setValue(value)
provide().edit().setter(prefKey, value).apply()
}
override fun onActive() {
super.onActive()
value = provide().getter(prefKey, defaultValue)
provide().registerOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, changedKey: String) {
if (changedKey == prefKey) {
value = sharedPreferences.getter(changedKey, defaultValue)
}
}
override fun onInactive() {
super.onInactive()
provide().unregisterOnSharedPreferenceChangeListener(this)
}
}
class SharedPreferencesProvider(private val provider: () -> SharedPreferences) {
internal fun provide() = provider()
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/SupportEmailGenerator.kt
================================================
package cz.covid19cz.erouska.utils
import android.app.Activity
import android.content.Context
import android.net.Uri
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
import cz.covid19cz.erouska.AppConfig
import cz.covid19cz.erouska.BuildConfig
import cz.covid19cz.erouska.R
import cz.covid19cz.erouska.db.DailySummariesDb
import cz.covid19cz.erouska.db.SharedPrefsRepository
import cz.covid19cz.erouska.exposurenotifications.ExposureNotificationsRepository
import cz.covid19cz.erouska.ext.*
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SupportEmailGenerator @Inject constructor(
@ApplicationContext private val context: Context,
private val deviceInfo: DeviceInfo,
private val exposureNotificationsRepository: ExposureNotificationsRepository,
private val prefs: SharedPrefsRepository,
private val db: DailySummariesDb
) {
fun sendSupportEmail(
activity: Activity,
scope: CoroutineScope,
recipient: String = AppConfig.supportEmail,
errorCode: String? = null,
isError: Boolean,
screenOrigin: String
) {
AlertDialog.Builder(activity)
.setMessage(R.string.support_request)
.setPositiveButton(R.string.support_request_allowed) { _, _ ->
scope.launch {
activity.sendEmail(
recipient,
R.string.support_email_subject,
getDiagnosticFile(errorCode, screenOrigin),
getEmailBodyText(isError)
)
}
}
.setNegativeButton(R.string.support_request_denied) { _, _ ->
activity.sendEmail(
recipient,
R.string.support_email_subject,
emailBody = getEmailBodyText(isError)
)
}.show()
}
fun sendVerificationEmail(
activity: Activity,
recipient: String = AppConfig.supportEmail,
) {
activity.sendEmail(
recipient,
R.string.verification_email_subject,
emailBody = R.string.verification_email_body_error
)
}
fun sendInvalidCodeEmail(
activity: Activity,
recipient: String = AppConfig.supportEmail,
) {
activity.sendEmail(
recipient,
R.string.send_data_code_invalid_header,
emailBody = R.string.no_code_email_body_error
)
}
private suspend fun getDiagnosticFile(errorCode: String?, screenOrigin: String): Uri {
return withContext(Dispatchers.IO) {
val file = File(context.cacheDir, context.getString(R.string.support_file_name))
file.writeText(generateDiagnosticText(errorCode, screenOrigin))
FileProvider.getUriForFile(
context,
context.getString(R.string.fileprovider_authorities),
file
)
}
}
private suspend fun generateDiagnosticText(errorCode: String?, screenOrigin: String): String {
return withContext(Dispatchers.Default) {
val lastExposureDaysSinceEpoch = db.dao().getLatest().firstOrNull()?.daysSinceEpoch
val lastNotifiedExposureImportTimestamp =
db.dao().getLastNotified().firstOrNull()?.importTimestamp
var text =
if (errorCode != null) formatLine(R.string.support_error_code, errorCode) else ""
text += formatLine(R.string.support_app_version, BuildConfig.VERSION_NAME)
text += formatLine(
R.string.support_system_version,
"Android ${deviceInfo.getAndroidVersion()}"
)
text += formatLine(
R.string.support_device,
"${deviceInfo.getManufacturer()} ${deviceInfo.getDeviceName()}"
)
text += formatLine(R.string.support_localization, LocaleUtils.getLocale())
text += formatLine(
R.string.support_bluetooth,
"${
deviceInfo.isBtEnabled().toOnOff()
} (${
deviceInfo.supportsBLE().toSupports("BLE")
}, ${deviceInfo.supportsMultiAds().toSupports("MultiAds")})"
)
text += formatLine(
R.string.support_location_services,
deviceInfo.isLocationEnabled().toOnOff()
)
text += formatLine(
"EN API Enabled",
exposureNotificationsRepository.isEnabled().toOnOff()
)
text += formatLine(
"EN API Status",
exposureNotificationsRepository.getStatus().joinToString { it.name }
)
text += formatLine(
R.string.support_primary_account,
deviceInfo.isUserDeviceOwner().toOnOff()
)
text += formatLine("Internet", context.isNetworkAvailable().toOnOff())
text += formatLine("Battery saver", deviceInfo.isBatterySaverOn().toOnOff())
text += formatLine(
R.string.support_battery_optimization_exception,
deviceInfo.isIgnoringBatteryOptimizations().toOnOff()
)
text += formatLine(R.string.support_installation, context.getInstallDay())
text += formatLine(
R.string.support_last_key_download,
prefs.getLastKeyImport().timestampToDateTime()
)
text += formatLine(
R.string.support_last_notified_risky_encounter,
lastNotifiedExposureImportTimestamp?.timestampToDateTime() ?: "N/A"
)
text += formatLine(
R.string.support_last_risky_encounter_from,
lastExposureDaysSinceEpoch?.daysSinceEpochToDateString() ?: "N/A"
)
text += formatLine(
R.string.support_screen_origin,
screenOrigin
)
text
}
}
@StringRes
private fun getEmailBodyText(isError: Boolean): Int {
return if (isError) {
R.string.support_email_body_error
} else {
R.string.support_email_body_contact
}
}
private fun formatLine(label: Int, value: String): String {
return "${context.getString(label)}: $value\n"
}
private fun formatLine(label: String, value: String): String {
return "$label: $value\n"
}
private fun Boolean.toOnOff(): String {
return if (this) "ON" else "OFF"
}
private fun Boolean.toSupports(subject: String): String {
return if (this) "${context.getString(R.string.support_bluetooth_supports)} $subject" else "${
context.getString(
R.string.support_bluetooth_doesnt_support
)
} $subject"
}
}
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/Text.kt
================================================
package cz.covid19cz.erouska.utils
import android.content.Context
import android.widget.TextView
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.databinding.BindingAdapter
import kotlinx.android.parcel.RawValue
sealed class Text {
data class CharText(val charSequence: CharSequence) : Text()
data class ResText(
@StringRes val resId: Int,
val formatArgs: List<@RawValue Any> = emptyList()
) : Text()
data class ResQuantityText(
@PluralsRes val resId: Int,
val quantity: Int,
val formatArgs: List<@RawValue Any> = emptyList()
) : Text()
data class ResolvedText(val resolver: @RawValue TextResolver): Text()
fun toCharSequence(context: Context): CharSequence {
return when (this) {
is CharText -> charSequence
is ResText -> if (formatArgs.isEmpty()) context.getText(resId) else context.getString(
resId,
*formatArgs.resolverFormatArgs(context).toTypedArray()
)
is ResQuantityText -> if (formatArgs.isEmpty()) context.resources.getQuantityString(
resId, quantity, quantity
) else context.resources.getQuantityString(
resId,
quantity,
*formatArgs.resolverFormatArgs(context).toTypedArray()
)
is ResolvedText -> resolver(context)
}
}
}
private fun List.resolverFormatArgs(context: Context): List {
return this.map { if (it is Text) it.toCharSequence(context) else it }
}
typealias TextResolver = (Context) -> CharSequence
fun CharSequence.toText() = Text.CharText(this)
fun @receiver:StringRes Int.toText(vararg formatArgs: Any) = Text.ResText(this, formatArgs.toList())
fun @receiver:PluralsRes Int.toQuantityText(quantity: Int, vararg formatArgs: Any) =
Text.ResQuantityText(this, quantity, formatArgs.toList())
@BindingAdapter("android:text")
fun setText(textView: TextView, text: Text?) {
textView.text = text?.toCharSequence(textView.context)
}
private val PHONE_REGEX = Regex("""(\+\d+)?\s*(\d{3})\s*(\d{3})\s*(\d{3})""")
================================================
FILE: app/src/main/kotlin/cz/covid19cz/erouska/utils/ViewUtils.kt
================================================
package cz.covid19cz.erouska.utils
import android.view.View
import cz.covid19cz.erouska.ext.hide
import cz.covid19cz.erouska.ext.show
fun View.showOrHide(show: Boolean) {
if (show) {
show()
} else {
hide()
}
}
================================================
FILE: app/src/main/res/drawable/highlight_selector.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_about.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_ack_case.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_act_case.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_action_close.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_action_up.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_active.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_antigen.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_arrow_right.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_balloon.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_bluetooth_onboard.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_calendar.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_chat.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_confirm.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_contacts.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_control.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_cured.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_data.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_death_toll.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_encounter.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_error.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_eval.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_exposure_info.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_face_mask.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_google_play_services.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_help.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_home.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_hospitalized.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_how_it_works_banner.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_injection_complete.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_injection_first.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_item_empty.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_launcher_foreground.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_mask.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_mzcr.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_no_risky_encounter.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_notif.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_notifications_sent.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_notifications_shown.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_off_bluetooth.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_off_location.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_pause.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_positive.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_prevention.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_privacy.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_restriction.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_risky_encounter.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_shortcut_resume.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_splashscreen_hands.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_splashscreen_logo.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_symptoms.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_test.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_travel.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_update_expansion.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_vacc.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_warn.xml
================================================
================================================
FILE: app/src/main/res/drawable/launchscreen.xml
================================================
================================================
FILE: app/src/main/res/drawable-anydpi-v24/ic_notification_normal.xml
================================================
================================================
FILE: app/src/main/res/drawable-night/ic_splashscreen_hands.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_ragnarok.xml
================================================
================================================
FILE: app/src/main/res/layout/dashboard_card_view.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_about.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_activation.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_activation_notifications.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_contacts.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_dashboard_plus.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_efgs.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_efgs_agreement.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_efgs_update.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_error.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_exposure.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_exposure_help.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_exposure_info.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_help.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_help_category.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_help_question.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_help_search.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_how_it_works.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_my_data.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_no_verification_code.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_play_services_update.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_publish_success.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_recent_exposures.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_sandbox.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_sandbox_config.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_sandbox_data.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_symptom_date.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_traveller.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_verification.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_welcome.xml
================================================
================================================
FILE: app/src/main/res/layout/item_contacts.xml
================================================
================================================
FILE: app/src/main/res/layout/item_daily_summary.xml
================================================
================================================
FILE: app/src/main/res/layout/item_exposure_help.xml
================================================
================================================
FILE: app/src/main/res/layout/item_exposure_help_title.xml
================================================
================================================
FILE: app/src/main/res/layout/item_exposure_window.xml
================================================
================================================
FILE: app/src/main/res/layout/item_help_about_category.xml
================================================
================================================
FILE: app/src/main/res/layout/item_help_faq_category.xml
================================================
================================================
FILE: app/src/main/res/layout/item_help_how_category.xml
================================================
================================================
FILE: app/src/main/res/layout/item_help_question.xml
================================================
================================================
FILE: app/src/main/res/layout/item_recent_exposure.xml
================================================
================================================
FILE: app/src/main/res/layout/item_recent_exposure_group_header.xml
================================================
================================================
FILE: app/src/main/res/layout/item_scan_instance.xml
================================================
================================================
FILE: app/src/main/res/layout/item_scan_instance_header.xml
================================================
================================================
FILE: app/src/main/res/layout/item_search.xml
================================================
================================================
FILE: app/src/main/res/layout/item_search_layout.xml
================================================
================================================
FILE: app/src/main/res/layout/item_tek.xml
================================================
================================================
FILE: app/src/main/res/layout/layout_sandbox_config_values.xml
================================================
================================================
FILE: app/src/main/res/layout/search_toolbar.xml
================================================
================================================
FILE: app/src/main/res/layout/traveller_dashboard_card_view.xml
================================================
================================================
FILE: app/src/main/res/layout/view_data_item.xml
================================================
================================================
FILE: app/src/main/res/menu/bottom_nav.xml
================================================
================================================
FILE: app/src/main/res/menu/dashboard.xml
================================================
================================================
FILE: app/src/main/res/menu/exposure.xml
================================================
================================================
FILE: app/src/main/res/menu/onboarding.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: app/src/main/res/navigation/nav_graph.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#2f80ed#2874ed#EE225B#000#444#fff#e8e8e8#ff0000#d81d49#8cc23f#DB7627#DF6061#DE1A1A#CCCCCC@color/white@android:color/black#C9E2F4#BBBBBB#e8e8e8true#FFF500#fff@color/white#717171
================================================
FILE: app/src/main/res/values/controls.xml
================================================
================================================
FILE: app/src/main/res/values/dimens.xml
================================================
16dp32dp56dp24dp
================================================
FILE: app/src/main/res/values/ids.xml
================================================
================================================
FILE: app/src/main/res/values/strings-notranslate.xml
================================================
Days since onset symptoms: %dReport type: %sRolling start: %s, refresh %sLast download: %s
================================================
FILE: app/src/main/res/values/strings.xml
================================================
eRouškaHelpBack to appEnable BluetoothEnableEnable BluetoothEnabling Bluetooth is necessary to log encounters with other eRouška users near you.eRouška is activeeRouška is pausedApplication is running in the background and monitoring your surroundings. Keep Bluetooth enabled and use your phone as usual.We will notify you if you encountered someone with COVID-19, and show you all important information.Last update on %1$s at %2$s.Application is now paused and is not collecting any data about other eRouška applications nearby. Resume the app and keep yourself and others safe. Don\'t forget to do that especially when leaving your home.Loading data...PauseStartWe value your privacy. You are always in control of your data.eRouška does not have any personal information about you and collects only anonymous data about other eRouška applications that you have encountered.Terms and conditions.]]>Finish activationPrivacyErrorActivation could not be finishedTry againHelpNewsStarting with version 2.0, eRouška is being developed by the Ministry of Health in collaboration with the National agency for communication and information technologies (NAKIT). Earlier versions of eRouška were developed by a team of volunteers from the COVID19CZ community. Most of the original eRouška developers continue to work on newer versions in the NAKIT team.Terms and conditions.]]>Terms and conditionsAbout applicationThe permission to access location needs to be granted to make the application work properly.You can find more information about why that is in Help.SettingsDismissHow does it work?Continue to activation If any of the users test positive for COVID-19, the app will notify you that there was a risky encounter.
If you test positive, the app will notify all application users that you encountered.]]>eRou\u0161ka will help you protect yourself and others tooContactsAnonymously notify othersSentShare eRouškaShare applicationHi, I\'m using eRouška. Install it so we can help stop the spread of COVID-19 together. The application can anonymously notify you about a risky encounter and recommend further steps. The application can be found at %sDelete registrationAbout applicationYour device seems to be offline.Application on the backgroundChat with Anežka – eRouška supportCurrent measuresInformation on current measuresCurrent situation and case numberseRouška in numbers%1$s yesterday%1$s on %2$s%1$s applications activated%1$s people who tested positive anonymously notified others%1$s notifications about risky encounters sent%1$s PCR tests performed%1$s antigen tests performed%1$s confirmed cases in total%1$s active cases%1$s cured individuals%1$s deceased%1$s currently hospitalized%1$s vaccine doses administeredIf you test positive for COVID-19 you will receive an SMS message with the verification code to notify other eRouška users.You last notified others on %s.Verification codeVerify dataVerification code was not entered correctlyVerification code has expiredRequest a new SMS message with a verification code from the public health officer.We couldn\'t send your dataPlease contact support at %1$s and mention this error code in the email: \'%2$s\'.BackCloseTry againData were successfully sentThank you for fighting against the spread of COVID-19.You have activated eRouška only recently, so it has not logged any other eRouška users. Therefore, the app will not notify anyone about a risky encounter.Please cooperate with public health officers on tracing everyone that you encountered.Follow the instructions of public health officers and medical workers.Date of first symptomsPlease enter the date of first symptoms. We will notify other eRouška apps that you met during this period.I am experiencing symptoms of COVID-19Cough, high temperature, laboured breathing, sore throat, headache, loss of smell and taste.Date of first symptomsContinueCross-border travelHave you travelled to an EU country in the past 14 days?Thanks to cross-border cooperation, you can notify users of foreign apps that you have met during your travel abroad about the risk of infection.Yes, I have been abroadI have not been abroadCross-border cooperationDo you consent to notifying users of foreign apps who you have met in the Czech Republic or abroad?Do you consent to notifying users of foreign apps that you have met in the Czech Republic?the personal data processing agreement.]]>I agreeI do not agreeRisky encounters updateCloseRisky encounterYou have recently encountered someone who tested positive for COVID-19. Watch your health status closely. Find more info in the app.Don\'t forget to resume itRisky encounterRisky encounters updateeRouška is pausedEnable COVID-19 Exposure NotificationseRouška cannot communicate with other eRouška applications nearby.\n\nEnable COVID-19 Exposure Notifications by pressing \"Enable\".Risky encountersCloseMore informationI have symptomsI don\'t have symptomsUpdate Google Play ServicesThe latest version of Google Play Services is needed to enable Exposure Notifications about risky encounters.UpdateNotificationsTo work properly, eRouška requires COVID-19 Exposure Notifications to be enabledThese notifications will indicate that you have encountered a user with eRouška who tested positive for COVID-19.The app can only work correctly if it collects and shares random identifiers which change automatically. We cannot identify an eRouška user based on these identifiers.ContinueCloseEnable Location ServicesAlthough we do not use your location data, please enable Location Services so that Bluetooth and eRouška work correctly.Enable Location ServicesEnable Bluetooth and Location ServicesWith Bluetooth and Location Services disabled, eRouška cannot log other devices near you.Although we do not use your location data, the app needs your permission to access Bluetooth and Location Services. Please enable them by pressing \"Enable Location Services\".Enable Bluetooth and Location ServicesNo risky encountersRisky encounters discovered on %s at %sPreviously discovered risky encountersFailed to activate eRouškaReason: %sYour device does not support Bluetooth Low Energy.Only the primary account on this device can activate eRouška.Your device does not support Bluetooth Multiple Advertisement.Your device does not support Exposure Notifications by Google.Unauthorised API usage.OKSearchWhat are you looking for?Previous resultNext resultNo search resultsWe haven\'t found anything yet, so keep typingHow eRouška worksFind out more about how the application worksOn %s, you last encountered someone who tested positive for COVID-19.You received the notification at least 24 hours after the infected individual tested positive for COVID-19 and entered their verification code in eRouška.\n\nThe encounter will be evaluated as risky if it was longer than 15 minutes and closer than 2 meters apart.\n\nTo keep identities anonymous, we do not know the time or place of the encounter.Next steps%1$d risky encounter in the past 14 days%1$d risky encounters in the past 14 daysOn %1$s, you last encountered someone who tested positive for COVID-19.Have you been tested for COVID-19?Notify others if your test is positive.Anonymously notify othersCross-border cooperationEnabledDisabledEmail supportWhen you press \"Email support\" we will create an email attachment with anonymous information about your device\'s settings (such as enabled Bluetooth, phone type etc.) and the error. This information will help us troubleshoot.Do you wish to attach a file with anonymous information about your device\'s settings? This information will help us troubleshoot.Yes, attach the fileNo, do not attach the fileSelect email application:Feedback from eRouškaError codeApplication versionSystem versionDeviceLocalizationBluetoothsupportsdoes NOT supportLocation ServicesPrimary accountBattery optimization exceptionInstallationKeys last downloadedLast notification of a risky encounterLast risky encounter ondiagnostic_information.txtTell us what we can help you with:\n\nTell us what happened before the error:\n\nScreenChecking for risky encounterseRouška is checking for risky encounters with COVID-19.Checking for risky encountersHow does eRouška work?Email supportAre you curious how eRouška works?Find out moreCloseeRouška will notify you about risky encounters with other eRouška users that you have encountered in the past 14 days and that have tested positive for COVID-19.\nWhen using eRouška, nobody can collect your location information or other data about you or your phone.Phones with eRouška are waiting for encountersWhen two phones with eRouška meet, they exchange some basic data about the encounter. They log the date, distance between each other, and an anonymous phone identifier. They will remember this encounter for 14 days.If COVID-19 infection is confirmedIf a person is tested positive for COVID-19, they will automatically receive an SMS with a verification code for eRouška. By entering the code in the application, the person can anonymously notify other eRouška users.Warnings by other eRouška apps"After you enter and confirm the code, eRouška sends out notifications about possible risky encounters to other eRouška users. "Processing encounters with infected individualsThe following day at the latest, other eRouška apps will check the information about newly infected individuals and compare it to those they have met.Risky encounters evaluationDisplaying notificationsThose who may have encountered an infected person will receive a warning in the app. It will explain the user what further steps and health measures to take.Do you still have more questions or eRouška does not work properly? Contact us.Check your internet connection and try again.Please contact support at %1$s and mention this error code in the email: %2$s.When you press \"Email support\" we will create an email attachment with anonymous information about your device\'s settings (such as enabled Bluetooth, phone type etc.) and the error. This information will help us troubleshoot.You have already used this code or it has expired. Request a new code at %1$s.Email supportI have not received the SMS with a verification code"Dear eRouška team,\n\nmy COVID-19 test is positive and I have not received the SMS with a verification code.\n\n\nFull name on the test request form: \n\n\nPhone number on the test request form: \n\n\nDate of test results: \n\n\nTesting location or laboratory which handled my test results: \n\n\nTest type (PCR/antigen): "I don\'t have the codeHave you tested positive but have not received your eRouška code in an SMS? Request your code at %1$s.Are you interested in learning how sending the code works? \n\n\nIf you are tested positive for COVID-19, you will receive test results in an SMS from the laboratory. The lab also sends the results to the central information system of the Ministry of Health. \n\n\nAfter the results are copied from the information system to the public health office\'s system, automatic eRouška SMS messages with verification codes are sent. \n\n\nIf the lab results are not copied in the public health office\'s system by 10 pm, you might not receive the SMS with the verification code until the next day. \n\n\nIf you did not receive eRouška SMS within one day after you received your results from the lab, please contact us at %1$s.Cross-border cooperationHelp to fight against COVID-19 when abroadCOVID-19 does not respect borders. Thanks to cooperation of EU countries, eRouška can warn you against risk of infection if you encountered foreigners who use similar applications of other countries.Please enable notifications about foreign risky encounters if you have been abroad in the past %1$s days or you travel regularly.You can change the settings anytime.Notify me about risky encounters abroadThis feature may increase the volume of downloaded data. Disable it %1$d days after you return from abroad.%1$d days after you return from abroad.]]>Keep enabledDisableVerification code is not validCheck the code and try entering it again. If a valid code fails repeatedly, contact us at %1$s."Dear eRouška team,\n\nmy COVID-19 test is positive and my verification code for eRouška has failed. What shall I do next?\n\n\nFull name on the test request form: \n\n\nPhone number on the test request form: \n\n\nDate of test results: \n\n\nTesting location or laboratory which handled my test results: \n\n\nTest type (PCR/antigen): "Vaccinations%1$s people vaccinated with 1 dose%1$s people fully vaccinatedMore info
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/main/res/values/themes.xml
================================================
================================================
FILE: app/src/main/res/values-cs/strings.xml
================================================
eRouškaNápovědaZpět do aplikaceZapnout BluetoothZapnoutZapněte BluetoothZapnuté Bluetooth je důležité pro sbírání kontaktů s ostatními eRouškami ve vašem okolí.eRouška je aktivníeRouška je pozastavenáAplikace aktuálně pracuje na pozadí a monitoruje okolí. Nechte zapnuté Bluetooth a s telefonem pracujte jako obvykle.Upozorníme vás v případě možného podezření na setkání s COVID-19 a zobrazíme vám všechny potřebné informace.Poslední aktualizace %1$s v %2$s.Aplikace je aktuálně pozastavená a nesbírá žádná data o ostatních eRouškách ve vašem okolí. Spusťte znovu eRoušku a chraňte sebe i své okolí. Nezapomínejte na to zejména ve chvíli, kdy opouštíte svůj domov.Probíhá aktualizace dat…PozastavitSpustitMyslíme na vaše soukromí, odesílání dat máte vždy pod kontroloueRouška neobsahuje žádné vaše osobní údaje a sbírá pouze anonymní data o ostatních eRouškách, se kterými se setkáte.podmínkách používání.]]>Dokončit aktivaciSoukromíChybaAktivaci aplikace nelze dokončitZkusit znovuNápovědaAktuálněAplikaci eRouška od verze 2.0 vyvíjí Ministerstvo zdravotnictví ve spolupráci s Národní agenturou pro komunikační a informační technologie (NAKIT). Předchozí verzi aplikace eRouška vytvořil tým dobrovolníků v rámci komunitní aktivity COVID19CZ. Většina z původních autorů eRoušky pokračuje na vývoji nových verzí v týmu NAKIT.podmínkách používání.]]>Podmínky použitíO aplikaciPro spr\u00e1vnou funkci aplikace je nutn\u00e9 opr\u00e1vn\u011bn\u00ed k\u00a0p\u0159\u00edstupu k\u00a0poloze.V\u00edce o\u00a0tom, pro\u010d tomu tak je, najdete v\u00a0n\u00e1pov\u011bd\u011b.NastaveníZrušitJak to fungujePokra\u010dovat k\u00a0aktivaci Pokud se u majitele kteréhokoliv z nich potvrdí onemocnění COVID-19, eRouška vyhodnotí, zda se jedná o rizikový kontakt a upozorní vás.
Když se potvrdí nákaza u vás, eRouška upozorní všechny ostatní uživatele aplikace, se s kterými jste se potkali.]]>D\u00edky eRou\u0161ce ochr\u00e1n\u00edte sebe i\u00a0ostatn\u00ed ve sv\u00e9m okol\u00edKontaktyAnonymně upozornit ostatníOdeslánoSdílet aplikaci eRouškaSdílet aplikaciAhoj, používám aplikaci eRouška. Nainstaluj si ji taky a společně pomožme zastavit šíření onemocnění COVID-19. Aplikace dokáže anonymně a včas upozornit na rizikové setkání s nakaženým uživatelem a doporučit další postup. Najdeš ji na %sZrušit registraciO\u00a0aplikaciVaše zařízení nemá aktivní přístup k internetu.Aplikace na pozadíNapište Anežce – podpoře eRouškyAktuální opatřeníInformace o aktuálních opatřeníchAktuální situace v číslecheRouška v číslechZa včerejší den %1$s%1$s za %2$s%1$s aktivovaných aplikací%1$s pozitivně testovaných anonymně varovalo ostatní%1$s upozornění na riziková setkání%1$s provedených PCR testů%1$s provedených antigenních testů%1$s celkem potvrzených případů%1$s aktivních případů%1$s vyléčených%1$s úmrtí%1$s aktuálně hospitalizovaných%1$s vykázaných očkováníV případě potvrzené nákazy onemocněním COVID-19 obdržíte SMS zprávu obsahující ověřovací kód pro povolení upozornění ostatních uživatelů eRoušky.Naposledy jste upozornili ostatní %s.Ověřovací kódOvěřitOvěřovací kód není správně zadanýVypršela platnost ověřovacího kóduPožádejte pracovníka hygienické stanice o zaslání nové SMS zprávy s ověřovacím kódem.Nepodařilo se nám odeslat dataKontaktujte prosím podporu na emailu %1$s a uveďte následující kód chyby: \'%2$s\'.ZpětZavřítZkusit znovuData jste úspěšně odeslaliDěkujeme, že pomáháte bojovat proti šíření onemocnění COVID-19.eRoušku zatím používáte jen chvíli a nemá zaznamenané žádné jiné eRoušky, proto nikoho ve vašem okolí neupozorní na rizikové setkání.Spolupracujte prosím s pracovníky hygienické stanice na dohledání všech osob, se kterými jste byli v kontaktu.Řiďte se pokyny hygieniků a lékařů.Datum prvních příznakůVyplňte prosím datum prvních příznaků. Upozorníme všechny uživatele eRoušky, které jste potkali ve dnech, kdy se u vás objevily první příznaky.Mám příznaky onemocnění COVID-19Kašel, teplota, dušnost, bolest v krku, bolest hlavy, ztráta čichu a chuti.Datum prvních příznakůPokračovatCesty do zahraničíCestovali jste v posledních 14 dnech do některé ze zemí Evropské unie?Díky spolupráci mezi zeměmi Evropské unie můžete upozornit o riziku možné nákazy i uživatele zahraničních aplikací, které jste potkali při svých cestách do zahraničí.Ano, byl(a) jsem v zahraničíNebyl(a) jsem v zahraničíSpolupráce se zahraničímSouhlasíte s upozorněním uživatelů zahraničních aplikací, které jste potkali v České republice i v zahraničí?Souhlasíte s upozorněním uživatelů zahraničních aplikací, které jste potkali v České republice?souhlasu se zpracováním osobních údajů.]]>SouhlasímNesouhlasímAktualizace rizikových setkáníZavřítRizikové setkáníSetkali jste se s osobou, u které bylo potvrzeno onemocnění COVID-19. Sledujte svůj zdravotní stav. Více informací naleznete v aplikaci.Nezapomeňte ji spustitRizikové setkáníAktualizace rizikových setkáníPozastavená eRouškaZapněte Oznámení o kontaktu s COVID-19eRouška nyní nemůže komunikovat s jinými eRouškami ve vašem okolí.\n\nZapněte Oznámení o kontaktu s COVID-19 pomocí tlačítka \"Zapnout\".Riziková setkáníZavřítVíce informacíMám příznakyNemám příznakyAktualizujte aplikaci Služby Google PlayNejnovější verze aplikace Služby Google Play je nezbytná pro aktivaci oznámení o setkání s osobou, u které bylo potvrzeno onemocnění COVID-19.AktualizovatOznámeníeRouška potřebuje pro správné fungování zapnutá oznámení o kontaktu s COVID-19Tato oznámení vás upozorní, pokud jste byli v blízkosti uživatele eRoušky s pozitivním testem na onemocnění COVID-19.Podstatou správného fungování je sdílení a shromažďování náhodných identifikačních čísel, která se automaticky mění. Uživatele eRoušky není možno podle těchto identifikátorů rozpoznat.PokračovatZavřítZapněte Polohové službyI když polohová data nijak nevyužíváme, zapněte Polohové služby pro správné fungování Bluetooth a aplikace eRouška.Zapnout Polohové službyZapněte Bluetooth a Polohové službyBez zapnutého Bluetooth a Polohových služeb nemůže eRouška vytvářet seznam ostatních eRoušek ve vašem okolí.I když polohová data nijak nevyužíváme, zapněte obě funkce pro správné fungování Bluetooth pomocí tlačítka \"Zapnout BT a Polohové služby\".Zapnout BT a Polohové službyŽádná riziková setkáníRiziková setkání zjištěná %s v %sStarší riziková setkáníeRoušku nelze aktivovatDůvod: %sToto zařízení nepodporuje Bluetooth Low Energy.eRoušku lze aktivovat jenom na primárním účtu tohoto zařízení.Toto zařízení nepodporuje Bluetooth Multiple Advertisement.Toto zařízení nepodporuje Oznámení o možném kontaktu od Google.Nepovolený přístup k API.OKHledatNapište, co hledátePředešlý výsledekNásledující výsledekŽádné výsledky vyhledáváníZatím jsme nic nenašli, pokračujte ve psaníJak eRouška fungujeZjistěte více o tom, jak aplikace fungujeNaposledy %s jste se setkali s osobou, u které bylo potvrzeno onemocnění COVID-19Upozornění se vám zobrazilo nejdříve 24 hodin poté, co se nakažený dozvěděl pozitivní výsledek testu na COVID-19 a zadal ověřovací kód do eRoušky.\n\nRizikový kontakt vyhodnotíme v případě, že jste s nakaženým byli v kontaktu na vzdálenost kratší než 2 metry po dobu alespoň 15 minut.\n\nKvůli zachování anonymity nakaženého neznáme čas ani místo setkání.Jak postupovat dálZa posledních 14\u00A0dní %1$d\u00A0rizikové setkáníZa posledních 14\u00A0dní %1$d\u00A0riziková setkáníZa posledních 14\u00A0dní %1$d\u00A0rizikových setkáníNaposledy %1$s jste se setkali s osobou, u které bylo potvrzeno onemocnění COVID-19.Byli jste na testu COVID-19?Pokud je výsledek pozitivní, upozorněte ostatní.Anonymně upozornit ostatníSpolupráce se zahraničímZapnutoVypnutoNapsat e-mail na podporuPo stisknutí tlačítka “Napsat e-mail na podporu” vytvoříme přílohu e-mailu s anonymními informacemi o nastavení telefonu (například zapnuté Bluetooth, typ telefonu) a chybě. Tyto informace nám pomohou s řešením chyby.Chcete do e-mailu přidat přílohu s anonymními informacemi o nastavení telefonu? Tyto informace nám pomohou s řešením případné chyby.Chci přidat přílohuNechci přidat přílohuVyberte e-mailovou aplikaci:Zpětná vazba z aplikace eRouškaKód chybyVerze aplikaceVerze systémuZařízeníLokalizaceBluetoothpodporujeNEpodporujePolohové službyPrimární účetVýjimka z optimalizace baterieInstalacePoslední stažení klíčůPoslední notifikace rizikového setkáníPoslední rizikové setkání zdiagnosticke_informace.txtNapište nám, s čím vám můžeme poradit:\n\nNapište nám, jak k chybě došlo:\n\nObrazovkaProbíhá kontrola rizikových kontaktůeRouška zjišťuje, zda jste byli v rizikovém kontaktu s onemocněním COVID-19.Kontrola rizikových kontaktůJak eRouška funguje?Napsat e-mail na podporuZajímá vás, jak eRouška funguje?Zjistit víceZavříteRouška vás upozorní na riziková setkání s ostatními uživateli eRoušky, které jste potkali v posledních 14 dnech a potvrdilo se u nich onemocnění COVID-19.\n\nPři používání aplikace eRouška nikdo nezná vaši polohu a žádné jiné údaje o vás nebo telefonu.Telefony s eRouškou čekají na setkáníKdyž se potkají telefony s aplikací eRouška, vymění si několik základních informací o setkání. Zaznamenají si den setkání, vzdálenost mezi sebou a anonymní označení telefonu. Tato setkání si pamatují 14 dní.Když se potvrdí COVID-19Pokud se u někoho prokáže onemocnění COVID-19, dostane automaticky SMS s ověřovacím kódem pro eRoušku. Zadáním kódu v aplikaci odemkne možnost anonymně varovat ostatní uživatele.Varování ostatních eRoušekPo zadání a potvrzení kódu dojde k informování ostatních eRoušek o možném rizikovém setkání.Zpracování setkání s nakaženýmiOstatní eRoušky nejpozději následující den zkontrolují informace o nově nakažených a porovnají je s těmi, které potkaly.Vyhodnocení rizikových setkáníZobrazení varováníTěm, kteří mohli přijít do rizikového kontaktu s nakaženým, se v aplikaci eRouška zobrazí upozornění. To navede uživatele, jak má dále postupovat a jaká hygienická opatření má dodržovat.Máte další dotazy nebo vám eRouška nefunguje správně? Napište nám.Zkontrolujte připojení k internetu a zkuste to znovu.Kontaktujte prosím podporu na e-mailu %1$s a uveďte následující kód chyby: %2$s.Po stisknutí tlačítka “Napsat e-mail na podporu” vytvoříme přílohu e-mailu s anonymními informacemi o nastavení telefonu (například zapnuté Bluetooth, typ telefonu) a chybě. Tyto informace nám pomohou s řešením chyby.Platnost kódu vypršela nebo jste ho už jednou použili. Pro nový kód napište na %1$s.Napsat e-mailNepřišla mi SMS s ověřovacím kódemMilý týme eRoušky,\nmám pozitivní test na COVID-19 a nepřišla mi SMS s ověřovacím kódem pro eRoušku.\n\nCelé jméno uvedené na žádance o test: \n\nTelefonní číslo uvedené na žádance o test: \n\nDatum výsledku testů: \n\nOdběrové místo / laboratoř, ze které mi přišly výsledky testu: \n\nTyp testu (PCR/antigen): Nemám ověřovací kódMáte pozitivní test a nepřišla vám SMS pro eRoušku? Napište si o náhradní kód na %1$s.Zajímá vás, jak odesílání kódu funguje?\n\nV případě, že máte pozitivní výsledek testu na COVID-19, měl by vám přijít výsledek testu v SMS od testovací laboratoře. Ta výsledky současně odesílá do centrálního informačního systému Ministerstva zdravotnictví.\n\nPoté, co se informace z informačního systému propíšou do systému hygieny, se odesílají automatické SMS eRoušky s ověřovacími kódy.\n\nPokud se výsledky z laboratoře nestihnou propsat do systému hygieny do 22:00, může vám SMS s ověřovacím kódem přijít až následující den.\n\nPokud vám SMS eRoušky nepřišla ani následující den poté, co vám přišla SMS s pozitivními výsledky z laboratoře, napište nám na %1$s.Spolupráce se zahraničímPomozte v boji s COVID-19 i při cestách do zahraničíCOVID-19 nezná hranice a díky spolupráci mezi evropskými státy vás eRouška může upozornit na riziková setkání i v případě, že jste se potkali s cizinci, kteří používají podobné aplikace jiných států.Povolte upozornění na zahraniční riziková setkání, pokud jste byli v posledních %1$s dnech v zahraničí nebo tam jezdíte pravidelně.Nastavení můžete kdykoliv změnit v aplikaci.Upozorňovat na zahraniční riziková setkáníVyužívání této funkce může způsobit vyšší objem stahovaných dat. Vypněte ji %1$d dní po návratu ze zahraničí.%1$d dní po návratu.]]>Ponechat zapnutéVypnoutZadaný kód není platnýZkontrolujte kód a zkuste ho zadat znovu. V případě, že se vám opakovaně nedaří zadat platný kód, kontaktujte nás na %1$s.Milý týme eRoušky,\nmám pozitivní test na COVID-19 a nedaří se mi zadat správně ověřovací kód pro eRoušku. Jak mám dále postupovat?\n\nCelé jméno uvedené na žádance o test: \n\nTelefonní číslo uvedené na žádance o test: \n\nDatum výsledku testů: \n\nOdběrové místo / laboratoř, ze které mi přišly výsledky testu: \n\nTyp testu (PCR/antigen): Očkování%1$s očkovaných první dávkou%1$s osob s ukončeným očkovánímVíce informací
================================================
FILE: app/src/main/res/values-night/colors.xml
================================================
#2f80ed#2874ed#6200FF@color/white#eee@android:color/black@android:color/white#001829@android:color/blackfalse#8D2F80ED#000#000@color/white
================================================
FILE: app/src/main/res/values-sk/strings.xml
================================================
eRouškaNápovedaSpäť do aplikácieZapnúť BluetoothZapnúťZapnite BluetoothZapnutý Bluetooth je dôležitý na zbieranie kontaktov s ostatnými eRouškami vo vašom okolí.eRouška je aktívnaeRouška je pozastavenáAplikácia aktuálne pracuje na pozadí a monitoruje okolie. Nechajte zapnuté Bluetooth a s telefónom pracujte ako obvykle.Upozorníme vás v prípade možného podozrenia na stretnutie s COVID-19 a zobrazíme vám všetky potrebné informácie.Posledná aktualizácia %1$s o %2$s.Aplikácia je aktuálne pozastavená a nezbiera žiadne údaje o ostatných eRouškach vo vašom okolí.
Spustite znovu zber dát a chráňte seba aj svoje okolie. Nezabúdajte na to hlavne v situácii, keď opúšťate svoj domov.Prebieha aktualizácia dát...PozastaviťSpustiťMyslíme na vaše súkromie, odosielanie údajov máte vždy pod kontroloueRouška neobsahuje žiadne vaše osobné údaje a zbiera iba anonymné dáta o ostatných eRouškach, s ktorými se stretnete.podmienkach používania.]]>Dokončiť aktiváciuSúkromieChybaAktiváciu aplikácie nie je možné dokončiťSkúsiť znovaNápovedaAktuálneAplikáciu eRouška od verzie 2.0 vyvíja Ministerstvo zdravotníctva ČR v spolupráci s Národnou agentúrou pre komunikačné a informačné technológie (NAKIT). Predošlú verziu aplikácie eRouška vytvoril tím dobrovoľníkov v rámci komunitnej aktivity COVID19CZ. Väčšina z pôvodných autorov eRoušky pokračuje na vývoji nových verzií v tíme NAKIT.podmienkach používania.]]>Podmienky používaniaO aplikáciiPre spr\u00e1vnu funkciu aplik\u00e1cie je nutn\u00e9 opr\u00e1vnenie k pr\u00edstupu k polohe.Viac o tom, pre\u010do to tak je, n\u00e1jdete v n\u00e1povede.NastaveniaZrušiťAko to fungujePokra\u010dova\u0165 na aktiv\u00e1ciu Pokiaľ sa u majiteľa ktoréhokoľvek z nich potvrdí ochorenie COVID-19, eRouška vyhodnotí, či ide o rizikový kontakt a upozorní vás.
Keď sa potvrdí nákaza u vás, eRouška upozorní všetkých ostatných užívateľov aplikácie, s s ktorými ste sa stretli.]]>V\u010faka eRou\u0161ke ochr\u00e1nite seba aj ostatn\u00fdch vo svojom okol\u00edKontaktyAnonymne upozorniť ostatnýchOdoslanéZdieľať aplikáciu eRouškaZdieľať aplikáciuAhoj, používam aplikáciu eRouška. Nainštaluj si ju tiež a spoločne pomôžme zastaviť šírenie ochorenia COVID-19. Aplikácia dokáže anonymne a včas upozorniť na rizikové stretnutie s nakazeným užívateľom a odporučiť ďalší postup. Nájdeš ju na %sZrušiť registráciuO aplik\u00e1ciiVaše zariadenie nemá aktívny prístup k internetu.Aplikácia na pozadíNapíšte Anežke - podpore eRouškyAktuálne opatreniaInformácie o aktuálnych opatreniachAktuálna situácia v číslacheRouška v číslachZa včerajší deň %1$s%1$s za %2$s%1$s aktivovaných aplikácií%1$s pozitívne testovaných anonymne upozornilo ostatných%1$s upozornení na rizikové stretnutia%1$s vykonaných testov%1$s vykonaných antigénnych testov%1$s celkom potvrdených prípadov%1$s aktívnych prípadov%1$s vyliečených%1$s úmrtí%1$s aktuálne hospitalizovaných%1$s vykázaných očkovaníV prípade potvrdenej nákazy ochorením COVID-19 obdržíte SMS správu obsahujúcu overovací kód pre povolenie upozornenia ostatných užívateľov eRoušky.Naposledy ste upozornili ostatných %s.Overovací kódOveriťOverovací kód nie je správne zadanýVypršala platnosť overovacieho kóduPožiadajte pracovníka hygienickej stanice o zaslanie novej SMS správy s overovacím kódomNepodarilo sa nám odoslať dátaKontaktujte, prosím, podporu na emaili %1$s a uveďte nasledujúci kód chyby: \'%2$s\'.SpäťZatvoriťSkúsiť znovuÚdaje ste úspešne odoslaliĎakujeme, že pomáhate bojovať proti šíreniu ochorenia COVID-19eRoušku zatiaľ používate len chvíľu a nemá zaznamenané žiadne iné eRoušky, preto nikoho vo vašom okolí neupozorní na rizikové stretnutie.Spolupracujte, prosím, s pracovníkmi hygienickej stanice na dohľadaní všetkých osôb, s ktorými ste boli v kontakte.Riaďte sa pokynmi hygienikov a lekárov.Dátum prvých príznakovVyplňte, prosím, dátum prvých príznakov. Upozorníme všetkých užívateľov eRoušky, ktorých ste stretli v dňoch, kedy sa u vás objavili prvé príznaky.Mám príznaky ochorenia COVID-19Kašeľ, teplota, dýchavičnosť, bolesť hrdla, bolesť hlavy, strata čuchu a chuti.Dátum prvých príznakovPokračovaťCesty do zahraničiaCestovali ste v posledných 14 dňoch do niektorej z krajín Európskej únie?Vďaka spolupráci medzi krajinami Európskej únie môžete upozorniť o riziku možnej nákazy aj užívateľov zahraničných aplikácií, ktorých ste stretli pri svojich cestách do zahraničia.Áno, bol(a) som v zahraničíNebol(a) som v zahraničíSpolupráca so zahraničímSúhlasíte s upozornením užívateľov zahraničných aplikácií, ktorých ste stretli v Českej republike i v zahraničí?Súhlasíte s upozornením užívateľov zahraničných aplikácií, ktorých ste stretli v Českej republike? súhlasu so spracovaním osobných údajov.]]>SúhlasímNesúhlasímAktualizácia rizikových stretnutíZatvoriťRizikové stretnutieStretli ste sa s osobou, u ktorej bolo potvrdené ochorenie COVID-19.
Sledujte svoj zdravotný stav. Viac informácií nájdete v aplikácii.Nezabudnite ju spustiťRizikové stretnutieAktualizácia rizikových stretnutíPozastavená eRouškaZapnite oznámenia o kontakte s COVID-19eRouška momentálne nemôže komunikovať s inými eRouškami vo vašom okolí.\n\nZapnite Oznámenia o kontakte s COVID-19 pomocou tlačidla \"Zapnúť\".Rizikové stretnutiaZatvoriťViac informáciíMám príznakyNemám príznakyAktualizujte aplikáciu Služby Google PlayNajnovšia verzia Google Play Services je potrebná na aktiváciu Oznámení o stretnutí s osobou, u ktorej bolo potvrdené ochorenie COVID-19.AktualizovaťUpozorneniaeRouška potrebuje na správne fungovanie zapnuté Oznámenia o kontakte s COVID-19Tieto oznámenia vás upozornia, ak ste boli v blízkosti užívateľa eRoušky s pozitívnym testom na ochorenie COVID-19Podstatou správneho fungovania je zdieľanie a zhromažďovanie náhodných identifikačných čísel, ktoré se automaticky menia. Uživateľov eRoušky nie je možné podľa týchto identifikátorov rozpoznať.PokračovaťZatvoriťZapnite Polohové službyAj keď dáta o polohe nijako nevyužívame, zapnite Polohové služby pre správne fungovanie Bluetooth a aplikácie eRouška.Zapnúť Polohové službyZapnite Bluetooth a Polohové službyBez zapnutého Bluetooth a Polohových služieb nemôže eRouška vytvárať zoznam ostatných eRoušok vo vašom okolí.Aj keď údaje o polohe nijak nevyužívame, zapnite, prosím, obe funkcie pre správne fungovanie Bluetooth pomocou tlačidla \"Zapnúť BT a Polohové služby\".Zapnúť BT a Polohové službyŽiadne rizikové stretnutiaRizikové stretnutia zistené %s o %sStaršie rizikové stretnutiaeRoušku nie je možné aktivovaťDôvod: %sToto zariadenie nepodporuje Bluetooth Low Energy.eRoušku je možné aktivovať iba na primárnom účte tohto zariadenia.Toto zariadenie nepodporuje Bluetooth Multiple Advertisement.Toto zariadenie nepodporuje Oznámenia o možnom kontakte od Google.Nepovolený prístup k API.OKHľadaťNapíšte, čo hľadátePredošlý výsledokNasledujúci výsledokŽiadne výsledky vyhľadávaniaZatiaľ sme nič nenašli, pokračujte v písaníAko eRouška fungujeZistite viac o tom, ako aplikácia fungujeNaposledy %s ste sa stretli s osobou, ktorej bolo potvrdené ochorenie COVID-19Upozornenie sa vám zobrazilo najskôr 24 hodín po tom, čo sa nakazený dozvedel pozitívny výsledok testu na COVID-19 a zadal overovací kód do eRoušky.\n\nRizikový kontakt vyhodnotíme v prípade, že ste s nakazeným boli v kontakte na vzdialenosť kratšiu ako 2 metre po dobu aspoň 15 minút.\n\nKvôli zachovaniu anonymity nakazeného nepoznáme čas ani miesto stretnutia.Ako postupovať ďalejZa posledn\u00fdch 14 dn\u00ed %1$d rizikov\u00e9 stretnutiaZa posledn\u00fdch 14 dn\u00ed %1$d rizikov\u00e9 stretnutieZa posledn\u00fdch 14 dn\u00ed %1$d rizikov\u00fdch stretnut\u00edZa posledn\u00fdch 14 dn\u00ed %1$d rizikov\u00e9 stretnutiaDňa %1$s ste sa stretli s osobou, u ktorej bolo potvrdené ochorenie COVID-19.Máte pozitívny výsledok COVID-19 testu?Ak je výsledok pozitívny, upozornite ostatných.Anonymne upozorniť ostatnýchSpolupráca so zahraničímZapnutéVypnutéNapísať e-mail podporePo stlačení tlačidla \"Napísať e-mail na podporu\" vytvoríme prílohu e-mailu s anonymnými informáciami o nastavení telefónu (napríklad zapnuté Bluetooth, typ telefónu) a chybe. Tieto informácie nám pomôžu s riešením chyby.Chcete do e-mailu pridať prílohu s anonymnými informáciami o nastavení telefónu? Tieto informácie nám pomôžu s riešením prípadnej chyby.Chcem pridať prílohuNechcem pridať prílohuVyberte e-mailovú aplikáciu:Spätná väzba z aplikácie eRouškaKód chybyVerzia aplikácieVerzia systémuZariadenieLokalizáciaBluetoothpodporujeNEpodporujePolohové službyPrimárny účetVýnimka z optimalizácie batérieInštaláciaPosledné stiahnutie kľúčovPosledná notifikácia rizikového stretnutiaPosledné rizikové stretnutie zdiagnosticke_informacie.txtNapíšte nám, s čím vám môžeme poradiť:\n\nNapíšte nám, ako k chybe došlo:\n\nObrazovkaPrebieha kontrola rizikových kontaktoveRouška zisťuje, či ste boli v rizikovom kontakte s ochorením COVID-19.Kontrola rizikových kontaktovAko eRouška funguje?Napísať e-mail na podporuZaujíma vás, ako eRouška funguje?Zistiť viacZavrieťeRouška vás upozorní na rizikové stretnutie s ostatnými užívateľmi eRoušky, ktorých ste stretli v posledných 14 dňoch a potvrdilo sa u nich ochorenie COVID-19.\n\nPri používaní aplikácie eRouška nikto nepozná vašu polohu a žiadne iné údaje o vás alebo telefóne.Telefóny s eRouškou čakajú na stretnutieKeď sa stretnú telefóny s aplikáciou eRouška, vymenia si niekoľko základných informácií o stretnutí. Zaznamenajú si deň stretnutia, vzdialenosť medzi sebou a anonymné označenie telefónu. Tieto stretnutia si pamätajú 14 dní.Keď sa potvrdí COVID-19Ak sa u niekoho preukáže ochorenie COVID-19, dostane automaticky SMS s overovacím kódom pre eRoušku. Zadaním kódu v aplikácii odomkne možnosť anonymne varovať ostatných užívateľov.Varovanie ostatných eRoušiekPo zadaní a potvrdení kódu dôjde k informovaniu ostatných eRoušiek o možnom rizikovom stretnutí.Spracovanie stretnutí s nakazenýmiOstatné eRoušky najneskôr nasledujúci deň skontrolujú informácie o novo nakazených a porovnajú ich s tými, ktoré stretli.Vyhodnotenia rizikových stretnutíZobrazenie varovaniaTým, ktorí mohli prísť do rizikového kontaktu s nakazeným, sa v aplikácii eRouška zobrazí upozornenie. To navedie používateľa, ako má ďalej postupovať a aké hygienické opatrenia má dodržiavať.Máte ďalšie otázky alebo vám eRouška nefunguje správne? Napíšte nám.Skontrolujte pripojenie k internetu a skúste to znova.Kontaktujte, prosím, podporu na emaili %1$s a uveďte nasledujúci kód chyby: %2$s.Po stlačení tlačidla \"Napísať e-mail na podporu\" vytvoríme prílohu e-mailu s anonymnými informáciami o nastavení telefónu (napríklad zapnuté Bluetooth, typ telefónu) a chybe. Tieto informácie nám pomôžu s riešením chyby.Platnosť kódu vypršala alebo ste ho už raz použili. Pre nový kód napíšte na %1$s.Napísať e-mailNeprišla mi SMS s overovacím kódomMilý tím eRoušky, <br>mám pozitívny test na COVID-19 a neprišla mi SMS s overovacím kódom pre eRoušku. <br><br>Celé meno uvedené na žiadanke o test: <br>Telefónne číslo uvedené na žiadanke o test: <br>Dátum výsledku testov: <br>Odberové miesto/laboratórium, z ktorého mi prišli výsledky testu: <br>Typ testu (PCR/antigén): <br><br><br>Nemám overovací kódMáte pozitívny test a neprišla vám SMS pre eRoušku? Napíšte si o náhradný kód na %1$s.Zaujíma vás, ako odosielanie kódu funguje? \n\nV prípade, že máte pozitívny výsledok testu na COVID-19, mal by vám prísť výsledok testu v SMS od testovacieho laboratória. To výsledky súčasne odosiela do centrálneho informačného systému Ministerstva zdravotníctva. \n\nPo tom, čo sa informácie z informačného systému prepíšu do systému hygieny, sa odosielajú automatické SMS eRoušky s overovacími kódmi. \n\nAk sa výsledky z laboratória nestihnú prepísať do systému hygieny do 22:00, môže vám SMS s overovacím kódom prísť až nasledujúci deň. Ak vám SMS eRoušky neprišla ani nasledujúci deň po tom, čo vám prišla SMS s pozitívnymi výsledkami z laboratória, napíšte nám na %1$s.Spolupráca so zahraničímPomôžte v boji s COVID-19 aj pri cestách do zahraničiaCOVID-19 nepozná hranice a vďaka spolupráci medzi európskymi štátmi vás eRouška môže upozorniť na rizikové stretnutie aj v prípade, že ste sa stretli s cudzincami, ktorí používajú podobné aplikácie iných štátov.Povoľte upozornenia na zahraničné rizikové stretnutia, ak ste boli v posledných %1$s dňoch v zahraničí alebo tam jazdíte pravidelne.Nastavenie môžete kedykoľvek zmeniť v aplikácii.Upozorňovať na zahraničné rizikové stretnutiaVyužívanie tejto funkcie môže spôsobiť vyšší objem sťahovaných dát. Vypnite ju %1$d dní po návrate zo zahraničia.%1$d dní po návrate.]]>Ponechať zapnutéVypnúťZadaný kód nie je platnýSkontrolujte kód a skúste ho zadať znova. V prípade, že sa vám opakovane nedarí zadať platný kód, kontaktujte nás na %1$s.Milý tím eRoušky,\nmám pozitívny test na COVID-19 a nedarí sa mi zadať správne overovací kód pre eRoušku. Ako mám ďalej postupovať?\n\nCelé meno uvedené na žiadanke o test:\n\nTelefónne číslo uvedené na žiadanke o test:\n\nDátum výsledku testov:\n\nOdberové miesto/laboratórium, z ktorej mi prišli výsledky testu:\n\nTyp testu (PCR/antigén):Očkovanie%1$s očkovaných prvou dávkou%1$s osob s ukončeným očkovanímViac informácií
================================================
FILE: app/src/main/res/xml/file_paths.xml
================================================
================================================
FILE: app/src/main/res/xml/remote_config_defaults.xml
================================================
v2_reportTypeWeights1;1;1;1;1;1v2_infectiousnessWeights0;0.6;2.0v2_attenuationBucketThresholdDb55;70;80v2_attenuationBucketWeights2.0;1;0.25;0v2_minimumWindowScore420v2_covidDataServerUrlhttps://europe-west1-erouska-key-server-dev.cloudfunctions.netv2_minGmsVersionCode203019000v2_keyImportDataOutdatedHours99999999v2_riskyEncountersTitleAnOn %1$s, you last encountered someone who tested positive for COVID-19.v2_keyImportPeriodHours6v2_keyExportUrlhttps://cdn.erouska.cz/v2_recentExposuresUITitlePrevious risky encountersv2_spreadPreventionUITitlePrinciples of responsible behaviourv2_symptomsUITitleMain symptomsv2_exposureUITitleRisky encountersv2_chatBotLinkhttps://erouska.cz/caste-dotazyv2_noEncounterBodyWe will notify you about a risky encounter in a notificationv2_noEncounterHeaderYou have not encountered anyone known to have COVID-19 in the past 14 days.v2_conditionsOfUseUrlhttps://erouska.cz/podminky-pouzivaniv2_riskyEncountersWithoutSymptomsYou don't have to stay at home if you are not experiencing any symptoms of COVID-19 infection. Behave responsibly and watch your health status.v2_riskyEncountersWithSymptomsIf you experienced respiratory symptoms within 14 days after encountering an infected person, call your general practitioner and follow their instructions. If you experience serious health problems call 155.v2_riskyEncountersTitleYou encountered a person infected with COVID-19 on %@.v2_contactsContentJson[{"title":"Thinking you are infected?","text":"Stay at home and call your general practitioner.","linkTitle":"Important contacts","link":"https://koronavirus.mzcr.cz/en/important-phone-numbers/"},{"title":"Need help?","text":"You can find questions and answers about COVID-19 on the Ministry of Health's website.","linkTitle":"Frequently asked questions","link":"https://koronavirus.mzcr.cz/"},{"title":"eRouška support","text":"Do you need help with the app? Send us e-mail.","linkTitle":"Send e-mail","link":"mailto:info@erouska.cz"},{"title":"About eRouška","text":"Are you curious how the app works?","linkTitle":"Visit erouska.cz","link":"https://erouska.cz/en"}]v2_preventionContentJson{"title":"COVID-19 infection is spread mainly via airborne droplets. It can also be transmitted via contaminated objects.","items":[{"iconUrl":"https://erouska.cz/img/prevention/ic_clean_hands.png","label":"Wash your hands with soap thoroughly and frequently, and use disinfection."},{"iconUrl":"https://erouska.cz/img/prevention/ic_disinfenction.png","label":"Disinfect your personal belongings (e.g. mobile phone) on a regular basis."},{"iconUrl":"https://erouska.cz/img/prevention/ic_tissue.png","label":"When coughing or sneezing, cover your mouth with a paper tissue or your sleeve."},{"iconUrl":"https://erouska.cz/img/prevention/ic_social_distance.png","label":"Avoid crowds and keep a safe 2-metre distance."},{"iconUrl":"https://erouska.cz/img/prevention/ic_mask.png","label":"In shops and on public transport, always wear respiratory protection as stated by currently valid regulations."},{"iconUrl":"https://erouska.cz/img/prevention/ic_stay_home.png","label":"If you experience respiratory symptoms, stay at home and call your general practitioner."}]}v2_symptomsContentJson{"title":"Symptoms may appear 2–14 days after a risky encounter.","items":[{"iconUrl":"https://erouska.cz/img/symptoms/ic_temperature.png","label":"High temperature"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_cough.png","label":"Cough"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_stuffiness.png","label":"Stuffiness"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_throatache.png","label":"Sore throat"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_headache.png","label":"Headache, muscle and joint pain"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_smelltaste.png","label":"Loss of smell and taste"}]}v2_encounterWarningYou have encountered a person infected with COVID-19. Watch your health status.v2_verificationServerApiKey4AP6RHk5VlNi7WIhj4ZupI9JZODdUyrPGb1C2mDYKo4cuaGIHdYtOhjyoqhg4vB5r7FDijxnySLb_1CUH6XdDA.1.oZLaUuVOPbvEaiWnzkQxasWlD_71iSTeM9aAIOKrfqhg5QO68t004CKxWutYmfISc7vqJe6uY2dRrt34OFkliwv2_helpMarkdown# Frequent problems\n## Android: Basic recommendations for troubleshooting\nIf the app displays an error message please check that your operating system is updated ([how to update Android](https://support.google.com/android/answer/7680439)) and that you are not using its beta version.\n\nThen, please check whether there are new updates of eRouška in your store ([Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska)).\n\nWe also recommend to enable automatic eRouška app updates. [Steps for Android are:](https://support.google.com/googleplay/answer/113412#autoone) Open Google Play Store app, press My apps & games → choose the app to update → press three dots and select Enable auto update.\n\n## Android: Cannot activate eRouška. Activation error 17 (Exposure Notification API is not available).\nOn Android devices, please follow these steps:\n\n1. Check if you are the _primary user_ of the device, that Play Services are installed, and that _Settings → Google → COVID-19 Exposure Notifications_ is available.\n2. Uninstall eRouška.\n3. Clear cache and data for Google Play Services and Play Store ([see steps 2 and 3](https://support.google.com/googleplay/answer/9037938)).\n4. Install eRouška and enable _COVID-19 Exposure Notifications_.\n\nIf you still experience issues with app activation, it is possible that your device does not support Bluetooth LE (Low Energy Advertising) or Bluetooth Multiple Advertisement, is not authorized for Exposure Notifications by Google, or does not have Exposure Notifications API.\n\n# Connection and data transmission\n## Do I need to keep Bluetooth turned on all the time?\nYes, you do. Without Bluetooth turned on, it is not possible to detect nearby devices with eRouška. You may have already enabled Bluetooth if you use wireless headphones, a smart watch or a connection with your car. We recommend you to set Bluetooth to be _visible for all devices_.\n\n## Does eRouška need internet connection?\nInternet connection is necessary to download (install) and activate eRouška. It also needs internet connection to download anonymous identifiers of infected users every day and display notifications about risky encounters.\n\nIf you are tested positive for COVID-19 and would like to anonymously notify others of a risky encounter, you would have to connect eRouška to the internet. Until then, your data is saved only in your phone's memory and is deleted every 14 days. The application also uses internet connection to download information about the epidemic's current development in the Czech Republic which is displayed on the _News_ page.\n\n## How often does the app update online data?\neRouška downloads keys (identifiers) of infected users several times a day. Statistics on the News page are updated once a day.\n\n## How much data does eRouška consume daily?\nIf _Notifications about foreign risky encounters_ is enabled, eRouška will also download keys of all infected individuals from other EU countries. The volume of downloaded data may be up to 2 MB daily.\n\nEven if _Notifications about foreign risky encounters_ are disabled, eRouška downloads about 1 MB of data every day. The new app version downloads keys not only from eRouška users, but also from infected users from other EU countries who indicated that they were travelling abroad and whom an eRouška user might have encountered.\n\nKeys of infected individuals are downloaded 2–3 times a day. To compare, an average web page requires about 1–5 MB of data. **The application does not necessarily need mobile internet.** It can download data when connected to WiFi.\n\n## Android: Why do I need to allow the application to access GPS/location data?\n**Update for Android 11 users:** You do not need to enable location services for the newest Android operating system versions for eRouška to work.\n\neRouška does not collect or store GPS data, but the operating system **Android 10 and older** also includes some Bluetooth LE (LE = low energy) services, which eRouška components need in order to function properly – specifically, Google Play Services. Therefore, the user's consent to the application's access to location data is required. This consent is not required on iOS. [Google explains](https://support.google.com/android/answer/9930236) why location services must be enabled in the phone.\n\nOther applications in your phone might use information on your location. If you want to check which apps use your location, go to your phone's settings → Location → App permission (or follow [these steps by Google](https://support.google.com/accounts/answer/6179507?hl=en)). You can deny location permission to other apps if you think they do not need it. eRouška does not need it so it will not be listed.\n\n## How much battery do Bluetooth and eRouška app drain?\nIf you already use a phone with Bluetooth enabled, power consumption should not increase significantly. If you do not actively use Bluetooth, the results of our testing where Bluetooth was turned on all the time showed that energy consumption increased by units of percent per day. In most cases, it is less than 20% of battery capacity per day. It depends on the specific smartphone, your style of use and battery condition.\n\nPower consumption related to eRouška use is affected by 2 factors. First, it's the application itself, and second, it's the use of the system's Exposure Notification which regularly logs encounters with other eRouška users. The system function is provided by operating system Android or iOS so its optimization depends on updates from Apple and Google.\n\nWhen interpreting power consumption, please consider that this percentage depends on your phone's full use during the past 24 hours (proportion of use by other apps). That means that the percentage of eRouška use might be high in low-use phones, while it might be low in highly used phones. So it does not show the percentage of battery use.\n\n**Android: Note for location services:** It is only possible to detect a Bluetooth device near you if you enable "Use location" in the settings. (Read more in [Why your phone’s location setting needs to be on? on Google's help page](https://support.google.com/android/answer/9930236?hl=cs).) It means that other applications in your phone might use information on your location, which might be the reason for a higher power consumption. Please check which apps use your location. Go to your phone's settings → Location → App permission (or follow [these steps by Google](https://support.google.com/accounts/answer/6179507?hl=cs)). You can deny location permission to other apps if you think they do not need it. eRouška does not need it so it will not be listed.\n\n## Is it possible that after turning on eRouška, my Wi-Fi or internet connection slowed down?\nUnfortunately, some Android mobile phones' Bluetooth negatively affects Wi-Fi signal and vice versa.\n\n## Can eRouška cause problems with other apps or devices that use Bluetooth (headphones, watches, wristbands)?\nVery rarely, users might experience strange behaviour of their Bluetooth devices after they install eRouška, such as connection interruptions, typically in smart watches or wristbands, wireless headphones or handsfree sets. Most probably, it is an operating system issue which we are not able to affect.\n\nPlease try to apply the following steps:\n\n1. Restart your phone.\n2. Unpair your Bluetooth device and pair it again.\n\nIf these steps do not help please contact your device's manufacturer:\n\n* Android: [Get help from your device manufacturer](https://support.google.com/android/answer/3094742?ref_topic=7313011&hl=en)\n\n## Why does the app use Bluetooth? Are there no better alternatives such as GPS or triangulation from phone network operators?\nUser privacy comes first, and Bluetooth technology offers the best balance between logging accuracy and maximal privacy. Neither GPS technology nor operator triangulation can provide necessary data with the required accuracy. Among other things, it is because their functionality inside buildings or metro is very limited. eRouška needs to know that an encounter has occurred, at what distance and its duration. It does not matter where the encounter took place.\n\n## Is Bluetooth a secure enough technology?\nBluetooth is a widely spread standard for connecting mobile devices with other devices such as wireless headphones, speakers, smart wristbands or smart watches. Since Apple and Google have developed the common protocol only for national Bluetooth LE-based contact tracing mobile applications, we are convinced that it was the right choice for eRouška as well. However, it has to be said that each and every technology has vulnerabilities. That is why we recommend our users to regularly update their apps as well as the operating system. The OS updates often contain important security patches.\n\n# General information\n## What should I do when the application notified me about a risky encounter with an infected user?\nIf you have symptoms of infection (e.g. high temperature, cough, stuffiness, sore throat, headache, or a sudden loss of smell or taste), call your general practitioner. In case of an emergency, call 155 for an ambulance or 112 for the integrated rescue system. The mentioned symptoms may appear between 2 and 14 days after the risky encounter.\n\nIf you have no symptoms please act responsibly:\n\n* Wear a mask or respirator over your mouth and nose.\n* Wash your hands with soap thoroughly and use hand sanitizer regularly.\n* Sanitize your personal belongings (e.g. your mobile phone) regularly.\n* Cough or sneeze into a tissue or your sleeve.\n* Use disposable tissues and throw them away after use.\n* Avoid unnecessary large group gatherings and keep a safe distance (approximately 2 meters).\n\nIf you receive a notification in eRouška and suspect that you encountered an infected person, call your general practitioner and follow their instructions. It is also possible that a public health officer will contact you after epidemiological investigation with the infected person. In that case, follow the officer's instructions.\n\nIf your health deteriorates within 14 days after the risky encounter and you experience COVID-19 symptoms, contact your general practitioner.\n\n## What shall I do when I am positive for COVID-19 or I received an SMS with a verification code for eRouška?\nIf you received and SMS from the laboratory saying you are positive for COVID-19, you should also receive another SMS with the eRouška verification code within a few hours. eRouška messages are sent automatically after your results are saved in the information system. Automated processes of the Smart quarantine cannot influence the moment when the laboratory sends the results to the information system.\n\nIf you receive an eRouška SMS message with a verification code, please [follow these instructions](https://erouska.cz/en/sms). This will allow you to anonymously notify other users about the risky encounter. Thank you!\n\nHave you tested positive but have not received the eRouška SMS? Request your code at [info@erouska.cz](mailto:info@erouska.cz?subject=).\n\n## So every time the app says that I was in contact with and infected person, I will have to stay in quarantine or get tested?\nNo, eRouška is fully anonymous, using it is voluntary and a risky encounter is not a reason to enforce quarantine. If a person is tested positive, they will be contacted by the public health office to find out who they encountered in the previous days. If that person uses eRouška they can contact even those who they do not know, such as fellow passengers on a bus. By entering the verification code, they can send out notifications to all other eRouška users who they encountered in a risky manner.\n\nWhen the app notifies you that you have encountered a person tested positive for COVID-19, you should follow the recommendations. If you experience health problems contact your general practitioner immediately.\n\n## Will eRouška find out if I break my quarantine for example by going to my cottage?\nThe application doesn't track your location – it's not its purpose – and it's not designed for it either. It will not find out if you have broken your quarantine or where you have gone. On the contrary, it should help to quarantine only people with a confirmed infection or suspect cases.\n\n## Can eRouška issue a sick leave note for me? Is the exposure notification a reason for incapacity for work?\nNo, it cannot and it is not. If eRouška displayed the notification, follow the recommendations. Exposure notification does not substitute a report from your general practitioner or a laboratory which handles your COVID-19 test results. Only a doctor can issue valid documents that you can send to your employer.\n\n## Why is eRouška needed? Can it effectively protect me from the virus?\nOne of eRouška's goals is to stop further spread of the infection by isolating potentially infected individuals from the rest. It also helps prevent implementing country-wide measures that have a negative impact on the society and economy of the Czech Republic. Similar applications were also introduced in other countries and are an effective tool to fight against COVID-19.\n\neRouška 2.0 is a complementary application of the Smart quarantine – Ministry of Health's system. Using Bluetooth technology, eRouška connects to other eRouška apps nearby and saves their anonymous identifiers. If a user is tested positive for COVID-19, they will receive an SMS with a verification code which allows the user to send their anonymous identifiers to other eRouška apps. According to the epidemiological model\*, other users' apps will then evaluate whether they were in close enough contact to send out notifications and guide users to further investigation. Thus eRouška 2.0 helps to inform other potential transmitters of the infection and to speed up their testing. This decreases the number of infected individuals in the society and the risk of further infections.\n\n_\* Closer than 2 metres apart and longer than 7 minutes._\n\n## What if someone does not admit they are infected?\nThis can also happen. However, we believe that eRouška users want to actively help stop further spread of the virus, and use the application in order to alert people with whom they've come into risky contact. **Intentional spread of COVID-19 is a criminal offence in the Czech Republic.**\n\n## Is it worth installing eRouška if there are not enough people using it?\nThis is the reason why you should not hesitate. The more of us use eRouška, the better network to protect us and warn against risk we will create. Help us. You can actively join the fight against COVID-19 by installing the app. You can also tell people around you about it and help them install it.\n\n## How does eRouška differ from other similar apps?\neRouška uses Bluetooth technology which is used by most smart mobile devices in the Czech Republic. It works inside buildings, underground parking lots and the Metro. The application is designed not to collect your location information (e.g. via GPS). It only collects anonymous information on which other eRouška users you encountered closely. It is the only application in the Czech Republic that can officially use the Apple/Google protocol which guarantees the app's security on Android and iOD platforms and lowers battery use.\n\n## What is the Smart Quarantine? What is the role of eRouška in it?\nIt is a set of clever measures that protect people from contagion and the economy from collapsing. Instead of keeping the whole nation in quarantine, the Smart Quarantine enables finding, testing and isolating only infected individuals and people with suspected infection.\n\nMore specifically, it is a system of management in forecasting, decision support, active contact tracing, testing including sampling point capacity management, laboratories, managing quarantines and related material and IT delivery to contain the pandemic of COVID-19. Smart Quarantine 2.0 is the name for transition of the project under the Ministry of Health which will be able to quickly react to any epidemics in the future thanks to new tools that are being developed.\n\neRouška application is one of the tools that were developed to support Smart Quarantine. Using technology, we can actively notify risky individuals who encountered an infected person to implement more protective measures, consider voluntary quarantine, and contact their GP in case they experience COVID-19 symptoms. We believe that thanks to these steps, we can contain the spread of the infection and save the health care system from a fatal overload.\n\n# More information about eRouška\n## How does eRouška collect and process data about users' encounters?\nYour smartphone with eRouška will record anonymous identifiers (ID) from other devices with this application via Bluetooth LE. It stores information about the “encounter” and its length in its internal memory.\n\nIf your COVID-19 test is positive you will receive a verification code in an SMS. You will enter it in eRouška ([follow these steps](https://erouska.cz/en/sms)) which will send your anonymous identifiers to the server. Other eRouška apps will download them for evaluation. The algorithm in each eRouška will automatically evaluate the data within the epidemiological model: it will compare the identifiers with all logged encounters. For each risky encounter, the app will display a notification, warn the user that they might be potentially infected, and recommend further steps.\n\nYou can find detailed information about data collection and processing in [Information about personal data processing in eRouška 2.0](https://erouska.cz/en/podminky-pouzivani).\n\n## How does eRouška evaluate risky encounters?\nOriginally, epidemiologists established that a risky encounter is one that takes place closer than 2 meters apart for more than 15 minutes. However, aggressive transmission of new coronavirus mutations shows that even a shorter encounter might be risky.\n\nWe consulted Apple/Google and other countries on next steps regarding more aggressive mutations and we are gradually changing the parameters of risky encounter evaluation, beginning in March 2021. We periodically evaluate their effect on the number of risky encounters. These days, we are adjusting the time parameter, which is currently set to about 7 minutes.\n\neRouška strives to evaluate the parameters of distance and length of encounters as precisely as possible with the current technology. The distance between app users' phones is estimated by Bluetooth signal strength. The length of an encounter is considered periodically: every few minutes, the phones search for other phones with eRouška nearby.\n\nThe precision of these parameters is technologically limited. The bigger the distance and the shorter the encounter is, the more difficult it is to evaluate it. Loosening the parameters of evaluation thus increases the probability of false positive and false negative results.\n\nA higher precision requires more frequent logging of other devices with eRouška nearby. Unfortunately, that results in a higher energy consumption. Measurement algorithms are set up by Google and Apple in their Exposure Notification protocol on which eRouška is built. eRouška developers can only partially change these parameters and filter on the results. You can find more explanation in [Risk evaluation](https://erouska.cz/en/vyhodnoceni-rizika).\n\n## How does eRouška react to new variants or mutations of the virus?\nNew virus variants that have appeared since the pandemic started are examined regarding their infectiousness and health impact. eRouška developers are in close contact with both Czech administration and Apple and Google that provide the exposure protocol. Therefore, they are able to alter the evaluation algorithm when the risk of infection changes due to research advances.\n\n## What if eRouška misinterprets a contact with another person who is, for example, in a car at a crossroads, behind a door or a thin wall?\neRouška works by measuring the signal strength between two devices. Technically, it cannot be ruled out that a false detection occurs. You can read more about technical limitations, risks of app use and the process of encounter evaluation on [Reliability of risky encounter evaluation](https://erouska.cz/en/vyhodnoceni-rizika) and on [Technical requirements in Terms and conditions](https://erouska.cz/en/podminky-pouzivani#technicke).\n\nIn order to evaluate the contact as risky, it is important to measure the distance (under 2 m) and length of contact (several minutes) – therefore stopping at traffic lights is insignificant.\n\n## I received an exposure notification, but eRouška shows that it has not logged any risky encounter near me.\nIf the notification with red virus icon and text Exposure notification (Android) or Exposure log (iOS) surprised you, no need to worry. It is only a system notification on Android and iOS which eRouška cannot influence. View [this picture](https://www.facebook.com/eRouska/posts/180938253548928) which shows what eRouška notification looks like.\n\nYou can find out whether you encountered an infected individual on the main page of eRouška.\n\nIf you saw a risky encounter in eRouška which then disappeared, it is possible that the evaluation parameters have changed in the Android app update so eRouška does not consider that encounter to be risky anymore.\n\n## So everyone will know I have COVID-19?\nNo, they won't. If you are tested positive, your identity is only known to the public health officer. People that received a notification about a risky encounter will never know who, where or when they could have been infected. The application only displays the general information about risky encounters and recommends further steps. Your identity is safe.\n\n## I was tested for COVID-19, but I still have not received the SMS with the verification code for eRouška. When can I expect it?\nFirst, you will receive test results in an SMS from the laboratory. The lab also sends the results to the central information system of the Ministry of Health. After the results are copied to the public health office's system, automatic eRouška SMS messages with verification codes are sent. If the lab does not send the results to the information system on the same day, you might not receive the SMS with the verification code until the next day.\n\n**Please check that the eRouška SMS was not blocked by the system as spam by mistake**\n\n* **Android:** In your Messages app press the options icon (three dots) and Spam and blocked conversations (or three dots → Settings → SIM → Spam). If eRouška SMS is there, click on Not spam.\n\n**If you do not receive eRouška SMS even the day after** you received an SMS confirming your infection from the laboratory, send us a message to [info@erouska.cz](mailto:info@erouska.cz?subject=) asking for a new SMS code. Please state the following information:\n\n* full name on your test request form,\n* phone number on your test request form,\n* date of test results,\n* sampling address / laboratory which handled your test results,\n* test type (PCR/antigen).\n\n## Will I receive the code in an SMS if I tested positive in an antigen test?\nIf [your antigen test results positive and you experience COVID-19 symptoms](https://drive.google.com/file/d/1HPwtltNRuzmowjZgE_c1u-a8m3W89T-p/view) (stated in the request form), you will receive the eRouška code in an SMS same as in a positive PCR test.\n\nIf [your antigen test is positive but you do not experience COVID-19 symptoms](https://covid.gov.cz/situace/antigenni-testovani/interpretace-vysledku-antigenniho-testu-jak-se-zachovat-po-obdrzeni), you will undergo a PCR test. If that results positive, you will receive the code in an SMS. If your PCR test is negative, you will not send your data from eRouška.\n\n## In which countries can I use eRouška? How does cooperation with foreign apps work?\nEuropean Commission developed a central gateway service for a secure information exchange (EFGS). This gateway allows eRouška to exchange warnings with other tracing applications used in various European countries. You can find more information about the gateway in [Coronavirus: EU interoperability gateway for contact tracing and warning apps – Questions and Answers](https://ec.europa.eu/commission/presscorner/detail/cs/qanda_20_1905#gateway).\n\nIf you want to view more information on joined countries and settings, tap Cross-border cooperation on the home page in the app. You can enable or disable the feature and view the list of countries. You can find the official list of EU countries at [European Commission's website](https://ec.europa.eu/health/sites/health/files/ehealth/docs/gateway_jointcontrollers_en.pdf).\n\n## Does eRouška use Apple/Google API?\nThe new version eRouška 2.0 for Android and iOS uses Apple/Google Exposure Notification API. Remember to update your eRouška app in [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska) or [App Store (iOS)](https://apps.apple.com/cz/app/erou%C5%A1ka/id1509210215) and activate it again.\n\n## What is Apple/Google Exposure Notification API (protocol)?\nTogether, Apple and Google develop technologies which can be used by public health institutions (Ministry of Health) to develop national applications for tracing risky encounters with individuals with COVID-19.\n\nIt is an interface which allows specific apps (eRouška in the Czech Republic) to reliably access system tools of Android and Apple devices and log encounters – mainly to use Bluetooth LE to verify the contact and run in the background without any more settings of the app or the operation system. Encrypted communication protocol then ensures a safe transmission of current lists of identifiers of infected individuals between tracing applications in order to evaluate risky encounters and send notifications.\n\nFind more information directly on [Apple](https://www.apple.com/covid19/contacttracing) and [Google](https://www.google.com/covid19/exposurenotifications/) websites.\n\n## Where in eRouška can I find information on stored encounters?\neRouška 2.0 is built on Apple/Google Exposure Notification protocol which manages a secure storage and evaluation of encounters. Due to security reasons, it does not let the app directly access the data of encounters.\n\nOn the main screen, you can see whether the app is working correctly. Information about stored encounters is saved in the system. However, these encounters might not be the risky ones. You can find them in Settings:\n\n* Android: Settings → Google → COVID-19 Exposure Notifications → press three dots in the upper right corner → Exposure Check\n\n## What are the records in Settings → Exposure Check?\nRecording contacts with the infection show how many keys from infected users were downloaded on your phone from the server and how many of those match those that your phone logged.\n\n**Exposure Check:** Date and time when keys were downloaded from the server in the past 2 weeks. To view more you need to open individual records.\n\n**Android: Number of keys:** Number of keys from users who tested positive. Your phone downloaded these keys on that day to check for possible encounters with infected individuals.\n\n**Android: Number of matches:** Number of downloaded keys paired with keys found near you. If this number is higher than 0, you have encountered an infected individual. That does not necessarily indicate that it was a risky encounter.\n\n**Timestamp:** Date and time of the check. It is not the time of a risky encounter (the time is missing to protect the users' privacy).\n\n**Data source:** Application that checks and evaluates risky encounters.\n\n**Hashing code:** Check sum of the file that contains keys from infected users. Until the file contents change, the check sum will not change during the 2-week period.\n\n## How will I know that eRouška works on the background? Why don't I see the app icon in the status bar?\nWe removed notification icon because storing and evaluation of encounters is done by Apple/Google protocol in the system. The app is responsible for downloading keys (identifiers) of infected users once or twice a day and does not need to run permanently.\n\nYou can view the app status on its main page. You can then check that the keys were downloaded in the Settings:\n\n* **Android:** Settings → Google → COVID-19 Exposure Notifications → press three dots in the upper right corner → Exposure Check\n* **iOS**: Settings → Exposure Notifications → Exposure Logging Status → Exposure Checks\n\n## Why does the date and time of "Last data update" on the main page of eRouška differ from date and time of "Exposure Checks" that I can see in Settings?\neRouška shows the date and time of the last successful attempt to check new keys (identifiers) of infected individuals on the server. Phone Settings show date and time when the keys were last found and evaluated.\n\nKeys of infected users download once or twice a day.\n\n## Why can't I view the exact time of a risky encounter in eRouška?\neRouška is built on Apple/Google Exposure Notification protocol which manages logging and evaluation of encounters in the operating system. Apple/Google protocol allows eRouška to access only the date of a risky encounter so that the privacy of users is ensured.\n\n## Why do I see "Found keys: 0" in Exposure Checks in the phone Settings?\nIt means that Apple/Google API in your phone has not evaluated any encounter with an infected individual. In other words, downloaded keys of infected users on a given day do not match keys stored in your device.\n\n## Why do I see keys for specific dates in the phone's Settings > Exposure Checks but eRouška does not display any risky encounters?\nThe number of keys indicates the number of logged encounters with infected individuals. However, they might not be risky encounters. If an encounter is not risky, eRouška will not notify you about it. That's why you cannot see it in the app.\n\n## I received a notification about a risky encounter that took place several days ago. Why did eRouška only notify me now?\neRouška cannot evaluate risky encounters and display notifications before the user with a positive test sends data from their eRouška. If today you receive a notification that you have encountered an infected individual several days ago, it means that the person only sent their data today or yesterday. Your eRouška downloaded and evaluated the data today.\n\n## I received a notification about a risky encounter, but I can see several logs with various dates on the Risky encounter page.\neRouška evaluated risky encounters after a user who tested positive for COVID-19 sends data from their eRouška. If you received one notification but there are more logs on the Risky encounter page, it means that you have met that person more than once or that more than one user sent their data on the same day.\n\n## Bude eRouška fungovat jako „covid pas“ pro zobrazování výsledků testů a prokázání očkování?\nAnonymní charakter aplikace eRouška ji neumožňuje kombinovat s tzv. digitálním zeleným certifikátem (Digital Green Certificate), který pracuje s ověřenou identitou a osobními údaji uživatele – výsledky testů nebo provedeným očkováním proti COVID-19.\n\n## Does eRouška publish app usage statistics?\nWe publish statistics in JSON format at [stats.erouska.cz](https://stats.erouska.cz). The data have the following structure:\n\n{\\n "data": {\\n "modified": 1608696001,\\n "date": "20201223",\\n "activations\_yesterday": 2042,\\n "activations\_total": 1475571,\\n "key\_publishers\_yesterday": 718,\\n "key\_publishers\_total": 39175,\\n "notifications\_yesterday": 631,\\n "notifications\_total": 197298\\n }\\n}\n\n* **modified** = Unix timestamp of generated statistics,\n* **date** = date of report created,\n* **activations\_yesterday** = number of activations on the previous day,\n* **activations\_total** = total number of eRouška activations,\n* **key\_publishers\_yesterday** = number of infected individuals who entered the code on the previous day,\n* **key\_publishers\_total** = total number of infected individuals who entered the code,\n* **notifications\_yesterday** = number of individuals who received the notification about a risky encounter on the previous day,\n* **notifications\_total** = total number of individuals who received the notification about a risky encounter.\n\nA call without a parameter will display current day's information. A call with parameter `date` will allow you to get data for any day after October 23, 2020 (we only have aggregated data before then). Using parameter `date` with value `all`, you can get all data that the interface provides.\n\n* today's data: [`https://stats.erouska.cz/`](https://stats.erouska.cz/)\n* data from December 1, 2020: [`https://stats.erouska.cz/?date=2020-12-01`](https://stats.erouska.cz/?date=2020-12-01)\n* all days' data: [`https://stats.erouska.cz/?date=all`](https://stats.erouska.cz/?date=all)\n\n# Data security\n## How do I know which data the application stores and sends?\neRouška application 2.0 uses Apple/Google Exposure Notification API which allows for collecting data and transmitting identifiers among users. Thanks to that, the application does not have a direct access to logged data which could be displayed to users, as it did in the previous version 1.0.\n\nThe app downloads a list of identifiers from users who tested positive on a daily basis. Only the user can send data with identifiers to others, and only after they enter the SMS code that they will automatically receive when they test positive for COVID-19.\n\nThe app will then send anonymous aggregated statistical information that individuals in risk were notified. This information is not connected to any user or phone identifier and is only used to calculate statistics about eRouška's efficiency.\n\nTo make sure eRouška works correctly, we collect data such as crashes and app usage on your phone. We use standard tools (Firebase Crashlytics and Google Analytics) developed by Google. Telemetry sent into these tools does not contain any data about you or your phone.\n\nYou can find detailed information about data collection and processing in [Information about personal data processing in eRouška 2.0](https://erouska.cz/en/podminky-pouzivani).\n\n## Will eRouška data be copied after I perform phone recovery or if I get a new phone?\nTo protect your privacy, neither Android nor iOS back up data from Exposure Notifications. If you reinstall your phone's system or get a new phone, the stored identifiers will not be recovered.\n\nIf you want to receive the information about a possible risky encounter, we recommend you check your old phone over the period of 14 days after you install eRouška on your new device. It is necessary to:\n\n* make sure that your device is turned on and connected to the internet so that it can download evaluation data,\n* open eRouška on your old device at least once a day.\n* If you test positive during these 14 days, please enter your code in the new device. To receive the code for your older device, please contact us at [info@erouska.cz](mailto:info@erouska.cz).\n\nAfter 14 days, your old phone will not contain any data useful for risky encounter evaluation. You can uninstall eRouška from it and keep the app only in the new device.\n\n## What if my phone is stolen or I lose it?\nOnly anonymous information about other devices detected by eRouška is stored in the app. This does not bring any risk to you or the owners of other devices. Other applications that you commonly use may store much more sensitive data on your phone. Keep your phone protected with a passcode or biometrics (fingerprint or face).\n\n## Is eRouška compliant with GDPR?\nThe entire system of the application eRouška including the website is designed in full compliance with GDPR and the Personal Data Processing Act.\n\nYou will find more detailed information here:\n\n* [Information about personal data processing in eRouška 2.0](https://erouska.cz/en/podminky-pouzivani)\n* [Application source code audit](https://erouska.cz/en/audit-kod)\n\n## How do you protect users from data abuse?\nWe protect users primarily by minimizing the range of logged data and storing them directly in the device. User data is not sent or processed anywhere without their knowledge and consent. The app stores only anonymous identifiers (IDs) of other eRouška users and information about the time, length and Bluetooth strength. Data is automatically processed on the server only after they are sent by the user.\n\n## Does an independent organization oversee the security of application data?\nThe whole system of eRouška application including the supporting website is developed in full compliance with GDPR. The application's code is open-sourced ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)) and underwent [an audit by independent education institutions](https://erouska.cz/en/audit-kod).\n\nYou can find more information in [Information about personal data processing in eRouška 2.0](https://erouska.cz/en/podminky-pouzivani).\n\n## Can someone track my location using eRouška?\nNo. The application does not collect information about your location and only you have access to the saved data. The data that the app sends after your consent cannot be used to track your location. Moreover, for safety and privacy reasons, eRouška changes its ID regularly to prevent it from being abused.\n\n## Can I verify that the app is not recording my location?\nYes. eRouška application is published as open-sourced code ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)), so an informed person can easily verify that the application indeed does not collect location data.\n\nThe source code of eRouška [is reviewed by independent authorities](https://erouska.cz/en/audit-kod) who confirm that the application:\n\n* does not track location\n* does not automatically send data\n\n## Can a user's application identifier (ID) be paired with a specific person?\nApplication eRouška 2.0 [does not work with personal data](https://erouska.cz/en/podminky-pouzivani). Generated anonymous eRouška identifiers are sent between the applications and the server via an encrypted communication protocol developed by Apple and Google. Neither users, nor developers or other authorities can access the identifiers.\n\n## Who has (could have) access to my data?\nNeither the user not any other person can access logged data about encounters in eRouška 2.0. The reason for this is that this app version uses Apple/Google protocol which does not allow access to logged data to users, developers or other individuals. Data (anonymous identifiers of infected individuals) are sent to the server and other users' devices encrypted and are evaluated on the devices automatically if a notification about a risky encounter is needed to be displayed. They are deleted from the devices and the server automatically after 14 days.\n\n## How old data is available? When and how is it deleted?\neRouška saves the data for 14 days. Older entries are automatically deleted. If you are tested positive for COVID-19, enter your verification code in eRouška and send identifiers from your phone to the server, the data will stay there for 14 days after which they will be deleted. You can delete data from your phone in Exposure Notification settings. The specific steps differ among devices and operating system versions.\n\n* **Android:** On Android devices, uninstalling the app is enough to delete stored keys (identifiers).\n\nFor information about data controllers and processors, see the [Terms and Conditions](https://erouska.cz/en/podminky-pouzivani).\n\n## Can I change my mind about using the app at any time? Can I delete the data that I send out to be processed?\nYou can uninstall the application any time you want. You can delete stored identifiers from your phone in Exposure Notification settings. The specific steps differ among devices and operating system versions. Data which were sent to the server are automatically deleted after 14 days.\n\n* **Android:** On Android devices, uninstalling the app is enough to delete stored keys (identifiers).\n\n## What happens to the data after I delete the app from my smartphone?\n**Android:** Uninstalling the app is enough to delete stored keys (identifiers) from phones or tablets with Android OS.\n\nIf you reinstall the application, it will start collecting and storing brand new data in the phone.\n\n## Where can I find the application's source code?\neRouška is published with open-source code. You can find it on GitHub ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)).\n\n# Installation and compatibility\n## For which phones is eRouška available? Why are the requirements stricter than in eRouška 1.0?\nTo install eRouška, you need a mobile device with the following parameters:\n\n* iPhone with iOS 13.5 and higher\n* OS Android 6.0 and higher with Google Play Services (only a small percentage of Huawei phones and some phones with their own ROM do not have this)\n* Bluetooth LE (Low Energy)\n\nIf your phone is not compatible with eRouška (it does not have the necessary features) Google Play Store or Apple App Store will not let you install it. The application is available only for some Android tablets. It is not available for iPads.\n\nApple and Google define the minimum requirements of compatibility. They are based on the Apple/Google Exposure Notification protocol which the new eRouška uses. We decided to start using the Apple/Google protocol to make sure that the app works on iPhones and to be compatible with similar applications in other European countries.\n\n## Where can I safely download and install eRouška?\neRouška is only available for:\n\n* Android: [in Google Play Store](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska).\n\n## Is it possible to download the application outside official application stores? Is an apk available?\neRouška 2.0 uses Apple/Google protocol to log encounters and transmit data. Therefore, it depends on system services, without which it would not work, so it is not possible to install it from other than official app stores. Application stores will automatically filter for compatible devices.\n\neRouška developers never provide users with links for installation packages (APKs) or installation files outside official application stores. We would not be able to guarantee security, updates or user support for unofficial app installs. Please, do not download eRouška from .apk files that you find online or receive by email. They might contain viruses, trojan horses or other harmful content.\n\n## Can I use eRouška with another tracing application of a foreign country (e.g. Corona-Warn-App)?\nIf you spend most of your time in the Czech Republic, we recommend to use eRouška to travel abroad as well. [eRouška now cooperates with other EU countries](https://erouska.cz/en/caste-dotazy#efgs).\n\nIn other cases, you can install more tracing apps along eRouška, such as Corona-Warn-App, Stopp Corona App, Imunni, Stop Covid ProteGO Safe etc. However, if they also use Apple/Google Exposure Notification protocol (especially in neighbour countries), only one of them can be active at any given moment. You would need to pause the others. Only one app can access the Apple/Google protocol's logging feature at the same time.\n\n## Funguje eRouška, když mám na mobilu úsporný režim / režim nízké spotřeby / spořič baterie?\n**Android:** Spořiče/šetřiče baterie na zařízeních s OS Android mohou do značné míry omezovat běh aplikací na pozadí. Ačkoliv systém oznámení o možném kontaktu s onemocněním COVID-19 nadále funguje, nemusí docházet k vyhodnocování rizikových kontaktů, a tudíž k případnému zobrazení notifikací.\n\n## Will I be able to use eRouška on smart watches or smart wristbands?\nThis might be possible in the future. Right now, we are not developing this because the smart watch or wristband would need to support Bluetooth LE (Low Energy) and Apple/Google protocol. Basic Bluetooth without LE only manages data transmission but cannot measure the intensity of user encounters via signal strength.\n\nv2_shareAppDynamicLinkhttps://erouska.cz/app/sdilejv2_minSupportedVersionCodeAndroid666v2_shouldCheckOSVersion1v2_unsupportedDeviceLinkhttps://koronavirus.mzcr.cz/en/v2_minSupportedVersion2.3.3v2_currentMeasuresUrlhttps://covid.gov.cz/en/v2_appleExposureConfiguration{"factorHigh":0.25,"factorStandard":1,"factorLow":2,"lowerThreshold":55,"higherThreshold":70,"triggerThreshold":10}v2_daysSinceOnsetToInfectiousness0;0;0;0;0;0;0;0;0;0;0;1;2;2;2;2;2;2;2;1;1;1;1;1;1;1;1;1;1v2_supportEmailinfo@erouska.czv2_appleServerConfiguration{"minSupportedVersion":"13.5","showExposureForDays":14,"healthAuthority":"cz.covid19cz.erouska","uploadURL":"https://exposure-fghz64a2xa-ew.a.run.app/v1/publish","downloadIndexName":"erouska/index.txt","downloadsURL":"https://google.com","verificationURL":"https://apiserver-jyvw4xgota-ew.a.run.app","verificationAdminKey":"","verificationDeviceKey":"4AP6RHk5VlNi7WIhj4ZupI9JZODdUyrPGb1C2mDYKo4cuaGIHdYtOhjyoqhg4vB5r7FDijxnySLb_1CUH6XdDA.1.oZLaUuVOPbvEaiWnzkQxasWlD_71iSTeM9aAIOKrfqhg5QO68t004CKxWutYmfISc7vqJe6uY2dRrt34OFkliw","appCurentDataURL":"https://europe-west1-daring-leaf-272223.cloudfunctions.net","firebaseURL":"https://europe-west1-daring-leaf-272223.cloudfunctions.net"}v2_handleError400AsExpiredOrUsedCodefalsev2_handleError500AsInvalidCodefalsev2_exposureHelpContentJson{"items":[{"iconUrl":"https://erouska.cz/img/exposure/ic_notification.png","label":"You will receive the notification at least 24 hours after the infected individual tests positive for COVID-19 and enters their verification code in eRouška."},{"iconUrl":"https://erouska.cz/img/exposure/ic_privacy.png","label":"To keep identities anonymous, we do not know the time or place of the encounter."},{"iconUrl":"https://erouska.cz/img/exposure/ic_distance.png","label":"The encounter will be evaluated as risky if it was longer than 7 minutes and closer than 2 meters apart."},{"iconUrl":"https://erouska.cz/img/exposure/ic_14days.png","label":"We evaluate risky encounters in the past 14 days because you might experience COVID-19 symptoms between 2 and 14 days after a risky encounter."}]}v2_exposureHelpUITitleHelpv2_updateNewsOnRequesttruev2_recentExposureNotificationTitleIt's been some time since your eRouška updated its data with risky encounters. Connect to the internet.v2_efgsCountriesAustria, Belgium, Croatia, Cyprus, Denmark, Finland, Germany, Ireland, Italy, Latvia, Netherlands, Norway, Poland, Slovenia and Spain currently cooperate with eRouška.v2_diagnosisKeysDataMappingLimitDays7v2_infectiousnessWhenDaysSinceOnsetMissing1v2_reportTypeWhenMissing1v2_noEncounterCardTitleYou have not encountered anyone with COVID-19 in the past 14 daysv2_encounterUpdateFrequencyData update once every %d hours.v2_dbCleanupDays15v2_selfCheckerPeriodHours4v2_showChatBotLinkfalsev2_efgsVisitedCountriesv2_efgsReportTypeConfirmedTestv2_efgsConsentToFederationfalsev2_efgsTravellerDefaultfalsev2_appleExposureConfigurationV2{"immediateDurationWeight":200,"nearDurationWeight":100,"mediumDurationWeight":25,"otherDurationWeight":0,"infectiousnessForDaysSinceOnsetOfSymptoms":{"0":2,"1":2,"2":2,"3":2,"4":2,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"unknown":1,"-14":0,"-13":0,"-12":0,"-11":0,"-10":0,"-9":0,"-8":0,"-7":0,"-6":0,"-5":0,"-4":0,"-3":1,"-2":2,"-1":2},"infectiousnessStandardWeight":100,"infectiousnessHighWeight":200,"reportTypeConfirmedTestWeight":100,"reportTypeConfirmedClinicalDiagnosisWeight":100,"reportTypeSelfReportedWeight":100,"reportTypeRecursiveWeight":100,"reportTypeNoneMap":1,"minimumRiskScore":0,"attenuationDurationThresholds":[55,70,80],"attenuationLevelValues":[1,2,3,4,5,6,7,8],"daysSinceLastExposureLevelValues":[1,2,3,4,5,6,7,8],"durationLevelValues":[1,2,3,4,5,6,7,8],"transmissionRiskLevelValues":[1,2,3,4,5,6,7,8],"minimumScore":420}v2_helpJson[{"title":"Frequent problems","subtitle":"Recommended temporary measures","icon":"https://erouska.cz/img/faq/tool.png","questions":[{"question":"Android: Basic recommendations for troubleshooting","answer":"If the app displays an error message please check that your operating system is updated ([how to update Android](https://support.google.com/android/answer/7680439)) and that you are not using its beta version.\n\nThen, please check whether there are new updates of eRouška in your store ([Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska)).\n\nWe also recommend to enable automatic eRouška app updates. [Steps for Android are:](https://support.google.com/googleplay/answer/113412#autoone) Open Google Play Store app, press My apps & games → choose the app to update → press three dots and select Enable auto update."},{"question":"Android: Cannot activate eRouška. Activation error 17 (Exposure Notification API is not available).","answer":"On Android devices, please follow these steps:\n\n1. Check if you are the _primary user_ of the device, that Play Services are installed, and that _Settings → Google → COVID-19 Exposure Notifications_ is available.\n2. Uninstall eRouška.\n3. Clear cache and data for Google Play Services and Play Store ([see steps 2 and 3](https://support.google.com/googleplay/answer/9037938)).\n4. Install eRouška and enable _COVID-19 Exposure Notifications_.\n\nIf you still experience issues with app activation, it is possible that your device does not support Bluetooth LE (Low Energy Advertising) or Bluetooth Multiple Advertisement, is not authorized for Exposure Notifications by Google, or does not have Exposure Notifications API."}]},{"title":"Connection and data transmission","subtitle":"Bluetooth, GPS, internet","icon":"https://erouska.cz/img/faq/connection.png","questions":[{"question":"Do I need to keep Bluetooth turned on all the time?","answer":"Yes, you do. Without Bluetooth turned on, it is not possible to detect nearby devices with eRouška. You may have already enabled Bluetooth if you use wireless headphones, a smart watch or a connection with your car. We recommend you to set Bluetooth to be _visible for all devices_."},{"question":"Does eRouška need internet connection?","answer":"Internet connection is necessary to download (install) and activate eRouška. It also needs internet connection to download anonymous identifiers of infected users every day and display notifications about risky encounters.\n\nIf you are tested positive for COVID-19 and would like to anonymously notify others of a risky encounter, you would have to connect eRouška to the internet. Until then, your data is saved only in your phone's memory and is deleted every 14 days. The application also uses internet connection to download information about the epidemic's current development in the Czech Republic which is displayed on the _News_ page."},{"question":"How often does the app update online data?","answer":"eRouška downloads keys (identifiers) of infected users several times a day. Statistics on the News page are updated once a day."},{"question":"How much data does eRouška consume daily?","answer":"If _Notifications about foreign risky encounters_ is enabled, eRouška will also download keys of all infected individuals from other EU countries. The volume of downloaded data may be up to 2 MB daily.\n\nEven if _Notifications about foreign risky encounters_ are disabled, eRouška downloads about 1 MB of data every day. The new app version downloads keys not only from eRouška users, but also from infected users from other EU countries who indicated that they were travelling abroad and whom an eRouška user might have encountered.\n\nKeys of infected individuals are downloaded 2–3 times a day. To compare, an average web page requires about 1–5 MB of data. **The application does not necessarily need mobile internet.** It can download data when connected to WiFi."},{"question":"Android: Why do I need to allow the application to access GPS/location data?","answer":"**Update for Android 11 users:** You do not need to enable location services for the newest Android operating system versions for eRouška to work.\n\neRouška does not collect or store GPS data, but the operating system **Android 10 and older** also includes some Bluetooth LE (LE = low energy) services, which eRouška components need in order to function properly – specifically, Google Play Services. Therefore, the user's consent to the application's access to location data is required. This consent is not required on iOS. [Google explains](https://support.google.com/android/answer/9930236) why location services must be enabled in the phone.\n\nOther applications in your phone might use information on your location. If you want to check which apps use your location, go to your phone's settings → Location → App permission (or follow [these steps by Google](https://support.google.com/accounts/answer/6179507?hl=en)). You can deny location permission to other apps if you think they do not need it. eRouška does not need it so it will not be listed."},{"question":"How much battery do Bluetooth and eRouška app drain?","answer":"If you already use a phone with Bluetooth enabled, power consumption should not increase significantly. If you do not actively use Bluetooth, the results of our testing where Bluetooth was turned on all the time showed that energy consumption increased by units of percent per day. In most cases, it is less than 20% of battery capacity per day. It depends on the specific smartphone, your style of use and battery condition.\n\nPower consumption related to eRouška use is affected by 2 factors. First, it's the application itself, and second, it's the use of the system's Exposure Notification which regularly logs encounters with other eRouška users. The system function is provided by operating system Android or iOS so its optimization depends on updates from Apple and Google.\n\nWhen interpreting power consumption, please consider that this percentage depends on your phone's full use during the past 24 hours (proportion of use by other apps). That means that the percentage of eRouška use might be high in low-use phones, while it might be low in highly used phones. So it does not show the percentage of battery use.\n\n**Android: Note for location services:** It is only possible to detect a Bluetooth device near you if you enable \"Use location\" in the settings. (Read more in [Why your phone’s location setting needs to be on? on Google's help page](https://support.google.com/android/answer/9930236?hl=cs).) It means that other applications in your phone might use information on your location, which might be the reason for a higher power consumption. Please check which apps use your location. Go to your phone's settings → Location → App permission (or follow [these steps by Google](https://support.google.com/accounts/answer/6179507?hl=cs)). You can deny location permission to other apps if you think they do not need it. eRouška does not need it so it will not be listed."},{"question":"Is it possible that after turning on eRouška, my Wi-Fi or internet connection slowed down?","answer":"Unfortunately, some Android mobile phones' Bluetooth negatively affects Wi-Fi signal and vice versa."},{"question":"Can eRouška cause problems with other apps or devices that use Bluetooth (headphones, watches, wristbands)?","answer":"Very rarely, users might experience strange behaviour of their Bluetooth devices after they install eRouška, such as connection interruptions, typically in smart watches or wristbands, wireless headphones or handsfree sets. Most probably, it is an operating system issue which we are not able to affect.\n\nPlease try to apply the following steps:\n\n1. Restart your phone.\n2. Unpair your Bluetooth device and pair it again.\n\nIf these steps do not help please contact your device's manufacturer:\n\n* Android: [Get help from your device manufacturer](https://support.google.com/android/answer/3094742?ref_topic=7313011&hl=en)"},{"question":"Why does the app use Bluetooth? Are there no better alternatives such as GPS or triangulation from phone network operators?","answer":"User privacy comes first, and Bluetooth technology offers the best balance between logging accuracy and maximal privacy. Neither GPS technology nor operator triangulation can provide necessary data with the required accuracy. Among other things, it is because their functionality inside buildings or metro is very limited. eRouška needs to know that an encounter has occurred, at what distance and its duration. It does not matter where the encounter took place."},{"question":"Is Bluetooth a secure enough technology?","answer":"Bluetooth is a widely spread standard for connecting mobile devices with other devices such as wireless headphones, speakers, smart wristbands or smart watches. Since Apple and Google have developed the common protocol only for national Bluetooth LE-based contact tracing mobile applications, we are convinced that it was the right choice for eRouška as well. However, it has to be said that each and every technology has vulnerabilities. That is why we recommend our users to regularly update their apps as well as the operating system. The OS updates often contain important security patches."}]},{"title":"General information","subtitle":"eRouška, quarantine, and procedures of the public health office","icon":"https://erouska.cz/img/faq/general.png","questions":[{"question":"What should I do when the application notified me about a risky encounter with an infected user?","answer":"If you have symptoms of infection (e.g. high temperature, cough, stuffiness, sore throat, headache, or a sudden loss of smell or taste), call your general practitioner. In case of an emergency, call 155 for an ambulance or 112 for the integrated rescue system. The mentioned symptoms may appear between 2 and 14 days after the risky encounter.\n\nIf you have no symptoms please act responsibly:\n\n* Wear a mask or respirator over your mouth and nose.\n* Wash your hands with soap thoroughly and use hand sanitizer regularly.\n* Sanitize your personal belongings (e.g. your mobile phone) regularly.\n* Cough or sneeze into a tissue or your sleeve.\n* Use disposable tissues and throw them away after use.\n* Avoid unnecessary large group gatherings and keep a safe distance (approximately 2 meters).\n\nIf you receive a notification in eRouška and suspect that you encountered an infected person, call your general practitioner and follow their instructions. It is also possible that a public health officer will contact you after epidemiological investigation with the infected person. In that case, follow the officer's instructions.\n\nIf your health deteriorates within 14 days after the risky encounter and you experience COVID-19 symptoms, contact your general practitioner."},{"question":"What shall I do when I am positive for COVID-19 or I received an SMS with a verification code for eRouška?","answer":"If you received and SMS from the laboratory saying you are positive for COVID-19, you should also receive another SMS with the eRouška verification code within a few hours. eRouška messages are sent automatically after your results are saved in the information system. Automated processes of the Smart quarantine cannot influence the moment when the laboratory sends the results to the information system.\n\nIf you receive an eRouška SMS message with a verification code, please [follow these instructions](https://erouska.cz/en/sms). This will allow you to anonymously notify other users about the risky encounter. Thank you!\n\nHave you tested positive but have not received the eRouška SMS? Request your code at [info@erouska.cz](mailto:info@erouska.cz?subject=)."},{"question":"So every time the app says that I was in contact with and infected person, I will have to stay in quarantine or get tested?","answer":"No, eRouška is fully anonymous, using it is voluntary and a risky encounter is not a reason to enforce quarantine. If a person is tested positive, they will be contacted by the public health office to find out who they encountered in the previous days. If that person uses eRouška they can contact even those who they do not know, such as fellow passengers on a bus. By entering the verification code, they can send out notifications to all other eRouška users who they encountered in a risky manner.\n\nWhen the app notifies you that you have encountered a person tested positive for COVID-19, you should follow the recommendations. If you experience health problems contact your general practitioner immediately."},{"question":"Will eRouška find out if I break my quarantine for example by going to my cottage?","answer":"The application doesn't track your location – it's not its purpose – and it's not designed for it either. It will not find out if you have broken your quarantine or where you have gone. On the contrary, it should help to quarantine only people with a confirmed infection or suspect cases."},{"question":"Can eRouška issue a sick leave note for me? Is the exposure notification a reason for incapacity for work?","answer":"No, it cannot and it is not. If eRouška displayed the notification, follow the recommendations. Exposure notification does not substitute a report from your general practitioner or a laboratory which handles your COVID-19 test results. Only a doctor can issue valid documents that you can send to your employer."},{"question":"Why is eRouška needed? Can it effectively protect me from the virus?","answer":"One of eRouška's goals is to stop further spread of the infection by isolating potentially infected individuals from the rest. It also helps prevent implementing country-wide measures that have a negative impact on the society and economy of the Czech Republic. Similar applications were also introduced in other countries and are an effective tool to fight against COVID-19.\n\neRouška 2.0 is a complementary application of the Smart quarantine – Ministry of Health's system. Using Bluetooth technology, eRouška connects to other eRouška apps nearby and saves their anonymous identifiers. If a user is tested positive for COVID-19, they will receive an SMS with a verification code which allows the user to send their anonymous identifiers to other eRouška apps. According to the epidemiological model\\*, other users' apps will then evaluate whether they were in close enough contact to send out notifications and guide users to further investigation. Thus eRouška 2.0 helps to inform other potential transmitters of the infection and to speed up their testing. This decreases the number of infected individuals in the society and the risk of further infections.\n\n_\\* Closer than 2 metres apart and longer than 7 minutes._"},{"question":"What if someone does not admit they are infected?","answer":"This can also happen. However, we believe that eRouška users want to actively help stop further spread of the virus, and use the application in order to alert people with whom they've come into risky contact. **Intentional spread of COVID-19 is a criminal offence in the Czech Republic.**"},{"question":"Is it worth installing eRouška if there are not enough people using it?","answer":"This is the reason why you should not hesitate. The more of us use eRouška, the better network to protect us and warn against risk we will create. Help us. You can actively join the fight against COVID-19 by installing the app. You can also tell people around you about it and help them install it."},{"question":"How does eRouška differ from other similar apps?","answer":"eRouška uses Bluetooth technology which is used by most smart mobile devices in the Czech Republic. It works inside buildings, underground parking lots and the Metro. The application is designed not to collect your location information (e.g. via GPS). It only collects anonymous information on which other eRouška users you encountered closely. It is the only application in the Czech Republic that can officially use the Apple/Google protocol which guarantees the app's security on Android and iOD platforms and lowers battery use."},{"question":"What is the Smart Quarantine? What is the role of eRouška in it?","answer":"It is a set of clever measures that protect people from contagion and the economy from collapsing. Instead of keeping the whole nation in quarantine, the Smart Quarantine enables finding, testing and isolating only infected individuals and people with suspected infection.\n\nMore specifically, it is a system of management in forecasting, decision support, active contact tracing, testing including sampling point capacity management, laboratories, managing quarantines and related material and IT delivery to contain the pandemic of COVID-19. Smart Quarantine 2.0 is the name for transition of the project under the Ministry of Health which will be able to quickly react to any epidemics in the future thanks to new tools that are being developed.\n\neRouška application is one of the tools that were developed to support Smart Quarantine. Using technology, we can actively notify risky individuals who encountered an infected person to implement more protective measures, consider voluntary quarantine, and contact their GP in case they experience COVID-19 symptoms. We believe that thanks to these steps, we can contain the spread of the infection and save the health care system from a fatal overload."}]},{"title":"More information about eRouška","subtitle":"Data collection and evaluation, and the importance of notifications","icon":"https://erouska.cz/img/faq/advanced.png","questions":[{"question":"How does eRouška collect and process data about users' encounters?","answer":"Your smartphone with eRouška will record anonymous identifiers (ID) from other devices with this application via Bluetooth LE. It stores information about the “encounter” and its length in its internal memory.\n\nIf your COVID-19 test is positive you will receive a verification code in an SMS. You will enter it in eRouška ([follow these steps](https://erouska.cz/en/sms)) which will send your anonymous identifiers to the server. Other eRouška apps will download them for evaluation. The algorithm in each eRouška will automatically evaluate the data within the epidemiological model: it will compare the identifiers with all logged encounters. For each risky encounter, the app will display a notification, warn the user that they might be potentially infected, and recommend further steps.\n\nYou can find detailed information about data collection and processing in [Information about personal data processing in eRouška 2.0](https://erouska.cz/en/podminky-pouzivani)."},{"question":"How does eRouška evaluate risky encounters?","answer":"Originally, epidemiologists established that a risky encounter is one that takes place closer than 2 meters apart for more than 15 minutes. However, aggressive transmission of new coronavirus mutations shows that even a shorter encounter might be risky.\n\nWe consulted Apple/Google and other countries on next steps regarding more aggressive mutations and we are gradually changing the parameters of risky encounter evaluation, beginning in March 2021. We periodically evaluate their effect on the number of risky encounters. These days, we are adjusting the time parameter, which is currently set to about 7 minutes.\n\neRouška strives to evaluate the parameters of distance and length of encounters as precisely as possible with the current technology. The distance between app users' phones is estimated by Bluetooth signal strength. The length of an encounter is considered periodically: every few minutes, the phones search for other phones with eRouška nearby.\n\nThe precision of these parameters is technologically limited. The bigger the distance and the shorter the encounter is, the more difficult it is to evaluate it. Loosening the parameters of evaluation thus increases the probability of false positive and false negative results.\n\nA higher precision requires more frequent logging of other devices with eRouška nearby. Unfortunately, that results in a higher energy consumption. Measurement algorithms are set up by Google and Apple in their Exposure Notification protocol on which eRouška is built. eRouška developers can only partially change these parameters and filter on the results. You can find more explanation in [Risk evaluation](https://erouska.cz/en/vyhodnoceni-rizika)."},{"question":"How does eRouška react to new variants or mutations of the virus?","answer":"New virus variants that have appeared since the pandemic started are examined regarding their infectiousness and health impact. eRouška developers are in close contact with both Czech administration and Apple and Google that provide the exposure protocol. Therefore, they are able to alter the evaluation algorithm when the risk of infection changes due to research advances."},{"question":"What if eRouška misinterprets a contact with another person who is, for example, in a car at a crossroads, behind a door or a thin wall?","answer":"eRouška works by measuring the signal strength between two devices. Technically, it cannot be ruled out that a false detection occurs. You can read more about technical limitations, risks of app use and the process of encounter evaluation on [Reliability of risky encounter evaluation](https://erouska.cz/en/vyhodnoceni-rizika) and on [Technical requirements in Terms and conditions](https://erouska.cz/en/podminky-pouzivani#technicke).\n\nIn order to evaluate the contact as risky, it is important to measure the distance (under 2 m) and length of contact (several minutes) – therefore stopping at traffic lights is insignificant."},{"question":"I received an exposure notification, but eRouška shows that it has not logged any risky encounter near me.","answer":"If the notification with red virus icon and text Exposure notification (Android) or Exposure log (iOS) surprised you, no need to worry. It is only a system notification on Android and iOS which eRouška cannot influence. View [this picture](https://www.facebook.com/eRouska/posts/180938253548928) which shows what eRouška notification looks like.\n\nYou can find out whether you encountered an infected individual on the main page of eRouška.\n\nIf you saw a risky encounter in eRouška which then disappeared, it is possible that the evaluation parameters have changed in the Android app update so eRouška does not consider that encounter to be risky anymore."},{"question":"So everyone will know I have COVID-19?","answer":"No, they won't. If you are tested positive, your identity is only known to the public health officer. People that received a notification about a risky encounter will never know who, where or when they could have been infected. The application only displays the general information about risky encounters and recommends further steps. Your identity is safe."},{"question":"I was tested for COVID-19, but I still have not received the SMS with the verification code for eRouška. When can I expect it?","answer":"First, you will receive test results in an SMS from the laboratory. The lab also sends the results to the central information system of the Ministry of Health. After the results are copied to the public health office's system, automatic eRouška SMS messages with verification codes are sent. If the lab does not send the results to the information system on the same day, you might not receive the SMS with the verification code until the next day.\n\n**Please check that the eRouška SMS was not blocked by the system as spam by mistake**\n\n* **Android:** In your Messages app press the options icon (three dots) and Spam and blocked conversations (or three dots → Settings → SIM → Spam). If eRouška SMS is there, click on Not spam.\n\n**If you do not receive eRouška SMS even the day after** you received an SMS confirming your infection from the laboratory, send us a message to [info@erouska.cz](mailto:info@erouska.cz?subject=) asking for a new SMS code. Please state the following information:\n\n* full name on your test request form,\n* phone number on your test request form,\n* date of test results,\n* sampling address / laboratory which handled your test results,\n* test type (PCR/antigen)."},{"question":"Will I receive the code in an SMS if I tested positive in an antigen test?","answer":"If [your antigen test results positive and you experience COVID-19 symptoms](https://drive.google.com/file/d/1HPwtltNRuzmowjZgE_c1u-a8m3W89T-p/view) (stated in the request form), you will receive the eRouška code in an SMS same as in a positive PCR test.\n\nIf [your antigen test is positive but you do not experience COVID-19 symptoms](https://covid.gov.cz/situace/antigenni-testovani/interpretace-vysledku-antigenniho-testu-jak-se-zachovat-po-obdrzeni), you will undergo a PCR test. If that results positive, you will receive the code in an SMS. If your PCR test is negative, you will not send your data from eRouška."},{"question":"In which countries can I use eRouška? How does cooperation with foreign apps work?","answer":"European Commission developed a central gateway service for a secure information exchange (EFGS). This gateway allows eRouška to exchange warnings with other tracing applications used in various European countries. You can find more information about the gateway in [Coronavirus: EU interoperability gateway for contact tracing and warning apps – Questions and Answers](https://ec.europa.eu/commission/presscorner/detail/cs/qanda_20_1905#gateway).\n\nIf you want to view more information on joined countries and settings, tap Cross-border cooperation on the home page in the app. You can enable or disable the feature and view the list of countries. You can find the official list of EU countries at [European Commission's website](https://ec.europa.eu/health/sites/health/files/ehealth/docs/gateway_jointcontrollers_en.pdf)."},{"question":"Does eRouška use Apple/Google API?","answer":"The new version eRouška 2.0 for Android and iOS uses Apple/Google Exposure Notification API. Remember to update your eRouška app in [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska) or [App Store (iOS)](https://apps.apple.com/cz/app/erou%C5%A1ka/id1509210215) and activate it again."},{"question":"What is Apple/Google Exposure Notification API (protocol)?","answer":"Together, Apple and Google develop technologies which can be used by public health institutions (Ministry of Health) to develop national applications for tracing risky encounters with individuals with COVID-19.\n\nIt is an interface which allows specific apps (eRouška in the Czech Republic) to reliably access system tools of Android and Apple devices and log encounters – mainly to use Bluetooth LE to verify the contact and run in the background without any more settings of the app or the operation system. Encrypted communication protocol then ensures a safe transmission of current lists of identifiers of infected individuals between tracing applications in order to evaluate risky encounters and send notifications.\n\nFind more information directly on [Apple](https://www.apple.com/covid19/contacttracing) and [Google](https://www.google.com/covid19/exposurenotifications/) websites."},{"question":"Where in eRouška can I find information on stored encounters?","answer":"eRouška 2.0 is built on Apple/Google Exposure Notification protocol which manages a secure storage and evaluation of encounters. Due to security reasons, it does not let the app directly access the data of encounters.\n\nOn the main screen, you can see whether the app is working correctly. Information about stored encounters is saved in the system. However, these encounters might not be the risky ones. You can find them in Settings:\n\n* Android: Settings → Google → COVID-19 Exposure Notifications → press three dots in the upper right corner → Exposure Check"},{"question":"What are the records in Settings → Exposure Check?","answer":"Recording contacts with the infection show how many keys from infected users were downloaded on your phone from the server and how many of those match those that your phone logged.\n\n**Exposure Check:** Date and time when keys were downloaded from the server in the past 2 weeks. To view more you need to open individual records.\n\n**Android: Number of keys:** Number of keys from users who tested positive. Your phone downloaded these keys on that day to check for possible encounters with infected individuals.\n\n**Android: Number of matches:** Number of downloaded keys paired with keys found near you. If this number is higher than 0, you have encountered an infected individual. That does not necessarily indicate that it was a risky encounter.\n\n**Timestamp:** Date and time of the check. It is not the time of a risky encounter (the time is missing to protect the users' privacy).\n\n**Data source:** Application that checks and evaluates risky encounters.\n\n**Hashing code:** Check sum of the file that contains keys from infected users. Until the file contents change, the check sum will not change during the 2-week period."},{"question":"How will I know that eRouška works on the background? Why don't I see the app icon in the status bar?","answer":"We removed notification icon because storing and evaluation of encounters is done by Apple/Google protocol in the system. The app is responsible for downloading keys (identifiers) of infected users once or twice a day and does not need to run permanently.\n\nYou can view the app status on its main page. You can then check that the keys were downloaded in the Settings:\n\n* **Android:** Settings → Google → COVID-19 Exposure Notifications → press three dots in the upper right corner → Exposure Check\n* **iOS**: Settings → Exposure Notifications → Exposure Logging Status → Exposure Checks"},{"question":"Why does the date and time of \"Last data update\" on the main page of eRouška differ from date and time of \"Exposure Checks\" that I can see in Settings?","answer":"eRouška shows the date and time of the last successful attempt to check new keys (identifiers) of infected individuals on the server. Phone Settings show date and time when the keys were last found and evaluated.\n\nKeys of infected users download once or twice a day."},{"question":"Why can't I view the exact time of a risky encounter in eRouška?","answer":"eRouška is built on Apple/Google Exposure Notification protocol which manages logging and evaluation of encounters in the operating system. Apple/Google protocol allows eRouška to access only the date of a risky encounter so that the privacy of users is ensured."},{"question":"Why do I see \"Found keys: 0\" in Exposure Checks in the phone Settings?","answer":"It means that Apple/Google API in your phone has not evaluated any encounter with an infected individual. In other words, downloaded keys of infected users on a given day do not match keys stored in your device."},{"question":"Why do I see keys for specific dates in the phone's Settings > Exposure Checks but eRouška does not display any risky encounters?","answer":"The number of keys indicates the number of logged encounters with infected individuals. However, they might not be risky encounters. If an encounter is not risky, eRouška will not notify you about it. That's why you cannot see it in the app."},{"question":"I received a notification about a risky encounter that took place several days ago. Why did eRouška only notify me now?","answer":"eRouška cannot evaluate risky encounters and display notifications before the user with a positive test sends data from their eRouška. If today you receive a notification that you have encountered an infected individual several days ago, it means that the person only sent their data today or yesterday. Your eRouška downloaded and evaluated the data today."},{"question":"I received a notification about a risky encounter, but I can see several logs with various dates on the Risky encounter page.","answer":"eRouška evaluated risky encounters after a user who tested positive for COVID-19 sends data from their eRouška. If you received one notification but there are more logs on the Risky encounter page, it means that you have met that person more than once or that more than one user sent their data on the same day."},{"question":"Bude eRouška fungovat jako „covid pas“ pro zobrazování výsledků testů a prokázání očkování?","answer":"Anonymní charakter aplikace eRouška ji neumožňuje kombinovat s tzv. digitálním zeleným certifikátem (Digital Green Certificate), který pracuje s ověřenou identitou a osobními údaji uživatele – výsledky testů nebo provedeným očkováním proti COVID-19."},{"question":"Does eRouška publish app usage statistics?","answer":"We publish statistics in JSON format at [stats.erouska.cz](https://stats.erouska.cz). The data have the following structure:\n\n{\\\\n \"data\": {\\\\n \"modified\": 1608696001,\\\\n \"date\": \"20201223\",\\\\n \"activations\\_yesterday\": 2042,\\\\n \"activations\\_total\": 1475571,\\\\n \"key\\_publishers\\_yesterday\": 718,\\\\n \"key\\_publishers\\_total\": 39175,\\\\n \"notifications\\_yesterday\": 631,\\\\n \"notifications\\_total\": 197298\\\\n }\\\\n}\n\n* **modified** = Unix timestamp of generated statistics,\n* **date** = date of report created,\n* **activations\\_yesterday** = number of activations on the previous day,\n* **activations\\_total** = total number of eRouška activations,\n* **key\\_publishers\\_yesterday** = number of infected individuals who entered the code on the previous day,\n* **key\\_publishers\\_total** = total number of infected individuals who entered the code,\n* **notifications\\_yesterday** = number of individuals who received the notification about a risky encounter on the previous day,\n* **notifications\\_total** = total number of individuals who received the notification about a risky encounter.\n\nA call without a parameter will display current day's information. A call with parameter `date` will allow you to get data for any day after October 23, 2020 (we only have aggregated data before then). Using parameter `date` with value `all`, you can get all data that the interface provides.\n\n* today's data: [`https://stats.erouska.cz/`](https://stats.erouska.cz/)\n* data from December 1, 2020: [`https://stats.erouska.cz/?date=2020-12-01`](https://stats.erouska.cz/?date=2020-12-01)\n* all days' data: [`https://stats.erouska.cz/?date=all`](https://stats.erouska.cz/?date=all)"}]},{"title":"Data security","subtitle":"Personal data and privacy protection","icon":"https://erouska.cz/img/faq/security.png","questions":[{"question":"How do I know which data the application stores and sends?","answer":"eRouška application 2.0 uses Apple/Google Exposure Notification API which allows for collecting data and transmitting identifiers among users. Thanks to that, the application does not have a direct access to logged data which could be displayed to users, as it did in the previous version 1.0.\n\nThe app downloads a list of identifiers from users who tested positive on a daily basis. Only the user can send data with identifiers to others, and only after they enter the SMS code that they will automatically receive when they test positive for COVID-19.\n\nThe app will then send anonymous aggregated statistical information that individuals in risk were notified. This information is not connected to any user or phone identifier and is only used to calculate statistics about eRouška's efficiency.\n\nTo make sure eRouška works correctly, we collect data such as crashes and app usage on your phone. We use standard tools (Firebase Crashlytics and Google Analytics) developed by Google. Telemetry sent into these tools does not contain any data about you or your phone.\n\nYou can find detailed information about data collection and processing in [Information about personal data processing in eRouška 2.0](https://erouska.cz/en/podminky-pouzivani)."},{"question":"Will eRouška data be copied after I perform phone recovery or if I get a new phone?","answer":"To protect your privacy, neither Android nor iOS back up data from Exposure Notifications. If you reinstall your phone's system or get a new phone, the stored identifiers will not be recovered.\n\nIf you want to receive the information about a possible risky encounter, we recommend you check your old phone over the period of 14 days after you install eRouška on your new device. It is necessary to:\n\n* make sure that your device is turned on and connected to the internet so that it can download evaluation data,\n* open eRouška on your old device at least once a day.\n* If you test positive during these 14 days, please enter your code in the new device. To receive the code for your older device, please contact us at [info@erouska.cz](mailto:info@erouska.cz).\n\nAfter 14 days, your old phone will not contain any data useful for risky encounter evaluation. You can uninstall eRouška from it and keep the app only in the new device."},{"question":"What if my phone is stolen or I lose it?","answer":"Only anonymous information about other devices detected by eRouška is stored in the app. This does not bring any risk to you or the owners of other devices. Other applications that you commonly use may store much more sensitive data on your phone. Keep your phone protected with a passcode or biometrics (fingerprint or face)."},{"question":"Is eRouška compliant with GDPR?","answer":"The entire system of the application eRouška including the website is designed in full compliance with GDPR and the Personal Data Processing Act.\n\nYou will find more detailed information here:\n\n* [Information about personal data processing in eRouška 2.0](https://erouska.cz/en/podminky-pouzivani)\n* [Application source code audit](https://erouska.cz/en/audit-kod)"},{"question":"How do you protect users from data abuse?","answer":"We protect users primarily by minimizing the range of logged data and storing them directly in the device. User data is not sent or processed anywhere without their knowledge and consent. The app stores only anonymous identifiers (IDs) of other eRouška users and information about the time, length and Bluetooth strength. Data is automatically processed on the server only after they are sent by the user."},{"question":"Does an independent organization oversee the security of application data?","answer":"The whole system of eRouška application including the supporting website is developed in full compliance with GDPR. The application's code is open-sourced ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)) and underwent [an audit by independent education institutions](https://erouska.cz/en/audit-kod).\n\nYou can find more information in [Information about personal data processing in eRouška 2.0](https://erouska.cz/en/podminky-pouzivani)."},{"question":"Can someone track my location using eRouška?","answer":"No. The application does not collect information about your location and only you have access to the saved data. The data that the app sends after your consent cannot be used to track your location. Moreover, for safety and privacy reasons, eRouška changes its ID regularly to prevent it from being abused."},{"question":"Can I verify that the app is not recording my location?","answer":"Yes. eRouška application is published as open-sourced code ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)), so an informed person can easily verify that the application indeed does not collect location data.\n\nThe source code of eRouška [is reviewed by independent authorities](https://erouska.cz/en/audit-kod) who confirm that the application:\n\n* does not track location\n* does not automatically send data"},{"question":"Can a user's application identifier (ID) be paired with a specific person?","answer":"Application eRouška 2.0 [does not work with personal data](https://erouska.cz/en/podminky-pouzivani). Generated anonymous eRouška identifiers are sent between the applications and the server via an encrypted communication protocol developed by Apple and Google. Neither users, nor developers or other authorities can access the identifiers."},{"question":"Who has (could have) access to my data?","answer":"Neither the user not any other person can access logged data about encounters in eRouška 2.0. The reason for this is that this app version uses Apple/Google protocol which does not allow access to logged data to users, developers or other individuals. Data (anonymous identifiers of infected individuals) are sent to the server and other users' devices encrypted and are evaluated on the devices automatically if a notification about a risky encounter is needed to be displayed. They are deleted from the devices and the server automatically after 14 days."},{"question":"How old data is available? When and how is it deleted?","answer":"eRouška saves the data for 14 days. Older entries are automatically deleted. If you are tested positive for COVID-19, enter your verification code in eRouška and send identifiers from your phone to the server, the data will stay there for 14 days after which they will be deleted. You can delete data from your phone in Exposure Notification settings. The specific steps differ among devices and operating system versions.\n\n* **Android:** On Android devices, uninstalling the app is enough to delete stored keys (identifiers).\n\nFor information about data controllers and processors, see the [Terms and Conditions](https://erouska.cz/en/podminky-pouzivani)."},{"question":"Can I change my mind about using the app at any time? Can I delete the data that I send out to be processed?","answer":"You can uninstall the application any time you want. You can delete stored identifiers from your phone in Exposure Notification settings. The specific steps differ among devices and operating system versions. Data which were sent to the server are automatically deleted after 14 days.\n\n* **Android:** On Android devices, uninstalling the app is enough to delete stored keys (identifiers)."},{"question":"What happens to the data after I delete the app from my smartphone?","answer":"**Android:** Uninstalling the app is enough to delete stored keys (identifiers) from phones or tablets with Android OS.\n\nIf you reinstall the application, it will start collecting and storing brand new data in the phone."},{"question":"Where can I find the application's source code?","answer":"eRouška is published with open-source code. You can find it on GitHub ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios))."}]},{"title":"Installation and compatibility","subtitle":"Supported devices and installation options","icon":"https://erouska.cz/img/faq/compatibility.png","questions":[{"question":"For which phones is eRouška available? Why are the requirements stricter than in eRouška 1.0?","answer":"To install eRouška, you need a mobile device with the following parameters:\n\n* iPhone with iOS 13.5 and higher\n* OS Android 6.0 and higher with Google Play Services (only a small percentage of Huawei phones and some phones with their own ROM do not have this)\n* Bluetooth LE (Low Energy)\n\nIf your phone is not compatible with eRouška (it does not have the necessary features) Google Play Store or Apple App Store will not let you install it. The application is available only for some Android tablets. It is not available for iPads.\n\nApple and Google define the minimum requirements of compatibility. They are based on the Apple/Google Exposure Notification protocol which the new eRouška uses. We decided to start using the Apple/Google protocol to make sure that the app works on iPhones and to be compatible with similar applications in other European countries."},{"question":"Where can I safely download and install eRouška?","answer":"eRouška is only available for:\n\n* Android: [in Google Play Store](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska)."},{"question":"Is it possible to download the application outside official application stores? Is an apk available?","answer":"eRouška 2.0 uses Apple/Google protocol to log encounters and transmit data. Therefore, it depends on system services, without which it would not work, so it is not possible to install it from other than official app stores. Application stores will automatically filter for compatible devices.\n\neRouška developers never provide users with links for installation packages (APKs) or installation files outside official application stores. We would not be able to guarantee security, updates or user support for unofficial app installs. Please, do not download eRouška from .apk files that you find online or receive by email. They might contain viruses, trojan horses or other harmful content."},{"question":"Can I use eRouška with another tracing application of a foreign country (e.g. Corona-Warn-App)?","answer":"If you spend most of your time in the Czech Republic, we recommend to use eRouška to travel abroad as well. [eRouška now cooperates with other EU countries](https://erouska.cz/en/caste-dotazy#efgs).\n\nIn other cases, you can install more tracing apps along eRouška, such as Corona-Warn-App, Stopp Corona App, Imunni, Stop Covid ProteGO Safe etc. However, if they also use Apple/Google Exposure Notification protocol (especially in neighbour countries), only one of them can be active at any given moment. You would need to pause the others. Only one app can access the Apple/Google protocol's logging feature at the same time."},{"question":"Funguje eRouška, když mám na mobilu úsporný režim / režim nízké spotřeby / spořič baterie?","answer":"**Android:** Spořiče/šetřiče baterie na zařízeních s OS Android mohou do značné míry omezovat běh aplikací na pozadí. Ačkoliv systém oznámení o možném kontaktu s onemocněním COVID-19 nadále funguje, nemusí docházet k vyhodnocování rizikových kontaktů, a tudíž k případnému zobrazení notifikací."},{"question":"Will I be able to use eRouška on smart watches or smart wristbands?","answer":"This might be possible in the future. Right now, we are not developing this because the smart watch or wristband would need to support Bluetooth LE (Low Energy) and Apple/Google protocol. Basic Bluetooth without LE only manages data transmission but cannot measure the intensity of user encounters via signal strength."}]}]v2_appleIgnoreAndroidtruev2_howItWorksUITitleHow eRouška worksv2_validationTokenExpirationLeewayMinutes15v2_efgsDays14v2_keyExportNonTravellerUrls[{"country":"CZ","url":"https://cdn.erouska.cz/erouska/index.txt"}]v2_keyExportEuTravellerUrls[{"country":"CZ","url":"https://cdn.erouska.cz/erouska/index.txt"},{"country":"IT","url":"https://cdn.erouska.cz/efgs/it/index.txt"},{"country":"LV","url":"https://cdn.erouska.cz/efgs/lv/index.txt"}]v2_howItWorksEvalContenteRouška apps will evaluate a risky encounter if it was longer than 7 minutes and closer than 2 meters apart, starting with the moment when the individual was supposedly infectious.v2_ragnarokHeadlineeRouška's contribution to the fight against the pandemic is overv2_ragnarokMoreInfohttps://erouska.cz/env2_ragnarokBodyThank you for joining more than 1.7 million people. You helped to protect other people by sending out more than 400,000 notifications about possible risk contacts with infected individuals. Exposure notifications will be switched off and your phone will not continue to search for contacts. eRouška is now inactive and will only send you a notification if it is reactivated.v2_ragnaroktrue
================================================
FILE: app/src/main/res/xml-cs/remote_config_defaults.xml
================================================
v2_reportTypeWeights1;1;1;1;1;1v2_infectiousnessWeights0;0.6;2.0v2_attenuationBucketThresholdDb55;70;80v2_attenuationBucketWeights2.0;1;0.25;0v2_minimumWindowScore420v2_covidDataServerUrlhttps://europe-west1-erouska-key-server-dev.cloudfunctions.netv2_minGmsVersionCode203019000v2_keyImportDataOutdatedHours99999999v2_riskyEncountersTitleAnNaposledy %1$s jste se setkali s osobou, u které bylo potvrzeno onemocnění COVID-19.v2_keyImportPeriodHours6v2_keyExportUrlhttps://cdn.erouska.cz/v2_recentExposuresUITitlePředchozí riziková setkánív2_spreadPreventionUITitleZásady zodpovědného chovánív2_symptomsUITitleHlavní příznakyv2_exposureUITitleRizikové setkánív2_chatBotLinkhttps://erouska.cz/caste-dotazyv2_noEncounterBodyNa rizikové setkání vás upozorníme pomocí oznámení.v2_noEncounterHeaderV posledních 14 dnech nebyla ve vaší blízkosti žádná osoba s potvrzeným onemocněním COVID-19v2_conditionsOfUseUrlhttps://erouska.cz/podminky-pouzivaniv2_riskyEncountersWithoutSymptomsJestliže se u vás neprojevují hlavní příznaky onemocnění COVID-19, nemusíte zůstávat doma. Dodržujte zásady zodpovědného chování a sledujte svůj zdravotní stav.v2_riskyEncountersWithSymptomsPokud se u vás v průběhu 14 dnů od kontaktu s nakaženým objevily příznaky respiračního onemocnění, kontaktujte telefonicky svého praktického lékaře a řiďte se jeho pokyny. V případě závažných zdravotních obtíží volejte 155.v2_riskyEncountersTitleDne %@ jste se setkali s osobou, u které bylo potvrzeno onemocnění COVID-19.v2_contactsContentJson[{"title":"Máte podezření na onemocnění COVID-19?","text":"Zůstaňte doma a telefonicky kontaktuje svého praktického lékaře.","linkTitle":"Důležité kontakty","link":"https://koronavirus.mzcr.cz/dulezite-kontakty-odkazy/"},{"title":"Potřebujete poradit?","text":"Odpovědi na veškeré otázky a nejasnosti týkající se onemocnění COVID-19 najdete na webu MZČR.","linkTitle":"Často kladené otázky","link":"https://koronavirus.mzcr.cz/"},{"title":"Podpora aplikace","text":"Potřebujete poradit s aplikací? Napište nám e-mail.","linkTitle":"Napsat e-mail","link":"mailto:info@erouska.cz"},{"title":"O aplikaci eRouška","text":"Zajímá vás, jak aplikace funguje? Podívejte se na web erouska.cz.","linkTitle":"Přejít na web erouska.cz","link":"https://erouska.cz"}]v2_preventionContentJson{"title":"COVID-19 se přenáší hlavně kapénkami, která se šíří vzduchem. Přenášet se může také kontaminovanými předměty.","items":[{"iconUrl":"https://erouska.cz/img/prevention/ic_clean_hands.png","label":"Často a důkladně si myjte ruce mýdlem a používejte dezinfekci."},{"iconUrl":"https://erouska.cz/img/prevention/ic_disinfenction.png","label":"Pravidelně dezinfikujte také osobní předměty (např. mobilní telefon)."},{"iconUrl":"https://erouska.cz/img/prevention/ic_tissue.png","label":"Kašlejte a kýchejte do jednorázového kapesníku či rukávu."},{"iconUrl":"https://erouska.cz/img/prevention/ic_social_distance.png","label":"Vyhýbejte se velkým skupinám osob a udržujte bezpečný odstup (cca 2 metry)."},{"iconUrl":"https://erouska.cz/img/prevention/ic_mask.png","label":"Do obchodů a hromadných dopravních prostředků vždy noste ochranu dýchacích cest dle aktuálních opatření."},{"iconUrl":"https://erouska.cz/img/prevention/ic_stay_home.png","label":"Pokud cítíte příznaky respiračního onemocnění, zůstaňte doma a kontaktujte lékaře."}]}v2_symptomsContentJson{"title":"Příznaky se mohou objevit 2–14 dní po rizikovém setkání.","items":[{"iconUrl":"https://erouska.cz/img/symptoms/ic_temperature.png","label":"Zvýšená teplota"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_cough.png","label":"Kašel"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_stuffiness.png","label":"Dušnost"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_throatache.png","label":"Bolest v krku"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_headache.png","label":"Bolest hlavy, svalů a kloubů"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_smelltaste.png","label":"Ztráta čichu a chuti"}]}v2_encounterWarningSetkali jste se s osobou, u které bylo potvrzeno onemocnění COVID-19. Sledujte svůj zdravotní stav.v2_verificationServerApiKey4AP6RHk5VlNi7WIhj4ZupI9JZODdUyrPGb1C2mDYKo4cuaGIHdYtOhjyoqhg4vB5r7FDijxnySLb_1CUH6XdDA.1.oZLaUuVOPbvEaiWnzkQxasWlD_71iSTeM9aAIOKrfqhg5QO68t004CKxWutYmfISc7vqJe6uY2dRrt34OFkliwv2_helpMarkdown# Nejčastější problémy\n## Android: Základní doporučení při řešení problémů s aplikací\nPokud vám aplikace zobrazuje chybovou hlášku, zkontrolujte prosím nejprve, zda máte nainstalované aktualizace operačního systému ([postup aktualizace Androidu](https://support.google.com/android/answer/7680439)) a nepoužíváte jejich beta verzi.\n\nDále prosím zkontrolujte v obchodu [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska), zda není pro eRoušku dostupná nová aktualizace.\n\nDoporučujeme rovněž povolit automatické aktualizace aplikace eRouška. [Postup pro Android zde:](https://support.google.com/googleplay/answer/113412#autoone) Spusťte aplikaci Obchod Google Play, klepněte na nabídku Moje aplikace a hry → Vyberte aplikaci, kterou chcete aktualizovat → Klepněte na ikonu možností a klepněte na Aktivovat automatické aktualizace.\n\n## Android: Nejde aktivovat eRouška. Chyba aktivace 17 (Exposure Notification API není k dispozici).\nPro vyřešení chyby na Android zařízení, zkuste prosím postupovat takto:\n\n1. Zkontrolujte, zda jste _hlavní uživatel_ zařízení, zda máte v telefonu služby Play a zda je k dispozici _Nastavení → Google → Oznámení o možném kontaktu (COVID-19 Exposure Notifications)_.\n2. Odinstalujte eRoušku.\n3. Smažte mezipaměť a data u Google Play Services a Obchodu Play ([postup zde, viz kroky 2 a 3](https://support.google.com/googleplay/answer/9037938)).\n4. Nainstalujte eRoušku a zapněte opět _Oznámení o možném kontaktu COVID-19_.\n\nPokud se vám i přes aktualizaci systému, aplikace a uvedené kroky nadále objevuje chyba aktivace, je bohužel možné, že vaše zařízení ještě nepodporuje Bluetooth LE (Low Energy Advertising), nepodporuje Bluetooth Multiple Advertisement, není autorizované Googlem pro Exposure Notifications nebo vůbec nemá Exposure Notifications API.\n\n# Připojení a přenos dat\n## Musím mít Bluetooth zapnutý neustále?\nAno. Bez zapnutého Bluetooth není možné zjišťovat blízkost dalších zařízení s nainstalovanou aplikací eRouška. Tuto funkci máte zapnutou možná i nyní, například v případě, že používáte bezdrátová sluchátka, připojení v autě, chytrý náramek nebo hodinky. V nastavení Bluetooth vašeho telefonu/tabletu navíc doporučujeme povolit, aby byl _viditelný pro všechna zařízení_.\n\n## Potřebuje aplikace připojení k internetu?\nPřipojení k internetu je nutné ke stažení (instalaci) a aktivaci eRoušky. Dále je potřeba, aby si eRouška mohla každý den stahovat aktuální seznam anonymních identifikátorů nakažených uživatelů, a zobrazit tak včas případnou notifikací o rizikovém setkání.\n\nPokud by u vás test potvrdil nákazu COVID-19 a vy byste chtěli anonymně upozornit ostatní na možný rizikový kontakt, bylo by opět potřeba eRoušku připojit k internetu. Do té doby jsou vaše data uložena pouze ve vašem telefonu, odkud se sama odmazávají po 14 dnech. Aplikace využívá připojení k internetu rovněž ke stažení informací o aktuálním vývoji epidemie COVID-19 v ČR, které zobrazuje na stránce _Aktuálně_.\n\n## Jak často si aplikace aktualizuje data z internetu?\nKlíče (identifikátory) nakažených uživatelů si eRouška stahuje několikrát denně. Statistiky nakažených osob na obrazovce Aktuálně se aktualizují jednou denně.\n\n## Jak velký objem mobilních dat eRouška denně spotřebuje?\nV případě zapnutého _Upozornění na zahraniční riziková setkání_ eRouška stahuje také klíče všech nakažených uživatelů z ostatních států EU. Objem stažených dat se v takovém případě může pohybovat až kolem 2 MB denně.\n\nI v případě vypnutého _Upozornění na zahraniční riziková setkání_ eRouška stahuje přibližně 1 MB denně. Nová verze aplikace stahuje nejen klíče nakažených uživatelů eRoušky, ale i klíče nakažených uživatelů z ostatních států EU, kteří ve svých aplikacích uvedli, že cestují do zahraničí, takže je uživatel eRoušky potenciálně mohl potkat i v České republice\n\nSoubory klíčů nakažených uživatelů se stahují postupně, 2–3× denně. Pro porovnání, načtení průměrné stránky zpravodajství vyžaduje 1–5 MB dat. **Aplikace nepotřebuje nutně mobilní datové připojení.** Data si stáhne sama i na Wi-Fi.\n\n## Android: Proč je potřeba mít povolený přístup k GPS/polohovým/lokačním údajům?\n**Aktualizace pro uživatele Android 11:** U nejnovější verze operačního systému Android již nemusíte pro fungování eRoušky povolovat služby určování polohy.\n\nAplikace eRouška nesbírá a neukládá data z GPS, avšak operační systém **Android 10 a starší** zahrnuje pod určování polohy také některé služby Bluetooth LE (LE = low energy), který komponenty eRoušky – konkrétně Google Play Services – pro své fungování vyžadují. Proto je nutný souhlas uživatele s přístupem aplikace k polohovým údajům. V iOS tento souhlas nutný není. Proč v telefonu musí být zapnuté určování polohy, [vysvětluje Google zde](https://support.google.com/android/answer/9930236).\n\nInformaci o vaší poloze mohou využívat i jiné aplikace ve vašem zařízení. Pokud chcete zkontrolovat, které aplikace k nim mají přístup, přejděte do nastavení zařízení a vyberte položky Zabezpečení a poloha → Umístění → Oprávnění na úrovni aplikace (případně následujte [postup od společnosti Google](https://support.google.com/accounts/answer/6179507?hl=cs)). Zde můžete jiným aplikacím odepřít oprávnění používat vaši polohu, pokud si nemyslíte, že to potřebují. Protože eRouška toto oprávnění nepotřebuje, nezobrazí se v seznamu.\n\n## Kolik zapnutý Bluetooth a aplikace eRouška spotřebují baterie?\nJestliže již nyní používáte telefon se stále zapnutým Bluetooth, spotřeba energie by se neměla výrazněji zvýšit. Pokud Bluetooth nyní běžně nepoužíváte, tak podle výsledků našeho testování s neustále zapnutým Bluetooth a ukládáním dat do aplikace bylo navýšení denní spotřeby energie v řádu jednotek procent. Ve většině případů to je méně než 20 % kapacity baterie za den. Záleží přitom na konkrétním chytrém telefonu, jeho stáří, stylu jeho používání a stavu baterie.\n\nSpotřebu baterie spojenou s použitím aplikace eRouška je ovlivňují dva faktory. Zaprvé samotným během aplikace a zadruhé využitím systémové funkce Oznámení o kontaktech s nákazou, která průběžně zaznamenává setkání s uživateli v okolí. Systémovou funkci zajišťuje OS Android nebo iOS, proto její optimalizace záleží na aktualizacích ze strany společností Apple a Google.\n\nPři interpretaci spotřeby baterie ze statistik v nastavení telefonu berte prosím v potaz, že toto procento odpovídá plnému využití vašeho telefonu za posledních 24 hodin (procentuální podíl běhu mezi ostatními aplikacemi). To znamená, že u málo využívaného telefonu může být procento u Kontaktů s nákazou/aplikace eRouška vysoké, u intenzivně využívaných telefonů naopak nízké. Nejde tedy přímo o procento spotřeby baterie.\n\n**Android: Poznámka k povolení polohových služeb:** Bluetooth zařízení ve vaší blízkosti lze detekovat, pouze pokud je v telefonu aktivována možnost „Použít polohu“. (Podrobnosti najdete v odstavci [Proč v telefonu musí být zapnuté určování polohy? na webu Googlu](https://support.google.com/android/answer/9930236?hl=cs).) To znamená, že informaci o vaší poloze mohou využívat i jiné aplikace ve vašem zařízení, což může být důvodem vyšší spotřeby baterie. Proto prosím zkontrolujte, které aplikace používají vaši polohu. Přejděte do nastavení zařízení a vyberte položky Zabezpečení a poloha → Umístění → Oprávnění na úrovni aplikace (případně následujte [postup od společnosti Google](https://support.google.com/accounts/answer/6179507?hl=cs)). Zde můžete jiným aplikacím odepřít oprávnění používat vaši polohu, pokud si nemyslíte, že to potřebují. Protože eRouška toto oprávnění nepotřebuje, nezobrazí se v seznamu.\n\n## Je možné, že se mi po zapnutí eRoušky zpomalilo Wi-Fi / připojení k internetu?\nU některých modelů mobilů s OS Android je bohužel známé, že vysílání Bluetooth může částečně negativně ovlivňovat signál Wi-Fi a naopak.\n\n## Může eRouška způsobovat problémy s dalšími aplikacemi nebo zařízeními/periferiemi (sluchátka, hodinky, náramek), které používají Bluetooth?\nVe výjimečných případech nám někteří uživatelé hlásí nestandardní chování jejich Bluetooth periferií poté, co nainstalovali aplikaci eRouška. Např. může docházet k přerušování spojení – typicky u chytrých hodinek či náramků, bezdrátových sluchátek nebo handsfree sad. S největší pravděpodobností jde o problém funkcí operačního systému, na které bohužel nemáme vliv.\n\nZkuste prosím následný postup:\n\n1. Restartujte telefon.\n2. Zrušte spárování s Bluetooth zařízením a znovu jej připojte.\n\nPokud kroky výše nepomohou, kontaktujte prosím výrobce zařízení:\n\n* Android: [Podpora výrobců zařízení](https://support.google.com/android/answer/3094742?hl=cs&ref_topic=7313011)\n\n## Proč aplikace používá zrovna Bluetooth? Neexistují lepší řešení jako GPS nebo triangulace od operátorů?\nSoukromí uživatelů je pro nás na prvním místě, a právě technologie Bluetooth nabízí nejvyváženější poměr přesnosti záznamu a minimálního zásahu do soukromí. Technologie GPS ani triangulace vysílačů od operátorů nemohou poskytnout potřebné údaje s požadovanou přesností. Mimo jiné i proto, že jejich funkčnost je ve vnitřních prostorách či metru velmi omezená. Aplikace eRouška potřebuje pro svoji funkci informace o tom, že došlo k setkání, na jakou vzdálenost a po jakou dobu. Není důležité, kde k setkání došlo.\n\n## Je Bluetooth dostatečně bezpečná technologie?\nBluetooth je dnes běžně rozšířená a podporovaná komunikační technologie pro propojení zařízení s dalšími periferiemi, jako jsou bezdrátová sluchátka, reproduktory, chytré náramky nebo hodinky. Vzhledem k tomu, že společnosti Apple a Google vytvořily společný protokol určený výhradně pro národní aplikace, které trasují riziková setkání s nakaženými COVID-19 na základě Bluetooth LE, jsme přesvědčení, že jsme pro eRoušku zvolili správně. Je však potřeba říct, že úplně každá technologie je zranitelná. I proto všem uživatelům doporučujeme, aby operační systém a aplikace ve svém telefonu pravidelně aktualizovali. Aktualizace totiž mohou obsahovat důležité bezpečnostní záplaty.\n\n# Obecné informace\n## Jak mám postupovat, když mi aplikace zobrazila upozornění na rizikové setkání s nakaženým uživatelem?\nPokud máte příznaky nákazy (například zvýšenou teplotu, kašel, dušnost, bolest v krku, bolest hlavy, náhlou ztrátu čichu a chuti), telefonicky kontaktujte svého praktického lékaře. V případě, že je váš stav opravdu akutní, volejte rychlou záchrannou službu 155, případě linku integrovaného záchranného systému 112. Uvedené příznaky se mohou objevit 2 až 14 dní po rizikovém setkání.\n\nPokud nemáte žádné příznaky, chovejte se prosím zodpovědně:\n\n* Noste roušku či respirátor přes ústa a nos.\n* Často a důkladně si myjte ruce vodou a mýdlem či používejte dezinfekci.\n* Pravidelně dezinfikujte vlastní předměty (např. mobilní telefon).\n* Kašlejte a kýchejte do kapesníku či rukávu.\n* Používejte jednorázové kapesníky a poté je vyhoďte.\n* Vyhýbejte se zbytečnému shromažďování a dodržujte bezpečný odstup od ostatních (přibližně 2 metry).\n\nPokud po notifikaci eRoušky máte vážné podezření na rizikové setkání, kontaktujte svého praktického lékaře a řiďte se jeho pokyny. Je také možné, že vás bude v blízké době kontaktovat pracovník hygienické služby na základě epidemiologického šetření s konkrétním nakaženým. V takovém případě postupujte podle pokynů hygienické služby.\n\nPokud by se v průběhu 14 dní od rizikového setkání váš zdravotní stav zhoršil a objevily se u vás příznaky onemocnění COVID-19, kontaktujte svého praktického lékaře.\n\n## Jak mám postupovat, když mám pozitivní výsledek testu na COVID-19 nebo mi přišla SMS eRoušky s ověřovacím kódem?\nJestliže vám přišla SMS z laboratoře s pozitivním výsledkem testu na COVID-19, měla by vám do několika hodin přijít také SMS eRoušky s ověřovacím kódem pro odeslání dat. SMS eRoušky se odesílají automatizovaně po zapsání výsledku testů do informačního systému. Okamžik, kdy laboratoř předá výsledky do informačního systému, nedokážou automatizované procesy Chytré karantény ovlivnit.\n\nJakmile vám přijde SMS eRoušky s ověřovacím kódem, [postupujte prosím podle tohoto návodu](https://erouska.cz/sms). Uvedený postup vám umožní anonymně informovat ostatní uživatele o případném rizikovém setkání. Děkujeme.\n\nMáte pozitivní test a nepřišla vám SMS eRoušky? Napište si o nový kód na [info@erouska.cz](mailto:info@erouska.cz?subject=).\n\n## To budu muset pokaždé automaticky do karantény nebo na testy, když aplikace zahlásí, že jsem byl v kontaktu s někým nakaženým?\nNe, aplikace eRouška je plně anonymní, její použití je zcela dobrovolné a upozornění na možný rizikový kontakt s nakaženým není důvod pro nařízení karantény. Pokud u někoho test potvrdí nákazu, kontaktuje ho hygienická stanice, aby zjistila, s kým byl v posledních dnech v rizikovém kontaktu, a mohla tak omezit další šíření. Když tento člověk používá aplikaci eRouška, může kontaktovat i ty, které nezná, protože s nimi třeba cestoval v autobuse. Zadáním unikátního kódu může poslat upozornění všem ostatním uživatelům aplikace eRouška, se kterým byl v epidemiologicky rizikovém kontaktu.\n\nV okamžiku, kdy vás aplikace upozorní, že jste se setkali s osobou, u které bylo prokázané onemocnění COVID-19, byste měli postupovat podle zobrazených doporučení a v případě zdravotních potíží bez prodlení kontaktovat svého praktického lékaře.\n\n## Pozná eRouška, když poruším lékařem nařízenou karanténu nebo třeba pojedu na chatu?\nAplikace nesleduje vaši polohu, není to její účel a ani pro to není navržena. Nepozná tedy, zda jste porušili karanténní opatření nebo kam jste odjeli. Má naopak pomoci k tomu, aby v karanténě byly pouze osoby s diagnostikovaným onemocněním nebo s vážným podezřením nákazy.\n\n## Může mi eRouška místo lékaře vystavit neschopenku? Je upozornění na možné rizikové setkání důvod pro pracovní neschopnost?\nNemůže, není. Pokud vám eRouška zobrazila varování, postupujte podle zobrazených doporučení. Upozornění na možný rizikový kontakt nijak nenahrazuje zprávu od vašeho lékaře nebo laboratoře, která vyhodnotí váš test na COVID-19. Pouze lékař vám může vystavit platné dokumenty, které předložíte svému zaměstnavateli.\n\n## Proč je eRouška potřeba? Dokáže mě vůbec efektivně chránit před nákazou?\nJedním z úkolů eRoušky je zastavit další šíření nákazy tím, že pomáhá co nejdříve izolovat potenciálně nakažené osoby od ostatních. To také pomáhá předcházet nutnosti zavádět plošná opatření, která mají negativní dopad na společnost a ekonomiku ČR. Aplikace na podobném principu fungují i v dalších státech a jsou účinným prostředkem v boji proti šíření nemoci COVID-19.\n\nAplikace eRouška 2.0 je doplňkovou aplikací Chytré karantény – systému ministerstva zdravotnictví. Prostřednictvím technologie Bluetooth se eRouška spojuje s dalšími eRouškami v nejbližším okolí a ukládá jejich anonymní identifikátory. Pokud test u člověka prokáže nákazu COVID-19, přijde mu automaticky ověřovací SMS kód, který mu umožní z aplikace odeslat své anonymní identifikátory do ostatních eRoušek. Aplikace ostatních uživatelů pak podle epidemiologického modelu\* co nejpřesněji vyhodnotí, jestli tito uživatelé přišli s nakaženým do kontaktu dost blízko a na dostatečně dlouhou dobu, aby bylo nutné je prostřednictvím notifikace v mobilu varovat a směřovat k dalšímu vyšetření. eRouška 2.0 tedy pomáhá co nejefektivněji informovat další potenciální přenašeče viru a urychlit jejich testování. Tím se sníží počet infikovaných ve společnosti i riziko další infekce.\n\n_\* Blíže než 2 metry po dobu delší než 7 minut._\n\n## Co když někdo nepřizná, že je nemocný?\nI to se může stát. Věříme však, že uživatelé eRoušky chtějí aktivně pomáhat se zastavením dalšího šíření nákazy a aplikaci využijí, aby mohli co nejdříve varovat osoby, se kterými přišli do rizikového kontaktu. **Záměrné šíření COVID-19 je v ČR považováno za trestný čin.**\n\n## Má smysl si eRoušku instalovat, když ji nebude používat dost lidí?\nPrávě proto byste neměli váhat. Čím více nás bude eRoušku používat, tím lepší vytvoříme síť, která nás bude vzájemně chránit a varovat před rizikem. Pomozte nám i vy. Instalací aplikace se můžete aktivně zapojit do boje proti COVID-19. O aplikaci můžete říct i lidem ve svém okolí, případně jim s jejich souhlasem pomoci s instalací.\n\n## Jak se eRouška liší od jiných podobných aplikací?\neRouška používá technologii Bluetooth, kterou je vybavená většina chytrých mobilních zařízení v Česku. Díky tomu funguje i v budovách, podzemních parkovištích nebo v metru. Aplikace je navržená tak, aby nesbírala informace o vaší poloze (např. přes GPS). Pouze anonymně zjišťuje, se kterými dalšími uživateli aplikace jste přišli do bližšího kontaktu. Jako jediná v ČR může oficiálně používat Apple/Google protokol, který garantuje bezpečnost aplikace na platformách Android a iOS a snižuje spotřebu baterie.\n\n## Co je to chytrá karanténa? Jakou v ní hraje roli eRouška?\nJde o soubor chytrých opatření, která chrání lidi před nákazou a ekonomiku před kolapsem. Místo toho, aby byla v karanténě celá země, chytrá karanténa usnadňuje dohledávání, testování a izolaci pouze nemocných a z nemoci důvodně podezřelých lidí.\n\nKonkrétně jde o systém řízení v oblastech předpovědi vývoje, podpory rozhodování, aktivního vyhledávání kontaktů, testování vč. řízení kapacit odběrových míst, laboratoří, řízení karantén a souvisejícího materiálního a IT zabezpečení ke zvládnutí pandemie onemocnění COVID-19. Chytrá karanténa 2.0 je označením pro přechod projektu pod Ministerstvo zdravotnictví, které bude díky novým nástrojům umět do budoucna kdykoliv velmi rychle reagovat na jakékoliv epidemie.\n\nAplikace eRouška je jeden z nástrojů, které vznikly na podporu chytré karantény. Pomocí technologií – eRoušky – můžeme aktivně upozornit rizikové jedince, kteří se setkali s nakaženým, aby zvýšili hygienická opatření, zvážili dobrovolnou karanténu, případně při pociťování příznaků kontaktovali obvodního lékaře. Díky těmto krokům věříme, že se podaří zmírnit šíření nákazy a ušetřit zdravotnictví fatálního přetížení.\n\n# Pokročilé informace o eRoušce\n## Jak eRouška zaznamenává a zpracovává data o setkáních uživatelů?\nChytrý telefon s aplikací eRouška zaznamenává přes Bluetooth LE anonymní identifikátory (ID) z jiných zařízení s touto aplikací. Informaci o „setkání“ a jeho délce ukládá do své vnitřní paměti.\n\nV případě, že budete mít pozitivní výsledek testu na COVID-19, přijde vám jednorázový SMS kód. Zadáte jej do aplikace ([postup zde](https://erouska.cz/sms)), a tím umožníte odeslání svých anonymních identifikátorů na server, odkud si je stáhnou ostatní eRoušky k vyhodnocení. Algoritmus v aplikaci každého uživatele na základě epidemiologického modelu automatizovaně vyhodnotí sesbíraná data – porovná identifikátory se všemi zaznamenanými setkáními. V případě rizikového kontaktu zobrazí upozornění, že hrozí nákaza kvůli setkání s pozitivně testovanou osobou, a doporučí další postup.\n\nPodrobné informace o sběru a zpracování dat naleznete v [Informacích o zpracování osobních údajů v aplikaci eRouška 2.0](https://erouska.cz/podminky-pouzivani).\n\n## Jak eRouška vyhodnocuje rizikové setkání?\nPůvodně epidemiologové stanovili jako rizikový kontakt jako setkání, které je ve vzdálenosti bližší než 2 metry po dobu alespoň 15 minut. Agresivnější šíření nových mutací koronaviru však ukazuje, že k nákaze stačí i kratší kontakt.\n\nNa vyšší agresivitu šíření mutací reagujeme po konzultacích postupu s Apple/Google a ostatními zeměmi EU postupným uvolňováním parametrů vyhodnocení rizikového kontaktu. Změny parametrů probíhají už od začátku března, průběžně vyhodnocujeme jejich vliv na počet nalezených kontaktů. Prozatím upravujeme zejména parametry odpovídající délce setkání, aktuálně jde přibližně o 7 minut.\n\nAplikace eRouška se snaží parametry vzdálenosti a času setkání vyhodnocovat co nejpřesněji dostupnými technologiemi. Vzdálenost mezi uživateli, respektive jejich telefony, se odhaduje na základě síly signálu Bluetooth. Doba setkání se posuzuje podle měřicích oken – telefon v několikaminutových intervalech zjišťuje, zda jsou v okolí jiné telefony s eRouškou.\n\nPřesnost měření výše uvedených parametrů má proto technická omezení. Čím větší vzdálenost a čím kratší kontakt, tím obtížnější je jeho přesné vyhodnocení. Uvolňování parametrů vyhodnocení tak zvyšuje pravděpodobnost falešných pozitivit a falešných negativit.\n\nVyšší přesnosti měření lze dosáhnout častějším vysíláním a zaznamenáváním dalších zařízení s eRouškou v okolí. To se bohužel promítne do vyšší spotřeby baterie. Algoritmy měření nastavují Apple a Google ve svém Exposure Notification protokolu, na kterém je eRouška postavená. Vývojáři eRoušky mohou pouze částečně upravovat některé parametry a provádět doplňující filtrování výsledků. Bližší vysvětlení najdete v sekci [Způsob vyhodnocení](https://erouska.cz/vyhodnoceni-rizika).\n\n## Jak eRouška reaguje na nové varianty/mutace viru?\nNové varianty virů, které se objevily od počátku pandemie, průběžně zkoumají odborníci z pohledu infekčnosti a dopadů na zdraví. Proto jsou vývojáři eRoušky v úzkém kontaktu s úřady i se společnostmi Apple a Google, které poskytují protokol pro vyhodnocování kontaktů s nákazou, aby v případě změny rizika infekce na základě nových poznatků mohli upravit algoritmus pro výpočet v aplikaci.\n\n## Nemůže se stát, že eRouška mylně vyhodnotí kontakt s jinou osobou, která je například vedle v autě na křižovatce, za dveřmi nebo tenkou zdí?\nAplikace eRouška funguje na principu měření síly signálu mezi dvěma zařízeními. Technicky proto nelze vyloučit nepřesný odhad vzdálenosti a mylnou detekci rizikového setkání. Technická omezení, rizika používání aplikace a postup vyhodnocování setkání popisujeme na stránce [Spolehlivost vyhodnocení rizikového kontaktu](https://erouska.cz/vyhodnoceni-rizika) a v sekci [Technické podmínky v Podmínkách zpracování](https://erouska.cz/podminky-pouzivani#technicke).\n\nPro vyhodnocení kontaktu jako rizikového je kromě vzdálenosti (do 2 m) relevantní též délka kontaktu (v řádu minut) – např. zastavení na semaforech na 1–2 minuty je tedy nevýznamné.\n\n## Zobrazilo se mi upozornění na možné setkání s nakaženým, ale eRouška po otevření ukazuje, že nezaznamenala nikoho nakaženého v mém okolí.\nPokud vás překvapilo oznámení s červenou ikonkou viru a textem Oznámení o možném kontaktu (Android) nebo Zaznamenání kontaktů (iOS), nemusíte se obávat. Jde pouze o systémové oznámení Androidu a iOS, na které nemá eRouška vliv. Jak vypadá skutečné oznámení eRoušky najdete [na tomto obrázku](https://www.facebook.com/eRouska/posts/180938253548928).\n\nPokud byste měli rizikový kontakt s nakaženou osobou, zjistíte to přímo na hlavní obrazovce eRoušky.\n\nPokud jste předtím skutečně viděli rizikové setkání přímo v eRoušce, odkud zmizelo, je možné, že aktualizací aplikace pro Android došlo k úpravě parametrů vyhodnocení rizikového kontaktu a nyní už eRouška nepovažuje dané setkání za rizikové.\n\n## Takže všichni budou vědět, že mám COVID-19?\nNebudou. Pokud onemocníte, vaše totožnost je známá jen a pouze pracovníkovi hygienické stanice. Osoby, kterým se zobrazí upozornění na možný rizikový kontakt, se nedozví, kdo ani kde je mohl nakazit. Aplikace zobrazuje pouze obecnou informaci o rizikovém setkání s nakaženým, den setkání a další doporučený postup. Vaše identita je ochráněna.\n\n## Byl jsem na testech na COVID-19, ale stále mi nepřišla SMS s ověřovacím kódem do eRoušky. Za jak dlouho ji mám čekat?\nNejprve by vám měl přijít výsledek testu v SMS od testovací laboratoře. Ta výsledky současně odesílá do centrálního informačního systému Ministerstva zdravotnictví. Poté, co se informace z informačního systému propíšou do systému hygieny, se odesílají automatické SMS eRoušky s ověřovacími kódy. Pokud laboratoř nestihne předat výsledky do informačního systému ve stejný den, může vám SMS s ověřovacím kódem přijít až následující den.\n\n**Zkontrolujte prosím, jestli info SMS eRoušky nezablokoval omylem systém jako spam**\n\n* **Android:** V aplikaci Zprávy klepněte na ikonu možností (tři tečky) a dále na Spam a zablokované konverzace (případně na tři tečky → Nastavení → SIM → Spam). Pokud je zde SMS eRoušky, klepněte na Není spam.\n\n**Pokud vám SMS eRoušky nepřišla ani následující den** poté, co vám přišla SMS s pozitivními výsledky z laboratoře, pošlete nám na [info@erouska.cz](mailto:info@erouska.cz?subject=) žádost o nový SMS kód. Uveďte prosím tyto informace:\n\n* celé jméno, které jste uvedli na žádance o test,\n* tel. číslo, které jste uvedli na žádance o test,\n* datum výsledku testů,\n* odběrové místo / laboratoř, ze které vám přišly výsledky testu,\n* typ testu (PCR/antigen).\n\n## Přijde mi SMS s kódem do eRoušky, když mám pozitivní antigenní test na COVID-19?\nPokud máte [pozitivní výsledek antigenního testu a máte příznaky COVID-19](https://drive.google.com/file/d/1HPwtltNRuzmowjZgE_c1u-a8m3W89T-p/view) (uvádíte v žádance), přijde vám SMS s kódem do eRoušky stejně jako v případě pozitivního PCR testu.\n\nV případě, že máte [pozitivní výsledek antigenního testu, ale nepociťujete příznaky](https://covid.gov.cz/situace/antigenni-testovani/interpretace-vysledku-antigenniho-testu-jak-se-zachovat-po-obdrzeni), půjdete na PCR test. Na základě pozitivního výsledku PCR testu by vám přišla SMS s kódem do eRoušky. V případě negativního PCR testu neodesíláte data z eRoušky.\n\n## Ve kterých státech EU můžu eRoušku používat? Jak funguje spolupráce se zahraničními aplikacemi?\nEvropská komise vyvinula centrální bránu EFGS pro bezpečnou výměnu informací („brána pro zajištění interoperability“). Tato brána umožňuje aplikaci eRouška přijímat a předávat si varování se zapojenými trasovacími aplikacemi používanými v jednotlivých státech EU. Podrobné informace o bráně jsou k dispozici na webu Evropské komise v dokumentu [Koronavirus: brána EU k zajištění interoperability aplikací pro vysledování kontaktů a varování – otázky a odpovědi](https://ec.europa.eu/commission/presscorner/detail/cs/qanda_20_1905#gateway).\n\nChcete-li zobrazit další informace o zapojených státech a nastavení, klepněte na hlavní obrazovce aplikace na Cesty do zahraničí, kde můžete funkci zapnout/vypnout a níže vidět seznam zapojených států. Oficiální aktuální seznam zapojených států EU najdete [na webu Evropské komise.](https://ec.europa.eu/health/sites/health/files/ehealth/docs/gateway_jointcontrollers_en.pdf)\n\n## Používá eRouška Apple/Google API?\nNová verze – eRouška 2.0 pro Android i iOS již používá Apple/Google Exposure Notification API. Nezapomeňte proto prosím aplikaci eRouška aktualizovat v [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska) nebo [App Store (iOS)](https://apps.apple.com/cz/app/erou%C5%A1ka/id1509210215) a znovu aktivovat.\n\n## Co je to Apple/Google Exposure Notification API (protokol)?\nSpolečnosti Apple a Google společně vyvíjejí technologie, díky kterým mohou veřejné zdravotní instituce (u nás MZČR) vyvíjet národní aplikace pro trasování rizikových kontaktů s osobami s onemocněním COVID-19.\n\nKonkrétně jde například o rozhraní, které daným aplikacím (u nás eRouška) umožňuje spolehlivě přistupovat k systémovým prostředkům Android a Apple zařízení za účelem zaznamenání setkání – zejména využívat Bluetooth LE pro ověření kontaktu a běžet na pozadí bez potřeby dalších nastavení aplikace nebo operačního systému. Šifrovaný komunikační protokol pak zajišťuje zabezpečený přenos aktuálních seznamů identifikátorů nakažených uživatelů mezi trasovacími aplikacemi za účelem vyhodnocení rizikových setkání a včasného zobrazení notifikace.\n\nBližší informace naleznete přímo na webových stránkách společností [Apple (anglicky)](https://www.apple.com/covid19/contacttracing) a [Google (česky)](https://www.google.com/covid19/exposurenotifications/).\n\n## Kde v eRoušce najdu informace o zaznamenaných setkáních?\nAplikace eRouška 2.0 je postavená na Apple/Google Exposure Notification protokolu, který zajišťuje zabezpečené zaznamenávání a vyhodnocení setkání. Z bezpečnostních důvodů však neposkytuje aplikaci přímý přístup k datům o zaznamenaných setkáních.\n\nZda aplikace funguje správně, se dozvíte na její hlavní obrazovce. Informace o všech zaznamenaných setkáních s nakaženými uživateli jsou uložená na úrovni systému. Tato setkání však nemusela být riziková. Naleznete je v Nastavení:\n\n* Android: Nastavení → Google → Oznámení o možném kontaktu COVID-19 → klepnout na tři tečky vpravo nahoře → Kontrola kontaktu\n\n## Co znamenají jednotlivé záznamy v Nastavení → Kontrola kontaktů?\nProtokolování kontaktů s nákazou ukazuje, kolik klíčů od nakažených uživatelů se stáhlo do vašeho telefonu ze serveru a kolik z nich odpovídá klíčům, které váš telefon zaznamenal.\n\n**Kontroly kontaktů:** Zde jsou data a časy jednotlivých stažení klíčů ze serveru za poslední dva týdny. Pro další kontrolu je potřeba otevřít jednotlivé záznamy.\n\n**Android: Počet klíčů:** Jde o počet klíčů od pozitivně testovaných uživatelů. Tyto klíče si váš v telefon v daném datu stáhl pro kontrolu možných kontaktů s nakaženými uživateli.\n\n**Android: Počet výsledků:** Jde o počet shod mezi staženými klíči a těmi, které zaznamenal váš telefon ve svém okolí. Pokud je číslo vyšší než 0, tak jste se setkali s nakaženým uživatelem. To však ještě neznamená, že nutně muselo jít o rizikové setkání.\n\n**Časová značka:** Datum a čas, kdy ke kontrole došlo. Nejde však o čas rizikového setkání (ten není k dispozici kvůli ochraně soukromí uživatelů).\n\n**Zdroj dat:** Aplikace, která zajišťuje kontrolu a vyhodnocení rizikových setkání.\n\n**Hašovací kód:** Kontrolní součet souboru obsahujícího stažení klíče nakažených uživatelů. Dokud se obsah souboru nezmění, tak se nezmění ani kontrolní součet během příslušného dvoutýdenního období.\n\n## Jak zjistím, že eRouška funguje na pozadí? Proč na Androidu nevidím ikonku eRoušky v záhlaví?\nNotifikační ikonu jsme odstranili, protože zaznamenávání a vyhodnocování setkání již zajišťuje přímo Apple/Google protokol na úrovni operačního systému. Samotná aplikace eRouška se pak stará o stahování klíčů (identifikátorů) nakažených uživatelů 1–2× denně a nemusí běžet permanentně na pozadí.\n\nŽe je aplikace aktuální, zjistíte na její hlavní obrazovce. Stahování klíčů nakažených pak můžete ověřit v Nastavení telefonu:\n\n* **Android:** Nastavení → Google → Oznámení o možném kontaktu COVID-19 → klepnout na tři tečky vpravo nahoře → Kontrola kontaktu\n\n## Proč se liší datum a čas „Poslední aktualizace dat“ na hlavní obrazovce eRoušky a data a časy „Kontroly kontaktů“, které vidím v Nastavení telefonu?\nAplikace eRouška ukazuje na hlavní obrazovce datum a čas posledního úspěšného pokusu o kontrolu nových klíčů (identifikátorů) nakažených uživatelů na serveru. V Nastavení telefonu se zobrazují pouze data a časy, kdy byly naposledy nové klíče nalezeny a vyhodnoceny v telefonu.\n\nKlíče nakažených uživatelů se do telefonu stahují 1–2× denně.\n\n## Proč v eRoušce nevidím konkrétní čas rizikového setkání?\nAplikace eRouška je postavená na Apple/Google Exposure Notification protokolu, který zajišťuje zaznamenávání a vyhodnocování setkání na úrovni operačního systému. Apple/Google protokol poskytuje eRoušce přístup pouze k datu rizikového setkání, bližší informace nejsou k dispozici z důvodu co nejvyšší ochrany soukromí uživatelů.\n\n## Proč v Kontrole kontaktů v Nastavení telefonu vidím „Nalezené klíče: 0“?\nZnamená to, že Apple/Google API ve vašem telefonu nevyhodnotilo v daném datu žádné setkání s nakaženým uživatelem. Jinak řečeno stažené klíče nakažených uživatelů se v daném dni neshodují s žádným ze zaznamenaných klíčů ve vašem zařízení.\n\n## Proč v Nastavení telefonu v sekci Kontrola kontaktu vidím u konkrétního data nalezené klíče, ale eRouška mi nezobrazuje žádná riziková setkání?\nPočet nalezených klíčů představuje počet zaznamenaných setkání s nakaženými uživateli. Neznamená to však nutně, že tato setkání byla riziková. Pokud setkání s nakaženým uživatelem nebylo rizikové, tak na něj eRouška neupozorňuje. Proto v aplikaci není vidět.\n\n## Zobrazilo se mi upozornění/notifikace na rizikové setkání, ke kterému došlo před několika dny. Proč mě eRouška upozornila až teď?\nAplikace eRouška nemůže vyhodnotit riziková setkání a zobrazit upozornění dříve, než uživatel s pozitivním testem na COVID-19 odešle data ze své eRoušky. Pokud se vám dnes zobrazila notifikace, která upozorňuje na rizikové setkání s nakaženým před několika dny, znamená to, že nakažený odeslal data ze své eRoušky až dnes, nejpozději včera. Vaše eRouška si je dnes stáhla a vyhodnotila.\n\n## Zobrazilo se mi upozornění/notifikace na rizikové setkání s nakaženým, ale na obrazovce Rizikové setkání vidím více záznamů s různými daty.\nAplikace eRouška vyhodnocuje riziková setkání až poté, co uživatel s pozitivním testem na COVID-19 odešle data ze své eRoušky. Pokud se vám zobrazila pouze jedna notifikace na rizikový kontakt, ale v Rizikových setkáních máte více záznamů, znamená to, že jste se s nakaženým setkali v předchozích dnech vícekrát nebo že v jeden den odeslalo data ze svého telefonu více nakažených uživatelů současně.\n\n## Bude eRouška fungovat jako „covid pas“ pro zobrazování výsledků testů a prokázání očkování?\nAnonymní charakter aplikace eRouška ji neumožňuje kombinovat s tzv. digitálním zeleným certifikátem (Digital Green Certificate), který pracuje s ověřenou identitou a osobními údaji uživatele – výsledky testů nebo provedeným očkováním proti COVID-19.\n\n## Publikuje eRouška nějaké statistiky o používání aplikace?\nStatistiky publikujeme ve formátu JSON na adrese [stats.erouska.cz](https://stats.erouska.cz). Data mají následující strukturu:\n\n{\\n "data": {\\n "modified": 1608696001,\\n "date": "20201223",\\n "activations\_yesterday": 2042,\\n "activations\_total": 1475571,\\n "key\_publishers\_yesterday": 718,\\n "key\_publishers\_total": 39175,\\n "notifications\_yesterday": 631,\\n "notifications\_total": 197298\\n }\\n}\n\n* **modified** = Unix timestamp vygenerovaných statistik,\n* **date** = datum vytvoření reportu,\n* **activations\_yesterday** = počet aktivací za předchozí kalendářní den,\n* **activations\_total** = celkový počet aktivací eRoušky,\n* **key\_publishers\_yesterday** = počet nakažených lidí, kteří zadali v předchozím dni do eRoušky kód pro odeslání dat,\n* **key\_publishers\_total** = celkový počet nakažených lidí, kteří zadali do eRoušky kód pro odeslání dat,\n* **notifications\_yesterday** = počet lidí, kterým se za předchozí den zobrazilo upozornění na rizikové setkání,\n* **notifications\_total** = celkový počet lidí, kterým se zobrazilo upozornění na rizikové setkání.\n\nZavolání bez parametru zobrazí informace k aktuálnímu datu. Zavolání s parameterem `date` umožní získat data pro libovolný den od 23. 10. 2020 (za všechny předchozí dny máme pouze agregovaná data). Pomocí parametru `date` s hodnotou `all` lze získat všechna data, která rozhraní poskytuje.\n\n* dnešní data: [`https://stats.erouska.cz/`](https://stats.erouska.cz/)\n* data z 1. 12. 2020: [`https://stats.erouska.cz/?date=2020-12-01`](https://stats.erouska.cz/?date=2020-12-01)\n* data za všechny dny: [`https://stats.erouska.cz/?date=all`](https://stats.erouska.cz/?date=all)\n\n# Zabezpečení dat\n## Jak zjistím, která všechna data přesně aplikace ukládá a odesílá?\nAplikace eRouška používá od verze 2.0 tzv. Apple/Google Exposure Notification API, které zprostředkovává sběr dat a předávání identifikátorů mezi uživateli. Aplikace kvůli tomu už nemá přímý přístup k naměřeným údajům, které by mohla uživatelům zobrazit, jako tomu bylo u verze 1.0.\n\nAplikace denně stahuje seznam identifikátorů od uživatelů, kterým test potvrdil nákazu. Data s identifikátory ke zpracování může ostatním odeslat pouze samotný uživatel, a to až po zadání jednorázového SMS kódu, který získá v případě pozitivního testu na nákazu nemocí COVID-19 automaticky, případně na vyžádání.\n\nAplikace dále odesílá na server anonymní statistickou/agregovanou informaci, že došlo k notifikaci rizikového kontaktu. Tato informace není vázána na žádný identifikátor uživatele ani jeho mobilu a slouží pouze k výpočtu statistických informací o účinnosti systému eRouška.\n\nAbychom zajistili funkčnost eRoušky, pracujeme také s údaji o jejím fungování (např. záznamy o pádech aplikace a používání aplikace) na vašem telefonu a používáme k tomu standardní nástroje (Firebase Crashlytics a Google Analytics) od společnosti Google. Telemetrická data odesílaná aplikací do těchto služeb neobsahují identifikátory vaší osoby nebo vašeho telefonu.\n\nPodrobné informace o sběru a zpracování dat naleznete v [Informacích o zpracování osobních údajů v aplikaci eRouška 2.0](https://erouska.cz/podminky-pouzivani).\n\n## Přenesou se data z eRoušky po obnově telefonu ze zálohy nebo při přechodu na nový telefon?\nZ důvodů ochrany soukromí uživatelů operační systémy Android ani iOS nezálohují data z Exposure Notifications. Při reinstalaci telefonu nebo přechodu na jiné zařízení tedy nelze přenést ani obnovit zaznamenané a vygenerované identifikátory.\n\nPokud chcete získat informaci o možném rizikovém setkání, doporučujeme sledovat stav starého telefonu ještě po dobu 14 dní po instalaci eRoušky na nové zařízení. Přitom je potřeba:\n\n* zajistit, aby vaše zařízení bylo zapnuté a připojené k internetu, aby si mohlo stahovat data k vyhodnocení,\n* ideálně alespoň jednou denně kontrolovat starší zařízení, tedy otevřít eRoušku.\n* Pokud byste během těchto 14 dní měli pozitivní test, zadejte prosím kód do nového zařízení a pro získání kódu pro starší mobil kontaktujte [info@erouska.cz](mailto:info@erouska.cz).\n\nPo 14 dnech už váš starý telefon nebude obsahovat žádná data pro vyhodnocení rizikového setkání. Můžete z něj proto odstranit eRoušku a ponechat jen v novém zařízení.\n\n## Co když mi telefon ukradnou nebo ho ztratím?\nV aplikaci eRouška jsou lokálně uložené jen anonymní informace o dalších zařízeních, která eRouška zaznamenala ve svém okolí. Žádné riziko to nepřináší pro vás ani pro majitele takto zaznamenaných zařízení. Jiné aplikace, které běžně využíváte, mohou ve vašem telefonu pravděpodobně ukládat mnohem citlivější údaje. Mějte proto svůj telefon chráněný kódem nebo biometricky (otiskem prstu nebo obličejem).\n\n## Je aplikace eRouška v souladu s GDPR?\nCelý systém aplikace eRouška, včetně webových stránek, je navržen plně v souladu s GDPR i zákonem o zpracování osobních údajů.\n\nZde naleznete bližší informace:\n\n* [Informace o zpracování osobních údajů v aplikaci eRouška 2.0](https://erouska.cz/podminky-pouzivani)\n* [Audit zdrojového kódu aplikace](https://erouska.cz/audit-kod)\n\n## Jak uživatele chráníte před zneužitím dat?\nUživatele chráníme především minimálním rozsahem zaznamenávaných dat a jejich ukládáním přímo do zařízení. Data uživatelů se bez jejich vědomí a souhlasu nikam neposílají ani nezpracovávají. Do telefonů se zaznamenávají anonymní identifikátory (ID) zařízení s nainstalovanou aplikací eRouška a informace o čase, délce a síle jejich Bluetooth signálu. Data se zpracovávají automatizovaně na serveru pouze po odsouhlaseném odeslání uživatelem.\n\n## Dohlíží na zabezpečení dat z aplikace nějaká nezávislá organizace?\nCelý systém aplikace eRouška, včetně podpůrných webových stránek, je navržený plně v souladu s GDPR. Kód aplikace je jako open-source volně k dispozici ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)) a prošel [auditem nezávislých vzdělávacích institucí](https://erouska.cz/audit-kod).\n\nDalší informace naleznete v [Informacích o zpracování osobních údajů v rámci aplikace eRouška 2.0](https://erouska.cz/podminky-pouzivani).\n\n## Může mě někdo pomocí aplikace eRouška sledovat?\nNe. Aplikace nesbírá údaje o vaší poloze a k uloženým datům máte přístup pouze vy. Samotné údaje, které aplikace po vašem souhlasu odesílá, nemohou být použity k vašemu sledování. Každá eRouška navíc mění z důvodu bezpečnosti a ochrany soukromí uživatelů pravidelně své vysílané ID, aby nebylo snadné jej odchytit a nějak zneužít.\n\n## Lze ověřit, že aplikace nezaznamenává moji polohu?\nAno. Aplikace eRouška je publikována s otevřeným zdrojovým kódem ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)), takže znalý člověk může snadno ověřit, že údaje o poloze aplikace opravdu nesbírá.\n\nZdrojové kódy aplikace eRouška [prověřují nezávislé autority](https://erouska.cz/audit-kod), které kontrolují, že aplikace:\n\n* nesleduje polohu\n* sama automaticky data s identifikátory nikam neodesílá\n\n## Lze identifikátor aplikace (ID) uživatele spárovat s konkrétní osobou?\nAplikace eRouška 2.0 [nepracuje s osobními údaji](https://erouska.cz/podminky-pouzivani). Vygenerovaná anonymní ID eRoušky se mezi aplikacemi a servery přenášejí výhradně po chráněném komunikačním protokolu, který společně vyvinuly společnosti Apple a Google. K identifikátorům nemají přístup ani uživatelé, ani vývojáři, ani jiná autorita.\n\n## Kdo má (může mít) přístup k mým datům?\nK zaznamenaným údajům o setkání nemá v eRoušce 2.0 přístup uživatel, ani nikdo jiný. Důvodem je, že aktuální verze aplikace používá Apple/Google protokol, který k zaznamenaným datům neposkytuje přístup ani uživatelům, ani vývojářům a správcům. Data (anonymní identifikátory nakažených) se přenášejí na server a odtud do ostatních eRoušek šifrovaná a vyhodnocují se až na koncových zařízeních automatizovaně za účelem zobrazení případných upozornění o rizikovém kontaktu. Ze serveru i ze zařízení se data mažou automaticky po 14 dnech.\n\n## Jak dlouho do minulosti jsou data k dispozici? Kdy a jak probíhá jejich mazání?\nV aplikaci eRouška se uchovávají data zaznamenaná za posledních 14 dní, starší záznamy se automaticky odmazávají. Pokud po pozitivním testu na COVID-19 zadáte ověřovací kód do eRoušky a odešlete identifikátory z telefonu na server, zůstanou zde 14 dní a poté dojde k jejich smazání. Data můžete ze svého zařízení smazat v Nastavení Oznámení o kontaktech s nákazou / Kontakty s nákazou / Exposure Notification – konkrétní postup záleží na typu a verzi operačního systému.\n\n* **Android:** Na zařízeních se systémem Android v současném nastavení dojde při odinstalování aplikace eRouška ke smazání zaznamenaných klíčů (identifikátorů).\n\nInformace o správcích a zpracovatelích dat naleznete v [Informacích o zpracování osobních údajů v rámci aplikace eRouška](https://erouska.cz/podminky-pouzivani).\n\n## Můžu si používání aplikace kdykoli rozmyslet? Můžu smazat data, která odešlu ke zpracování?\nAplikaci můžete kdykoliv odinstalovat. Vygenerované a zaznamenané klíče (záznamy setkání) můžete ze svého zařízení smazat v Nastavení Oznámení o kontaktech s nákazou / Kontakty s nákazou / Exposure Notification – konkrétní postup záleží na typu a verzi operačního systému. Data odeslaná na server se smažou automaticky po 14 dnech.\n\n* **Android:** Na zařízeních se systémem Android v současném nastavení dojde při odinstalování aplikace eRouška také ke smazání zaznamenaných klíčů (identifikátorů).\n\n## Co se děje s daty po smazání aplikace z mobilu?\n**Android:** Když smažete aplikaci z telefonu nebo tabletu s OS Android, dojde také ke smazání zaznamenaných klíčů (identifikátorů).\n\nPokud aplikaci znovu nainstalujete, začnou se sbírat a do telefonu ukládat zcela nová data.\n\n## Kde najdu zdrojový kód aplikace?\nAplikace eRouška je publikována s otevřeným zdrojovým kódem. Najdete jej na GitHubu ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)).\n\n# Instalace a kompatibilita\n## Na jakých telefonech aplikace eRouška funguje? Proč jsou nároky vyšší než u eRoušky 1.0?\nPro instalaci aplikace eRouška potřebujete mobilní zařízení s těmito parametry:\n\n* iPhone s iOS verze 13.5 a vyšší\n* OS Android verze 6.0 a vyšší s Google Play Services (nemá jen malé procento telefonů Huawei a některé telefony s vlastní ROM)\n* Bluetooth LE (Low Energy)\n\nPokud vaše mobilní zařízení není s eRouškou kompatibilní (nemá potřebné technologie/funkce), Google Play ani Apple App Store vám neumožní aplikaci nainstalovat. Aplikace je dostupná pouze pro některé tablety Android. Není k dispozici pro iPad.\n\nMinimální požadavky na kompatibilitu stanovují společnosti Apple a Google. Vycházejí z požadavků Apple/Google Exposure Notification protokolu, který nová eRouška používá. Na Apple/Google protokol jsme se rozhodli přejít kvůli zajištění fungování eRoušky na iPhonech a kvůli umožnění přeshraniční kompatibility s podobnými aplikacemi dalších evropských států.\n\n## Odkud si můžu eRoušku bezpečně stáhnout a nainstalovat?\nAplikace eRouška je dostupná pouze pro:\n\n* Android: [v Obchodě Google Play](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska).\n\n## Je možné aplikaci stáhnout i jinak než z oficiálních aplikačních obchodů? Je k dispozici balíček apk?\nAplikace eRouška 2.0 používá Apple/Google protokol pro zaznamenávání setkání a přenos dat. Proto je také závislá na systémových službách, bez kterých nebude fungovat, a není ji tedy možné instalovat z jiných než oficiálních aplikačních obchodů. Aplikační obchody spolehlivě vyfiltrují podporovaná zařízení.\n\nVývojáři eRoušky nikdy neposkytují uživatelům odkazy na instalační balíčky APK nebo samotné instalační soubory mimo oficiální aplikační obchody. Pro aplikace nainstalované neoficiální cestou bychom nedokázali garantovat jejich zabezpečení, zajistit jejich aktualizace ani stejnou úroveň uživatelské podpory. Proto prosím nikdy neinstalujte eRoušku z .apk souborů, které jste našli někde na internetu nebo vám je někdo poslal e-mailem. Mohou obsahovat viry, trojské koně nebo jiný závadný obsah.\n\n## Můžu používat eRoušku spolu s jinou oficiální trasovací aplikací jiného státu (např. Corona-Warn-App)?\nPokud pobýváte primárně na území ČR, doporučujeme používat eRoušku i pro cesty do zahraničí. [Aplikace nyní spolupracuje i s dalšími státy EU](https://erouska.cz/caste-dotazy#efgs).\n\nV ostatních případech můžete mít v telefonu nainstalováno více trasovacích aplikací typu eRoušky, jako jsou Corona-Warn-App, Stopp Corona App, Imunni, Stop Covid ProteGO Safe a další. Pokud však také používají Apple/Google Exposure Notification protokol (platí zejména pro sousední státy ČR), můžete mít v jeden okamžik aktivní pouze jednu z těchto aplikací. Ostatní je potřeba pozastavit. K zaznamenávání setkání v Apple/Google protokolu totiž může mít přístup vždy pouze jedna aplikace.\n\n## Funguje eRouška, když mám na mobilu úsporný režim / režim nízké spotřeby / spořič baterie?\n**Android:** Spořiče/šetřiče baterie na zařízeních s OS Android mohou do značné míry omezovat běh aplikací na pozadí. Ačkoliv systém oznámení o možném kontaktu s onemocněním COVID-19 nadále funguje, nemusí docházet k vyhodnocování rizikových kontaktů, a tudíž k případnému zobrazení notifikací.\n\n## Bude možné používat eRoušku i na chytrých hodinkách či náramcích?\nRozšíření je teoreticky do budoucna možné. Zatím jej ale nezvažujeme zejména proto, že by chytré hodinky či náramek musely podporovat Bluetooth LE (Low Energy) a zejména Apple/Google protokol. Základní Bluetooth bez LE sám o sobě pouze zajišťuje přenos dat, ale neumožňuje měření intenzity setkání uživatelů na základě síly signálu.\n\nv2_shareAppDynamicLinkhttps://erouska.cz/app/sdilejv2_minSupportedVersionCodeAndroid666v2_shouldCheckOSVersion1v2_unsupportedDeviceLinkhttps://koronavirus.mzcr.czv2_minSupportedVersion2.3.3v2_currentMeasuresUrlhttps://covid.gov.cz/opatreniv2_appleExposureConfiguration{"factorHigh":0.25,"factorStandard":1,"factorLow":2,"lowerThreshold":55,"higherThreshold":70,"triggerThreshold":10}v2_daysSinceOnsetToInfectiousness0;0;0;0;0;0;0;0;0;0;0;1;2;2;2;2;2;2;2;1;1;1;1;1;1;1;1;1;1v2_supportEmailinfo@erouska.czv2_appleServerConfiguration{"minSupportedVersion":"13.5","showExposureForDays":14,"healthAuthority":"cz.covid19cz.erouska","uploadURL":"https://exposure-fghz64a2xa-ew.a.run.app/v1/publish","downloadIndexName":"erouska/index.txt","downloadsURL":"https://google.com","verificationURL":"https://apiserver-jyvw4xgota-ew.a.run.app","verificationAdminKey":"","verificationDeviceKey":"4AP6RHk5VlNi7WIhj4ZupI9JZODdUyrPGb1C2mDYKo4cuaGIHdYtOhjyoqhg4vB5r7FDijxnySLb_1CUH6XdDA.1.oZLaUuVOPbvEaiWnzkQxasWlD_71iSTeM9aAIOKrfqhg5QO68t004CKxWutYmfISc7vqJe6uY2dRrt34OFkliw","appCurentDataURL":"https://europe-west1-daring-leaf-272223.cloudfunctions.net","firebaseURL":"https://europe-west1-daring-leaf-272223.cloudfunctions.net"}v2_handleError400AsExpiredOrUsedCodefalsev2_handleError500AsInvalidCodefalsev2_exposureHelpContentJson{"items":[{"iconUrl":"https://erouska.cz/img/exposure/ic_notification.png","label":"Upozornění se vám zobrazí nejdříve 24 hodin poté, co se nakažený dozví pozitivní výsledek testu na COVID-19 a zadá ověřovací kód do eRoušky."},{"iconUrl":"https://erouska.cz/img/exposure/ic_privacy.png","label":"Kvůli zachování anonymity neznáme čas ani místo setkání."},{"iconUrl":"https://erouska.cz/img/exposure/ic_distance.png","label":"Rizikový kontakt vyhodnotíme v případě, že jste s nakaženým byli v kontaktu na vzdálenost kratší než 2 metry po dobu alespoň 7 minut."},{"iconUrl":"https://erouska.cz/img/exposure/ic_14days.png","label":"Riziková setkání vyhodnocujeme za posledních 14 dní, protože příznaky onemocnění COVID-19 se u vás mohou projevit až 2–14 dní po setkání s nakaženým."}]}v2_exposureHelpUITitleNápovědav2_updateNewsOnRequesttruev2_recentExposureNotificationTitleVaše eRouška si dlouhou dobu neaktualizovala data, zda jste se setkali s nakaženým uživatelem eRoušky. Připojte se k internetu.v2_efgsCountriesAktuálně s eRouškou spolupracuje Belgie, Dánsko, Finsko, Chorvatsko, Irsko, Itálie, Kypr, Lotyšsko, Německo, Nizozemsko, Norsko, Polsko, Rakousko, Slovinsko a Španělsko.v2_diagnosisKeysDataMappingLimitDays7v2_infectiousnessWhenDaysSinceOnsetMissing1v2_reportTypeWhenMissing1v2_noEncounterCardTitleZa posledních 14 dní žádné rizikové setkánív2_encounterUpdateFrequencyAktualizace probíhá jednou za %d hodin.v2_dbCleanupDays15v2_selfCheckerPeriodHours4v2_showChatBotLinkfalsev2_efgsVisitedCountriesv2_efgsReportTypeConfirmedTestv2_efgsConsentToFederationfalsev2_efgsTravellerDefaultfalsev2_appleExposureConfigurationV2{"immediateDurationWeight":200,"nearDurationWeight":100,"mediumDurationWeight":25,"otherDurationWeight":0,"infectiousnessForDaysSinceOnsetOfSymptoms":{"0":2,"1":2,"2":2,"3":2,"4":2,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"unknown":1,"-14":0,"-13":0,"-12":0,"-11":0,"-10":0,"-9":0,"-8":0,"-7":0,"-6":0,"-5":0,"-4":0,"-3":1,"-2":2,"-1":2},"infectiousnessStandardWeight":100,"infectiousnessHighWeight":200,"reportTypeConfirmedTestWeight":100,"reportTypeConfirmedClinicalDiagnosisWeight":100,"reportTypeSelfReportedWeight":100,"reportTypeRecursiveWeight":100,"reportTypeNoneMap":1,"minimumRiskScore":0,"attenuationDurationThresholds":[55,70,80],"attenuationLevelValues":[1,2,3,4,5,6,7,8],"daysSinceLastExposureLevelValues":[1,2,3,4,5,6,7,8],"durationLevelValues":[1,2,3,4,5,6,7,8],"transmissionRiskLevelValues":[1,2,3,4,5,6,7,8],"minimumScore":420}v2_helpJson[{"title":"Nejčastější problémy","subtitle":"Doporučení dočasných oprav","icon":"https://erouska.cz/img/faq/tool.png","questions":[{"question":"Android: Základní doporučení při řešení problémů s aplikací","answer":"Pokud vám aplikace zobrazuje chybovou hlášku, zkontrolujte prosím nejprve, zda máte nainstalované aktualizace operačního systému ([postup aktualizace Androidu](https://support.google.com/android/answer/7680439)) a nepoužíváte jejich beta verzi.\n\nDále prosím zkontrolujte v obchodu [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska), zda není pro eRoušku dostupná nová aktualizace.\n\nDoporučujeme rovněž povolit automatické aktualizace aplikace eRouška. [Postup pro Android zde:](https://support.google.com/googleplay/answer/113412#autoone) Spusťte aplikaci Obchod Google Play, klepněte na nabídku Moje aplikace a hry → Vyberte aplikaci, kterou chcete aktualizovat → Klepněte na ikonu možností a klepněte na Aktivovat automatické aktualizace."},{"question":"Android: Nejde aktivovat eRouška. Chyba aktivace 17 (Exposure Notification API není k dispozici).","answer":"Pro vyřešení chyby na Android zařízení, zkuste prosím postupovat takto:\n\n1. Zkontrolujte, zda jste _hlavní uživatel_ zařízení, zda máte v telefonu služby Play a zda je k dispozici _Nastavení → Google → Oznámení o možném kontaktu (COVID-19 Exposure Notifications)_.\n2. Odinstalujte eRoušku.\n3. Smažte mezipaměť a data u Google Play Services a Obchodu Play ([postup zde, viz kroky 2 a 3](https://support.google.com/googleplay/answer/9037938)).\n4. Nainstalujte eRoušku a zapněte opět _Oznámení o možném kontaktu COVID-19_.\n\nPokud se vám i přes aktualizaci systému, aplikace a uvedené kroky nadále objevuje chyba aktivace, je bohužel možné, že vaše zařízení ještě nepodporuje Bluetooth LE (Low Energy Advertising), nepodporuje Bluetooth Multiple Advertisement, není autorizované Googlem pro Exposure Notifications nebo vůbec nemá Exposure Notifications API."}]},{"title":"Připojení a přenos dat","subtitle":"Bluetooth, GPS, internet","icon":"https://erouska.cz/img/faq/connection.png","questions":[{"question":"Musím mít Bluetooth zapnutý neustále?","answer":"Ano. Bez zapnutého Bluetooth není možné zjišťovat blízkost dalších zařízení s nainstalovanou aplikací eRouška. Tuto funkci máte zapnutou možná i nyní, například v případě, že používáte bezdrátová sluchátka, připojení v autě, chytrý náramek nebo hodinky. V nastavení Bluetooth vašeho telefonu/tabletu navíc doporučujeme povolit, aby byl _viditelný pro všechna zařízení_."},{"question":"Potřebuje aplikace připojení k internetu?","answer":"Připojení k internetu je nutné ke stažení (instalaci) a aktivaci eRoušky. Dále je potřeba, aby si eRouška mohla každý den stahovat aktuální seznam anonymních identifikátorů nakažených uživatelů, a zobrazit tak včas případnou notifikací o rizikovém setkání.\n\nPokud by u vás test potvrdil nákazu COVID-19 a vy byste chtěli anonymně upozornit ostatní na možný rizikový kontakt, bylo by opět potřeba eRoušku připojit k internetu. Do té doby jsou vaše data uložena pouze ve vašem telefonu, odkud se sama odmazávají po 14 dnech. Aplikace využívá připojení k internetu rovněž ke stažení informací o aktuálním vývoji epidemie COVID-19 v ČR, které zobrazuje na stránce _Aktuálně_."},{"question":"Jak často si aplikace aktualizuje data z internetu?","answer":"Klíče (identifikátory) nakažených uživatelů si eRouška stahuje několikrát denně. Statistiky nakažených osob na obrazovce Aktuálně se aktualizují jednou denně."},{"question":"Jak velký objem mobilních dat eRouška denně spotřebuje?","answer":"V případě zapnutého _Upozornění na zahraniční riziková setkání_ eRouška stahuje také klíče všech nakažených uživatelů z ostatních států EU. Objem stažených dat se v takovém případě může pohybovat až kolem 2 MB denně.\n\nI v případě vypnutého _Upozornění na zahraniční riziková setkání_ eRouška stahuje přibližně 1 MB denně. Nová verze aplikace stahuje nejen klíče nakažených uživatelů eRoušky, ale i klíče nakažených uživatelů z ostatních států EU, kteří ve svých aplikacích uvedli, že cestují do zahraničí, takže je uživatel eRoušky potenciálně mohl potkat i v České republice\n\nSoubory klíčů nakažených uživatelů se stahují postupně, 2–3× denně. Pro porovnání, načtení průměrné stránky zpravodajství vyžaduje 1–5 MB dat. **Aplikace nepotřebuje nutně mobilní datové připojení.** Data si stáhne sama i na Wi-Fi."},{"question":"Android: Proč je potřeba mít povolený přístup k GPS/polohovým/lokačním údajům?","answer":"**Aktualizace pro uživatele Android 11:** U nejnovější verze operačního systému Android již nemusíte pro fungování eRoušky povolovat služby určování polohy.\n\nAplikace eRouška nesbírá a neukládá data z GPS, avšak operační systém **Android 10 a starší** zahrnuje pod určování polohy také některé služby Bluetooth LE (LE = low energy), který komponenty eRoušky – konkrétně Google Play Services – pro své fungování vyžadují. Proto je nutný souhlas uživatele s přístupem aplikace k polohovým údajům. V iOS tento souhlas nutný není. Proč v telefonu musí být zapnuté určování polohy, [vysvětluje Google zde](https://support.google.com/android/answer/9930236).\n\nInformaci o vaší poloze mohou využívat i jiné aplikace ve vašem zařízení. Pokud chcete zkontrolovat, které aplikace k nim mají přístup, přejděte do nastavení zařízení a vyberte položky Zabezpečení a poloha → Umístění → Oprávnění na úrovni aplikace (případně následujte [postup od společnosti Google](https://support.google.com/accounts/answer/6179507?hl=cs)). Zde můžete jiným aplikacím odepřít oprávnění používat vaši polohu, pokud si nemyslíte, že to potřebují. Protože eRouška toto oprávnění nepotřebuje, nezobrazí se v seznamu."},{"question":"Kolik zapnutý Bluetooth a aplikace eRouška spotřebují baterie?","answer":"Jestliže již nyní používáte telefon se stále zapnutým Bluetooth, spotřeba energie by se neměla výrazněji zvýšit. Pokud Bluetooth nyní běžně nepoužíváte, tak podle výsledků našeho testování s neustále zapnutým Bluetooth a ukládáním dat do aplikace bylo navýšení denní spotřeby energie v řádu jednotek procent. Ve většině případů to je méně než 20 % kapacity baterie za den. Záleží přitom na konkrétním chytrém telefonu, jeho stáří, stylu jeho používání a stavu baterie.\n\nSpotřebu baterie spojenou s použitím aplikace eRouška je ovlivňují dva faktory. Zaprvé samotným během aplikace a zadruhé využitím systémové funkce Oznámení o kontaktech s nákazou, která průběžně zaznamenává setkání s uživateli v okolí. Systémovou funkci zajišťuje OS Android nebo iOS, proto její optimalizace záleží na aktualizacích ze strany společností Apple a Google.\n\nPři interpretaci spotřeby baterie ze statistik v nastavení telefonu berte prosím v potaz, že toto procento odpovídá plnému využití vašeho telefonu za posledních 24 hodin (procentuální podíl běhu mezi ostatními aplikacemi). To znamená, že u málo využívaného telefonu může být procento u Kontaktů s nákazou/aplikace eRouška vysoké, u intenzivně využívaných telefonů naopak nízké. Nejde tedy přímo o procento spotřeby baterie.\n\n**Android: Poznámka k povolení polohových služeb:** Bluetooth zařízení ve vaší blízkosti lze detekovat, pouze pokud je v telefonu aktivována možnost „Použít polohu“. (Podrobnosti najdete v odstavci [Proč v telefonu musí být zapnuté určování polohy? na webu Googlu](https://support.google.com/android/answer/9930236?hl=cs).) To znamená, že informaci o vaší poloze mohou využívat i jiné aplikace ve vašem zařízení, což může být důvodem vyšší spotřeby baterie. Proto prosím zkontrolujte, které aplikace používají vaši polohu. Přejděte do nastavení zařízení a vyberte položky Zabezpečení a poloha → Umístění → Oprávnění na úrovni aplikace (případně následujte [postup od společnosti Google](https://support.google.com/accounts/answer/6179507?hl=cs)). Zde můžete jiným aplikacím odepřít oprávnění používat vaši polohu, pokud si nemyslíte, že to potřebují. Protože eRouška toto oprávnění nepotřebuje, nezobrazí se v seznamu."},{"question":"Je možné, že se mi po zapnutí eRoušky zpomalilo Wi-Fi / připojení k internetu?","answer":"U některých modelů mobilů s OS Android je bohužel známé, že vysílání Bluetooth může částečně negativně ovlivňovat signál Wi-Fi a naopak."},{"question":"Může eRouška způsobovat problémy s dalšími aplikacemi nebo zařízeními/periferiemi (sluchátka, hodinky, náramek), které používají Bluetooth?","answer":"Ve výjimečných případech nám někteří uživatelé hlásí nestandardní chování jejich Bluetooth periferií poté, co nainstalovali aplikaci eRouška. Např. může docházet k přerušování spojení – typicky u chytrých hodinek či náramků, bezdrátových sluchátek nebo handsfree sad. S největší pravděpodobností jde o problém funkcí operačního systému, na které bohužel nemáme vliv.\n\nZkuste prosím následný postup:\n\n1. Restartujte telefon.\n2. Zrušte spárování s Bluetooth zařízením a znovu jej připojte.\n\nPokud kroky výše nepomohou, kontaktujte prosím výrobce zařízení:\n\n* Android: [Podpora výrobců zařízení](https://support.google.com/android/answer/3094742?hl=cs&ref_topic=7313011)"},{"question":"Proč aplikace používá zrovna Bluetooth? Neexistují lepší řešení jako GPS nebo triangulace od operátorů?","answer":"Soukromí uživatelů je pro nás na prvním místě, a právě technologie Bluetooth nabízí nejvyváženější poměr přesnosti záznamu a minimálního zásahu do soukromí. Technologie GPS ani triangulace vysílačů od operátorů nemohou poskytnout potřebné údaje s požadovanou přesností. Mimo jiné i proto, že jejich funkčnost je ve vnitřních prostorách či metru velmi omezená. Aplikace eRouška potřebuje pro svoji funkci informace o tom, že došlo k setkání, na jakou vzdálenost a po jakou dobu. Není důležité, kde k setkání došlo."},{"question":"Je Bluetooth dostatečně bezpečná technologie?","answer":"Bluetooth je dnes běžně rozšířená a podporovaná komunikační technologie pro propojení zařízení s dalšími periferiemi, jako jsou bezdrátová sluchátka, reproduktory, chytré náramky nebo hodinky. Vzhledem k tomu, že společnosti Apple a Google vytvořily společný protokol určený výhradně pro národní aplikace, které trasují riziková setkání s nakaženými COVID-19 na základě Bluetooth LE, jsme přesvědčení, že jsme pro eRoušku zvolili správně. Je však potřeba říct, že úplně každá technologie je zranitelná. I proto všem uživatelům doporučujeme, aby operační systém a aplikace ve svém telefonu pravidelně aktualizovali. Aktualizace totiž mohou obsahovat důležité bezpečnostní záplaty."}]},{"title":"Obecné informace","subtitle":"eRouška, karanténa a postupy hygieny","icon":"https://erouska.cz/img/faq/general.png","questions":[{"question":"Jak mám postupovat, když mi aplikace zobrazila upozornění na rizikové setkání s nakaženým uživatelem?","answer":"Pokud máte příznaky nákazy (například zvýšenou teplotu, kašel, dušnost, bolest v krku, bolest hlavy, náhlou ztrátu čichu a chuti), telefonicky kontaktujte svého praktického lékaře. V případě, že je váš stav opravdu akutní, volejte rychlou záchrannou službu 155, případě linku integrovaného záchranného systému 112. Uvedené příznaky se mohou objevit 2 až 14 dní po rizikovém setkání.\n\nPokud nemáte žádné příznaky, chovejte se prosím zodpovědně:\n\n* Noste roušku či respirátor přes ústa a nos.\n* Často a důkladně si myjte ruce vodou a mýdlem či používejte dezinfekci.\n* Pravidelně dezinfikujte vlastní předměty (např. mobilní telefon).\n* Kašlejte a kýchejte do kapesníku či rukávu.\n* Používejte jednorázové kapesníky a poté je vyhoďte.\n* Vyhýbejte se zbytečnému shromažďování a dodržujte bezpečný odstup od ostatních (přibližně 2 metry).\n\nPokud po notifikaci eRoušky máte vážné podezření na rizikové setkání, kontaktujte svého praktického lékaře a řiďte se jeho pokyny. Je také možné, že vás bude v blízké době kontaktovat pracovník hygienické služby na základě epidemiologického šetření s konkrétním nakaženým. V takovém případě postupujte podle pokynů hygienické služby.\n\nPokud by se v průběhu 14 dní od rizikového setkání váš zdravotní stav zhoršil a objevily se u vás příznaky onemocnění COVID-19, kontaktujte svého praktického lékaře."},{"question":"Jak mám postupovat, když mám pozitivní výsledek testu na COVID-19 nebo mi přišla SMS eRoušky s ověřovacím kódem?","answer":"Jestliže vám přišla SMS z laboratoře s pozitivním výsledkem testu na COVID-19, měla by vám do několika hodin přijít také SMS eRoušky s ověřovacím kódem pro odeslání dat. SMS eRoušky se odesílají automatizovaně po zapsání výsledku testů do informačního systému. Okamžik, kdy laboratoř předá výsledky do informačního systému, nedokážou automatizované procesy Chytré karantény ovlivnit.\n\nJakmile vám přijde SMS eRoušky s ověřovacím kódem, [postupujte prosím podle tohoto návodu](https://erouska.cz/sms). Uvedený postup vám umožní anonymně informovat ostatní uživatele o případném rizikovém setkání. Děkujeme.\n\nMáte pozitivní test a nepřišla vám SMS eRoušky? Napište si o nový kód na [info@erouska.cz](mailto:info@erouska.cz?subject=)."},{"question":"To budu muset pokaždé automaticky do karantény nebo na testy, když aplikace zahlásí, že jsem byl v kontaktu s někým nakaženým?","answer":"Ne, aplikace eRouška je plně anonymní, její použití je zcela dobrovolné a upozornění na možný rizikový kontakt s nakaženým není důvod pro nařízení karantény. Pokud u někoho test potvrdí nákazu, kontaktuje ho hygienická stanice, aby zjistila, s kým byl v posledních dnech v rizikovém kontaktu, a mohla tak omezit další šíření. Když tento člověk používá aplikaci eRouška, může kontaktovat i ty, které nezná, protože s nimi třeba cestoval v autobuse. Zadáním unikátního kódu může poslat upozornění všem ostatním uživatelům aplikace eRouška, se kterým byl v epidemiologicky rizikovém kontaktu.\n\nV okamžiku, kdy vás aplikace upozorní, že jste se setkali s osobou, u které bylo prokázané onemocnění COVID-19, byste měli postupovat podle zobrazených doporučení a v případě zdravotních potíží bez prodlení kontaktovat svého praktického lékaře."},{"question":"Pozná eRouška, když poruším lékařem nařízenou karanténu nebo třeba pojedu na chatu?","answer":"Aplikace nesleduje vaši polohu, není to její účel a ani pro to není navržena. Nepozná tedy, zda jste porušili karanténní opatření nebo kam jste odjeli. Má naopak pomoci k tomu, aby v karanténě byly pouze osoby s diagnostikovaným onemocněním nebo s vážným podezřením nákazy."},{"question":"Může mi eRouška místo lékaře vystavit neschopenku? Je upozornění na možné rizikové setkání důvod pro pracovní neschopnost?","answer":"Nemůže, není. Pokud vám eRouška zobrazila varování, postupujte podle zobrazených doporučení. Upozornění na možný rizikový kontakt nijak nenahrazuje zprávu od vašeho lékaře nebo laboratoře, která vyhodnotí váš test na COVID-19. Pouze lékař vám může vystavit platné dokumenty, které předložíte svému zaměstnavateli."},{"question":"Proč je eRouška potřeba? Dokáže mě vůbec efektivně chránit před nákazou?","answer":"Jedním z úkolů eRoušky je zastavit další šíření nákazy tím, že pomáhá co nejdříve izolovat potenciálně nakažené osoby od ostatních. To také pomáhá předcházet nutnosti zavádět plošná opatření, která mají negativní dopad na společnost a ekonomiku ČR. Aplikace na podobném principu fungují i v dalších státech a jsou účinným prostředkem v boji proti šíření nemoci COVID-19.\n\nAplikace eRouška 2.0 je doplňkovou aplikací Chytré karantény – systému ministerstva zdravotnictví. Prostřednictvím technologie Bluetooth se eRouška spojuje s dalšími eRouškami v nejbližším okolí a ukládá jejich anonymní identifikátory. Pokud test u člověka prokáže nákazu COVID-19, přijde mu automaticky ověřovací SMS kód, který mu umožní z aplikace odeslat své anonymní identifikátory do ostatních eRoušek. Aplikace ostatních uživatelů pak podle epidemiologického modelu\\* co nejpřesněji vyhodnotí, jestli tito uživatelé přišli s nakaženým do kontaktu dost blízko a na dostatečně dlouhou dobu, aby bylo nutné je prostřednictvím notifikace v mobilu varovat a směřovat k dalšímu vyšetření. eRouška 2.0 tedy pomáhá co nejefektivněji informovat další potenciální přenašeče viru a urychlit jejich testování. Tím se sníží počet infikovaných ve společnosti i riziko další infekce.\n\n_\\* Blíže než 2 metry po dobu delší než 7 minut._"},{"question":"Co když někdo nepřizná, že je nemocný?","answer":"I to se může stát. Věříme však, že uživatelé eRoušky chtějí aktivně pomáhat se zastavením dalšího šíření nákazy a aplikaci využijí, aby mohli co nejdříve varovat osoby, se kterými přišli do rizikového kontaktu. **Záměrné šíření COVID-19 je v ČR považováno za trestný čin.**"},{"question":"Má smysl si eRoušku instalovat, když ji nebude používat dost lidí?","answer":"Právě proto byste neměli váhat. Čím více nás bude eRoušku používat, tím lepší vytvoříme síť, která nás bude vzájemně chránit a varovat před rizikem. Pomozte nám i vy. Instalací aplikace se můžete aktivně zapojit do boje proti COVID-19. O aplikaci můžete říct i lidem ve svém okolí, případně jim s jejich souhlasem pomoci s instalací."},{"question":"Jak se eRouška liší od jiných podobných aplikací?","answer":"eRouška používá technologii Bluetooth, kterou je vybavená většina chytrých mobilních zařízení v Česku. Díky tomu funguje i v budovách, podzemních parkovištích nebo v metru. Aplikace je navržená tak, aby nesbírala informace o vaší poloze (např. přes GPS). Pouze anonymně zjišťuje, se kterými dalšími uživateli aplikace jste přišli do bližšího kontaktu. Jako jediná v ČR může oficiálně používat Apple/Google protokol, který garantuje bezpečnost aplikace na platformách Android a iOS a snižuje spotřebu baterie."},{"question":"Co je to chytrá karanténa? Jakou v ní hraje roli eRouška?","answer":"Jde o soubor chytrých opatření, která chrání lidi před nákazou a ekonomiku před kolapsem. Místo toho, aby byla v karanténě celá země, chytrá karanténa usnadňuje dohledávání, testování a izolaci pouze nemocných a z nemoci důvodně podezřelých lidí.\n\nKonkrétně jde o systém řízení v oblastech předpovědi vývoje, podpory rozhodování, aktivního vyhledávání kontaktů, testování vč. řízení kapacit odběrových míst, laboratoří, řízení karantén a souvisejícího materiálního a IT zabezpečení ke zvládnutí pandemie onemocnění COVID-19. Chytrá karanténa 2.0 je označením pro přechod projektu pod Ministerstvo zdravotnictví, které bude díky novým nástrojům umět do budoucna kdykoliv velmi rychle reagovat na jakékoliv epidemie.\n\nAplikace eRouška je jeden z nástrojů, které vznikly na podporu chytré karantény. Pomocí technologií – eRoušky – můžeme aktivně upozornit rizikové jedince, kteří se setkali s nakaženým, aby zvýšili hygienická opatření, zvážili dobrovolnou karanténu, případně při pociťování příznaků kontaktovali obvodního lékaře. Díky těmto krokům věříme, že se podaří zmírnit šíření nákazy a ušetřit zdravotnictví fatálního přetížení."}]},{"title":"Pokročilé informace o eRoušce","subtitle":"Sběr a vyhodnocení dat, význam upozornění","icon":"https://erouska.cz/img/faq/advanced.png","questions":[{"question":"Jak eRouška zaznamenává a zpracovává data o setkáních uživatelů?","answer":"Chytrý telefon s aplikací eRouška zaznamenává přes Bluetooth LE anonymní identifikátory (ID) z jiných zařízení s touto aplikací. Informaci o „setkání“ a jeho délce ukládá do své vnitřní paměti.\n\nV případě, že budete mít pozitivní výsledek testu na COVID-19, přijde vám jednorázový SMS kód. Zadáte jej do aplikace ([postup zde](https://erouska.cz/sms)), a tím umožníte odeslání svých anonymních identifikátorů na server, odkud si je stáhnou ostatní eRoušky k vyhodnocení. Algoritmus v aplikaci každého uživatele na základě epidemiologického modelu automatizovaně vyhodnotí sesbíraná data – porovná identifikátory se všemi zaznamenanými setkáními. V případě rizikového kontaktu zobrazí upozornění, že hrozí nákaza kvůli setkání s pozitivně testovanou osobou, a doporučí další postup.\n\nPodrobné informace o sběru a zpracování dat naleznete v [Informacích o zpracování osobních údajů v aplikaci eRouška 2.0](https://erouska.cz/podminky-pouzivani)."},{"question":"Jak eRouška vyhodnocuje rizikové setkání?","answer":"Původně epidemiologové stanovili jako rizikový kontakt jako setkání, které je ve vzdálenosti bližší než 2 metry po dobu alespoň 15 minut. Agresivnější šíření nových mutací koronaviru však ukazuje, že k nákaze stačí i kratší kontakt.\n\nNa vyšší agresivitu šíření mutací reagujeme po konzultacích postupu s Apple/Google a ostatními zeměmi EU postupným uvolňováním parametrů vyhodnocení rizikového kontaktu. Změny parametrů probíhají už od začátku března, průběžně vyhodnocujeme jejich vliv na počet nalezených kontaktů. Prozatím upravujeme zejména parametry odpovídající délce setkání, aktuálně jde přibližně o 7 minut.\n\nAplikace eRouška se snaží parametry vzdálenosti a času setkání vyhodnocovat co nejpřesněji dostupnými technologiemi. Vzdálenost mezi uživateli, respektive jejich telefony, se odhaduje na základě síly signálu Bluetooth. Doba setkání se posuzuje podle měřicích oken – telefon v několikaminutových intervalech zjišťuje, zda jsou v okolí jiné telefony s eRouškou.\n\nPřesnost měření výše uvedených parametrů má proto technická omezení. Čím větší vzdálenost a čím kratší kontakt, tím obtížnější je jeho přesné vyhodnocení. Uvolňování parametrů vyhodnocení tak zvyšuje pravděpodobnost falešných pozitivit a falešných negativit.\n\nVyšší přesnosti měření lze dosáhnout častějším vysíláním a zaznamenáváním dalších zařízení s eRouškou v okolí. To se bohužel promítne do vyšší spotřeby baterie. Algoritmy měření nastavují Apple a Google ve svém Exposure Notification protokolu, na kterém je eRouška postavená. Vývojáři eRoušky mohou pouze částečně upravovat některé parametry a provádět doplňující filtrování výsledků. Bližší vysvětlení najdete v sekci [Způsob vyhodnocení](https://erouska.cz/vyhodnoceni-rizika)."},{"question":"Jak eRouška reaguje na nové varianty/mutace viru?","answer":"Nové varianty virů, které se objevily od počátku pandemie, průběžně zkoumají odborníci z pohledu infekčnosti a dopadů na zdraví. Proto jsou vývojáři eRoušky v úzkém kontaktu s úřady i se společnostmi Apple a Google, které poskytují protokol pro vyhodnocování kontaktů s nákazou, aby v případě změny rizika infekce na základě nových poznatků mohli upravit algoritmus pro výpočet v aplikaci."},{"question":"Nemůže se stát, že eRouška mylně vyhodnotí kontakt s jinou osobou, která je například vedle v autě na křižovatce, za dveřmi nebo tenkou zdí?","answer":"Aplikace eRouška funguje na principu měření síly signálu mezi dvěma zařízeními. Technicky proto nelze vyloučit nepřesný odhad vzdálenosti a mylnou detekci rizikového setkání. Technická omezení, rizika používání aplikace a postup vyhodnocování setkání popisujeme na stránce [Spolehlivost vyhodnocení rizikového kontaktu](https://erouska.cz/vyhodnoceni-rizika) a v sekci [Technické podmínky v Podmínkách zpracování](https://erouska.cz/podminky-pouzivani#technicke).\n\nPro vyhodnocení kontaktu jako rizikového je kromě vzdálenosti (do 2 m) relevantní též délka kontaktu (v řádu minut) – např. zastavení na semaforech na 1–2 minuty je tedy nevýznamné."},{"question":"Zobrazilo se mi upozornění na možné setkání s nakaženým, ale eRouška po otevření ukazuje, že nezaznamenala nikoho nakaženého v mém okolí.","answer":"Pokud vás překvapilo oznámení s červenou ikonkou viru a textem Oznámení o možném kontaktu (Android) nebo Zaznamenání kontaktů (iOS), nemusíte se obávat. Jde pouze o systémové oznámení Androidu a iOS, na které nemá eRouška vliv. Jak vypadá skutečné oznámení eRoušky najdete [na tomto obrázku](https://www.facebook.com/eRouska/posts/180938253548928).\n\nPokud byste měli rizikový kontakt s nakaženou osobou, zjistíte to přímo na hlavní obrazovce eRoušky.\n\nPokud jste předtím skutečně viděli rizikové setkání přímo v eRoušce, odkud zmizelo, je možné, že aktualizací aplikace pro Android došlo k úpravě parametrů vyhodnocení rizikového kontaktu a nyní už eRouška nepovažuje dané setkání za rizikové."},{"question":"Takže všichni budou vědět, že mám COVID-19?","answer":"Nebudou. Pokud onemocníte, vaše totožnost je známá jen a pouze pracovníkovi hygienické stanice. Osoby, kterým se zobrazí upozornění na možný rizikový kontakt, se nedozví, kdo ani kde je mohl nakazit. Aplikace zobrazuje pouze obecnou informaci o rizikovém setkání s nakaženým, den setkání a další doporučený postup. Vaše identita je ochráněna."},{"question":"Byl jsem na testech na COVID-19, ale stále mi nepřišla SMS s ověřovacím kódem do eRoušky. Za jak dlouho ji mám čekat?","answer":"Nejprve by vám měl přijít výsledek testu v SMS od testovací laboratoře. Ta výsledky současně odesílá do centrálního informačního systému Ministerstva zdravotnictví. Poté, co se informace z informačního systému propíšou do systému hygieny, se odesílají automatické SMS eRoušky s ověřovacími kódy. Pokud laboratoř nestihne předat výsledky do informačního systému ve stejný den, může vám SMS s ověřovacím kódem přijít až následující den.\n\n**Zkontrolujte prosím, jestli info SMS eRoušky nezablokoval omylem systém jako spam**\n\n* **Android:** V aplikaci Zprávy klepněte na ikonu možností (tři tečky) a dále na Spam a zablokované konverzace (případně na tři tečky → Nastavení → SIM → Spam). Pokud je zde SMS eRoušky, klepněte na Není spam.\n\n**Pokud vám SMS eRoušky nepřišla ani následující den** poté, co vám přišla SMS s pozitivními výsledky z laboratoře, pošlete nám na [info@erouska.cz](mailto:info@erouska.cz?subject=) žádost o nový SMS kód. Uveďte prosím tyto informace:\n\n* celé jméno, které jste uvedli na žádance o test,\n* tel. číslo, které jste uvedli na žádance o test,\n* datum výsledku testů,\n* odběrové místo / laboratoř, ze které vám přišly výsledky testu,\n* typ testu (PCR/antigen)."},{"question":"Přijde mi SMS s kódem do eRoušky, když mám pozitivní antigenní test na COVID-19?","answer":"Pokud máte [pozitivní výsledek antigenního testu a máte příznaky COVID-19](https://drive.google.com/file/d/1HPwtltNRuzmowjZgE_c1u-a8m3W89T-p/view) (uvádíte v žádance), přijde vám SMS s kódem do eRoušky stejně jako v případě pozitivního PCR testu.\n\nV případě, že máte [pozitivní výsledek antigenního testu, ale nepociťujete příznaky](https://covid.gov.cz/situace/antigenni-testovani/interpretace-vysledku-antigenniho-testu-jak-se-zachovat-po-obdrzeni), půjdete na PCR test. Na základě pozitivního výsledku PCR testu by vám přišla SMS s kódem do eRoušky. V případě negativního PCR testu neodesíláte data z eRoušky."},{"question":"Ve kterých státech EU můžu eRoušku používat? Jak funguje spolupráce se zahraničními aplikacemi?","answer":"Evropská komise vyvinula centrální bránu EFGS pro bezpečnou výměnu informací („brána pro zajištění interoperability“). Tato brána umožňuje aplikaci eRouška přijímat a předávat si varování se zapojenými trasovacími aplikacemi používanými v jednotlivých státech EU. Podrobné informace o bráně jsou k dispozici na webu Evropské komise v dokumentu [Koronavirus: brána EU k zajištění interoperability aplikací pro vysledování kontaktů a varování – otázky a odpovědi](https://ec.europa.eu/commission/presscorner/detail/cs/qanda_20_1905#gateway).\n\nChcete-li zobrazit další informace o zapojených státech a nastavení, klepněte na hlavní obrazovce aplikace na Cesty do zahraničí, kde můžete funkci zapnout/vypnout a níže vidět seznam zapojených států. Oficiální aktuální seznam zapojených států EU najdete [na webu Evropské komise.](https://ec.europa.eu/health/sites/health/files/ehealth/docs/gateway_jointcontrollers_en.pdf)"},{"question":"Používá eRouška Apple/Google API?","answer":"Nová verze – eRouška 2.0 pro Android i iOS již používá Apple/Google Exposure Notification API. Nezapomeňte proto prosím aplikaci eRouška aktualizovat v [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska) nebo [App Store (iOS)](https://apps.apple.com/cz/app/erou%C5%A1ka/id1509210215) a znovu aktivovat."},{"question":"Co je to Apple/Google Exposure Notification API (protokol)?","answer":"Společnosti Apple a Google společně vyvíjejí technologie, díky kterým mohou veřejné zdravotní instituce (u nás MZČR) vyvíjet národní aplikace pro trasování rizikových kontaktů s osobami s onemocněním COVID-19.\n\nKonkrétně jde například o rozhraní, které daným aplikacím (u nás eRouška) umožňuje spolehlivě přistupovat k systémovým prostředkům Android a Apple zařízení za účelem zaznamenání setkání – zejména využívat Bluetooth LE pro ověření kontaktu a běžet na pozadí bez potřeby dalších nastavení aplikace nebo operačního systému. Šifrovaný komunikační protokol pak zajišťuje zabezpečený přenos aktuálních seznamů identifikátorů nakažených uživatelů mezi trasovacími aplikacemi za účelem vyhodnocení rizikových setkání a včasného zobrazení notifikace.\n\nBližší informace naleznete přímo na webových stránkách společností [Apple (anglicky)](https://www.apple.com/covid19/contacttracing) a [Google (česky)](https://www.google.com/covid19/exposurenotifications/)."},{"question":"Kde v eRoušce najdu informace o zaznamenaných setkáních?","answer":"Aplikace eRouška 2.0 je postavená na Apple/Google Exposure Notification protokolu, který zajišťuje zabezpečené zaznamenávání a vyhodnocení setkání. Z bezpečnostních důvodů však neposkytuje aplikaci přímý přístup k datům o zaznamenaných setkáních.\n\nZda aplikace funguje správně, se dozvíte na její hlavní obrazovce. Informace o všech zaznamenaných setkáních s nakaženými uživateli jsou uložená na úrovni systému. Tato setkání však nemusela být riziková. Naleznete je v Nastavení:\n\n* Android: Nastavení → Google → Oznámení o možném kontaktu COVID-19 → klepnout na tři tečky vpravo nahoře → Kontrola kontaktu"},{"question":"Co znamenají jednotlivé záznamy v Nastavení → Kontrola kontaktů?","answer":"Protokolování kontaktů s nákazou ukazuje, kolik klíčů od nakažených uživatelů se stáhlo do vašeho telefonu ze serveru a kolik z nich odpovídá klíčům, které váš telefon zaznamenal.\n\n**Kontroly kontaktů:** Zde jsou data a časy jednotlivých stažení klíčů ze serveru za poslední dva týdny. Pro další kontrolu je potřeba otevřít jednotlivé záznamy.\n\n**Android: Počet klíčů:** Jde o počet klíčů od pozitivně testovaných uživatelů. Tyto klíče si váš v telefon v daném datu stáhl pro kontrolu možných kontaktů s nakaženými uživateli.\n\n**Android: Počet výsledků:** Jde o počet shod mezi staženými klíči a těmi, které zaznamenal váš telefon ve svém okolí. Pokud je číslo vyšší než 0, tak jste se setkali s nakaženým uživatelem. To však ještě neznamená, že nutně muselo jít o rizikové setkání.\n\n**Časová značka:** Datum a čas, kdy ke kontrole došlo. Nejde však o čas rizikového setkání (ten není k dispozici kvůli ochraně soukromí uživatelů).\n\n**Zdroj dat:** Aplikace, která zajišťuje kontrolu a vyhodnocení rizikových setkání.\n\n**Hašovací kód:** Kontrolní součet souboru obsahujícího stažení klíče nakažených uživatelů. Dokud se obsah souboru nezmění, tak se nezmění ani kontrolní součet během příslušného dvoutýdenního období."},{"question":"Jak zjistím, že eRouška funguje na pozadí? Proč na Androidu nevidím ikonku eRoušky v záhlaví?","answer":"Notifikační ikonu jsme odstranili, protože zaznamenávání a vyhodnocování setkání již zajišťuje přímo Apple/Google protokol na úrovni operačního systému. Samotná aplikace eRouška se pak stará o stahování klíčů (identifikátorů) nakažených uživatelů 1–2× denně a nemusí běžet permanentně na pozadí.\n\nŽe je aplikace aktuální, zjistíte na její hlavní obrazovce. Stahování klíčů nakažených pak můžete ověřit v Nastavení telefonu:\n\n* **Android:** Nastavení → Google → Oznámení o možném kontaktu COVID-19 → klepnout na tři tečky vpravo nahoře → Kontrola kontaktu"},{"question":"Proč se liší datum a čas „Poslední aktualizace dat“ na hlavní obrazovce eRoušky a data a časy „Kontroly kontaktů“, které vidím v Nastavení telefonu?","answer":"Aplikace eRouška ukazuje na hlavní obrazovce datum a čas posledního úspěšného pokusu o kontrolu nových klíčů (identifikátorů) nakažených uživatelů na serveru. V Nastavení telefonu se zobrazují pouze data a časy, kdy byly naposledy nové klíče nalezeny a vyhodnoceny v telefonu.\n\nKlíče nakažených uživatelů se do telefonu stahují 1–2× denně."},{"question":"Proč v eRoušce nevidím konkrétní čas rizikového setkání?","answer":"Aplikace eRouška je postavená na Apple/Google Exposure Notification protokolu, který zajišťuje zaznamenávání a vyhodnocování setkání na úrovni operačního systému. Apple/Google protokol poskytuje eRoušce přístup pouze k datu rizikového setkání, bližší informace nejsou k dispozici z důvodu co nejvyšší ochrany soukromí uživatelů."},{"question":"Proč v Kontrole kontaktů v Nastavení telefonu vidím „Nalezené klíče: 0“?","answer":"Znamená to, že Apple/Google API ve vašem telefonu nevyhodnotilo v daném datu žádné setkání s nakaženým uživatelem. Jinak řečeno stažené klíče nakažených uživatelů se v daném dni neshodují s žádným ze zaznamenaných klíčů ve vašem zařízení."},{"question":"Proč v Nastavení telefonu v sekci Kontrola kontaktu vidím u konkrétního data nalezené klíče, ale eRouška mi nezobrazuje žádná riziková setkání?","answer":"Počet nalezených klíčů představuje počet zaznamenaných setkání s nakaženými uživateli. Neznamená to však nutně, že tato setkání byla riziková. Pokud setkání s nakaženým uživatelem nebylo rizikové, tak na něj eRouška neupozorňuje. Proto v aplikaci není vidět."},{"question":"Zobrazilo se mi upozornění/notifikace na rizikové setkání, ke kterému došlo před několika dny. Proč mě eRouška upozornila až teď?","answer":"Aplikace eRouška nemůže vyhodnotit riziková setkání a zobrazit upozornění dříve, než uživatel s pozitivním testem na COVID-19 odešle data ze své eRoušky. Pokud se vám dnes zobrazila notifikace, která upozorňuje na rizikové setkání s nakaženým před několika dny, znamená to, že nakažený odeslal data ze své eRoušky až dnes, nejpozději včera. Vaše eRouška si je dnes stáhla a vyhodnotila."},{"question":"Zobrazilo se mi upozornění/notifikace na rizikové setkání s nakaženým, ale na obrazovce Rizikové setkání vidím více záznamů s různými daty.","answer":"Aplikace eRouška vyhodnocuje riziková setkání až poté, co uživatel s pozitivním testem na COVID-19 odešle data ze své eRoušky. Pokud se vám zobrazila pouze jedna notifikace na rizikový kontakt, ale v Rizikových setkáních máte více záznamů, znamená to, že jste se s nakaženým setkali v předchozích dnech vícekrát nebo že v jeden den odeslalo data ze svého telefonu více nakažených uživatelů současně."},{"question":"Bude eRouška fungovat jako „covid pas“ pro zobrazování výsledků testů a prokázání očkování?","answer":"Anonymní charakter aplikace eRouška ji neumožňuje kombinovat s tzv. digitálním zeleným certifikátem (Digital Green Certificate), který pracuje s ověřenou identitou a osobními údaji uživatele – výsledky testů nebo provedeným očkováním proti COVID-19."},{"question":"Publikuje eRouška nějaké statistiky o používání aplikace?","answer":"Statistiky publikujeme ve formátu JSON na adrese [stats.erouska.cz](https://stats.erouska.cz). Data mají následující strukturu:\n\n{\\\\n \"data\": {\\\\n \"modified\": 1608696001,\\\\n \"date\": \"20201223\",\\\\n \"activations\\_yesterday\": 2042,\\\\n \"activations\\_total\": 1475571,\\\\n \"key\\_publishers\\_yesterday\": 718,\\\\n \"key\\_publishers\\_total\": 39175,\\\\n \"notifications\\_yesterday\": 631,\\\\n \"notifications\\_total\": 197298\\\\n }\\\\n}\n\n* **modified** = Unix timestamp vygenerovaných statistik,\n* **date** = datum vytvoření reportu,\n* **activations\\_yesterday** = počet aktivací za předchozí kalendářní den,\n* **activations\\_total** = celkový počet aktivací eRoušky,\n* **key\\_publishers\\_yesterday** = počet nakažených lidí, kteří zadali v předchozím dni do eRoušky kód pro odeslání dat,\n* **key\\_publishers\\_total** = celkový počet nakažených lidí, kteří zadali do eRoušky kód pro odeslání dat,\n* **notifications\\_yesterday** = počet lidí, kterým se za předchozí den zobrazilo upozornění na rizikové setkání,\n* **notifications\\_total** = celkový počet lidí, kterým se zobrazilo upozornění na rizikové setkání.\n\nZavolání bez parametru zobrazí informace k aktuálnímu datu. Zavolání s parameterem `date` umožní získat data pro libovolný den od 23. 10. 2020 (za všechny předchozí dny máme pouze agregovaná data). Pomocí parametru `date` s hodnotou `all` lze získat všechna data, která rozhraní poskytuje.\n\n* dnešní data: [`https://stats.erouska.cz/`](https://stats.erouska.cz/)\n* data z 1. 12. 2020: [`https://stats.erouska.cz/?date=2020-12-01`](https://stats.erouska.cz/?date=2020-12-01)\n* data za všechny dny: [`https://stats.erouska.cz/?date=all`](https://stats.erouska.cz/?date=all)"}]},{"title":"Zabezpečení dat","subtitle":"Osobní údaje a ochrana soukromí","icon":"https://erouska.cz/img/faq/security.png","questions":[{"question":"Jak zjistím, která všechna data přesně aplikace ukládá a odesílá?","answer":"Aplikace eRouška používá od verze 2.0 tzv. Apple/Google Exposure Notification API, které zprostředkovává sběr dat a předávání identifikátorů mezi uživateli. Aplikace kvůli tomu už nemá přímý přístup k naměřeným údajům, které by mohla uživatelům zobrazit, jako tomu bylo u verze 1.0.\n\nAplikace denně stahuje seznam identifikátorů od uživatelů, kterým test potvrdil nákazu. Data s identifikátory ke zpracování může ostatním odeslat pouze samotný uživatel, a to až po zadání jednorázového SMS kódu, který získá v případě pozitivního testu na nákazu nemocí COVID-19 automaticky, případně na vyžádání.\n\nAplikace dále odesílá na server anonymní statistickou/agregovanou informaci, že došlo k notifikaci rizikového kontaktu. Tato informace není vázána na žádný identifikátor uživatele ani jeho mobilu a slouží pouze k výpočtu statistických informací o účinnosti systému eRouška.\n\nAbychom zajistili funkčnost eRoušky, pracujeme také s údaji o jejím fungování (např. záznamy o pádech aplikace a používání aplikace) na vašem telefonu a používáme k tomu standardní nástroje (Firebase Crashlytics a Google Analytics) od společnosti Google. Telemetrická data odesílaná aplikací do těchto služeb neobsahují identifikátory vaší osoby nebo vašeho telefonu.\n\nPodrobné informace o sběru a zpracování dat naleznete v [Informacích o zpracování osobních údajů v aplikaci eRouška 2.0](https://erouska.cz/podminky-pouzivani)."},{"question":"Přenesou se data z eRoušky po obnově telefonu ze zálohy nebo při přechodu na nový telefon?","answer":"Z důvodů ochrany soukromí uživatelů operační systémy Android ani iOS nezálohují data z Exposure Notifications. Při reinstalaci telefonu nebo přechodu na jiné zařízení tedy nelze přenést ani obnovit zaznamenané a vygenerované identifikátory.\n\nPokud chcete získat informaci o možném rizikovém setkání, doporučujeme sledovat stav starého telefonu ještě po dobu 14 dní po instalaci eRoušky na nové zařízení. Přitom je potřeba:\n\n* zajistit, aby vaše zařízení bylo zapnuté a připojené k internetu, aby si mohlo stahovat data k vyhodnocení,\n* ideálně alespoň jednou denně kontrolovat starší zařízení, tedy otevřít eRoušku.\n* Pokud byste během těchto 14 dní měli pozitivní test, zadejte prosím kód do nového zařízení a pro získání kódu pro starší mobil kontaktujte [info@erouska.cz](mailto:info@erouska.cz).\n\nPo 14 dnech už váš starý telefon nebude obsahovat žádná data pro vyhodnocení rizikového setkání. Můžete z něj proto odstranit eRoušku a ponechat jen v novém zařízení."},{"question":"Co když mi telefon ukradnou nebo ho ztratím?","answer":"V aplikaci eRouška jsou lokálně uložené jen anonymní informace o dalších zařízeních, která eRouška zaznamenala ve svém okolí. Žádné riziko to nepřináší pro vás ani pro majitele takto zaznamenaných zařízení. Jiné aplikace, které běžně využíváte, mohou ve vašem telefonu pravděpodobně ukládat mnohem citlivější údaje. Mějte proto svůj telefon chráněný kódem nebo biometricky (otiskem prstu nebo obličejem)."},{"question":"Je aplikace eRouška v souladu s GDPR?","answer":"Celý systém aplikace eRouška, včetně webových stránek, je navržen plně v souladu s GDPR i zákonem o zpracování osobních údajů.\n\nZde naleznete bližší informace:\n\n* [Informace o zpracování osobních údajů v aplikaci eRouška 2.0](https://erouska.cz/podminky-pouzivani)\n* [Audit zdrojového kódu aplikace](https://erouska.cz/audit-kod)"},{"question":"Jak uživatele chráníte před zneužitím dat?","answer":"Uživatele chráníme především minimálním rozsahem zaznamenávaných dat a jejich ukládáním přímo do zařízení. Data uživatelů se bez jejich vědomí a souhlasu nikam neposílají ani nezpracovávají. Do telefonů se zaznamenávají anonymní identifikátory (ID) zařízení s nainstalovanou aplikací eRouška a informace o čase, délce a síle jejich Bluetooth signálu. Data se zpracovávají automatizovaně na serveru pouze po odsouhlaseném odeslání uživatelem."},{"question":"Dohlíží na zabezpečení dat z aplikace nějaká nezávislá organizace?","answer":"Celý systém aplikace eRouška, včetně podpůrných webových stránek, je navržený plně v souladu s GDPR. Kód aplikace je jako open-source volně k dispozici ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)) a prošel [auditem nezávislých vzdělávacích institucí](https://erouska.cz/audit-kod).\n\nDalší informace naleznete v [Informacích o zpracování osobních údajů v rámci aplikace eRouška 2.0](https://erouska.cz/podminky-pouzivani)."},{"question":"Může mě někdo pomocí aplikace eRouška sledovat?","answer":"Ne. Aplikace nesbírá údaje o vaší poloze a k uloženým datům máte přístup pouze vy. Samotné údaje, které aplikace po vašem souhlasu odesílá, nemohou být použity k vašemu sledování. Každá eRouška navíc mění z důvodu bezpečnosti a ochrany soukromí uživatelů pravidelně své vysílané ID, aby nebylo snadné jej odchytit a nějak zneužít."},{"question":"Lze ověřit, že aplikace nezaznamenává moji polohu?","answer":"Ano. Aplikace eRouška je publikována s otevřeným zdrojovým kódem ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)), takže znalý člověk může snadno ověřit, že údaje o poloze aplikace opravdu nesbírá.\n\nZdrojové kódy aplikace eRouška [prověřují nezávislé autority](https://erouska.cz/audit-kod), které kontrolují, že aplikace:\n\n* nesleduje polohu\n* sama automaticky data s identifikátory nikam neodesílá"},{"question":"Lze identifikátor aplikace (ID) uživatele spárovat s konkrétní osobou?","answer":"Aplikace eRouška 2.0 [nepracuje s osobními údaji](https://erouska.cz/podminky-pouzivani). Vygenerovaná anonymní ID eRoušky se mezi aplikacemi a servery přenášejí výhradně po chráněném komunikačním protokolu, který společně vyvinuly společnosti Apple a Google. K identifikátorům nemají přístup ani uživatelé, ani vývojáři, ani jiná autorita."},{"question":"Kdo má (může mít) přístup k mým datům?","answer":"K zaznamenaným údajům o setkání nemá v eRoušce 2.0 přístup uživatel, ani nikdo jiný. Důvodem je, že aktuální verze aplikace používá Apple/Google protokol, který k zaznamenaným datům neposkytuje přístup ani uživatelům, ani vývojářům a správcům. Data (anonymní identifikátory nakažených) se přenášejí na server a odtud do ostatních eRoušek šifrovaná a vyhodnocují se až na koncových zařízeních automatizovaně za účelem zobrazení případných upozornění o rizikovém kontaktu. Ze serveru i ze zařízení se data mažou automaticky po 14 dnech."},{"question":"Jak dlouho do minulosti jsou data k dispozici? Kdy a jak probíhá jejich mazání?","answer":"V aplikaci eRouška se uchovávají data zaznamenaná za posledních 14 dní, starší záznamy se automaticky odmazávají. Pokud po pozitivním testu na COVID-19 zadáte ověřovací kód do eRoušky a odešlete identifikátory z telefonu na server, zůstanou zde 14 dní a poté dojde k jejich smazání. Data můžete ze svého zařízení smazat v Nastavení Oznámení o kontaktech s nákazou / Kontakty s nákazou / Exposure Notification – konkrétní postup záleží na typu a verzi operačního systému.\n\n* **Android:** Na zařízeních se systémem Android v současném nastavení dojde při odinstalování aplikace eRouška ke smazání zaznamenaných klíčů (identifikátorů).\n\nInformace o správcích a zpracovatelích dat naleznete v [Informacích o zpracování osobních údajů v rámci aplikace eRouška](https://erouska.cz/podminky-pouzivani)."},{"question":"Můžu si používání aplikace kdykoli rozmyslet? Můžu smazat data, která odešlu ke zpracování?","answer":"Aplikaci můžete kdykoliv odinstalovat. Vygenerované a zaznamenané klíče (záznamy setkání) můžete ze svého zařízení smazat v Nastavení Oznámení o kontaktech s nákazou / Kontakty s nákazou / Exposure Notification – konkrétní postup záleží na typu a verzi operačního systému. Data odeslaná na server se smažou automaticky po 14 dnech.\n\n* **Android:** Na zařízeních se systémem Android v současném nastavení dojde při odinstalování aplikace eRouška také ke smazání zaznamenaných klíčů (identifikátorů)."},{"question":"Co se děje s daty po smazání aplikace z mobilu?","answer":"**Android:** Když smažete aplikaci z telefonu nebo tabletu s OS Android, dojde také ke smazání zaznamenaných klíčů (identifikátorů).\n\nPokud aplikaci znovu nainstalujete, začnou se sbírat a do telefonu ukládat zcela nová data."},{"question":"Kde najdu zdrojový kód aplikace?","answer":"Aplikace eRouška je publikována s otevřeným zdrojovým kódem. Najdete jej na GitHubu ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios))."}]},{"title":"Instalace a kompatibilita","subtitle":"Podporované mobily a možnosti instalace","icon":"https://erouska.cz/img/faq/compatibility.png","questions":[{"question":"Na jakých telefonech aplikace eRouška funguje? Proč jsou nároky vyšší než u eRoušky 1.0?","answer":"Pro instalaci aplikace eRouška potřebujete mobilní zařízení s těmito parametry:\n\n* iPhone s iOS verze 13.5 a vyšší\n* OS Android verze 6.0 a vyšší s Google Play Services (nemá jen malé procento telefonů Huawei a některé telefony s vlastní ROM)\n* Bluetooth LE (Low Energy)\n\nPokud vaše mobilní zařízení není s eRouškou kompatibilní (nemá potřebné technologie/funkce), Google Play ani Apple App Store vám neumožní aplikaci nainstalovat. Aplikace je dostupná pouze pro některé tablety Android. Není k dispozici pro iPad.\n\nMinimální požadavky na kompatibilitu stanovují společnosti Apple a Google. Vycházejí z požadavků Apple/Google Exposure Notification protokolu, který nová eRouška používá. Na Apple/Google protokol jsme se rozhodli přejít kvůli zajištění fungování eRoušky na iPhonech a kvůli umožnění přeshraniční kompatibility s podobnými aplikacemi dalších evropských států."},{"question":"Odkud si můžu eRoušku bezpečně stáhnout a nainstalovat?","answer":"Aplikace eRouška je dostupná pouze pro:\n\n* Android: [v Obchodě Google Play](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska)."},{"question":"Je možné aplikaci stáhnout i jinak než z oficiálních aplikačních obchodů? Je k dispozici balíček apk?","answer":"Aplikace eRouška 2.0 používá Apple/Google protokol pro zaznamenávání setkání a přenos dat. Proto je také závislá na systémových službách, bez kterých nebude fungovat, a není ji tedy možné instalovat z jiných než oficiálních aplikačních obchodů. Aplikační obchody spolehlivě vyfiltrují podporovaná zařízení.\n\nVývojáři eRoušky nikdy neposkytují uživatelům odkazy na instalační balíčky APK nebo samotné instalační soubory mimo oficiální aplikační obchody. Pro aplikace nainstalované neoficiální cestou bychom nedokázali garantovat jejich zabezpečení, zajistit jejich aktualizace ani stejnou úroveň uživatelské podpory. Proto prosím nikdy neinstalujte eRoušku z .apk souborů, které jste našli někde na internetu nebo vám je někdo poslal e-mailem. Mohou obsahovat viry, trojské koně nebo jiný závadný obsah."},{"question":"Můžu používat eRoušku spolu s jinou oficiální trasovací aplikací jiného státu (např. Corona-Warn-App)?","answer":"Pokud pobýváte primárně na území ČR, doporučujeme používat eRoušku i pro cesty do zahraničí. [Aplikace nyní spolupracuje i s dalšími státy EU](https://erouska.cz/caste-dotazy#efgs).\n\nV ostatních případech můžete mít v telefonu nainstalováno více trasovacích aplikací typu eRoušky, jako jsou Corona-Warn-App, Stopp Corona App, Imunni, Stop Covid ProteGO Safe a další. Pokud však také používají Apple/Google Exposure Notification protokol (platí zejména pro sousední státy ČR), můžete mít v jeden okamžik aktivní pouze jednu z těchto aplikací. Ostatní je potřeba pozastavit. K zaznamenávání setkání v Apple/Google protokolu totiž může mít přístup vždy pouze jedna aplikace."},{"question":"Funguje eRouška, když mám na mobilu úsporný režim / režim nízké spotřeby / spořič baterie?","answer":"**Android:** Spořiče/šetřiče baterie na zařízeních s OS Android mohou do značné míry omezovat běh aplikací na pozadí. Ačkoliv systém oznámení o možném kontaktu s onemocněním COVID-19 nadále funguje, nemusí docházet k vyhodnocování rizikových kontaktů, a tudíž k případnému zobrazení notifikací."},{"question":"Bude možné používat eRoušku i na chytrých hodinkách či náramcích?","answer":"Rozšíření je teoreticky do budoucna možné. Zatím jej ale nezvažujeme zejména proto, že by chytré hodinky či náramek musely podporovat Bluetooth LE (Low Energy) a zejména Apple/Google protokol. Základní Bluetooth bez LE sám o sobě pouze zajišťuje přenos dat, ale neumožňuje měření intenzity setkání uživatelů na základě síly signálu."}]}]v2_appleIgnoreAndroidtruev2_howItWorksUITitleJak eRouška fungujev2_validationTokenExpirationLeewayMinutes15v2_efgsDays14v2_keyExportNonTravellerUrls[{"country":"CZ","url":"https://cdn.erouska.cz/erouska/index.txt"}]v2_keyExportEuTravellerUrls[{"country":"CZ","url":"https://cdn.erouska.cz/erouska/index.txt"},{"country":"IT","url":"https://cdn.erouska.cz/efgs/it/index.txt"},{"country":"LV","url":"https://cdn.erouska.cz/efgs/lv/index.txt"}]v2_howItWorksEvalContenteRoušky riziková setkání vyhodnotí v případě, že byly s nakaženým v kontaktu na vzdálenost bližší než 2 metry a po dobu alespoň 7 minut od okamžiku, kdy byl podle dostupných informací nakažlivým.v2_ragnarokHeadlinePomoc eRoušky v boji s pandemií je u koncev2_ragnarokMoreInfohttps://erouska.czv2_ragnarokBodyDěkujeme, že jste se stali součástí sítě 1,7 miliónu lidí. Pomohli jste ochránit své okolí rozesláním více než 400 tisíc upozornění na riziková setkání s nakaženými. Oznámení o rizikovém kontaktu vypneme a váš telefon již nebude hledat rizikové kontakty. eRouška je neaktivní a v případě jejího obnovení vám pošle oznámení.v2_ragnaroktrue
================================================
FILE: app/src/main/res/xml-sk/remote_config_defaults.xml
================================================
v2_reportTypeWeights1;1;1;1;1;1v2_infectiousnessWeights0;0.6;2.0v2_attenuationBucketThresholdDb55;70;80v2_attenuationBucketWeights2.0;1;0.25;0v2_minimumWindowScore420v2_covidDataServerUrlhttps://europe-west1-erouska-key-server-dev.cloudfunctions.netv2_minGmsVersionCode203019000v2_keyImportDataOutdatedHours99999999v2_riskyEncountersTitleAnNaposledy %1$s ste sa stretli s osobou, u ktorej bolo potvrdené ochorenie COVID-19.v2_keyImportPeriodHours6v2_keyExportUrlhttps://cdn.erouska.cz/v2_recentExposuresUITitlePredchádzajúce rizikové stretnutiav2_spreadPreventionUITitleZásady zodpovedného správaniav2_symptomsUITitleHlavné príznakyv2_exposureUITitleRizikové stretnutiev2_chatBotLinkhttps://erouska.cz/caste-dotazyv2_noEncounterBodyNa rizikové stretnutie vás upozorníme pomocou oznámenia.v2_noEncounterHeaderV posledných 14 dňoch nebola vo vašej blízkosti žiadna osoba s potvrdeným ochorením COVID-19v2_conditionsOfUseUrlhttps://erouska.cz/podminky-pouzivaniv2_riskyEncountersWithoutSymptomsAk sa u vás neprejavujú hlavné príznaky ochorenia COVID-19, nemusíte zostávať doma. Dodržiavajte zásady zodpovedného správania a sledujte svoj zdravotný stav.v2_riskyEncountersWithSymptomsAk sa u vás v priebehu 14 dní od kontaktu s nakazeným objavili príznaky respiračného ochorenia, kontaktujte telefonicky svojho praktického lekára a riaďte sa jeho pokynmi. V prípade závažných zdravotných ťažkostí volajte 155.v2_riskyEncountersTitleDňa %@ ste sa stretli s osobou, u ktorej bolo potvrdené ochorenie COVID-19.v2_contactsContentJson[{"title":"Máte podozrenie na ochorenie COVID-19?","text":"Zostaňte doma a telefonicky kontaktuje svojho praktického lekára.","linkTitle":"Dôležité kontakty","link":"https://koronavirus.mzcr.cz/dulezite-kontakty-odkazy/"},{"title":"Potrebujete poradiť?","text":"Odpovede na všetky otázky a nejasnosti týkajúce sa ochorenia COVID-19 nájdete na webe MZČR.","linkTitle":"Často kladené otázky","link":"https://koronavirus.mzcr.cz/"},{"title":"Podpora aplikácie","text":"Potrebujete poradiť s aplikáciou? Napíšte nám e-mail.","linkTitle":"Napísať e-mail","link":"mailto:info@erouska.cz"},{"title":"O aplikácii eRouška","text":"Zaujíma vás, ako aplikácia funguje? Pozrite sa na web erouska.cz.","linkTitle":"Prejsť na web erouska.cz","link":"https://erouska.cz"}]v2_preventionContentJson{"title":"COVID-19 sa prenáša hlavne kvapôčkami, ktoré sa šíria vzduchom. Prenášať sa môže tiež kontaminovanými predmetmi.","items":[{"iconUrl":"https://erouska.cz/img/prevention/ic_clean_hands.png","label":"Často a dôkladne si umývajte ruky mydlom a používajte dezinfekciu."},{"iconUrl":"https://erouska.cz/img/prevention/ic_disinfenction.png","label":"Pravidelne dezinfikujte tiež osobné predmety (napr. mobilný telefón)."},{"iconUrl":"https://erouska.cz/img/prevention/ic_tissue.png","label":"Kašľajte a kýchajte do jednorázovej vreckovky či rukáva."},{"iconUrl":"https://erouska.cz/img/prevention/ic_social_distance.png","label":"Vyhýbajte sa veľkým skupinám osôb a udržujte bezpečný odstup (cca 2 metre)."},{"iconUrl":"https://erouska.cz/img/prevention/ic_mask.png","label":"Do obchodov a hromadných dopravných prostriedkov vždy noste ochranu dýchacích ciest podľa aktuálnych opatrení."},{"iconUrl":"https://erouska.cz/img/prevention/ic_stay_home.png","label":"Ak cítite príznaky respiračného ochorenia, zostaňte doma a kontaktujte lekára."}]}v2_symptomsContentJson{"title":"Príznaky sa môžu objaviť 2-14 dní po rizikovom stretnutí.","items":[{"iconUrl":"https://erouska.cz/img/symptoms/ic_temperature.png","label":"Zvýšená teplota"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_cough.png","label":"Kašeľ"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_stuffiness.png","label":"Dušnosť"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_throatache.png","label":"Bolesť v krku"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_headache.png","label":"Bolesť hlavy, svalov a kĺbov"},{"iconUrl":"https://erouska.cz/img/symptoms/ic_smelltaste.png","label":"Strata čuchu a chuti"}]}v2_encounterWarningStretli ste sa s osobou, u ktorej bolo potvrdené ochorenie COVID-19. Sledujte svoj zdravotný stav.v2_verificationServerApiKey4AP6RHk5VlNi7WIhj4ZupI9JZODdUyrPGb1C2mDYKo4cuaGIHdYtOhjyoqhg4vB5r7FDijxnySLb_1CUH6XdDA.1.oZLaUuVOPbvEaiWnzkQxasWlD_71iSTeM9aAIOKrfqhg5QO68t004CKxWutYmfISc7vqJe6uY2dRrt34OFkliwv2_helpMarkdown# Najčastejšie problémy\n## Android: Základné odporúčania pri riešení problémov s aplikáciou\nAk vám aplikácia zobrazuje chybovú hlášku, skontrolujte prosím najskôr, či máte nainštalované aktualizácie operačného systému ( [postup aktualizácie Androidu](https://support.google.com/android/answer/7680439) ) a nepoužívate ich beta verziu.\n\nĎalej prosím skontrolujte v obchode [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska), či nie je pre eRoušku dostupná nová aktualizácia.\n\nOdporúčame tiež povoliť automatické aktualizácie aplikácie eRouška. [Postup pre Android tu:](https://support.google.com/googleplay/answer/113412#autoone) Spustite aplikáciu Obchod Google Play, kliknite na ponuku Moje aplikácie a hry → Vyberte aplikáciu, ktorú chcete aktualizovať → kliknite na ikonu možností a kliknite na Aktivovať automatické aktualizácie.\n\n## Android: Nejde aktivovať eRouška. Chyba aktivácie 17 (Exposure Notification API nie je k dispozícii).\nPre vyriešenie chyby na Android zariadeniach, skúste, prosím, postupovať takto:\n\n1. Skontrolujte, či ste _hlavný užívateľ_ zariadenia, či máte v telefóne služby Play a či je k dispozícii _Nastavenia → Google → Oznámenie o možnom kontakte (COVID-19 Exposure Notifications)_ .\n2. Odinštalujte eRoušku.\n3. Vymažte vyrovnávaciu pamäť a dáta u Google Play Services a Obchodu Play ([postup tu, viď kroky 2 a 3](https://support.google.com/googleplay/answer/9037938) ).\n4. Nainštalujte eRoušku a zapnite opäť _Oznámenia o možnom kontakte COVID-19_ .\n\nAk sa vám aj napriek aktualizácii systému, aplikácie a uvedených krokov naďalej objavuje chyba aktivácie, je bohužiaľ možné, že vaše zariadenie ešte nepodporuje Bluetooth LE (Low Energy Advertising), nepodporuje Bluetooth Multiple Advertisement, nie je autorizované Googlom pre Exposure Notifications alebo vôbec nemá Exposure Notifications API.\n\n# Pripojenie a prenos dát\n## Musím mať Bluetooth zapnutý neustále?\nÁno. Bez zapnutého Bluetooth nie je možné zisťovať blízkosť ďalších zariadení s nainštalovanou aplikáciou eRouška. Túto funkciu máte zapnutú možno aj teraz, napríklad v prípade, že používate bezdrôtové slúchadlá, pripojenie v aute, smart náramok alebo hodinky. V nastavení Bluetooth vášho telefónu / tabletu navyše odporúčame povoliť, aby bol _viditeľný pre všetky zariadenia_ .\n\n## Potrebuje aplikácia pripojenie k internetu?\nPripojenie k internetu je nutné pre stiahnutie (inštaláciu) a aktiváciu eRoušky. Ďalej je potrebné, aby si eRouška mohla každý deň sťahovať aktuálny zoznam anonymných identifikátorov nakazených užívateľov, a zobraziť tak včas prípadnú notifikáciu o rizikovom stretnutí.\n\nAk by u vás test potvrdil nákazu COVID-19 a vy by ste chceli anonymne upozorniť ostatných na možný rizikový kontakt, bolo by opäť potreba eRoušku pripojiť k internetu. Dovtedy sú vaše dáta uložené iba vo vašom telefóne, odkiaľ sa samy odmazávajú po 14 dňoch. Aplikácia využíva pripojenie k internetu aj na stiahnutie informácií o aktuálnom vývoji epidémie COVID-19 v ČR, ktoré zobrazuje na stránke _Aktuálne_ .\n\n## Ako často si aplikácia aktualizuje dáta z internetu?\nKľúče (identifikátory) nakazených užívateľov si eRouška sťahuje niekoľkokrát denne. Štatistiky nakazených osôb na obrazovke Aktuálne sa aktualizujú raz denne.\n\n## Ako veľký objem mobilných dát eRouška denne spotrebuje?\nV prípade zapnutého _Upozornenia na zahraničné riziková stretnutí_ eRouška sťahuje tiež kľúče všetkých nakazených užívateľov z ostatných štátov EÚ. Objem stiahnutých dát sa v takom prípade môže pohybovať až okolo 2 MB denne.\n\nAj v prípade vypnutého _Upozornenia na zahraničné rizikové stretnutia_ eRouška sťahuje približne 1 MB denne. Nová verzia aplikácie sťahuje nielen kľúče nakazených užívateľov eRoušky, ale aj kľúče nakazených užívateľov z ostatných štátov EÚ, ktorí vo svojich aplikáciách uviedli, že cestujú do zahraničia, takže ich užívateľ eRoušky potenciálne mohol stretnúť aj v Českej republike\n\nSúbory kľúčov nakazených užívateľov sa sťahujú postupne, 2-3× denne. Pre porovnanie, načítanie priemernej stránky spravodajstva vyžaduje 1-5 MB dát. **Aplikácia nepotrebuje nutne mobilné dátové pripojenie.** Dáta si stiahne sama aj na Wi-Fi.\n\n## Android: Prečo je potrebné mať povolený prístup k GPS/lokalizačným/lokačným údajom?\n**Aktualizácia pre užívateľov Android 11:** V najnovšej verzii operačného systému Android už nemusíte pre fungovanie eRoušky povoľovať služby určovania polohy.\n\nAplikácia eRouška nezbiera a neukladá dáta z GPS, avšak operačný systém **Android 10 a staršie** zahŕňa pod určovanie polohy aj niektoré služby Bluetooth LE (LE = low energy), ktorý komponenty eRoušky - konkrétne Google Play Services - pre svoje fungovanie vyžadujú. Preto je nutný súhlas užívateľov s prístupom aplikácie k lokalizačným údajom. V iOS tento súhlas potrebný nie je. Prečo v telefóne musí byť zapnuté určovanie polohy, [vysvetľuje Google tu](https://support.google.com/android/answer/9930236) .\n\nInformáciu o vašej polohe môžu využívať aj iné aplikácie vo vašom zariadení. Ak chcete skontrolovať, ktoré aplikácie majú k nim prístup, prejdite do nastavení zariadenia a vyberte položky Zabezpečenie a poloha → Umiestnenie → Oprávnenie na úrovni aplikácie (prípadne nasledujte [postup od spoločnosti Google](https://support.google.com/accounts/answer/6179507?hl=cs)). Tu môžete iným aplikáciám odoprieť oprávnenie používať vašu polohu, keď si nemyslíte, že to potrebujú. Pretože eRouška toto oprávnenie nepotrebuje, nezobrazí sa v zozname.\n\n## Koľko zapnutý Bluetooth a aplikácia eRouška spotrebujú batérie?\nAk už teraz používate telefón so stále zapnutým Bluetooth, spotreba energie by sa nemala výraznejšie zvýšiť. Ak Bluetooth teraz bežne nepoužívate, tak podľa výsledkov nášho testovania s neustále zapnutým Bluetooth a ukladaním dát do aplikácie bolo navýšenie dennej spotreby energie v ráde jednotiek percent. Vo väčšine prípadov to je menej ako 20% kapacity batérie za deň. Záleží pritom na konkrétnom smartfóne, jeho veku, štýlu jeho používania a stavu batérie.\n\nSpotrebu batérie spojenú s použitím aplikácie eRouška ovplyvňujú dva faktory. Po prvé samotný beh aplikácie a po druhé využitie systémovej funkcie Oznámenie o kontaktoch s nákazou, ktorá priebežne zaznamenáva stretnutia s používateľmi v okolí. Systémovú funkciu zabezpečuje OS Android alebo iOS, preto jej optimalizácia záleží na aktualizáciách zo strany spoločností Apple a Google.\n\nPri interpretácii spotreby batérie zo štatistík v nastaveniach telefónu berte prosím do úvahy, že toto percento zodpovedá plnému využitiu vášho telefónu za posledných 24 hodín (percentuálny podiel behu medzi ostatnými aplikáciami). To znamená, že u málo využívaného telefónu môže byť percento pri Kontaktoch s nákazou / aplikácii eRouška vysoké, u intenzívne využívaných telefónov naopak nízke. Nejde teda priamo o percento spotreby batérie.\n\n**Android: Poznámka k povoleniu lokalizačných služieb:** Bluetooth zariadenia vo vašej blízkosti sa dá detekovať, len ak je v telefóne aktivovaná možnosť "Použiť polohu". (Podrobnosti nájdete v odseku [Prečo v telefóne musí byť zapnuté určovanie polohy? Na webe Googlu](https://support.google.com/android/answer/9930236?hl=cs) .) To znamená, že informáciu o vašej polohe môžu využívať aj iné aplikácie vo vašom zariadení, čo môže byť dôvodom vyššej spotreby batérie. Preto, prosím, skontrolujte, ktoré aplikácie používajú vašu polohu. Prejdite do nastavení [zariadenia a vyberte položky Zabezpečenie a poloha → Umiestnenie → Oprávnenie na úrovni aplikácie (prípadne nasledujte](https://support.google.com/android/answer/9930236?hl=cs) [postup od spoločnosti Google](https://support.google.com/accounts/answer/6179507?hl=cs) ). Tu môžete iným aplikáciám odoprieť oprávnenie používať vašu polohu, keď si nemyslíte, že to potrebujú. Pretože eRouška toto oprávnenie nepotrebuje, nezobrazí sa v zozname.\n\n## Je možné, že sa mi po zapnutí eRoušky spomalilo Wi-Fi / pripojenie k internetu?\nPri niektorých modeloch mobilov s OS Android je bohužiaľ známe, že vysielanie Bluetooth môže čiastočne negatívne ovplyvňovať signál Wi-Fi a naopak.\n\n## Môže eRouška spôsobovať problémy s ďalšími aplikáciami alebo zariadeniami / perifériami (slúchadlá, hodinky, náramok), ktoré používajú Bluetooth?\nVo výnimočných prípadoch nám niektorí užívatelia hlásia neštandardné správanie ich Bluetooth periférií po tom, čo nainštalovali aplikáciu eRouška. Napr. môže dochádzať k prerušovaniu spojenia - typicky u smart hodiniek či náramkov, bezdrôtových slúchadiel alebo handsfree sád. S najväčšou pravdepodobnosťou ide o problém funkcií operačného systému, na ktoré bohužiaľ nemáme vplyv.\n\nSkúste prosím nsledujúci postup:\n\n1. Reštartujte telefón.\n2. Zrušte spárovanie s Bluetooth zariadením a znovu ho pripojte.\n\nAk kroky vyššie nepomôžu, kontaktujte prosím výrobcu zariadenia:\n\n* Android: [Podpora výrobcov zariadení](https://support.google.com/android/answer/3094742?hl=cs&ref_topic=7313011)\n\n## Prečo aplikácia používa zrovna Bluetooth? Neexistujú lepšie riešenia ako GPS alebo triangulácia od operátorov?\nSúkromie užívateľov je pre nás na prvom mieste, a práve technológia Bluetooth ponúka najvyváženejší pomer presnosti záznamu a minimálneho zásahu do súkromia. Technológia GPS ani triangulácie vysielačov od operátorov nemôžu poskytnúť potrebné údaje s požadovanou presnosťou. Okrem iného aj preto, že ich funkčnosť je vo vnútorných priestoroch či metra veľmi obmedzená. Aplikácia eRouška potrebuje pre svoju funkciu informácie o tom, že došlo k stretnutiu, na akú vzdialenosť a po akú dobu. Nie je dôležité, kde k stretnutiu došlo.\n\n## Je Bluetooth dostatočne bezpečná technológia?\nBluetooth je dnes bežne rozšírená a podporovaná komunikačná technológia na prepojenie zariadení s ďalšími perifériami, ako sú bezdrôtové slúchadlá, reproduktory, smart náramky alebo hodinky. Vzhľadom k tomu, že spoločnosti Apple a Google vytvorili spoločný protokol určený výhradne pre národné aplikácie, ktoré trasujú rizikové stretnutia s nakazenými COVID-19 na základe Bluetooth LE, sme presvedčení, že sme pre eRoušku zvolili správne. Je však potrebné povedať, že úplne každá technológia je zraniteľná. Aj preto všetkým užívateľom odporúčame, aby operačný systém a aplikácie vo svojom telefóne pravidelne aktualizovali. Aktualizácie totiž môžu obsahovať dôležité bezpečnostné záplaty.\n\n# Všeobecné informácie\n## Ako mám postupovať, ak mi aplikácia zobrazila upozornenie na rizikové stretnute s nakazeným užívateľom?\nAk máte príznaky choroby (napríklad zvýšenú teplotu, kašeľ, dýchavičnosť, bolesť hrdla, bolesť hlavy, náhlu stratu čuchu a chuti), telefonicky kontaktujte svojho praktického lekára. V prípade, že je váš stav naozaj akútny, volajte rýchlu záchrannú službu 155, prípadne linku integrovaného záchranného systému 112. Uvedené príznaky sa môžu objaviť 2 až 14 dní po rizikovom stretnutí.\n\nAk nemáte žiadne príznaky, správajte sa, prosím, zodpovedne:\n\n* Noste rúško či respirátor cez ústa a nos.\n* Často a dôkladne si umývajte ruky vodou a mydlom alebo používajte dezinfekciu.\n* Pravidelne dezinfikujte vlastné predmety (napr. mobilný telefón).\n* Kašlite a kýchajte do vreckovky či rukávu.\n* Používajte jednorázové vreckovky a potom ich vyhoďte.\n* Vyhýbajte sa zbytočnému zhromažďovaniu a dodržujte bezpečný odstup od ostatných (približne 2 metre).\n\nPokiaľ po notifikácii eRoušky máte vážne podozrenie na rizikové stretnutie, kontaktujte svojho praktického lekára a riaďte sa jeho pokynmi. Je tiež možné, že vás bude v blízkej dobe kontaktovať pracovník hygienickej služby na základe epidemiologického vyšetrenia s konkrétnym nakazeným. V takom prípade postupujte podľa pokynov hygienickej služby.\n\nAk by sa v priebehu 14 dní od rizikového stretnutia váš zdravotný stav zhoršil a objavili sa u vás príznaky ochorenia COVID-19, kontaktujte svojho praktického lekára.\n\n## Ako mám postupovať, keď mám pozitívny výsledok testu na COVID-19 alebo mi prišla SMS eRoušky s overovacím kódom?\nAk vám prišla SMS z laboratória s pozitívnym výsledkom testu na COVID-19, mala by vám do niekoľkých hodín prísť tiež SMS eRoušky s overovacím kódom pre odoslanie dát. SMS eRoušky sa odosielajú automatizovane po zapísaní výsledku testov do informačného systému. Okamih, kedy laboratórium odovzdá výsledky do informačného systému, nedokážu automatizované procesy Chytrej karantény ovplyvniť.\n\nAkonáhle vám príde SMS eRoušky s overovacím kódom, [postupujte, prosím, podľa tohto návodu](https://erouska.cz/sk/sms) . Uvedený postup vám umožní anonymne informovať ostatných užívateľov o prípadnom rizikovom stretnutí. Ďakujeme.\n\nMáte pozitívny test a neprišla vám SMS eRoušky? Napíšte si o nový kód na [info@erouska.cz](mailto:info@erouska.cz?subject=).\n\n## To budem musieť zakaždým automaticky do karantény alebo na testy, keď aplikácia zahlási, že som bol v kontakte s niekým nakazeným?\nNie, aplikácia eRouška je plne anonymná, jej použitie je úplne dobrovoľné a upozornenie na možný rizikový kontakt s nakazeným nie je dôvod pre nariadenie karantény. Ak u niekoho test potvrdí nákazu, kontaktuje ho hygienická stanica, aby zistila, s kým bol v posledných dňoch v rizikovom kontakte, a mohla tak obmedziť ďalšie šírenie. Keď tento človek používa aplikáciu eRouška, môže kontaktovať aj tých, ktorých nepozná, pretože s nimi napríklad cestoval v autobuse. Zadaním unikátneho kódu môže poslať upozornenie všetkým ostatným používateľom aplikácie eRouška, s ktorým bol v epidemiologicky rizikovom kontakte.\n\nV okamihu, keď vás aplikácia upozorní, že ste sa stretli s osobou, u ktorej bolo preukázané ochorenie COVID-19, by ste mali postupovať podľa zobrazených odporúčaní a v prípade zdravotných ťažkostí bez odkladu kontaktovať svojho praktického lekára.\n\n## Spozná eRouška, keď poruším lekárom nariadenú karanténu alebo keď napríklad pôjdem na chatu?\nAplikácia nesleduje vašu polohu, nie je to jej účel a ani pre to nie je navrhnutá. Nespozná teda, či ste porušili karanténne opatrenie alebo kam ste odišli. Má naopak pomôcť k tomu, aby v karanténe boli iba osoby s diagnostikovaným ochorením alebo s vážnym podozrením nákazy.\n\n## Môže mi eRouška miesto lekára vystaviť práceneschopnosť? Je upozornenie na možné rizikové stretnutie dôvod pre pracovnú neschopnosť?\nNemôže, nie je. Ak vám eRouška zobrazila varovanie, postupujte podľa zobrazených odporúčaní. Upozornenie na možný rizikový kontakt nijako nenahrádza správu od vášho lekára alebo laboratória, ktorá vyhodnotí váš test na COVID-19. Iba lekár vám vystaví platné dokumenty, ktoré predložíte svojmu zamestnávateľovi.\n\n## Prečo je eRouška potrebná? Dokáže ma vôbec efektívne chrániť pred nákazou?\nJednou z úloh eRoušky je zastaviť ďalšie šírenie nákazy tým, že pomáha čo najskôr izolovať potenciálne nakazené osoby od ostatných. Vďaka tomu potom nie je nutné zavádzať plošnú karanténu a obmedzenia, čo umožňuje znižovať dopady pandémie na spoločnosť a ekonomiku ČR. Aplikácie na podobnom princípe fungujú aj v ďalších štátoch a sú účinným prostriedkom v boji proti šíreniu choroby COVID-19.\n\nAplikácia eRouška 2.0 je doplnkovou aplikáciou Chytrej karantény - systému ministerstva zdravotníctva. Prostredníctvom technológie Bluetooth sa eRouška spája s ďalšími eRouškami v najbližšom okolí a ukladá ich anonymné identifikátory. Ak sa u človeka preukáže nákaza COVID-19, spojí sa s ním pracovník hygienickej stanice a overí, či osoba používa aplikáciu eRouška. Ak áno, pošle jej overovací SMS kód, ktorý užívateľovi umožní odoslať svoje anonymné identifikátory do ostatných eRoušek. Aplikácia ostatných užívateľov potom podľa epidemiologického modelu \* čo najpresnejšie vyhodnotí, či títo používatelia prišli s nakazeným do kontaktu dosť blízko a na dostatočne dlhú dobu, aby bolo nutné ich prostredníctvom notifikácie v smartfóne varovať a smerovať k ďalšiemu vyšetreniu. eRouška 2.0 teda pomáha čo najefektívnejšie informovať ďalších potenciálnych prenášačov vírusu a urýchliť ich testovanie. Tým sa zníži počet infikovaných v spoločnosti aj riziko vašej infekcie.\n\n_\*Bližšie ako 2 metre po dobu dlhšiu ako 7 min._\n\n## Čo ak niekto neprizná, že je chorý?\nAj to sa môže stať. Veríme však, že používatelia eRoušky chcú aktívne pomáhať so zastavením ďalšieho šírenia nákazy a aplikáciu využijú, aby mohli čo najskôr varovať osoby, s ktorými prišli do rizikového kontaktu. **Zámerné šírenie COVID-19 je v ČR považované za trestný čin.**\n\n## Má zmysel si eRoušku inštalovať, keď ju nebude používať dosť ľudí?\nPráve preto by ste nemali váhať. Čím viac nás bude eRoušku používať, tým lepšie vytvoríme sieť, ktorá nás bude vzájomne chrániť a varovať pred rizikom. Pomôžte nám aj vy. Inštaláciou aplikácie sa môžete aktívne zapojiť do boja proti COVID-19. O aplikácii môžete povedať aj ľuďom vo svojom okolí, prípadne im s ich súhlasom pomôcť s inštaláciou.\n\n## Ako sa eRouška líši od iných podobných aplikácií?\neRouška používa technológiu Bluetooth, ktorou je vybavená väčšina chytrých mobilných zariadení v Česku. Vďaka tomu funguje aj v budovách, podzemných parkoviskách alebo v metre. Aplikácia je navrhnutá tak, aby nezbierala informácie o vašej polohe (napr. cez GPS). Iba anonymne zisťuje, s ktorými ďalšími používateľmi aplikácie ste prišli do bližšieho kontaktu. Ako jediná v ČR môže oficiálne používať Apple / Google protokol, ktorý garantuje bezpečnosť aplikácie na platformách Android a iOS a znižuje spotrebu batérie.\n\n## Čo je to chytrá karanténa? Akú v nej hrá úlohu eRouška?\nIde o súbor chytrých opatrení, ktoré chránia ľudí pred nákazou a ekonomiku pred kolapsom. Namiesto toho, aby bola v karanténe celá krajina, chytrá karanténa uľahčuje dohľadávanie, testovanie a izoláciu len chorých a z choroby dôvodne podozrivých ľudí.\n\nKonkrétne ide o systém riadenia v oblastiach predpovedi vývoja, podpory rozhodovania, aktívneho vyhľadávania kontaktov, testovania vr. riadenia kapacít odberových miest, laboratórií, riadenia karantén a súvisiaceho materiálneho a IT zabezpečenia na zvládnutie pandémie ochorenia COVID-19. Chytrá karanténa 2.0 je označením pre prechod projektu pod Ministerstvo zdravotníctva, ktoré bude vďaka novým nástrojom vedieť do budúcna kedykoľvek veľmi rýchlo reagovať na akékoľvek epidémie.\n\nAplikácia eRouška je jeden z nástrojov, ktoré vznikli na podporu chytrej karantény. Pomocou technológií - eRoušky - môžeme aktívne upozorniť rizikových jedincov, ktorí sa stretli s nakazeným, aby zvýšili hygienické opatrenia, zvážili dobrovoľnú karanténu, prípadne pri pociťovaní príznakov kontaktovali obvodného lekára. Vďaka týmto krokom veríme, že sa podarí zmierniť šírenie nákazy a ušetriť zdravotníctvo fatálneho preťaženia.\n\n# Pokročilé informácie o eRouške\n## Ako eRouška zaznamenáva a spracováva dáta o stretnutiach užívateľov?\nChytrý telefón s aplikáciou eRouška zaznamená cez Bluetooth LE anonymné identifikátory (ID) z iných zariadení s touto aplikáciou. Informáciu o "stretnutí" a jeho dĺžke ukladá do svojej vnútornej pamäte.\n\nV prípade, že budete mať pozitívny výsledok testu na COVID-19, príde vám jednorázový SMS kód. Zadáte ho do aplikácie ( [postup tu](https://erouska.cz/sk/sms) ), a tým umožníte odoslanie svojich anonymných identifikátorov na server, odkiaľ si ich stiahnu ostatné eRoušky na vyhodnotenie. Algoritmus v aplikácii každého užívateľa na základe epidemiologického modelu automatizovane vyhodnotí zozbierané dáta - porovná identifikátory so všetkými zaznamenanými stretnutiami. V prípade rizikového kontaktu zobrazí upozornenie, že hrozí nákaza kvôli stretnutiu s pozitívne testovanou osobou, a odporučí ďalší postup.\n\nPodrobné informácie o zbere a spracovaní dát, nájdete v [Informáciách o spracování osobných údajov v aplikácii eRouška 2.0](https://erouska.cz/sk/podminky-pouzivani).\n\n## Ako eRouška vyhodnocuje rizikové stretnutia?\nPôvodne epidemiológovia stanovili rizikový kontakt ako stretnutie, ktoré je vo vzdialenosti bližšie ako 2 metre po dobu aspoň 15 minút. Agresívnejšie šírenie nových mutácií koronavírusu však ukazuje, že k nákaze stačí aj kratší kontakt.\n\nNa vyššiu agresivitu šírenia mutácií reagujeme po konzultáciách postupu s Apple/Google a ostatnými krajinami EÚ postupným uvoľňovaním parametrov vyhodnotenia rizikového kontaktu. Zmeny parametrov prebiehajú už od začiatku marca, priebežne vyhodnocujeme ich vplyv na počet nájdených kontaktov. Zatiaľ upravujeme predovšetkým parametre zodpovedajúce dĺžke stretnutia, aktuálne ide približne o 7 minút.\n\nAplikácia eRouška sa snaží parametre vzdialenosti a času stretnutia vyhodnocovať čo najpresnejšie dostupnými technológiami. Vzdialenosť medzi používateľmi, respektíve ich telefónmi, sa odhaduje na základe sily signálu Bluetooth. Doba stretnutia sa posudzuje podľa meracích okien - telefón v niekoľkominútových intervaloch zisťuje, či sú v okolí iné telefóny s eRouškou.\n\nPresnosť merania vyššie uvedených parametrov má preto technické obmedzenia. Čím väčšia vzdialenosť a čím kratší kontakt, tým ťažšie je jeho presné vyhodnotenie. Uvoľňovanie parametrov vyhodnotenia tak zvyšuje pravdepodobnosť falošnej pozitivity a falošnej negativity.\n\nVyššiu presnosť merania možno dosiahnuť častejším vysielaním a zaznamenávaním ďalších zariadení s eRouškou v okolí. To sa bohužiaľ premietne do vyššej spotreby batérie. Algoritmy merania nastavujú Apple a Google vo svojom Exposure Notification protokole, na ktorom je eRouška postavená. Vývojári eRoušky môžu iba čiastočne upravovať niektoré parametre a vykonávať doplňujúce filtrovanie výsledkov. Bližšie vysvetlenie nájdete v sekcii [Spôsob vyhodnotenia](https://erouska.cz/sk/vyhodnoceni-rizika) .\n\n## Ako eRouška reaguje na nové varianty / mutácie vírusu?\nNové varianty vírusov, ktoré sa objavili od začiatku pandémie, priebežne skúmajú odborníci z pohľadu infekčnosti a vplyvov na zdravie. Preto sú vývojári eRoušky v úzkom kontakte s úradmi aj so spoločnosťami Apple a Google, ktoré poskytujú protokol pre vyhodnocovanie kontaktov s nákazou, aby v prípade zmeny rizika infekcie na základe nových poznatkov mohli upraviť algoritmus pre výpočet v aplikácii.\n\n## Nemôže sa stať, že eRouška mylne vyhodnotí kontakt s inou osobou, ktorá je napríklad vedľa v aute na križovatke, za dverami alebo tenkou stenou?\nAplikácia eRouška funguje na princípe merania sily signálu medzi dvoma zariadeniami. Technicky preto nemožno vylúčiť nepresný odhad vzdialenosti a mylnú detekciu rizikového stretnutia. Technické obmedzenia, riziká používania aplikácie a postup vyhodnocovania stretnutia popisujeme na stránke [Spoľahlivosť vyhodnotenia rizikového kontaktu](https://erouska.cz/sk/vyhodnoceni-rizika) a v sekcii [Technické podmienky v Podmienkach spracovania](https://erouska.cz/sk/podminky-pouzivani#technicke) .\n\nNa vyhodnotenie kontaktu ako rizikového je okrem vzdialenosti (do 2 m) relevantná tiež dĺžka kontaktu (aspoň 15 minút) - napr. zastavenie na semaforoch na 1-2 minúty je teda nevýznamné.\n\n## Zobrazilo sa mi upozornenie na možné stretnutie s nakazeným, ale eRouška po otvorení ukazuje, že nezaznamenala nikoho nakazeného v mojom okolí.\nAk vás prekvapilo oznámenie s červenou ikonkou vírusu a textom Oznámenie o možnom kontakte (Android) alebo Zaznamenanie kontaktov (iOS), nemusíte sa obávať. Ide len o systémové oznámenia Androidu a iOS, na ktoré nemá eRouška vplyv. Ako vyzerá skutočné oznámenie eRoušky nájdete [na tomto obrázku](https://www.facebook.com/eRouska/posts/180938253548928).\n\nAk by ste mali rizikový kontakt s nakazenou osobou, zistíte to priamo na hlavnej obrazovke eRoušky.\n\nAk ste predtým skutočne videli rizikové stretnutie priamo v eRouške, odkiaľ zmizlo, je možné, že aktualizáciou aplikácie pre Android došlo k úprave parametrov vyhodnotenia rizikového kontaktu a teraz už eRouška nepovažuje dané stretnutie za rizikové.\n\n## Takže všetci budú vedieť, že mám COVID-19?\nNebudú. Ak ochoriete, vaša totožnosť je známa len a len pracovníkovi hygienickej stanice. Osoby, ktorým sa zobrazí upozornenie na možný rizikový kontakt, sa nedozvadia, kto ani kde ich mohol nakaziť. Aplikácia zobrazuje len všeobecnú informáciu o rizikovom stretnutí s nakazeným, deň stretnutia a ďalší odporúčaný postup. Vaša identita je ochránená.\n\n## Bol som na testoch na COVID-19, ale stále mi neprišla SMS s overovacím kódom do eRoušky. Za ako dlho ju mám čakať?\nNajprv by vám mal prísť výsledok testu v SMS od testovacieho laboratória. To výsledky súčasne odosiela do centrálneho informačného systému Ministerstva zdravotníctva. Po tom, čo sa informácie z informačného systému prepíšu do systému hygieny, sa odosielajú automatické SMS eRoušky s overovacími kódmi. Ak laboratórium nestihne odovzdať výsledky do informačného systému v rovnaký deň, môže vám SMS s overovacím kódom prísť až nasledujúci deň.\n\n**Skontrolujte, prosím, či info SMS eRoušky nezablokoval omylom systém ako spam**\n\n* **Android:** V aplikácii Správy kliknite na ikonu možností (tri bodky) a ďalej na Spam a zablokované konverzácie (prípadne na tri bodky → Nastavenia → SIM → Spam). Ak je tu SMS eRoušky, kliknite na Nie je spam.\n\n**Ak vám SMS eRoušky neprišla ani nasledujúci deň** potom, čo vám prišla SMS s pozitívnymi výsledkami z laboratória, pošlite nám na [info@erouska.cz](mailto:info@erouska.cz?subject=) žiadosť o nový SMS kód. Uveďte prosím tieto informácie:\n\n* celé meno, ktoré ste uviedli na žiadanke o test,\n* tel. číslo, ktoré ste uviedli na žiadanke o test,\n* dátum výsledku testov,\n* odberové miesto / laboratórium, z ktorého vám prišli výsledky testu,\n* typ testu (PCR / antigén).\n\n## Príde mi SMS s kódom do eRoušky, keď mám pozitívny antigénny test na COVID-19?\nPokiaľ máte [pozitívny výsledok antigénneho testu a máte príznaky COVID-19](https://drive.google.com/file/d/1HPwtltNRuzmowjZgE_c1u-a8m3W89T-p/view) (uvádzate v žiadanke), príde vám SMS s kódom do eRoušky rovnako ako v prípade pozitívneho PCR testu.\n\nV prípade, že máte [pozitívny výsledok antigénneho testu, ale nepociťujete príznaky](https://covid.gov.cz/situace/antigenni-testovani/interpretace-vysledku-antigenniho-testu-jak-se-zachovat-po-obdrzeni) , pôjdete na PCR test. Na základe pozitívneho výsledku PCR testu by vám prišla SMS s kódom do eRoušky. V prípade negatívneho PCR testu neodosielate dáta z eRoušky.\n\n## V ktorých štátoch EÚ môžem eRoušku používať? Ako funguje spolupráca so zahraničnými aplikáciami?\nEurópska komisia vyvinula centrálnu bránu EFGS pre bezpečnú výmenu informácií ("brána pre zabezpečenie interoperability"). Táto brána umožňuje aplikácii eRouška prijímať a odovzdávať si varovania so zapojenými trasovacím aplikáciami používanými v jednotlivých štátoch EÚ. Podrobné informácie o bráne sú k dispozícii na webe Európskej komisie v dokumente [Koronavírus: brána EÚ na zabezpečenie interoperability aplikácií pre vysledovanie kontaktov a varovaní - otázky a odpovede](https://ec.europa.eu/commission/presscorner/detail/cs/qanda_20_1905#gateway) .\n\nAk chcete zobraziť ďalšie informácie o zapojených štátoch a nastaveniach, kliknite na hlavnej obrazovke aplikácie na Cesty do zahraničia, kde môžete funkciu zapnúť/vypnúť a nižšie vidieť zoznam zapojených štátov. Oficiálny aktuálny zoznam zapojených štátov EÚ nájdete [na webe Európskej komisie.](https://ec.europa.eu/health/sites/health/files/ehealth/docs/gateway_jointcontrollers_en.pdf)\n\n## Používa eRouška Apple/Google API?\nNová verzia - eRouška 2.0 pre Android aj iOS už používa Apple / Google Exposure Notification API. Nezabudnite preto prosím aplikáciu eRouška aktualizovať v [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska) alebo [App Store (iOS)](https://apps.apple.com/cz/app/erou%C5%A1ka/id1509210215) a znovu aktivovať.\n\n## Čo je to Apple/Google Exposure Notification API (protokol)?\nSpoločnosti Apple a Google spoločne vyvíjajú technológie, vďaka ktorým môžu verejné zdravotné inštitúcie (u nás MZČR) vyvíjať národné aplikácie na trasovanie rizikových kontaktov s osobami s ochorením COVID-19.\n\nKonkrétne ide napríklad o rozhranie, ktoré daným aplikáciám (u nás eRouška) umožňuje spoľahlivo pristupovať k systémovým prostriedkom Android a Apple zariadení za účelom zaznamenanie stretnutia - najmä využívať Bluetooth LE na overenie kontaktu a bežať na pozadí bez potreby ďalších nastavení aplikácie alebo operačného systému. Šifrovaný komunikačný protokol potom zaisťuje zabezpečený prenos aktuálnych zoznamov identifikátorov nakazených užívateľov medzi trasovacími aplikáciami za účelom vyhodnotenia rizikového stretnutia a včasného zobrazenie notifikácie.\n\nBližšie informácie nájdete priamo na webových stránkach spoločností [Apple (anglicky)](https://www.apple.com/covid19/contacttracing) a [Google (česky)](https://www.google.com/covid19/exposurenotifications/).\n\n## Kde v eRouške nájdem informácie o zaznamenaných stretnutiach?\nAplikácia eRouška 2.0 je postavená na Apple / Google Exposure Notification protokole, ktorý zaisťuje zabezpečené zaznamenávanie a vyhodnotenie stretnutí. Z bezpečnostných dôvodov však neposkytuje aplikácii priamy prístup k dátam o zaznamenaných stretnutiach.\n\nČi aplikácia funguje správne, sa dozviete na jej hlavnej obrazovke. Informácie o všetkých zaznamenaných stretnutiach s nakazenými užívateľmi sú uložené na úrovni systému. Tieto stretnutia však nemusela byť rizikové. Nájdete ich v Nastaveniach:\n\n* Android: Nastavenia → Google → Oznámenia o možnom kontakte COVID-19\n\n## Čo znamenajú jednotlivé záznamy v Nastavenia → Kontrola kontaktov?\nProtokolovanie kontaktov s nákazou ukazuje, koľko kľúčov od nakazených užívateľov sa stiahlo do vášho telefónu zo servera a koľko z nich zodpovedá kľúčom, ktoré váš telefón zaznamenal.\n\n**Kontroly kontaktov:** Tu sú dáta a časy jednotlivých stiahnutí kľúčov zo servera za posledné dva týždne. Pre ďalšiu kontrolu je potreba otvoriť jednotlivé záznamy.\n\n**Android: Počet kľúčov:** Ide o počet kľúčov od pozitívne testovaných používateľov. Tieto kľúče si váš telefón v danom dátume stiahol pre kontrolu možných kontaktov s nakazenými užívateľmi.\n\n**Android: Počet výsledkov** Ide o počet zhôd medzi stiahnutými kľúčmi a tými, ktoré zaznamenal váš telefón vo svojom okolí. Ak je číslo vyššie ako 0, tak ste sa stretli s nakazeným užívateľom. To však ešte neznamená, že nutne muselo ísť o rizikové stretnutie.\n\n**Časová značka:** Dátum a čas, kedy ku kontrole došlo. Nejde však o čas rizikového stretnutie (ten nie je k dispozícii kvôli ochrane súkromia užívateľov).\n\n**Zdroj údajov:** Aplikácia, ktorá zaisťuje kontrolu a vyhodnotenie rizikových stretnutí.\n\n**Hašovací kód:** Kontrolný súčet súboru obsahujúceho stiahnutie kľúčov nakazených užívateľov. Kým sa obsah súboru nezmení, tak sa nezmení ani kontrolný súčet počas príslušného dvojtýždňového obdobia.\n\n## Ako zistím, že eRouška funguje na pozadí? Prečo na Androide nevidím ikonku eRoušky v záhlaví?\nNotifikačnú ikonu sme odstránili, pretože zaznamenávanie a vyhodnocovanie stretnutia už zabezpečuje priamo Apple / Google protokol na úrovni operačného systému. Samotná aplikácia eRouška sa potom stará o sťahovanie kľúčov (identifikátory) nakazených užívateľov 1-2× denne a nemusí bežať permanentne na pozadí.\n\nŽe je aplikácia aktuálna, zistíte na hlavnej obrazovke. Stiahnutie kľúčov nakazených potom môžete overiť v Nastaveniach telefónu:\n\n* **Android:** Nastavenia → Google → Oznámenia o možnom kontakte COVID-19 → kliknúť na tri bodky vpravo hore → Kontrola kontaktu\n\n## Prečo sa líši dátum a čas "Poslednej aktualizácie dát" na hlavnej obrazovke eRoušky a údaje a časy "Kontroly kontaktov", ktoré vidím v Nastaveniach telefónu?\nAplikácia eRouška ukazuje na hlavnej obrazovke dátum a čas posledného úspešného pokusu o kontrolu nových kľúčov (identifikátory) nakazených používateľov na serveri. V Nastaveniach telefónu sa zobrazujú iba dátumy a časy, kedy boli naposledy nové kľúče nájdené a vyhodnotené v telefóne.\n\nKľúče nakazených užívateľov sa do telefónu sťahujú 1-2× denne.\n\n## Prečo v eRouške nevidím konkrétny čas rizikového stretnutia?\nAplikácia eRouška je postavená na Apple / Google Exposure Notification protokolu, ktorý zaisťuje zaznamenávanie a vyhodnocovanie stretnutia na úrovni operačného systému. Apple / Google protokol poskytuje eRouške prístup len k dátumu rizikového stretnutia, bližšie informácie nie sú k dispozícii z dôvodu čo najvyššej ochrany súkromia používateľov.\n\n## Prečo v Kontrole kontaktov v Nastaveniach telefónu vidím "Nájdené kľúče: 0"?\nZnamená to, že Apple / Google API vo vašom telefóne nevyhodnotilo v danom dátume žiadne stretnutie s nakazeným užívateľom. Inak povedané, stiahnuté kľúče nakazených užívateľov sa v danom dni nezhodujú so žiadnym zo zaznamenaných kľúčov vo vašom zariadení.\n\n## Prečo v Nastaveniach telefónu v sekcii Kontrola kontaktu vidím u konkrétneho dátumu nájdené kľúče, ale eRouška mi nezobrazuje žiadne rizikové stretnutia?\nPočet nájdených kľúčov predstavuje počet zaznamenaných stretnutí s nakazenými užívateľmi. Neznamená to však nutne, že tieto stretnutia bola rizikové. Ak stretnutie s nakazeným užívateľom nebolo rizikové, tak na neho eRouška neupozorňuje. Preto v aplikácii nie je vidieť.\n\n## Zobrazilo sa mi upozornenie / notifikácia na rizikové stretnutie, ku ktorému došlo pred niekoľkými dňami. Prečo ma eRouška upozornila až teraz?\nAplikácia eRouška nemôže vyhodnotiť riziko stretnutia a zobraziť upozornenie skôr, než užívateľ s pozitívnym testom na COVID-19 odošle dáta zo svojej eRoušky. Ak sa vám dnes zobrazila notifikácia, ktorá upozorňuje na rizikové stretnutie s nakazeným pred niekoľkými dňami, znamená to, že nakazený odoslal dáta zo svojej eRoušky len včera. Vaša eRouška si ich dnes stiahla a vyhodnotila.\n\n## Zobrazilo sa mi upozornenie / notifikácia na rizikové stretnutie s nakazeným, ale na obrazovke Rizikové stretnutia vidím viac záznamov s rôznymi dátami.\nAplikácia eRouška vyhodnocuje rizikové stretnutia až po tom, čo používateľ s pozitívnym testom na COVID-19 odošle dáta zo svojej eRoušky. Ak sa vám zobrazila iba jedna notifikácia na rizikový kontakt, ale v Rizikových stretnutiach máte viac záznamov, znamená to, že ste sa s nakazeným stretli v predchádzajúcich dňoch viackrát alebo že v jeden deň odoslalo dáta zo svojho telefónu viac nakazených užívateľov súčasne.\n\n## Bude eRouška fungovať ako "covid pas" pre zobrazovanie výsledkov testov a preukázanie očkovania?\nAnonymný charakter aplikácie eRouška ju neumožňuje kombinovať s tzv. Digitálnym zeleným certifikátom (Digital Green Certificate), ktorý pracuje s overenou identitou a osobnými údajmi užívateľa - výsledkami testov alebo vykonaným očkovaním proti COVID-19.\n\n## Publikuje eRouška nejaké štatistiky o používaní aplikácie?\nŠtatistiky publikujeme vo formáte JSON na adrese [stats.erouska.cz](https://stats.erouska.cz) . Dáta majú nasledujúcu štruktúru:\n\n{\\n "data": {\\n "modified": 1608696001,\\n "date": "20201223",\\n "activations\_yesterday": 2042,\\n "activations\_total": 1475571,\\n "key\_publishers\_yesterday": 718,\\n "key\_publishers\_total": 39175,\\n "notifications\_yesterday": 631,\\n "notifications\_total": 197298\\n }\\n}\n\n* **modified** = Unix timestamp vygenerovaných štatistík,\n* **date** = dátum vytvorenia reportu,\n* **activations\_yesterday** \= počet aktivácií za predchádzajúci kalendárny deň,\n* **activations\_total** = celkový počet aktivácií eRoušky,\n* **key\_publishers\_yesterday** \= počet nakazených ľudí, ktorí zadali v predchádzajúcom dni do eRoušky kód pre odoslanie dát,\n* **key\_publishers\_total** \= celkový počet nakazených ľudí, ktorí zadali do eRoušky kód pre odoslanie dát,\n* **notifications\_yesterday** \= počet ľudí, ktorým sa za predchádzajúci deň zobrazilo upozornenie na rizikové stretnutie,\n* **notifications\_total** = celkový počet ľudí, ktorým sa zobrazilo upozornenie na rizikové stretnutie.\n\nZavolanie bez parametra zobrazí informácie k aktuálnemu dátumu. Zavolanie s parametrom `date` umožní získať dáta pre ľubovoľný deň od 23. 10. 2020 (za všetky predchádzajúce dni máme iba agregované dáta). Pomocou parametra `date` s hodnotou `all` možno získať všetky dáta, ktoré rozhranie poskytuje.\n\n* dnešné dáta: [`https://stats.erouska.cz/`](https://stats.erouska.cz/)\n* dáta z 1. 12. 2020: [`https://stats.erouska.cz/?date=2020-12-01`](https://stats.erouska.cz/?date=2020-12-01)\n* dáta za všetky dni: [`https://stats.erouska.cz/?date=all`](https://stats.erouska.cz/?date=all)\n\n# Zabezpečenie dát\n## Ako zistím, ktorá všetky dáta presne aplikácie ukladá a odosiela?\nAplikácia eRouška používa od verzie 2.0 tzv. Apple / Google Exposure Notification API, ktoré sprostredkúva zber dát a odovzdávanie identifikátorov medzi užívateľmi. Aplikácia kvôli tomu už nemá priamy prístup k nameraným údajom, ktoré by mohla používateľom zobraziť, ako to bolo vo verzii 1.0.\n\nAplikácia každý deň sťahuje zoznam identifikátorov od používateľov, ktorým test potvrdil nákazu. Údaje s identifikátormi na spracovanie môže ostatným odoslať iba jediný užívateľ, a to až po zadaní jednorázového SMS kódu, ktorý získa v prípade pozitívneho testu na nákazu ochorením COVID-19 automaticky, prípadne na vyžiadanie.\n\nAplikácia ďalej odosiela na server anonymnú štatistickú / agregovanú informáciu, že došlo k notifikácii rizikového kontaktu. Táto informácia nie je viazaná na žiadny identifikátor užívateľa ani jeho mobilu a slúži iba na výpočet štatistických informácií o účinnosti systému eRouška.\n\nAby sme zaistili funkčnosť eRoušky, pracujeme tiež s údajmi o fungovaní (napr. záznamy o pádoch aplikácie a používaní aplikácie) na telefóne a používame k tomu štandardné nástroje (Firebase Crashlytics a Google Analytics) od spoločnosti Google. Telemetrické údaje odosielané aplikáciou do týchto služieb používajú identifikátory vašej osoby alebo telefónu.\n\nPodrobné informácie o zhromažďovaní a spracovaní údajov nájdete v [Informáciách o spracovaní osobných údajov v aplikácii eRouška 2.0](https://erouska.cz/sk/podminky-pouzivani) .\n\n## Prenesú sa dáta z eRoušky po obnove telefónu zo zálohy alebo pri prechode na nový telefón?\nZ dôvodu ochrany súkromia užívateľov operačné systémy Android ani iOS nezálohujú dáta z Exposure Notification. Pri preinštalácii telefónu alebo prechode na iné zariadenie teda nemožno preniesť ani obnoviť zaznamenané a vygenerované identifikátory.\n\nAk chcete získať informáciu o možnom rizikovom stretnutí, odporúčame sledovať stav starého telefónu ešte po dobu 14 dní po inštalácii eRoušky na nové zariadenie. Pritom je potrebné:\n\n* zabezpečiť, aby vaše zariadenie bolo zapnuté a pripojené k internetu, aby si mohlo sťahovať dáta na vyhodnotenie,\n* ideálne aspoň raz denne kontrolovať staršie zariadenia, teda otvoriť eRoušku.\n* Ak by ste počas týchto 14 dní mali pozitívny test, zadajte prosím kód do nového zariadenia a na získanie kódu pre staršie telefóny kontaktujte [info@erouska.cz](mailto:info@erouska.cz) .\n\nPo 14 dňoch už váš starý telefón nebude obsahovať žiadne dáta pre vyhodnotenie rizikového stretnutia. Môžete z neho preto odstrániť eRoušku a ponechať len v novom zariadení.\n\n## Čo keď mi telefón ukradnú alebo ho stratím?\nV aplikácii eRouška sú lokálne uložené len anonymné informácie o ďalších zariadeniach, ktoré eRouška zaznamenala vo svojom okolí. Žiadne riziko to neprináša pre vás ani pre majiteľov takto zaznamenaných zariadení. Iné aplikácie, ktoré bežne využívate, môžu vo vašom telefóne pravdepodobne ukladať oveľa citlivejšie údaje. Majte preto svoj telefón chránený kódom alebo biometricky (odtlačkom prsta alebo tvárou).\n\n## Je aplikácia eRouška v súlade s GDPR?\nCelý systém aplikácie eRouška, vrátane webových stránok, je navrhnutý plne v súlade s GDPR aj zákonom o spracovaní osobných údajov.\n\nTu nájdete bližšie informácie:\n\n* [Informácie o spracovaní osobných údajov v aplikácii eRouška 2.0](https://erouska.cz/sk/podminky-pouzivani)\n* [Audit zdrojového kódu aplikácie](https://erouska.cz/sk/audit-kod)\n\n## Ako užívateľa chránite pred zneužitím dát?\nUžívateľa chránime predovšetkým minimálnym rozsahom zaznamenávaných dát a ich ukladaním priamo do zariadenia. Dáta používateľov sa bez ich vedomia a súhlasu nikam neposielajú ani nespracovávajú. Do telefónov sa zaznamenávajú anonymné identifikátory (ID) zariadenia s nainštalovanou aplikáciou eRouška a informácie o čase, dĺžke a sile ich Bluetooth signálu. Dáta sa spracovávajú automatizovane na serveri iba po odsúhlasenom odoslaní užívateľom.\n\n## Dohliada na zabezpečenie dát z aplikácie nejaká nezávislá organizácia?\nCelý systém aplikácie eRouška, vrátane podporných webových stránok, je navrhnutý plne v súlade s GDPR. Kód aplikácie je ako open-source voľne k dispozícii ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)) a prešiel [auditom nezávislých vzdelávacích inštitúcií](https://erouska.cz/sk/audit-kod).\n\nĎalšie informácie nájdete v [Informáciách o spracovaní osobných údajov v rámci aplikácie eRouška 2.0](https://erouska.cz/sk/podminky-pouzivani) .\n\n## Môže ma niekto pomocou aplikácie eRouška sledovať?\nNie. Aplikácia nezbiera údaje o vašej polohe a k uloženým dátam máte prístup iba vy. Samotné údaje, ktoré aplikácia po vašom súhlase odosiela, nemôžu byť použité k vášmu sledovaniu. Každá eRouška navyše mení z dôvodu bezpečnosti a ochrany súkromia užívateľov pravidelne svoje vysielané ID, aby nebolo ľahké ho odchytiť a nejako zneužiť.\n\n## Je možné overiť, že aplikácia nezaznamenáva moju polohu?\nÁno. Aplikácia eRouška je publikovaná s otvoreným zdrojovým kódom ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)), takže znalý človek môže ľahko overiť, že údaje o polohe aplikácie naozaj nezbiera.\n\nZdrojové kódy aplikácie eRouška [preverujú nezávislé autority](https://erouska.cz/sk/audit-kod) , ktoré kontrolujú, že aplikácia:\n\n* nesleduje polohu\n* sama automaticky dáta s identifikátormi nikam neposiela\n\n## Je možné identifikátor aplikácie (ID) používateľa spárovať s konkrétnou osobou?\nAplikácia eRouška 2.0 [nepracuje s osobnými údajmi](https://erouska.cz/sk/podminky-pouzivani) . Vygenerované anonymné ID eRoušky sa medzi aplikáciami a servermi prenášajú výlučne po chránenom komunikačným protokolom, ktorý spoločne vyvinuli spoločnosti Apple a Google. K identifikátorom nemajú prístup ani používatelia, ani vývojári, ani iná autorita.\n\n## Kto má (môže mať) prístup k mojim údajom?\nK zaznamenaným údajom o stretnutí nemá v eRouške 2.0 prístup užívateľ, ani nikto iný. Dôvodom je, že aktuálna verzia aplikácie používa Apple / Google protokol, ktorý k zaznamenaným dátam neposkytuje prístup ani užívateľom, ani vývojárom a správcom. Dáta (anonymné identifikátory nakazených) sa prenášajú na server a odtiaľ do ostatných eRoušiek šifrovane a vyhodnocujú sa až na koncových zariadeniach automatizovane za účelom zobrazenia prípadných upozornení o rizikovom kontakte. Zo servera aj zo zariadení sa dáta mažú automaticky po 14 dňoch.\n\n## Ako dlho do minulosti sú dáta k dispozícii? Kedy a ako prebieha ich mazanie?\nV aplikácii eRouška sa uchovávajú dáta zaznamenané za posledných 14 dní, staršie záznamy sa automaticky odmazávajú. Pokiaľ po pozitívnom teste na COVID-19 zadáte overovací kód do eRoušky a odošlete identifikátory z telefónu na server, zostanú tu 14 dní a potom dôjde k ich zmazaniu. Dáta môžete zo svojho zariadenia zmazať v Nastaveniach Oznámenie o kontaktoch s nákazou/Kontakty s nákazou/Exposure Notification - konkrétny postup záleží na type a verzii operačného systému.\n\n* **Android:** Na zariadeniach so systémom Android v súčasnom nastavení dôjde pri odinštalovaní aplikácie eRouška k zmazaniu zaznamenaných kľúčov (identifikátorov).\n\nInformácie o správcoch a spracovateľoch údajov nájdete v [Informáciách o spracovaní osobných údajov v rámci aplikácie eRouška](https://erouska.cz/sk/podminky-pouzivani) .\n\n## Môžem si používanie aplikácie kedykoľvek rozmyslieť? Môžem zmazať dáta, ktoré odošlem na spracovanie?\nAplikáciu môžete kedykoľvek odinštalovať. Vygenerované a zaznamenané kľúče (záznamy stretnutí) môžete zo svojho zariadenia zmazať v Nastaveniach Oznámenie o kontaktoch s nákazou/Kontakty s nákazou/Exposure Notification - konkrétny postup záleží na type a verzii operačného systému. Údaje odoslané na server sa zmažú automaticky po 14 dňoch.\n\n* **Android:** Na zariadeniach so systémom Android v súčasnom nastavení dôjde pri odinštalovaní aplikácie eRouška tiež k zmazaniu zaznamenaných kľúčov (identifikátorov).\n\n## Čo sa deje s dátami po vymazaní aplikácie z mobilu?\n**Android:** Keď zmažete aplikáciu z telefónu alebo tabletu s OS Android, dôjde tiež k zmazaniu zaznamenaných kľúčov (identifikátorov).\n\nAk aplikáciu znova nainštalujete, začnú sa zbierať a do telefónu ukladať úplne nové dáta.\n\n## Kde nájdem zdrojový kód aplikácie?\nAplikácia eRouška je publikovaná s otvoreným zdrojovým kódom. Nájdete ho na GitHube ([Android](https://github.com/covid19cz/erouska-android) , [iOS](https://github.com/covid19cz/erouska-ios)).\n\n# Inštalácia a kompatibilita\n## Na akých telefónoch aplikácia eRouška funguje? Prečo sú nároky vyššie než v eRouške 2.0?\nNa inštaláciu aplikácie eRouška potrebujete mobilné zariadenia s týmito parametrami:\n\n* iPhone s iOS verzie 13.5 a vyššej\n* OS Android verzie 6.0 a vyššej s Google Play Services (nemá len malé percento telefónov Huawei a niektoré telefony s vlastnou ROM)\n* Bluetooth LE (Low Energy)\n\nAk vaše mobilné zariadenie nie je s eRouškou kompatibilné (nemá potrebné technológie / funkcie), Google Play ani Apple App Store vám neumožní aplikáciu nainštalovať. Aplikácia je dostupná len pre niektoré tablety Android. Nie je k dispozícii pre iPad.\n\nMinimálne požiadavky na kompatibilitu stanovujú spoločnosti Apple a Google. Vychádzajú z požiadaviek Apple / Google Exposure Notification protokolu, ktorý nová eRouška používa. Na Apple / Google protokol sme sa rozhodli prejsť kvôli zabezpečeniu fungovania eRoušky na iPhonoch a kvôli umožneniu cezhraničnej kompatibility s podobnými aplikáciami ďalších európskych štátov.\n\n## Odkiaľ si môžem eRoušku bezpečne stiahnuť a nainštalovať?\nAplikácia eRouška je dostupná iba pre:\n\n* Android: [v Obchode Google Play](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska) .\n\n## Je možné aplikáciu stiahnuť aj inak ako z oficiálnych aplikačných obchodov? Je k dispozícii balíček apk?\nAplikácia eRouška 2.0 používa Apple/Google protokol pre zaznamenávanie stretnutia a prenos dát. Preto je tiež závislá na systémových službách, bez ktorých nebude fungovať, a nie je ju teda možné inštalovať z iných ako oficiálnych aplikačných obchodov. Aplikačné obchody spoľahlivo vyfiltrujú podporované zariadenia.\n\nVývojári eRoušky nikdy neposkytujú užívateľom odkazy na inštalačné balíčky APK alebo samotné inštalačné súbory mimo oficiálne obchody s aplikáciami. V aplikáciách nainštalovaných neoficiálnou cestou by sme nedokázali garantovať ich zabezpečenie, zabezpečiť ich aktualizáciu ani rovnakú úroveň užívateľskej podpory. Preto prosím nikdy neinštalujte eRoušku z .apk súborov, ktoré ste našli niekde na internete alebo vám ich niekto poslal e-mailom. Môžu obsahovať vírusy, trójske kone alebo iný škodlivý obsah.\n\n## Môžem používať eRoušku spolu s inou oficiálnou trasovacou aplikáciou iného štátu (napr. Corona-Warn-App)?\nAk sa zdržiavate primárne na území ČR, odporúčame používať eRoušku aj pre cesty do zahraničia. [Aplikácia teraz spolupracuje aj s ďalšími štátmi EÚ](https://erouska.cz/sk/caste-dotazy#efgs) .\n\nV ostatných prípadoch môžete mať v telefóne nainštalované viaceré trasovacie aplikácie typu eRouška, ako sú Corona-Warn-App, Stopp Corona App, Imunni, Stop Covid ProteGO Safe a ďalšie. Ak však tiež používajú Apple/Google Exposure Notification protokol (platí najmä pre susedné štáty ČR), môžete mať v jeden okamih aktívnu iba jednu z týchto aplikácií. Ostatné je potreba pozastaviť. K zaznamenávaniu stretnutí v Apple/Google protokole totiž môže mať prístup vždy len jedna aplikácia.\n\n## Funguje eRouška, keď mám na mobile úsporný režim/režim nízkej spotreby/šetrič batérie?\n**Android:** Šetriče/sporiče batérie na zariadeniach s OS Android môžu do značnej miery obmedzovať beh aplikácií na pozadí. Hoci systém oznámení o možnom kontakte s ochorením COVID-19 naďalej funguje, nemusí dochádzať k vyhodnocovaniu rizikových kontaktov, a teda k prípadnému zobrazeniu notifikácií.\n\n## Bude možné používať eRoušku aj na smart hodinkách či náramkoch?\nRozšírenie je teoreticky do budúcnosti možné. Zatiaľ ho ale nezvažujeme najmä preto, že by smart hodinky či náramok museli podporovať Bluetooth LE (Low Energy) a najmä Apple / Google protokol. Základný Bluetooth bez LE sám o sebe iba zaisťuje prenos dát, ale neumožňuje meranie intenzity stretnutí užívateľov na základe sily signálu.\n\nv2_shareAppDynamicLinkhttps://erouska.cz/app/sdilejv2_minSupportedVersionCodeAndroid666v2_shouldCheckOSVersion1v2_unsupportedDeviceLinkhttps://koronavirus.mzcr.czv2_minSupportedVersion2.3.3v2_currentMeasuresUrlhttps://covid.gov.cz/opatreniv2_appleExposureConfiguration{"factorHigh":0.25,"factorStandard":1,"factorLow":2,"lowerThreshold":55,"higherThreshold":70,"triggerThreshold":10}v2_daysSinceOnsetToInfectiousness0;0;0;0;0;0;0;0;0;0;0;1;2;2;2;2;2;2;2;1;1;1;1;1;1;1;1;1;1v2_supportEmailinfo@erouska.czv2_appleServerConfiguration{"minSupportedVersion":"13.5","showExposureForDays":14,"healthAuthority":"cz.covid19cz.erouska","uploadURL":"https://exposure-fghz64a2xa-ew.a.run.app/v1/publish","downloadIndexName":"erouska/index.txt","downloadsURL":"https://google.com","verificationURL":"https://apiserver-jyvw4xgota-ew.a.run.app","verificationAdminKey":"","verificationDeviceKey":"4AP6RHk5VlNi7WIhj4ZupI9JZODdUyrPGb1C2mDYKo4cuaGIHdYtOhjyoqhg4vB5r7FDijxnySLb_1CUH6XdDA.1.oZLaUuVOPbvEaiWnzkQxasWlD_71iSTeM9aAIOKrfqhg5QO68t004CKxWutYmfISc7vqJe6uY2dRrt34OFkliw","appCurentDataURL":"https://europe-west1-daring-leaf-272223.cloudfunctions.net","firebaseURL":"https://europe-west1-daring-leaf-272223.cloudfunctions.net"}v2_handleError400AsExpiredOrUsedCodefalsev2_handleError500AsInvalidCodefalsev2_exposureHelpContentJson{"items":[{"iconUrl":"https://erouska.cz/img/exposure/ic_notification.png","label":"Upozornenie sa vám zobrazí najskôr 24 hodín po tom, čo sa nakazený dozvie pozitívny výsledok testu na COVID-19 a zadá overovací kód do eRoušky."},{"iconUrl":"https://erouska.cz/img/exposure/ic_privacy.png","label":"Kvôli zachovaniu anonymity nepoznáme čas ani miesto stretnutia."},{"iconUrl":"https://erouska.cz/img/exposure/ic_distance.png","label":"Rizikový kontakt vyhodnotíme v prípade, že ste s nakazeným boli v kontakte na vzdialenosť kratšiu ako 2 metre po dobu aspoň 7 minút."},{"iconUrl":"https://erouska.cz/img/exposure/ic_14days.png","label":"Rizikové stretnutie vyhodnocujeme za posledných 14 dní, pretože príznaky ochorenia COVID-19 sa u vás môžu prejaviť až 2-14 dní po stretnutí s nakazeným."}]}v2_exposureHelpUITitleNápovedav2_updateNewsOnRequesttruev2_recentExposureNotificationTitleVaša eRouška si dlhú dobu neaktualizovala dáta, či ste sa stretli s nakazeným užívateľom eRoušky. Pripojte sa k internetu.v2_efgsCountriesAktuálne s eRouškou spolupracuje Belgicko, Dánsko, Fínsko, Chorvátsko, Írsko, Taliansko, Cyprus, Lotyšsko, Nemecko, Holandsko, Nórsko, Poľsko, Rakúsko, Slovinsko a Španielsko.v2_diagnosisKeysDataMappingLimitDays7v2_infectiousnessWhenDaysSinceOnsetMissing1v2_reportTypeWhenMissing1v2_noEncounterCardTitleZa posledných 14 dní žiadne rizikové stretnutiev2_encounterUpdateFrequencyAktualizácia prebieha raz za %d hodín.v2_dbCleanupDays15v2_selfCheckerPeriodHours4v2_showChatBotLinkfalsev2_efgsVisitedCountriesv2_efgsReportTypeConfirmedTestv2_efgsConsentToFederationfalsev2_efgsTravellerDefaultfalsev2_appleExposureConfigurationV2{"immediateDurationWeight":200,"nearDurationWeight":100,"mediumDurationWeight":25,"otherDurationWeight":0,"infectiousnessForDaysSinceOnsetOfSymptoms":{"0":2,"1":2,"2":2,"3":2,"4":2,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"unknown":1,"-14":0,"-13":0,"-12":0,"-11":0,"-10":0,"-9":0,"-8":0,"-7":0,"-6":0,"-5":0,"-4":0,"-3":1,"-2":2,"-1":2},"infectiousnessStandardWeight":100,"infectiousnessHighWeight":200,"reportTypeConfirmedTestWeight":100,"reportTypeConfirmedClinicalDiagnosisWeight":100,"reportTypeSelfReportedWeight":100,"reportTypeRecursiveWeight":100,"reportTypeNoneMap":1,"minimumRiskScore":0,"attenuationDurationThresholds":[55,70,80],"attenuationLevelValues":[1,2,3,4,5,6,7,8],"daysSinceLastExposureLevelValues":[1,2,3,4,5,6,7,8],"durationLevelValues":[1,2,3,4,5,6,7,8],"transmissionRiskLevelValues":[1,2,3,4,5,6,7,8],"minimumScore":420}v2_helpJson[{"title":"Najčastejšie problémy","subtitle":"Odporúčania dočasných opráv","icon":"https://erouska.cz/img/faq/tool.png","questions":[{"question":"Android: Základné odporúčania pri riešení problémov s aplikáciou","answer":"Ak vám aplikácia zobrazuje chybovú hlášku, skontrolujte prosím najskôr, či máte nainštalované aktualizácie operačného systému ( [postup aktualizácie Androidu](https://support.google.com/android/answer/7680439) ) a nepoužívate ich beta verziu.\n\nĎalej prosím skontrolujte v obchode [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska), či nie je pre eRoušku dostupná nová aktualizácia.\n\nOdporúčame tiež povoliť automatické aktualizácie aplikácie eRouška. [Postup pre Android tu:](https://support.google.com/googleplay/answer/113412#autoone) Spustite aplikáciu Obchod Google Play, kliknite na ponuku Moje aplikácie a hry → Vyberte aplikáciu, ktorú chcete aktualizovať → kliknite na ikonu možností a kliknite na Aktivovať automatické aktualizácie."},{"question":"Android: Nejde aktivovať eRouška. Chyba aktivácie 17 (Exposure Notification API nie je k dispozícii).","answer":"Pre vyriešenie chyby na Android zariadeniach, skúste, prosím, postupovať takto:\n\n1. Skontrolujte, či ste _hlavný užívateľ_ zariadenia, či máte v telefóne služby Play a či je k dispozícii _Nastavenia → Google → Oznámenie o možnom kontakte (COVID-19 Exposure Notifications)_ .\n2. Odinštalujte eRoušku.\n3. Vymažte vyrovnávaciu pamäť a dáta u Google Play Services a Obchodu Play ([postup tu, viď kroky 2 a 3](https://support.google.com/googleplay/answer/9037938) ).\n4. Nainštalujte eRoušku a zapnite opäť _Oznámenia o možnom kontakte COVID-19_ .\n\nAk sa vám aj napriek aktualizácii systému, aplikácie a uvedených krokov naďalej objavuje chyba aktivácie, je bohužiaľ možné, že vaše zariadenie ešte nepodporuje Bluetooth LE (Low Energy Advertising), nepodporuje Bluetooth Multiple Advertisement, nie je autorizované Googlom pre Exposure Notifications alebo vôbec nemá Exposure Notifications API."}]},{"title":"Pripojenie a prenos dát","subtitle":"Bluetooth, GPS, internet","icon":"https://erouska.cz/img/faq/connection.png","questions":[{"question":"Musím mať Bluetooth zapnutý neustále?","answer":"Áno. Bez zapnutého Bluetooth nie je možné zisťovať blízkosť ďalších zariadení s nainštalovanou aplikáciou eRouška. Túto funkciu máte zapnutú možno aj teraz, napríklad v prípade, že používate bezdrôtové slúchadlá, pripojenie v aute, smart náramok alebo hodinky. V nastavení Bluetooth vášho telefónu / tabletu navyše odporúčame povoliť, aby bol _viditeľný pre všetky zariadenia_ ."},{"question":"Potrebuje aplikácia pripojenie k internetu?","answer":"Pripojenie k internetu je nutné pre stiahnutie (inštaláciu) a aktiváciu eRoušky. Ďalej je potrebné, aby si eRouška mohla každý deň sťahovať aktuálny zoznam anonymných identifikátorov nakazených užívateľov, a zobraziť tak včas prípadnú notifikáciu o rizikovom stretnutí.\n\nAk by u vás test potvrdil nákazu COVID-19 a vy by ste chceli anonymne upozorniť ostatných na možný rizikový kontakt, bolo by opäť potreba eRoušku pripojiť k internetu. Dovtedy sú vaše dáta uložené iba vo vašom telefóne, odkiaľ sa samy odmazávajú po 14 dňoch. Aplikácia využíva pripojenie k internetu aj na stiahnutie informácií o aktuálnom vývoji epidémie COVID-19 v ČR, ktoré zobrazuje na stránke _Aktuálne_ ."},{"question":"Ako často si aplikácia aktualizuje dáta z internetu?","answer":"Kľúče (identifikátory) nakazených užívateľov si eRouška sťahuje niekoľkokrát denne. Štatistiky nakazených osôb na obrazovke Aktuálne sa aktualizujú raz denne."},{"question":"Ako veľký objem mobilných dát eRouška denne spotrebuje?","answer":"V prípade zapnutého _Upozornenia na zahraničné riziková stretnutí_ eRouška sťahuje tiež kľúče všetkých nakazených užívateľov z ostatných štátov EÚ. Objem stiahnutých dát sa v takom prípade môže pohybovať až okolo 2 MB denne.\n\nAj v prípade vypnutého _Upozornenia na zahraničné rizikové stretnutia_ eRouška sťahuje približne 1 MB denne. Nová verzia aplikácie sťahuje nielen kľúče nakazených užívateľov eRoušky, ale aj kľúče nakazených užívateľov z ostatných štátov EÚ, ktorí vo svojich aplikáciách uviedli, že cestujú do zahraničia, takže ich užívateľ eRoušky potenciálne mohol stretnúť aj v Českej republike\n\nSúbory kľúčov nakazených užívateľov sa sťahujú postupne, 2-3× denne. Pre porovnanie, načítanie priemernej stránky spravodajstva vyžaduje 1-5 MB dát. **Aplikácia nepotrebuje nutne mobilné dátové pripojenie.** Dáta si stiahne sama aj na Wi-Fi."},{"question":"Android: Prečo je potrebné mať povolený prístup k GPS/lokalizačným/lokačným údajom?","answer":"**Aktualizácia pre užívateľov Android 11:** V najnovšej verzii operačného systému Android už nemusíte pre fungovanie eRoušky povoľovať služby určovania polohy.\n\nAplikácia eRouška nezbiera a neukladá dáta z GPS, avšak operačný systém **Android 10 a staršie** zahŕňa pod určovanie polohy aj niektoré služby Bluetooth LE (LE = low energy), ktorý komponenty eRoušky - konkrétne Google Play Services - pre svoje fungovanie vyžadujú. Preto je nutný súhlas užívateľov s prístupom aplikácie k lokalizačným údajom. V iOS tento súhlas potrebný nie je. Prečo v telefóne musí byť zapnuté určovanie polohy, [vysvetľuje Google tu](https://support.google.com/android/answer/9930236) .\n\nInformáciu o vašej polohe môžu využívať aj iné aplikácie vo vašom zariadení. Ak chcete skontrolovať, ktoré aplikácie majú k nim prístup, prejdite do nastavení zariadenia a vyberte položky Zabezpečenie a poloha → Umiestnenie → Oprávnenie na úrovni aplikácie (prípadne nasledujte [postup od spoločnosti Google](https://support.google.com/accounts/answer/6179507?hl=cs)). Tu môžete iným aplikáciám odoprieť oprávnenie používať vašu polohu, keď si nemyslíte, že to potrebujú. Pretože eRouška toto oprávnenie nepotrebuje, nezobrazí sa v zozname."},{"question":"Koľko zapnutý Bluetooth a aplikácia eRouška spotrebujú batérie?","answer":"Ak už teraz používate telefón so stále zapnutým Bluetooth, spotreba energie by sa nemala výraznejšie zvýšiť. Ak Bluetooth teraz bežne nepoužívate, tak podľa výsledkov nášho testovania s neustále zapnutým Bluetooth a ukladaním dát do aplikácie bolo navýšenie dennej spotreby energie v ráde jednotiek percent. Vo väčšine prípadov to je menej ako 20% kapacity batérie za deň. Záleží pritom na konkrétnom smartfóne, jeho veku, štýlu jeho používania a stavu batérie.\n\nSpotrebu batérie spojenú s použitím aplikácie eRouška ovplyvňujú dva faktory. Po prvé samotný beh aplikácie a po druhé využitie systémovej funkcie Oznámenie o kontaktoch s nákazou, ktorá priebežne zaznamenáva stretnutia s používateľmi v okolí. Systémovú funkciu zabezpečuje OS Android alebo iOS, preto jej optimalizácia záleží na aktualizáciách zo strany spoločností Apple a Google.\n\nPri interpretácii spotreby batérie zo štatistík v nastaveniach telefónu berte prosím do úvahy, že toto percento zodpovedá plnému využitiu vášho telefónu za posledných 24 hodín (percentuálny podiel behu medzi ostatnými aplikáciami). To znamená, že u málo využívaného telefónu môže byť percento pri Kontaktoch s nákazou / aplikácii eRouška vysoké, u intenzívne využívaných telefónov naopak nízke. Nejde teda priamo o percento spotreby batérie.\n\n**Android: Poznámka k povoleniu lokalizačných služieb:** Bluetooth zariadenia vo vašej blízkosti sa dá detekovať, len ak je v telefóne aktivovaná možnosť \"Použiť polohu\". (Podrobnosti nájdete v odseku [Prečo v telefóne musí byť zapnuté určovanie polohy? Na webe Googlu](https://support.google.com/android/answer/9930236?hl=cs) .) To znamená, že informáciu o vašej polohe môžu využívať aj iné aplikácie vo vašom zariadení, čo môže byť dôvodom vyššej spotreby batérie. Preto, prosím, skontrolujte, ktoré aplikácie používajú vašu polohu. Prejdite do nastavení [zariadenia a vyberte položky Zabezpečenie a poloha → Umiestnenie → Oprávnenie na úrovni aplikácie (prípadne nasledujte](https://support.google.com/android/answer/9930236?hl=cs) [postup od spoločnosti Google](https://support.google.com/accounts/answer/6179507?hl=cs) ). Tu môžete iným aplikáciám odoprieť oprávnenie používať vašu polohu, keď si nemyslíte, že to potrebujú. Pretože eRouška toto oprávnenie nepotrebuje, nezobrazí sa v zozname."},{"question":"Je možné, že sa mi po zapnutí eRoušky spomalilo Wi-Fi / pripojenie k internetu?","answer":"Pri niektorých modeloch mobilov s OS Android je bohužiaľ známe, že vysielanie Bluetooth môže čiastočne negatívne ovplyvňovať signál Wi-Fi a naopak."},{"question":"Môže eRouška spôsobovať problémy s ďalšími aplikáciami alebo zariadeniami / perifériami (slúchadlá, hodinky, náramok), ktoré používajú Bluetooth?","answer":"Vo výnimočných prípadoch nám niektorí užívatelia hlásia neštandardné správanie ich Bluetooth periférií po tom, čo nainštalovali aplikáciu eRouška. Napr. môže dochádzať k prerušovaniu spojenia - typicky u smart hodiniek či náramkov, bezdrôtových slúchadiel alebo handsfree sád. S najväčšou pravdepodobnosťou ide o problém funkcií operačného systému, na ktoré bohužiaľ nemáme vplyv.\n\nSkúste prosím nsledujúci postup:\n\n1. Reštartujte telefón.\n2. Zrušte spárovanie s Bluetooth zariadením a znovu ho pripojte.\n\nAk kroky vyššie nepomôžu, kontaktujte prosím výrobcu zariadenia:\n\n* Android: [Podpora výrobcov zariadení](https://support.google.com/android/answer/3094742?hl=cs&ref_topic=7313011)"},{"question":"Prečo aplikácia používa zrovna Bluetooth? Neexistujú lepšie riešenia ako GPS alebo triangulácia od operátorov?","answer":"Súkromie užívateľov je pre nás na prvom mieste, a práve technológia Bluetooth ponúka najvyváženejší pomer presnosti záznamu a minimálneho zásahu do súkromia. Technológia GPS ani triangulácie vysielačov od operátorov nemôžu poskytnúť potrebné údaje s požadovanou presnosťou. Okrem iného aj preto, že ich funkčnosť je vo vnútorných priestoroch či metra veľmi obmedzená. Aplikácia eRouška potrebuje pre svoju funkciu informácie o tom, že došlo k stretnutiu, na akú vzdialenosť a po akú dobu. Nie je dôležité, kde k stretnutiu došlo."},{"question":"Je Bluetooth dostatočne bezpečná technológia?","answer":"Bluetooth je dnes bežne rozšírená a podporovaná komunikačná technológia na prepojenie zariadení s ďalšími perifériami, ako sú bezdrôtové slúchadlá, reproduktory, smart náramky alebo hodinky. Vzhľadom k tomu, že spoločnosti Apple a Google vytvorili spoločný protokol určený výhradne pre národné aplikácie, ktoré trasujú rizikové stretnutia s nakazenými COVID-19 na základe Bluetooth LE, sme presvedčení, že sme pre eRoušku zvolili správne. Je však potrebné povedať, že úplne každá technológia je zraniteľná. Aj preto všetkým užívateľom odporúčame, aby operačný systém a aplikácie vo svojom telefóne pravidelne aktualizovali. Aktualizácie totiž môžu obsahovať dôležité bezpečnostné záplaty."}]},{"title":"Všeobecné informácie","subtitle":"eRouška, karanténa a postupy hygieny","icon":"https://erouska.cz/img/faq/general.png","questions":[{"question":"Ako mám postupovať, ak mi aplikácia zobrazila upozornenie na rizikové stretnute s nakazeným užívateľom?","answer":"Ak máte príznaky choroby (napríklad zvýšenú teplotu, kašeľ, dýchavičnosť, bolesť hrdla, bolesť hlavy, náhlu stratu čuchu a chuti), telefonicky kontaktujte svojho praktického lekára. V prípade, že je váš stav naozaj akútny, volajte rýchlu záchrannú službu 155, prípadne linku integrovaného záchranného systému 112. Uvedené príznaky sa môžu objaviť 2 až 14 dní po rizikovom stretnutí.\n\nAk nemáte žiadne príznaky, správajte sa, prosím, zodpovedne:\n\n* Noste rúško či respirátor cez ústa a nos.\n* Často a dôkladne si umývajte ruky vodou a mydlom alebo používajte dezinfekciu.\n* Pravidelne dezinfikujte vlastné predmety (napr. mobilný telefón).\n* Kašlite a kýchajte do vreckovky či rukávu.\n* Používajte jednorázové vreckovky a potom ich vyhoďte.\n* Vyhýbajte sa zbytočnému zhromažďovaniu a dodržujte bezpečný odstup od ostatných (približne 2 metre).\n\nPokiaľ po notifikácii eRoušky máte vážne podozrenie na rizikové stretnutie, kontaktujte svojho praktického lekára a riaďte sa jeho pokynmi. Je tiež možné, že vás bude v blízkej dobe kontaktovať pracovník hygienickej služby na základe epidemiologického vyšetrenia s konkrétnym nakazeným. V takom prípade postupujte podľa pokynov hygienickej služby.\n\nAk by sa v priebehu 14 dní od rizikového stretnutia váš zdravotný stav zhoršil a objavili sa u vás príznaky ochorenia COVID-19, kontaktujte svojho praktického lekára."},{"question":"Ako mám postupovať, keď mám pozitívny výsledok testu na COVID-19 alebo mi prišla SMS eRoušky s overovacím kódom?","answer":"Ak vám prišla SMS z laboratória s pozitívnym výsledkom testu na COVID-19, mala by vám do niekoľkých hodín prísť tiež SMS eRoušky s overovacím kódom pre odoslanie dát. SMS eRoušky sa odosielajú automatizovane po zapísaní výsledku testov do informačného systému. Okamih, kedy laboratórium odovzdá výsledky do informačného systému, nedokážu automatizované procesy Chytrej karantény ovplyvniť.\n\nAkonáhle vám príde SMS eRoušky s overovacím kódom, [postupujte, prosím, podľa tohto návodu](https://erouska.cz/sk/sms) . Uvedený postup vám umožní anonymne informovať ostatných užívateľov o prípadnom rizikovom stretnutí. Ďakujeme.\n\nMáte pozitívny test a neprišla vám SMS eRoušky? Napíšte si o nový kód na [info@erouska.cz](mailto:info@erouska.cz?subject=)."},{"question":"To budem musieť zakaždým automaticky do karantény alebo na testy, keď aplikácia zahlási, že som bol v kontakte s niekým nakazeným?","answer":"Nie, aplikácia eRouška je plne anonymná, jej použitie je úplne dobrovoľné a upozornenie na možný rizikový kontakt s nakazeným nie je dôvod pre nariadenie karantény. Ak u niekoho test potvrdí nákazu, kontaktuje ho hygienická stanica, aby zistila, s kým bol v posledných dňoch v rizikovom kontakte, a mohla tak obmedziť ďalšie šírenie. Keď tento človek používa aplikáciu eRouška, môže kontaktovať aj tých, ktorých nepozná, pretože s nimi napríklad cestoval v autobuse. Zadaním unikátneho kódu môže poslať upozornenie všetkým ostatným používateľom aplikácie eRouška, s ktorým bol v epidemiologicky rizikovom kontakte.\n\nV okamihu, keď vás aplikácia upozorní, že ste sa stretli s osobou, u ktorej bolo preukázané ochorenie COVID-19, by ste mali postupovať podľa zobrazených odporúčaní a v prípade zdravotných ťažkostí bez odkladu kontaktovať svojho praktického lekára."},{"question":"Spozná eRouška, keď poruším lekárom nariadenú karanténu alebo keď napríklad pôjdem na chatu?","answer":"Aplikácia nesleduje vašu polohu, nie je to jej účel a ani pre to nie je navrhnutá. Nespozná teda, či ste porušili karanténne opatrenie alebo kam ste odišli. Má naopak pomôcť k tomu, aby v karanténe boli iba osoby s diagnostikovaným ochorením alebo s vážnym podozrením nákazy."},{"question":"Môže mi eRouška miesto lekára vystaviť práceneschopnosť? Je upozornenie na možné rizikové stretnutie dôvod pre pracovnú neschopnosť?","answer":"Nemôže, nie je. Ak vám eRouška zobrazila varovanie, postupujte podľa zobrazených odporúčaní. Upozornenie na možný rizikový kontakt nijako nenahrádza správu od vášho lekára alebo laboratória, ktorá vyhodnotí váš test na COVID-19. Iba lekár vám vystaví platné dokumenty, ktoré predložíte svojmu zamestnávateľovi."},{"question":"Prečo je eRouška potrebná? Dokáže ma vôbec efektívne chrániť pred nákazou?","answer":"Jednou z úloh eRoušky je zastaviť ďalšie šírenie nákazy tým, že pomáha čo najskôr izolovať potenciálne nakazené osoby od ostatných. Vďaka tomu potom nie je nutné zavádzať plošnú karanténu a obmedzenia, čo umožňuje znižovať dopady pandémie na spoločnosť a ekonomiku ČR. Aplikácie na podobnom princípe fungujú aj v ďalších štátoch a sú účinným prostriedkom v boji proti šíreniu choroby COVID-19.\n\nAplikácia eRouška 2.0 je doplnkovou aplikáciou Chytrej karantény - systému ministerstva zdravotníctva. Prostredníctvom technológie Bluetooth sa eRouška spája s ďalšími eRouškami v najbližšom okolí a ukladá ich anonymné identifikátory. Ak sa u človeka preukáže nákaza COVID-19, spojí sa s ním pracovník hygienickej stanice a overí, či osoba používa aplikáciu eRouška. Ak áno, pošle jej overovací SMS kód, ktorý užívateľovi umožní odoslať svoje anonymné identifikátory do ostatných eRoušek. Aplikácia ostatných užívateľov potom podľa epidemiologického modelu \\* čo najpresnejšie vyhodnotí, či títo používatelia prišli s nakazeným do kontaktu dosť blízko a na dostatočne dlhú dobu, aby bolo nutné ich prostredníctvom notifikácie v smartfóne varovať a smerovať k ďalšiemu vyšetreniu. eRouška 2.0 teda pomáha čo najefektívnejšie informovať ďalších potenciálnych prenášačov vírusu a urýchliť ich testovanie. Tým sa zníži počet infikovaných v spoločnosti aj riziko vašej infekcie.\n\n_\\*Bližšie ako 2 metre po dobu dlhšiu ako 7 min._"},{"question":"Čo ak niekto neprizná, že je chorý?","answer":"Aj to sa môže stať. Veríme však, že používatelia eRoušky chcú aktívne pomáhať so zastavením ďalšieho šírenia nákazy a aplikáciu využijú, aby mohli čo najskôr varovať osoby, s ktorými prišli do rizikového kontaktu. **Zámerné šírenie COVID-19 je v ČR považované za trestný čin.**"},{"question":"Má zmysel si eRoušku inštalovať, keď ju nebude používať dosť ľudí?","answer":"Práve preto by ste nemali váhať. Čím viac nás bude eRoušku používať, tým lepšie vytvoríme sieť, ktorá nás bude vzájomne chrániť a varovať pred rizikom. Pomôžte nám aj vy. Inštaláciou aplikácie sa môžete aktívne zapojiť do boja proti COVID-19. O aplikácii môžete povedať aj ľuďom vo svojom okolí, prípadne im s ich súhlasom pomôcť s inštaláciou."},{"question":"Ako sa eRouška líši od iných podobných aplikácií?","answer":"eRouška používa technológiu Bluetooth, ktorou je vybavená väčšina chytrých mobilných zariadení v Česku. Vďaka tomu funguje aj v budovách, podzemných parkoviskách alebo v metre. Aplikácia je navrhnutá tak, aby nezbierala informácie o vašej polohe (napr. cez GPS). Iba anonymne zisťuje, s ktorými ďalšími používateľmi aplikácie ste prišli do bližšieho kontaktu. Ako jediná v ČR môže oficiálne používať Apple / Google protokol, ktorý garantuje bezpečnosť aplikácie na platformách Android a iOS a znižuje spotrebu batérie."},{"question":"Čo je to chytrá karanténa? Akú v nej hrá úlohu eRouška?","answer":"Ide o súbor chytrých opatrení, ktoré chránia ľudí pred nákazou a ekonomiku pred kolapsom. Namiesto toho, aby bola v karanténe celá krajina, chytrá karanténa uľahčuje dohľadávanie, testovanie a izoláciu len chorých a z choroby dôvodne podozrivých ľudí.\n\nKonkrétne ide o systém riadenia v oblastiach predpovedi vývoja, podpory rozhodovania, aktívneho vyhľadávania kontaktov, testovania vr. riadenia kapacít odberových miest, laboratórií, riadenia karantén a súvisiaceho materiálneho a IT zabezpečenia na zvládnutie pandémie ochorenia COVID-19. Chytrá karanténa 2.0 je označením pre prechod projektu pod Ministerstvo zdravotníctva, ktoré bude vďaka novým nástrojom vedieť do budúcna kedykoľvek veľmi rýchlo reagovať na akékoľvek epidémie.\n\nAplikácia eRouška je jeden z nástrojov, ktoré vznikli na podporu chytrej karantény. Pomocou technológií - eRoušky - môžeme aktívne upozorniť rizikových jedincov, ktorí sa stretli s nakazeným, aby zvýšili hygienické opatrenia, zvážili dobrovoľnú karanténu, prípadne pri pociťovaní príznakov kontaktovali obvodného lekára. Vďaka týmto krokom veríme, že sa podarí zmierniť šírenie nákazy a ušetriť zdravotníctvo fatálneho preťaženia."}]},{"title":"Pokročilé informácie o eRouške","subtitle":"Zber a vyhodnotenie dát, význam upozornení","icon":"https://erouska.cz/img/faq/advanced.png","questions":[{"question":"Ako eRouška zaznamenáva a spracováva dáta o stretnutiach užívateľov?","answer":"Chytrý telefón s aplikáciou eRouška zaznamená cez Bluetooth LE anonymné identifikátory (ID) z iných zariadení s touto aplikáciou. Informáciu o \"stretnutí\" a jeho dĺžke ukladá do svojej vnútornej pamäte.\n\nV prípade, že budete mať pozitívny výsledok testu na COVID-19, príde vám jednorázový SMS kód. Zadáte ho do aplikácie ( [postup tu](https://erouska.cz/sk/sms) ), a tým umožníte odoslanie svojich anonymných identifikátorov na server, odkiaľ si ich stiahnu ostatné eRoušky na vyhodnotenie. Algoritmus v aplikácii každého užívateľa na základe epidemiologického modelu automatizovane vyhodnotí zozbierané dáta - porovná identifikátory so všetkými zaznamenanými stretnutiami. V prípade rizikového kontaktu zobrazí upozornenie, že hrozí nákaza kvôli stretnutiu s pozitívne testovanou osobou, a odporučí ďalší postup.\n\nPodrobné informácie o zbere a spracovaní dát, nájdete v [Informáciách o spracování osobných údajov v aplikácii eRouška 2.0](https://erouska.cz/sk/podminky-pouzivani)."},{"question":"Ako eRouška vyhodnocuje rizikové stretnutia?","answer":"Pôvodne epidemiológovia stanovili rizikový kontakt ako stretnutie, ktoré je vo vzdialenosti bližšie ako 2 metre po dobu aspoň 15 minút. Agresívnejšie šírenie nových mutácií koronavírusu však ukazuje, že k nákaze stačí aj kratší kontakt.\n\nNa vyššiu agresivitu šírenia mutácií reagujeme po konzultáciách postupu s Apple/Google a ostatnými krajinami EÚ postupným uvoľňovaním parametrov vyhodnotenia rizikového kontaktu. Zmeny parametrov prebiehajú už od začiatku marca, priebežne vyhodnocujeme ich vplyv na počet nájdených kontaktov. Zatiaľ upravujeme predovšetkým parametre zodpovedajúce dĺžke stretnutia, aktuálne ide približne o 7 minút.\n\nAplikácia eRouška sa snaží parametre vzdialenosti a času stretnutia vyhodnocovať čo najpresnejšie dostupnými technológiami. Vzdialenosť medzi používateľmi, respektíve ich telefónmi, sa odhaduje na základe sily signálu Bluetooth. Doba stretnutia sa posudzuje podľa meracích okien - telefón v niekoľkominútových intervaloch zisťuje, či sú v okolí iné telefóny s eRouškou.\n\nPresnosť merania vyššie uvedených parametrov má preto technické obmedzenia. Čím väčšia vzdialenosť a čím kratší kontakt, tým ťažšie je jeho presné vyhodnotenie. Uvoľňovanie parametrov vyhodnotenia tak zvyšuje pravdepodobnosť falošnej pozitivity a falošnej negativity.\n\nVyššiu presnosť merania možno dosiahnuť častejším vysielaním a zaznamenávaním ďalších zariadení s eRouškou v okolí. To sa bohužiaľ premietne do vyššej spotreby batérie. Algoritmy merania nastavujú Apple a Google vo svojom Exposure Notification protokole, na ktorom je eRouška postavená. Vývojári eRoušky môžu iba čiastočne upravovať niektoré parametre a vykonávať doplňujúce filtrovanie výsledkov. Bližšie vysvetlenie nájdete v sekcii [Spôsob vyhodnotenia](https://erouska.cz/sk/vyhodnoceni-rizika) ."},{"question":"Ako eRouška reaguje na nové varianty / mutácie vírusu?","answer":"Nové varianty vírusov, ktoré sa objavili od začiatku pandémie, priebežne skúmajú odborníci z pohľadu infekčnosti a vplyvov na zdravie. Preto sú vývojári eRoušky v úzkom kontakte s úradmi aj so spoločnosťami Apple a Google, ktoré poskytujú protokol pre vyhodnocovanie kontaktov s nákazou, aby v prípade zmeny rizika infekcie na základe nových poznatkov mohli upraviť algoritmus pre výpočet v aplikácii."},{"question":"Nemôže sa stať, že eRouška mylne vyhodnotí kontakt s inou osobou, ktorá je napríklad vedľa v aute na križovatke, za dverami alebo tenkou stenou?","answer":"Aplikácia eRouška funguje na princípe merania sily signálu medzi dvoma zariadeniami. Technicky preto nemožno vylúčiť nepresný odhad vzdialenosti a mylnú detekciu rizikového stretnutia. Technické obmedzenia, riziká používania aplikácie a postup vyhodnocovania stretnutia popisujeme na stránke [Spoľahlivosť vyhodnotenia rizikového kontaktu](https://erouska.cz/sk/vyhodnoceni-rizika) a v sekcii [Technické podmienky v Podmienkach spracovania](https://erouska.cz/sk/podminky-pouzivani#technicke) .\n\nNa vyhodnotenie kontaktu ako rizikového je okrem vzdialenosti (do 2 m) relevantná tiež dĺžka kontaktu (aspoň 15 minút) - napr. zastavenie na semaforoch na 1-2 minúty je teda nevýznamné."},{"question":"Zobrazilo sa mi upozornenie na možné stretnutie s nakazeným, ale eRouška po otvorení ukazuje, že nezaznamenala nikoho nakazeného v mojom okolí.","answer":"Ak vás prekvapilo oznámenie s červenou ikonkou vírusu a textom Oznámenie o možnom kontakte (Android) alebo Zaznamenanie kontaktov (iOS), nemusíte sa obávať. Ide len o systémové oznámenia Androidu a iOS, na ktoré nemá eRouška vplyv. Ako vyzerá skutočné oznámenie eRoušky nájdete [na tomto obrázku](https://www.facebook.com/eRouska/posts/180938253548928).\n\nAk by ste mali rizikový kontakt s nakazenou osobou, zistíte to priamo na hlavnej obrazovke eRoušky.\n\nAk ste predtým skutočne videli rizikové stretnutie priamo v eRouške, odkiaľ zmizlo, je možné, že aktualizáciou aplikácie pre Android došlo k úprave parametrov vyhodnotenia rizikového kontaktu a teraz už eRouška nepovažuje dané stretnutie za rizikové."},{"question":"Takže všetci budú vedieť, že mám COVID-19?","answer":"Nebudú. Ak ochoriete, vaša totožnosť je známa len a len pracovníkovi hygienickej stanice. Osoby, ktorým sa zobrazí upozornenie na možný rizikový kontakt, sa nedozvadia, kto ani kde ich mohol nakaziť. Aplikácia zobrazuje len všeobecnú informáciu o rizikovom stretnutí s nakazeným, deň stretnutia a ďalší odporúčaný postup. Vaša identita je ochránená."},{"question":"Bol som na testoch na COVID-19, ale stále mi neprišla SMS s overovacím kódom do eRoušky. Za ako dlho ju mám čakať?","answer":"Najprv by vám mal prísť výsledok testu v SMS od testovacieho laboratória. To výsledky súčasne odosiela do centrálneho informačného systému Ministerstva zdravotníctva. Po tom, čo sa informácie z informačného systému prepíšu do systému hygieny, sa odosielajú automatické SMS eRoušky s overovacími kódmi. Ak laboratórium nestihne odovzdať výsledky do informačného systému v rovnaký deň, môže vám SMS s overovacím kódom prísť až nasledujúci deň.\n\n**Skontrolujte, prosím, či info SMS eRoušky nezablokoval omylom systém ako spam**\n\n* **Android:** V aplikácii Správy kliknite na ikonu možností (tri bodky) a ďalej na Spam a zablokované konverzácie (prípadne na tri bodky → Nastavenia → SIM → Spam). Ak je tu SMS eRoušky, kliknite na Nie je spam.\n\n**Ak vám SMS eRoušky neprišla ani nasledujúci deň** potom, čo vám prišla SMS s pozitívnymi výsledkami z laboratória, pošlite nám na [info@erouska.cz](mailto:info@erouska.cz?subject=) žiadosť o nový SMS kód. Uveďte prosím tieto informácie:\n\n* celé meno, ktoré ste uviedli na žiadanke o test,\n* tel. číslo, ktoré ste uviedli na žiadanke o test,\n* dátum výsledku testov,\n* odberové miesto / laboratórium, z ktorého vám prišli výsledky testu,\n* typ testu (PCR / antigén)."},{"question":"Príde mi SMS s kódom do eRoušky, keď mám pozitívny antigénny test na COVID-19?","answer":"Pokiaľ máte [pozitívny výsledok antigénneho testu a máte príznaky COVID-19](https://drive.google.com/file/d/1HPwtltNRuzmowjZgE_c1u-a8m3W89T-p/view) (uvádzate v žiadanke), príde vám SMS s kódom do eRoušky rovnako ako v prípade pozitívneho PCR testu.\n\nV prípade, že máte [pozitívny výsledok antigénneho testu, ale nepociťujete príznaky](https://covid.gov.cz/situace/antigenni-testovani/interpretace-vysledku-antigenniho-testu-jak-se-zachovat-po-obdrzeni) , pôjdete na PCR test. Na základe pozitívneho výsledku PCR testu by vám prišla SMS s kódom do eRoušky. V prípade negatívneho PCR testu neodosielate dáta z eRoušky."},{"question":"V ktorých štátoch EÚ môžem eRoušku používať? Ako funguje spolupráca so zahraničnými aplikáciami?","answer":"Európska komisia vyvinula centrálnu bránu EFGS pre bezpečnú výmenu informácií (\"brána pre zabezpečenie interoperability\"). Táto brána umožňuje aplikácii eRouška prijímať a odovzdávať si varovania so zapojenými trasovacím aplikáciami používanými v jednotlivých štátoch EÚ. Podrobné informácie o bráne sú k dispozícii na webe Európskej komisie v dokumente [Koronavírus: brána EÚ na zabezpečenie interoperability aplikácií pre vysledovanie kontaktov a varovaní - otázky a odpovede](https://ec.europa.eu/commission/presscorner/detail/cs/qanda_20_1905#gateway) .\n\nAk chcete zobraziť ďalšie informácie o zapojených štátoch a nastaveniach, kliknite na hlavnej obrazovke aplikácie na Cesty do zahraničia, kde môžete funkciu zapnúť/vypnúť a nižšie vidieť zoznam zapojených štátov. Oficiálny aktuálny zoznam zapojených štátov EÚ nájdete [na webe Európskej komisie.](https://ec.europa.eu/health/sites/health/files/ehealth/docs/gateway_jointcontrollers_en.pdf)"},{"question":"Používa eRouška Apple/Google API?","answer":"Nová verzia - eRouška 2.0 pre Android aj iOS už používa Apple / Google Exposure Notification API. Nezabudnite preto prosím aplikáciu eRouška aktualizovať v [Google Play (Android)](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska) alebo [App Store (iOS)](https://apps.apple.com/cz/app/erou%C5%A1ka/id1509210215) a znovu aktivovať."},{"question":"Čo je to Apple/Google Exposure Notification API (protokol)?","answer":"Spoločnosti Apple a Google spoločne vyvíjajú technológie, vďaka ktorým môžu verejné zdravotné inštitúcie (u nás MZČR) vyvíjať národné aplikácie na trasovanie rizikových kontaktov s osobami s ochorením COVID-19.\n\nKonkrétne ide napríklad o rozhranie, ktoré daným aplikáciám (u nás eRouška) umožňuje spoľahlivo pristupovať k systémovým prostriedkom Android a Apple zariadení za účelom zaznamenanie stretnutia - najmä využívať Bluetooth LE na overenie kontaktu a bežať na pozadí bez potreby ďalších nastavení aplikácie alebo operačného systému. Šifrovaný komunikačný protokol potom zaisťuje zabezpečený prenos aktuálnych zoznamov identifikátorov nakazených užívateľov medzi trasovacími aplikáciami za účelom vyhodnotenia rizikového stretnutia a včasného zobrazenie notifikácie.\n\nBližšie informácie nájdete priamo na webových stránkach spoločností [Apple (anglicky)](https://www.apple.com/covid19/contacttracing) a [Google (česky)](https://www.google.com/covid19/exposurenotifications/)."},{"question":"Kde v eRouške nájdem informácie o zaznamenaných stretnutiach?","answer":"Aplikácia eRouška 2.0 je postavená na Apple / Google Exposure Notification protokole, ktorý zaisťuje zabezpečené zaznamenávanie a vyhodnotenie stretnutí. Z bezpečnostných dôvodov však neposkytuje aplikácii priamy prístup k dátam o zaznamenaných stretnutiach.\n\nČi aplikácia funguje správne, sa dozviete na jej hlavnej obrazovke. Informácie o všetkých zaznamenaných stretnutiach s nakazenými užívateľmi sú uložené na úrovni systému. Tieto stretnutia však nemusela byť rizikové. Nájdete ich v Nastaveniach:\n\n* Android: Nastavenia → Google → Oznámenia o možnom kontakte COVID-19"},{"question":"Čo znamenajú jednotlivé záznamy v Nastavenia → Kontrola kontaktov?","answer":"Protokolovanie kontaktov s nákazou ukazuje, koľko kľúčov od nakazených užívateľov sa stiahlo do vášho telefónu zo servera a koľko z nich zodpovedá kľúčom, ktoré váš telefón zaznamenal.\n\n**Kontroly kontaktov:** Tu sú dáta a časy jednotlivých stiahnutí kľúčov zo servera za posledné dva týždne. Pre ďalšiu kontrolu je potreba otvoriť jednotlivé záznamy.\n\n**Android: Počet kľúčov:** Ide o počet kľúčov od pozitívne testovaných používateľov. Tieto kľúče si váš telefón v danom dátume stiahol pre kontrolu možných kontaktov s nakazenými užívateľmi.\n\n**Android: Počet výsledkov** Ide o počet zhôd medzi stiahnutými kľúčmi a tými, ktoré zaznamenal váš telefón vo svojom okolí. Ak je číslo vyššie ako 0, tak ste sa stretli s nakazeným užívateľom. To však ešte neznamená, že nutne muselo ísť o rizikové stretnutie.\n\n**Časová značka:** Dátum a čas, kedy ku kontrole došlo. Nejde však o čas rizikového stretnutie (ten nie je k dispozícii kvôli ochrane súkromia užívateľov).\n\n**Zdroj údajov:** Aplikácia, ktorá zaisťuje kontrolu a vyhodnotenie rizikových stretnutí.\n\n**Hašovací kód:** Kontrolný súčet súboru obsahujúceho stiahnutie kľúčov nakazených užívateľov. Kým sa obsah súboru nezmení, tak sa nezmení ani kontrolný súčet počas príslušného dvojtýždňového obdobia."},{"question":"Ako zistím, že eRouška funguje na pozadí? Prečo na Androide nevidím ikonku eRoušky v záhlaví?","answer":"Notifikačnú ikonu sme odstránili, pretože zaznamenávanie a vyhodnocovanie stretnutia už zabezpečuje priamo Apple / Google protokol na úrovni operačného systému. Samotná aplikácia eRouška sa potom stará o sťahovanie kľúčov (identifikátory) nakazených užívateľov 1-2× denne a nemusí bežať permanentne na pozadí.\n\nŽe je aplikácia aktuálna, zistíte na hlavnej obrazovke. Stiahnutie kľúčov nakazených potom môžete overiť v Nastaveniach telefónu:\n\n* **Android:** Nastavenia → Google → Oznámenia o možnom kontakte COVID-19 → kliknúť na tri bodky vpravo hore → Kontrola kontaktu"},{"question":"Prečo sa líši dátum a čas \"Poslednej aktualizácie dát\" na hlavnej obrazovke eRoušky a údaje a časy \"Kontroly kontaktov\", ktoré vidím v Nastaveniach telefónu?","answer":"Aplikácia eRouška ukazuje na hlavnej obrazovke dátum a čas posledného úspešného pokusu o kontrolu nových kľúčov (identifikátory) nakazených používateľov na serveri. V Nastaveniach telefónu sa zobrazujú iba dátumy a časy, kedy boli naposledy nové kľúče nájdené a vyhodnotené v telefóne.\n\nKľúče nakazených užívateľov sa do telefónu sťahujú 1-2× denne."},{"question":"Prečo v eRouške nevidím konkrétny čas rizikového stretnutia?","answer":"Aplikácia eRouška je postavená na Apple / Google Exposure Notification protokolu, ktorý zaisťuje zaznamenávanie a vyhodnocovanie stretnutia na úrovni operačného systému. Apple / Google protokol poskytuje eRouške prístup len k dátumu rizikového stretnutia, bližšie informácie nie sú k dispozícii z dôvodu čo najvyššej ochrany súkromia používateľov."},{"question":"Prečo v Kontrole kontaktov v Nastaveniach telefónu vidím \"Nájdené kľúče: 0\"?","answer":"Znamená to, že Apple / Google API vo vašom telefóne nevyhodnotilo v danom dátume žiadne stretnutie s nakazeným užívateľom. Inak povedané, stiahnuté kľúče nakazených užívateľov sa v danom dni nezhodujú so žiadnym zo zaznamenaných kľúčov vo vašom zariadení."},{"question":"Prečo v Nastaveniach telefónu v sekcii Kontrola kontaktu vidím u konkrétneho dátumu nájdené kľúče, ale eRouška mi nezobrazuje žiadne rizikové stretnutia?","answer":"Počet nájdených kľúčov predstavuje počet zaznamenaných stretnutí s nakazenými užívateľmi. Neznamená to však nutne, že tieto stretnutia bola rizikové. Ak stretnutie s nakazeným užívateľom nebolo rizikové, tak na neho eRouška neupozorňuje. Preto v aplikácii nie je vidieť."},{"question":"Zobrazilo sa mi upozornenie / notifikácia na rizikové stretnutie, ku ktorému došlo pred niekoľkými dňami. Prečo ma eRouška upozornila až teraz?","answer":"Aplikácia eRouška nemôže vyhodnotiť riziko stretnutia a zobraziť upozornenie skôr, než užívateľ s pozitívnym testom na COVID-19 odošle dáta zo svojej eRoušky. Ak sa vám dnes zobrazila notifikácia, ktorá upozorňuje na rizikové stretnutie s nakazeným pred niekoľkými dňami, znamená to, že nakazený odoslal dáta zo svojej eRoušky len včera. Vaša eRouška si ich dnes stiahla a vyhodnotila."},{"question":"Zobrazilo sa mi upozornenie / notifikácia na rizikové stretnutie s nakazeným, ale na obrazovke Rizikové stretnutia vidím viac záznamov s rôznymi dátami.","answer":"Aplikácia eRouška vyhodnocuje rizikové stretnutia až po tom, čo používateľ s pozitívnym testom na COVID-19 odošle dáta zo svojej eRoušky. Ak sa vám zobrazila iba jedna notifikácia na rizikový kontakt, ale v Rizikových stretnutiach máte viac záznamov, znamená to, že ste sa s nakazeným stretli v predchádzajúcich dňoch viackrát alebo že v jeden deň odoslalo dáta zo svojho telefónu viac nakazených užívateľov súčasne."},{"question":"Bude eRouška fungovať ako \"covid pas\" pre zobrazovanie výsledkov testov a preukázanie očkovania?","answer":"Anonymný charakter aplikácie eRouška ju neumožňuje kombinovať s tzv. Digitálnym zeleným certifikátom (Digital Green Certificate), ktorý pracuje s overenou identitou a osobnými údajmi užívateľa - výsledkami testov alebo vykonaným očkovaním proti COVID-19."},{"question":"Publikuje eRouška nejaké štatistiky o používaní aplikácie?","answer":"Štatistiky publikujeme vo formáte JSON na adrese [stats.erouska.cz](https://stats.erouska.cz) . Dáta majú nasledujúcu štruktúru:\n\n{\\\\n \"data\": {\\\\n \"modified\": 1608696001,\\\\n \"date\": \"20201223\",\\\\n \"activations\\_yesterday\": 2042,\\\\n \"activations\\_total\": 1475571,\\\\n \"key\\_publishers\\_yesterday\": 718,\\\\n \"key\\_publishers\\_total\": 39175,\\\\n \"notifications\\_yesterday\": 631,\\\\n \"notifications\\_total\": 197298\\\\n }\\\\n}\n\n* **modified** = Unix timestamp vygenerovaných štatistík,\n* **date** = dátum vytvorenia reportu,\n* **activations\\_yesterday** \\= počet aktivácií za predchádzajúci kalendárny deň,\n* **activations\\_total** = celkový počet aktivácií eRoušky,\n* **key\\_publishers\\_yesterday** \\= počet nakazených ľudí, ktorí zadali v predchádzajúcom dni do eRoušky kód pre odoslanie dát,\n* **key\\_publishers\\_total** \\= celkový počet nakazených ľudí, ktorí zadali do eRoušky kód pre odoslanie dát,\n* **notifications\\_yesterday** \\= počet ľudí, ktorým sa za predchádzajúci deň zobrazilo upozornenie na rizikové stretnutie,\n* **notifications\\_total** = celkový počet ľudí, ktorým sa zobrazilo upozornenie na rizikové stretnutie.\n\nZavolanie bez parametra zobrazí informácie k aktuálnemu dátumu. Zavolanie s parametrom `date` umožní získať dáta pre ľubovoľný deň od 23. 10. 2020 (za všetky predchádzajúce dni máme iba agregované dáta). Pomocou parametra `date` s hodnotou `all` možno získať všetky dáta, ktoré rozhranie poskytuje.\n\n* dnešné dáta: [`https://stats.erouska.cz/`](https://stats.erouska.cz/)\n* dáta z 1. 12. 2020: [`https://stats.erouska.cz/?date=2020-12-01`](https://stats.erouska.cz/?date=2020-12-01)\n* dáta za všetky dni: [`https://stats.erouska.cz/?date=all`](https://stats.erouska.cz/?date=all)"}]},{"title":"Zabezpečenie dát","subtitle":"Osobné údaje a ochrana súkromia","icon":"https://erouska.cz/img/faq/security.png","questions":[{"question":"Ako zistím, ktorá všetky dáta presne aplikácie ukladá a odosiela?","answer":"Aplikácia eRouška používa od verzie 2.0 tzv. Apple / Google Exposure Notification API, ktoré sprostredkúva zber dát a odovzdávanie identifikátorov medzi užívateľmi. Aplikácia kvôli tomu už nemá priamy prístup k nameraným údajom, ktoré by mohla používateľom zobraziť, ako to bolo vo verzii 1.0.\n\nAplikácia každý deň sťahuje zoznam identifikátorov od používateľov, ktorým test potvrdil nákazu. Údaje s identifikátormi na spracovanie môže ostatným odoslať iba jediný užívateľ, a to až po zadaní jednorázového SMS kódu, ktorý získa v prípade pozitívneho testu na nákazu ochorením COVID-19 automaticky, prípadne na vyžiadanie.\n\nAplikácia ďalej odosiela na server anonymnú štatistickú / agregovanú informáciu, že došlo k notifikácii rizikového kontaktu. Táto informácia nie je viazaná na žiadny identifikátor užívateľa ani jeho mobilu a slúži iba na výpočet štatistických informácií o účinnosti systému eRouška.\n\nAby sme zaistili funkčnosť eRoušky, pracujeme tiež s údajmi o fungovaní (napr. záznamy o pádoch aplikácie a používaní aplikácie) na telefóne a používame k tomu štandardné nástroje (Firebase Crashlytics a Google Analytics) od spoločnosti Google. Telemetrické údaje odosielané aplikáciou do týchto služieb používajú identifikátory vašej osoby alebo telefónu.\n\nPodrobné informácie o zhromažďovaní a spracovaní údajov nájdete v [Informáciách o spracovaní osobných údajov v aplikácii eRouška 2.0](https://erouska.cz/sk/podminky-pouzivani) ."},{"question":"Prenesú sa dáta z eRoušky po obnove telefónu zo zálohy alebo pri prechode na nový telefón?","answer":"Z dôvodu ochrany súkromia užívateľov operačné systémy Android ani iOS nezálohujú dáta z Exposure Notification. Pri preinštalácii telefónu alebo prechode na iné zariadenie teda nemožno preniesť ani obnoviť zaznamenané a vygenerované identifikátory.\n\nAk chcete získať informáciu o možnom rizikovom stretnutí, odporúčame sledovať stav starého telefónu ešte po dobu 14 dní po inštalácii eRoušky na nové zariadenie. Pritom je potrebné:\n\n* zabezpečiť, aby vaše zariadenie bolo zapnuté a pripojené k internetu, aby si mohlo sťahovať dáta na vyhodnotenie,\n* ideálne aspoň raz denne kontrolovať staršie zariadenia, teda otvoriť eRoušku.\n* Ak by ste počas týchto 14 dní mali pozitívny test, zadajte prosím kód do nového zariadenia a na získanie kódu pre staršie telefóny kontaktujte [info@erouska.cz](mailto:info@erouska.cz) .\n\nPo 14 dňoch už váš starý telefón nebude obsahovať žiadne dáta pre vyhodnotenie rizikového stretnutia. Môžete z neho preto odstrániť eRoušku a ponechať len v novom zariadení."},{"question":"Čo keď mi telefón ukradnú alebo ho stratím?","answer":"V aplikácii eRouška sú lokálne uložené len anonymné informácie o ďalších zariadeniach, ktoré eRouška zaznamenala vo svojom okolí. Žiadne riziko to neprináša pre vás ani pre majiteľov takto zaznamenaných zariadení. Iné aplikácie, ktoré bežne využívate, môžu vo vašom telefóne pravdepodobne ukladať oveľa citlivejšie údaje. Majte preto svoj telefón chránený kódom alebo biometricky (odtlačkom prsta alebo tvárou)."},{"question":"Je aplikácia eRouška v súlade s GDPR?","answer":"Celý systém aplikácie eRouška, vrátane webových stránok, je navrhnutý plne v súlade s GDPR aj zákonom o spracovaní osobných údajov.\n\nTu nájdete bližšie informácie:\n\n* [Informácie o spracovaní osobných údajov v aplikácii eRouška 2.0](https://erouska.cz/sk/podminky-pouzivani)\n* [Audit zdrojového kódu aplikácie](https://erouska.cz/sk/audit-kod)"},{"question":"Ako užívateľa chránite pred zneužitím dát?","answer":"Užívateľa chránime predovšetkým minimálnym rozsahom zaznamenávaných dát a ich ukladaním priamo do zariadenia. Dáta používateľov sa bez ich vedomia a súhlasu nikam neposielajú ani nespracovávajú. Do telefónov sa zaznamenávajú anonymné identifikátory (ID) zariadenia s nainštalovanou aplikáciou eRouška a informácie o čase, dĺžke a sile ich Bluetooth signálu. Dáta sa spracovávajú automatizovane na serveri iba po odsúhlasenom odoslaní užívateľom."},{"question":"Dohliada na zabezpečenie dát z aplikácie nejaká nezávislá organizácia?","answer":"Celý systém aplikácie eRouška, vrátane podporných webových stránok, je navrhnutý plne v súlade s GDPR. Kód aplikácie je ako open-source voľne k dispozícii ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)) a prešiel [auditom nezávislých vzdelávacích inštitúcií](https://erouska.cz/sk/audit-kod).\n\nĎalšie informácie nájdete v [Informáciách o spracovaní osobných údajov v rámci aplikácie eRouška 2.0](https://erouska.cz/sk/podminky-pouzivani) ."},{"question":"Môže ma niekto pomocou aplikácie eRouška sledovať?","answer":"Nie. Aplikácia nezbiera údaje o vašej polohe a k uloženým dátam máte prístup iba vy. Samotné údaje, ktoré aplikácia po vašom súhlase odosiela, nemôžu byť použité k vášmu sledovaniu. Každá eRouška navyše mení z dôvodu bezpečnosti a ochrany súkromia užívateľov pravidelne svoje vysielané ID, aby nebolo ľahké ho odchytiť a nejako zneužiť."},{"question":"Je možné overiť, že aplikácia nezaznamenáva moju polohu?","answer":"Áno. Aplikácia eRouška je publikovaná s otvoreným zdrojovým kódom ([Android](https://github.com/covid19cz/erouska-android), [iOS](https://github.com/covid19cz/erouska-ios)), takže znalý človek môže ľahko overiť, že údaje o polohe aplikácie naozaj nezbiera.\n\nZdrojové kódy aplikácie eRouška [preverujú nezávislé autority](https://erouska.cz/sk/audit-kod) , ktoré kontrolujú, že aplikácia:\n\n* nesleduje polohu\n* sama automaticky dáta s identifikátormi nikam neposiela"},{"question":"Je možné identifikátor aplikácie (ID) používateľa spárovať s konkrétnou osobou?","answer":"Aplikácia eRouška 2.0 [nepracuje s osobnými údajmi](https://erouska.cz/sk/podminky-pouzivani) . Vygenerované anonymné ID eRoušky sa medzi aplikáciami a servermi prenášajú výlučne po chránenom komunikačným protokolom, ktorý spoločne vyvinuli spoločnosti Apple a Google. K identifikátorom nemajú prístup ani používatelia, ani vývojári, ani iná autorita."},{"question":"Kto má (môže mať) prístup k mojim údajom?","answer":"K zaznamenaným údajom o stretnutí nemá v eRouške 2.0 prístup užívateľ, ani nikto iný. Dôvodom je, že aktuálna verzia aplikácie používa Apple / Google protokol, ktorý k zaznamenaným dátam neposkytuje prístup ani užívateľom, ani vývojárom a správcom. Dáta (anonymné identifikátory nakazených) sa prenášajú na server a odtiaľ do ostatných eRoušiek šifrovane a vyhodnocujú sa až na koncových zariadeniach automatizovane za účelom zobrazenia prípadných upozornení o rizikovom kontakte. Zo servera aj zo zariadení sa dáta mažú automaticky po 14 dňoch."},{"question":"Ako dlho do minulosti sú dáta k dispozícii? Kedy a ako prebieha ich mazanie?","answer":"V aplikácii eRouška sa uchovávajú dáta zaznamenané za posledných 14 dní, staršie záznamy sa automaticky odmazávajú. Pokiaľ po pozitívnom teste na COVID-19 zadáte overovací kód do eRoušky a odošlete identifikátory z telefónu na server, zostanú tu 14 dní a potom dôjde k ich zmazaniu. Dáta môžete zo svojho zariadenia zmazať v Nastaveniach Oznámenie o kontaktoch s nákazou/Kontakty s nákazou/Exposure Notification - konkrétny postup záleží na type a verzii operačného systému.\n\n* **Android:** Na zariadeniach so systémom Android v súčasnom nastavení dôjde pri odinštalovaní aplikácie eRouška k zmazaniu zaznamenaných kľúčov (identifikátorov).\n\nInformácie o správcoch a spracovateľoch údajov nájdete v [Informáciách o spracovaní osobných údajov v rámci aplikácie eRouška](https://erouska.cz/sk/podminky-pouzivani) ."},{"question":"Môžem si používanie aplikácie kedykoľvek rozmyslieť? Môžem zmazať dáta, ktoré odošlem na spracovanie?","answer":"Aplikáciu môžete kedykoľvek odinštalovať. Vygenerované a zaznamenané kľúče (záznamy stretnutí) môžete zo svojho zariadenia zmazať v Nastaveniach Oznámenie o kontaktoch s nákazou/Kontakty s nákazou/Exposure Notification - konkrétny postup záleží na type a verzii operačného systému. Údaje odoslané na server sa zmažú automaticky po 14 dňoch.\n\n* **Android:** Na zariadeniach so systémom Android v súčasnom nastavení dôjde pri odinštalovaní aplikácie eRouška tiež k zmazaniu zaznamenaných kľúčov (identifikátorov)."},{"question":"Čo sa deje s dátami po vymazaní aplikácie z mobilu?","answer":"**Android:** Keď zmažete aplikáciu z telefónu alebo tabletu s OS Android, dôjde tiež k zmazaniu zaznamenaných kľúčov (identifikátorov).\n\nAk aplikáciu znova nainštalujete, začnú sa zbierať a do telefónu ukladať úplne nové dáta."},{"question":"Kde nájdem zdrojový kód aplikácie?","answer":"Aplikácia eRouška je publikovaná s otvoreným zdrojovým kódom. Nájdete ho na GitHube ([Android](https://github.com/covid19cz/erouska-android) , [iOS](https://github.com/covid19cz/erouska-ios))."}]},{"title":"Inštalácia a kompatibilita","subtitle":"Podporované telefóny a možnosti inštalácie","icon":"https://erouska.cz/img/faq/compatibility.png","questions":[{"question":"Na akých telefónoch aplikácia eRouška funguje? Prečo sú nároky vyššie než v eRouške 2.0?","answer":"Na inštaláciu aplikácie eRouška potrebujete mobilné zariadenia s týmito parametrami:\n\n* iPhone s iOS verzie 13.5 a vyššej\n* OS Android verzie 6.0 a vyššej s Google Play Services (nemá len malé percento telefónov Huawei a niektoré telefony s vlastnou ROM)\n* Bluetooth LE (Low Energy)\n\nAk vaše mobilné zariadenie nie je s eRouškou kompatibilné (nemá potrebné technológie / funkcie), Google Play ani Apple App Store vám neumožní aplikáciu nainštalovať. Aplikácia je dostupná len pre niektoré tablety Android. Nie je k dispozícii pre iPad.\n\nMinimálne požiadavky na kompatibilitu stanovujú spoločnosti Apple a Google. Vychádzajú z požiadaviek Apple / Google Exposure Notification protokolu, ktorý nová eRouška používa. Na Apple / Google protokol sme sa rozhodli prejsť kvôli zabezpečeniu fungovania eRoušky na iPhonoch a kvôli umožneniu cezhraničnej kompatibility s podobnými aplikáciami ďalších európskych štátov."},{"question":"Odkiaľ si môžem eRoušku bezpečne stiahnuť a nainštalovať?","answer":"Aplikácia eRouška je dostupná iba pre:\n\n* Android: [v Obchode Google Play](https://play.google.com/store/apps/details?id=cz.covid19cz.erouska) ."},{"question":"Je možné aplikáciu stiahnuť aj inak ako z oficiálnych aplikačných obchodov? Je k dispozícii balíček apk?","answer":"Aplikácia eRouška 2.0 používa Apple/Google protokol pre zaznamenávanie stretnutia a prenos dát. Preto je tiež závislá na systémových službách, bez ktorých nebude fungovať, a nie je ju teda možné inštalovať z iných ako oficiálnych aplikačných obchodov. Aplikačné obchody spoľahlivo vyfiltrujú podporované zariadenia.\n\nVývojári eRoušky nikdy neposkytujú užívateľom odkazy na inštalačné balíčky APK alebo samotné inštalačné súbory mimo oficiálne obchody s aplikáciami. V aplikáciách nainštalovaných neoficiálnou cestou by sme nedokázali garantovať ich zabezpečenie, zabezpečiť ich aktualizáciu ani rovnakú úroveň užívateľskej podpory. Preto prosím nikdy neinštalujte eRoušku z .apk súborov, ktoré ste našli niekde na internete alebo vám ich niekto poslal e-mailom. Môžu obsahovať vírusy, trójske kone alebo iný škodlivý obsah."},{"question":"Môžem používať eRoušku spolu s inou oficiálnou trasovacou aplikáciou iného štátu (napr. Corona-Warn-App)?","answer":"Ak sa zdržiavate primárne na území ČR, odporúčame používať eRoušku aj pre cesty do zahraničia. [Aplikácia teraz spolupracuje aj s ďalšími štátmi EÚ](https://erouska.cz/sk/caste-dotazy#efgs) .\n\nV ostatných prípadoch môžete mať v telefóne nainštalované viaceré trasovacie aplikácie typu eRouška, ako sú Corona-Warn-App, Stopp Corona App, Imunni, Stop Covid ProteGO Safe a ďalšie. Ak však tiež používajú Apple/Google Exposure Notification protokol (platí najmä pre susedné štáty ČR), môžete mať v jeden okamih aktívnu iba jednu z týchto aplikácií. Ostatné je potreba pozastaviť. K zaznamenávaniu stretnutí v Apple/Google protokole totiž môže mať prístup vždy len jedna aplikácia."},{"question":"Funguje eRouška, keď mám na mobile úsporný režim/režim nízkej spotreby/šetrič batérie?","answer":"**Android:** Šetriče/sporiče batérie na zariadeniach s OS Android môžu do značnej miery obmedzovať beh aplikácií na pozadí. Hoci systém oznámení o možnom kontakte s ochorením COVID-19 naďalej funguje, nemusí dochádzať k vyhodnocovaniu rizikových kontaktov, a teda k prípadnému zobrazeniu notifikácií."},{"question":"Bude možné používať eRoušku aj na smart hodinkách či náramkoch?","answer":"Rozšírenie je teoreticky do budúcnosti možné. Zatiaľ ho ale nezvažujeme najmä preto, že by smart hodinky či náramok museli podporovať Bluetooth LE (Low Energy) a najmä Apple / Google protokol. Základný Bluetooth bez LE sám o sebe iba zaisťuje prenos dát, ale neumožňuje meranie intenzity stretnutí užívateľov na základe sily signálu."}]}]v2_appleIgnoreAndroidtruev2_howItWorksUITitleAko eRouška fungujev2_validationTokenExpirationLeewayMinutes15v2_efgsDays14v2_keyExportNonTravellerUrls[{"country":"CZ","url":"https://cdn.erouska.cz/erouska/index.txt"}]v2_keyExportEuTravellerUrls[{"country":"CZ","url":"https://cdn.erouska.cz/erouska/index.txt"},{"country":"IT","url":"https://cdn.erouska.cz/efgs/it/index.txt"},{"country":"LV","url":"https://cdn.erouska.cz/efgs/lv/index.txt"}]v2_howItWorksEvalContenteRoušky rizikové stretnutia vyhodnotia v prípade, že boli s nakazeným v kontakte na vzdialenosť bližšiu ako 2 metre a počas aspoň 7 minút od okamihu, keď bol podľa dostupných informácií nákazlivým.v2_ragnarokHeadlinePomoc eRoušky v boji s pandemií je u koncev2_ragnarokMoreInfohttps://erouska.czv2_ragnarokBodyDěkujeme, že jste se stali součástí sítě 1,7 miliónu lidí. Pomohli jste ochránit své okolí rozesláním více než 400 tisíc upozornění na riziková setkání s nakaženými. Oznámení o rizikovém kontaktu vypneme a váš telefon již nebude hledat rizikové kontakty. eRouška je neaktivní a v případě jejího obnovení vám pošle oznámení.v2_ragnaroktrue
================================================
FILE: app/src/prod/google-services.json
================================================
{
"project_info": {
"project_number": "941144972907",
"firebase_url": "https://daring-leaf-272223.firebaseio.com",
"project_id": "daring-leaf-272223",
"storage_bucket": "daring-leaf-272223.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:941144972907:android:937903c1584d72a673db2e",
"android_client_info": {
"package_name": "cz.covid19cz.erouska"
}
},
"oauth_client": [
{
"client_id": "941144972907-n11bsjourafod10sin5fomirse6crnfa.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "cz.covid19cz.erouska",
"certificate_hash": "ef76fa2a3481eccda91db9777d375d91456898d2"
}
},
{
"client_id": "941144972907-r2kuhq8prsgdi48ldkhg5oqv6hiianuu.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "cz.covid19cz.erouska",
"certificate_hash": "fc5858910faaf9e981cba216e0180a92655f6db7"
}
},
{
"client_id": "941144972907-32illceaaugnl3dhe54kuv5d75e2unk8.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDRe2nIq_Z0sbEp10Sh52v6TkBUdmDzHoA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "941144972907-32illceaaugnl3dhe54kuv5d75e2unk8.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}
================================================
FILE: app/src/prod/res/values/controls.xml
================================================
erouskahttps://europe-west1-daring-leaf-272223.cloudfunctions.net/https://apiserver-jyvw4xgota-ew.a.run.app/https://europe-west1-daring-leaf-272223.cloudfunctions.netcz.covid19cz.erouska.fileprovider
================================================
FILE: app/src/test/kotlin/com/covid19cz/bt_tracing/ExampleUnitTest.kt
================================================
package cz.covid19cz.bt_tracing
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
================================================
FILE: arch/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 21
targetSdkVersion 30
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
dataBinding = true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.appcompat:appcompat:1.3.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.1"
// Navigation Components
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
implementation "androidx.navigation:navigation-ui-ktx:2.3.5"
// Material Components
implementation "com.google.android.material:material:1.4.0"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'com.github.bumptech.glide:compiler:4.12.0'
// Koin for Android
implementation 'org.koin:koin-android:2.0.1'
implementation 'org.koin:koin-androidx-scope:2.0.1'
implementation 'org.koin:koin-androidx-viewmodel:2.0.1'
}
================================================
FILE: arch/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel type.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true
================================================
FILE: arch/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: arch/src/main/AndroidManifest.xml
================================================
================================================
FILE: arch/src/main/java/arch/BaseApp.kt
================================================
package arch
import androidx.multidex.MultiDexApplication
open class BaseApp : MultiDexApplication() {
companion object {
lateinit var instance: BaseApp private set
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
================================================
FILE: arch/src/main/java/arch/adapter/BaseRecyclerAdapter.kt
================================================
package arch.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingUtil
import androidx.databinding.ObservableList
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import arch.viewmodel.BaseArchViewModel
import cz.stepansonsky.mvvm.BR
/**
* Created by Stepan on 9.11.2016.
*/
abstract class BaseRecyclerAdapter : RecyclerView.Adapter.BaseMvvmRecyclerViewHolder> {
private var viewModel: BaseArchViewModel? = null
private var items: ObservableList? = null
var lifecycleOwner: LifecycleOwner? = null
var parentItem: Any? = null
private var onListChangedCallback: ObservableList.OnListChangedCallback>? = null
@LayoutRes
protected abstract fun getLayoutId(itemType: Int): Int
constructor(items: ObservableList) {
this.items = items
initOnListChangedListener()
}
constructor(items: ObservableList, viewModel: BaseArchViewModel?) {
this.viewModel = viewModel
this.items = items
initOnListChangedListener()
}
private fun initOnListChangedListener() {
onListChangedCallback = object : ObservableList.OnListChangedCallback>() {
override fun onChanged(sender: ObservableList) {
notifyDataSetChanged()
}
override fun onItemRangeChanged(sender: ObservableList, positionStart: Int, itemCount: Int) {
notifyItemRangeChanged(positionStart, itemCount)
}
override fun onItemRangeInserted(sender: ObservableList, positionStart: Int, itemCount: Int) {
notifyItemRangeInserted(positionStart, itemCount)
}
override fun onItemRangeMoved(
sender: ObservableList,
fromPosition: Int,
toPosition: Int,
itemCount: Int
) {
notifyDataSetChanged()
}
override fun onItemRangeRemoved(sender: ObservableList, positionStart: Int, itemCount: Int) {
notifyItemRangeRemoved(positionStart, itemCount)
}
}
items!!.addOnListChangedCallback(onListChangedCallback)
}
private fun getViewHolderBinding(parent: ViewGroup, @LayoutRes itemLayoutId: Int): ViewDataBinding {
return DataBindingUtil.inflate(LayoutInflater.from(parent.context), itemLayoutId, parent, false)
}
override fun onBindViewHolder(holder: BaseMvvmRecyclerViewHolder, position: Int) {
val item = items!![position]
holder.bind(item, holder.binder)
holder.binder!!.executePendingBindings()
}
fun getItem(position: Int): T {
return items!![position]
}
override fun getItemCount(): Int {
return if (items != null) {
items!!.size
} else 0
}
fun getItems(): List? {
return items
}
fun setItems(items: ObservableList?) {
if (items != null && items == this.items) {
//notifyDataSetChanged();
} else {
this.items = items
initOnListChangedListener()
notifyDataSetChanged()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseMvvmRecyclerViewHolder {
return BaseMvvmRecyclerViewHolder(getViewHolderBinding(parent, getLayoutId(viewType)))
}
inner class BaseMvvmRecyclerViewHolder(v: ViewDataBinding) : RecyclerView.ViewHolder(v.root) {
val binder: ViewDataBinding? = DataBindingUtil.bind(v.root)
fun bind(item: T, binder: ViewDataBinding?) {
binder!!.setVariable(BR.vm, viewModel)
binder.setVariable(BR.item, item)
if (lifecycleOwner != null) {
binder.setVariable(BR.lifecycle, lifecycleOwner)
binder.lifecycleOwner = lifecycleOwner
}
if (parentItem != null) {
binder.setVariable(BR.parentItem, parentItem)
}
}
}
}
================================================
FILE: arch/src/main/java/arch/adapter/RecyclerLayoutStrategy.kt
================================================
package arch.adapter
interface RecyclerLayoutStrategy {
fun getLayoutId(item: Any): Int
}
================================================
FILE: arch/src/main/java/arch/adapter/SingleTypeRecyclerAdapter.kt
================================================
package arch.adapter
import androidx.annotation.LayoutRes
import androidx.databinding.ObservableArrayList
import arch.viewmodel.BaseArchViewModel
/**
* Handcrafted by Štěpán Šonský on 15.01.2018.
*/
class SingleTypeRecyclerAdapter : BaseRecyclerAdapter {
@LayoutRes
private var layoutId: Int = 0
constructor(items: ObservableArrayList, viewModel: BaseArchViewModel?, itemLaoyutId: Int) : super(items, viewModel) {
this.layoutId = itemLaoyutId
}
constructor(items: ObservableArrayList, itemLaoyutId: Int) : super(items) {
this.layoutId = itemLaoyutId
}
override fun getLayoutId(itemType: Int): Int {
return layoutId
}
}
================================================
FILE: arch/src/main/java/arch/adapter/StrategyRecyclerAdapter.kt
================================================
package arch.adapter
import androidx.databinding.ObservableArrayList
import arch.viewmodel.BaseArchViewModel
class StrategyRecyclerAdapter(items: ObservableArrayList, var strategy: RecyclerLayoutStrategy, vm: BaseArchViewModel?) :
BaseRecyclerAdapter(items, vm) {
override fun getLayoutId(itemType: Int): Int {
return itemType
}
override fun getItemViewType(position: Int): Int {
return strategy.getLayoutId(getItem(position))
}
}
================================================
FILE: arch/src/main/java/arch/binding/EditTextBindings.kt
================================================
package arch.binding
import androidx.databinding.BindingAdapter
import com.google.android.material.textfield.TextInputLayout
@BindingAdapter("errorResource")
fun TextInputLayout.setErrorResource(resId: Int?) {
error = resId?.let {
context.getString(resId)
}
}
================================================
FILE: arch/src/main/java/arch/binding/ImageViewBindings.kt
================================================
package arch.binding
import android.graphics.drawable.Drawable
import android.net.Uri
import android.widget.ImageButton
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.bumptech.glide.Glide
import java.io.File
@BindingAdapter(value = ["imageResource"], requireAll = false)
fun setImageResourceCircular(view: ImageButton, resId: Int) {
view.setImageResource(resId)
}
@BindingAdapter(value = ["url", "placeholder"], requireAll = false)
fun setUrlCircular(view: ImageView, url: String?, placeholder: Drawable?) {
if (placeholder == null) {
Glide.with(view.context).load(url).into(view)
} else {
Glide.with(view.context).load(url).placeholder(placeholder).into(view)
}
}
@BindingAdapter(value = ["uri"], requireAll = false)
fun setUriCircular(view: ImageView, uri: Uri?) {
Glide.with(view.context).load(uri).into(view)
}
@BindingAdapter(value = ["file"], requireAll = false)
fun setFile(view: ImageView, file: File?) {
Glide.with(view.context).load(file).into(view)
}
================================================
FILE: arch/src/main/java/arch/binding/ProgressbarBindings.kt
================================================
package arch.binding
import android.content.res.ColorStateList
import android.widget.ProgressBar
import androidx.core.content.ContextCompat
import androidx.databinding.BindingAdapter
@BindingAdapter(value = ["progressTintResource"], requireAll = false)
fun setImageResourceCircular(view: ProgressBar, resId: Int) {
view.progressTintList = ColorStateList.valueOf(ContextCompat.getColor(view.context, resId))
}
================================================
FILE: arch/src/main/java/arch/binding/RecyclerViewBindings.kt
================================================
package arch.binding
import androidx.databinding.BindingAdapter
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import arch.adapter.BaseRecyclerAdapter
import arch.adapter.RecyclerLayoutStrategy
import arch.adapter.SingleTypeRecyclerAdapter
import arch.adapter.StrategyRecyclerAdapter
import arch.viewmodel.BaseArchViewModel
/**
* Created by Stepan on 23.11.2016.
*/
@BindingAdapter(value = ["viewModel", "items", "layoutId", "layoutStrategy", "orientation", "spanCount", "lifecycle", "parentItem"], requireAll = false)
fun bindItems(
view: RecyclerView,
vm: BaseArchViewModel?,
items: ObservableArrayList,
layoutId: Int?,
layoutStrategy: RecyclerLayoutStrategy?,
orientation: Int?,
spanCount: Int?,
lifecycleOwner: LifecycleOwner?,
parentItem: Any?
) {
if (view.adapter == null) {
if (view.layoutManager == null) {
if (spanCount == null) {
view.layoutManager = object : LinearLayoutManager(
view.context, orientation
?: RecyclerView.VERTICAL, false
) {
override fun supportsPredictiveItemAnimations(): Boolean {
return false
}
}
} else {
view.layoutManager = object : GridLayoutManager(
view.context, spanCount) {
override fun supportsPredictiveItemAnimations(): Boolean {
return false
}
}
}
}
if (layoutStrategy == null) {
if (layoutId != null) {
view.adapter = SingleTypeRecyclerAdapter(items, vm, layoutId)
}
} else {
view.adapter = StrategyRecyclerAdapter(items as ObservableArrayList, layoutStrategy, vm)
}
} else {
(view.adapter as BaseRecyclerAdapter).setItems(items)
}
(view.adapter as BaseRecyclerAdapter<*>).lifecycleOwner = lifecycleOwner
(view.adapter as BaseRecyclerAdapter<*>).parentItem = parentItem
}
================================================
FILE: arch/src/main/java/arch/binding/TextViewBindings.kt
================================================
package arch.binding
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.databinding.BindingAdapter
@BindingAdapter("textColorResource")
fun setTextColor(editText: TextView, resId: Int?) {
if (resId != null && resId != 0) {
editText.setTextColor(ContextCompat.getColor(editText.context, resId))
}
}
@BindingAdapter("textResource")
fun setTextResource(editText: TextView, resId: Int?) {
if (resId != null) {
editText.setText(resId)
} else {
editText.text = null
}
}
================================================
FILE: arch/src/main/java/arch/binding/ViewBindings.kt
================================================
package arch.binding
import android.view.View
import androidx.databinding.BindingAdapter
@BindingAdapter("backgroundResource")
fun setBackgroundResource(view: View, resId: Int) {
if (resId != 0) {
view.setBackgroundResource(resId)
} else {
view.background = null
}
}
@BindingAdapter("visibleOrGone")
fun setVisibleOrGone(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.GONE
}
@BindingAdapter("visibleOrInvisible")
fun setVisibleOrInvisible(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
}
================================================
FILE: arch/src/main/java/arch/binding/WebViewBindings.kt
================================================
package arch.binding
import android.webkit.WebView
import androidx.databinding.BindingAdapter
@BindingAdapter("url")
fun setUrl(view: WebView, url: String?) {
url?.let {
view.loadUrl(it)
}
}
@BindingAdapter("java_script_enabled")
fun setJavScriptEnabled(view: WebView, enabled: Boolean) {
view.settings.javaScriptEnabled = enabled
}
================================================
FILE: arch/src/main/java/arch/event/LiveEvent.kt
================================================
package arch.event
/**
* Handcrafted by Štěpán Šonský on 15.01.2018.
*/
abstract class LiveEvent
================================================
FILE: arch/src/main/java/arch/event/LiveEventMap.kt
================================================
package arch.event
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import java.util.*
import kotlin.reflect.KClass
/**
* Handcrafted by Štěpán Šonský on 15.01.2018.
*/
class LiveEventMap {
private val events = HashMap, SingleLiveEvent>()
fun subscribe(lifecycleOwner: LifecycleOwner, eventClass: KClass, eventObserver: Observer) {
var liveEvent: SingleLiveEvent? = events[eventClass] as SingleLiveEvent?
if (liveEvent == null) {
liveEvent = initUiEvent(eventClass)
}
liveEvent.observe(lifecycleOwner, eventObserver)
}
fun publish(event: T) {
var liveEvent: SingleLiveEvent? = events[event::class] as SingleLiveEvent?
if (liveEvent == null) {
liveEvent = initUiEvent(event::class)
}
liveEvent.postValue(event)
}
private fun initUiEvent(eventClass: KClass): SingleLiveEvent {
val liveEvent = SingleLiveEvent()
events[eventClass] = liveEvent
return liveEvent
}
}
================================================
FILE: arch/src/main/java/arch/event/NavigationEvent.kt
================================================
package arch.event
import android.os.Bundle
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
class NavigationEvent() : LiveEvent() {
var resId: Int? = null
var navArgs: Bundle? = null
var navDirections: NavDirections? = null
var navOptions: NavOptions? = null
constructor(resId: Int, navArgs: Bundle? = null, navOptions: NavOptions? = null) : this() {
this.resId = resId
this.navArgs = navArgs
this.navOptions = navOptions
}
constructor(navDirections: NavDirections, navOptions: NavOptions? = null) : this() {
this.navDirections = navDirections
this.navOptions = navOptions
}
}
================================================
FILE: arch/src/main/java/arch/event/NavigationGraphEvent.kt
================================================
package arch.event
class NavigationGraphEvent() : LiveEvent() {
var navGraphId: Int? = null
var navStartDestinationId: Int? = null
constructor(navGraphId: Int, navStartDestinationId: Int) : this() {
this.navGraphId = navGraphId
this.navStartDestinationId = navStartDestinationId
}
}
================================================
FILE: arch/src/main/java/arch/event/SingleLiveEvent.java
================================================
package arch.event;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
* Note that only one observer is going to be notified of changes.
*/
public class SingleLiveEvent extends MutableLiveData {
private static final String TAG = "SingleLiveEvent";
private final AtomicBoolean mPending = new AtomicBoolean(false);
@MainThread
@Override
public void observe(LifecycleOwner owner, final Observer super T> observer) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
}
// Observe the internal MutableLiveData
super.observe(owner, new Observer() {
@Override
public void onChanged(@Nullable T t) {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t);
}
}
});
}
@MainThread
public void setValue(@Nullable T t) {
mPending.set(true);
super.setValue(t);
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
public void call() {
setValue(null);
}
}
================================================
FILE: arch/src/main/java/arch/extensions/navExtensions.kt
================================================
package arch.extensions
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import arch.event.NavigationEvent
import arch.event.NavigationGraphEvent
import arch.utils.safeLet
import cz.stepansonsky.mvvm.BuildConfig
fun NavController.safeNavigate(navEvent: NavigationEvent) {
try {
if (navEvent.resId != null) {
navigate(navEvent.resId!!, navEvent.navArgs, navEvent.navOptions)
} else if (navEvent.navDirections != null) {
navigate(navEvent.navDirections!!, navEvent.navOptions)
}
} catch (it: Throwable){
if (BuildConfig.DEBUG) {
it.printStackTrace()
}
}
}
fun NavController.safeNavigate(@IdRes resId: Int, args: Bundle? = null, navOptions: NavOptions? = null) {
try {
navigate(resId, args, navOptions)
} catch (it: Throwable){
if (BuildConfig.DEBUG) {
it.printStackTrace()
}
}
}
fun NavController.safeNavigate(directions: NavDirections, navOptions: NavOptions? = null) {
try {
navigate(directions, navOptions)
} catch (it: Throwable){
if (BuildConfig.DEBUG) {
it.printStackTrace()
}
}
}
fun NavController.setNavigationGraph(navEvent: NavigationGraphEvent) {
safeLet(navEvent.navGraphId, navEvent.navStartDestinationId) { graphId, startNavId ->
val graph = navInflater.inflate(graphId)
graph.startDestination = startNavId
setGraph(graph)
}
}
================================================
FILE: arch/src/main/java/arch/livedata/SafeMutableLiveData.kt
================================================
package arch.livedata
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
class SafeMutableLiveData(initValue: T) : MutableLiveData(initValue) {
override fun getValue(): T {
return super.getValue()!!
}
override fun setValue(value: T) {
super.setValue(value)
}
fun observe(owner: LifecycleOwner, body: (T) -> Unit) {
observe(owner, Observer { t -> body(t!!) })
}
override fun postValue(value: T) {
super.postValue(value)
}
}
================================================
FILE: arch/src/main/java/arch/utils/NullableUtils.kt
================================================
package arch.utils
inline fun safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? {
return if (p1 != null && p2 != null) block(p1, p2) else null
}
inline fun safeLet(p1: T1?, p2: T2?, p3: T3?,block: (T1, T2, T3)->R?): R? {
return if (p1 != null && p2 != null && p3 != null) block(p1, p2, p3) else null
}
================================================
FILE: arch/src/main/java/arch/view/BaseArchActivity.kt
================================================
package arch.view
import android.content.Intent
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import androidx.navigation.findNavController
import arch.event.LiveEvent
import arch.event.NavigationEvent
import arch.event.NavigationGraphEvent
import arch.extensions.safeNavigate
import arch.extensions.setNavigationGraph
import arch.viewmodel.BaseArchViewModel
import cz.stepansonsky.mvvm.BR
import cz.stepansonsky.mvvm.R
import kotlin.reflect.KClass
/**
* Created by Stepan on 06.10.2016.
*/
abstract class BaseArchActivity(@LayoutRes var layoutId: Int, viewModelClass: KClass) :
AppCompatActivity() {
protected val viewModel: VM by lazy { ViewModelProvider(this).get(viewModelClass.java) }
protected lateinit var binding: B
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
binding = DataBindingUtil.setContentView(this, layoutId)
binding.lifecycleOwner = this
binding.setVariable(BR.lifecycle, this)
binding.setVariable(BR.vm, viewModel)
subscribe(NavigationGraphEvent::class, Observer { event ->
navController().setNavigationGraph(event)
})
subscribe(NavigationEvent::class, Observer { event ->
navController().safeNavigate(event)
})
}
protected fun subscribe(eventClass: KClass, eventObserver: Observer) {
viewModel.subscribe(this, eventClass, eventObserver)
}
override fun onDestroy() {
lifecycle.removeObserver(viewModel)
super.onDestroy()
}
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
}
protected fun navigate(@IdRes resId: Int, args: Bundle? = null, navOptions: NavOptions? = null) {
navController().safeNavigate(resId, args, navOptions)
}
protected fun navigate(directions: NavDirections, navOptions: NavOptions? = null) {
navController().safeNavigate(directions, navOptions)
}
protected fun navController(): NavController {
return findNavController(R.id.nav_host_fragment)
}
}
================================================
FILE: arch/src/main/java/arch/view/BaseArchDialogFragment.kt
================================================
package arch.view
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import arch.event.LiveEvent
import arch.viewmodel.BaseArchViewModel
import cz.stepansonsky.mvvm.BR
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import kotlin.reflect.KClass
abstract class BaseArchDialogFragment(@LayoutRes val layoutId: Int, viewModelClass: KClass) :
DialogFragment() {
protected lateinit var binding: B
protected val viewModel: VM by sharedViewModel(viewModelClass)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, layoutId, container, false)
binding.lifecycleOwner = this
binding.setVariable(BR.vm, viewModel)
return binding.root
}
override fun onDestroy() {
lifecycle.removeObserver(viewModel)
super.onDestroy()
}
protected fun subscribe(eventClass: KClass, eventObserver: Observer) {
viewModel.subscribe(this, eventClass, eventObserver)
}
}
================================================
FILE: arch/src/main/java/arch/view/BaseArchFragment.kt
================================================
package arch.view
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment
import arch.event.LiveEvent
import arch.event.NavigationEvent
import arch.extensions.safeNavigate
import arch.viewmodel.BaseArchViewModel
import cz.stepansonsky.mvvm.BR
import kotlin.reflect.KClass
/**
* Created by Stepan on 11.10.2016.
*/
abstract class BaseArchFragment(
@LayoutRes val layoutId: Int, viewModelClass: KClass
) : Fragment() {
protected lateinit var binding: B
protected val viewModel: VM by lazy { ViewModelProvider(this).get(viewModelClass.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
subscribe(NavigationEvent::class) {
navController().safeNavigate(it)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, layoutId, container, false)
binding.lifecycleOwner = this
binding.setVariable(BR.lifecycle, this)
binding.setVariable(BR.vm, viewModel)
return binding.root
}
override fun onDestroy() {
lifecycle.removeObserver(viewModel)
super.onDestroy()
}
protected fun subscribe(eventClass: KClass, eventObserver: (T) -> Unit) {
viewModel.subscribe(this, eventClass, Observer(eventObserver))
}
protected fun navController(): NavController {
return NavHostFragment.findNavController(this)
}
protected fun navigate(@IdRes resId: Int, args: Bundle? = null, navOptions: NavOptions? = null) {
navController().safeNavigate(resId, args, navOptions)
}
protected fun navigate(directions: NavDirections, navOptions: NavOptions? = null) {
navController().safeNavigate(directions, navOptions)
}
}
================================================
FILE: arch/src/main/java/arch/view/BaseDialogFragment.kt
================================================
package arch.view
import android.app.Activity
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import arch.viewmodel.BaseDialogViewModel
import cz.stepansonsky.mvvm.R
import kotlin.reflect.KClass
abstract class BaseDialogFragment(@LayoutRes layoutId: Int, viewModelClass: KClass) :
BaseArchDialogFragment(layoutId, viewModelClass) {
companion object {
const val CANCELED = Activity.RESULT_CANCELED
const val BUTTON_POSITIVE = AlertDialog.BUTTON_POSITIVE
const val BUTTON_NEGATIVE = AlertDialog.BUTTON_NEGATIVE
const val BUTTON_NEUTRAL = AlertDialog.BUTTON_NEUTRAL
const val ARG_TITLE_RES = "ARG_TITLE_RES"
const val ARG_MESSAGE_RES = "ARG_MESSAGE_RES"
const val ARG_POSITIVE_BUTTON_RES = "ARG_POSITIVE_BUTTON_RES"
const val ARG_NEGATIVE_BUTTON_RES = "ARG_NEGATIVE_BUTTON_RES"
const val ARG_NEUTRAL_BUTTON_RES = "ARG_NEUTRAL_BUTTON_RES"
const val ARG_TITLE = "ARG_TITLE"
const val ARG_MESSAGE = "ARG_MESSAGE"
const val ARG_POSITIVE_BUTTON = "ARG_POSITIVE_BUTTON"
const val ARG_NEGATIVE_BUTTON = "ARG_NEGATIVE_BUTTON"
const val ARG_NEUTRAL_BUTTON = "ARG_NEUTRAL_BUTTON"
const val ARG_REQUEST_CODE = "ARG_REQUEST_CODE"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments?.containsKey(ARG_TITLE_RES) == true) {
viewModel.title = getString(arguments?.getInt(ARG_TITLE_RES)!!)
} else if (arguments?.containsKey(ARG_TITLE) == true) {
viewModel.title = arguments?.getString(ARG_TITLE)
}
if (arguments?.containsKey(ARG_MESSAGE_RES) == true) {
viewModel.message = getString(arguments?.getInt(ARG_MESSAGE_RES)!!)
} else if (arguments?.containsKey(ARG_MESSAGE) == true) {
viewModel.message = arguments?.getString(ARG_MESSAGE)
}
if (arguments?.containsKey(ARG_POSITIVE_BUTTON_RES) == true) {
viewModel.positiveButton = getString(arguments?.getInt(ARG_POSITIVE_BUTTON_RES)!!)
} else if (arguments?.containsKey(ARG_POSITIVE_BUTTON) == true) {
viewModel.positiveButton = arguments?.getString(ARG_POSITIVE_BUTTON)
}
if (arguments?.containsKey(ARG_NEGATIVE_BUTTON_RES) == true) {
viewModel.negativeButton = getString(arguments?.getInt(ARG_NEGATIVE_BUTTON_RES)!!)
} else if (arguments?.containsKey(ARG_NEGATIVE_BUTTON) == true) {
viewModel.negativeButton = arguments?.getString(ARG_NEGATIVE_BUTTON)
}
if (arguments?.containsKey(ARG_NEUTRAL_BUTTON_RES) == true) {
viewModel.negativeButton = getString(arguments?.getInt(ARG_NEUTRAL_BUTTON_RES)!!)
} else if (arguments?.containsKey(ARG_NEUTRAL_BUTTON) == true) {
viewModel.negativeButton = arguments?.getString(ARG_NEUTRAL_BUTTON)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding.root.findViewById