├── .github └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── .name ├── checkstyle-idea.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── runConfigurations.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── appcenter-pre-build.sh ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── org │ │ │ └── coepi │ │ │ └── android │ │ │ ├── App.kt │ │ │ ├── MainActivity.kt │ │ │ ├── NotReferencedDependenciesActivator.kt │ │ │ ├── ble │ │ │ ├── BleContextExtensions.kt │ │ │ ├── BleDeviceExtensions.kt │ │ │ ├── BleEnabler.kt │ │ │ ├── BleInitializer.kt │ │ │ ├── BleManagerImpl.kt │ │ │ ├── BlePreconditions.kt │ │ │ ├── BlePreconditionsNotifier.kt │ │ │ └── BleSimulator.kt │ │ │ ├── cross │ │ │ └── ScannedTcnsHandler.kt │ │ │ ├── di │ │ │ └── Modules.kt │ │ │ ├── domain │ │ │ ├── model │ │ │ │ └── Symptom.kt │ │ │ └── symptomflow │ │ │ │ ├── SymptomFlow.kt │ │ │ │ ├── SymptomFlowManager.kt │ │ │ │ ├── SymptomRouter.kt │ │ │ │ └── SymptomStep.kt │ │ │ ├── extensions │ │ │ ├── ByteArrayExtensions.kt │ │ │ ├── DateExtensions.kt │ │ │ ├── InstantExtensions.kt │ │ │ ├── ListExtensions.kt │ │ │ ├── LiveDataExtensions.kt │ │ │ ├── ResultExtensions.kt │ │ │ ├── SetExtensions.kt │ │ │ ├── StringExtensions.kt │ │ │ └── rx │ │ │ │ ├── CompletableExtensions.kt │ │ │ │ ├── NotificationExtensions.kt │ │ │ │ └── ObservableExtensions.kt │ │ │ ├── repo │ │ │ ├── AlertFilters.kt │ │ │ ├── AlertsRepo.kt │ │ │ ├── SymptomRepo.kt │ │ │ └── reportsupdate │ │ │ │ └── NewAlertsNotificationShower.kt │ │ │ ├── system │ │ │ ├── AppCenterInitializer.kt │ │ │ ├── Clipboard.kt │ │ │ ├── Email.kt │ │ │ ├── EnvInfos.kt │ │ │ ├── LocaleProvider.kt │ │ │ ├── Preferences.kt │ │ │ ├── Resources.kt │ │ │ ├── ScreenUnitsConverter.kt │ │ │ ├── UnitsProvider.kt │ │ │ ├── WebLaunchEventEmitter.kt │ │ │ ├── WebLauncher.kt │ │ │ ├── intent │ │ │ │ ├── InfectionsNotificationIntentHandler.kt │ │ │ │ ├── IntentForwarder.kt │ │ │ │ ├── IntentHandler.kt │ │ │ │ ├── IntentKey.kt │ │ │ │ └── IntentNoValue.kt │ │ │ ├── log │ │ │ │ ├── CachingLog.kt │ │ │ │ ├── CompositeLog.kt │ │ │ │ └── Log.kt │ │ │ └── rx │ │ │ │ ├── ObservablePreferences.kt │ │ │ │ ├── OperationForwarder.kt │ │ │ │ ├── OperationState.kt │ │ │ │ └── OperationStateConsumer.kt │ │ │ ├── ui │ │ │ ├── alerts │ │ │ │ ├── AlertViewData.kt │ │ │ │ ├── AlertsAdapter.kt │ │ │ │ ├── AlertsFragment.kt │ │ │ │ └── AlertsViewModel.kt │ │ │ ├── alertsdetails │ │ │ │ ├── AlertDetailsViewData.kt │ │ │ │ ├── AlertsDetailsFragment.kt │ │ │ │ ├── AlertsDetailsLinkedAlertsAdapter.kt │ │ │ │ └── AlertsDetailsViewModel.kt │ │ │ ├── alertsinfo │ │ │ │ ├── AlertsInfoFragment.kt │ │ │ │ └── AlertsInfoViewModel.kt │ │ │ ├── common │ │ │ │ ├── ActivityFinisher.kt │ │ │ │ ├── KeyboardHider.kt │ │ │ │ ├── LimitedSizeQueue.kt │ │ │ │ ├── NotificationsObserver.kt │ │ │ │ ├── SwipeToDeleteCallback.kt │ │ │ │ ├── UINotification.kt │ │ │ │ ├── UINotificationData.kt │ │ │ │ └── UINotifier.kt │ │ │ ├── debug │ │ │ │ ├── DebugBleObservable.kt │ │ │ │ ├── DebugFragment.kt │ │ │ │ ├── DebugPagerAdapter.kt │ │ │ │ ├── DebugViewModel.kt │ │ │ │ ├── ble │ │ │ │ │ ├── DebugBleFragment.kt │ │ │ │ │ ├── DebugBleItemViewData.kt │ │ │ │ │ ├── DebugBleRecyclerAdapter.kt │ │ │ │ │ └── DebugBleViewModel.kt │ │ │ │ └── logs │ │ │ │ │ ├── LogsAdapter.kt │ │ │ │ │ ├── LogsFragment.kt │ │ │ │ │ └── LogsViewModel.kt │ │ │ ├── extensions │ │ │ │ ├── AdapterViewExtensions.kt │ │ │ │ ├── AlertExtensions.kt │ │ │ │ ├── FragmentExtensions.kt │ │ │ │ ├── PublicReportExtensions.kt │ │ │ │ ├── TextViewExtensions.kt │ │ │ │ └── rx │ │ │ │ │ └── ObservableExtensions.kt │ │ │ ├── formatters │ │ │ │ ├── DateFormatters.kt │ │ │ │ ├── LengthFormatter.kt │ │ │ │ └── NumberFormatters.kt │ │ │ ├── home │ │ │ │ ├── HomeAdapter.kt │ │ │ │ ├── HomeCard.kt │ │ │ │ ├── HomeFragment.kt │ │ │ │ └── HomeViewModel.kt │ │ │ ├── navigation │ │ │ │ ├── NavigationCommand.kt │ │ │ │ ├── Navigator.kt │ │ │ │ └── RootNavigation.kt │ │ │ ├── notifications │ │ │ │ ├── AppNotificationChannels.kt │ │ │ │ ├── NotificationChannelCreator.kt │ │ │ │ ├── NotificationConfig.kt │ │ │ │ ├── NotificationShower.kt │ │ │ │ ├── ReminderAlarmHandler.kt │ │ │ │ ├── ReminderAlarmHandlerExt.kt │ │ │ │ └── ReminderNotificationShower.kt │ │ │ ├── onboarding │ │ │ │ ├── OnboardingAdapter.kt │ │ │ │ ├── OnboardingCardViewData.kt │ │ │ │ ├── OnboardingClickEvent.kt │ │ │ │ ├── OnboardingFragment.kt │ │ │ │ ├── OnboardingPermissionsChecker.kt │ │ │ │ ├── OnboardingShower.kt │ │ │ │ └── OnboardingViewModel.kt │ │ │ ├── settings │ │ │ │ ├── UserSettingViewData.kt │ │ │ │ ├── UserSettingsAdapter.kt │ │ │ │ ├── UserSettingsFragment.kt │ │ │ │ └── UserSettingsViewModel.kt │ │ │ ├── symptoms │ │ │ │ ├── SymptomViewData.kt │ │ │ │ ├── SymptomsAdapter.kt │ │ │ │ ├── SymptomsFragment.kt │ │ │ │ ├── SymptomsViewModel.kt │ │ │ │ ├── breathless │ │ │ │ │ ├── BreathlessAdapter.kt │ │ │ │ │ ├── BreathlessFragment.kt │ │ │ │ │ ├── BreathlessViewData.kt │ │ │ │ │ └── BreathlessViewModel.kt │ │ │ │ ├── cough │ │ │ │ │ ├── CoughStatusAdapter.kt │ │ │ │ │ ├── CoughStatusFragment.kt │ │ │ │ │ ├── CoughStatusViewData.kt │ │ │ │ │ ├── CoughStatusViewModel.kt │ │ │ │ │ ├── CoughTypeFragment.kt │ │ │ │ │ └── CoughTypeViewModel.kt │ │ │ │ ├── earliestsymptom │ │ │ │ │ ├── EarliestSymptomFragment.kt │ │ │ │ │ └── EarliestSymptomViewModel.kt │ │ │ │ └── fever │ │ │ │ │ ├── FeverHighestTemperatureFragment.kt │ │ │ │ │ ├── FeverHighestTemperatureViewModel.kt │ │ │ │ │ ├── FeverTakenTodayFragment.kt │ │ │ │ │ ├── FeverTakenTodayViewModel.kt │ │ │ │ │ ├── FeverTemperatureSpotFragment.kt │ │ │ │ │ └── FeverTemperatureSpotViewModel.kt │ │ │ ├── thanks │ │ │ │ ├── ThanksFragment.kt │ │ │ │ └── ThanksViewModel.kt │ │ │ └── xmlbindingadapters │ │ │ │ └── XmlBindingAdapters.kt │ │ │ └── worker │ │ │ └── tcnfetcher │ │ │ ├── ContactsFetchManager.kt │ │ │ └── ContactsFetchWorker.kt │ └── res │ │ ├── color │ │ ├── button_text_color.xml │ │ └── item_symptom_text.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── background_black_rounded_border.xml │ │ ├── background_circle.xml │ │ ├── background_edittext.xml │ │ ├── background_gradient.xml │ │ ├── background_onboarding_card.xml │ │ ├── background_rounded_border.xml │ │ ├── background_splash.xml │ │ ├── circle_button_background.xml │ │ ├── circle_shadowed_button.xml │ │ ├── circle_shadowed_button_selected.xml │ │ ├── dark_button.xml │ │ ├── ic_alert.xml │ │ ├── ic_alert_circle.xml │ │ ├── ic_arrow_back_white_24dp.xml │ │ ├── ic_breathless_exercise.xml │ │ ├── ic_breathless_ground.xml │ │ ├── ic_breathless_hill.xml │ │ ├── ic_breathless_house.xml │ │ ├── ic_breathless_tired.xml │ │ ├── ic_close.xml │ │ ├── ic_coepi_cloud.xml │ │ ├── ic_coepi_copyright.xml │ │ ├── ic_coepi_family_logo_text.xml │ │ ├── ic_coepi_title.xml │ │ ├── ic_coepicloud_splash.xml │ │ ├── ic_contact_alert_icon.xml │ │ ├── ic_family_icon.xml │ │ ├── ic_geometric_dark_background.xml │ │ ├── ic_geometric_light_background.xml │ │ ├── ic_health_icon.xml │ │ ├── ic_intro_1.xml │ │ ├── ic_intro_2.xml │ │ ├── ic_intro_3.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_settings.xml │ │ ├── item_symptom_background.xml │ │ ├── ripple_background_black_rounded_border.xml │ │ ├── ripple_background_rounded_border.xml │ │ ├── ripple_dark_button.xml │ │ ├── ripple_shadowed_rounded_button.xml │ │ ├── ripple_submit_button.xml │ │ ├── ripple_symptom_button.xml │ │ ├── rounded_button_shape.xml │ │ ├── rounded_shadowed_button.xml │ │ ├── rounded_shadowed_button_deselected.xml │ │ ├── rounded_shadowed_button_selected.xml │ │ ├── rounded_shadowed_dark_button.xml │ │ ├── rounded_shadowed_submit_button_disabled.xml │ │ ├── rounded_shadowed_submit_button_enabled.xml │ │ ├── stepper_icon_selected.xml │ │ ├── stepper_icon_unselected.xml │ │ └── submit_button.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── card_onboarding_large.xml │ │ ├── card_onboarding_small.xml │ │ ├── fragment_alerts.xml │ │ ├── fragment_alerts_details.xml │ │ ├── fragment_breathless.xml │ │ ├── fragment_cough_status.xml │ │ ├── fragment_cough_type.xml │ │ ├── fragment_debug.xml │ │ ├── fragment_debug_ble.xml │ │ ├── fragment_earliest_symptom.xml │ │ ├── fragment_exposure_alerts_information.xml │ │ ├── fragment_fever_highest_temperature.xml │ │ ├── fragment_fever_taken_today.xml │ │ ├── fragment_fever_temperature_spot.xml │ │ ├── fragment_home.xml │ │ ├── fragment_logs.xml │ │ ├── fragment_onboarding.xml │ │ ├── fragment_symptoms.xml │ │ ├── fragment_thanks.xml │ │ ├── fragment_user_settings.xml │ │ ├── item_alert.xml │ │ ├── item_alert_details_linked_alert.xml │ │ ├── item_alert_header.xml │ │ ├── item_breathless.xml │ │ ├── item_cough_status.xml │ │ ├── item_debug_ble_header.xml │ │ ├── item_debug_ble_item.xml │ │ ├── item_home_card.xml │ │ ├── item_log_entry.xml │ │ ├── item_min_loglevel.xml │ │ ├── item_setting_section_header.xml │ │ ├── item_setting_text.xml │ │ ├── item_setting_toggle.xml │ │ ├── item_symptom.xml │ │ ├── progress_bar_layout.xml │ │ └── progress_indicator.xml │ │ ├── menu │ │ ├── alert_details.xml │ │ └── bottom_navigation_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ ├── nav_graph.xml │ │ └── nav_graph_root.xml │ │ ├── values │ │ ├── attrs_onboarding_permissions_checker.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── font_weights.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── text_sizes.xml │ │ └── xml │ │ ├── file_paths.xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── android │ └── util │ └── Log.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── tools └── detekt.yml /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | # At the moment all the tests are in the core app. 11 | # Reenable if added Unit Tests. 12 | # test: 13 | # name: Run Unit Tests 14 | # runs-on: ubuntu-latest 15 | # 16 | # steps: 17 | # - uses: actions/checkout@v2 18 | # - name: set up JDK 1.8 19 | # uses: actions/setup-java@v1 20 | # with: 21 | # java-version: 1.8 22 | # - name: Unit tests 23 | # run: ./gradlew test --stacktrace 24 | # - name: Unit tests results 25 | # uses: actions/upload-artifact@v2 26 | # with: 27 | # name: unit-tests-results 28 | # path: app/build/reports/tests/testDebugUnitTest/index.html 29 | 30 | detekt: 31 | name: Run Gradle Detekt 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: set up JDK 1.8 37 | uses: actions/setup-java@v1 38 | with: 39 | java-version: 1.8 40 | - name: Gradle Detekt 41 | run: ./gradlew detekt 42 | - name: Detekt Linter Results 43 | uses: actions/upload-artifact@v2 44 | with: 45 | name: detekt-linter-results 46 | path: app/build/reports/detekt/results/detekt-report.html 47 | 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | .idea/jarRepositories.xml 48 | # Android Studio 3 in .gitignore file. 49 | .idea/caches 50 | .idea/modules.xml 51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 52 | .idea/navEditor.xml 53 | 54 | .idea/kotlinScripting.xml 55 | 56 | # Keystore files 57 | # Uncomment the following lines if you do not want to check your keystore files in. 58 | #*.jks 59 | #*.keystore 60 | 61 | # External native build folder generated in Android Studio 2.2 and later 62 | .externalNativeBuild 63 | .cxx/ 64 | 65 | # Google Services (e.g. APIs or Firebase) 66 | # google-services.json 67 | 68 | # Freeline 69 | freeline.py 70 | freeline/ 71 | freeline_project_description.json 72 | 73 | # fastlane 74 | fastlane/report.xml 75 | fastlane/Preview.html 76 | fastlane/screenshots 77 | fastlane/test_output 78 | fastlane/readme.md 79 | 80 | # Version control 81 | vcs.xml 82 | 83 | # lint 84 | lint/intermediates/ 85 | lint/generated/ 86 | lint/outputs/ 87 | lint/tmp/ 88 | # lint/reports/ 89 | 90 | # Core binaries 91 | coreBinariesCache 92 | app/src/main/jniLibs 93 | 94 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | CoEpi -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/.idea/checkstyle-idea.xml -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 1.8 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CoEpi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoEpi for Android 2 | 3 | This is the repository for the Android implementation of CoEpi. See [CoEpi.org/vision](https://www.coepi.org/vision) for background, and see the rest of our CoEpi repositories [here](https://github.com/Co-Epi). 4 | 5 | ## Build Status 6 | Beta branch: [![Build status](https://build.appcenter.ms/v0.1/apps/b313d675-577e-4bc4-b2db-d63532fbe872/branches/beta/badge)](https://appcenter.ms/users/danamlewis/apps/CoEpi-Android/build/branches/beta) 7 | 8 | Develop branch: [![Build status](https://build.appcenter.ms/v0.1/apps/47ac0cf2-0d7b-478d-aa84-7ac684085222/branches/develop/badge)](https://appcenter.ms/users/scottleibrand/apps/CoEpi-Android/build/branches/develop) 9 | 10 | ## Rust core 11 | 12 | The core functionality (domain services / networking / db) of this app was moved to a [Rust](https://www.rust-lang.org/) library, in order to share it with iOS. The repository can be found [here](https://github.com/Co-Epi/app-backend-rust). [Here](https://github.com/Co-Epi/app-android/tree/9e3d7619885da3dafc5613e2e57c15af44bebd06) you can find the previous Kotlin-only code base, if needed for whatever reason. 13 | 14 | [More details about the architecture](https://github.com/Co-Epi/app-android/wiki/Architecture) 15 | 16 | ## Contribute 17 | 18 | CoEpi is an open source project with an MIT license - please do feel free to contribute improvements! 19 | 20 | 1. Some [code guidelines](https://github.com/Co-Epi/app-android/wiki/Code-guidelines) and recommendations exist. 21 | 2. For new contributors, fork the `develop` branch to get started. 22 | 3. Commit changes to your version branch. 23 | 4. Push your code, and make a pull request back to the CoEpi `develop` branch. 24 | 25 | Need help getting started? Just ask! You can [open an issue](https://github.com/Co-Epi/app-android/issues/new/choose), or start your PR, and tag `@danamlewis` or `@scottleibrand` in a comment to ask for assistance. 26 | 27 | ## Test 28 | * If you're not a developer, try using the download link [here](https://bit.ly/CoEpiAndroidbeta) to get a version of the app to test on your phone, without needing to sign in anywhere. 29 | * `Note: Because you are not getting the app through the app store, Android will warn you that this is an untrusted app. There is no information gathered by the CoEpi app; no username, email, or any other information gathered. If you like, you can review all the code in this repository as part of your evaluation to trust the app on your phone.` 30 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/appcenter-pre-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # App Center Pre-Build Template sourced from: 4 | # https://github.com/microsoft/appcenter/blob/master/sample-build-scripts/xamarin/version-name/appcenter-pre-build.sh 5 | 6 | echo "Running appcenter-pre-build.sh script" 7 | 8 | if [ -z "$APPCENTER_BUILD_ID" ] 9 | then 10 | echo "You need define APPCENTER_BUILD_ID variable or run in App Center" 11 | exit 12 | fi 13 | 14 | if [ -z "$APPCENTER_SOURCE_DIRECTORY" ] 15 | then 16 | echo "You need define APPCENTER_SOURCE_DIRECTORY variable or run in App Center" 17 | exit 18 | fi 19 | 20 | BUILD_GRADLE_FILE=$APPCENTER_SOURCE_DIRECTORY/app/build.gradle 21 | 22 | if [ -e "$BUILD_GRADLE_FILE" ] 23 | then 24 | echo "Updating version code to $APPCENTER_BUILD_ID in $BUILD_GRADLE_FILE" 25 | sed -i '' 's/versionCode [0-9]*/versionCode '$APPCENTER_BUILD_ID'/' $BUILD_GRADLE_FILE 26 | 27 | echo "File versionCode content:" 28 | cat $BUILD_GRADLE_FILE | grep versionCode 29 | else 30 | echo "File not found: $BUILD_GRADLE_FILE" 31 | fi 32 | 33 | echo "Done running appcenter-pre-build.sh script" 34 | 35 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/App.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android 2 | 3 | import android.app.Application 4 | import org.coepi.android.di.appModule 5 | import org.koin.android.ext.koin.androidContext 6 | import org.koin.core.context.startKoin 7 | 8 | class App : Application() { 9 | 10 | @ExperimentalUnsignedTypes 11 | override fun onCreate() { 12 | super.onCreate() 13 | startKoin { 14 | androidContext(this@App) 15 | modules(appModule) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/NotReferencedDependenciesActivator.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android 2 | 3 | import org.coepi.android.cross.ScannedTcnsHandler 4 | import org.coepi.android.system.intent.InfectionsNotificationIntentHandler 5 | import org.coepi.android.ui.notifications.AppNotificationChannels 6 | import org.coepi.android.worker.tcnfetcher.ContactsFetchManager 7 | 8 | class NotReferencedDependenciesActivator( 9 | scannedTcnsHandler: ScannedTcnsHandler, 10 | notificationChannelsInitializer: AppNotificationChannels, 11 | contactsFetchManager: ContactsFetchManager, 12 | infectionsNotificationIntentHandler: InfectionsNotificationIntentHandler 13 | ) { 14 | init { 15 | listOf( 16 | scannedTcnsHandler, 17 | notificationChannelsInitializer, 18 | contactsFetchManager, 19 | infectionsNotificationIntentHandler 20 | ).forEach { it.toString() } 21 | } 22 | 23 | fun activate() {} 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ble/BleContextExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ble 2 | 3 | import android.bluetooth.BluetoothManager 4 | import android.content.Context 5 | import org.coepi.android.system.log.log 6 | 7 | val Context.bluetoothManager get(): BluetoothManager? = 8 | getSystemService(Context.BLUETOOTH_SERVICE).also { 9 | if (it == null) { 10 | log.e("Couldn't get bluetooth service") 11 | } 12 | }.let { service -> 13 | (service as? BluetoothManager).also { manager -> 14 | if (manager == null) { 15 | log.e("Service: $service hasn't expected class.") 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ble/BleDeviceExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ble 2 | 3 | import android.bluetooth.BluetoothDevice 4 | 5 | val BluetoothDevice.debugDescription 6 | get() = "{Address: $address, name: $name, bt class: $bluetoothClass, " + 7 | "bond state: $bondState, type: $type, uuids: $uuids}" 8 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ble/BleEnabler.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ble 2 | 3 | import android.app.Activity 4 | import android.app.Activity.RESULT_CANCELED 5 | import android.app.Activity.RESULT_OK 6 | import android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE 7 | import android.content.Intent 8 | import io.reactivex.subjects.PublishSubject 9 | import io.reactivex.subjects.PublishSubject.create 10 | import org.coepi.android.MainActivity.RequestCodes 11 | import org.coepi.android.system.log.LogTag.PERM 12 | import org.coepi.android.system.log.log 13 | 14 | class BleEnabler { 15 | private val requestCode = RequestCodes.enableBluetooth 16 | 17 | val observable: PublishSubject = create() 18 | 19 | fun enable(activity: Activity) { 20 | val adapter = activity.bluetoothManager?.adapter 21 | 22 | if (adapter != null) { 23 | if (adapter.isEnabled) { 24 | log.d("Bluetooth is enabled", PERM) 25 | observable.onNext(true) 26 | } else { 27 | log.d("Bluetooth not enabled. Requesting...", PERM) 28 | val enableBluetoothIntent = Intent(ACTION_REQUEST_ENABLE) 29 | activity.startActivityForResult(enableBluetoothIntent, requestCode) 30 | } 31 | } else { 32 | // No BT adapter 33 | observable.onNext(false) 34 | } 35 | } 36 | 37 | /** 38 | * Update state if for reasons extraneous to this class, BT will not be enabled. 39 | * Currently when required permissions are not granted. 40 | */ 41 | fun notifyWillNotBeEnabled() { 42 | observable.onNext(false) 43 | } 44 | 45 | fun onActivityResult(requestCode: Int, resultCode: Int) { 46 | if (requestCode == requestCode) { 47 | when (resultCode) { 48 | RESULT_OK -> observable.onNext(true) 49 | RESULT_CANCELED -> observable.onNext(false) 50 | else -> throw Exception("Unexpected result code: $resultCode") 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ble/BleInitializer.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ble 2 | 3 | import io.reactivex.disposables.CompositeDisposable 4 | import io.reactivex.rxkotlin.plusAssign 5 | import io.reactivex.rxkotlin.subscribeBy 6 | import org.coepi.android.system.log.log 7 | 8 | class BleInitializer( 9 | private val blePreconditions: BlePreconditionsNotifier, 10 | private val bleManager: BleManager 11 | ) { 12 | private val disposables = CompositeDisposable() 13 | 14 | fun start() { 15 | startBleWhenEnabled() 16 | } 17 | 18 | private fun startBleWhenEnabled() { 19 | disposables += blePreconditions.bleEnabled 20 | .take(1) 21 | .subscribeBy(onNext = { 22 | log.i("BlePreconditions met - starting BLE") 23 | bleManager.startService() 24 | }, onError = { 25 | log.i("Error enabling bluetooth: $it") 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ble/BlePreconditionsNotifier.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ble 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.subjects.BehaviorSubject 5 | 6 | interface BlePreconditionsNotifier { 7 | val bleEnabled: Observable 8 | 9 | fun notifyBleEnabled() 10 | } 11 | 12 | class BlePreconditionsNotifierImpl: BlePreconditionsNotifier { 13 | 14 | override val bleEnabled: BehaviorSubject = BehaviorSubject.create() 15 | 16 | override fun notifyBleEnabled() { 17 | bleEnabled.onNext(Unit) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ble/BleSimulator.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ble 2 | 3 | import io.reactivex.Observable 4 | import org.coepi.android.extensions.hexToByteArray 5 | import org.coepi.android.system.log.log 6 | import org.coepi.core.domain.model.Tcn 7 | 8 | class BleSimulator : BleManager { 9 | 10 | // Emits all the TCNs at once and terminates 11 | override val observedTcns: Observable = Observable.fromIterable(listOf( 12 | RecordedTcn(Tcn("2485a64b57addcaea3ed1b538d07dbce".hexToByteArray()), 0.0) 13 | )) 14 | 15 | init { 16 | log.i("Using Bluetooth simulator") 17 | } 18 | 19 | override fun startService() {} 20 | override fun stopService() {} 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/cross/ScannedTcnsHandler.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.cross 2 | 3 | import io.reactivex.disposables.CompositeDisposable 4 | import io.reactivex.rxkotlin.plusAssign 5 | import io.reactivex.rxkotlin.subscribeBy 6 | import org.coepi.android.ble.BleManager 7 | import org.coepi.android.system.log.log 8 | import org.coepi.android.ui.debug.DebugBleObservable 9 | import org.coepi.core.services.ObservedTcnsRecorder 10 | 11 | class ScannedTcnsHandler( 12 | bleManager: BleManager, 13 | private val debugBleObservable: DebugBleObservable, 14 | private val observedTcnsRecorder: ObservedTcnsRecorder 15 | ) { 16 | private val disposables = CompositeDisposable() 17 | 18 | init { 19 | disposables += bleManager.observedTcns 20 | .subscribeBy(onNext = { tcn -> 21 | log.d("Observed TCN: $tcn") 22 | observedTcnsRecorder.recordTcn(tcn.tcn, tcn.distance.toFloat()) 23 | debugBleObservable.setObservedTcn(tcn.tcn) 24 | }, onError = { 25 | log.e("Error scanning: $it") 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/domain/model/Symptom.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.domain.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | import org.coepi.core.domain.model.SymptomId 6 | 7 | @Parcelize 8 | data class Symptom( 9 | val id: SymptomId, 10 | val name: String 11 | ) : Parcelable 12 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/domain/symptomflow/SymptomStep.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.domain.symptomflow 2 | 3 | enum class SymptomStep { 4 | COUGH_TYPE, COUGH_DESCRIPTION, BREATHLESSNESS_DESCRIPTION, FEVER_TEMPERATURE_TAKEN_TODAY, 5 | FEVER_TEMPERATURE_SPOT, FEVER_TEMPERATURE_SPOT_INPUT, FEVER_HIGHEST_TEMPERATURE, 6 | EARLIEST_SYMPTOM 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/ByteArrayExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions 2 | 3 | private val HEX_ARRAY = "0123456789ABCDEF".toCharArray() 4 | fun ByteArray.toHex(): String { 5 | val hexChars = CharArray(size * 2) 6 | for (j in indices) { 7 | val v: Int = this[j].toInt() and 0xFF 8 | hexChars[j * 2] = HEX_ARRAY[v ushr 4] 9 | hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F] 10 | } 11 | return String(hexChars) 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/DateExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions 2 | 3 | import org.coepi.core.domain.model.UnixTime 4 | import java.util.Date 5 | 6 | fun Date.toUnixTime() = UnixTime.fromDate(this) 7 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/InstantExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions 2 | 3 | import org.coepi.core.domain.model.UnixTime 4 | import org.threeten.bp.Instant 5 | 6 | fun Instant.toUnixTime() = UnixTime.fromInstant(this) 7 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/ListExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions 2 | 3 | fun List.add(index: Int, element: T): List = 4 | toMutableList().apply { 5 | add(index, element) 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/LiveDataExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.Observer 6 | 7 | fun LiveData.observeWith(lifecycleOwner: LifecycleOwner, observer: (T) -> Unit) { 8 | observe(lifecycleOwner, Observer { observer.invoke(it) }) 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/ResultExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions 2 | 3 | import org.coepi.android.system.log.log 4 | import org.coepi.core.domain.common.Result 5 | import org.coepi.core.domain.common.Result.Success 6 | import org.coepi.core.domain.common.Result.Failure 7 | 8 | fun Result.expect(): T = 9 | when (this) { 10 | is Success -> success 11 | is Failure -> { 12 | // Logging before of a crash can be useful with a persistent log 13 | log.e("Failure: $error") 14 | error(error.toString()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/SetExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions 2 | 3 | fun Set.toggle(element: T): Set = 4 | if (contains(element)) { 5 | this - element 6 | } else { 7 | this + element 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/StringExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions 2 | 3 | fun String.hexToByteArray(): ByteArray { 4 | val carr = toCharArray() 5 | val size = carr.size 6 | val res = ByteArray(size / 2) 7 | var i = 0 8 | while (i < size) { 9 | val hex2 = "" + carr[i] + carr[i + 1] 10 | val byte: Byte = hex2.toLong(radix = 16).toByte() 11 | res[i / 2] = byte 12 | i += 2 13 | } 14 | return res 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/rx/CompletableExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions.rx 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Observable 5 | 6 | fun Completable.toObservable(value: T): Observable = 7 | andThen(Observable.just(value)) 8 | 9 | fun Completable.toUnitObservable(): Observable = 10 | toObservable(Unit) 11 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/rx/NotificationExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions.rx 2 | 3 | import io.reactivex.Notification 4 | import org.coepi.android.system.rx.OperationState 5 | import org.coepi.android.system.rx.OperationState.Failure 6 | 7 | fun Notification.toOperationState(): OperationState? = 8 | when { 9 | isOnNext -> 10 | value?.let { value -> 11 | OperationState.Success(value) 12 | } ?: Failure(IllegalStateException("Value is null")) 13 | isOnError -> 14 | Failure(error ?: Throwable("Unknown error")) 15 | else -> null 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/extensions/rx/ObservableExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.extensions.rx 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.LiveDataReactiveStreams.fromPublisher 5 | import io.reactivex.BackpressureStrategy.BUFFER 6 | import io.reactivex.Notification 7 | import io.reactivex.Observable 8 | import io.reactivex.Observable.empty 9 | import io.reactivex.Observable.just 10 | import io.reactivex.android.schedulers.AndroidSchedulers.mainThread 11 | import org.coepi.android.system.rx.OperationState 12 | import org.coepi.android.system.rx.OperationState.Failure 13 | import org.coepi.android.system.rx.OperationState.NotStarted 14 | import org.coepi.android.system.rx.OperationState.Progress 15 | import org.coepi.android.system.rx.OperationState.Success 16 | 17 | fun Observable.toLiveData(): LiveData = 18 | fromPublisher(observeOn(mainThread()).toFlowable(BUFFER)) 19 | 20 | fun Observable>.toOperationState(): Observable> = 21 | flatMap { notification -> 22 | notification.toOperationState()?.let { just(it) } ?: empty() 23 | } 24 | 25 | fun Observable>.doOnNextSuccess(f: (T) -> Unit): Observable> = 26 | doOnNext { state -> 27 | if (state is Success) { 28 | f(state.data) 29 | } 30 | } 31 | 32 | fun Observable>.mapSuccess(f: (T) -> U): Observable> = 33 | map { state -> 34 | state.map(f) 35 | } 36 | 37 | fun Observable>.flatMapSuccess( 38 | f: (T) -> 39 | Observable> 40 | ): Observable> = 41 | flatMap { state: OperationState -> 42 | when (state) { 43 | is NotStarted -> just(NotStarted) 44 | is Success -> f(state.data) 45 | is Progress -> just(Progress) 46 | is Failure -> just(Failure(state.t)) 47 | } 48 | } 49 | 50 | fun Observable>.success(): Observable = 51 | flatMap { 52 | when (it) { 53 | is Success -> just(it.data) 54 | else -> empty() 55 | } 56 | } 57 | 58 | fun Observable>.toIsInProgress(): Observable = 59 | map { it is Progress } 60 | .onErrorReturnItem(false) 61 | 62 | fun Observable.asSequence(): Observable> = 63 | scan(emptyList(), { acc, element -> acc + listOf(element) }) 64 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/repo/SymptomRepo.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.repo 2 | 3 | import io.reactivex.Single 4 | import io.reactivex.Single.just 5 | import org.coepi.android.R.string.symptom_report_title_breathless 6 | import org.coepi.android.R.string.symptom_report_title_cough 7 | import org.coepi.android.R.string.symptom_report_title_diarrhea 8 | import org.coepi.android.R.string.symptom_report_title_fever 9 | import org.coepi.android.R.string.symptom_report_title_loss_smell 10 | import org.coepi.android.R.string.symptom_report_title_muscle_aches 11 | import org.coepi.android.R.string.symptom_report_title_no_symptoms 12 | import org.coepi.android.R.string.symptom_report_title_other 13 | import org.coepi.android.R.string.symptom_report_title_runny_nose 14 | import org.coepi.android.domain.model.Symptom 15 | import org.coepi.core.domain.model.SymptomId.BREATHLESSNESS 16 | import org.coepi.core.domain.model.SymptomId.COUGH 17 | import org.coepi.core.domain.model.SymptomId.DIARRHEA 18 | import org.coepi.core.domain.model.SymptomId.FEVER 19 | import org.coepi.core.domain.model.SymptomId.LOSS_SMELL_OR_TASTE 20 | import org.coepi.core.domain.model.SymptomId.MUSCLE_ACHES 21 | import org.coepi.core.domain.model.SymptomId.NONE 22 | import org.coepi.core.domain.model.SymptomId.OTHER 23 | import org.coepi.core.domain.model.SymptomId.RUNNY_NOSE 24 | import org.coepi.android.system.Resources 25 | 26 | interface SymptomRepo { 27 | fun symptoms(): Single> 28 | } 29 | 30 | class SymptomRepoImpl( 31 | private val resources: Resources 32 | ) : SymptomRepo { 33 | 34 | override fun symptoms(): Single> = just( 35 | listOf( 36 | Symptom(NONE, resources.getString(symptom_report_title_no_symptoms)), 37 | Symptom(COUGH, resources.getString(symptom_report_title_cough)), 38 | Symptom(BREATHLESSNESS, resources.getString(symptom_report_title_breathless)), 39 | Symptom(FEVER, resources.getString(symptom_report_title_fever)), 40 | Symptom(MUSCLE_ACHES, resources.getString(symptom_report_title_muscle_aches)), 41 | Symptom(LOSS_SMELL_OR_TASTE, resources.getString(symptom_report_title_loss_smell)), 42 | Symptom(DIARRHEA, resources.getString(symptom_report_title_diarrhea)), 43 | Symptom(RUNNY_NOSE, resources.getString(symptom_report_title_runny_nose)), 44 | Symptom(OTHER, resources.getString(symptom_report_title_other)) 45 | ) 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/repo/reportsupdate/NewAlertsNotificationShower.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.repo.reportsupdate 2 | 3 | import org.coepi.android.R.drawable 4 | import org.coepi.android.R.plurals 5 | import org.coepi.android.R.string 6 | import org.coepi.android.system.Resources 7 | import org.coepi.android.system.intent.IntentKey.NOTIFICATION_INFECTION_ARGS 8 | import org.coepi.android.system.intent.IntentNoValue 9 | import org.coepi.android.system.log.log 10 | import org.coepi.android.ui.notifications.AppNotificationChannels 11 | import org.coepi.android.ui.notifications.NotificationConfig 12 | import org.coepi.android.ui.notifications.NotificationIntentArgs 13 | import org.coepi.android.ui.notifications.NotificationPriority.HIGH 14 | import org.coepi.android.ui.notifications.NotificationsShower 15 | 16 | interface NewAlertsNotificationShower { 17 | fun showNotification(newAlertsCount: Int, notificationId: Int) 18 | fun cancelNotification(notificationId: Int) 19 | } 20 | 21 | class NewAlertsNotificationShowerImpl( 22 | private val notificationsShower: NotificationsShower, 23 | private val notificationChannelsInitializer: AppNotificationChannels, 24 | private val resources: Resources 25 | ) : NewAlertsNotificationShower { 26 | 27 | override fun showNotification(newAlertsCount: Int, notificationId: Int) { 28 | log.d("Showing notification...") 29 | notificationsShower.showNotification(notificationConfiguration(newAlertsCount, notificationId)) 30 | } 31 | 32 | override fun cancelNotification(notificationId: Int) { 33 | log.d("Canceling notification $notificationId") 34 | notificationsShower.cancelNotification(notificationId) 35 | } 36 | 37 | private fun notificationConfiguration(newAlertsCount: Int, noticationId: Int): NotificationConfig = 38 | NotificationConfig( 39 | drawable.ic_launcher_foreground, 40 | noticationId, 41 | resources.getString(string.infection_notification_title), 42 | resources.getQuantityString(plurals.alerts_new_notifications_count, newAlertsCount), 43 | HIGH, 44 | notificationChannelsInitializer.reportsChannelId, 45 | NotificationIntentArgs(NOTIFICATION_INFECTION_ARGS, IntentNoValue()) 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/AppCenterInitializer.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import android.app.Application 4 | import com.microsoft.appcenter.AppCenter.start 5 | import com.microsoft.appcenter.analytics.Analytics 6 | import com.microsoft.appcenter.crashes.Crashes 7 | import org.coepi.android.BuildConfig.DEBUG 8 | 9 | interface AppCenterInitializer { 10 | fun onActivityCreated() 11 | } 12 | 13 | class AppCenterInitializerImpl( 14 | private val application: Application 15 | ) : AppCenterInitializer { 16 | 17 | override fun onActivityCreated() { 18 | if (!DEBUG) { 19 | init() 20 | } 21 | } 22 | 23 | fun init() { 24 | start( 25 | application, 26 | "1c70b9e6-0458-4205-8bc3-8df5c5d29a0c", 27 | Analytics::class.java, 28 | Crashes::class.java 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/Clipboard.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import android.content.ClipData.newPlainText 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.content.Context.CLIPBOARD_SERVICE 7 | 8 | interface Clipboard { 9 | fun putInClipboard(text: String) 10 | } 11 | 12 | class ClipboardImpl(context: Context): Clipboard { 13 | private val clipboard = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager 14 | 15 | override fun putInClipboard(text: String) { 16 | clipboard.setPrimaryClip(newPlainText("Text", text)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/Email.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.content.Intent.ACTION_SEND 6 | import android.content.Intent.EXTRA_EMAIL 7 | import android.content.Intent.EXTRA_STREAM 8 | import android.content.Intent.EXTRA_SUBJECT 9 | import android.content.Intent.EXTRA_TEXT 10 | import android.content.Intent.createChooser 11 | import android.net.Uri 12 | import org.coepi.android.system.log.log 13 | 14 | interface Email { 15 | fun open(activity: Activity, recipient: String, subject: String, attachmentUri: Uri? = null, message: String = "") 16 | } 17 | 18 | class EmailImpl: Email { 19 | override fun open(activity: Activity, recipient: String, subject: String, attachmentUri: Uri?, message: String) { 20 | val intent = Intent(ACTION_SEND).apply { 21 | data = Uri.parse("mailto:") 22 | type = "message/rfc822" 23 | putExtra(EXTRA_EMAIL, arrayOf(recipient)) 24 | putExtra(EXTRA_SUBJECT, subject) 25 | putExtra(EXTRA_TEXT, message) 26 | putExtra(EXTRA_STREAM, attachmentUri) 27 | } 28 | try { 29 | activity.startActivity(createChooser(intent, "Choose Email Client...")) 30 | } catch (e: Exception) { 31 | log.e("Couldn't open email client: $e") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/EnvInfos.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import android.os.Build 4 | import android.os.Build.VERSION.RELEASE 5 | import org.coepi.android.BuildConfig.VERSION_CODE 6 | import org.coepi.android.BuildConfig.VERSION_NAME 7 | 8 | interface EnvInfos { 9 | val deviceName: String 10 | val appVersionName: String 11 | val appVersionCode: Int 12 | val osVersion: String 13 | } 14 | 15 | class EnvInfosImpl: EnvInfos { 16 | 17 | override val deviceName: String get() { 18 | val manufacturer = Build.MANUFACTURER 19 | val model = Build.MODEL 20 | return if (model.startsWith(manufacturer)) { 21 | model 22 | } else "$manufacturer $model" 23 | } 24 | override val appVersionName: String get() = VERSION_NAME 25 | override val appVersionCode: Int get() = VERSION_CODE 26 | 27 | override val osVersion: String get() = RELEASE 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/LocaleProvider.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import android.content.Context 4 | import android.os.Build.VERSION.SDK_INT 5 | import android.os.Build.VERSION_CODES.N 6 | import io.reactivex.Observable 7 | import io.reactivex.subjects.BehaviorSubject 8 | import io.reactivex.subjects.BehaviorSubject.createDefault 9 | import org.coepi.android.system.log.log 10 | import java.util.Locale 11 | 12 | interface LocaleProvider { 13 | val locale: Observable 14 | fun update() 15 | } 16 | 17 | class LocaleProviderImpl( 18 | private val context: Context 19 | ): LocaleProvider { 20 | override val locale: BehaviorSubject = createDefault(getPreferredLocale()) 21 | 22 | override fun update() { 23 | locale.onNext(getPreferredLocale().also { 24 | log.i("Updating locale to: $it") 25 | }) 26 | } 27 | 28 | private fun getPreferredLocale(): Locale = 29 | if (SDK_INT >= N) { 30 | context.resources.configuration.locales.get(0) ?: { 31 | log.w("Locales is empty. Falling back to (deprecated) main locale.") 32 | context.resources.configuration.locale 33 | }() 34 | } else { 35 | @Suppress("DEPRECATION") 36 | context.resources.configuration.locale 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/Resources.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import androidx.annotation.DrawableRes 6 | 7 | class Resources(private val context: Context) { 8 | fun getString(id: Int, vararg args: Any): String = context.getString(id, *args) 9 | 10 | fun getDrawable(@DrawableRes id: Int): Drawable? = context.getDrawable(id) 11 | 12 | fun getQuantityString(id: Int, quantity: Int): String = 13 | context.resources.getQuantityString(id, quantity, quantity.toString()) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/ScreenUnitsConverter.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import android.util.DisplayMetrics 4 | import android.util.DisplayMetrics.DENSITY_DEFAULT 5 | 6 | class ScreenUnitsConverter(private val displayMetrics: DisplayMetrics) { 7 | fun dpToPixel(dp: Float): Float = 8 | dp * (displayMetrics.densityDpi.toFloat() / DENSITY_DEFAULT) 9 | 10 | fun pixelsToDp(px: Float): Float = 11 | px / (displayMetrics.densityDpi.toFloat() / DENSITY_DEFAULT) 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/UnitsProvider.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import io.reactivex.Observable 4 | import org.coepi.core.domain.model.LengthtUnit 5 | import org.coepi.core.domain.model.LengthtUnit.FEET 6 | import org.coepi.core.domain.model.LengthtUnit.METERS 7 | import java.util.Locale 8 | 9 | // If we wanted this to be more generic, we'd have a component 10 | // for the unit system (metric/imperial) and another for the units. 11 | // For the time being we'll use only meter/feet so simplifying. 12 | interface UnitsProvider { 13 | val lengthUnit: Observable 14 | } 15 | 16 | class UnitsProviderImpl( 17 | localeProvider: LocaleProvider 18 | ): UnitsProvider { 19 | override val lengthUnit: Observable = localeProvider.locale.map { 20 | deriveLengthUnit(it) 21 | } 22 | 23 | private fun deriveLengthUnit(locale: Locale): LengthtUnit = 24 | when (locale.country.toUpperCase(locale)) { 25 | "US", "LR", "MM" -> FEET 26 | else -> METERS 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/WebLaunchEventEmitter.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import android.net.Uri 4 | import io.reactivex.subjects.PublishSubject 5 | import io.reactivex.subjects.PublishSubject.create 6 | 7 | interface WebLaunchEventEmitter { 8 | val uri: PublishSubject 9 | fun launch(uri: Uri) 10 | } 11 | 12 | class WebLaunchEventEmitterImpl : WebLaunchEventEmitter { 13 | override val uri: PublishSubject = create() 14 | 15 | override fun launch(uri: Uri) { 16 | this.uri.onNext(uri) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/WebLauncher.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.content.Intent.ACTION_VIEW 6 | import android.net.Uri 7 | 8 | interface WebLauncher { 9 | fun show(activity: Activity, uri: Uri) 10 | } 11 | 12 | class WebLauncherImpl : WebLauncher { 13 | override fun show(activity: Activity, uri: Uri) { 14 | val intent = Intent(ACTION_VIEW, uri) 15 | activity.startActivity(intent) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/intent/InfectionsNotificationIntentHandler.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.intent 2 | 3 | import android.content.Intent 4 | import org.coepi.android.system.intent.IntentKey.NOTIFICATION_INFECTION_ARGS 5 | import org.coepi.android.system.log.log 6 | import org.coepi.android.ui.alerts.AlertsFragmentDirections.Companion.actionGlobalAlerts 7 | import org.coepi.android.ui.navigation.NavigationCommand.ToDestination 8 | import org.coepi.android.ui.navigation.RootNavigation 9 | 10 | class InfectionsNotificationIntentHandler( 11 | private val navigation: RootNavigation, 12 | compositeIntentHandler: IntentForwarder 13 | ): IntentHandler { 14 | 15 | init { 16 | compositeIntentHandler.register(this) 17 | } 18 | 19 | override fun handle(intent: Intent) { 20 | if (!intent.hasExtra(NOTIFICATION_INFECTION_ARGS.toString())) return 21 | log.d("Opened infection notification, navigating to alerts") 22 | 23 | navigation.navigate(ToDestination(actionGlobalAlerts())) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/intent/IntentForwarder.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.intent 2 | 3 | import android.content.Intent 4 | 5 | interface IntentForwarder { 6 | fun onActivityCreated(intent: Intent) 7 | fun onNewIntent(intent: Intent?) 8 | 9 | fun register(intentHandler: IntentHandler) 10 | } 11 | 12 | class IntentForwarderImpl: IntentForwarder { 13 | private var handlers: List = emptyList() 14 | 15 | override fun onActivityCreated(intent: Intent) { 16 | handle(intent) 17 | } 18 | 19 | override fun onNewIntent(intent: Intent?) { 20 | intent?.let { handle(it) } 21 | } 22 | 23 | @Synchronized 24 | private fun handle(intent: Intent) { 25 | handlers.forEach { it.handle(intent) } 26 | } 27 | 28 | @Synchronized 29 | override fun register(intentHandler: IntentHandler) { 30 | handlers = handlers + intentHandler 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/intent/IntentHandler.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.intent 2 | 3 | import android.content.Intent 4 | 5 | interface IntentHandler { 6 | fun handle(intent: Intent) 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/intent/IntentKey.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.intent 2 | 3 | enum class IntentKey { 4 | NOTIFICATION_INFECTION_ARGS, 5 | NOTIFICATION_REMINDER_ARGS 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/intent/IntentNoValue.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.intent 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | class IntentNoValue(val notUsed: Int = 0): Parcelable 8 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/log/CachingLog.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.log 2 | 3 | import io.reactivex.disposables.CompositeDisposable 4 | import io.reactivex.rxkotlin.plusAssign 5 | import io.reactivex.rxkotlin.withLatestFrom 6 | import io.reactivex.subjects.BehaviorSubject.createDefault 7 | import io.reactivex.subjects.PublishSubject 8 | import org.coepi.android.system.log.LogLevel.D 9 | import org.coepi.android.system.log.LogLevel.E 10 | import org.coepi.android.system.log.LogLevel.I 11 | import org.coepi.android.system.log.LogLevel.V 12 | import org.coepi.android.system.log.LogLevel.W 13 | import org.coepi.android.ui.common.LimitedSizeQueue 14 | import java.util.Date 15 | 16 | class CachingLog : Log { 17 | val logs = createDefault>( 18 | LimitedSizeQueue(1000) 19 | ) 20 | 21 | private val addLogTrigger: PublishSubject = PublishSubject.create() 22 | private val disposables = CompositeDisposable() 23 | 24 | init { 25 | disposables += addLogTrigger.withLatestFrom(logs) 26 | .subscribe { (logMessage, logs) -> 27 | this.logs.onNext(logs.apply { add(logMessage) }) 28 | } 29 | } 30 | 31 | override fun setup() {} 32 | 33 | override fun v(message: String, tag: LogTag?) { 34 | log(LogMessage(V, addTag(tag, message))) 35 | } 36 | 37 | override fun d(message: String, tag: LogTag?) { 38 | log(LogMessage(D, addTag(tag, message))) 39 | } 40 | 41 | override fun i(message: String, tag: LogTag?) { 42 | log(LogMessage(I, addTag(tag, message))) 43 | } 44 | 45 | override fun w(message: String, tag: LogTag?) { 46 | log(LogMessage(W, addTag(tag, message))) 47 | } 48 | 49 | override fun e(message: String, tag: LogTag?) { 50 | log(LogMessage(E, addTag(tag, message))) 51 | } 52 | 53 | private fun log(message: LogMessage) { 54 | addLogTrigger.onNext(message) 55 | } 56 | 57 | private fun addTag(tag: LogTag?, message: String) = 58 | (tag?.let { "$it - " } ?: "") + message 59 | } 60 | 61 | data class LogMessage(val level: LogLevel, val text: String, val time: Date = Date()) 62 | 63 | enum class LogLevel(val text: String) { 64 | V("Verbose"), 65 | D("Debug"), 66 | I("Info"), 67 | W("Warn"), 68 | E("Error") 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/log/CompositeLog.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.log 2 | 3 | class CompositeLog(private vararg val logs: Log) : Log { 4 | 5 | override fun setup() { 6 | logs.forEach { it.setup() } 7 | } 8 | 9 | override fun v(message: String, tag: LogTag?) { 10 | logs.forEach { it.v(message, tag) } 11 | } 12 | 13 | override fun d(message: String, tag: LogTag?) { 14 | logs.forEach { it.d(message, tag) } 15 | } 16 | 17 | override fun i(message: String, tag: LogTag?) { 18 | logs.forEach { it.i(message, tag) } 19 | } 20 | 21 | override fun w(message: String, tag: LogTag?) { 22 | logs.forEach { it.w(message, tag) } 23 | } 24 | 25 | override fun e(message: String, tag: LogTag?) { 26 | logs.forEach { it.e(message, tag) } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/log/Log.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.log 2 | 3 | val cachingLog = CachingLog() 4 | val log: Log = CompositeLog( 5 | cachingLog, 6 | DefaultLog() 7 | ).apply { setup() } 8 | 9 | // To filter logcat by multiple tags, use regex, e.g. (tag1)|(tag2) 10 | enum class LogTag { 11 | BLE, // General BLE (can't be assigned to peripheral or central) 12 | NET, // Networking 13 | DB, // DB 14 | TCN_MATCHING, // Worker updating reports 15 | CORE, 16 | PERM 17 | } 18 | 19 | interface Log { 20 | fun setup() 21 | fun v(message: String, tag: LogTag? = null) 22 | fun d(message: String, tag: LogTag? = null) 23 | fun i(message: String, tag: LogTag? = null) 24 | fun w(message: String, tag: LogTag? = null) 25 | fun e(message: String, tag: LogTag? = null) 26 | } 27 | 28 | class DefaultLog : Log { 29 | private val tag = "LOGGER" 30 | 31 | override fun setup() {} 32 | 33 | override fun v(message: String, tag: LogTag?) { 34 | android.util.Log.v(tag.toString(), message) 35 | } 36 | 37 | override fun d(message: String, tag: LogTag?) { 38 | android.util.Log.d(tag.toString(), message) 39 | } 40 | 41 | override fun i(message: String, tag: LogTag?) { 42 | android.util.Log.i(tag.toString(), message) 43 | } 44 | 45 | override fun w(message: String, tag: LogTag?) { 46 | android.util.Log.w(tag.toString(), message) 47 | } 48 | 49 | override fun e(message: String, tag: LogTag?) { 50 | android.util.Log.e(tag.toString(), message) 51 | } 52 | 53 | private fun LogTag?.toString() = 54 | this?.toString() ?: tag 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/rx/ObservablePreferences.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.rx 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.subjects.BehaviorSubject 5 | import io.reactivex.subjects.BehaviorSubject.createDefault 6 | import org.coepi.android.system.Preferences 7 | import org.coepi.android.system.PreferencesKey.FILTER_ALERTS_WITH_LONG_DURATION 8 | import org.coepi.android.system.PreferencesKey.FILTER_ALERTS_WITH_SHORT_DISTANCE 9 | import org.coepi.android.system.PreferencesKey.FILTER_ALERTS_WITH_SYMPTOMS 10 | 11 | interface ObservablePreferences { 12 | val filterAlertsWithSymptoms: Observable 13 | val filterAlertsWithLongDuration: Observable 14 | val filterAlertsWithShortDistance: Observable 15 | 16 | fun setFilterAlertsWithSymptoms(value: Boolean) 17 | fun setFilterAlertsWithLongDuration(value: Boolean) 18 | fun setFilterAlertsWithShortDistance(value: Boolean) 19 | } 20 | 21 | class ObservablePreferencesImpl( 22 | private val keyValueStore: Preferences 23 | ): ObservablePreferences { 24 | override val filterAlertsWithSymptoms: BehaviorSubject = 25 | createDefault(keyValueStore.getBoolean(FILTER_ALERTS_WITH_SYMPTOMS)) 26 | override val filterAlertsWithLongDuration: BehaviorSubject = 27 | createDefault(keyValueStore.getBoolean(FILTER_ALERTS_WITH_LONG_DURATION)) 28 | override val filterAlertsWithShortDistance: BehaviorSubject = 29 | createDefault(keyValueStore.getBoolean(FILTER_ALERTS_WITH_SHORT_DISTANCE)) 30 | 31 | override fun setFilterAlertsWithSymptoms(value: Boolean) { 32 | keyValueStore.putBoolean(FILTER_ALERTS_WITH_SYMPTOMS, value) 33 | filterAlertsWithSymptoms.onNext(value) 34 | } 35 | 36 | override fun setFilterAlertsWithLongDuration(value: Boolean) { 37 | keyValueStore.putBoolean(FILTER_ALERTS_WITH_LONG_DURATION, value) 38 | filterAlertsWithLongDuration.onNext(value) 39 | } 40 | 41 | override fun setFilterAlertsWithShortDistance(value: Boolean) { 42 | keyValueStore.putBoolean(FILTER_ALERTS_WITH_SHORT_DISTANCE, value) 43 | filterAlertsWithShortDistance.onNext(value) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/rx/OperationForwarder.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.rx 2 | 3 | import io.reactivex.Observer 4 | import io.reactivex.functions.Consumer 5 | 6 | class OperationForwarder( 7 | private val observer: Observer> 8 | ): Consumer> { 9 | 10 | override fun accept(operationState: OperationState?) { 11 | if (operationState == null) { return } 12 | observer.onNext(operationState) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/rx/OperationState.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.rx 2 | 3 | /** 4 | * Represents the state of a long running operation 5 | * Can be used by UI to show progress indicator and success / error notifications 6 | */ 7 | sealed class OperationState { 8 | object NotStarted: OperationState() 9 | object Progress: OperationState() 10 | data class Success(val data: T): OperationState() 11 | data class Failure(val t: Throwable): OperationState() 12 | 13 | fun map(f: (T) -> U): OperationState = 14 | when (this) { 15 | is NotStarted -> NotStarted 16 | is Success -> Success(f(data)) 17 | is Progress -> Progress 18 | is Failure -> Failure(t) 19 | } 20 | 21 | fun flatMap(f: (T) -> OperationState): OperationState = 22 | when (this) { 23 | is NotStarted -> NotStarted 24 | is Success -> f(data) 25 | is Progress -> Progress 26 | is Failure -> Failure(t) 27 | } 28 | } 29 | 30 | typealias VoidOperationState = OperationState 31 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/system/rx/OperationStateConsumer.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.system.rx 2 | 3 | import io.reactivex.Notification 4 | import io.reactivex.Observer 5 | import io.reactivex.functions.Consumer 6 | import org.coepi.android.system.rx.OperationState.Failure 7 | import org.coepi.android.system.rx.OperationState.Success 8 | 9 | /** 10 | * Forwards operation success/error to observer as VoidOperationState 11 | */ 12 | class OperationStateNotifier( 13 | private val observer: Observer> 14 | ): Consumer> { 15 | 16 | override fun accept(notification: Notification?) { 17 | if (notification == null) { return } 18 | when { 19 | notification.isOnNext -> 20 | observer.onNext(notification.value?.let { value -> 21 | Success(value) 22 | } ?: Failure(IllegalStateException("Value is null")) ) 23 | notification.isOnError -> 24 | (notification.error ?: Throwable("Unknown error")).let { t -> 25 | observer.onNext(Failure(t)) 26 | } 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/alerts/AlertViewData.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.alerts 2 | 3 | import org.coepi.core.domain.model.Alert 4 | 5 | sealed class AlertCellViewData { 6 | data class Header(val text: String) : AlertCellViewData() 7 | data class Item(val viewData: AlertViewData) : AlertCellViewData() 8 | } 9 | 10 | data class AlertViewData( 11 | val exposureType: String, 12 | val contactTime: String, 13 | val contactTimeMonth: String, 14 | val showUnreadDot: Boolean, 15 | val showRepeatedInteraction: Boolean, 16 | val alert: Alert 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/alertsdetails/AlertDetailsViewData.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.alertsdetails 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | import org.coepi.core.domain.model.Alert 6 | 7 | @Parcelize 8 | data class AlertDetailsViewData( 9 | val title: String, 10 | val contactStart: String, 11 | val contactDuration: String, 12 | val avgDistance: String, 13 | val minDistance: String, 14 | val reportTime: String, 15 | val symptoms: String, 16 | val alert: Alert, 17 | val showOtherExposuresHeader: Boolean 18 | ) : Parcelable 19 | 20 | enum class LinkedAlertViewDataConnectionImage { 21 | Top, Body, Bottom; 22 | 23 | companion object { 24 | fun from(alertIndex: Int, alertsCount: Int): LinkedAlertViewDataConnectionImage = 25 | when (alertIndex) { 26 | 0 -> Top 27 | alertsCount - 1 -> Bottom 28 | else -> Body 29 | } 30 | } 31 | } 32 | 33 | data class LinkedAlertViewData( 34 | val date: String, 35 | val contactStart: String, 36 | val contactDuration: String, 37 | val symptoms: String, 38 | val alert: Alert, 39 | val image: LinkedAlertViewDataConnectionImage, 40 | val bottomLine: Boolean 41 | ) 42 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/alertsinfo/AlertsInfoFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.alertsinfo 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import org.coepi.android.databinding.FragmentExposureAlertsInformationBinding.inflate 9 | import org.koin.androidx.viewmodel.ext.android.viewModel 10 | 11 | class AlertsInfoFragment : Fragment() { 12 | private val viewModel by viewModel() 13 | 14 | override fun onCreateView( 15 | inflater: LayoutInflater, container: ViewGroup?, 16 | savedInstanceState: Bundle? 17 | ): View? = inflate(inflater, container, false).apply { 18 | lifecycleOwner = viewLifecycleOwner 19 | vm = viewModel 20 | }.root 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/alertsinfo/AlertsInfoViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.alertsinfo 2 | 3 | import android.net.Uri.parse 4 | import androidx.lifecycle.ViewModel 5 | import org.coepi.android.R.string.privacy_link 6 | import org.coepi.android.system.Resources 7 | import org.coepi.android.system.WebLaunchEventEmitter 8 | 9 | class AlertsInfoViewModel( 10 | private val resources: Resources, 11 | private val webLaunchEventEmitter: WebLaunchEventEmitter 12 | ) : ViewModel() { 13 | 14 | fun onClickPrivacyLink() { 15 | webLaunchEventEmitter.launch(parse(resources.getString(privacy_link))) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/common/ActivityFinisher.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.common 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.subjects.PublishSubject 5 | 6 | interface ActivityFinisher { 7 | val observable: Observable 8 | fun finish() 9 | } 10 | 11 | class ActivityFinisherImpl : ActivityFinisher { 12 | override val observable: PublishSubject = PublishSubject.create() 13 | 14 | override fun finish() { 15 | observable.onNext(Unit) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/common/KeyboardHider.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.common 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.view.View 6 | import android.view.inputmethod.InputMethodManager 7 | 8 | class KeyboardHider { 9 | fun hideKeyboard(context: Context, view: View) { 10 | val imm: InputMethodManager = 11 | context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 12 | imm.hideSoftInputFromWindow(view?.windowToken, 0) 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/common/LimitedSizeQueue.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.common 2 | 3 | class LimitedSizeQueue(private val maxSize: Int) : ArrayList() { 4 | 5 | override fun add(element: T): Boolean = 6 | super.add(element).also { 7 | if (size > maxSize) { 8 | removeRange(0, size - maxSize - 1) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/common/NotificationsObserver.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.common 2 | 3 | import android.app.Activity 4 | import androidx.lifecycle.Observer 5 | 6 | class NotificationsObserver(private val activity: Activity?) : Observer { 7 | override fun onChanged(notification: UINotificationData?) { 8 | notification?.let { notificationData -> 9 | activity?.let { activity -> 10 | UINotification().show(notificationData, activity) 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/common/SwipeToDeleteCallback.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.common 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.graphics.PorterDuff 6 | import android.graphics.PorterDuffXfermode 7 | import androidx.recyclerview.widget.ItemTouchHelper 8 | import androidx.recyclerview.widget.RecyclerView 9 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 10 | 11 | abstract class SwipeToDeleteCallback: 12 | ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) { 13 | 14 | private val clearPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) } 15 | 16 | override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: ViewHolder): Int { 17 | /** 18 | * To disable "swipe" for specific item return 0 here. 19 | * For example: 20 | * if (viewHolder?.itemViewType == YourAdapter.SOME_TYPE) return 0 21 | * if (viewHolder?.adapterPosition == 0) return 0 22 | */ 23 | 24 | if (viewHolder.adapterPosition == 10 || viewHolder.itemViewType == 0) return 0 25 | return super.getMovementFlags(recyclerView, viewHolder) 26 | } 27 | 28 | override fun onMove( 29 | recyclerView: RecyclerView, 30 | viewHolder: ViewHolder, 31 | target: ViewHolder 32 | ): Boolean { 33 | return false 34 | } 35 | 36 | override fun onChildDraw( 37 | c: Canvas, 38 | recyclerView: RecyclerView, 39 | viewHolder: ViewHolder, 40 | dX: Float, 41 | dY: Float, 42 | actionState: Int, 43 | isCurrentlyActive: Boolean 44 | ) { 45 | 46 | val itemView = viewHolder.itemView 47 | val isCanceled = dX == 0f && !isCurrentlyActive 48 | 49 | if (isCanceled) { 50 | clearCanvas( 51 | c, 52 | itemView.right + dX, 53 | itemView.top.toFloat(), 54 | itemView.right.toFloat(), 55 | itemView.bottom.toFloat() 56 | ) 57 | super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) 58 | return 59 | } 60 | 61 | super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) 62 | } 63 | 64 | private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) { 65 | c?.drawRect(left, top, right, bottom, clearPaint) 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/common/UINotification.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.common 2 | 3 | import android.app.Activity 4 | import com.tapadoo.alerter.Alerter.Companion.create 5 | import org.coepi.android.R.color.green 6 | import org.coepi.android.R.color.red 7 | import org.coepi.android.R.style.Text_Alert 8 | import org.coepi.android.ui.common.UINotificationData.Failure 9 | import org.coepi.android.ui.common.UINotificationData.Success 10 | 11 | class UINotification { 12 | fun show(notification: UINotificationData, activity: Activity) { 13 | when (notification) { 14 | is Success -> 15 | successAlert(notification.message, activity) 16 | is Failure -> 17 | errorAlert(notification.message, activity) 18 | }.show() 19 | } 20 | 21 | private fun successAlert(message: String, activity: Activity) = 22 | alert(message, activity) 23 | .setBackgroundColorRes(green) 24 | 25 | private fun errorAlert(message: String, activity: Activity) = 26 | alert(message, activity) 27 | // .setTitle("Error") 28 | .setBackgroundColorRes(red) 29 | 30 | private fun alert(message: String, activity: Activity) = 31 | create(activity) 32 | .hideIcon() 33 | .setTitleAppearance(Text_Alert) 34 | .setTextAppearance(Text_Alert) 35 | .setText(message) 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/common/UINotificationData.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.common 2 | 3 | sealed class UINotificationData(open val message: String) { 4 | data class Success(override val message: String): UINotificationData(message) 5 | data class Failure(override val message: String): UINotificationData(message) 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/common/UINotifier.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.common 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.subjects.PublishSubject 5 | 6 | interface UINotifier { 7 | val notifications: Observable 8 | 9 | fun notify(data: UINotificationData) 10 | } 11 | 12 | class UINotifierImpl: UINotifier { 13 | override val notifications: PublishSubject = PublishSubject.create() 14 | 15 | override fun notify(data: UINotificationData) { 16 | notifications.onNext(data) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/debug/DebugBleObservable.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.debug 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.subjects.BehaviorSubject 5 | import io.reactivex.subjects.BehaviorSubject.create 6 | import org.coepi.core.domain.model.Tcn 7 | 8 | interface DebugBleObservable { 9 | val myTcn: Observable 10 | val observedTcns: Observable 11 | 12 | fun setMyTcn(tcn: Tcn) 13 | fun setObservedTcn(tcn: Tcn) 14 | } 15 | 16 | class DebugBleObservableImpl: DebugBleObservable { 17 | override val myTcn: BehaviorSubject = create() 18 | override val observedTcns: BehaviorSubject = create() 19 | 20 | override fun setMyTcn(tcn: Tcn) { 21 | myTcn.onNext(tcn) 22 | } 23 | 24 | override fun setObservedTcn(tcn: Tcn) { 25 | observedTcns.onNext(tcn) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/debug/DebugFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.debug 2 | 3 | import android.graphics.Color.WHITE 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import com.google.android.material.tabs.TabLayout.Tab 10 | import com.google.android.material.tabs.TabLayoutMediator 11 | import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy 12 | import org.coepi.android.R.drawable.ic_close 13 | import org.coepi.android.databinding.FragmentDebugBinding.inflate 14 | import org.koin.androidx.viewmodel.ext.android.viewModel 15 | 16 | class DebugFragment: Fragment() { 17 | 18 | private val viewModel by viewModel() 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, 22 | container: ViewGroup?, 23 | savedInstanceState: Bundle? 24 | ): View? = inflate(inflater, container, false).apply { 25 | vm = viewModel 26 | 27 | toolbar.apply { 28 | setNavigationIcon(ic_close) 29 | setNavigationOnClickListener { 30 | viewModel.onCloseClick() 31 | } 32 | navigationIcon?.mutate()?.let { 33 | it.setTint(WHITE) 34 | navigationIcon = it 35 | } 36 | } 37 | 38 | val pagerAdapter = DebugPagerAdapter(this@DebugFragment) 39 | pager.adapter = pagerAdapter 40 | 41 | TabLayoutMediator(tabs, pager, 42 | TabConfigurationStrategy { tab: Tab, position: Int -> when (position) { 43 | 0 -> tab.text = "BLE" 44 | 1 -> tab.text = "Logs" 45 | }} 46 | ).attach() 47 | }.root 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/debug/DebugPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.debug 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.viewpager2.adapter.FragmentStateAdapter 5 | import org.coepi.android.ui.debug.ble.DebugBleFragment 6 | import org.coepi.android.ui.debug.logs.LogsFragment 7 | 8 | class DebugPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { 9 | override fun getItemCount(): Int = 2 10 | 11 | override fun createFragment(position: Int): Fragment = when (position) { 12 | 0 -> DebugBleFragment() 13 | 1 -> LogsFragment() 14 | else -> error("Not handled: $position") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/debug/DebugViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.debug 2 | 3 | import androidx.lifecycle.ViewModel 4 | import org.coepi.android.ui.navigation.NavigationCommand.Back 5 | import org.coepi.android.ui.navigation.RootNavigation 6 | 7 | class DebugViewModel( 8 | private val rootNav: RootNavigation 9 | ) : ViewModel() { 10 | 11 | fun onCloseClick() { 12 | rootNav.navigate(Back) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/debug/ble/DebugBleFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.debug.ble 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.RecyclerView.VERTICAL 10 | import org.coepi.android.databinding.FragmentDebugBleBinding.inflate 11 | import org.coepi.android.extensions.observeWith 12 | import org.koin.androidx.viewmodel.ext.android.viewModel 13 | 14 | class DebugBleFragment: Fragment() { 15 | 16 | private val viewModel by viewModel() 17 | 18 | override fun onCreateView( 19 | inflater: LayoutInflater, 20 | container: ViewGroup?, 21 | savedInstanceState: Bundle? 22 | ): View? = inflate(inflater, container, false).apply { 23 | vm = viewModel 24 | 25 | val bleAdapter = DebugBleRecyclerAdapter() 26 | 27 | logsRecyclerView.run { 28 | layoutManager = LinearLayoutManager(inflater.context, VERTICAL, false) 29 | adapter = bleAdapter 30 | } 31 | 32 | viewModel.items.observeWith(viewLifecycleOwner) { 33 | bleAdapter.submitList(it) 34 | } 35 | 36 | }.root 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/debug/ble/DebugBleItemViewData.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.debug.ble 2 | 3 | sealed class DebugBleItemViewData(open val text: String) { 4 | data class Header(override val text: String): DebugBleItemViewData(text) 5 | data class Item(override val text: String): DebugBleItemViewData(text) 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/debug/ble/DebugBleViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.debug.ble 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import io.reactivex.rxkotlin.Observables.combineLatest 6 | import org.coepi.android.extensions.rx.asSequence 7 | import org.coepi.android.extensions.rx.toLiveData 8 | import org.coepi.android.ui.debug.DebugBleObservable 9 | import org.coepi.android.ui.debug.ble.DebugBleItemViewData.Header 10 | import org.coepi.android.ui.debug.ble.DebugBleItemViewData.Item 11 | 12 | class DebugBleViewModel(debugBleObservable: DebugBleObservable) : ViewModel() { 13 | 14 | val items: LiveData> = combineLatest( 15 | debugBleObservable.myTcn.asSequence().map { it.distinct() }, 16 | debugBleObservable.observedTcns.asSequence().map { it.distinct() } 17 | 18 | ).map { (myTcns, observedTcns) -> 19 | listOf(Header("My TCN")) + 20 | myTcns.map { Item(it.toHex()) } + 21 | listOf(Header("Discovered")) + 22 | observedTcns.map { Item(it.toHex()) } 23 | }.toLiveData() 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/debug/logs/LogsAdapter.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.debug.logs 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.HapticFeedbackConstants.LONG_PRESS 5 | import android.view.LayoutInflater.from 6 | import android.view.ViewGroup 7 | import androidx.recyclerview.widget.DiffUtil.ItemCallback 8 | import androidx.recyclerview.widget.ListAdapter 9 | import androidx.recyclerview.widget.RecyclerView 10 | import org.coepi.android.databinding.ItemLogEntryBinding 11 | import org.coepi.android.databinding.ItemLogEntryBinding.inflate 12 | import org.coepi.android.ui.debug.logs.LogsRecyclerViewAdapter.ViewHolder 13 | 14 | class LogsRecyclerViewAdapter(private val onItemLongClick: () -> Unit) 15 | : ListAdapter(LogsDiffCallback()) { 16 | 17 | class ViewHolder( 18 | parent: ViewGroup, 19 | private val binding : ItemLogEntryBinding = 20 | inflate(from(parent.context), parent, false) 21 | ) : RecyclerView.ViewHolder(binding.root) { 22 | 23 | fun bind(item: LogMessageViewData, onItemClick: () -> Unit): Unit = binding.run { 24 | this.item = item 25 | time.setTextColor(item.textColor) 26 | message.setTextColor(item.textColor) 27 | 28 | root.setOnLongClickListener { 29 | root.performHapticFeedback(LONG_PRESS) 30 | onItemClick() 31 | true 32 | } 33 | } 34 | } 35 | 36 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = 37 | ViewHolder(parent) 38 | 39 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 40 | holder.bind(getItem(position), onItemLongClick) 41 | } 42 | } 43 | 44 | private class LogsDiffCallback : ItemCallback() { 45 | override fun areItemsTheSame(oldItem: LogMessageViewData, newItem: LogMessageViewData) 46 | : Boolean = 47 | oldItem === newItem 48 | 49 | @SuppressLint("DiffUtilEquals") 50 | // 2 messages with the same content should be handled as different, so identity based 51 | override fun areContentsTheSame(oldItem: LogMessageViewData, newItem: LogMessageViewData) 52 | : Boolean = 53 | oldItem === newItem 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/extensions/AdapterViewExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.extensions 2 | 3 | import android.view.View 4 | import android.widget.Adapter 5 | import android.widget.AdapterView 6 | 7 | fun AdapterView.onItemSelected(callback: (Int) -> Unit) { 8 | onItemSelectedListener = object : AdapterView.OnItemSelectedListener { 9 | override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { 10 | callback(position) 11 | } 12 | override fun onNothingSelected(parent: AdapterView<*>) {} 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/extensions/AlertExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.extensions 2 | 3 | import org.coepi.android.ui.extensions.ExposureDurationForUI.HoursMinutes 4 | import org.coepi.android.ui.extensions.ExposureDurationForUI.Minutes 5 | import org.coepi.android.ui.extensions.ExposureDurationForUI.Seconds 6 | import org.coepi.core.domain.model.Alert 7 | 8 | val Alert.durationSeconds: Long get() = contactEnd.value - contactStart.value 9 | 10 | val Alert.durationForUI: ExposureDurationForUI get() = when { 11 | durationSeconds >= secondsInHour -> { 12 | val (hours, mins) = secondsToHoursMinutes(durationSeconds) 13 | HoursMinutes(hours, mins) 14 | } 15 | durationSeconds >= secondsInMinute -> Minutes((durationSeconds / secondsInMinute).toInt()) 16 | else -> Seconds(durationSeconds.toInt()) 17 | } 18 | 19 | sealed class ExposureDurationForUI { 20 | data class Seconds(val value: Int): ExposureDurationForUI() 21 | data class Minutes(val value: Int): ExposureDurationForUI() 22 | data class HoursMinutes(val hours: Int, val minutes: Int): ExposureDurationForUI() 23 | } 24 | 25 | private fun secondsToHoursMinutes(seconds: Long): HourMinutes = HourMinutes( 26 | (seconds / secondsInHour).toInt(), 27 | (seconds % secondsInHour / secondsInMinute).toInt() 28 | ) 29 | 30 | private data class HourMinutes(val hours: Int, val minutes: Int) 31 | 32 | private const val secondsInHour: Int = 3600 33 | private const val secondsInMinute: Int = 60 34 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/extensions/FragmentExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.extensions 2 | 3 | import androidx.activity.OnBackPressedCallback 4 | import androidx.fragment.app.Fragment 5 | import androidx.navigation.fragment.findNavController 6 | 7 | fun Fragment.onBack(consume: Boolean = false, callback: () -> Unit) { 8 | activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner, 9 | object : OnBackPressedCallback(true) { 10 | override fun handleOnBackPressed() { 11 | callback() 12 | if (!consume) { 13 | // Emulate default back button behavior 14 | findNavController().navigateUp() 15 | } 16 | } 17 | } 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/extensions/TextViewExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.extensions 2 | 3 | import android.widget.TextView 4 | import androidx.core.widget.doOnTextChanged 5 | 6 | fun TextView.onTextChanged(f: (String) -> Unit) { 7 | doOnTextChanged { text, _, _, _ -> 8 | f(text?.toString() ?: "") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/extensions/rx/ObservableExtensions.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.extensions.rx 2 | 3 | import io.reactivex.Observable 4 | import io.reactivex.Observable.empty 5 | import io.reactivex.Observable.just 6 | import org.coepi.android.system.rx.OperationState 7 | import org.coepi.android.system.rx.OperationState.Failure 8 | import org.coepi.android.system.rx.OperationState.NotStarted 9 | import org.coepi.android.system.rx.OperationState.Progress 10 | import org.coepi.android.system.rx.OperationState.Success 11 | import org.coepi.android.ui.common.UINotificationData 12 | 13 | /** 14 | * Maps to error notifications and optionally a success notification for the operation state. 15 | */ 16 | fun Observable>.toNotification(successMessage: String? = null): Observable = 17 | flatMap { 18 | when (it) { 19 | is NotStarted -> empty() 20 | is Success -> successMessage?.let { 21 | just(UINotificationData.Success(successMessage)) 22 | } ?: empty() 23 | is Failure -> just(UINotificationData.Failure( 24 | it.t.message ?: "Unknown error" 25 | )) 26 | is Progress -> empty() 27 | } 28 | } 29 | 30 | fun Observable.filterFailure(): Observable = 31 | flatMap { 32 | when (it) { 33 | is UINotificationData.Failure -> just(UINotificationData.Failure(it.message)) 34 | else -> empty() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/formatters/DateFormatters.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.formatters 2 | 3 | import android.annotation.SuppressLint 4 | import java.text.SimpleDateFormat 5 | import java.util.Date 6 | 7 | object DateFormatters { 8 | val dotFormatter = DotDateFormatter() 9 | val hourMinuteFormatter = HourMinuteFormatter() 10 | val monthDayFormatter = MonthDayFormatter() 11 | val hourMinuteSecFormatter = HourMinuteSecFormatter() 12 | } 13 | 14 | class DotDateFormatter { 15 | @SuppressLint("SimpleDateFormat") 16 | val formatDayMonthYear = SimpleDateFormat("dd.MM.yyyy") 17 | 18 | fun formatDayMonthYear(date: Date): String = formatDayMonthYear.format(date) 19 | } 20 | 21 | class HourMinuteFormatter { 22 | @SuppressLint("SimpleDateFormat") 23 | val formatTime = SimpleDateFormat("h:mm a") 24 | 25 | fun formatTime(date: Date): String = formatTime.format(date) 26 | } 27 | 28 | class HourMinuteSecFormatter { 29 | @SuppressLint("SimpleDateFormat") 30 | val formatTime = SimpleDateFormat("h:mm:ss") 31 | 32 | fun formatTime(date: Date): String = formatTime.format(date) 33 | } 34 | 35 | class MonthDayFormatter { 36 | @SuppressLint("SimpleDateFormat") 37 | val formatMonthDay = SimpleDateFormat("MMM dd") 38 | 39 | fun formatMonthDay(date: Date): String = formatMonthDay.format(date) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/formatters/LengthFormatter.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.formatters 2 | 3 | import org.coepi.android.R.string.unit_length_format_feet 4 | import org.coepi.android.R.string.unit_length_format_meters 5 | import org.coepi.android.system.Resources 6 | import org.coepi.android.ui.formatters.NumberFormatters.twoDecimals 7 | import org.coepi.core.domain.model.Length 8 | import org.coepi.core.domain.model.LengthtUnit.FEET 9 | import org.coepi.core.domain.model.LengthtUnit.METERS 10 | 11 | class LengthFormatter( 12 | private val resources: Resources 13 | ) { 14 | fun format(measure: Length): String = 15 | when (measure.unit) { 16 | METERS -> resources.getString( 17 | unit_length_format_meters, 18 | twoDecimals.format(measure.value) 19 | ) 20 | FEET -> resources.getString( 21 | unit_length_format_feet, 22 | twoDecimals.format(measure.value) 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/formatters/NumberFormatters.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.formatters 2 | 3 | import java.text.DecimalFormat 4 | import java.text.NumberFormat 5 | 6 | object NumberFormatters { 7 | val oneDecimal: NumberFormat = DecimalFormat().apply { 8 | maximumFractionDigits = 1 9 | } 10 | val twoDecimals: NumberFormat = DecimalFormat().apply { 11 | maximumFractionDigits = 2 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/home/HomeAdapter.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.home 2 | 3 | import android.view.LayoutInflater.from 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil.ItemCallback 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import org.coepi.android.databinding.ItemHomeCardBinding 9 | import org.coepi.android.databinding.ItemHomeCardBinding.inflate 10 | import org.coepi.android.ui.home.HomeAdapter.ViewHolder 11 | 12 | class HomeAdapter( 13 | private val onItemClicked: (HomeCard) -> Unit 14 | ) : ListAdapter(HomeItemDiffCallback()) { 15 | 16 | class ViewHolder( 17 | private val parent: ViewGroup, private val binding: ItemHomeCardBinding = 18 | inflate(from(parent.context), parent, false) 19 | ) : RecyclerView.ViewHolder(binding.root) { 20 | 21 | fun bind(item: HomeCard, onClick: (HomeCard) -> Unit): Unit = binding.run { 22 | this.item = item 23 | root.setOnClickListener { 24 | onClick(item) 25 | } 26 | } 27 | } 28 | 29 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = 30 | ViewHolder(parent) 31 | 32 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 33 | holder.bind(getItem(position), onItemClicked) 34 | } 35 | } 36 | 37 | private class HomeItemDiffCallback : ItemCallback() { 38 | override fun areItemsTheSame(oldItem: HomeCard, newItem: HomeCard): Boolean = 39 | oldItem.id == newItem.id 40 | 41 | override fun areContentsTheSame(oldItem: HomeCard, newItem: HomeCard): Boolean = 42 | oldItem == newItem 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/home/HomeCard.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.home 2 | 3 | import android.os.Parcelable 4 | import kotlinx.android.parcel.Parcelize 5 | 6 | @Parcelize 7 | data class HomeCard( 8 | val id: HomeCardId, 9 | val title: String, 10 | val message: String, 11 | val hasNotification: Boolean = false, 12 | val notificationText: String = "", 13 | val titleVisible: Boolean = true 14 | ) : Parcelable 15 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/home/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.home 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.fragment.app.Fragment 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL 12 | import org.coepi.android.databinding.FragmentHomeBinding.inflate 13 | import org.coepi.android.extensions.observeWith 14 | import org.koin.androidx.viewmodel.ext.android.viewModel 15 | 16 | class HomeFragment : Fragment() { 17 | private val viewModel by viewModel() 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, container: ViewGroup?, 21 | savedInstanceState: Bundle? 22 | ): View? = inflate(inflater, container, false).apply { 23 | lifecycleOwner = viewLifecycleOwner 24 | vm = viewModel 25 | 26 | privacyLink.setOnClickListener { 27 | val webpage: Uri = Uri.parse("https://www.coepi.org/privacy/") 28 | val intent = Intent(Intent.ACTION_VIEW, webpage) 29 | startActivity(intent) 30 | } 31 | 32 | homeCardsRecyclerView.apply { 33 | setHasFixedSize(true) 34 | layoutManager = LinearLayoutManager(context, VERTICAL, false) 35 | } 36 | 37 | val adapter = HomeAdapter(onItemClicked = { item -> 38 | viewModel.onClicked(item) 39 | }) 40 | homeCardsRecyclerView.adapter = adapter 41 | 42 | viewModel.homeCardObservable.observeWith(viewLifecycleOwner) { 43 | adapter.submitList(it) 44 | } 45 | }.root 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/navigation/NavigationCommand.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.navigation 2 | 3 | import androidx.navigation.NavDirections 4 | import java.io.Serializable 5 | 6 | sealed class NavigationCommand : Serializable { 7 | data class ToDestination(val destinationId: NavDirections) : NavigationCommand() 8 | data class ToDirections(val directions: NavDirections) : NavigationCommand() 9 | object Back : NavigationCommand() 10 | data class BackTo(val destinationId: Int) : NavigationCommand() 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/navigation/Navigator.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.navigation 2 | 3 | import androidx.navigation.NavController 4 | import org.coepi.android.ui.navigation.NavigationCommand.Back 5 | import org.coepi.android.ui.navigation.NavigationCommand.BackTo 6 | import org.coepi.android.ui.navigation.NavigationCommand.ToDestination 7 | import org.coepi.android.ui.navigation.NavigationCommand.ToDirections 8 | 9 | class Navigator(private val navController: NavController) { 10 | 11 | fun navigate(command: NavigationCommand) { 12 | when (command) { 13 | is ToDirections -> navController.navigate(command.directions) 14 | is ToDestination -> navController.navigate(command.destinationId) 15 | is Back -> navController.popBackStack() 16 | is BackTo -> navController.popBackStack(command.destinationId, false) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/navigation/RootNavigation.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.navigation 2 | 3 | import io.reactivex.subjects.PublishSubject 4 | import io.reactivex.subjects.PublishSubject.create 5 | 6 | class RootNavigation { 7 | val navigationCommands: PublishSubject = create() 8 | 9 | fun navigate(command: NavigationCommand) { 10 | navigationCommands.onNext(command) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/notifications/AppNotificationChannels.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.notifications 2 | 3 | import android.app.NotificationManager.IMPORTANCE_DEFAULT 4 | import android.os.Build.VERSION.SDK_INT 5 | import android.os.Build.VERSION_CODES.N 6 | import android.os.Build.VERSION_CODES.O 7 | import androidx.annotation.RequiresApi 8 | import org.coepi.android.R.string.infection_notification_channel_description 9 | import org.coepi.android.R.string.infection_notification_channel_name 10 | import org.coepi.android.system.Resources 11 | import org.coepi.android.ui.notifications.LocalNotificationChannelId.INFECTION_REPORTS_CHANNEL 12 | 13 | /** 14 | * Initializes the app's notification channels and provides their ids. 15 | */ 16 | class AppNotificationChannels( 17 | private val channels: NotificationChannelsCreator, 18 | private val resources: Resources 19 | ) { 20 | val reportsChannelId: LocalNotificationChannelId = INFECTION_REPORTS_CHANNEL 21 | 22 | init { 23 | if (SDK_INT >= O) { 24 | channelConfigs().forEach { 25 | channels.createNotificationChannel(it) 26 | } 27 | } 28 | } 29 | 30 | @RequiresApi(N) 31 | private fun channelConfigs(): List = listOf( 32 | NotificationChannelConfig( 33 | reportsChannelId.toString(), 34 | resources.getString(infection_notification_channel_name), 35 | resources.getString(infection_notification_channel_description), 36 | IMPORTANCE_DEFAULT 37 | ) 38 | ) 39 | } 40 | 41 | enum class LocalNotificationChannelId { 42 | INFECTION_REPORTS_CHANNEL 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/notifications/NotificationChannelCreator.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.notifications 2 | 3 | import android.app.NotificationChannel 4 | import android.app.NotificationManager 5 | import android.content.Context 6 | import android.content.Context.NOTIFICATION_SERVICE 7 | import android.os.Build.VERSION_CODES 8 | import androidx.annotation.RequiresApi 9 | 10 | class NotificationChannelsCreator(private val context: Context) { 11 | 12 | @RequiresApi(VERSION_CODES.O) 13 | fun createNotificationChannel(config: NotificationChannelConfig) { 14 | val channel = NotificationChannel( 15 | config.id.toString(), config.name, 16 | config.importance 17 | ).apply { 18 | description = config.description 19 | } 20 | val notificationManager: NotificationManager = 21 | context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager 22 | notificationManager.createNotificationChannel(channel) 23 | } 24 | } 25 | 26 | data class NotificationChannelConfig( 27 | val id: String, 28 | val name: String, 29 | val description: String, 30 | val importance: Int 31 | ) 32 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/notifications/NotificationConfig.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.notifications 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.DrawableRes 5 | import org.coepi.android.system.intent.IntentKey 6 | 7 | data class NotificationConfig( 8 | @DrawableRes val smallIcon: Int, 9 | val notificationId: Int, 10 | val title: String, 11 | val text: String, 12 | val priority: NotificationPriority, 13 | val channelId: LocalNotificationChannelId, 14 | val intentArgs: NotificationIntentArgs 15 | ) 16 | 17 | enum class NotificationPriority { 18 | DEFAULT, LOW, MIN, HIGH, MAX 19 | } 20 | 21 | data class NotificationIntentArgs( 22 | val key: IntentKey, 23 | val value: Parcelable 24 | ) 25 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/notifications/ReminderAlarmHandler.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.notifications 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import org.coepi.android.system.log.log 7 | import org.koin.core.KoinComponent 8 | import org.koin.core.inject 9 | 10 | class ReminderAlarmHandler : BroadcastReceiver(), KoinComponent { 11 | 12 | private val reminderNotificationShower: ReminderNotificationShower by inject() 13 | internal val injectedContext: Context by inject() 14 | 15 | override fun onReceive(context: Context?, intent: Intent?) { 16 | val info : Int? = intent?.getIntExtra("code",0) 17 | log.d("[Reminder] received $info") 18 | reminderNotificationShower.showNotification(info ?: 0) 19 | } 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/notifications/ReminderAlarmHandlerExt.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.notifications 2 | 3 | import android.app.AlarmManager 4 | import android.app.PendingIntent 5 | import android.content.Context 6 | import android.content.Intent 7 | import org.coepi.android.system.log.log 8 | 9 | public fun ReminderAlarmHandler.cancelReminderWith(id: Int){ 10 | val alarmManager = injectedContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager 11 | val intent = Intent(injectedContext, ReminderAlarmHandler::class.java) 12 | val pendingIntent = PendingIntent.getBroadcast(injectedContext,id,intent, PendingIntent.FLAG_UPDATE_CURRENT) 13 | log.d("[Reminder] cancelling notification with id: $id") 14 | pendingIntent.cancel() 15 | alarmManager.cancel(pendingIntent) 16 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/notifications/ReminderNotificationShower.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.notifications 2 | 3 | import org.coepi.android.R.drawable 4 | import org.coepi.android.R.string 5 | import org.coepi.android.system.Resources 6 | import org.coepi.android.system.intent.IntentKey.NOTIFICATION_REMINDER_ARGS 7 | import org.coepi.android.system.intent.IntentNoValue 8 | import org.coepi.android.system.log.log 9 | import org.coepi.android.ui.notifications.NotificationPriority.HIGH 10 | 11 | interface ReminderNotificationShower { 12 | fun showNotification(notificationId: Int) 13 | } 14 | 15 | class ReminderNotificationShowerImpl( 16 | private val notificationsShower: NotificationsShower, 17 | private val notificationChannelsInitializer: AppNotificationChannels, 18 | private val resources: Resources 19 | ) : ReminderNotificationShower { 20 | 21 | override fun showNotification(notificationId: Int) { 22 | log.d("[Reminder] Showing reminder notification with id: $notificationId") 23 | val title = resources.getString(string.reminder_notification_title) 24 | val text = resources.getString(string.reminder_notification_text) 25 | notificationsShower.showNotification(notificationConfiguration(notificationId, title, text)) 26 | } 27 | 28 | private fun notificationConfiguration( notificationId: Int, title: String, text: String): NotificationConfig = 29 | NotificationConfig( 30 | drawable.ic_launcher_foreground, 31 | notificationId, 32 | title, 33 | text, 34 | HIGH, 35 | notificationChannelsInitializer.reportsChannelId, 36 | NotificationIntentArgs(NOTIFICATION_REMINDER_ARGS, IntentNoValue()) 37 | ) 38 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/onboarding/OnboardingCardViewData.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.onboarding 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | sealed class OnboardingCardViewData(val title: String, val message: CharSequence) { 6 | class SmallCard(title: String, message: CharSequence, val highlightedDot: Int, 7 | @DrawableRes val image: Int) 8 | : OnboardingCardViewData(title, message) 9 | 10 | class LargeCard(title: String, message: CharSequence) 11 | : OnboardingCardViewData(title, message) 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/onboarding/OnboardingClickEvent.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.onboarding 2 | 3 | sealed class OnboardingClickEvent { 4 | object NextClicked : OnboardingClickEvent() 5 | object PrivacyLinkClicked : OnboardingClickEvent() 6 | object JoinClicked : OnboardingClickEvent() 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/onboarding/OnboardingFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.onboarding 2 | 3 | import android.content.Intent 4 | import android.content.Intent.ACTION_VIEW 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.fragment.app.Fragment 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import org.coepi.android.databinding.FragmentOnboardingBinding.inflate 12 | import org.coepi.android.extensions.observeWith 13 | import org.coepi.android.ui.extensions.onBack 14 | import org.koin.androidx.viewmodel.ext.android.viewModel 15 | 16 | class OnboardingFragment : Fragment() { 17 | private val viewModel by viewModel() 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, container: ViewGroup?, 21 | savedInstanceState: Bundle? 22 | ): View? = inflate(inflater, container, false).apply { 23 | lifecycleOwner = viewLifecycleOwner 24 | vm = viewModel 25 | 26 | onboardingInfoRecycler.layoutManager = 27 | object : LinearLayoutManager(context, HORIZONTAL, false) { 28 | override fun canScrollHorizontally(): Boolean = false 29 | } 30 | 31 | onboardingInfoRecycler.adapter = OnboardingAdapter( 32 | viewModel.viewData, 33 | onEvent = viewModel::onCardEvent 34 | ) 35 | 36 | viewModel.openLink.observeWith(viewLifecycleOwner) { uri -> 37 | startActivity(Intent(ACTION_VIEW, uri)) 38 | } 39 | 40 | onBack(consume = true) { viewModel.onBack() } 41 | 42 | viewModel.recyclerViewScrollPosition.observeWith(viewLifecycleOwner) { position -> 43 | onboardingInfoRecycler.scrollToPosition(position) 44 | } 45 | }.root 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/onboarding/OnboardingShower.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.onboarding 2 | 3 | import org.coepi.android.system.Preferences 4 | import org.coepi.android.system.PreferencesKey.SEEN_ONBOARDING 5 | import org.coepi.android.ui.navigation.NavigationCommand.ToDirections 6 | import org.coepi.android.ui.navigation.RootNavigation 7 | import org.coepi.android.ui.onboarding.OnboardingFragmentDirections.Companion.actionGlobalOnboarding 8 | 9 | class OnboardingShower( 10 | private val rootNavigation: RootNavigation, 11 | private val preferences: Preferences 12 | ) { 13 | fun showIfNeeded() { 14 | if (preferences.getBoolean(SEEN_ONBOARDING).not()) { 15 | rootNavigation.navigate(ToDirections(actionGlobalOnboarding())) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/settings/UserSettingViewData.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.settings 2 | 3 | sealed class UserSettingViewData { 4 | data class SectionHeader(val title: String, val description: String) : UserSettingViewData() 5 | data class Toggle( 6 | val text: String, val value: Boolean, val hasBottomLine: Boolean, 7 | val id: UserSettingToggleId 8 | ) : UserSettingViewData() 9 | data class Text(val text: String, val id: UserSettingClickId, 10 | val hasBottomLine: Boolean) : UserSettingViewData() 11 | } 12 | 13 | enum class UserSettingToggleId { 14 | FILTER_ALERTS_WITH_SYMPTOMS, 15 | FILTER_ALERTS_WITH_LONG_DURATION, 16 | } 17 | 18 | enum class UserSettingClickId { 19 | PRIVACY_STATEMENT, 20 | REPORT_PROBLEM, 21 | APP_VERSION, 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/SymptomViewData.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms 2 | 3 | import org.coepi.android.domain.model.Symptom 4 | 5 | data class SymptomViewData(val name: String, val isChecked: Boolean, val symptom: Symptom) 6 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/SymptomsAdapter.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms 2 | 3 | import android.view.LayoutInflater.from 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil.ItemCallback 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import org.coepi.android.databinding.ItemSymptomBinding 9 | import org.coepi.android.databinding.ItemSymptomBinding.inflate 10 | import org.coepi.android.ui.symptoms.SymptomsAdapter.ViewHolder 11 | 12 | class SymptomsAdapter( 13 | private val onItemChecked: (SymptomViewData) -> Unit 14 | ) : ListAdapter(SymptomDiffCallback()) { 15 | 16 | class ViewHolder(private val parent: ViewGroup, private val binding: ItemSymptomBinding = 17 | inflate(from(parent.context), parent, false) 18 | ) : RecyclerView.ViewHolder(binding.root) { 19 | 20 | fun bind(item: SymptomViewData, onChecked: (SymptomViewData) -> Unit): Unit = binding.run { 21 | this.item = item 22 | checkbox.isChecked = item.isChecked 23 | root.setOnClickListener { 24 | onChecked(item) 25 | } 26 | } 27 | } 28 | 29 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = 30 | ViewHolder(parent) 31 | 32 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 33 | holder.bind(getItem(position), onItemChecked) 34 | } 35 | } 36 | 37 | private class SymptomDiffCallback : ItemCallback() { 38 | override fun areItemsTheSame(oldItem: SymptomViewData, newItem: SymptomViewData): Boolean = 39 | oldItem.symptom.id == newItem.symptom.id 40 | 41 | override fun areContentsTheSame(oldItem: SymptomViewData, newItem: SymptomViewData): Boolean = 42 | oldItem == newItem 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/SymptomsFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL 10 | import org.coepi.android.databinding.FragmentSymptomsBinding.inflate 11 | import org.coepi.android.extensions.observeWith 12 | import org.koin.androidx.viewmodel.ext.android.viewModel 13 | 14 | class SymptomsFragment : Fragment() { 15 | private val viewModel by viewModel() 16 | 17 | override fun onCreateView( 18 | inflater: LayoutInflater, container: ViewGroup?, 19 | savedInstanceState: Bundle? 20 | ): View? = inflate(inflater, container, false).apply { 21 | lifecycleOwner = viewLifecycleOwner 22 | vm = viewModel 23 | 24 | toolbar.setNavigationOnClickListener { viewModel.onBack() } 25 | 26 | productsRecyclerView.apply { 27 | setHasFixedSize(true) 28 | layoutManager = LinearLayoutManager(context, VERTICAL, false) 29 | } 30 | 31 | val adapter = SymptomsAdapter(onItemChecked = { item -> 32 | viewModel.onChecked(item) 33 | }) 34 | productsRecyclerView.adapter = adapter 35 | 36 | viewModel.symptoms.observeWith(viewLifecycleOwner) { 37 | adapter.submitList(it.toMutableList()) 38 | } 39 | }.root 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/breathless/BreathlessAdapter.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.breathless 2 | 3 | import android.view.LayoutInflater.from 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil.ItemCallback 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import org.coepi.android.databinding.ItemBreathlessBinding 9 | import org.coepi.android.databinding.ItemBreathlessBinding.inflate 10 | import org.coepi.android.ui.symptoms.breathless.BreathlessAdapter.ViewHolder 11 | 12 | class BreathlessAdapter( 13 | private val onItemChecked: (BreathlessViewData) -> Unit 14 | ) : ListAdapter(BreathlessDiffCallback()) { 15 | 16 | class ViewHolder(private val parent: ViewGroup, private val binding: ItemBreathlessBinding = 17 | inflate(from(parent.context), parent, false) 18 | ) : RecyclerView.ViewHolder(binding.root) { 19 | 20 | fun bind(item: BreathlessViewData, onChecked: (BreathlessViewData) -> Unit): Unit = binding.run { 21 | this.item = item 22 | checkbox.isChecked = item.isChecked 23 | root.setOnClickListener { 24 | onChecked(item) 25 | } 26 | } 27 | } 28 | 29 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = 30 | ViewHolder(parent) 31 | 32 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 33 | holder.bind(getItem(position), onItemChecked) 34 | } 35 | } 36 | 37 | private class BreathlessDiffCallback : ItemCallback() { 38 | override fun areItemsTheSame(oldItem: BreathlessViewData, newItem: BreathlessViewData): Boolean = 39 | oldItem.breathless == newItem.breathless 40 | 41 | override fun areContentsTheSame(oldItem: BreathlessViewData, newItem: BreathlessViewData): Boolean = 42 | oldItem == newItem 43 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/breathless/BreathlessFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.breathless 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL 10 | import org.coepi.android.databinding.FragmentBreathlessBinding.inflate 11 | import org.coepi.android.extensions.observeWith 12 | import org.coepi.android.ui.extensions.onBack 13 | import org.koin.androidx.viewmodel.ext.android.viewModel 14 | 15 | class BreathlessFragment : Fragment() { 16 | 17 | private val viewModel by viewModel() 18 | 19 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 20 | savedInstanceState: Bundle? 21 | ): View? = inflate(inflater, container, false).apply { 22 | lifecycleOwner = viewLifecycleOwner 23 | vm = viewModel 24 | 25 | onBack { viewModel.onBack() } 26 | toolbar.setNavigationOnClickListener { viewModel.onBackPressed() } 27 | 28 | causeRecyclerView.apply { 29 | setHasFixedSize(true) 30 | layoutManager = LinearLayoutManager(context, VERTICAL, false) 31 | } 32 | 33 | val adapter = BreathlessAdapter(onItemChecked = { item -> 34 | viewModel.onSelected(item) 35 | }) 36 | 37 | causeRecyclerView.adapter = adapter 38 | 39 | viewModel.causes.observeWith(viewLifecycleOwner) { 40 | adapter.submitList(it.toMutableList()) 41 | } 42 | }.root 43 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/breathless/BreathlessViewData.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.breathless 2 | 3 | import android.graphics.drawable.Drawable 4 | import org.coepi.core.domain.model.SymptomInputs.Breathlessness 5 | 6 | data class BreathlessViewData(val name: String, val icon: Drawable?, val isChecked: Boolean, 7 | val breathless: Breathlessness.Cause) -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/cough/CoughStatusAdapter.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.cough 2 | 3 | import android.view.LayoutInflater.from 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.DiffUtil.ItemCallback 6 | import androidx.recyclerview.widget.ListAdapter 7 | import androidx.recyclerview.widget.RecyclerView 8 | import org.coepi.android.databinding.ItemCoughStatusBinding 9 | import org.coepi.android.databinding.ItemCoughStatusBinding.inflate 10 | import org.coepi.android.ui.symptoms.cough.CoughStatusAdapter.ViewHolder 11 | 12 | class CoughStatusAdapter( 13 | private val onItemChecked: (CoughStatusViewData) -> Unit 14 | ) : ListAdapter(CoughStatusDiffCallback()) { 15 | 16 | class ViewHolder(private val parent: ViewGroup, private val binding: ItemCoughStatusBinding = 17 | inflate(from(parent.context), parent, false) 18 | ) : RecyclerView.ViewHolder(binding.root) { 19 | 20 | fun bind(item: CoughStatusViewData, onChecked: (CoughStatusViewData) -> Unit): Unit = binding.run { 21 | this.item = item 22 | checkbox.isChecked = item.isChecked 23 | root.setOnClickListener { 24 | onChecked(item) 25 | } 26 | } 27 | } 28 | 29 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = 30 | ViewHolder(parent) 31 | 32 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 33 | holder.bind(getItem(position), onItemChecked) 34 | } 35 | } 36 | 37 | private class CoughStatusDiffCallback : ItemCallback() { 38 | override fun areItemsTheSame(oldItem: CoughStatusViewData, newItem: CoughStatusViewData): Boolean = 39 | oldItem.status == newItem.status 40 | 41 | override fun areContentsTheSame(oldItem: CoughStatusViewData, newItem: CoughStatusViewData): Boolean = 42 | oldItem == newItem 43 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/cough/CoughStatusFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.cough 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.LinearLayoutManager 9 | import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL 10 | import org.coepi.android.databinding.FragmentCoughStatusBinding.inflate 11 | import org.coepi.android.extensions.observeWith 12 | import org.coepi.android.ui.extensions.onBack 13 | import org.koin.androidx.viewmodel.ext.android.viewModel 14 | 15 | class CoughStatusFragment : Fragment() { 16 | 17 | private val viewModel by viewModel() 18 | 19 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 20 | savedInstanceState: Bundle? 21 | ): View? = inflate(inflater, container, false).apply { 22 | lifecycleOwner = viewLifecycleOwner 23 | vm = viewModel 24 | 25 | onBack { viewModel.onBack() } 26 | toolbar.setNavigationOnClickListener { viewModel.onBackPressed() } 27 | 28 | statusRecyclerView.apply { 29 | setHasFixedSize(true) 30 | layoutManager = LinearLayoutManager(context, VERTICAL, false) 31 | } 32 | 33 | val adapter = CoughStatusAdapter(onItemChecked = { item -> 34 | viewModel.onSelected(item) 35 | }) 36 | 37 | statusRecyclerView.adapter = adapter 38 | 39 | viewModel.statuses.observeWith(viewLifecycleOwner) { 40 | adapter.submitList(it.toMutableList()) 41 | } 42 | }.root 43 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/cough/CoughStatusViewData.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.cough 2 | 3 | import org.coepi.core.domain.model.SymptomInputs.Cough 4 | 5 | data class CoughStatusViewData(val name: String, val isChecked: Boolean, val status: Cough.Status) -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/cough/CoughTypeFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.cough 2 | 3 | import androidx.fragment.app.Fragment 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import org.coepi.android.databinding.FragmentCoughTypeBinding.inflate 9 | import org.coepi.android.ui.extensions.onBack 10 | import org.koin.androidx.viewmodel.ext.android.viewModel 11 | 12 | class CoughTypeFragment : Fragment() { 13 | 14 | private val viewModel by viewModel() 15 | 16 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 17 | savedInstanceState: Bundle? 18 | ): View? = inflate(inflater, container, false).apply { 19 | lifecycleOwner = viewLifecycleOwner 20 | vm = viewModel 21 | 22 | onBack { viewModel.onBack() } 23 | toolbar.setNavigationOnClickListener { viewModel.onBackPressed() } 24 | }.root 25 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/cough/CoughTypeViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.cough 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import io.reactivex.android.schedulers.AndroidSchedulers.mainThread 6 | import org.coepi.android.domain.symptomflow.SymptomFlowManager 7 | import org.coepi.android.extensions.rx.toIsInProgress 8 | import org.coepi.android.extensions.rx.toLiveData 9 | import org.coepi.android.ui.navigation.NavigationCommand.Back 10 | import org.coepi.android.ui.navigation.RootNavigation 11 | import org.coepi.core.domain.model.SymptomInputs.Cough.Type.DRY 12 | import org.coepi.core.domain.model.SymptomInputs.Cough.Type.WET 13 | import org.coepi.core.domain.model.UserInput.Some 14 | 15 | class CoughTypeViewModel( 16 | private val navigation: RootNavigation, 17 | private val symptomFlowManager: SymptomFlowManager 18 | ) : ViewModel() { 19 | 20 | val isInProgress: LiveData = symptomFlowManager.submitSymptomsState 21 | .toIsInProgress() 22 | .observeOn(mainThread()) 23 | .toLiveData() 24 | 25 | fun onClickWet() { 26 | symptomFlowManager.setCoughType(Some(WET)) 27 | symptomFlowManager.navigateForward() 28 | } 29 | 30 | fun onClickDry() { 31 | symptomFlowManager.setCoughType(Some(DRY)) 32 | symptomFlowManager.navigateForward() 33 | } 34 | 35 | fun onClickSkip() { 36 | symptomFlowManager.navigateForward() 37 | } 38 | 39 | fun onBack() { 40 | symptomFlowManager.onBack() 41 | } 42 | 43 | fun onBackPressed() { 44 | onBack() 45 | navigation.navigate(Back) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/earliestsymptom/EarliestSymptomFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.earliestsymptom 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import org.coepi.android.databinding.FragmentEarliestSymptomBinding.inflate 9 | import org.coepi.android.ui.common.KeyboardHider 10 | import org.coepi.android.ui.extensions.onBack 11 | import org.coepi.android.ui.extensions.onTextChanged 12 | import org.koin.androidx.viewmodel.ext.android.viewModel 13 | 14 | class EarliestSymptomFragment : Fragment() { 15 | 16 | private val viewModel by viewModel() 17 | 18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 19 | savedInstanceState: Bundle?) 20 | : View? = inflate(inflater, container, false).apply { 21 | lifecycleOwner = viewLifecycleOwner 22 | vm = viewModel 23 | 24 | earliestSymptom.onTextChanged { 25 | viewModel.onDurationChanged(it) 26 | } 27 | 28 | 29 | onBack { viewModel.onBack() } 30 | toolbar.setNavigationOnClickListener { viewModel.onBackPressed() } 31 | }.root 32 | 33 | override fun onStop() { 34 | KeyboardHider().hideKeyboard(this.requireContext(), this.requireView()) 35 | super.onStop() 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/earliestsymptom/EarliestSymptomViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.earliestsymptom 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import io.reactivex.android.schedulers.AndroidSchedulers 6 | import org.coepi.android.domain.symptomflow.SymptomFlowManager 7 | import org.coepi.android.extensions.rx.toIsInProgress 8 | import org.coepi.android.extensions.rx.toLiveData 9 | import org.coepi.android.ui.navigation.NavigationCommand.Back 10 | import org.coepi.android.ui.navigation.RootNavigation 11 | import org.coepi.core.domain.model.UserInput.None 12 | import org.coepi.core.domain.model.UserInput.Some 13 | 14 | class EarliestSymptomViewModel( 15 | val navigation: RootNavigation, 16 | private val symptomFlowManager: SymptomFlowManager 17 | ) : ViewModel() { 18 | 19 | val isInProgress: LiveData = symptomFlowManager.submitSymptomsState 20 | .toIsInProgress() 21 | .observeOn(AndroidSchedulers.mainThread()) 22 | .toLiveData() 23 | 24 | 25 | fun onDurationChanged(durationStr: String) { 26 | if (durationStr.isEmpty()) { 27 | symptomFlowManager.setEarliestSymptomStartedDaysAgo(None) 28 | } else { 29 | val duration: Int = durationStr.toIntOrNull() ?: error("Invalid input: $durationStr") 30 | symptomFlowManager.setEarliestSymptomStartedDaysAgo(Some(duration)) 31 | } 32 | } 33 | 34 | fun onClickSubmit() { 35 | symptomFlowManager.navigateForward() 36 | } 37 | 38 | fun onClickUnknown() { 39 | symptomFlowManager.navigateForward() 40 | } 41 | 42 | fun onClickSkip() { 43 | symptomFlowManager.navigateForward() 44 | } 45 | 46 | fun onBack() { 47 | symptomFlowManager.onBack() 48 | } 49 | 50 | fun onBackPressed() { 51 | onBack() 52 | navigation.navigate(Back) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/fever/FeverHighestTemperatureFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.fever 2 | 3 | import androidx.fragment.app.Fragment 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import org.coepi.android.databinding.FragmentFeverHighestTemperatureBinding.inflate 9 | import org.coepi.android.ui.common.KeyboardHider 10 | import org.coepi.android.ui.extensions.onBack 11 | import org.coepi.android.ui.extensions.onTextChanged 12 | import org.koin.androidx.viewmodel.ext.android.viewModel 13 | 14 | class FeverHighestTemperatureFragment : Fragment() { 15 | 16 | private val viewModel by viewModel() 17 | 18 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 19 | savedInstanceState: Bundle? 20 | ): View? = inflate(inflater, container, false).apply { 21 | lifecycleOwner = viewLifecycleOwner 22 | vm = viewModel 23 | 24 | feverTemp.onTextChanged { 25 | viewModel.onTempChanged(it) 26 | } 27 | 28 | onBack { viewModel.onBack() } 29 | toolbar.setNavigationOnClickListener { viewModel.onBackPressed() } 30 | }.root 31 | 32 | override fun onStop() { 33 | KeyboardHider().hideKeyboard(this.requireContext(), this.requireView()) 34 | super.onStop() 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/fever/FeverHighestTemperatureViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.fever 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import io.reactivex.android.schedulers.AndroidSchedulers.mainThread 6 | import org.coepi.android.domain.symptomflow.SymptomFlowManager 7 | import org.coepi.android.extensions.rx.toIsInProgress 8 | import org.coepi.android.extensions.rx.toLiveData 9 | import org.coepi.android.ui.navigation.NavigationCommand.Back 10 | import org.coepi.android.ui.navigation.RootNavigation 11 | import org.coepi.core.domain.model.Temperature.Fahrenheit 12 | import org.coepi.core.domain.model.UserInput.None 13 | import org.coepi.core.domain.model.UserInput.Some 14 | 15 | class FeverHighestTemperatureViewModel( 16 | val navigation: RootNavigation, 17 | private val symptomFlowManager: SymptomFlowManager 18 | ) : ViewModel() { 19 | 20 | val isInProgress: LiveData = symptomFlowManager.submitSymptomsState 21 | .toIsInProgress() 22 | .observeOn(mainThread()) 23 | .toLiveData() 24 | 25 | // TODO Hardcoded to use Fahrenheit, add ability to select scale 26 | fun onTempChanged(tempStr: String) { 27 | if (tempStr.isEmpty()) { 28 | symptomFlowManager.setFeverHighestTemperatureTaken(None) 29 | } else { 30 | val temperature: Float= tempStr.toFloatOrNull() ?: error("Invalid input: $tempStr") 31 | symptomFlowManager.setFeverHighestTemperatureTaken(Some(Fahrenheit(temperature))) 32 | } 33 | } 34 | 35 | fun onClickSubmit() { 36 | symptomFlowManager.navigateForward() 37 | } 38 | 39 | fun onClickUnknown() { 40 | symptomFlowManager.navigateForward() 41 | } 42 | 43 | fun onClickSkip() { 44 | symptomFlowManager.navigateForward() 45 | } 46 | 47 | fun onBack() { 48 | symptomFlowManager.onBack() 49 | } 50 | 51 | fun onBackPressed() { 52 | onBack() 53 | navigation.navigate(Back) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/fever/FeverTakenTodayFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.fever 2 | 3 | import androidx.fragment.app.Fragment 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import org.coepi.android.databinding.FragmentFeverTakenTodayBinding.inflate 9 | import org.coepi.android.ui.extensions.onBack 10 | import org.koin.androidx.viewmodel.ext.android.viewModel 11 | 12 | class FeverTakenTodayFragment : Fragment() { 13 | 14 | private val viewModel by viewModel() 15 | 16 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 17 | savedInstanceState: Bundle? 18 | ): View? = inflate(inflater, container, false).apply { 19 | lifecycleOwner = viewLifecycleOwner 20 | vm = viewModel 21 | 22 | onBack { viewModel.onBack() } 23 | toolbar.setNavigationOnClickListener { viewModel.onBackPressed() } 24 | }.root 25 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/fever/FeverTakenTodayViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.fever 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import io.reactivex.android.schedulers.AndroidSchedulers.mainThread 6 | import org.coepi.android.domain.symptomflow.SymptomFlowManager 7 | import org.coepi.android.domain.symptomflow.SymptomStep.FEVER_HIGHEST_TEMPERATURE 8 | import org.coepi.android.extensions.rx.toIsInProgress 9 | import org.coepi.android.extensions.rx.toLiveData 10 | import org.coepi.android.ui.navigation.NavigationCommand.Back 11 | import org.coepi.android.ui.navigation.RootNavigation 12 | import org.coepi.core.domain.model.UserInput.Some 13 | 14 | class FeverTakenTodayViewModel( 15 | private val navigation: RootNavigation, 16 | private val symptomFlowManager: SymptomFlowManager 17 | ) : ViewModel() { 18 | 19 | val isInProgress: LiveData = symptomFlowManager.submitSymptomsState 20 | .toIsInProgress() 21 | .observeOn(mainThread()) 22 | .toLiveData() 23 | 24 | // TODO toggle? Does it make sense to let user clear selection? 25 | 26 | fun onClickYes() { 27 | symptomFlowManager.setFeverTakenTemperatureToday(Some(true)) 28 | symptomFlowManager.addUniqueStepAfterCurrent(FEVER_HIGHEST_TEMPERATURE) 29 | symptomFlowManager.navigateForward() 30 | } 31 | 32 | fun onClickNo() { 33 | symptomFlowManager.setFeverTakenTemperatureToday(Some(false)) 34 | symptomFlowManager.removeIfPresent(FEVER_HIGHEST_TEMPERATURE) 35 | symptomFlowManager.navigateForward() 36 | } 37 | 38 | fun onClickSkip() { 39 | symptomFlowManager.navigateForward() 40 | } 41 | 42 | fun onBack() { 43 | symptomFlowManager.onBack() 44 | } 45 | 46 | fun onBackPressed() { 47 | onBack() 48 | navigation.navigate(Back) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/fever/FeverTemperatureSpotFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.fever 2 | 3 | import androidx.fragment.app.Fragment 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import org.coepi.android.databinding.FragmentFeverTemperatureSpotBinding.inflate 9 | import org.coepi.android.ui.common.KeyboardHider 10 | import org.coepi.android.ui.extensions.onBack 11 | import org.koin.androidx.viewmodel.ext.android.viewModel 12 | 13 | class FeverTemperatureSpotFragment : Fragment() { 14 | 15 | private val viewModel by viewModel() 16 | 17 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 18 | savedInstanceState: Bundle? 19 | ): View? = inflate(inflater, container, false).apply { 20 | lifecycleOwner = viewLifecycleOwner 21 | vm = viewModel 22 | 23 | onBack { viewModel.onBack() } 24 | toolbar.setNavigationOnClickListener { viewModel.onBackPressed() } 25 | }.root 26 | 27 | override fun onStop() { 28 | KeyboardHider().hideKeyboard(this.requireContext(), this.requireView()) 29 | super.onStop() 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/symptoms/fever/FeverTemperatureSpotViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.symptoms.fever 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.ViewModel 5 | import io.reactivex.android.schedulers.AndroidSchedulers.mainThread 6 | import org.coepi.android.domain.symptomflow.SymptomFlowManager 7 | import org.coepi.android.extensions.rx.toIsInProgress 8 | import org.coepi.android.extensions.rx.toLiveData 9 | import org.coepi.android.ui.navigation.NavigationCommand.Back 10 | import org.coepi.android.ui.navigation.RootNavigation 11 | import org.coepi.core.domain.model.SymptomInputs.Fever.TemperatureSpot 12 | import org.coepi.core.domain.model.SymptomInputs.Fever.TemperatureSpot.Armpit 13 | import org.coepi.core.domain.model.SymptomInputs.Fever.TemperatureSpot.Ear 14 | import org.coepi.core.domain.model.SymptomInputs.Fever.TemperatureSpot.Mouth 15 | import org.coepi.core.domain.model.SymptomInputs.Fever.TemperatureSpot.Other 16 | import org.coepi.core.domain.model.UserInput.Some 17 | 18 | class FeverTemperatureSpotViewModel ( 19 | private val navigation: RootNavigation, 20 | private val symptomFlowManager: SymptomFlowManager 21 | ) : ViewModel() { 22 | 23 | val isInProgress: LiveData = symptomFlowManager.submitSymptomsState 24 | .toIsInProgress() 25 | .observeOn(mainThread()) 26 | .toLiveData() 27 | 28 | fun onClickMouth() { 29 | onSelectSpot(Mouth) 30 | } 31 | 32 | fun onClickEar() { 33 | onSelectSpot(Ear) 34 | } 35 | 36 | fun onClickArmpit() { 37 | onSelectSpot(Armpit) 38 | } 39 | 40 | fun onClickOther() { 41 | onSelectSpot(Other) 42 | } 43 | 44 | private fun onSelectSpot(spot: TemperatureSpot) { 45 | symptomFlowManager.setFeverTakenTemperatureSpot(Some(spot)) 46 | symptomFlowManager.navigateForward() 47 | } 48 | 49 | fun onClickSkip() { 50 | symptomFlowManager.navigateForward() 51 | } 52 | 53 | fun onBack() { 54 | symptomFlowManager.onBack() 55 | } 56 | 57 | fun onBackPressed() { 58 | onBack() 59 | navigation.navigate(Back) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/thanks/ThanksFragment.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.thanks 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import org.coepi.android.databinding.FragmentThanksBinding.inflate 9 | import org.koin.androidx.viewmodel.ext.android.viewModel 10 | 11 | class ThanksFragment : Fragment() { 12 | 13 | private val viewModel by viewModel() 14 | 15 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, 16 | savedInstanceState: Bundle? 17 | ): View? = inflate(inflater, container, false).apply { 18 | lifecycleOwner = viewLifecycleOwner 19 | vm = viewModel 20 | }.root 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/thanks/ThanksViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.thanks 2 | 3 | import androidx.lifecycle.ViewModel 4 | import org.coepi.android.NavGraphRootDirections.Companion.actionGlobalHomeFragment 5 | import org.coepi.android.ui.alerts.AlertsFragmentDirections.Companion.actionGlobalAlerts 6 | import org.coepi.android.ui.navigation.NavigationCommand.ToDestination 7 | import org.coepi.android.ui.navigation.RootNavigation 8 | 9 | class ThanksViewModel( 10 | private val navigation: RootNavigation 11 | ) : ViewModel() { 12 | 13 | fun onSeeAlertsClick() { 14 | navigation.navigate(ToDestination(actionGlobalAlerts())) 15 | } 16 | 17 | fun onCloseClick() { 18 | navigation.navigate(ToDestination(actionGlobalHomeFragment())) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/ui/xmlbindingadapters/XmlBindingAdapters.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.ui.xmlbindingadapters 2 | 3 | import android.view.View 4 | import android.view.View.GONE 5 | import android.view.View.VISIBLE 6 | import android.widget.ImageView 7 | import androidx.databinding.BindingAdapter 8 | 9 | object XmlBindingAdapters { 10 | 11 | @JvmStatic 12 | @BindingAdapter("isVisible") 13 | fun setIsVisible(view: View, isVisible: Boolean?) { 14 | view.visibility = if (isVisible == true) VISIBLE else GONE 15 | } 16 | 17 | @JvmStatic 18 | @BindingAdapter("android:src") 19 | fun setImageResource(imageView: ImageView, resource: Int) { 20 | imageView.setImageResource(resource) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/worker/tcnfetcher/ContactsFetchManager.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.worker.tcnfetcher 2 | 3 | import android.content.Context 4 | import androidx.work.Constraints 5 | import androidx.work.ExistingPeriodicWorkPolicy.REPLACE 6 | import androidx.work.NetworkType.CONNECTED 7 | import androidx.work.PeriodicWorkRequest 8 | import androidx.work.PeriodicWorkRequest.Builder 9 | import androidx.work.WorkManager 10 | import java.util.concurrent.TimeUnit.MINUTES 11 | 12 | class ContactsFetchManager(context: Context) { 13 | 14 | init { 15 | val workManager = WorkManager.getInstance(context) 16 | workManager.enqueueUniquePeriodicWork("tcns_fetch_worker", REPLACE, 17 | createWorkerRequest()) 18 | } 19 | 20 | private fun createWorkerRequest(): PeriodicWorkRequest { 21 | val constraints: Constraints = Constraints.Builder() 22 | .setRequiredNetworkType(CONNECTED) 23 | .build() 24 | 25 | return Builder(ContactsFetchWorker::class.java, 15L, MINUTES) 26 | // .setInitialDelay(1, SECONDS) // If using BLE simulator, ensure it can store keys first 27 | .setConstraints(constraints) 28 | .build() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/org/coepi/android/worker/tcnfetcher/ContactsFetchWorker.kt: -------------------------------------------------------------------------------- 1 | package org.coepi.android.worker.tcnfetcher 2 | 3 | import android.content.Context 4 | import androidx.work.CoroutineWorker 5 | import androidx.work.ListenableWorker.Result.success 6 | import androidx.work.WorkerParameters 7 | import org.coepi.android.repo.AlertsRepo 8 | import org.coepi.android.system.log.LogTag.TCN_MATCHING 9 | import org.coepi.android.system.log.log 10 | import org.koin.core.KoinComponent 11 | import org.koin.core.inject 12 | 13 | class ContactsFetchWorker( 14 | appContext: Context, 15 | workerParams: WorkerParameters 16 | ) : CoroutineWorker(appContext, workerParams), KoinComponent { 17 | 18 | private val alertsRepo: AlertsRepo by inject() 19 | 20 | override suspend fun doWork(): Result { 21 | log.d("Contacts fetch worker started.", TCN_MATCHING) 22 | alertsRepo.requestUpdateReports() 23 | log.d("Contacts fetch worker finished.", TCN_MATCHING) 24 | return success() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/color/button_text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/color/item_symptom_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_black_rounded_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_edittext.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_onboarding_card.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_rounded_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_shadowed_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle_shadowed_button_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dark_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_alert.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_alert_circle.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_breathless_exercise.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_breathless_ground.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 45 | 49 | 53 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_breathless_house.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_coepi_cloud.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_contact_alert_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_family_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_geometric_dark_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 15 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_geometric_light_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 12 | 18 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_health_icon.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/item_symptom_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple_background_black_rounded_border.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple_background_rounded_border.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple_dark_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple_shadowed_rounded_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple_submit_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ripple_symptom_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_button_shape.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_shadowed_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_shadowed_button_deselected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_shadowed_button_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_shadowed_dark_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_shadowed_submit_button_disabled.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_shadowed_submit_button_enabled.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/stepper_icon_selected.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/stepper_icon_unselected.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/submit_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_debug.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 16 | 17 | 27 | 28 | 37 | 38 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_debug_ble.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 16 | 17 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_logs.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 15 | 16 | 24 | 30 | 31 | 36 | 37 | 38 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_onboarding.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 11 | 12 | 13 | 21 | 22 | 30 | 31 | 43 | 44 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_user_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 16 | 17 | 26 | 27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_alert_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_breathless.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 17 | 18 | 24 | 25 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_cough_status.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_debug_ble_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 18 | 19 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_debug_ble_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 18 | 19 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_log_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 18 | 19 | 26 | 27 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_min_loglevel.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_setting_section_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 18 | 19 | 36 | 37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_setting_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 19 | 20 | 29 | 30 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_setting_toggle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 19 | 20 | 26 | 27 | 38 | 39 | 45 | 46 | 47 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_symptom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 17 | 18 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/progress_bar_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/progress_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/menu/alert_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_navigation_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 17 | 23 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/navigation/nav_graph.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 20 | 21 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs_onboarding_permissions_checker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #524A8F 4 | #524A8F 5 | #524A8F 6 | #007AFF 7 | #5561D6 8 | #86FFEA 9 | #018786 10 | #C4C4C4 11 | #88C4C4C4 12 | #a1a1a1 13 | #ffffff 14 | #1f000000 15 | 16 | #19a337 17 | #ff0000 18 | 19 | @android:color/black 20 | @color/textDefault 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32dp 4 | 40dp 5 | 80dp 6 | 26dp 7 | 1000dp 8 | 9 | 10 | 336dp 11 | 504dp 12 | 34dp 13 | 136dp 14 | 15 | 16 | 30dp 17 | 38dp 18 | 53dp 19 | 20 | 146dp 21 | 20dp 22 | 23 | 18dp 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/font_weights.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 300 4 | 500 5 | 600 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/text_sizes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @dimen/text_size_100 4 | @dimen/text_size_50 5 | @dimen/text_size_30 6 | 7 | 12sp 8 | 13sp 9 | 17sp 10 | 18sp 11 | 28sp 12 | 45sp 13 | 72sp 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/test/java/android/util/Log.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Log") 2 | 3 | /** 4 | * Mocking Android log https://stackoverflow.com/questions/36787449/how-to-mock-method-e-in-log 5 | * Using this exceptionally for the log, as it's inconvenient to inject it everywhere. 6 | */ 7 | package android.util 8 | 9 | fun v(tag: String, msg: String): Int { 10 | println("VERBOSE: $msg") 11 | return 0 12 | } 13 | 14 | fun d(tag: String, msg: String): Int { 15 | println("DEBUG: $msg") 16 | return 0 17 | } 18 | 19 | fun i(tag: String, msg: String): Int { 20 | println("INFO: $msg") 21 | return 0 22 | } 23 | 24 | fun w(tag: String, msg: String): Int { 25 | println("WARN: $msg") 26 | return 0 27 | } 28 | 29 | fun e(tag: String, msg: String, t: Throwable): Int { 30 | println("ERROR: $msg") 31 | return 0 32 | } 33 | 34 | fun e(tag: String, msg: String): Int { 35 | return 0 36 | } 37 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.72' 5 | ext.nav_version = "2.3.0" 6 | repositories { 7 | google() 8 | jcenter() 9 | maven { 10 | url 'https://oss.jfrog.org/artifactory/oss-snapshot-local' 11 | } 12 | } 13 | 14 | dependencies { 15 | classpath 'com.android.tools.build:gradle:4.0.1' 16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 17 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" 18 | // NOTE: Do not place your application dependencies here; they belong 19 | // in the individual module build.gradle files 20 | } 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | google() 26 | jcenter() 27 | maven { url 'https://jitpack.io' } 28 | maven { 29 | url 'https://oss.jfrog.org/artifactory/oss-snapshot-local' 30 | } 31 | // mavenCentral() 32 | } 33 | } 34 | 35 | task clean(type: Delete) { 36 | delete rootProject.buildDir 37 | } 38 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Co-Epi/app-android/54cffa441d27d18ba33d7719a34dc9b5c9125262/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jul 27 12:37:55 CEST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip 7 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='CoEpi' 2 | include ':app' 3 | --------------------------------------------------------------------------------