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-dev https://europe-west1-erouska-key-server-dev.cloudfunctions.net/ https://apiserver-eyrqoibmxa-ew.a.run.app/ https://europe-west1-erouska-key-server-dev.cloudfunctions.net cz.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 #e8e8e8 true #FFF500 #fff @color/white #717171 ================================================ FILE: app/src/main/res/values/controls.xml ================================================ ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 16dp 32dp 56dp 24dp ================================================ FILE: app/src/main/res/values/ids.xml ================================================ ================================================ FILE: app/src/main/res/values/strings-notranslate.xml ================================================ Days since onset symptoms: %d Report type: %s Rolling start: %s, refresh %s Last download: %s ================================================ FILE: app/src/main/res/values/strings.xml ================================================ eRouška Help Back to app Enable Bluetooth Enable Enable Bluetooth Enabling Bluetooth is necessary to log encounters with other eRouška users near you. eRouška is active eRouška is paused Application 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... Pause Start We 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 activation Privacy Error Activation could not be finished Try again Help News Starting 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 conditions About application The 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. Settings Dismiss How 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 too Contacts Anonymously notify others Sent Share eRouška Share application Hi, 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 %s Delete registration About application Your device seems to be offline. Application on the background Chat with Anežka – eRouška support Current measures Information on current measures Current situation and case numbers eRouš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 administered If 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 code Verify data Verification code was not entered correctly Verification code has expired Request a new SMS message with a verification code from the public health officer. We couldn\'t send your data Please contact support at %1$s and mention this error code in the email: \'%2$s\'. Back Close Try again Data were successfully sent Thank 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 symptoms Please 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-19 Cough, high temperature, laboured breathing, sore throat, headache, loss of smell and taste. Date of first symptoms Continue Cross-border travel Have 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 abroad I have not been abroad Cross-border cooperation Do 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 agree I do not agree Risky encounters update Close Risky encounter You 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 it Risky encounter Risky encounters update eRouška is paused Enable COVID-19 Exposure Notifications eRouška cannot communicate with other eRouška applications nearby.\n\nEnable COVID-19 Exposure Notifications by pressing \"Enable\". Risky encounters Close More information I have symptoms I don\'t have symptoms Update Google Play Services The latest version of Google Play Services is needed to enable Exposure Notifications about risky encounters. Update Notifications To work properly, eRouška requires COVID-19 Exposure Notifications to be enabled These 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. Continue Close Enable Location Services Although we do not use your location data, please enable Location Services so that Bluetooth and eRouška work correctly. Enable Location Services Enable Bluetooth and Location Services With 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 Services No risky encounters Risky encounters discovered on %s at %s Previously discovered risky encounters Failed to activate eRouška Reason: %s Your 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. OK Search What are you looking for? Previous result Next result No search results We haven\'t found anything yet, so keep typing How eRouška works Find out more about how the application works On %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 days On %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 others Cross-border cooperation Enabled Disabled Email support 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. Do you wish to attach a file with anonymous information about your device\'s settings? This information will help us troubleshoot. Yes, attach the file No, do not attach the file Select email application: Feedback from eRouška Error code Application version System version Device Localization Bluetooth supports does NOT support Location Services Primary account Battery optimization exception Installation Keys last downloaded Last notification of a risky encounter Last risky encounter on diagnostic_information.txt Tell us what we can help you with:\n\n Tell us what happened before the error:\n\n Screen Checking for risky encounters eRouška is checking for risky encounters with COVID-19. Checking for risky encounters How does eRouška work? Email support Are you curious how eRouška works? Find out more Close eRouš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 encounters When 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 confirmed If 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 individuals The 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 evaluation Displaying notifications Those 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 support I 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 code Have 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 cooperation Help to fight against COVID-19 when abroad COVID-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 abroad This 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 enabled Disable Verification code is not valid Check 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 vaccinated More 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ška Nápověda Zpět do aplikace Zapnout Bluetooth Zapnout Zapněte Bluetooth Zapnuté 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… Pozastavit Spustit Myslíme na vaše soukromí, odesílání dat máte vždy pod kontrolou eRouš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 aktivaci Soukromí Chyba Aktivaci aplikace nelze dokončit Zkusit znovu Nápověda Aktuá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 aplikaci Pro 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šit Jak to funguje Pokra\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\u00ed Kontakty Anonymně upozornit ostatní Odesláno Sdílet aplikaci eRouška Sdílet aplikaci Ahoj, 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 %s Zrušit registraci O\u00a0aplikaci Vaše zařízení nemá aktivní přístup k internetu. Aplikace na pozadí Napište Anežce – podpoře eRoušky Aktuální opatření Informace o aktuálních opatřeních Aktuální situace v číslech eRouška v číslech Za 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ód Ověřit Ověřovací kód není správně zadaný Vypršela platnost ověřovacího kódu Požádejte pracovníka hygienické stanice o zaslání nové SMS zprávy s ověřovacím kódem. Nepodařilo se nám odeslat data Kontaktujte prosím podporu na emailu %1$s a uveďte následující kód chyby: \'%2$s\'. Zpět Zavřít Zkusit znovu Data jste úspěšně odeslali Dě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-19 Kašel, teplota, dušnost, bolest v krku, bolest hlavy, ztráta čichu a chuti. Datum prvních příznaků Pokračovat Cesty 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čím Souhlasí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ím Nesouhlasím Aktualizace rizikových setkání Zavřít Rizikové 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 spustit Rizikové setkání Aktualizace rizikových setkání Pozastavená eRouška Zapněte Oznámení o kontaktu s COVID-19 eRouš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řít Více informací Mám příznaky Nemám příznaky Aktualizujte aplikaci Služby Google Play Nejnově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. Aktualizovat Oznámení eRouška potřebuje pro správné fungování zapnutá oznámení o kontaktu s COVID-19 Tato 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čovat Zavřít Zapněte Polohové služby I 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žby Zapněte Bluetooth a Polohové služby Bez 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 %s Starší riziková setkání eRoušku nelze aktivovat Důvod: %s Toto 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. OK Hledat Napište, co hledáte Předešlý výsledek Následující výsledek Žádné výsledky vyhledávání Zatím jsme nic nenašli, pokračujte ve psaní Jak eRouška funguje Zjistěte více o tom, jak aplikace funguje Naposledy %s jste se setkali s osobou, u které bylo potvrzeno onemocnění COVID-19 Upozorně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ál Za 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čím Zapnuto Vypnuto Napsat e-mail na podporu 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. 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řílohu Nechci přidat přílohu Vyberte e-mailovou aplikaci: Zpětná vazba z aplikace eRouška Kód chyby Verze aplikace Verze systému Zařízení Lokalizace Bluetooth podporuje NEpodporuje Polohové služby Primární účet Výjimka z optimalizace baterie Instalace Poslední stažení klíčů Poslední notifikace rizikového setkání Poslední rizikové setkání z diagnosticke_informace.txt Napište nám, s čím vám můžeme poradit:\n\n Napište nám, jak k chybě došlo:\n\n Obrazovka Probí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 podporu Zajímá vás, jak eRouška funguje? Zjistit více Zavřít eRouš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-19 Pokud 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šek Po 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ými Ostatní 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-mail Nepřišla mi SMS s ověřovacím kódem Milý 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ód Má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čím Pomozte 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é Vypnout Zadaný 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ím Ví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/black false #8D2F80ED #000 #000 @color/white ================================================ FILE: app/src/main/res/values-sk/strings.xml ================================================ eRouška Nápoveda Späť do aplikácie Zapnúť Bluetooth Zapnúť Zapnite Bluetooth Zapnutý Bluetooth je dôležitý na zbieranie kontaktov s ostatnými eRouškami vo vašom okolí. eRouška je aktívna eRouš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 kontrolou eRouš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áciu Súkromie Chyba Aktiváciu aplikácie nie je možné dokončiť Skúsiť znova Nápoveda Aktuálne Apliká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žívania O aplikácii Pre 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. Nastavenia Zrušiť Ako to funguje Pokra\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\u00ed Kontakty Anonymne upozorniť ostatných Odoslané Zdieľať aplikáciu eRouška Zdieľať aplikáciu Ahoj, 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 %s Zrušiť registráciu O aplik\u00e1cii Vaše zariadenie nemá aktívny prístup k internetu. Aplikácia na pozadí Napíšte Anežke - podpore eRoušky Aktuálne opatrenia Informácie o aktuálnych opatreniach Aktuálna situácia v číslach eRouška v číslach Za 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ód Overiť Overovací kód nie je správne zadaný Vypršala platnosť overovacieho kódu Požiadajte pracovníka hygienickej stanice o zaslanie novej SMS správy s overovacím kódom Nepodarilo sa nám odoslať dáta Kontaktujte, 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-19 eRouš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íznakov Vyplň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-19 Kašeľ, teplota, dýchavičnosť, bolesť hrdla, bolesť hlavy, strata čuchu a chuti. Dátum prvých príznakov Pokračovať Cesty do zahraničia Cestovali 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čím Sú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ím Nesúhlasím Aktualizácia rizikových stretnutí Zatvoriť Rizikové stretnutie Stretli 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é stretnutie Aktualizácia rizikových stretnutí Pozastavená eRouška Zapnite oznámenia o kontakte s COVID-19 eRouš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é stretnutia Zatvoriť Viac informácií Mám príznaky Nemám príznaky Aktualizujte aplikáciu Služby Google Play Najnovšia verzia Google Play Services je potrebná na aktiváciu Oznámení o stretnutí s osobou, u ktorej bolo potvrdené ochorenie COVID-19. Aktualizovať Upozornenia eRouška potrebuje na správne fungovanie zapnuté Oznámenia o kontakte s COVID-19 Tieto oznámenia vás upozornia, ak ste boli v blízkosti užívateľa eRoušky s pozitívnym testom na ochorenie COVID-19 Podstatou 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žby Aj 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žby Zapnite Bluetooth a Polohové služby Bez 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é stretnutia Rizikové stretnutia zistené %s o %s Staršie rizikové stretnutia eRoušku nie je možné aktivovať Dôvod: %s Toto 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. OK Hľadať Napíšte, čo hľadáte Predošlý výsledok Nasledujúci výsledok Žiadne výsledky vyhľadávania Zatiaľ sme nič nenašli, pokračujte v písaní Ako eRouška funguje Zistite viac o tom, ako aplikácia funguje Naposledy %s ste sa stretli s osobou, ktorej bolo potvrdené ochorenie COVID-19 Upozornenie 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ť ďalej Za posledn\u00fdch 14 dn\u00ed %1$d rizikov\u00e9 stretnutia Za posledn\u00fdch 14 dn\u00ed %1$d rizikov\u00e9 stretnutie Za posledn\u00fdch 14 dn\u00ed %1$d rizikov\u00fdch stretnut\u00ed Za posledn\u00fdch 14 dn\u00ed %1$d rizikov\u00e9 stretnutia Dň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ých Spolupráca so zahraničím Zapnuté Vypnuté Napísať e-mail podpore 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. 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ílohu Nechcem pridať prílohu Vyberte e-mailovú aplikáciu: Spätná väzba z aplikácie eRouška Kód chyby Verzia aplikácie Verzia systému Zariadenie Lokalizácia Bluetooth podporuje NEpodporuje Polohové služby Primárny účet Výnimka z optimalizácie batérie Inštalácia Posledné stiahnutie kľúčov Posledná notifikácia rizikového stretnutia Posledné rizikové stretnutie z diagnosticke_informacie.txt Napíšte nám, s čím vám môžeme poradiť:\n\n Napíšte nám, ako k chybe došlo:\n\n Obrazovka Prebieha kontrola rizikových kontaktov eRouška zisťuje, či ste boli v rizikovom kontakte s ochorením COVID-19. Kontrola rizikových kontaktov Ako eRouška funguje? Napísať e-mail na podporu Zaujíma vás, ako eRouška funguje? Zistiť viac Zavrieť 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 stretnutie Keď 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-19 Ak 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šiek Po zadaní a potvrdení kódu dôjde k informovaniu ostatných eRoušiek o možnom rizikovom stretnutí. Spracovanie stretnutí s nakazenými Ostatné 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 varovania Tý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-mail Neprišla mi SMS s overovacím kódom Milý 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ód Má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čím Pomôžte v boji s COVID-19 aj pri cestách do zahraničia COVID-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é stretnutia Využí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ím Viac informácií
================================================ FILE: app/src/main/res/xml/file_paths.xml ================================================ ================================================ FILE: app/src/main/res/xml/remote_config_defaults.xml ================================================ v2_reportTypeWeights 1;1;1;1;1;1 v2_infectiousnessWeights 0;0.6;2.0 v2_attenuationBucketThresholdDb 55;70;80 v2_attenuationBucketWeights 2.0;1;0.25;0 v2_minimumWindowScore 420 v2_covidDataServerUrl https://europe-west1-erouska-key-server-dev.cloudfunctions.net v2_minGmsVersionCode 203019000 v2_keyImportDataOutdatedHours 99999999 v2_riskyEncountersTitleAn On %1$s, you last encountered someone who tested positive for COVID-19. v2_keyImportPeriodHours 6 v2_keyExportUrl https://cdn.erouska.cz/ v2_recentExposuresUITitle Previous risky encounters v2_spreadPreventionUITitle Principles of responsible behaviour v2_symptomsUITitle Main symptoms v2_exposureUITitle Risky encounters v2_chatBotLink https://erouska.cz/caste-dotazy v2_noEncounterBody We will notify you about a risky encounter in a notification v2_noEncounterHeader You have not encountered anyone known to have COVID-19 in the past 14 days. v2_conditionsOfUseUrl https://erouska.cz/podminky-pouzivani v2_riskyEncountersWithoutSymptoms You 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_riskyEncountersWithSymptoms If 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_riskyEncountersTitle You 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_encounterWarning You have encountered a person infected with COVID-19. Watch your health status. v2_verificationServerApiKey 4AP6RHk5VlNi7WIhj4ZupI9JZODdUyrPGb1C2mDYKo4cuaGIHdYtOhjyoqhg4vB5r7FDijxnySLb_1CUH6XdDA.1.oZLaUuVOPbvEaiWnzkQxasWlD_71iSTeM9aAIOKrfqhg5QO68t004CKxWutYmfISc7vqJe6uY2dRrt34OFkliw v2_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\n v2_shareAppDynamicLink https://erouska.cz/app/sdilej v2_minSupportedVersionCodeAndroid 666 v2_shouldCheckOSVersion 1 v2_unsupportedDeviceLink https://koronavirus.mzcr.cz/en/ v2_minSupportedVersion 2.3.3 v2_currentMeasuresUrl https://covid.gov.cz/en/ v2_appleExposureConfiguration {"factorHigh":0.25,"factorStandard":1,"factorLow":2,"lowerThreshold":55,"higherThreshold":70,"triggerThreshold":10} v2_daysSinceOnsetToInfectiousness 0;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;1 v2_supportEmail info@erouska.cz v2_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_handleError400AsExpiredOrUsedCode false v2_handleError500AsInvalidCode false v2_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_exposureHelpUITitle Help v2_updateNewsOnRequest true v2_recentExposureNotificationTitle It's been some time since your eRouška updated its data with risky encounters. Connect to the internet. v2_efgsCountries Austria, Belgium, Croatia, Cyprus, Denmark, Finland, Germany, Ireland, Italy, Latvia, Netherlands, Norway, Poland, Slovenia and Spain currently cooperate with eRouška. v2_diagnosisKeysDataMappingLimitDays 7 v2_infectiousnessWhenDaysSinceOnsetMissing 1 v2_reportTypeWhenMissing 1 v2_noEncounterCardTitle You have not encountered anyone with COVID-19 in the past 14 days v2_encounterUpdateFrequency Data update once every %d hours. v2_dbCleanupDays 15 v2_selfCheckerPeriodHours 4 v2_showChatBotLink false v2_efgsVisitedCountries v2_efgsReportType ConfirmedTest v2_efgsConsentToFederation false v2_efgsTravellerDefault false v2_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_appleIgnoreAndroid true v2_howItWorksUITitle How eRouška works v2_validationTokenExpirationLeewayMinutes 15 v2_efgsDays 14 v2_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_howItWorksEvalContent eRouš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_ragnarokHeadline eRouška's contribution to the fight against the pandemic is over v2_ragnarokMoreInfo https://erouska.cz/en v2_ragnarokBody Thank 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_ragnarok true ================================================ FILE: app/src/main/res/xml-cs/remote_config_defaults.xml ================================================ v2_reportTypeWeights 1;1;1;1;1;1 v2_infectiousnessWeights 0;0.6;2.0 v2_attenuationBucketThresholdDb 55;70;80 v2_attenuationBucketWeights 2.0;1;0.25;0 v2_minimumWindowScore 420 v2_covidDataServerUrl https://europe-west1-erouska-key-server-dev.cloudfunctions.net v2_minGmsVersionCode 203019000 v2_keyImportDataOutdatedHours 99999999 v2_riskyEncountersTitleAn Naposledy %1$s jste se setkali s osobou, u které bylo potvrzeno onemocnění COVID-19. v2_keyImportPeriodHours 6 v2_keyExportUrl https://cdn.erouska.cz/ v2_recentExposuresUITitle Předchozí riziková setkání v2_spreadPreventionUITitle Zásady zodpovědného chování v2_symptomsUITitle Hlavní příznaky v2_exposureUITitle Rizikové setkání v2_chatBotLink https://erouska.cz/caste-dotazy v2_noEncounterBody Na rizikové setkání vás upozorníme pomocí oznámení. v2_noEncounterHeader V posledních 14 dnech nebyla ve vaší blízkosti žádná osoba s potvrzeným onemocněním COVID-19 v2_conditionsOfUseUrl https://erouska.cz/podminky-pouzivani v2_riskyEncountersWithoutSymptoms Jestliž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_riskyEncountersWithSymptoms Pokud 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_riskyEncountersTitle Dne %@ 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_encounterWarning Setkali jste se s osobou, u které bylo potvrzeno onemocnění COVID-19. Sledujte svůj zdravotní stav. v2_verificationServerApiKey 4AP6RHk5VlNi7WIhj4ZupI9JZODdUyrPGb1C2mDYKo4cuaGIHdYtOhjyoqhg4vB5r7FDijxnySLb_1CUH6XdDA.1.oZLaUuVOPbvEaiWnzkQxasWlD_71iSTeM9aAIOKrfqhg5QO68t004CKxWutYmfISc7vqJe6uY2dRrt34OFkliw v2_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\n v2_shareAppDynamicLink https://erouska.cz/app/sdilej v2_minSupportedVersionCodeAndroid 666 v2_shouldCheckOSVersion 1 v2_unsupportedDeviceLink https://koronavirus.mzcr.cz v2_minSupportedVersion 2.3.3 v2_currentMeasuresUrl https://covid.gov.cz/opatreni v2_appleExposureConfiguration {"factorHigh":0.25,"factorStandard":1,"factorLow":2,"lowerThreshold":55,"higherThreshold":70,"triggerThreshold":10} v2_daysSinceOnsetToInfectiousness 0;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;1 v2_supportEmail info@erouska.cz v2_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_handleError400AsExpiredOrUsedCode false v2_handleError500AsInvalidCode false v2_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_exposureHelpUITitle Nápověda v2_updateNewsOnRequest true v2_recentExposureNotificationTitle Vaš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_efgsCountries Aktuá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_diagnosisKeysDataMappingLimitDays 7 v2_infectiousnessWhenDaysSinceOnsetMissing 1 v2_reportTypeWhenMissing 1 v2_noEncounterCardTitle Za posledních 14 dní žádné rizikové setkání v2_encounterUpdateFrequency Aktualizace probíhá jednou za %d hodin. v2_dbCleanupDays 15 v2_selfCheckerPeriodHours 4 v2_showChatBotLink false v2_efgsVisitedCountries v2_efgsReportType ConfirmedTest v2_efgsConsentToFederation false v2_efgsTravellerDefault false v2_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_appleIgnoreAndroid true v2_howItWorksUITitle Jak eRouška funguje v2_validationTokenExpirationLeewayMinutes 15 v2_efgsDays 14 v2_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_howItWorksEvalContent eRouš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_ragnarokHeadline Pomoc eRoušky v boji s pandemií je u konce v2_ragnarokMoreInfo https://erouska.cz v2_ragnarokBody Dě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_ragnarok true ================================================ FILE: app/src/main/res/xml-sk/remote_config_defaults.xml ================================================ v2_reportTypeWeights 1;1;1;1;1;1 v2_infectiousnessWeights 0;0.6;2.0 v2_attenuationBucketThresholdDb 55;70;80 v2_attenuationBucketWeights 2.0;1;0.25;0 v2_minimumWindowScore 420 v2_covidDataServerUrl https://europe-west1-erouska-key-server-dev.cloudfunctions.net v2_minGmsVersionCode 203019000 v2_keyImportDataOutdatedHours 99999999 v2_riskyEncountersTitleAn Naposledy %1$s ste sa stretli s osobou, u ktorej bolo potvrdené ochorenie COVID-19. v2_keyImportPeriodHours 6 v2_keyExportUrl https://cdn.erouska.cz/ v2_recentExposuresUITitle Predchádzajúce rizikové stretnutia v2_spreadPreventionUITitle Zásady zodpovedného správania v2_symptomsUITitle Hlavné príznaky v2_exposureUITitle Rizikové stretnutie v2_chatBotLink https://erouska.cz/caste-dotazy v2_noEncounterBody Na rizikové stretnutie vás upozorníme pomocou oznámenia. v2_noEncounterHeader V posledných 14 dňoch nebola vo vašej blízkosti žiadna osoba s potvrdeným ochorením COVID-19 v2_conditionsOfUseUrl https://erouska.cz/podminky-pouzivani v2_riskyEncountersWithoutSymptoms Ak 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_riskyEncountersWithSymptoms Ak 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_riskyEncountersTitle Dň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_encounterWarning Stretli ste sa s osobou, u ktorej bolo potvrdené ochorenie COVID-19. Sledujte svoj zdravotný stav. v2_verificationServerApiKey 4AP6RHk5VlNi7WIhj4ZupI9JZODdUyrPGb1C2mDYKo4cuaGIHdYtOhjyoqhg4vB5r7FDijxnySLb_1CUH6XdDA.1.oZLaUuVOPbvEaiWnzkQxasWlD_71iSTeM9aAIOKrfqhg5QO68t004CKxWutYmfISc7vqJe6uY2dRrt34OFkliw v2_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\n v2_shareAppDynamicLink https://erouska.cz/app/sdilej v2_minSupportedVersionCodeAndroid 666 v2_shouldCheckOSVersion 1 v2_unsupportedDeviceLink https://koronavirus.mzcr.cz v2_minSupportedVersion 2.3.3 v2_currentMeasuresUrl https://covid.gov.cz/opatreni v2_appleExposureConfiguration {"factorHigh":0.25,"factorStandard":1,"factorLow":2,"lowerThreshold":55,"higherThreshold":70,"triggerThreshold":10} v2_daysSinceOnsetToInfectiousness 0;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;1 v2_supportEmail info@erouska.cz v2_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_handleError400AsExpiredOrUsedCode false v2_handleError500AsInvalidCode false v2_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_exposureHelpUITitle Nápoveda v2_updateNewsOnRequest true v2_recentExposureNotificationTitle Vaš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_efgsCountries Aktuá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_diagnosisKeysDataMappingLimitDays 7 v2_infectiousnessWhenDaysSinceOnsetMissing 1 v2_reportTypeWhenMissing 1 v2_noEncounterCardTitle Za posledných 14 dní žiadne rizikové stretnutie v2_encounterUpdateFrequency Aktualizácia prebieha raz za %d hodín. v2_dbCleanupDays 15 v2_selfCheckerPeriodHours 4 v2_showChatBotLink false v2_efgsVisitedCountries v2_efgsReportType ConfirmedTest v2_efgsConsentToFederation false v2_efgsTravellerDefault false v2_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_appleIgnoreAndroid true v2_howItWorksUITitle Ako eRouška funguje v2_validationTokenExpirationLeewayMinutes 15 v2_efgsDays 14 v2_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_howItWorksEvalContent eRouš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_ragnarokHeadline Pomoc eRoušky v boji s pandemií je u konce v2_ragnarokMoreInfo https://erouska.cz v2_ragnarokBody Dě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_ragnarok true ================================================ 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 ================================================ erouska https://europe-west1-daring-leaf-272223.cloudfunctions.net/ https://apiserver-jyvw4xgota-ew.a.run.app/ https://europe-west1-daring-leaf-272223.cloudfunctions.net cz.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 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