├── .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 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 1.8
19 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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: [](https://appcenter.ms/users/danamlewis/apps/CoEpi-Android/build/branches/beta)
7 |
8 | Develop branch: [](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 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_navigation_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------