diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 29c882b6dc..ee762804f0 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -18,11 +18,6 @@ on: description: 'tag' required: true type: string - is_patch: - description: 'Is the new version a patch' - required: true - type: boolean - default: false jobs: Build-Apk: @@ -70,20 +65,8 @@ jobs: whatsNewDirectory: whatsnew userFraction: 0.99 - - - name: Github Patch Release - if: ${{ (inputs.github_release == true) && (inputs.is_patch == true) }} - uses: ncipollo/release-action@v1 - with: - allowUpdates: true - draft: true - generateReleaseNotes: true - name: "Android Capture App for DHIS 2 (v${{ inputs.release_tag_name }}) - Patch version" - tag: ${{ inputs.release_tag_name }} - artifacts: ${{ env.main_project_module }}/build/outputs/apk/dhis/release/dhis2-v${{ steps.read-version.outputs.vName }}.apk,${{ env.main_project_module }}/build/outputs/apk/dhisPlayServices/release/dhis2-v${{ steps.read-version.outputs.vName }}-googlePlay.apk,${{ env.main_project_module }}/build/outputs/apk/dhis/debug/dhis2-v${{ steps.read-version.outputs.vName }}-training.apk - - - name: Github New Release - if: ${{ (inputs.github_release == true) && (inputs.is_patch == false) }} + - name: Upload to Github + if: ${{ (inputs.github_release }} uses: ncipollo/release-action@v1 with: allowUpdates: true diff --git a/.gitignore b/.gitignore index dd5de987f1..7d2b8cb57c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /venv /*.secrets.yml /gradle.deps +.kotlin/ diff --git a/.tx/config b/.tx/config index d17aec75f4..0fa80f633d 100644 --- a/.tx/config +++ b/.tx/config @@ -57,3 +57,10 @@ source_file = compose-table/src/main/res/values/strings.xml source_lang = en type = ANDROID minimum_perc = 0 + +[o:hisp-uio:p:dhis2-android-capture-app:r:tracker-strings-xml] +file_filter = tracker/src/main/res/values-/strings.xml +source_file = tracker/src/main/res/values/strings.xml +source_lang = en +type = ANDROID +minimum_perc = 0 diff --git a/Jenkinsfile b/Jenkinsfile index 5c8ba67d08..60ad35fff2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,6 +3,10 @@ pipeline { label "ec2-android" } + triggers { + cron('0 0 * * *') + } + options { buildDiscarder(logRotator(daysToKeepStr: '5')) timeout(time: 50) diff --git a/RELEASE.md b/RELEASE.md index de88994d97..a42609a8c0 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,89 +1,58 @@ -# Release notes - Android App for DHIS2 - 3.0.1 -### Bug +## NEW FUNCTIONALITY AND WEB PARITY -[ANDROAPP-5753](https://dhis2.atlassian.net/browse/ANDROAPP-5753) Formatting Issues with Attribute Values on TEI Card Dashboard +**New Capture Coordinates process:** The 3.1 version introduces a list of new features designed to enhance the capture coordinates process. These improvements aim to provide greater accuracy, flexibility, and control over location data capture. +- **Accuracy:** The capture coordinates process now includes a feature that displays the precision of the captured location. This allows users to see how accurate their location data is in real-time. This parameter can also be restricted using the Android Settings WebApp. +- **Search Functionality:** A new search functionality has been added, allowing users to look up specific locations by name or address. Users are also able to navigate through the map and perform area searches to discover other locations within a specified region. +- **Block Manual Capture:** Using the Android Settings Web App, administrators now have the option to block manual location capture. When this setting is enabled, users can only capture the current location and cannot manually select or search a different one. This ensures that location data remains consistent and accurate. -[ANDROAPP-5808](https://dhis2.atlassian.net/browse/ANDROAPP-5808) GS1 QR code input does not display popup if Scanned QR code is not GS1 type +[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6330) | [Card1](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-disabled-manual-capture.png) | [Card2](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-map-accuracy.png) | [Card3](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-map-search.png) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_map_accuracy) -[ANDROAPP-5873](https://dhis2.atlassian.net/browse/ANDROAPP-5873) Thread lock when app is unable to download reserved values +**Improve transfers flow:** Significant enhancements to the transfer flow, aimed at making the process more user-friendly and transparent. The transfer button has been moved to a more accessible location within the three dot menu in the TEI Dashboard, ensuring that users can easily find and initiate transfers without unnecessary navigation. It also has introduced new dialogs throughout the transfer process. These dialogs provide clear, step-by-step guidance, ensuring that users understand each part of the process. -[ANDROAPP-5953](https://dhis2.atlassian.net/browse/ANDROAPP-5953) Option set not working well in data set if the code of option constains character "\_" +[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6228) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_transfers) -[ANDROAPP-6051](https://dhis2.atlassian.net/browse/ANDROAPP-6051) No feedback is received after the database import is complete +**New relationship section:** Major updates have been made in the relationship tabs, enhancing both functionality and user experience. Relationship cards have been updated with the new design to offer a more intuitive and visually appealing experience. The new design emphasizes clarity and usability, making it easier to view and manage relationships at a glance. -[ANDROAPP-6057](https://dhis2.atlassian.net/browse/ANDROAPP-6057) Form scrolling improvement to prevent overlap with save button +To prevent accidental deletions and enhance user control, a new confirmation dialog also has been added when deleting a relationship. This dialog will prompt users to confirm their action, ensuring that relationships are only deleted intentionally. -[ANDROAPP-6088](https://dhis2.atlassian.net/browse/ANDROAPP-6088) Turning off a working list does not scroll user back to top of screen +[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6362) | [Card1](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-relationship-sections.png) | [Card2](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-new-relationship-cards.png) | [Card3](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-relationship-deletion.png) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_relationships) -[ANDROAPP-6094](https://dhis2.atlassian.net/browse/ANDROAPP-6094) Data set duplicates records in unavailable capture OUs +**Sort of unique attributes in the search screen:** Aimed at aligning it with the web instance for a more consistent user experience, this version of the Android app, by default, sorts the unique attributes (QR, barcode) at the top of the list of searchable attributes. Users can quickly and easily find the attributes for a more exact search. -[ANDROAPP-6101](https://dhis2.atlassian.net/browse/ANDROAPP-6101) User is allowed to save errors when the event is with status "complete" +[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6039) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_unique_qrBar_search) -[ANDROAPP-6116](https://dhis2.atlassian.net/browse/ANDROAPP-6116) App doesn't respect program specification constraints when displaying the list of available relationship types +**Support of biometric dialog:** An enhancement to the biometric authentication feature has been made in 3.1.0. When there is only one account configured, the user can configure biometric authentication (fingerprint or face ID). -[ANDROAPP-6131](https://dhis2.atlassian.net/browse/ANDROAPP-6131) Event program displays no events created message on intial load +[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-4676) | [Documentation](https://docs.dhis2.org/en/use/android-app/android-specific-features.html#capture_app_generic_biometrics_login) -[ANDROAPP-6132](https://dhis2.atlassian.net/browse/ANDROAPP-6132) Double tap on event/enrollment creation could generate duplicates +**Line Listing improvements:** This version of the Android App introduces support for the Category Option Dimension in line listings. This enhancement enables users to apply category options directly within line listings to filter data according to precise criteria, improving data exploration and decision-making processes. This feature greatly enhances the versatility and utility of line listings, empowering users to perform more sophisticated reporting. -[ANDROAPP-6137](https://dhis2.atlassian.net/browse/ANDROAPP-6137) Category Combo section shows incorrect number of fields +Additionally, it has been improved the text alignment within the Line Listing tables to support left alignment. This enhancement ensures better readability and a cleaner presentation of data, making it easier for users to review and analyze their information quickly. -[ANDROAPP-6146](https://dhis2.atlassian.net/browse/ANDROAPP-6146) Incomplete "Download" label when exporting a DB +[Jira1](https://dhis2.atlassian.net/browse/ANDROAPP-6353) | [Jira2](https://dhis2.atlassian.net/browse/ANDROAPP-6121) | [Documentation](https://docs.dhis2.org/en/use/android-app/visual-configurations.html#capture_app_visual_event_visualizations) -[ANDROAPP-6158](https://dhis2.atlassian.net/browse/ANDROAPP-6158) Data set - Sections without DE's never stops displaying the loading icon +## USER EXPERIENCE -[ANDROAPP-6174](https://dhis2.atlassian.net/browse/ANDROAPP-6174) When same day \(eg. 20 June\) is chosen for a future month, the scheduled date always shows "Today" +**Responsive Home Screen:** In this Android App version a new dynamic home screen that adapts to the number of programs available has been implemented. This update replaces the old static list that didn’t adjust to the screen, providing a more responsive and user-friendly interface.The responsive design makes better use of screen real estate, providing a more engaging and functional home screen layout. -[ANDROAPP-6181](https://dhis2.atlassian.net/browse/ANDROAPP-6181) ConcurrentModificationException +[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5394) | [Card](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-responsive-home-screen.png) | [Documentation](https://docs.dhis2.org/en/use/android-app/android-specific-features.html#capture_app_home) -[ANDROAPP-6182](https://dhis2.atlassian.net/browse/ANDROAPP-6182) \(RuntimeException\) Crash when rotating device in schedule screen +**Scheduled events dialog:** As a continuation of the new schedule dialog introduced in the version 3.0, a new intuitive and user-friendly schedule dialog has been implemented to enhance the overall user experience, making it easier to book, reschedule, or cancel events. -[ANDROAPP-6183](https://dhis2.atlassian.net/browse/ANDROAPP-6183) \(UnsupportedOperationException\) crash when opening a map +[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-6229) | [Card1](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-schedule-new.png) | [Card2](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-enter-cancel-reschedule.png) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_scheduling) -[ANDROAPP-6184](https://dhis2.atlassian.net/browse/ANDROAPP-6184) \(RuntimeException\) crash when rotating device in settings activity +**Improve menus and navigation bar:** A revamped of the menus and navigation bar has been made to be more user-friendly and accessible. It includes a cleaner, more modern look that improves readability and usability. These updates are designed to provide a more efficient and enjoyable user experience. -[ANDROAPP-6185](https://dhis2.atlassian.net/browse/ANDROAPP-6185) Event report date is not updated when changing the due date \(keeping the overdue status\) +[Jira1](https://dhis2.atlassian.net/browse/ANDROAPP-6036) | [Jira2](https://dhis2.atlassian.net/browse/ANDROAPP-6113) | [Card1](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-menu.png) | [Card2](https://s3.eu-west-1.amazonaws.com/content.dhis2.org/dhis2-android/release+notes+3.1/release+cards/Android-3-1-navigation-bar.png) | [Documentation +](https://docs.dhis2.org/en/use/android-app/visual-configurations.html#capture_app_visual_menu_bars_update) +## CROSS PRODUCT -[ANDROAPP-6187](https://dhis2.atlassian.net/browse/ANDROAPP-6187) Form is not refreshed when changing from closed org unit to open one +**Support for customized Tracker terminology:** Some DHIS2 terminology is not familiar for the end users. For this reason, we are gradually enabling the possibility to customize it to each particular use case. In this version, the term "event" (program label context) is customizable. The admin user will be able to configure it for each program using the Maintenance App, and the Android Capture App will display the customized term instead of the generic one. -[ANDROAPP-6193](https://dhis2.atlassian.net/browse/ANDROAPP-6193) App asks device location permission after granting location permission +[Jira](https://dhis2.atlassian.net/browse/ANDROAPP-5947) | [Documentation](https://docs.dhis2.org/en/use/android-app/program-features.html#capture_app_programs_common_features_customized_terminology) -[ANDROAPP-6197](https://dhis2.atlassian.net/browse/ANDROAPP-6197) Incorrect header in TEI Dashboard card +--- -[ANDROAPP-6198](https://dhis2.atlassian.net/browse/ANDROAPP-6198) Android adding '.0' to Data Element causing sync error - -[ANDROAPP-6209](https://dhis2.atlassian.net/browse/ANDROAPP-6209) NaN displayed in program indicators - -[ANDROAPP-6212](https://dhis2.atlassian.net/browse/ANDROAPP-6212) Cannot share database due to device permissions - -[ANDROAPP-6225](https://dhis2.atlassian.net/browse/ANDROAPP-6225) RuntimeException: Unable to start activity ComponentInfo\{com.dhis2/org.dhis2.usescases.searchTrackEntity.SearchTEAc... - -[ANDROAPP-6272](https://dhis2.atlassian.net/browse/ANDROAPP-6272) ApplicationNotResponding: ANR for at least 5000 ms. - -[ANDROAPP-6273](https://dhis2.atlassian.net/browse/ANDROAPP-6273) ApplicationNotResponding: ANR for at least 5000 ms. - -[ANDROAPP-6277](https://dhis2.atlassian.net/browse/ANDROAPP-6277) Working lists aren't applied even when active - -[ANDROAPP-6315](https://dhis2.atlassian.net/browse/ANDROAPP-6315) Fix Mobile ui breaking changes in capture app - -[ANDROAPP-6318](https://dhis2.atlassian.net/browse/ANDROAPP-6318) Order of TEIs change when moving between landscape and portrait - -[ANDROAPP-6332](https://dhis2.atlassian.net/browse/ANDROAPP-6332) \[DEFECT\] Incorrect workflow of Org unit when creating event - -[ANDROAPP-6345](https://dhis2.atlassian.net/browse/ANDROAPP-6345) Keyboard malfunction after stock distribution - -[ANDROAPP-6346](https://dhis2.atlassian.net/browse/ANDROAPP-6346) \[ANR\] jdk.internal.misc.Unsafe in park - -[ANDROAPP-6379](https://dhis2.atlassian.net/browse/ANDROAPP-6379) Time recorded when creating notes - -[ANDROAPP-6380](https://dhis2.atlassian.net/browse/ANDROAPP-6380) Keyboard navigation - -[ANDROAPP-6407](https://dhis2.atlassian.net/browse/ANDROAPP-6407) \[Defect\] Data entry not saving more than one value - -[ANDROAPP-6414](https://dhis2.atlassian.net/browse/ANDROAPP-6414) Clicking save, not now or sync several times - -[ANDROAPP-6415](https://dhis2.atlassian.net/browse/ANDROAPP-6415) Login error in landscape - -[ANDROAPP-6416](https://dhis2.atlassian.net/browse/ANDROAPP-6416) Percentage input is showing % twice - -[ANDROAPP-6417](https://dhis2.atlassian.net/browse/ANDROAPP-6417) Cannot navigate to event details or sync event after navigating back from details +##### **DETAILS** +You can find the list of all new features and all bugs fixed in 3.1.0 [here.](https://dhis2.atlassian.net/projects/ANDROAPP/versions/10851/tab/release-report-all-issues) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d99a20c7a4..c808767b48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ import com.android.build.api.variant.impl.VariantOutputImpl import com.android.build.gradle.internal.scope.ProjectInfo.Companion.getBaseName +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.io.ByteArrayOutputStream import java.text.SimpleDateFormat import java.util.Date @@ -8,8 +9,10 @@ plugins { id("com.android.application") kotlin("android") kotlin("kapt") + id("kotlin-parcelize") id("kotlinx-serialization") id("dagger.hilt.android.plugin") + alias(libs.plugins.kotlin.compose.compiler) } apply(from = "${project.rootDir}/jacoco/jacoco.gradle.kts") @@ -64,8 +67,6 @@ android { } } - ndkVersion = libs.versions.ndk.get() - compileSdk = libs.versions.sdk.get().toInt() namespace = "org.dhis2" testNamespace = "org.dhis2.test" @@ -75,21 +76,18 @@ android { defaultConfig { applicationId = "com.dhis2" - minSdk = libs.versions.minSdk.get().toInt() + compileSdk = libs.versions.sdk.get().toInt() targetSdk = libs.versions.sdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() versionCode = libs.versions.vCode.get().toInt() versionName = libs.versions.vName.get() testInstrumentationRunner = "org.dhis2.Dhis2Runner" vectorDrawables.useSupportLibrary = true multiDexEnabled = true - val defMapboxToken = - "pk.eyJ1IjoiZGhpczJhbmRyb2lkIiwiYSI6ImNrcWt1a2hzYzE5Ymsyb254MWtlbGt4Y28ifQ.JrP61q9BFTVEKO4SwRUwDw" - val mapboxAccessToken = System.getenv("MAPBOX_ACCESS_TOKEN") ?: defMapboxToken val bitriseSentryDSN = System.getenv("SENTRY_DSN") ?: "" buildConfigField("String", "SDK_VERSION", "\"" + libs.versions.dhis2sdk.get() + "\"") - buildConfigField("String", "MAPBOX_ACCESS_TOKEN", "\"" + mapboxAccessToken + "\"") buildConfigField("String", "MATOMO_URL", "\"https://usage.analytics.dhis2.org/matomo.php\"") buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}") buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"") @@ -130,12 +128,6 @@ android { } } - testOptions { - unitTests { - isReturnDefaultValues = true - } - } - buildTypes { getByName("debug") { @@ -218,13 +210,6 @@ android { } } - kotlinOptions { - jvmTarget = "17" - } - - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerExtensionVersion.get() - } lint { abortOnError = false checkReleaseBuilds = false @@ -250,6 +235,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) implementation(project(":viewpagerdotsindicator")) @@ -260,6 +251,7 @@ dependencies { implementation(project(":compose-table")) implementation(project(":stock-usecase")) implementation(project(":dhis2-mobile-program-rules")) + implementation(project(":tracker")) implementation(libs.security.conscrypt) implementation(libs.security.rootbeer) @@ -274,14 +266,13 @@ dependencies { implementation(libs.androidx.work) implementation(libs.androidx.workrx) implementation(libs.androidx.exifinterface) - implementation(libs.google.flexbox) + implementation(libs.androidx.biometric) + implementation(libs.androidx.material3) implementation(libs.google.guava) implementation(libs.github.pinlock) implementation(libs.github.fancyshowcase) implementation(libs.lottie) implementation(libs.dagger.hilt.android) - implementation(libs.rx.kotlin) - implementation(libs.network.gsonconverter) implementation(libs.network.okhttp) implementation(libs.dates.jodatime) implementation(libs.analytics.matomo) @@ -297,7 +288,6 @@ dependencies { debugImplementation(libs.analytics.flipper.network) debugImplementation(libs.analytics.flipper.leak) debugImplementation(libs.analytics.leakcanary) - debugImplementation(libs.test.ui.test.manifest) releaseImplementation(libs.analytics.leakcanary.noop) releaseImplementation(libs.analytics.flipper.noop) @@ -307,7 +297,6 @@ dependencies { kapt(libs.dagger.compiler) kapt(libs.dagger.hilt.android.compiler) - kapt(libs.dagger.hilt.compiler) kapt(libs.deprecated.autoValueParcel) testImplementation(libs.test.archCoreTesting) @@ -324,12 +313,9 @@ dependencies { androidTestImplementation(libs.test.testRunner) androidTestImplementation(libs.test.espresso.intents) androidTestImplementation(libs.test.espresso.contrib) - androidTestImplementation(libs.test.espresso.accessibility) - androidTestImplementation(libs.test.espresso.web) androidTestImplementation(libs.test.uiautomator) androidTestImplementation(libs.test.testCore) androidTestImplementation(libs.test.rules) - androidTestImplementation(libs.test.coreKtx) androidTestImplementation(libs.test.junitKtx) androidTestImplementation(libs.test.mockitoCore) androidTestImplementation(libs.test.dexmaker.mockitoInline) @@ -340,4 +326,4 @@ dependencies { androidTestImplementation(libs.test.compose.ui.test) androidTestImplementation(libs.test.hamcrest) androidTestImplementation(libs.dispatcher.dispatchEspresso) -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt b/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt index de1e6693b7..322263b0b5 100644 --- a/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt +++ b/app/src/androidTest/java/org/dhis2/common/filters/FiltersRobot.kt @@ -1,24 +1,16 @@ package org.dhis2.common.filters -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollTo import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.TypeTextAction import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.RecyclerViewActions -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withId import org.dhis2.R import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.DatePickerMatchers.Companion.matchesDate import org.dhis2.commons.filters.FilterHolder -import org.dhis2.ui.dialogs.orgunit.DONE_TEST_TAG -import org.dhis2.ui.dialogs.orgunit.ITEM_CHECK_TEST_TAG -import org.dhis2.ui.dialogs.orgunit.ITEM_TEST_TAG fun filterRobotCommon(robotBody: FiltersRobot.() -> Unit) { FiltersRobot().apply { @@ -28,7 +20,7 @@ fun filterRobotCommon(robotBody: FiltersRobot.() -> Unit) { class FiltersRobot : BaseRobot() { fun openFilterAtPosition(position: Int) { - onView(withId(R.id.filterRecycler)).perform( + onView(withId(R.id.filterRecyclerLayout)).perform( RecyclerViewActions.actionOnItemAtPosition(position, click()) ) } @@ -56,7 +48,7 @@ class FiltersRobot : BaseRobot() { } fun selectNotSyncedState() { - onView( withId(R.id.stateNotSynced)).perform(click()) + onView(withId(R.id.stateNotSynced)).perform(click()) } fun acceptDateSelected() { @@ -66,6 +58,4 @@ class FiltersRobot : BaseRobot() { fun checkDate(year: Int, monthOfYear: Int, dayOfMonth: Int) { onView(withId(R.id.datePicker)).check(matches(matchesDate(year, monthOfYear, dayOfMonth))) } - - } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/common/idlingresources/MapIdlingResource.kt b/app/src/androidTest/java/org/dhis2/common/idlingresources/MapIdlingResource.kt deleted file mode 100644 index 97232e6e67..0000000000 --- a/app/src/androidTest/java/org/dhis2/common/idlingresources/MapIdlingResource.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.dhis2.common.idlingresources - -import androidx.test.espresso.IdlingResource -import androidx.test.rule.ActivityTestRule -import com.mapbox.mapboxsdk.maps.MapView -import com.mapbox.mapboxsdk.maps.MapboxMap -import com.mapbox.mapboxsdk.maps.OnMapReadyCallback -import org.dhis2.R - -class MapIdlingResource( - activityTestRule: ActivityTestRule<*> -) : IdlingResource, OnMapReadyCallback { - - var map: MapboxMap? = null - private var resourceCallback: IdlingResource.ResourceCallback? = null - - init { - try { - val mapView = activityTestRule.activity.findViewById(R.id.mapView) - mapView.getMapAsync(this) - } catch (err: Exception) { - throw RuntimeException(err) - } - } - - override fun getName(): String = javaClass.simpleName - - override fun isIdleNow() = map != null - - override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) { - this.resourceCallback = resourceCallback - } - - override fun onMapReady(mapboxMap: MapboxMap) { - this.map = mapboxMap - if (resourceCallback != null) { - resourceCallback!!.onTransitionToIdle() - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/common/matchers/ChartMatchers.kt b/app/src/androidTest/java/org/dhis2/common/matchers/ChartMatchers.kt index 5b80f30c8d..f49cc96f2b 100644 --- a/app/src/androidTest/java/org/dhis2/common/matchers/ChartMatchers.kt +++ b/app/src/androidTest/java/org/dhis2/common/matchers/ChartMatchers.kt @@ -28,7 +28,7 @@ class ChartMatchers { ChartType.LINE_CHART -> view is LineChart ChartType.BAR_CHART -> view is BarChart ChartType.TABLE, ChartType.LINE_LISTING -> view is ComposeView - ChartType.SINGLE_VALUE -> view.findViewById(R.id.singleValueTitle) != null + ChartType.SINGLE_VALUE -> view is ComposeView ChartType.NUTRITION -> view is LineChart ChartType.RADAR -> view is RadarChart ChartType.PIE_CHART -> view is PieChart diff --git a/app/src/androidTest/java/org/dhis2/common/matchers/DrawableMatchers.java b/app/src/androidTest/java/org/dhis2/common/matchers/DrawableMatchers.java deleted file mode 100644 index ffecf549c4..0000000000 --- a/app/src/androidTest/java/org/dhis2/common/matchers/DrawableMatchers.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.dhis2.common.matchers; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.view.View; -import android.widget.ImageView; - -import org.hamcrest.Description; -import org.hamcrest.TypeSafeMatcher; - -public class DrawableMatchers extends TypeSafeMatcher { - - private final int expectedId; - private String resourceName; - static final int EMPTY = -1; - static final int ANY = -2; - - public DrawableMatchers(int expectedId) { - super(View.class); - this.expectedId = expectedId; - } - - @Override - protected boolean matchesSafely(View target) { - if (!(target instanceof ImageView)) { - return false; - } - ImageView imageView = (ImageView) target; - if (expectedId == EMPTY) { - return imageView.getDrawable() == null; - } - if (expectedId == ANY) { - return imageView.getDrawable() != null; - } - Resources resources = target.getContext().getResources(); - Drawable expectedDrawable = resources.getDrawable(expectedId); - resourceName = resources.getResourceEntryName(expectedId); - - if (expectedDrawable == null) { - return false; - } - - Bitmap bitmap = getBitmap(imageView.getDrawable()); - Bitmap otherBitmap = getBitmap(expectedDrawable); - return bitmap.sameAs(otherBitmap); - } - - private Bitmap getBitmap(Drawable drawable) { - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - return bitmap; - } - - @Override - public void describeTo(Description description) { - description.appendText("with drawable from resource id: "); - description.appendValue(expectedId); - if (resourceName != null) { - description.appendText("["); - description.appendText(resourceName); - description.appendText("]"); - } - } -} diff --git a/app/src/androidTest/java/org/dhis2/common/matchers/RecyclerviewMatchers.kt b/app/src/androidTest/java/org/dhis2/common/matchers/RecyclerviewMatchers.kt index a2e2b54478..02f87def3e 100644 --- a/app/src/androidTest/java/org/dhis2/common/matchers/RecyclerviewMatchers.kt +++ b/app/src/androidTest/java/org/dhis2/common/matchers/RecyclerviewMatchers.kt @@ -1,7 +1,6 @@ package org.dhis2.common.matchers import android.view.View -import android.widget.TextView import androidx.annotation.NonNull import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView @@ -10,8 +9,6 @@ import org.dhis2.usescases.searchTrackEntity.listView.SearchResult import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.TypeSafeMatcher -import java.sql.Date -import java.text.SimpleDateFormat class RecyclerviewMatchers { @@ -115,36 +112,6 @@ class RecyclerviewMatchers { } } - fun dateIsInRange(id:Int, startDate: String, endDate: String) : Matcher { - return object : BoundedMatcher(RecyclerView::class.java) { - override fun describeTo(description: Description) { - description.appendText("all elements have dates between $startDate and $endDate : ") - } - override fun matchesSafely(view: RecyclerView): Boolean { - val adapter: RecyclerView.Adapter = if(view.adapter is ConcatAdapter){ - (view.adapter as ConcatAdapter).adapters[1] as RecyclerView.Adapter - }else{ - view.adapter!! - } - for (position in 0 until adapter.itemCount) { - val type = adapter.getItemViewType(position) - val holder = adapter.createViewHolder(view, type) - adapter.onBindViewHolder(holder, position) - - val start = Date.valueOf(startDate) - val end = Date.valueOf(endDate) - val range = start..end - val date = holder.itemView.findViewById(id).text.toString() - val initialFormattedDate = SimpleDateFormat("dd/M/yyyy").parse(date) - val formatter = SimpleDateFormat("yyyy-MM-dd") - val parsedDate = formatter.format(initialFormattedDate) - if (Date.valueOf(parsedDate) !in range) return false - } - return true - } - } - } - fun hasNoMoreResultsInProgram():Matcher{ return object : BoundedMatcher(RecyclerView::class.java) { override fun describeTo(description: Description) { diff --git a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt index e9d76fa684..b08151e87e 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/BaseTest.kt @@ -18,7 +18,6 @@ import org.dhis2.common.keystore.KeyStoreRobot.Companion.USERNAME import org.dhis2.common.mockwebserver.MockWebServerRobot import org.dhis2.common.preferences.PreferencesRobot import org.dhis2.common.rules.DisableAnimations -import org.dhis2.commons.featureconfig.model.Feature import org.dhis2.commons.idlingresource.CountingIdlingResourceSingleton import org.dhis2.commons.idlingresource.SearchIdlingResourceSingleton import org.dhis2.commons.prefs.Preference @@ -26,6 +25,7 @@ import org.dhis2.form.ui.idling.FormCountingIdlingResource import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton import org.dhis2.usescases.programEventDetail.eventList.EventListIdlingResourceSingleton import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton +import org.dhis2.maps.utils.OnMapReadyIdlingResourceSingleton import org.junit.After import org.junit.Before import org.junit.ClassRule @@ -74,7 +74,6 @@ open class BaseTest { keyStoreRobot = providesKeyStoreRobot(context) preferencesRobot = providesPreferencesRobot(context) mockWebServerRobot = providesMockWebserverRobot(context) - disableComposeForms() } } @@ -86,6 +85,7 @@ open class BaseTest { SearchIdlingResourceSingleton.countingIdlingResource, TeiDataIdlingResourceSingleton.countingIdlingResource, EventIdlingResourceSingleton.countingIdlingResource, + OnMapReadyIdlingResourceSingleton.countingIdlingResource, ) } @@ -165,23 +165,11 @@ open class BaseTest { (context.applicationContext as AppTest).deleteDatabase(DB_TO_IMPORT) } - private fun disableComposeForms() { - preferencesRobot.saveValue(SET_FROM_TESTING, true) - preferencesRobot.saveValue(Feature.COMPOSE_FORMS.name, false) - } - - - fun enableComposeForms() { - preferencesRobot.saveValue(SET_FROM_TESTING, true) - preferencesRobot.saveValue(Feature.COMPOSE_FORMS.name, true) - } - companion object { @ClassRule @JvmField val disableAnimationsTestRule = DisableAnimations() const val MOCK_SERVER_URL = "http://127.0.0.1:8080" const val API = "api" - const val SET_FROM_TESTING = "SET_FROM_TESTING" } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/FlowTestsSuite.kt b/app/src/androidTest/java/org/dhis2/usescases/FlowTestsSuite.kt index 04d299aa61..22d11b012b 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/FlowTestsSuite.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/FlowTestsSuite.kt @@ -3,7 +3,6 @@ package org.dhis2.usescases import org.dhis2.usescases.flow.searchFlow.SearchFlowTest import org.dhis2.usescases.flow.syncFlow.SyncFlowTest import org.dhis2.usescases.flow.teiFlow.TeiFlowTest -import org.dhis2.usescases.form.FormTest import org.junit.runner.RunWith import org.junit.runners.Suite @@ -12,6 +11,5 @@ import org.junit.runners.Suite SearchFlowTest::class, SyncFlowTest::class, TeiFlowTest::class, - FormTest::class ) class FlowTestsSuite diff --git a/app/src/androidTest/java/org/dhis2/usescases/UseCaseTestsSuite.kt b/app/src/androidTest/java/org/dhis2/usescases/UseCaseTestsSuite.kt index a0e85ac6b7..8d49b3beda 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/UseCaseTestsSuite.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/UseCaseTestsSuite.kt @@ -4,8 +4,7 @@ import org.dhis2.usescases.about.AboutTest import org.dhis2.usescases.datasets.DataSetTest import org.dhis2.usescases.enrollment.EnrollmentTest import org.dhis2.usescases.event.EventTest -import org.dhis2.usescases.filters.FilterTest -import org.dhis2.usescases.jira.JiraTest +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.EventInitialTest import org.dhis2.usescases.login.LoginTest import org.dhis2.usescases.main.MainTest import org.dhis2.usescases.pin.PinTest @@ -23,9 +22,8 @@ import org.junit.runners.Suite AboutTest::class, DataSetTest::class, EnrollmentTest::class, + EventInitialTest::class, EventTest::class, - FilterTest::class, - JiraTest::class, LoginTest::class, MainTest::class, PinTest::class, diff --git a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTableRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTableRobot.kt index 3c3ac917b7..4021120d2b 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTableRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTableRobot.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performScrollTo @@ -13,6 +14,7 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry import org.dhis2.R import org.dhis2.common.BaseRobot import org.dhis2.composetable.ui.INPUT_TEST_FIELD_TEST_TAG @@ -51,7 +53,9 @@ class DataSetTableRobot( } fun clickOnMenuReOpen() { - onView(withText(R.string.re_open)).perform(click()) + with(InstrumentationRegistry.getInstrumentation().targetContext) { + composeTestRule.onNodeWithText(getString(R.string.re_open)).performClick() + } } fun typeOnCell(tableId: String, rowIndex: Int, columnIndex: Int) { diff --git a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt index 659b9d2bd8..baea240a96 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt @@ -46,9 +46,10 @@ class DataSetTest : BaseTest() { } } + @Ignore("Indeterministic it will be addressed in ANDROAPP-6458") @Test fun shouldCreateNewDataSet() { - val period = "Oct 2023" + val period = "Aug 2024" val orgUnit = "Ngelehun CHC" startDataSetDetailActivity("ZOV1a5R4gqH", "DS EXTRA TEST", ruleDataSetDetail) diff --git a/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentFormRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentFormRobot.kt index fa27b89bca..ac890f913d 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentFormRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentFormRobot.kt @@ -4,17 +4,11 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.PickerActions -import androidx.test.espresso.contrib.RecyclerViewActions -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import org.dhis2.R import org.dhis2.common.BaseRobot -import org.dhis2.common.viewactions.clickChildViewWithId -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder -import org.dhis2.usescases.teidashboard.robot.EnrollmentRobot -import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.equalTo @@ -26,16 +20,6 @@ fun enrollmentFormRobot(enrollmentFormRobot: EnrollmentFormRobot.() -> Unit) { class EnrollmentFormRobot : BaseRobot() { - fun clickOnDateOfBirth() { - onView(withId(R.id.recyclerView)) - .perform( - RecyclerViewActions.actionOnItem( - ViewMatchers.hasDescendant(withText(containsString(EnrollmentRobot.DATE_OF_BIRTH))), - clickChildViewWithId(R.id.inputEditText) - ) - ) - } - fun changePickerDate() { onView(withId(equalTo(R.id.datePicker))).perform(PickerActions.setDate(2020, 1, 1)) } diff --git a/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentIntents.kt b/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentIntents.kt index 5ce32de944..28ab23f89d 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentIntents.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentIntents.kt @@ -1,7 +1,8 @@ package org.dhis2.usescases.enrollment import android.content.Intent -import androidx.test.rule.ActivityTestRule +import androidx.test.core.app.ApplicationProvider +import org.dhis2.LazyActivityScenarioRule private const val CHILD_PROGRAM_UID_VALUE = "IpHINAT79UW" private const val ENROLLMENT_VALUE_AUTOGENERATED_EVENTS = "LZg2cOvrAlU" @@ -11,21 +12,22 @@ fun startEnrollmentActivity( enrollmentUid: String?, enrollmentMode: EnrollmentActivity.EnrollmentMode, forRelationship: Boolean, - rule: ActivityTestRule + rule: LazyActivityScenarioRule ) { - Intent().apply { + Intent( + ApplicationProvider.getApplicationContext(), + EnrollmentActivity::class.java + ).apply { putExtra(EnrollmentActivity.ENROLLMENT_UID_EXTRA, enrollmentUid) putExtra(EnrollmentActivity.PROGRAM_UID_EXTRA, programUid) putExtra(EnrollmentActivity.MODE_EXTRA, enrollmentMode.name) putExtra(EnrollmentActivity.FOR_RELATIONSHIP, forRelationship) }.also { - rule.launchActivity(it) + rule.launch(it) } } -fun prepareTeiWithAutogeneratedEvents( - rule: ActivityTestRule -) { +fun prepareTeiWithAutogeneratedEvents(rule: LazyActivityScenarioRule) { startEnrollmentActivity( CHILD_PROGRAM_UID_VALUE, ENROLLMENT_VALUE_AUTOGENERATED_EVENTS, diff --git a/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentTest.kt b/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentTest.kt index 85192efe22..62e8ab22a4 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/enrollment/EnrollmentTest.kt @@ -1,9 +1,10 @@ package org.dhis2.usescases.enrollment +import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule +import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.searchTrackEntity.SearchTEActivity +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -12,11 +13,12 @@ import org.junit.runner.RunWith class EnrollmentTest : BaseTest() { @get:Rule - val ruleSearch = ActivityTestRule(SearchTEActivity::class.java, false, false) + val ruleEnrollment = lazyActivityScenarioRule(launchActivity = false) @get:Rule - val ruleEnrollment = ActivityTestRule(EnrollmentActivity::class.java, false, false) + val composeTestRule = createComposeRule() + @Ignore("Not working in new form, fixed on ANDROAPP-6179") @Test fun shouldShowDateEditionWarningMessage() { setupCredentials() @@ -24,7 +26,7 @@ class EnrollmentTest : BaseTest() { prepareTeiWithAutogeneratedEvents(ruleEnrollment) enrollmentFormRobot { - clickOnDateOfBirth() +// clickOnDateOfBirth() changePickerDate() clickOnAcceptEnrollmentDate() checkDateWarningIsDisplayed() diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt index 1bd0c7ccdb..bc205bde2f 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt @@ -3,30 +3,36 @@ package org.dhis2.usescases.event import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry import org.dhis2.R import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.hasCompletedPercentage -fun eventRegistrationRobot(eventRegistrationRobot: EventRegistrationRobot.() -> Unit) { - EventRegistrationRobot().apply { +fun eventRegistrationRobot( + composeTestRule: ComposeTestRule, + eventRegistrationRobot: EventRegistrationRobot.() -> Unit +) { + EventRegistrationRobot(composeTestRule).apply { eventRegistrationRobot() } } -class EventRegistrationRobot : BaseRobot() { +class EventRegistrationRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun openMenuMoreOptions() { onView(withId(R.id.moreOptions)).perform(click()) } fun clickOnDelete() { - onView(withText(R.string.delete)).perform(click()) + with(InstrumentationRegistry.getInstrumentation().targetContext) { + composeTestRule.onNodeWithText(getString(R.string.delete)).performClick() + } } fun checkEventDataEntryIsOpened(completion: Int, email: String, composeTestRule: ComposeTestRule) { @@ -36,7 +42,9 @@ class EventRegistrationRobot : BaseRobot() { } fun clickOnShare() { - onView(withText(R.string.share)).perform(click()) + with(InstrumentationRegistry.getInstrumentation().targetContext) { + composeTestRule.onNodeWithText(getString(R.string.share)).performClick() + } } private fun clickOnNextQR() { diff --git a/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt b/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt index b557dbb3b5..77ccc5a6d7 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventTest.kt @@ -11,7 +11,6 @@ import org.dhis2.usescases.event.entity.TEIProgramStagesUIModel import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity -import org.dhis2.usescases.programEventDetail.eventList.EventListFragment import org.dhis2.usescases.programevent.robot.programEventsRobot import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity import org.dhis2.usescases.teidashboard.robot.eventRobot @@ -52,7 +51,7 @@ class EventTest : BaseTest() { clickOnEventGroupByStageUsingDate(tbVisitDate) } - eventRegistrationRobot { + eventRegistrationRobot(composeTestRule) { openMenuMoreOptions() clickOnDelete() clickOnDeleteDialog() @@ -68,11 +67,9 @@ class EventTest : BaseTest() { val completion = 92 val email = "mail@mail.com" - enableComposeForms() - prepareEventDetailsIntentAndLaunchActivity(rule) - eventRegistrationRobot { + eventRegistrationRobot(composeTestRule) { checkEventDataEntryIsOpened(completion, email, composeTestRule) } } @@ -83,7 +80,7 @@ class EventTest : BaseTest() { prepareEventToShareIntentAndLaunchActivity(ruleEventDetail) - eventRegistrationRobot { + eventRegistrationRobot(composeTestRule) { openMenuMoreOptions() clickOnShare() clickOnAllQR(qrList) @@ -105,7 +102,7 @@ class EventTest : BaseTest() { } eventRobot(composeTestRule) { - fillRadioButtonForm(radioFormLength) +// fillRadioButtonForm(radioFormLength) clickOnFormFabButton() clickOnCompleteButton() } @@ -126,11 +123,11 @@ class EventTest : BaseTest() { programEventsRobot(composeTestRule) { clickOnAddEvent() } - eventRegistrationRobot { + eventRegistrationRobot(composeTestRule) { clickNextButton() } eventRobot(composeTestRule) { - typeOnRequiredEventForm("125", 1) +// typeOnRequiredEventForm("125", 1) clickOnFormFabButton() checkSecondaryButtonNotVisible() } diff --git a/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt b/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt index 2b0733d650..e7b31f267c 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/EventInitialTest.kt @@ -7,22 +7,23 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag -import kotlinx.coroutines.ExperimentalCoroutinesApi import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.date.DateUtils import org.dhis2.commons.locationprovider.LocationProvider import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.EventResourcesProvider +import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.data.GeometryController import org.dhis2.form.data.GeometryParserImpl import org.dhis2.form.model.FieldUiModel +import org.dhis2.ui.MetadataIconData import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCoordinates import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatComboUiModel @@ -44,15 +45,12 @@ import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.program.ProgramStage import org.junit.Rule import org.junit.Test +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import java.text.SimpleDateFormat import java.util.Date -import org.dhis2.commons.resources.MetadataIconProvider -import org.dhis2.ui.MetadataIconData -import org.mockito.kotlin.any -@ExperimentalCoroutinesApi class EventInitialTest { @get:Rule @@ -64,31 +62,15 @@ class EventInitialTest { val date: Date? = dateFormat.parse(dateString) - private val eventDetailsRepository: EventDetailsRepository = mock { - on { getProgramStage() } doReturn programStage - on { catCombo() } doReturn catCombo - on { getEvent() } doReturn null - on { getObjectStyle() } doReturn style - on { getOrganisationUnit(ORG_UNIT_UID) } doReturn orgUnit - on { getGeometryModel() } doReturn geometryModel - on { getCatOptionCombos(CAT_COMBO_UID) } doReturn listOf(categoryOptionCombo) - on { getEditableStatus() } doReturn EventEditableStatus.Editable() - on { getEnrollmentDate(ENROLLMENT_UID) } doReturn date - on { getStageLastDate(ENROLLMENT_UID) } doReturn DateUtils.uiDateFormat() - .parse("20/8/2023")!! - - } - - private val metadataIconProvider:MetadataIconProvider = mock{ - on { invoke(any()) }doReturn MetadataIconData.defaultIcon() + private val metadataIconProvider: MetadataIconProvider = mock { + on { invoke(any()) } doReturn MetadataIconData.defaultIcon() } private lateinit var viewModel: EventDetailsViewModel - private val locationProvider: LocationProvider = mock() private val resourceManager: ResourceManager = mock() - + private val eventResourcesProvider: EventResourcesProvider = mock() private val periodUtils: DhisPeriodUtils = mock() private val preferencesProvider: PreferenceProvider = mock() @@ -119,9 +101,19 @@ class EventInitialTest { on { provideDueDate() } doReturn "Due date" } - private fun createConfigureEventTemp(eventCreationType: EventCreationType) = ConfigureEventTemp( - creationType = eventCreationType, - ) + private val eventDetailsRepository: EventDetailsRepository = mock { + on { getProgramStage() } doReturn programStage + on { catCombo() } doReturn catCombo + on { getEvent() } doReturn null + on { getObjectStyle() } doReturn style + on { getOrganisationUnit(ORG_UNIT_UID) } doReturn orgUnit + on { getGeometryModel() } doReturn geometryModel + on { getCatOptionCombos(CAT_COMBO_UID) } doReturn listOf(categoryOptionCombo) + on { getEditableStatus() } doReturn EventEditableStatus.Editable() + on { getEnrollmentDate(ENROLLMENT_UID) } doReturn date + on { getStageLastDate(ENROLLMENT_UID) } doReturn DateUtils.uiDateFormat() + .parse("20/8/2023")!! + } private fun createConfigureEventCatCombo() = ConfigureEventCatCombo( repository = eventDetailsRepository, @@ -161,10 +153,15 @@ class EventInitialTest { resourcesProvider = provideEventResourcesProvider(), creationType = eventCreationType, enrollmentStatus = enrollmentStatus, - metadataIconProvider = metadataIconProvider + metadataIconProvider = metadataIconProvider, ) - private fun provideEventResourcesProvider() = EventDetailResourcesProvider(PROGRAM_UID, programStage.uid(), resourceManager) + private fun provideEventResourcesProvider() = EventDetailResourcesProvider( + PROGRAM_UID, + programStage.uid(), + resourceManager, + eventResourcesProvider, + ) private fun createOrUpdateEventDetails() = CreateOrUpdateEventDetails( repository = eventDetailsRepository, @@ -204,7 +201,6 @@ class EventInitialTest { configureOrgUnit = createConfigureOrgUnit(eventCreationType), configureEventCoordinates = createConfigureEventCoordinates(), configureEventCatCombo = createConfigureEventCatCombo(), - configureEventTemp = createConfigureEventTemp(eventCreationType), periodType = periodType, eventUid = EVENT_UID, geometryController = createGeometryController(), @@ -213,82 +209,72 @@ class EventInitialTest { resourcesProvider = provideEventResourcesProvider(), ) - @Test fun shouldAddStandardIntervalDaysIfScheduleIntervalIsGreaterThanZero() { - viewModel = initViewModel( periodType = null, eventCreationType = EventCreationType.SCHEDULE, enrollmentStatus = EnrollmentStatus.ACTIVE, - scheduleInterval = 20 + scheduleInterval = 20, ) composeTestRule.setContent { - - val date by viewModel.eventDate.collectAsState() val details by viewModel.eventDetails.collectAsState() ProvideInputDate( EventInputDateUiModel( eventDate = date, detailsEnabled = details.enabled, - onDateClick = {} , + onDateClick = {}, onDateSelected = { dateValues -> viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day) }, onClear = { viewModel.onClearEventReportDate() }, required = true, - ) + ), ) - } composeTestRule.onNodeWithTag(INPUT_EVENT_INITIAL_DATE).assertIsDisplayed() - assert(viewModel.eventDate.value.dateValue == "9/9/2023") + assert(viewModel.eventDate.value.dateValue == "09/09/2023") } @Test fun shouldNotAddStandardIntervalDaysIfScheduleIntervalIsZero() { - viewModel = initViewModel( periodType = null, eventCreationType = EventCreationType.SCHEDULE, enrollmentStatus = EnrollmentStatus.ACTIVE, - scheduleInterval = 0 + scheduleInterval = 0, ) composeTestRule.setContent { - - val date by viewModel.eventDate.collectAsState() val details by viewModel.eventDetails.collectAsState() ProvideInputDate( EventInputDateUiModel( eventDate = date, detailsEnabled = details.enabled, - onDateClick = {}, + onDateClick = {}, onDateSelected = { dateValues -> viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day) }, onClear = { viewModel.onClearEventReportDate() }, required = true, - ) - - ) + ), + ) } composeTestRule.onNodeWithTag(INPUT_EVENT_INITIAL_DATE).assertIsDisplayed() - assert(viewModel.eventDate.value.dateValue == "20/8/2023") + assert(viewModel.eventDate.value.dateValue == "20/08/2023") } @Test fun shouldShowEmptyCategorySelectorIfCategoryHasNoOptions() { - viewModel = initViewModel( periodType = null, eventCreationType = EventCreationType.SCHEDULE, enrollmentStatus = EnrollmentStatus.ACTIVE, - scheduleInterval = 0 + scheduleInterval = 0, ) composeTestRule.setContent { val date by viewModel.eventDate.collectAsState() @@ -310,9 +296,9 @@ class EventInitialTest { required = true, noOptionsText = "No options available", catComboText = "No options catCombo", - ) + ), ) } composeTestRule.onNodeWithTag(EMPTY_CATEGORY_SELECTOR).assertIsDisplayed() } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/org/dhis2/usescases/filters/FilterTest.kt b/app/src/androidTest/java/org/dhis2/usescases/filters/FilterTest.kt deleted file mode 100644 index 966881f270..0000000000 --- a/app/src/androidTest/java/org/dhis2/usescases/filters/FilterTest.kt +++ /dev/null @@ -1,181 +0,0 @@ -package org.dhis2.usescases.filters - -import android.content.Intent -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.rule.ActivityTestRule -import org.dhis2.common.filters.filterRobotCommon -import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.flow.syncFlow.robot.eventWithoutRegistrationRobot -import org.dhis2.usescases.form.formRobot -import org.dhis2.usescases.main.AVOID_SYNC -import org.dhis2.usescases.main.MainActivity -import org.dhis2.usescases.main.homeRobot -import org.dhis2.usescases.orgunitselector.orgUnitSelectorRobot -import org.dhis2.usescases.teidashboard.robot.eventRobot -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test - -class FilterTest : BaseTest() { - - @get:Rule - val rule = ActivityTestRule(MainActivity::class.java, false, false) - - @get:Rule - val composeTestRule = createComposeRule() - - @Test - fun checkFromToDateFilter() { - setupCredentials() - startActivity() - setDatePicker() - - homeRobot { - openFilters() - } - - filterRobotCommon { - openFilterAtPosition(0) - clickOnFromToDateOption() - selectDate(2020, 6, 15) - acceptDateSelected() - selectDate(2020, 11, 7) - acceptDateSelected() - } - homeRobot { - openFilters() - checkItemsInProgram(composeTestRule, 3, "Child Programme", "3") - checkItemsInProgram(composeTestRule, 5, "Contraceptives Voucher Program", "5") - checkItemsInProgram(composeTestRule, 26, "Mortality < 5 years", "4") - } - cleanLocalDatabase() - } - - @Test - fun checkWritingOrgUnitFilter() { - setupCredentials() - startActivity() - - homeRobot { - openFilters() - } - - filterRobotCommon { - openFilterAtPosition(1) - typeOrgUnit("OU TEST PARENT") - clickAddOrgUnit() - closeKeyboard() - } - homeRobot { - openFilters() - checkItemsInProgram(composeTestRule, 3, "Child Programme", "0") - checkItemsInProgram(composeTestRule, 41, "XX TEST EVENT FULL", "2") - checkItemsInProgram(composeTestRule, 43, "XX TEST TRACKER PROGRAM", "4") - } - cleanLocalDatabase() - } - - @Test - fun checkTreeOrgUnitFilter() { - startActivity() - setupCredentials() - - homeRobot { - openFilters() - } - - filterRobotCommon { - openFilterAtPosition(1) - clickOnOrgUnitTree() - orgUnitSelectorRobot(composeTestRule) { - selectTreeOrgUnit("OU TEST PARENT") - } - } - homeRobot { - openFilters() - checkItemsInProgram(composeTestRule, 3, "Child Programme", "0") - checkItemsInProgram(composeTestRule, 41, "XX TEST EVENT FULL", "2") - checkItemsInProgram(composeTestRule, 43, "XX TEST TRACKER PROGRAM", "4") - } - cleanLocalDatabase() - } - - @Ignore("Undeterministic") - @Test - fun checkSyncFilter() { - setupCredentials() - startActivity() - - homeRobot { - openProgramByPosition(composeTestRule, 0) - waitToDebounce(700) - } - eventWithoutRegistrationRobot(composeTestRule) { - clickOnEventAtPosition(0) - } - eventRobot(composeTestRule) { - clickOnFormFabButton() - clickOnCompleteButton() - pressBack() - } - homeRobot { - openFilters() - } - filterRobotCommon { - openFilterAtPosition(2) - selectNotSyncedState() - } - homeRobot { - openFilters() - waitToDebounce(1000) - checkItemsInProgram(composeTestRule, 0, "Antenatal care visit", "1") - checkItemsInProgram(composeTestRule, 3, "Child Programme", "0") - } - cleanLocalDatabase() - } - - @Ignore("TODO: Review why is failing on browserstack") - @Test - fun checkCombinedFilters() { - setupCredentials() - startActivity() - - homeRobot { - openProgramByPosition(composeTestRule, 41) - } - eventWithoutRegistrationRobot(composeTestRule) { - clickOnEventAtPosition(0) - } - formRobot(composeTestRule) { - clickOnSelectOption(1, 1) - pressBack() - pressBack() - pressBack() - } - homeRobot { - openFilters() - } - - filterRobotCommon { - openFilterAtPosition(1) - typeOrgUnit("OU TEST PARENT") - clickAddOrgUnit() - closeKeyboard() - openFilterAtPosition(2) - selectNotSyncedState() - } - homeRobot { - openFilters() - checkItemsInProgram(composeTestRule, 37, "TB program", "0") - waitToDebounce(700) - checkItemsInProgram(composeTestRule, 41, "XX TEST EVENT FULL", "1") - waitToDebounce(700) - } - cleanLocalDatabase() - } - - private fun startActivity() { - val intent = Intent().putExtra(AVOID_SYNC, true) - rule.launchActivity(intent) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowRobot.kt index da09cd4963..196425e022 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowRobot.kt @@ -1,18 +1,22 @@ package org.dhis2.usescases.flow.searchFlow +import androidx.compose.ui.test.junit4.ComposeTestRule import org.dhis2.common.BaseRobot import org.dhis2.usescases.searchte.robot.filterRobot -fun searchFlowRobot(searchFlowRobot: SearchFlowRobot.() -> Unit) { - SearchFlowRobot().apply { +fun searchFlowRobot( + composeTestRule: ComposeTestRule, + searchFlowRobot: SearchFlowRobot.() -> Unit +) { + SearchFlowRobot(composeTestRule).apply { searchFlowRobot() } } -class SearchFlowRobot : BaseRobot() { +class SearchFlowRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun filterByOpenEnrollmentStatus(enrollmentStatus: String) { - filterRobot { + filterRobot(composeTestRule) { clickOnFilter() clickOnFilterBy(enrollmentStatus) clickOnFilterActiveOption() @@ -20,19 +24,15 @@ class SearchFlowRobot : BaseRobot() { } } - fun checkSearchCounters(filterAtPositionCount: String, filter: String, filterTotalCount: String) { - filterRobot { + fun checkSearchCounters( + filterAtPositionCount: String, + filter: String, + filterTotalCount: String + ) { + filterRobot(composeTestRule) { checkFilterCounter(filterTotalCount) checkCountAtFilter(filter, filterAtPositionCount) clickOnFilter() } } - - fun checkTEIEnrollment() { - filterRobot { - checkTEIsAreOpen() - checkTEINotSync() - } - } - -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt index 4b26bb4f14..c99fc61778 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/searchFlow/SearchFlowTest.kt @@ -14,7 +14,9 @@ import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel import org.dhis2.usescases.flow.teiFlow.teiFlowRobot import org.dhis2.usescases.searchTrackEntity.SearchTEActivity +import org.dhis2.usescases.searchte.robot.filterRobot import org.hisp.dhis.android.core.mockwebserver.ResponseController +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -37,6 +39,7 @@ class SearchFlowTest : BaseTest() { } @Test + @Ignore("Flaky test, will be looked up in ANDROAPP-6478") fun shouldCreateTEIAndFilterByEnrollment() { mockWebServerRobot.addResponse( ResponseController.GET, @@ -53,7 +56,6 @@ class SearchFlowTest : BaseTest() { ) val filterCounter = "1" val filterTotalCount = "2" - enableComposeForms() prepareWomanProgrammeIntentAndLaunchActivity(rule) teiFlowRobot(composeTestRule) { @@ -61,10 +63,13 @@ class SearchFlowTest : BaseTest() { pressBack() } - searchFlowRobot { + searchFlowRobot(composeTestRule) { filterByOpenEnrollmentStatus(enrollmentStatus) checkSearchCounters(filterCounter, enrollmentStatus, filterTotalCount) - checkTEIEnrollment() + } + + filterRobot(composeTestRule) { + checkTEINotSync() } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt index cdfe80d3b1..df2083d8d1 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/SyncFlowTest.kt @@ -1,8 +1,6 @@ package org.dhis2.usescases.flow.syncFlow import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick import androidx.lifecycle.MutableLiveData import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -13,6 +11,7 @@ import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest import org.dhis2.usescases.datasets.dataSetTableRobot import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailActivity +import org.dhis2.usescases.development.ConflictGenerator import org.dhis2.usescases.flow.syncFlow.robot.dataSetRobot import org.dhis2.usescases.flow.syncFlow.robot.eventWithoutRegistrationRobot import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity @@ -20,7 +19,9 @@ import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.teidashboard.robot.eventRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot -import org.hisp.dhis.android.core.mockwebserver.ResponseController.GET +import org.hisp.dhis.android.core.D2Manager +import org.hisp.dhis.android.core.imports.ImportStatus +import org.hisp.dhis.android.core.mockwebserver.ResponseController.Companion.GET import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -46,6 +47,9 @@ class SyncFlowTest : BaseTest() { private lateinit var workInfoStatusLiveData: MutableLiveData> + private val d2 = D2Manager.getD2() + private val conflictGenerator = ConflictGenerator(d2) + override fun setUp() { super.setUp() setupMockServer() @@ -53,7 +57,7 @@ class SyncFlowTest : BaseTest() { ApplicationProvider.getApplicationContext().mutableWorkInfoStatuses } - @Ignore("Flaky test, will be fixed in next release") + @Ignore("Flaky test, will be addressed in issue ANDROAPP-6465") @Test fun shouldShowErrorWhenTEISyncFails() { mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) @@ -70,7 +74,7 @@ class SyncFlowTest : BaseTest() { openNextSearchParameter("Last name") typeOnNextSearchTextParameter(teiLastName) clickOnSearch() - clickOnTEI(teiLastName, composeTestRule) + clickOnTEI(teiName) } teiDashboardRobot(composeTestRule) { @@ -78,17 +82,13 @@ class SyncFlowTest : BaseTest() { } eventRobot(composeTestRule) { - fillRadioButtonForm(4) +// fillRadioButtonForm(4) clickOnFormFabButton() clickOnCompleteButton() } - teiDashboardRobot(composeTestRule) { - composeTestRule.onNodeWithText("Sync").performClick() - } - syncFlowRobot(composeTestRule) { - waitToDebounce(500) + clickOnEventToSync() clickOnSyncButton() workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.FAILED))) @@ -97,7 +97,7 @@ class SyncFlowTest : BaseTest() { cleanLocalDatabase() } - @Ignore("Flaky test, will be addressed in issue #ANDROAPP-6155") + @Ignore("Flaky test, will be addressed in issue ANDROAPP-6465") @Test fun shouldSuccessfullySyncSavedEvent() { mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) @@ -116,6 +116,7 @@ class SyncFlowTest : BaseTest() { syncFlowRobot(composeTestRule) { clickOnEventToSync() clickOnSyncButton() + conflictGenerator.generateConflictInEvent(ANTENATAL_CARE_EVENT_UID, ImportStatus.SUCCESS) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.SUCCEEDED))) checkSyncWasSuccessfully() @@ -123,8 +124,8 @@ class SyncFlowTest : BaseTest() { cleanLocalDatabase() } + @Ignore("Flaky test, will be addressed in issue ANDROAPP-6465") @Test - @Ignore("Flaky test, will be addressed in issue #ANDROAPP-6139") fun shouldShowErrorWhenSyncEventFails() { mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) @@ -142,6 +143,7 @@ class SyncFlowTest : BaseTest() { syncFlowRobot(composeTestRule) { clickOnEventToSync() clickOnSyncButton() + conflictGenerator.generateConflictInEvent(ANTENATAL_CARE_EVENT_UID, ImportStatus.ERROR) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.FAILED))) checkSyncFailed() @@ -149,6 +151,7 @@ class SyncFlowTest : BaseTest() { cleanLocalDatabase() } + @Ignore("Flaky test, will be addressed in issue ANDROAPP-6465") @Test fun shouldSuccessfullySyncSavedDataSet() { mockWebServerRobot.addResponse(GET, "/api/system/ping", API_PING_RESPONSE_OK) @@ -178,23 +181,21 @@ class SyncFlowTest : BaseTest() { syncFlowRobot(composeTestRule) { clickOnDataSetToSync(0) clickOnSyncButton() + conflictGenerator.generateStatusConflictInDataSet(ImportStatus.SUCCESS) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) - composeTestRule.waitForIdle() workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.SUCCEEDED))) - composeTestRule.waitForIdle() - waitToDebounce(3000) - checkSyncWasSuccessfully() //sync failed + checkSyncWasSuccessfully() } cleanLocalDatabase() } - @Ignore("Flaky test, will be addressed in next release") + @Ignore("Flaky test, will be addressed in issue ANDROAPP-6465") @Test fun shouldShowErrorWhenSyncDataSetFails() { prepareFacilityDataSetIntentAndLaunchActivity(ruleDataSet) dataSetRobot { - clickOnDataSetAtPosition(1) + clickOnDataSetAtPosition(0) } dataSetTableRobot(composeTestRule) { @@ -213,8 +214,9 @@ class SyncFlowTest : BaseTest() { } syncFlowRobot(composeTestRule) { - clickOnDataSetToSync(1) + clickOnDataSetToSync(0) clickOnSyncButton() + conflictGenerator.generateStatusConflictInDataSet(ImportStatus.ERROR) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.RUNNING))) workInfoStatusLiveData.postValue(arrayListOf(mockedGranularWorkInfo(WorkInfo.State.FAILED))) checkSyncFailed() @@ -230,12 +232,13 @@ class SyncFlowTest : BaseTest() { arrayListOf("GRANULAR"), Data.EMPTY, 0, - 0 + 0, ) } companion object { + const val ANTENATAL_CARE_EVENT_UID = "onXW2DQHRGS" const val LAB_MONITORING_EVENT_DATE = "28/6/2020" const val API_PING_RESPONSE_OK = "mocks/systeminfo/ping.txt" } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/SyncFlowRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/SyncFlowRobot.kt index ff74718f41..44dbf0d4fb 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/SyncFlowRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/syncFlow/robot/SyncFlowRobot.kt @@ -30,15 +30,17 @@ class SyncFlowRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { composeTestRule.onNodeWithTag(MAIN_BUTTON_TAG, useUnmergedTree = true).performClick() } + @OptIn(ExperimentalTestApi::class) fun checkSyncWasSuccessfully() { val expectedTitle = InstrumentationRegistry.getInstrumentation() - .targetContext.getString(R.string.sync_dialog_title_not_synced) - composeTestRule.onNodeWithTag(TITLE, useUnmergedTree = true).assert(hasText(expectedTitle)) + .targetContext.getString(R.string.sync_dialog_title_synced) + composeTestRule.waitUntilAtLeastOneExists(hasText(expectedTitle), 2_000L) + composeTestRule.onNodeWithTag(TITLE, useUnmergedTree = true).assert(hasText(expectedTitle, true)) } fun checkSyncFailed() { val expectedTitle = InstrumentationRegistry.getInstrumentation() - .targetContext.getString(R.string.sync_dialog_title_not_synced) + .targetContext.getString(R.string.sync_dialog_title_error) composeTestRule.onNodeWithTag(TITLE, useUnmergedTree = true).assert(hasText(expectedTitle)) } diff --git a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt index acfe77dd6c..0e394eacdb 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/flow/teiFlow/TeiFlowTest.kt @@ -2,17 +2,18 @@ package org.dhis2.usescases.flow.teiFlow import android.content.Intent import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule +import org.dhis2.LazyActivityScenarioRule import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE import org.dhis2.commons.date.DateUtils +import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.flow.teiFlow.entity.EnrollmentListUIModel import org.dhis2.usescases.flow.teiFlow.entity.RegisterTEIUIModel import org.dhis2.usescases.searchTrackEntity.SearchTEActivity -import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity import org.hisp.dhis.android.core.mockwebserver.ResponseController import org.junit.Rule import org.junit.Test @@ -22,12 +23,8 @@ import java.util.Date @RunWith(AndroidJUnit4::class) class TeiFlowTest : BaseTest() { - - @get:Rule - val rule = ActivityTestRule(TeiDashboardMobileActivity::class.java, false, false) - @get:Rule - val ruleSearch = ActivityTestRule(SearchTEActivity::class.java, false, false) + val ruleSearch = lazyActivityScenarioRule(launchActivity = false) @get:Rule val composeTestRule = createComposeRule() @@ -53,7 +50,6 @@ class TeiFlowTest : BaseTest() { val enrollmentListDetails = createEnrollmentList() val registerTeiDetails = createRegisterTEI() - enableComposeForms() setupCredentials() setDatePicker() prepareWomanProgrammeIntentAndLaunchActivity(ruleSearch) @@ -100,11 +96,16 @@ class TeiFlowTest : BaseTest() { return dateFormat } - private fun prepareWomanProgrammeIntentAndLaunchActivity(ruleSearch: ActivityTestRule) { - Intent().apply { + private fun prepareWomanProgrammeIntentAndLaunchActivity( + ruleSearch: LazyActivityScenarioRule + ) { + Intent( + ApplicationProvider.getApplicationContext(), + SearchTEActivity::class.java + ).apply { putExtra(PROGRAM_UID, WOMAN_PROGRAM_UID_VALUE) putExtra(TE_TYPE, WOMAN_TE_TYPE_VALUE) - }.also { ruleSearch.launchActivity(it) } + }.also { ruleSearch.launch(it) } } companion object { @@ -117,5 +118,7 @@ class TeiFlowTest : BaseTest() { const val ORG_UNIT = "Ngelehun CHC" const val NAME = "Marta" const val LASTNAME = "Stuart" + + const val DATE_FORMAT = "dd/M/yyyy" } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/form/FormIntents.kt b/app/src/androidTest/java/org/dhis2/usescases/form/FormIntents.kt deleted file mode 100644 index c7c55ef64d..0000000000 --- a/app/src/androidTest/java/org/dhis2/usescases/form/FormIntents.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.dhis2.usescases.form - -import androidx.test.core.app.ApplicationProvider -import org.dhis2.LazyActivityScenarioRule -import org.dhis2.form.model.EventMode -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity - -const val PROGRAM_UID = "PROGRAM_UID" -const val PROGRAM_XX_PROGRAM_RULES = "jIT6KcSZiAN" -const val EVENT_GAMMA = "MIZVQnTD4HW" - - -fun prepareIntentAndLaunchEventActivity(rule: LazyActivityScenarioRule) { - EventCaptureActivity.intent( - ApplicationProvider.getApplicationContext(), - EVENT_GAMMA, - PROGRAM_XX_PROGRAM_RULES, - EventMode.CHECK - ).also { rule.launch(it) } -} diff --git a/app/src/androidTest/java/org/dhis2/usescases/form/FormRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/form/FormRobot.kt deleted file mode 100644 index e42dfdab16..0000000000 --- a/app/src/androidTest/java/org/dhis2/usescases/form/FormRobot.kt +++ /dev/null @@ -1,156 +0,0 @@ -package org.dhis2.usescases.form - -import android.app.Activity -import android.view.MenuItem -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onAllNodesWithTag -import androidx.compose.ui.test.onFirst -import androidx.compose.ui.test.onNodeWithText -import androidx.test.espresso.Espresso.onData -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.doesNotExist -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition -import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup -import androidx.test.espresso.matcher.RootMatchers.withDecorView -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import org.dhis2.R -import org.dhis2.common.BaseRobot -import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.atPosition -import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasItem -import org.dhis2.common.viewactions.clickChildViewWithId -import org.dhis2.common.viewactions.scrollToBottomRecyclerView -import org.dhis2.form.ui.FormViewHolder -import org.dhis2.usescases.form.FormTest.Companion.NO_ACTION_POSITION -import org.hamcrest.CoreMatchers.allOf -import org.hamcrest.CoreMatchers.anything -import org.hamcrest.CoreMatchers.instanceOf -import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.CoreMatchers.not - - -fun formRobot( - composeTestRule: ComposeTestRule, - formRobot: FormRobot.() -> Unit -) { - FormRobot(composeTestRule).apply { - formRobot() - } -} - -class FormRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { - - fun clickOnASpecificSection(sectionLabel: String) { - onView(withText(sectionLabel)).perform(click()) - } - - private fun clickOnSpinner(position: Int) { - onView(withId(R.id.recyclerView)) - .perform( - actionOnItemAtPosition( - position, clickChildViewWithId(R.id.inputEditText) - ) - ) - } - - private fun selectAction(position: Int) { - onData(anything()) - .inRoot(isPlatformPopup()) - .atPosition(position) - .perform(click()) - } - - fun resetToNoAction(position: Int) { - clickOnSpinner(position) - selectAction(NO_ACTION_POSITION) - } - - fun checkHiddenField(label: String) { - onView(withId(R.id.recyclerView)) - .check(matches(not(hasItem(withText(label))))) - } - - fun checkHiddenSection(label: String) { - onView(withId(R.id.recyclerView)) - .check(matches(not(hasItem(withText(label))))) - } - - fun checkValueWasAssigned(value: String) { - onView(withId(R.id.recyclerView)) - .check( - matches( - hasItem( - allOf( - hasDescendant(withId(R.id.input_editText)), - hasDescendant(withText(value)) - ) - ) - ) - ) - } - - fun checkWarningIsShown() { - onView(withId(R.id.recyclerView)) - .check(matches(hasItem(hasDescendant(withText("Warning with Current Event "))))) - } - - fun checkErrorIsShown() { - onView(withId(R.id.recyclerView)) - .check(matches(hasItem(hasDescendant(withText("Error with current event "))))) - } - - fun checkPopUpWithMessageOnCompleteIsShown(message: String, composeTestRule: ComposeTestRule) { - composeTestRule.onAllNodesWithTag(message).onFirst().assertExists() - } - - fun checkIndicatorIsDisplayed(name: String, value: String) { - composeTestRule.onNodeWithText(name).assertIsDisplayed() - composeTestRule.onNodeWithText(value).assertIsDisplayed() - } - - fun checkLabel(label: String, position: Int) { - onView(withId(R.id.recyclerView)) - .check(matches(atPosition(position, hasDescendant(withText(label))))) - } - - fun clickOnSaveForm() { - onView(withId(R.id.actionButton)).perform(click()) - } - - fun checkHiddenOption(label: String, position: Int) { - clickOnSpinner(position) - onView(allOf(instanceOf(MenuItem::class.java), hasDescendant(withText(label)))) - .check(doesNotExist()) - selectAction(0) - } - - fun checkDisplayedOption(label: String, position: Int, activity: Activity) { - clickOnSpinner(position) - onView(withText(label)) - .inRoot(withDecorView(not(`is`(activity.window.decorView)))) - .check(matches(isDisplayed())) - selectAction(0) - } - - fun clickOnSelectOption(position: Int, optionPosition: Int) { - clickOnSpinner(position) - selectAction(optionPosition) - } - - fun scrollToBottomForm() { - onView(withId(R.id.recyclerView)).perform(scrollToBottomRecyclerView()) - } - - fun goToAnalytics() { - onView(withId(R.id.navigation_analytics)).perform(click()) - } - - fun goToDataEntry() { - onView(withId(R.id.navigation_data_entry)).perform(click()) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/form/FormTest.kt b/app/src/androidTest/java/org/dhis2/usescases/form/FormTest.kt deleted file mode 100644 index 06c74900a8..0000000000 --- a/app/src/androidTest/java/org/dhis2/usescases/form/FormTest.kt +++ /dev/null @@ -1,246 +0,0 @@ -package org.dhis2.usescases.form - -import android.util.Log -import androidx.compose.ui.test.junit4.createComposeRule -import org.dhis2.lazyActivityScenarioRule -import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity -import org.junit.After -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test - -const val firstSectionPosition = 2 - -class FormTest : BaseTest() { - - @get:Rule - val ruleEvent = lazyActivityScenarioRule(launchActivity = false) - - - @get:Rule - val composeTestRule = createComposeRule() - - @After - override fun teardown() { - cleanLocalDatabase() - super.teardown() - } - - @Ignore("When added Event details section the test fails, is commented to be refactored with new form in a specific issue") - @Test - fun shouldApplyProgramRules() { - prepareIntentAndLaunchEventActivity(ruleEvent) - - formRobot(composeTestRule) { - clickOnASpecificSection("Gamma Rules A") - } - applyHideField() - applyHideSection() - applyShowWarning() - applyShowError() - applySetMandatoryField() - applyHideOption() - applyHideOptionGroup() - applyShowOptionGroup() - applyAssignValue() - applyDisplayText() - applyDisplayKeyValue() - applyWarningOnComplete() - applyErrorOnComplete() - } - - private fun applyHideField() { - formRobot(composeTestRule) { - clickOnSelectOption( - firstSectionPosition, - HIDE_FIELD_POSITION - ) - checkHiddenField("ZZ TEST LONGTEST") - } - } - - private fun applyHideSection() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - HIDE_SECTION_POSITION - ) - checkHiddenSection("Gamma Rules A") - } - } - - private fun applyShowWarning() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - SHOW_WARNING_POSITION - ) - checkWarningIsShown() - } - } - - private fun applyShowError() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - SHOW_ERROR_POSITION - ) - checkErrorIsShown() - } - } - - private fun applySetMandatoryField() { - formRobot(composeTestRule) { - val nonMandatoryLabel = "ZZ TEST NUMBER" - val mandatoryLabel = "ZZ TEST NUMBER *" - val position = 5 - resetToNoAction(firstSectionPosition) - checkLabel(nonMandatoryLabel, position) - clickOnSelectOption( - firstSectionPosition, - MANDATORY_FIELD_POSITION - ) - checkLabel(mandatoryLabel, position) - } - } - - private fun applyHideOption() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - HIDE_OPTION_POSITION - ) - checkHiddenOption("North", OPTION_SET_FIELD_POSITION) - } - } - - private fun applyHideOptionGroup() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - HIDE_OPTION_GROUP_POSITION - ) - checkHiddenOption("North", OPTION_SET_FIELD_POSITION) - checkHiddenOption("West", OPTION_SET_FIELD_POSITION) - } - } - - private fun applyShowOptionGroup() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - SHOW_OPTION_POSITION - ) - - val activity = waitForActivityScenario() - checkDisplayedOption("North", OPTION_SET_FIELD_POSITION, activity) - checkDisplayedOption("West", OPTION_SET_FIELD_POSITION, activity) - } - } - - private fun applyAssignValue() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - ASSIGN_VALUE_POSITION - ) - checkValueWasAssigned(ASSIGNED_VALUE_TEXT) - } - } - - private fun applyDisplayText() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - DISPLAY_TEXT_POSITION - ) - pressBack() - goToAnalytics() - checkIndicatorIsDisplayed("Info", "Current Option Selected: DT") - goToDataEntry() - } - } - - private fun applyDisplayKeyValue() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - DISPLAY_KEY_POSITION - ) - pressBack() - goToAnalytics() - checkIndicatorIsDisplayed("Current Option", "DKVP") - goToDataEntry() - } - } - - private fun applyWarningOnComplete() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - WARNING_COMPLETE_POSITION - ) - scrollToBottomForm() - waitToDebounce(1000) - clickOnSaveForm() - checkPopUpWithMessageOnCompleteIsShown("WARNING_ON_COMPLETE", composeTestRule) - pressBack() - } - } - - private fun applyErrorOnComplete() { - formRobot(composeTestRule) { - resetToNoAction(firstSectionPosition) - clickOnSelectOption( - firstSectionPosition, - ERROR_COMPLETE_POSITION - ) - scrollToBottomForm() - waitToDebounce(1000) - clickOnSaveForm() - checkPopUpWithMessageOnCompleteIsShown("ERROR_ON_COMPLETE", composeTestRule) - pressBack() - } - } - - private fun waitForActivityScenario(): EventCaptureActivity { - var activity: EventCaptureActivity? = null - ruleEvent.getScenario().onActivity { - activity = it - } - while (activity == null) { - Log.d("FormTest", "Waiting for activity to be initialized") - } - return activity!! - } - - companion object { - const val NO_ACTION_POSITION = 0 - const val HIDE_FIELD_POSITION = 1 - const val HIDE_SECTION_POSITION = 2 - const val HIDE_OPTION_POSITION = 3 - const val OPTION_SET_FIELD_POSITION = 6 - const val HIDE_OPTION_GROUP_POSITION = 4 - const val ASSIGN_VALUE_POSITION = 5 - const val ASSIGNED_VALUE_TEXT = "Result for current event" - const val SHOW_WARNING_POSITION = 6 - const val WARNING_COMPLETE_POSITION = 7 - const val SHOW_ERROR_POSITION = 8 - const val ERROR_COMPLETE_POSITION = 9 - const val MANDATORY_FIELD_POSITION = 10 - const val DISPLAY_TEXT_POSITION = 11 - const val DISPLAY_KEY_POSITION = 12 - const val SHOW_OPTION_POSITION = 14 - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/jira/JiraTest.kt b/app/src/androidTest/java/org/dhis2/usescases/jira/JiraTest.kt deleted file mode 100644 index 4880b75867..0000000000 --- a/app/src/androidTest/java/org/dhis2/usescases/jira/JiraTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.dhis2.usescases.jira - -import android.Manifest -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule -import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.main.MainActivity -import org.dhis2.usescases.main.MainRobot -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class JiraTest : BaseTest() { - private lateinit var mainRobot: MainRobot - - @get:Rule - val rule = ActivityTestRule(MainActivity::class.java, false, false) - - override fun setUp() { - super.setUp() - mainRobot = MainRobot() - } - - @Test - fun openOnJiraIssue() { - startActivity() - mainRobot.clickOnNavigationDrawerMenu() - .clickJiraIssue() - } - - fun startActivity() { - rule.launchActivity(null) - } -} diff --git a/app/src/androidTest/java/org/dhis2/usescases/login/LoginRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/login/LoginRobot.kt index fcb4e03899..ec386cf72c 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/login/LoginRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/login/LoginRobot.kt @@ -1,7 +1,9 @@ package org.dhis2.usescases.login -import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.TypeTextAction @@ -15,25 +17,33 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry import org.dhis2.R -import org.dhis2.usescases.BaseTest import org.dhis2.common.BaseRobot import org.dhis2.common.viewactions.ClickDrawableAction import org.dhis2.ui.dialogs.bottomsheet.CLICKABLE_TEXT_TAG -import org.dhis2.ui.dialogs.bottomsheet.MAIN_BUTTON_TAG import org.dhis2.usescases.BaseTest.Companion.MOCK_SERVER_URL import org.dhis2.usescases.about.PolicyView import org.dhis2.usescases.qrScanner.ScanActivity import org.dhis2.utils.WebViewActivity import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.not -fun loginRobot(loginBody: LoginRobot.() -> Unit) { - LoginRobot().apply { +import androidx.compose.ui.test.hasText + + +fun loginRobot( + composeTestRule: ComposeTestRule, + loginBody: LoginRobot.() -> Unit +) { + LoginRobot(composeTestRule).apply { loginBody() } } -class LoginRobot : BaseRobot() { +class LoginRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { + + + val context = InstrumentationRegistry.getInstrumentation().targetContext fun typeServer(server: String) { onView(withId(R.id.server_url_edit)).perform(TypeTextAction(server)) @@ -44,6 +54,10 @@ class LoginRobot : BaseRobot() { onView(withId(R.id.server_url_edit)).perform(clearText()) } + fun selectUsernameField() { + onView(withId(R.id.user_name_edit)).perform(click()) + } + fun typeUsername(username: String) { onView(withId(R.id.user_name_edit)).perform(TypeTextAction(username)) pressImeActionButton() @@ -62,6 +76,10 @@ class LoginRobot : BaseRobot() { onView(withId(R.id.clearPassButton)).perform(click()) } + fun clearURLField() { + onView(withId(R.id.clearUrl)).perform(click()) + } + fun clickLoginButton() { onView(withId(R.id.login)).perform(click()) } @@ -74,10 +92,27 @@ class LoginRobot : BaseRobot() { onView(withId(R.id.login)).check(matches(not(isEnabled()))) } + fun checkLoginButtonIsVisible() { + onView(withId(R.id.login)).check(matches((isEnabled()))) + } + + fun checkAuthErrorAlertIsVisible() { onView(withText(LOGIN_ERROR_TITLE)).check(matches(isDisplayed())) } + fun clickOKAuthErrorAlert() { + onView(withText(OK)).perform(click()) + } + + fun clickCancelAuthErrorAlert() { + onView(withText(Cancel)).perform(click()) + } + + fun checkAuthErrorOKButtonIsVisible() { + onView(withText(OK)).check(matches(isDisplayed())) + } + fun checkUnblockSessionViewIsVisible() { onView(withId(R.id.cardview_pin)).check(matches(isDisplayed())) } @@ -90,6 +125,10 @@ class LoginRobot : BaseRobot() { onView(withId(R.id.user_pass_edit)).check(matches(withText(""))) } + fun checkURLFieldIsClear() { + onView(withId(R.id.server_url_edit)).check(matches(withText(""))) + } + fun checkURL(url: String) { onView(withId(R.id.server_url_edit)).check(matches(withText(url))) } @@ -118,12 +157,20 @@ class LoginRobot : BaseRobot() { onView(withId(android.R.id.content)).check(matches(isDisplayed())) } - fun clickOnPrivacyPolicy(composeTestRule: ComposeContentTestRule) { + fun clickOnPrivacyPolicy() { composeTestRule.onNodeWithTag(CLICKABLE_TEXT_TAG).performClick() } - fun acceptTrackerDialog(composeTestRule: ComposeContentTestRule){ - composeTestRule.onNodeWithTag(MAIN_BUTTON_TAG).performClick() + fun acceptTrackerDialog() { + val title = InstrumentationRegistry + .getInstrumentation() + .targetContext.getString(R.string.improve_app_msg_title) + composeTestRule.onNodeWithText(title).assertIsDisplayed() + } + + fun clickYesOnAcceptTrackerDialog() { + composeTestRule.onNodeWithText(context.getString(R.string.yes)) + .performClick() } fun checkPrivacyViewIsOpened() { @@ -132,5 +179,7 @@ class LoginRobot : BaseRobot() { companion object { const val LOGIN_ERROR_TITLE = "Login error" + const val OK = "OK" + const val Cancel = "cancel" } } diff --git a/app/src/androidTest/java/org/dhis2/usescases/login/LoginTest.kt b/app/src/androidTest/java/org/dhis2/usescases/login/LoginTest.kt index 0f8037df19..1ebcbc2fdc 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/login/LoginTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/login/LoginTest.kt @@ -4,34 +4,28 @@ import android.app.Activity import android.app.Instrumentation import android.content.Intent import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.matcher.IntentMatchers -import androidx.test.rule.ActivityTestRule -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.dhis2.commons.Constants.EXTRA_DATA import org.dhis2.commons.prefs.Preference.Companion.PIN import org.dhis2.commons.prefs.Preference.Companion.SESSION_LOCKED +import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.main.MainActivity import org.dhis2.usescases.qrScanner.ScanActivity import org.hamcrest.CoreMatchers.allOf import org.hisp.dhis.android.core.D2Manager -import org.hisp.dhis.android.core.mockwebserver.ResponseController.API_ME_PATH -import org.hisp.dhis.android.core.mockwebserver.ResponseController.API_SYSTEM_INFO_PATH -import org.hisp.dhis.android.core.mockwebserver.ResponseController.GET +import org.hisp.dhis.android.core.mockwebserver.ResponseController.Companion.API_ME_PATH +import org.hisp.dhis.android.core.mockwebserver.ResponseController.Companion.API_SYSTEM_INFO_PATH +import org.hisp.dhis.android.core.mockwebserver.ResponseController.Companion.GET +import org.junit.Ignore import org.junit.Rule import org.junit.Test -class LoginTest : BaseTest() { - @get:Rule - val ruleLogin = ActivityTestRule(LoginActivity::class.java, false, false) +class LoginTest : BaseTest() { @get:Rule - val mainRule = ActivityTestRule(MainActivity::class.java, false, false) + val ruleLogin = lazyActivityScenarioRule(launchActivity = false) @get:Rule val composeTestRule = createComposeRule() @@ -43,87 +37,65 @@ class LoginTest : BaseTest() { } @Test - fun shouldLoginSuccessfullyWhenCredentialsAreRight() { + fun loginFlow() { mockWebServerRobot.addResponse(GET, API_ME_PATH, API_ME_RESPONSE_OK) mockWebServerRobot.addResponse(GET, API_SYSTEM_INFO_PATH, API_SYSTEM_INFO_RESPONSE_OK) - mockWebServerRobot.addResponse(GET, PATH_WEBAPP_GENERAL_SETTINGS, API_METADATA_SETTINGS_RESPONSE_ERROR, 404) + mockWebServerRobot.addResponse( + GET, + PATH_WEBAPP_GENERAL_SETTINGS, + API_METADATA_SETTINGS_RESPONSE_ERROR, + 404 + ) mockWebServerRobot.addResponse(GET, PATH_WEBAPP_INFO, API_METADATA_SETTINGS_INFO_ERROR, 404) startLoginActivity() - loginRobot { - clearServerField() - typeServer(MOCK_SERVER_URL) - typeUsername(USERNAME) - typePassword(PASSWORD) - clickLoginButton() - CoroutineScope(Dispatchers.IO).launch { - delay(2000) - withContext(Dispatchers.Main) { - acceptTrackerDialog(composeTestRule) - } - } - } - cleanDatabase() - } - @Test - fun shouldGetAuthErrorWhenCredentialsAreWrong() { - mockWebServerRobot.addResponse(GET, API_ME_PATH, API_ME_UNAUTHORIZE, HTTP_UNAUTHORIZE) - startLoginActivity() - loginRobot { - clearServerField() - typeServer(MOCK_SERVER_URL) - typeUsername(USERNAME) - typePassword(PASSWORD) - clickLoginButton() - CoroutineScope(Dispatchers.IO).launch { - delay(2000) - withContext(Dispatchers.Main) { - checkAuthErrorAlertIsVisible() - } - } - } - } + loginRobot(composeTestRule) { - @Test - fun shouldHideLoginButtonIfPasswordIsMissing() { - startLoginActivity() - - loginRobot { + // Test case - [ANDROAPP-4122](https://dhis2.atlassian.net/browse/ANDROAPP-4122) clearServerField() typeServer(MOCK_SERVER_URL) typeUsername(USERNAME) typePassword(PASSWORD) + clearUsernameField() clearPasswordField() + checkUsernameFieldIsClear() + checkPasswordFieldIsClear() + + //Test case - [ANDROAPP-4123](https://dhis2.atlassian.net/browse/ANDROAPP-4123) checkLoginButtonIsHidden() - } - } - @Test - fun shouldLaunchWebViewWhenClickAccountRecoveryAndServerIsFilled() { - enableIntents() - startLoginActivity() - loginRobot { + // Test case - [ANDROAPP-4126](https://dhis2.atlassian.net/browse/ANDROAPP-4126) + enableIntents() clearServerField() typeServer(MOCK_SERVER_URL) clickAccountRecovery() checkWebviewWithRecoveryAccountIsOpened() - } - } + pressBack() - @Test - fun shouldClearFieldsAndHideLoginButtonWhenClickCredentialXButton() { - startLoginActivity() - loginRobot { - clearServerField() - typeServer(MOCK_SERVER_URL) + // Test case - [ANDROAPP-4121](https://dhis2.atlassian.net/browse/ANDROAPP-4121) + mockWebServerRobot.addResponse(GET, API_ME_PATH, API_ME_UNAUTHORIZE, HTTP_UNAUTHORIZE) + selectUsernameField() typeUsername(USERNAME) typePassword(PASSWORD) - clearUsernameField() + clickLoginButton() + checkAuthErrorAlertIsVisible() + clickOKAuthErrorAlert() + + // Test case - [ANDROAPP-4121](https://dhis2.atlassian.net/browse/ANDROAPP-4121) + mockWebServerRobot.addResponse(GET, API_ME_PATH, API_ME_RESPONSE_OK) clearPasswordField() - checkUsernameFieldIsClear() - checkPasswordFieldIsClear() - checkLoginButtonIsHidden() + typePassword(PASSWORD) + clickLoginButton() + + //Test case - [ANDROAPP-5184](https://dhis2.atlassian.net/browse/ANDROAPP-5184) + checkShareDataDialogIsDisplayed() + clickOnPrivacyPolicy() + checkPrivacyViewIsOpened() + pressBack() + acceptTrackerDialog() + clickYesOnAcceptTrackerDialog() } + cleanDatabase() } @Test @@ -133,24 +105,18 @@ class LoginTest : BaseTest() { startLoginActivity() - loginRobot { + loginRobot(composeTestRule) { checkUnblockSessionViewIsVisible() } } - @Test - fun shouldGoToHomeScreenWhenUserIsLoggedIn() { - setupCredentials() - startLoginActivity() - } - @Test fun shouldGenerateLoginThroughQR() { enableIntents() mockOnActivityForResult() startLoginActivity() - loginRobot { + loginRobot(composeTestRule) { clickQRButton() checkQRScanIsOpened() checkURL(MOCK_SERVER_URL) @@ -167,36 +133,13 @@ class LoginTest : BaseTest() { ) } - @Test - fun shouldDisplayShareDataDialogAndOpenPrivacyPolicy() { - mockWebServerRobot.addResponse(GET, API_ME_PATH, API_ME_RESPONSE_OK) - mockWebServerRobot.addResponse(GET, API_SYSTEM_INFO_PATH, API_SYSTEM_INFO_RESPONSE_OK) - mockWebServerRobot.addResponse(GET, PATH_WEBAPP_GENERAL_SETTINGS, API_METADATA_SETTINGS_RESPONSE_ERROR, 404) - mockWebServerRobot.addResponse(GET, PATH_WEBAPP_INFO, API_METADATA_SETTINGS_INFO_ERROR, 404) - startLoginActivity() - loginRobot { - clearServerField() - typeServer(MOCK_SERVER_URL) - typeUsername(USERNAME) - typePassword(PASSWORD) - clickLoginButton() - CoroutineScope(Dispatchers.IO).launch { - delay(2000) - withContext(Dispatchers.Main) { - checkShareDataDialogIsDisplayed() - clickOnPrivacyPolicy(composeTestRule) - checkPrivacyViewIsOpened() - } - } - } - } - - fun startMainActivity() { - mainRule.launchActivity(null) - } - private fun startLoginActivity() { - ruleLogin.launchActivity(null) + val intent = Intent( + ApplicationProvider.getApplicationContext(), + LoginActivity::class.java + ) + ruleLogin.launch(intent) + } private fun cleanDatabase() { @@ -210,14 +153,10 @@ class LoginTest : BaseTest() { const val API_SYSTEM_INFO_RESPONSE_OK = "mocks/systeminfo/systeminfo.json" const val API_METADATA_SETTINGS_RESPONSE_ERROR = "mocks/settingswebapp/generalsettings_404.json" - const val API_METADATA_SETTINGS_PROGRAM_RESPONSE_ERROR = - "mocks/settingswebapp/programsettings_404.json" - const val API_METADATA_SETTINGS_DATASET_RESPONSE_ERROR = - "mocks/settingswebapp/datasetsettings_404.json" const val API_METADATA_SETTINGS_INFO_ERROR = "mocks/settingswebapp/infosettings_404.json" - const val PATH_WEBAPP_GENERAL_SETTINGS = "/api/dataStore/ANDROID_SETTING_APP/general_settings?.*" + const val PATH_WEBAPP_GENERAL_SETTINGS = + "/api/dataStore/ANDROID_SETTING_APP/general_settings?.*" const val PATH_WEBAPP_INFO = "/api/dataStore/ANDROID_SETTINGS_APP/info?.*" - const val PATH_APPS = "/api/apps?.*" const val DB_GENERATED_BY_LOGIN = "127-0-0-1-8080_test_unencrypted.db" const val PIN_PASSWORD = 1234 const val USERNAME = "test" diff --git a/app/src/androidTest/java/org/dhis2/usescases/main/MainRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/main/MainRobot.kt index c144a6327a..f30b9162cd 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/main/MainRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/main/MainRobot.kt @@ -1,28 +1,21 @@ package org.dhis2.usescases.main -import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assert import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertTextEquals -import androidx.compose.ui.test.hasAnyDescendant -import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onChildAt import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollToIndex import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.contrib.NavigationViewActions import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText import org.dhis2.R import org.dhis2.common.BaseRobot import org.dhis2.usescases.login.LoginActivity -import org.dhis2.usescases.main.program.HOME_ITEM import org.dhis2.usescases.main.program.HOME_ITEMS +import org.dhis2.usescases.main.program.HasPrograms import org.hamcrest.CoreMatchers.allOf fun homeRobot(robotBody: MainRobot.() -> Unit) { @@ -46,26 +39,19 @@ class MainRobot : BaseRobot() { onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.block_button)) } - fun clickOnLogout() { - onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.logout_button)) - waitToDebounce(LOGOUT_TRANSITION) - } - fun clickAbout() = apply { onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.menu_about)) waitToDebounce(FRAGMENT_TRANSITION) } - fun clickJiraIssue() = apply { - onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.menu_jira)) - } - fun clickDeleteAccount() = apply { onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.delete_account)) } fun checkViewIsNotEmpty(composeTestRule: ComposeTestRule) { - composeTestRule.onNodeWithTag(HOME_ITEMS).assertIsDisplayed() + composeTestRule.onNodeWithTag(HOME_ITEMS).assert( + SemanticsMatcher.expectValue(HasPrograms, true) + ) } fun checkLogInIsLaunched() { @@ -76,36 +62,6 @@ class MainRobot : BaseRobot() { composeTestRule.onNodeWithTag(HOME_ITEMS).assertIsDisplayed() } - fun openFilters() { - onView(withId(R.id.filterActionButton)).perform(click()) - } - - fun openProgramByPosition(composeTestRule: ComposeTestRule, position: Int) { - composeTestRule.onNodeWithTag(HOME_ITEMS) - .onChildAt(position) - .performClick() - } - - fun filterByPeriodToday() { - onView(withId(R.id.filter)).perform(click()) - onView(withId(R.id.filterLayout)) - onView(withId(R.id.today)).perform(click()) - } - - @OptIn(ExperimentalTestApi::class) - fun checkItemsInProgram( - composeTestRule: ComposeTestRule, - position: Int, - program: String, - items: String - ) { - composeTestRule.onNodeWithTag(HOME_ITEMS, useUnmergedTree = true) - .performScrollToIndex(position) - composeTestRule.onNodeWithTag(HOME_ITEM.format(position)) - .assert(hasText(program)) - .assert(hasText(items, substring = true)) - } - companion object { const val FRAGMENT_TRANSITION = 1500L const val LOGOUT_TRANSITION = 2000L diff --git a/app/src/androidTest/java/org/dhis2/usescases/main/MainTest.kt b/app/src/androidTest/java/org/dhis2/usescases/main/MainTest.kt index 3af157411e..db251e45a7 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/main/MainTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/main/MainTest.kt @@ -1,13 +1,12 @@ package org.dhis2.usescases.main -import androidx.compose.ui.test.junit4.createComposeRule import android.content.Intent +import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule -import org.dhis2.usescases.BaseTest import org.dhis2.common.filters.filterRobotCommon +import org.dhis2.usescases.BaseTest import org.dhis2.usescases.login.loginRobot -import org.dhis2.usescases.searchte.robot.filterRobot import org.dhis2.usescases.settings.settingsRobot import org.junit.Ignore import org.junit.Rule @@ -49,30 +48,6 @@ class MainTest : BaseTest() { } } - @Test - fun checkDateFilterSetInitialDateWhenOpenedAgain(){ - setupCredentials() - setDatePicker() - startActivity() - - homeRobot { - openFilters() - } - - filterRobotCommon { - openFilterAtPosition(0) - clickOnFromToDateOption() - selectDate(2020,6,15) - acceptDateSelected() - selectDate(2020,11,7) - acceptDateSelected() - clickOnFromToDateOption() - checkDate(2020,6,15) - acceptDateSelected() - checkDate(2020,11,7) - } - } - @Ignore @Test fun shouldShowDialogToDeleteAccount() { @@ -89,7 +64,7 @@ class MainTest : BaseTest() { clickOnAcceptDialog() } - loginRobot { + loginRobot(composeTestRule) { checkUsernameFieldIsClear() checkPasswordFieldIsClear() } diff --git a/app/src/androidTest/java/org/dhis2/usescases/main/program/ProgramUiTest.kt b/app/src/androidTest/java/org/dhis2/usescases/main/program/ProgramUiTest.kt deleted file mode 100644 index 580871fb50..0000000000 --- a/app/src/androidTest/java/org/dhis2/usescases/main/program/ProgramUiTest.kt +++ /dev/null @@ -1,104 +0,0 @@ -package org.dhis2.usescases.main.program - -import android.content.Context -import android.graphics.Color -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.test.platform.app.InstrumentationRegistry -import org.dhis2.R -import org.dhis2.ui.MetadataIconData -import org.dhis2.ui.toColor -import org.hisp.dhis.android.core.common.State -import org.hisp.dhis.mobile.ui.designsystem.component.internal.ImageCardData -import org.junit.Rule -import org.junit.Test - -class ProgramUiTest { - - @get:Rule - val composeTestRule = createComposeRule() - - val context: Context = InstrumentationRegistry.getInstrumentation().targetContext - - @Test - fun shouldShowProgramDescriptionIcon() { - //Given a program with description - initProgramItem(programViewModel = provideFakeProgramViewModel()) - - //Then description icon is visible - val programDescription = getString(R.string.program_description) - composeTestRule.onNodeWithContentDescription(programDescription).assertIsDisplayed() - } - - @Test - fun shouldNotShowProgramDescriptionIcon() { - //Given a program without description - initProgramItem( - programViewModel = provideFakeProgramViewModel().copy(description = null) - ) - - //Then description icon is not visible - val programDescription = getString(R.string.program_description) - composeTestRule.onNodeWithContentDescription(programDescription).assertDoesNotExist() - } - - @Test - fun shouldShowDescriptionDialog() { - //Given a program with description - initProgramItem(programViewModel = provideFakeProgramViewModel()) - - //When user taps on description icon - val programDescription = getString(R.string.program_description) - composeTestRule.onNodeWithContentDescription(programDescription).performClick() - - //Then dialog is shown - composeTestRule.onNodeWithText(getString(R.string.info)).assertIsDisplayed() - } - - @Test - fun shouldDismissDescriptionDialogWhenTapsOnClose() { - //Given program description dialog is shown - initProgramItem(programViewModel = provideFakeProgramViewModel()) - val programDescription = getString(R.string.program_description) - composeTestRule.onNodeWithContentDescription(programDescription).performClick() - - //When user taps on close - composeTestRule.onNodeWithText(getString(R.string.action_close).uppercase()).performClick() - - //Then dialog is closed - composeTestRule.onNodeWithText(getString(R.string.info)).assertDoesNotExist() - } - - private fun getString(stringResource: Int) = context.resources.getString(stringResource) - - private fun initProgramItem(programViewModel: ProgramViewModel) { - composeTestRule.setContent { - ProgramItem(programViewModel = programViewModel) - } - } - - private fun provideFakeProgramViewModel() = - ProgramViewModel( - uid = "qweqwe", - title = "Program title", - MetadataIconData( - imageCardData = ImageCardData.IconCardData("", "", "ic_info", "#00BCD4".toColor()), - color = "#00BCD4".toColor(), - ), - count = 12, - type = "type", - typeName = "Persons", - programType = "WITH_REGISTRATION", - description = "Program description", - onlyEnrollOnce = false, - accessDataWrite = true, - state = State.SYNCED, - hasOverdueEvent = true, - false, - downloadState = ProgramDownloadState.NONE, - stockConfig = null - ) -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/orgunitselector/OrgUnitSelectorRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/orgunitselector/OrgUnitSelectorRobot.kt index 0bf70d5177..264acb7818 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/orgunitselector/OrgUnitSelectorRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/orgunitselector/OrgUnitSelectorRobot.kt @@ -1,12 +1,12 @@ package org.dhis2.usescases.orgunitselector +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import org.dhis2.common.BaseRobot -import org.dhis2.ui.dialogs.orgunit.DONE_TEST_TAG -import org.dhis2.ui.dialogs.orgunit.ITEM_CHECK_TEST_TAG fun orgUnitSelectorRobot( composeTestRule: ComposeTestRule, @@ -19,9 +19,11 @@ fun orgUnitSelectorRobot( class OrgUnitSelectorRobot(private val composeTestRule: ComposeTestRule) : BaseRobot() { fun selectTreeOrgUnit(orgUnitName: String) { - composeTestRule.onNodeWithTag("$ITEM_CHECK_TEST_TAG$orgUnitName") + composeTestRule.onNodeWithTag("ORG_TREE_ITEM_$orgUnitName") .performScrollTo() .performClick() - composeTestRule.onNodeWithTag(DONE_TEST_TAG).performClick() + composeTestRule.waitForIdle() + composeTestRule.onNodeWithText("Done").assertIsDisplayed() + composeTestRule.onNodeWithText("Done").performClick() } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt index 828d499de2..b582ab4314 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt @@ -7,11 +7,9 @@ import androidx.test.core.app.ApplicationProvider import org.dhis2.AppTest.Companion.DB_TO_IMPORT import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest -import org.dhis2.usescases.orgunitselector.orgUnitSelectorRobot import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity import org.dhis2.usescases.programevent.robot.programEventsRobot import org.dhis2.usescases.teidashboard.robot.eventRobot -import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -31,13 +29,8 @@ class ProgramEventTest : BaseTest() { return arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) } - @Before - override fun setUp() { - super.setUp() - enableComposeForms() - } - @Test + @Ignore("Flaky test, will be looked up in ANDROAPP-6476") fun shouldCreateNewEventAndCompleteIt() { prepareProgramAndLaunchActivity(antenatalCare) @@ -74,7 +67,6 @@ class ProgramEventTest : BaseTest() { } } - @Ignore("Flaky test, will be look om issue ANDROAPP-6030") @Test fun shouldCompleteAnEventAndReopenIt() { val eventDate = "15/03/2020" @@ -126,7 +118,6 @@ class ProgramEventTest : BaseTest() { @Test fun shouldOpenEventAndShowMap() { - prepareProgramAndLaunchActivity(informationCampaign) programEventsRobot(composeTestRule) { diff --git a/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt index d987222258..83eed903fc 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/robot/ProgramEventsRobot.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.test.hasAnyDescendant import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.espresso.Espresso.onView @@ -38,7 +39,7 @@ class ProgramEventsRobot(val composeTestRule: ComposeContentTestRule) : BaseRobo } fun clickOnMap() { - onView(withId(R.id.navigation_map_view)).perform(click()) + composeTestRule.onNodeWithTag("NAVIGATION_BAR_ITEM_Map").performClick() } @OptIn(ExperimentalTestApi::class) @@ -58,9 +59,11 @@ class ProgramEventsRobot(val composeTestRule: ComposeContentTestRule) : BaseRobo ).assertIsDisplayed() } + @OptIn(ExperimentalTestApi::class) fun checkEventIsComplete(eventDate: String) { - composeTestRule.onNodeWithText(eventDate).assertIsDisplayed() - composeTestRule.onNodeWithText("Event completed").assertIsDisplayed() + composeTestRule.waitUntilAtLeastOneExists(hasText("Event completed", true), 2000) + composeTestRule.onNodeWithText(eventDate,true).assertIsDisplayed() + composeTestRule.onNodeWithText("Event completed",true).assertIsDisplayed() } fun checkEventWasDeleted(eventDate: String) { @@ -68,8 +71,9 @@ class ProgramEventsRobot(val composeTestRule: ComposeContentTestRule) : BaseRobo } fun checkMapIsDisplayed() { - onView(withId(R.id.mapView)).check(matches(isDisplayed())) - onView(withId(R.id.map_carousel)).check(matches(isDisplayed())) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("MAP", true).assertIsDisplayed() + composeTestRule.onNodeWithTag("MAP_CAROUSEL",true).assertIsDisplayed() } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt index a02f53d928..e4f8f693e1 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/SearchTETest.kt @@ -3,23 +3,15 @@ package org.dhis2.usescases.searchte import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.intl.Locale -import androidx.test.espresso.IdlingRegistry -import androidx.test.espresso.IdlingResourceTimeoutException -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import androidx.test.uiautomator.By -import androidx.test.uiautomator.UiDevice -import androidx.test.uiautomator.Until import dispatch.android.espresso.IdlingDispatcherProvider import dispatch.android.espresso.IdlingDispatcherProviderRule import org.dhis2.R import org.dhis2.bindings.app -import org.dhis2.common.idlingresources.MapIdlingResource import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_EVENTS_PATH import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_EVENTS_RESPONSE import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_PATH import org.dhis2.common.mockwebserver.MockWebServerRobot.Companion.API_OLD_TRACKED_ENTITY_RESPONSE -import org.dhis2.commons.date.DateUtils.SIMPLE_DATE_FORMAT +import org.dhis2.commons.resources.SIMPLE_DATE_FORMAT import org.dhis2.lazyActivityScenarioRule import org.dhis2.usescases.BaseTest import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel @@ -31,22 +23,17 @@ import org.dhis2.usescases.searchte.robot.filterRobot import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot import org.hisp.dhis.android.core.mockwebserver.ResponseController -import org.junit.After import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith import java.text.SimpleDateFormat import java.util.Date -@RunWith(AndroidJUnit4::class) class SearchTETest : BaseTest() { @get:Rule val rule = lazyActivityScenarioRule(launchActivity = false) - private var mapIdlingResource: MapIdlingResource? = null - private val customDispatcherProvider = context.applicationContext.app().appComponent().customDispatcherProvider() @@ -64,6 +51,7 @@ class SearchTETest : BaseTest() { setupMockServer() } + @Ignore("Test needs to be fixed in ANDROAPP-6459") @Test fun shouldSuccessfullySearchByName() { mockWebServerRobot.addResponse( @@ -81,9 +69,7 @@ class SearchTETest : BaseTest() { clickOnOpenSearch() openNextSearchParameter("First name") typeOnNextSearchTextParameter(firstName) - waitToDebounce(1000) clickOnSearch() - composeTestRule.waitForIdle() checkListOfSearchTEI( title = "First name: $firstName", attributes = mapOf("Last name" to lastName), @@ -183,7 +169,7 @@ class SearchTETest : BaseTest() { } @Test - fun shouldSuccessfullyFilterByEnrollmentStatusActive() { + fun shouldSuccessfullyFilterByEnrollmentStatusCompleted() { val enrollmentStatusFilter = context.getString(R.string.filters_title_enrollment_status) .format( context.resources.getQuantityString(R.plurals.enrollment, 1) @@ -194,20 +180,18 @@ class SearchTETest : BaseTest() { prepareChildProgrammeIntentAndLaunchActivity(rule) - filterRobot { + filterRobot(composeTestRule) { clickOnFilter() clickOnFilterBy(enrollmentStatusFilter) - clickOnFilterActiveOption() + clickOnFilterCompletedOption() clickOnSortByField(enrollmentStatusFilter) checkFilterCounter(totalFilterCount) checkCountAtFilter(enrollmentStatusFilter, filterCount) clickOnFilter() - waitToDebounce(2000) - checkTEIsAreOpen() + checkTeiAreCompleted() } } - @Ignore("Test needs to be fixed in ANDROAPP-6340") @Test fun shouldSuccessfullyFilterByEventStatusOverdue() { mockWebServerRobot.addResponse( @@ -220,7 +204,6 @@ class SearchTETest : BaseTest() { API_OLD_EVENTS_PATH, API_OLD_EVENTS_RESPONSE, ) - enableComposeForms() val eventStatusFilter = context.getString(R.string.filters_title_event_status) val totalCount = "1" val registerTeiDetails = createRegisterTEI() @@ -235,10 +218,11 @@ class SearchTETest : BaseTest() { teiFlowRobot(composeTestRule) { registerTEI(registerTeiDetails) changeDueDate(scheduledEventTitle) + composeTestRule.waitForIdle() pressBack() } composeTestRule.waitForIdle() - filterRobot { + filterRobot(composeTestRule) { clickOnFilter() clickOnFilterBy(eventStatusFilter) clickOnFilterOverdueOption() @@ -252,6 +236,7 @@ class SearchTETest : BaseTest() { } @Test + @Ignore("Test not checking nothing, try to create integration test") fun shouldSuccessfullyFilterByOrgUnitAndUseSort() { val orgUnitFilter = "ORG. UNIT" val orgUnitNgelehun = "Ngelehun CHC" @@ -259,7 +244,7 @@ class SearchTETest : BaseTest() { val filterCount = "1" prepareChildProgrammeIntentAndLaunchActivity(rule) - filterRobot { + filterRobot(composeTestRule) { clickOnFilter() clickOnFilterBy(orgUnitFilter) clickOnSortByField(orgUnitFilter) @@ -271,20 +256,19 @@ class SearchTETest : BaseTest() { } } + @Ignore("Flaky test, will be looked up in ANDROAPP-6541") @Test fun shouldSuccessfullyFilterByEnrollmentDateAndSort() { val enrollmentDate = "DATE OF ENROLLMENT" val enrollmentDateFrom = createFromEnrollmentDate() val enrollmentDateTo = createToEnrollmentDate() - val startDate = "2021-05-01" - val endDate = "2021-05-31" val totalFilterCount = "2" val filterCount = "1" setDatePicker() prepareChildProgrammeIntentAndLaunchActivity(rule) - filterRobot { + filterRobot(composeTestRule) { clickOnFilter() clickOnFilterBy(enrollmentDate) clickOnFromToDate() @@ -294,24 +278,32 @@ class SearchTETest : BaseTest() { checkFilterCounter(totalFilterCount) checkCountAtFilter(enrollmentDate, filterCount) clickOnFilter() - checkDateIsInRange(startDate, endDate) + } + searchTeiRobot(composeTestRule) { + clickOnTEI("Alan") + } + + teiDashboardRobot(composeTestRule) { + composeTestRule.waitForIdle() + checkEnrollmentDate(enrollmentDateFrom) } } + @Ignore("Flaky test, will be looked up in ANDROAPP-6545") @Test fun shouldSuccessfullyFilterByEventDateAndSort() { val eventDate = context.getString(R.string.filters_title_event_date) val eventDateFrom = createFromEventDate() val eventDateTo = createToEventDate() - val startDate = "2020-05-01" - val endDate = "2020-05-31" val totalCount = "2" val filterCount = "1" + val name = "Heather" + val lastName = "Greene" setDatePicker() prepareChildProgrammeIntentAndLaunchActivity(rule) - filterRobot { + filterRobot(composeTestRule) { clickOnFilter() clickOnFilterBy(eventDate) clickOnFromToDate() @@ -321,7 +313,13 @@ class SearchTETest : BaseTest() { checkFilterCounter(totalCount) checkCountAtFilter(eventDate, filterCount) clickOnFilter() - checkDateIsInRange(startDate, endDate) + } + + searchTeiRobot(composeTestRule) { + checkListOfSearchTEI( + title = "First name: $name", + attributes = mapOf("Last name" to lastName), + ) } } @@ -347,7 +345,7 @@ class SearchTETest : BaseTest() { openNextSearchParameter("Last name") typeOnNextSearchTextParameter(teiLastName) clickOnSearch() - clickOnTEI(teiName, composeTestRule) + clickOnTEI(teiName) } teiDashboardRobot(composeTestRule) { @@ -356,7 +354,7 @@ class SearchTETest : BaseTest() { pressBack() } - filterRobot { + filterRobot(composeTestRule) { clickOnFilter() clickOnFilterBy(syncFilter) clickOnNotSync() @@ -395,10 +393,9 @@ class SearchTETest : BaseTest() { waitToDebounce(2000) clickOnSearch() composeTestRule.waitForIdle() - } - filterRobot { + filterRobot(composeTestRule) { clickOnFilter() clickOnFilterBy(enrollmentStatus) clickOnFilterActiveOption() @@ -411,7 +408,7 @@ class SearchTETest : BaseTest() { searchTeiRobot(composeTestRule) { checkListOfSearchTEI( title = "First name: $name", - attributes = mapOf("Last name" to lastName) + attributes = mapOf("Last name" to lastName), ) } } @@ -424,20 +421,7 @@ class SearchTETest : BaseTest() { searchTeiRobot(composeTestRule) { clickOnShowMap() - try { - val device = UiDevice.getInstance(getInstrumentation()) - device.wait(Until.hasObject(By.desc(MAP_LOADED)), 6000) - checkCarouselTEICardInfo(firstName) - } catch (ex: IdlingResourceTimeoutException) { - throw RuntimeException("Could not start test") - } - } - } - - @After - fun unregisterIdlingResource() { - if (mapIdlingResource != null) { - IdlingRegistry.getInstance().unregister(mapIdlingResource) + checkCarouselTEICardInfo(firstName) } } @@ -448,50 +432,50 @@ class SearchTETest : BaseTest() { "sarah@gmail.com", "Main street 1", "56", - "167" + "167", ) private fun createFromEnrollmentDate() = DateRegistrationUIModel( 2021, 5, - 1 + 1, ) private fun createToEnrollmentDate() = DateRegistrationUIModel( 2021, 5, - 31 + 31, ) private fun createFromEventDate() = DateRegistrationUIModel( 2020, 5, - 1 + 1, ) private fun createToEventDate() = DateRegistrationUIModel( 2020, 5, - 31 + 31, ) private fun createRegisterTEI() = RegisterTEIUIModel( "ADRIANNA", "ROBERTS", dateRegistration, - dateEnrollment + dateEnrollment, ) private fun createFirstSpecificDate() = DateRegistrationUIModel( 2000, 6, - 30 + 30, ) private fun createEnrollmentDate() = DateRegistrationUIModel( 2020, - 10, - 30 + 9, + 30, ) private val dateRegistration = createFirstSpecificDate() diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/FilterRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/FilterRobot.kt index 7ba5e7de9b..66106cf968 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/FilterRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/FilterRobot.kt @@ -1,5 +1,10 @@ package org.dhis2.usescases.searchte.robot +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.TypeTextAction import androidx.test.espresso.action.ViewActions.click @@ -15,7 +20,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import org.dhis2.R import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.allElementsWithHolderTypeHave -import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.dateIsInRange import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasItem import org.dhis2.common.viewactions.clickChildViewWithId import org.dhis2.common.viewactions.scrollToBottomRecyclerView @@ -23,13 +27,16 @@ import org.dhis2.commons.filters.FilterHolder import org.dhis2.usescases.searchTrackEntity.adapters.SearchTEViewHolder import org.hamcrest.CoreMatchers.allOf -fun filterRobot(filterRobot: FilterRobot.() -> Unit) { - FilterRobot().apply { +fun filterRobot( + composeTestRule: ComposeTestRule, + filterRobot: FilterRobot.() -> Unit +) { + FilterRobot(composeTestRule).apply { filterRobot() } } -class FilterRobot : BaseRobot() { +class FilterRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun clickOnFilter() { onView(withId(R.id.search_filter_general)).perform(click()) @@ -45,6 +52,10 @@ class FilterRobot : BaseRobot() { onView(withId(R.id.stateActive)).perform(click()) } + fun clickOnFilterCompletedOption() { + onView(withId(R.id.stateEnrollmentCompleted)).perform(click()) + } + fun clickOnFilterOverdueOption() { onView(withId(R.id.filterRecyclerLayout)) .perform(scrollToBottomRecyclerView()) @@ -81,9 +92,8 @@ class FilterRobot : BaseRobot() { onView(withId(R.id.acceptBtn)).perform(click()) } - fun checkTEIsAreOpen() { - onView(withId(R.id.scrollView)) - .check(matches(allElementsWithHolderTypeHave(SearchTEViewHolder::class.java,hasDescendant(withText(R.string.event_open))))) + fun checkEventsAreOverdue() { + composeTestRule.onAllNodesWithText("overdue", substring = true, useUnmergedTree = true).assertCountEquals(4) } fun checkTEIWithOrgUnit(orgUnit: String) { @@ -92,16 +102,11 @@ class FilterRobot : BaseRobot() { } fun checkTEINotSync() { - onView(withId(R.id.scrollView)) - .check(matches(allElementsWithHolderTypeHave(SearchTEViewHolder::class.java,hasDescendant(withId(R.id.syncState))))) - } - - fun checkDateIsInRange(startDate: String, endDate: String) { - onView(withId(R.id.scrollView)) - .check(matches(dateIsInRange(R.id.sorting_field_value, startDate, endDate))) + composeTestRule.onNodeWithText("Sync", useUnmergedTree = true).assertIsDisplayed() } fun checkFilterCounter(filterCount: String) { + waitForView(withId(R.id.filterCounter)) onView(allOf(withId(R.id.filterCounter), isDisplayed(), withParent(withId(R.id.mainToolbar)))) .check(matches(withChild(withText(filterCount)))) } @@ -110,4 +115,8 @@ class FilterRobot : BaseRobot() { onView(withId(R.id.filterRecyclerLayout)) .check(matches(hasItem(allOf(hasDescendant(withText(filter)), hasDescendant(withText(count)))))) } + + fun checkTeiAreCompleted() { + composeTestRule.onAllNodesWithText("Enrollment completed", true).assertCountEquals(4) + } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt index afa3452ffc..acd570f39f 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/searchte/robot/SearchTeiRobot.kt @@ -2,12 +2,15 @@ package org.dhis2.usescases.searchte.robot import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasAnySibling import androidx.compose.ui.test.hasParent import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onLast +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -26,8 +29,6 @@ import org.dhis2.common.matchers.RecyclerviewMatchers import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasItem import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasNoMoreResultsInProgram import org.dhis2.common.viewactions.openSpinnerPopup -import org.dhis2.common.viewactions.typeChildViewWithId -import org.dhis2.usescases.searchTrackEntity.adapters.SearchTEViewHolder import org.dhis2.usescases.searchTrackEntity.listView.SearchResult import org.dhis2.usescases.searchte.entity.DisplayListFieldsUIModel import org.hamcrest.CoreMatchers.allOf @@ -46,7 +47,7 @@ fun searchTeiRobot( class SearchTeiRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { - fun clickOnTEI( teiName: String,composeTestRule: ComposeTestRule) { + fun clickOnTEI(teiName: String) { composeTestRule.waitForIdle() composeTestRule.onNodeWithText("First name: $teiName", true).performClick() composeTestRule.waitForIdle() @@ -85,25 +86,6 @@ class SearchTeiRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { } } - fun typeAttributeAtPosition(searchWord: String, position: Int) { - onView(withId(R.id.recyclerView)) - .perform( - actionOnItemAtPosition( - position, - click() - ) - ) - - onView(withId(R.id.recyclerView)) - .perform( - actionOnItemAtPosition( - position, - typeChildViewWithId(searchWord, R.id.input_editText) - ) - ) - closeKeyboard() - } - fun clickOnSearch() { closeKeyboard() composeTestRule.onNodeWithTag("SEARCH_BUTTON").performClick() @@ -151,7 +133,7 @@ class SearchTeiRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { onView(withId(R.id.spinner_text)).check(matches(withText(program))) } - + @OptIn(ExperimentalTestApi::class) fun checkFieldsFromDisplayList( displayListFieldsUIModel: DisplayListFieldsUIModel ) { @@ -160,9 +142,10 @@ class SearchTeiRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { val displayedAttributes = createAttributesList(displayListFieldsUIModel) val showMoreText = InstrumentationRegistry.getInstrumentation() .targetContext.getString(R.string.show_more) - //When we expand all attribute list - composeTestRule.onNodeWithText(showMoreText, useUnmergedTree = true).performClick() composeTestRule.waitForIdle() + composeTestRule.waitUntilAtLeastOneExists(hasText(showMoreText)) + //When we expand all attribute list + composeTestRule.onNodeWithText(showMoreText, true, useUnmergedTree = true).performClick() //Then The title and all attributes are displayed composeTestRule.onNodeWithText(title).assertIsDisplayed() displayedAttributes.forEach { item -> @@ -175,12 +158,17 @@ class SearchTeiRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { } fun clickOnShowMap() { - onView(withId(R.id.navigation_map_view)).perform(click()) + composeTestRule.onNodeWithTag("NAVIGATION_BAR_ITEM_Map").performClick() } fun checkCarouselTEICardInfo(firstName: String) { - onView(withId(R.id.map_carousel)) - .check(matches(hasItem(hasDescendant(withText(firstName))))) + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("MAP_CAROUSEL", true) + .assertIsDisplayed() + composeTestRule.onNode( + hasParent(hasTestTag("LIST_CARD_ADDITIONAL_INFO_COLUMN")) + and hasText(firstName, true), useUnmergedTree = true + ).assertIsDisplayed() } fun clickOnOpenSearch() { @@ -193,11 +181,7 @@ class SearchTeiRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun checkListOfSearchTEIWithAdditionalInfo(title: String, additionalText: String) { composeTestRule.onNodeWithText(title).assertIsDisplayed() - composeTestRule.onNode( - hasParent(hasTestTag("LIST_CARD_ADDITIONAL_INFO_COLUMN")) - and hasText(additionalText, true), - useUnmergedTree = true, - ).assertIsDisplayed() + composeTestRule.onNodeWithContentDescription(additionalText).assertIsDisplayed() } private fun createAttributesList(displayListFieldsUIModel: DisplayListFieldsUIModel) = listOf( diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt index 5a221799fb..d0df3188aa 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardMobileActivityTest.kt @@ -21,6 +21,7 @@ import org.dhis2.usescases.teiDashboard.TeiAttributesProvider import org.dhis2.usescases.teiDashboard.TeiDashboardContracts import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity import org.dhis2.utils.analytics.AnalyticsHelper +import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.TrackedEntityType @@ -78,6 +79,7 @@ class TeiDashboardMobileActivityTest { private val presenter: TeiDashboardContracts.Presenter = mock() private val filterManager: FilterManager = mock() private val networkUtils: NetworkUtils = mock() + private val pageConfigurator: NavigationPageConfigurator = mock() companion object { const val ENROLLMENT_UID = "enrollmentUid" @@ -98,6 +100,8 @@ class TeiDashboardMobileActivityTest { repository, analyticsHelper, dispatcher, + pageConfigurator = pageConfigurator, + resourcesManager = resources, ) } diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt index b4a9a81a75..37cb7f7cb5 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt @@ -148,7 +148,6 @@ class TeiDashboardTest : BaseTest() { clickOnFab() clickOnReferral() clickOnFirstReferralEvent() - clickOnReferralOption(context.getString(R.string.one_time)) clickOnReferralNextButton() checkEventWasCreated(LAB_MONITORING) } @@ -184,7 +183,6 @@ class TeiDashboardTest : BaseTest() { } eventRobot(composeTestRule) { - scrollToBottomForm() clickOnFormFabButton() clickOnNotNow() } @@ -242,7 +240,7 @@ class TeiDashboardTest : BaseTest() { eventRobot(composeTestRule) { waitToDebounce(600) - fillRadioButtonForm(4) +// fillRadioButtonForm(4) clickOnFormFabButton() clickOnCompleteButton() waitToDebounce(600) @@ -257,7 +255,7 @@ class TeiDashboardTest : BaseTest() { fun shouldEnrollToOtherProgramWhenClickOnProgramEnrollments() { val womanProgram = "MNCH / PNC (Adult Woman)" val personAttribute = - context.getString(R.string.enrollment_single_section_label).replace("%s", "") + context.getString(R.string.enrollment_single_section_label).replace("%s", "Person") val visitPNCEvent = "PNC Visit" val deliveryEvent = "Delivery" val visitANCEvent = "ANC Visit (2-4+)" @@ -276,11 +274,8 @@ class TeiDashboardTest : BaseTest() { enrollmentRobot(composeTestRule) { clickOnAProgramForEnrollment(composeTestRule, womanProgram) clickOnAcceptInDatePicker() - clickOnPersonAttributes(personAttribute) - waitToDebounce(5000) - clickOnCalendarItem() - clickOnAcceptInDatePicker() - scrollToBottomProgramForm() + openFormSection(personAttribute) + typeOnInputDateField("01012000", "Date of birth") clickOnSaveEnrollment() } @@ -324,8 +319,8 @@ class TeiDashboardTest : BaseTest() { private fun createExpectedEnrollmentInformation() = EnrollmentUIModel( - "10/1/2021", - "10/1/2021", + "10/01/2021", + "10/01/2021", "Ngelehun CHC", "40.48713205295354", "-3.6847423830882633", diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTestNoComposable.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTestNoComposable.kt index f8b1156681..e3523fc3fb 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTestNoComposable.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTestNoComposable.kt @@ -8,7 +8,6 @@ import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.dhis2.usescases.searchte.robot.searchTeiRobot import org.dhis2.usescases.teidashboard.robot.relationshipRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -22,20 +21,19 @@ class TeiDashboardTestNoComposable : BaseTest() { @get:Rule val composeTestRule = createComposeRule() - @Ignore @Test fun shouldSuccessfullyCreateRelationshipWhenClickAdd() { val teiName = "Tim" val teiLastName = "Johnson" val relationshipName = "Filona" val relationshipLastName = "Ryder" - val completeName = "Ryder Filona" + val completeName = "Filona Ryder" setupCredentials() prepareChildProgrammeIntentAndLaunchActivity(ruleSearch) searchTeiRobot(composeTestRule) { - clickOnTEI(teiName, composeTestRule) + clickOnTEI(teiName) } teiDashboardRobot(composeTestRule) { @@ -51,15 +49,12 @@ class TeiDashboardTestNoComposable : BaseTest() { searchTeiRobot(composeTestRule) { clickOnOpenSearch() - typeAttributeAtPosition(relationshipName, 0) - typeAttributeAtPosition(relationshipLastName, 1) + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(relationshipName) + openNextSearchParameter("Last name") + typeOnNextSearchTextParameter(relationshipLastName) clickOnSearch() - waitToDebounce(5000) - clickOnTEI(relationshipName, composeTestRule) - } - - relationshipRobot { - checkRelationshipWasCreated(0, completeName) + clickOnTEI(relationshipName) } } @@ -67,24 +62,24 @@ class TeiDashboardTestNoComposable : BaseTest() { fun shouldDeleteTeiSuccessfully() { val teiName = "Gertrude" val teiLastName = "Fjordsen" - val firstNamePosition = 0 - val lastNamePosition = 1 setupCredentials() prepareChildProgrammeIntentAndLaunchActivity(ruleSearch) searchTeiRobot(composeTestRule) { clickOnOpenSearch() - typeAttributeAtPosition(teiName, firstNamePosition) - typeAttributeAtPosition(teiLastName, lastNamePosition) + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(teiName) + openNextSearchParameter("Last name") + typeOnNextSearchTextParameter(teiLastName) clickOnSearch() - clickOnTEI(teiName, composeTestRule) - //scrollToTEIandClick() + clickOnTEI(teiName) } teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnMenuDeleteTEI() + clickOnConfirmDeleteTEI() } searchTeiRobot(composeTestRule) { @@ -96,24 +91,24 @@ class TeiDashboardTestNoComposable : BaseTest() { fun shouldDeleteEnrollmentSuccessfully() { val teiName = "Anna" val teiLastName = "Jones" - val firstNamePosition = 0 - val lastNamePosition = 1 setupCredentials() prepareChildProgrammeIntentAndLaunchActivity(ruleSearch) searchTeiRobot(composeTestRule) { clickOnOpenSearch() - typeAttributeAtPosition(teiName, firstNamePosition) - typeAttributeAtPosition(teiLastName, lastNamePosition) + openNextSearchParameter("First name") + typeOnNextSearchTextParameter(teiName) + openNextSearchParameter("Last name") + typeOnNextSearchTextParameter(teiLastName) clickOnSearch() - // waitToDebounce(400) - clickOnTEI(teiName, composeTestRule) + clickOnTEI(teiName) } teiDashboardRobot(composeTestRule) { clickOnMenuMoreOptions() clickOnMenuDeleteEnrollment() + clickOnConfirmDeleteEnrollment() } searchTeiRobot(composeTestRule) { diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt index 877eed683f..5308b00ed6 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/dialogs/scheduling/SchedulingDialogUiTest.kt @@ -1,20 +1,26 @@ package org.dhis2.usescases.teidashboard.dialogs.scheduling +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import kotlinx.coroutines.flow.MutableStateFlow +import org.dhis2.commons.data.EventCreationType import org.dhis2.composetable.test.TestActivity import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialogUi import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingViewModel import org.hisp.dhis.android.core.category.CategoryOption +import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.program.ProgramStage import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mockito.Mockito.mock @@ -27,6 +33,7 @@ class SchedulingDialogUiTest { val composeTestRule = createAndroidComposeRule() private val viewModel: SchedulingViewModel = mock() + private val enrollment = Enrollment.builder().uid("enrollmentUid").build() @Before fun setUp() { @@ -55,15 +62,24 @@ class SchedulingDialogUiTest { val programStages = listOf(ProgramStage.builder().uid("stageUid").displayName("PS A").build()) whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) + whenever(viewModel.programStages).thenReturn(MutableStateFlow(programStages)) + whenever(viewModel.enrollment).thenReturn(MutableStateFlow(enrollment)) + composeTestRule.setContent { SchedulingDialogUi( - programStages = programStages, viewModel = viewModel, - orgUnitUid = "orgUnitUid", + launchMode = SchedulingDialog.LaunchMode.NewSchedule( + enrollmentUid = enrollment.uid(), + programStagesUids = programStages.map { it.uid() }, + showYesNoOptions = false, + eventCreationType = EventCreationType.SCHEDULE, + ) ) { } } - composeTestRule.onNodeWithText("Schedule next " + programStages.first().displayName() + "?") + + val eventLabel = programStages.first().displayEventLabel() ?: "event" + composeTestRule.onNodeWithText("Schedule next $eventLabel?") .assertExists() composeTestRule.onNodeWithText("Program stage").assertDoesNotExist() composeTestRule.onNodeWithText("Date").assertExists() @@ -78,11 +94,18 @@ class SchedulingDialogUiTest { ProgramStage.builder().uid("stageUidB").displayName("PS B").build(), ) whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) + whenever(viewModel.programStages).thenReturn(MutableStateFlow(programStages)) + whenever(viewModel.enrollment).thenReturn(MutableStateFlow(enrollment)) + composeTestRule.setContent { SchedulingDialogUi( - programStages = programStages, viewModel = viewModel, - orgUnitUid = "orgUnitUid", + launchMode = SchedulingDialog.LaunchMode.NewSchedule( + enrollmentUid = enrollment.uid(), + programStagesUids = programStages.map { it.uid() }, + showYesNoOptions = false, + eventCreationType = EventCreationType.SCHEDULE, + ) ) { } } @@ -97,11 +120,18 @@ class SchedulingDialogUiTest { ProgramStage.builder().uid("stageUidB").displayName("PS B").build(), ) whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) + whenever(viewModel.programStages).thenReturn(MutableStateFlow(programStages)) + whenever(viewModel.enrollment).thenReturn(MutableStateFlow(enrollment)) + composeTestRule.setContent { SchedulingDialogUi( - programStages = programStages, viewModel = viewModel, - orgUnitUid = "orgUnitUid", + launchMode = SchedulingDialog.LaunchMode.NewSchedule( + enrollmentUid = enrollment.uid(), + programStagesUids = programStages.map { it.uid() }, + showYesNoOptions = true, + eventCreationType = EventCreationType.SCHEDULE, + ) ) { } } @@ -113,7 +143,7 @@ class SchedulingDialogUiTest { composeTestRule.onNodeWithText("Done").assertExists() } - @Ignore("Not working") + @OptIn(ExperimentalTestApi::class) @Test fun selectProgramStage() { val programStages = listOf( @@ -121,16 +151,24 @@ class SchedulingDialogUiTest { ProgramStage.builder().uid("stageUidB").displayName("PS B").build(), ) whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) + whenever(viewModel.programStages).thenReturn(MutableStateFlow(programStages)) + whenever(viewModel.enrollment).thenReturn(MutableStateFlow(enrollment)) + composeTestRule.setContent { SchedulingDialogUi( - programStages = programStages, viewModel = viewModel, - orgUnitUid = "orgUnitUid", + launchMode = SchedulingDialog.LaunchMode.NewSchedule( + enrollmentUid = enrollment.uid(), + programStagesUids = programStages.map { it.uid() }, + showYesNoOptions = false, + eventCreationType = EventCreationType.SCHEDULE, + ) ) { } } - composeTestRule.onNodeWithText("Program stage").performClick() + composeTestRule.onAllNodesWithTag("INPUT_DROPDOWN").onFirst().performClick() + composeTestRule.waitUntilExactlyOneExists(hasTestTag("INPUT_DROPDOWN_MENU_ITEM_1")) composeTestRule.waitForIdle() composeTestRule.onNodeWithTag( testTag = "INPUT_DROPDOWN_MENU_ITEM_1", @@ -139,4 +177,30 @@ class SchedulingDialogUiTest { verify(viewModel).updateStage(programStages[1]) } -} \ No newline at end of file + + @Test + fun yesNoFieldsShouldNotBeShownWhenTurnedOff() { + val programStages = listOf( + ProgramStage.builder().uid("stageUidA").displayName("PS A").build(), + ProgramStage.builder().uid("stageUidB").displayName("PS B").build(), + ) + whenever(viewModel.programStage).thenReturn(MutableStateFlow(programStages.first())) + whenever(viewModel.programStages).thenReturn(MutableStateFlow(programStages)) + whenever(viewModel.enrollment).thenReturn(MutableStateFlow(enrollment)) + + composeTestRule.setContent { + SchedulingDialogUi( + viewModel = viewModel, + launchMode = SchedulingDialog.LaunchMode.NewSchedule( + enrollmentUid = enrollment.uid(), + programStagesUids = programStages.map { it.uid() }, + showYesNoOptions = false, + eventCreationType = EventCreationType.SCHEDULE, + ) + ) { + } + } + + composeTestRule.onNodeWithTag("YES_NO_OPTIONS").assertDoesNotExist() + } +} diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt index e21db95929..546e4e0401 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EnrollmentRobot.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextReplacement import androidx.test.espresso.Espresso.onView @@ -18,14 +19,10 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import org.dhis2.R import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.atPosition -import org.dhis2.common.viewactions.clickChildViewWithId -import org.dhis2.common.viewactions.scrollToBottomRecyclerView -import org.dhis2.form.ui.FormViewHolder import org.dhis2.usescases.flow.teiFlow.entity.EnrollmentListUIModel import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder import org.dhis2.usescases.teiDashboard.teiProgramList.ui.PROGRAM_TO_ENROLL import org.hamcrest.CoreMatchers.allOf -import org.hamcrest.CoreMatchers.containsString fun enrollmentRobot( composeTestRule: ComposeTestRule, @@ -52,30 +49,6 @@ class EnrollmentRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { onView(withId(R.id.save)).perform(click()) } - fun clickOnPersonAttributes(attribute: String) { - onView(withId(R.id.recyclerView)) - .perform( - actionOnItem( - hasDescendant(withText(containsString(attribute))), - clickChildViewWithId(R.id.section_details) - ) - ) - } - - fun scrollToBottomProgramForm() { - onView(withId(R.id.recyclerView)).perform(scrollToBottomRecyclerView()) - } - - fun clickOnCalendarItem() { - onView(withId(R.id.recyclerView)) - .perform( - actionOnItem( - hasDescendant(withText(containsString(DATE_OF_BIRTH))), - clickChildViewWithId(R.id.inputEditText) - ) - ) - } - fun checkActiveAndPastEnrollmentDetails(enrollmentListUIModel: EnrollmentListUIModel) { checkHeaderAndProgramDetails( enrollmentListUIModel, @@ -139,9 +112,25 @@ class EnrollmentRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { } } + fun openFormSection(personAttribute: String) { + composeTestRule.onNodeWithText(personAttribute).performClick() + } + + fun typeOnInputDateField(dateValue: String, title: String) { + composeTestRule.apply { + onNode( + hasTestTag( + "INPUT_DATE_TIME_TEXT_FIELD" + ) and hasAnySibling( + hasText(title) + ), + useUnmergedTree = true, + ).performTextReplacement(dateValue) + } + } + companion object { const val ACTIVE_PROGRAMS = "Active programs" const val PAST_PROGRAMS = "Past programs" - const val DATE_OF_BIRTH = "Date of birth" } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt index 03f7f02a67..2132350602 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/EventRobot.kt @@ -2,6 +2,7 @@ package org.dhis2.usescases.teidashboard.robot import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasAnySibling import androidx.compose.ui.test.hasTestTag @@ -16,19 +17,14 @@ import androidx.compose.ui.test.performTextReplacement import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry import org.dhis2.R import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.hasCompletedPercentage -import org.dhis2.common.viewactions.clickChildViewWithId -import org.dhis2.common.viewactions.scrollToBottomRecyclerView -import org.dhis2.common.viewactions.typeChildViewWithId -import org.dhis2.form.ui.FormViewHolder import org.dhis2.ui.dialogs.bottomsheet.MAIN_BUTTON_TAG import org.dhis2.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder fun eventRobot( composeTestRule: ComposeTestRule, @@ -41,10 +37,6 @@ fun eventRobot( class EventRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { - fun scrollToBottomForm() { - onView(withId(R.id.recyclerView)).perform(scrollToBottomRecyclerView()) - } - fun clickOnFormFabButton() { waitForView(withId(R.id.actionButton)).perform(click()) } @@ -65,40 +57,19 @@ class EventRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { composeTestRule.onNodeWithTag("REOPEN_BUTTON").performClick() } - fun fillRadioButtonForm(numberFields: Int) { - var formLength = 0 - - while (formLength < numberFields) { - onView(withId(R.id.recyclerView)) - .perform( - actionOnItemAtPosition( - formLength, - clickChildViewWithId(R.id.yes) - ) - ) - formLength++ - } - } - fun acceptUpdateEventDate() { composeTestRule.onNodeWithText("OK", true).performClick() } - fun typeOnRequiredEventForm(text: String, position: Int) { - onView(withId(R.id.recyclerView)) - .perform( - actionOnItemAtPosition( //EditTextCustomHolder - position, typeChildViewWithId(text, R.id.input_editText) - ) - ) - } - fun openMenuMoreOptions() { onView(withId(R.id.moreOptions)).perform(click()) } fun clickOnDelete() { - onView(withText(R.string.delete)).perform(click()) + with(InstrumentationRegistry.getInstrumentation().targetContext) { + val deleteLabel = getString(R.string.delete) + composeTestRule.onNodeWithText(deleteLabel).performClick() + } } fun clickOnDeleteDialog() { @@ -147,7 +118,7 @@ class EventRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { } fun checkEventIsOpen() { - composeTestRule.onNodeWithTag("REOPEN_BUTTON").assertDoesNotExist() + composeTestRule.onNodeWithTag("REOPEN_BUTTON").assertIsNotDisplayed() } private fun formatStoredDateToUI(dateValue: String): String { diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/RelationshipRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/RelationshipRobot.kt index b81be62860..4168a476ef 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/RelationshipRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/RelationshipRobot.kt @@ -2,18 +2,10 @@ package org.dhis2.usescases.teidashboard.robot import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withTagValue -import androidx.test.espresso.matcher.ViewMatchers.withText -import org.dhis2.R import org.dhis2.common.BaseRobot -import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.atPosition -import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.isNotEmpty import org.dhis2.utils.dialFloatingActionButton.FAB_ID -import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.equalTo fun relationshipRobot(relationshipRobot: RelationshipRobot.() -> Unit) { @@ -35,21 +27,6 @@ class RelationshipRobot : BaseRobot() { ).perform(click()) } - fun checkRelationshipWasCreated(position: Int, tei: String) { - onView(withId(R.id.relationship_recycler)) - .check(matches( - allOf( - isDisplayed(), isNotEmpty(), - atPosition( - position, allOf( - hasDescendant(withText(relationshipType)), - hasDescendant(withText(tei)) - ) - ) - ) - )) - } - companion object { const val relationshipType = "Mother-Child_a-to-b_(Person-Person)" } diff --git a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt index 8a6c68c2b8..d846ef17fd 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/robot/TeiDashboardRobot.kt @@ -1,8 +1,8 @@ package org.dhis2.usescases.teidashboard.robot import android.content.Context -import android.view.View import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnySibling import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onAllNodesWithText @@ -10,15 +10,13 @@ import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.recyclerview.widget.RecyclerView +import androidx.compose.ui.test.performScrollTo import androidx.test.espresso.Espresso.onView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition -import androidx.test.espresso.matcher.BoundedMatcher import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -31,11 +29,10 @@ import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.atPosition import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.hasItem import org.dhis2.common.matchers.RecyclerviewMatchers.Companion.isNotEmpty -import org.dhis2.common.viewactions.clickChildViewWithId import org.dhis2.usescases.event.entity.EventStatusUIModel import org.dhis2.usescases.event.entity.TEIProgramStagesUIModel +import org.dhis2.usescases.flow.teiFlow.entity.DateRegistrationUIModel import org.dhis2.usescases.programStageSelection.ProgramStageSelectionViewHolder -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventViewHolder import org.dhis2.usescases.teiDashboard.ui.STATE_INFO_BAR_TEST_TAG import org.dhis2.usescases.teiDashboard.ui.TEST_ADD_EVENT_BUTTON @@ -44,13 +41,10 @@ import org.dhis2.usescases.teidashboard.entity.UpperEnrollmentUIModel import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.equalTo -import org.hamcrest.CoreMatchers.not -import org.hamcrest.Description -import org.hamcrest.Matcher fun teiDashboardRobot( composeTestRule: ComposeTestRule, - teiDashboardRobot: TeiDashboardRobot.() -> Unit + teiDashboardRobot: TeiDashboardRobot.() -> Unit, ) { TeiDashboardRobot(composeTestRule).apply { teiDashboardRobot() @@ -60,17 +54,29 @@ fun teiDashboardRobot( class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun goToNotes() { - onView(withId(R.id.navigation_notes)).perform(click()) + composeTestRule.onNodeWithText( + InstrumentationRegistry.getInstrumentation().targetContext.getString( + R.string.navigation_notes + ) + ).performClick() Thread.sleep(500) } fun goToRelationships() { - onView(withId(R.id.navigation_relationships)).perform(click()) + composeTestRule.onNodeWithText( + InstrumentationRegistry.getInstrumentation().targetContext.getString( + R.string.navigation_relations + ) + ).performClick() Thread.sleep(500) } fun goToAnalytics() { - onView(withId(R.id.navigation_analytics)).perform(click()) + composeTestRule.onNodeWithText( + InstrumentationRegistry.getInstrumentation().targetContext.getString( + R.string.navigation_analytics + ) + ).performClick() Thread.sleep(500) } @@ -79,7 +85,9 @@ class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { } fun clickOnMenuReOpen() { - onView(withText(R.string.re_open)).perform(click()) + with(InstrumentationRegistry.getInstrumentation().targetContext) { + composeTestRule.onNodeWithText(getString(R.string.re_open)).performClick() + } } fun checkCancelledStateInfoBarIsDisplay() { @@ -102,8 +110,8 @@ class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { fun clickOnReferral() { val targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext - val referalTag = targetContext.resources.getString(R.string.referral) - composeTestRule.onNodeWithTag(referalTag).performClick() + val referalTag = targetContext.resources.getString(R.string.refer) + composeTestRule.onNodeWithText(referalTag, true).performClick() } fun clickOnFirstReferralEvent() { @@ -125,19 +133,21 @@ class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { .check( matches( allOf( - isDisplayed(), isNotEmpty(), + isDisplayed(), + isNotEmpty(), atPosition( - 0, hasDescendant( + 0, + hasDescendant( hasSibling( allOf( withId(R.id.programStageName), - withText(eventName) - ) - ) - ) - ) - ) - ) + withText(eventName), + ), + ), + ), + ), + ), + ), ) } @@ -173,11 +183,15 @@ class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { } fun clickOnMenuDeactivate() { - onView(withText(R.string.deactivate)).perform(click()) + with(InstrumentationRegistry.getInstrumentation().targetContext) { + composeTestRule.onNodeWithText(getString(R.string.deactivate)).performClick() + } } fun clickOnMenuComplete() { - onView(withText(R.string.complete)).perform(click()) + with(InstrumentationRegistry.getInstrumentation().targetContext) { + composeTestRule.onNodeWithText(getString(R.string.complete)).performClick() + } } fun checkCompleteStateInfoBarIsDisplay() { @@ -185,27 +199,31 @@ class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { composeTestRule.onNodeWithText("Enrollment completed").assertIsDisplayed() } - fun checkCanNotAddEvent() { composeTestRule.onNodeWithTag(TEST_ADD_EVENT_BUTTON, useUnmergedTree = true) .assertDoesNotExist() } fun clickOnShareButton() { - onView(withText(R.string.share)).perform(click()) + with(InstrumentationRegistry.getInstrumentation().targetContext) { + composeTestRule.onNodeWithText(getString(R.string.share)).performClick() + } } fun clickOnNextQR() { var qrLenght = 1 while (qrLenght < 8) { + waitForView(withId(R.id.next)) onView(withId(R.id.next)).perform(click()) qrLenght++ } } fun clickOnMenuDeleteTEI() { - onView(withText(R.string.dashboard_menu_delete_person)).perform(click()) + with(InstrumentationRegistry.getInstrumentation().targetContext) { + composeTestRule.onNodeWithText(getString(R.string.dashboard_menu_delete_person)).performClick() + } } fun checkUpperInfo(upperInformation: UpperEnrollmentUIModel) { @@ -218,152 +236,36 @@ class TeiDashboardRobot(val composeTestRule: ComposeTestRule) : BaseRobot() { } fun checkFullDetails(enrollmentUIModel: EnrollmentUIModel) { - onView(withId(R.id.recyclerView)).check( - matches( - not( - recyclerChildViews( - hasItem( - hasDescendant( - withText(enrollmentUIModel.enrollmentDate) - ) - ) - ) - ) - ) - ) - - onView(withId(R.id.recyclerView)).check( - matches( - not( - recyclerChildViews( - hasItem( - hasDescendant( - withText(enrollmentUIModel.birthday) - ) - ) - ) - ) - ) - ) - - onView(withId(R.id.recyclerView)).check( - matches( - not( - recyclerChildViews( - hasItem( - hasDescendant( - withText(enrollmentUIModel.orgUnit) - ) - ) - ) - ) - ) - ) - - onView(withId(R.id.recyclerView)).check( - matches( - not( - recyclerChildViews( - hasItem( - hasDescendant( - withText(enrollmentUIModel.latitude) - ) - ) - ) - ) - ) - ) - - onView(withId(R.id.recyclerView)).check( - matches( - not( - recyclerChildViews( - hasItem( - hasDescendant( - withText(enrollmentUIModel.longitude) - ) - ) - ) - ) - ) - ) - - - onView(withId(R.id.recyclerView)) - .perform( - actionOnItemAtPosition( - 6, - clickChildViewWithId(R.id.section_details) - ) - ) - - waitToDebounce(2000) - - onView(withId(R.id.recyclerView)).check( - matches( - not( - recyclerChildViews( - hasItem( - hasDescendant( - withText(enrollmentUIModel.name) - ) - ) - ) - ) - ) - ) - - onView(withId(R.id.recyclerView)).check( - matches( - not( - recyclerChildViews( - hasItem( - hasDescendant( - withText(enrollmentUIModel.lastName) - ) - ) - ) - ) - ) - ) - - onView(withId(R.id.recyclerView)).check( - matches( - not( - recyclerChildViews( - hasItem( - hasDescendant( - withText(enrollmentUIModel.sex) - ) - ) - ) - ) - ) - ) - - } - - private fun recyclerChildViews(matcher: Matcher): BoundedMatcher = - object : BoundedMatcher(RecyclerView::class.java) { - override fun describeTo(description: Description) { - description.appendText("RecyclerView child views: ") - matcher.describeTo(description) - } - - override fun matchesSafely(recyclerView: RecyclerView): Boolean = - matcher.matches(sequence { - val adapter: RecyclerView.Adapter = - recyclerView.adapter as RecyclerView.Adapter - for (position in 0.. - - @@ -116,7 +115,7 @@ diff --git a/app/src/main/java/org/dhis2/App.java b/app/src/main/java/org/dhis2/App.java index 099a38e85d..afe22a35e1 100644 --- a/app/src/main/java/org/dhis2/App.java +++ b/app/src/main/java/org/dhis2/App.java @@ -4,7 +4,6 @@ import android.content.Context; import android.os.Looper; -import android.os.StrictMode; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -32,6 +31,7 @@ import org.dhis2.commons.reporting.CrashReportModule; import org.dhis2.commons.schedulers.SchedulerModule; import org.dhis2.commons.schedulers.SchedulersProviderImpl; +import org.dhis2.commons.service.SessionManagerModule; import org.dhis2.commons.sync.SyncComponentProvider; import org.dhis2.data.appinspector.AppInspector; import org.dhis2.data.dispatcher.DispatcherModule; @@ -201,6 +201,7 @@ protected AppComponent.Builder prepareAppComponent() { .preferenceModule(new PreferenceModule()) .networkUtilsModule(new NetworkUtilsModule()) .workManagerController(new WorkManagerModule()) + .sessionManagerService(new SessionManagerModule()) .coroutineDispatchers(new DispatcherModule()) .crashReportModule(new CrashReportModule()) .customDispatcher(new CustomDispatcherModule()) @@ -406,4 +407,5 @@ private boolean areTrackingPermissionGranted() { .value(DATA_STORE_ANALYTICS_PERMISSION_KEY).blockingGet(); return granted != null && Boolean.parseBoolean(granted.value()); } + } diff --git a/app/src/main/java/org/dhis2/AppComponent.java b/app/src/main/java/org/dhis2/AppComponent.java index 4e59e8e475..2a288eace7 100644 --- a/app/src/main/java/org/dhis2/AppComponent.java +++ b/app/src/main/java/org/dhis2/AppComponent.java @@ -1,15 +1,20 @@ package org.dhis2; import org.dhis2.commons.featureconfig.di.FeatureConfigModule; -import org.dhis2.commons.network.NetworkUtils; -import org.dhis2.commons.network.NetworkUtilsModule; -import org.dhis2.data.dispatcher.DispatcherModule; -import org.dhis2.data.forms.dataentry.validation.ValidatorModule; import org.dhis2.commons.locationprovider.LocationModule; import org.dhis2.commons.locationprovider.LocationProvider; +import org.dhis2.commons.matomo.MatomoAnalyticsController; +import org.dhis2.commons.network.NetworkUtils; +import org.dhis2.commons.network.NetworkUtilsModule; import org.dhis2.commons.prefs.PreferenceModule; import org.dhis2.commons.prefs.PreferenceProvider; +import org.dhis2.commons.reporting.CrashReportController; +import org.dhis2.commons.reporting.CrashReportModule; import org.dhis2.commons.schedulers.SchedulerModule; +import org.dhis2.commons.service.SessionManagerModule; +import org.dhis2.commons.service.SessionManagerService; +import org.dhis2.data.dispatcher.DispatcherModule; +import org.dhis2.data.forms.dataentry.validation.ValidatorModule; import org.dhis2.data.server.ServerComponent; import org.dhis2.data.server.ServerModule; import org.dhis2.data.service.workManager.WorkManagerController; @@ -20,11 +25,7 @@ import org.dhis2.usescases.splash.SplashModule; import org.dhis2.utils.Validator; import org.dhis2.utils.analytics.AnalyticsModule; -import org.dhis2.commons.matomo.MatomoAnalyticsController; import org.dhis2.utils.analytics.matomo.MatomoAnalyticsModule; -import org.dhis2.commons.filters.di.FilterModule; -import org.dhis2.commons.reporting.CrashReportController; -import org.dhis2.commons.reporting.CrashReportModule; import org.hisp.dhis.android.core.common.ValueType; import java.util.Map; @@ -44,6 +45,7 @@ AnalyticsModule.class, PreferenceModule.class, WorkManagerModule.class, + SessionManagerModule.class, MatomoAnalyticsModule.class, ValidatorModule.class, CrashReportModule.class, @@ -67,6 +69,8 @@ interface Builder { Builder workManagerController(WorkManagerModule workManagerModule); + Builder sessionManagerService(SessionManagerModule sessionManagerModule); + Builder crashReportModule(CrashReportModule crashReportModule); Builder coroutineDispatchers(DispatcherModule dispatcherModule); @@ -88,6 +92,8 @@ interface Builder { WorkManagerController workManagerController(); + SessionManagerService sessionManagerService(); + MatomoAnalyticsController matomoController(); org.dhis2.commons.viewmodel.DispatcherProvider dispatcherProvider(); diff --git a/app/src/main/java/org/dhis2/animations/CarouselViewAnimations.kt b/app/src/main/java/org/dhis2/animations/CarouselViewAnimations.kt deleted file mode 100644 index 747ef4f69c..0000000000 --- a/app/src/main/java/org/dhis2/animations/CarouselViewAnimations.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.dhis2.animations - -import android.view.animation.DecelerateInterpolator - -class CarouselViewAnimations { - - fun initMapLoading(view: org.dhis2.maps.views.CarouselView) { - view.animate().apply { - duration = 500 - interpolator = DecelerateInterpolator() - alpha(0.25f) - withStartAction { view.setEnabledStatus(false) } - start() - } - } - - fun endMapLoading(view: org.dhis2.maps.views.CarouselView) { - view.animate().apply { - duration = 500 - interpolator = DecelerateInterpolator() - alpha(1f) - withEndAction { - view.setEnabledStatus(true) - view.selectFirstItem() - } - start() - } - } -} diff --git a/app/src/main/java/org/dhis2/bindings/Bindings.java b/app/src/main/java/org/dhis2/bindings/Bindings.java index f61f6b15e4..0354b034f2 100644 --- a/app/src/main/java/org/dhis2/bindings/Bindings.java +++ b/app/src/main/java/org/dhis2/bindings/Bindings.java @@ -21,7 +21,7 @@ import org.dhis2.R; import org.dhis2.commons.animations.ViewAnimationsKt; -import org.dhis2.utils.DateUtils; +import org.dhis2.commons.date.DateUtils; import org.dhis2.utils.NetworkUtils; import org.hisp.dhis.android.core.enrollment.Enrollment; import org.hisp.dhis.android.core.enrollment.EnrollmentStatus; diff --git a/app/src/main/java/org/dhis2/data/biometric/BiometricController.kt b/app/src/main/java/org/dhis2/data/biometric/BiometricController.kt new file mode 100644 index 0000000000..dafab9f3f9 --- /dev/null +++ b/app/src/main/java/org/dhis2/data/biometric/BiometricController.kt @@ -0,0 +1,47 @@ +package org.dhis2.data.biometric + +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import org.dhis2.R + +class BiometricController( + private val fragmentActivity: FragmentActivity, +) { + + private var biometricPrompt: BiometricPrompt? = null + + fun hasBiometric(): Boolean { + val biometricManager = BiometricManager.from(fragmentActivity) + return when (biometricManager.canAuthenticate(BIOMETRIC_WEAK)) { + BiometricManager.BIOMETRIC_SUCCESS -> true + else -> false + } + } + + fun authenticate(onSuccess: () -> Unit) { + if (biometricPrompt == null) { + biometricPrompt = BiometricPrompt( + fragmentActivity, + ContextCompat.getMainExecutor(fragmentActivity), + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult, + ) { + super.onAuthenticationSucceeded(result) + onSuccess() + } + }, + ) + } + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(fragmentActivity.getString(R.string.biometric_title)) + .setNegativeButtonText(fragmentActivity.getString(R.string.use_password)) + .build() + + biometricPrompt?.authenticate(promptInfo) + } +} diff --git a/app/src/main/java/org/dhis2/data/biometric/BiometricModule.kt b/app/src/main/java/org/dhis2/data/biometric/BiometricModule.kt new file mode 100644 index 0000000000..dcf460592f --- /dev/null +++ b/app/src/main/java/org/dhis2/data/biometric/BiometricModule.kt @@ -0,0 +1,19 @@ +package org.dhis2.data.biometric + +import dagger.Module +import dagger.Provides +import org.dhis2.commons.di.dagger.PerActivity +import org.dhis2.usescases.general.ActivityGlobalAbstract + +@Module +object BiometricModule { + + @JvmStatic + @Provides + @PerActivity + fun provideBiometricController( + context: ActivityGlobalAbstract, + ): BiometricController { + return BiometricController(context) + } +} diff --git a/app/src/main/java/org/dhis2/data/dhislogic/EnrollmentEventGenerator.kt b/app/src/main/java/org/dhis2/data/dhislogic/EnrollmentEventGenerator.kt index 02fcb566ff..0152df3c40 100644 --- a/app/src/main/java/org/dhis2/data/dhislogic/EnrollmentEventGenerator.kt +++ b/app/src/main/java/org/dhis2/data/dhislogic/EnrollmentEventGenerator.kt @@ -1,12 +1,13 @@ package org.dhis2.data.dhislogic import org.dhis2.commons.Constants -import org.dhis2.utils.DateUtils +import org.dhis2.commons.date.DateUtils import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.program.ProgramStage import timber.log.Timber import java.util.Calendar +import java.util.Date class EnrollmentEventGenerator( private val generatorRepository: EnrollmentEventGeneratorRepository, @@ -121,10 +122,11 @@ class EnrollmentEventGenerator( calendar.set(Calendar.SECOND, 0) calendar.set(Calendar.MILLISECOND, 0) var eventDate = calendar.time - + val currentDate = DateUtils.getInstance().getStartOfDay(Date()) periodType?.let { eventDate = generatorRepository.periodStartingDate(it, eventDate) } - - generatorRepository.setEventDate(eventUid, eventDate) + if (eventDate.before(currentDate) || eventDate == currentDate) { + generatorRepository.setEventDate(eventUid, eventDate) + } } catch (d2Error: D2Error) { Timber.e(d2Error) } diff --git a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintController.kt b/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintController.kt deleted file mode 100644 index 92651117a0..0000000000 --- a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintController.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.dhis2.data.fingerprint - -import io.reactivex.Observable - -interface FingerPrintController { - fun hasFingerPrint(): Boolean - fun authenticate(): Observable -} diff --git a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintControllerImpl.kt b/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintControllerImpl.kt deleted file mode 100644 index 2481af7de9..0000000000 --- a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintControllerImpl.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.dhis2.data.fingerprint - -import io.reactivex.Observable - -class FingerPrintControllerImpl( - val mapper: FingerPrintMapper, -) : FingerPrintController { - - /*** - *Checks if device supports fingerprint hardware - * */ - override fun hasFingerPrint(): Boolean { - return false - } - - /*** - *Auth using fingerprint and map to result - * */ - override fun authenticate(): Observable { - return Observable.empty() - } -} diff --git a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintMapper.kt b/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintMapper.kt deleted file mode 100644 index 3160de6c58..0000000000 --- a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintMapper.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.dhis2.data.fingerprint - -class FingerPrintMapper { - fun mapToFingerPrintResult(): FingerPrintResult { - return FingerPrintResult(Type.INFO, "") - } -} diff --git a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintModule.kt b/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintModule.kt deleted file mode 100644 index 0dd51b37ad..0000000000 --- a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintModule.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.dhis2.data.fingerprint - -import dagger.Module -import dagger.Provides -import org.dhis2.commons.di.dagger.PerActivity - -@Module -object FingerPrintModule { - - @JvmStatic - @Provides - @PerActivity - fun provideFingerPrintController(mapper: FingerPrintMapper): FingerPrintController { - return FingerPrintControllerImpl(mapper) - } - - @JvmStatic - @Provides - fun provideFingerPrintMapper(): FingerPrintMapper { - return FingerPrintMapper() - } -} diff --git a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintResult.kt b/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintResult.kt deleted file mode 100644 index fd3216b495..0000000000 --- a/app/src/main/java/org/dhis2/data/fingerprint/FingerPrintResult.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.dhis2.data.fingerprint - -data class FingerPrintResult(val type: Type, val message: String?) - -enum class Type { - SUCCESS, - INFO, - ERROR, -} - -enum class Reason { - HARDWARE_UNAVAILABLE, - UNABLE_TO_PROCESS, - TIMEOUT, - NO_SPACE, - CANCELED, - LOCKOUT, - VENDOR, - LOCKOUT_PERMANENT, - USER_CANCELED, - GOOD, - PARTIAL, - INSUFFICIENT, - IMAGER_DIRTY, - TOO_SLOW, - TOO_FAST, - AUTHENTICATION_START, - AUTHENTICATION_SUCCESS, - AUTHENTICATION_FAIL, - UNKNOWN, -} diff --git a/app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.kt b/app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.kt deleted file mode 100644 index a97016ff3e..0000000000 --- a/app/src/main/java/org/dhis2/data/forms/EnrollmentFormRepository.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.dhis2.data.forms - -import io.reactivex.Flowable -import io.reactivex.Single -import io.reactivex.schedulers.Schedulers -import org.dhis2.commons.rules.RuleEngineContextData -import org.dhis2.form.data.RulesRepository -import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.rules.api.RuleEngineContext - -class EnrollmentFormRepository( - private val rulesRepository: RulesRepository, - private val enrollmentUid: String, - private val d2: D2, -) : FormRepository { - private var cachedRuleEngineFlowable: Flowable - private var enrollmentOrgUnitUid: String? = null - - init { - enrollmentOrgUnitUid = if (enrollmentUid.isNotEmpty()) { - d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet()!! - .organisationUnit() - } else { - "" - } - // We don't want to rebuild RuleEngine on each request, since metadata of - // the event is not changing throughout lifecycle of FormComponent. - cachedRuleEngineFlowable = enrollmentProgram() - .switchMap { program -> - Single.zip( - rulesRepository.rulesNew(program, null).subscribeOn(Schedulers.io()), - rulesRepository.ruleVariables(program).subscribeOn(Schedulers.io()), - rulesRepository.enrollmentEvents(enrollmentUid).subscribeOn(Schedulers.io()), - rulesRepository.queryConstants().subscribeOn(Schedulers.io()), - rulesRepository.supplementaryData(enrollmentOrgUnitUid!!) - .subscribeOn(Schedulers.io()), - ) { rules, variables, events, constants, supplementaryData -> - - val ruleEngineContext = RuleEngineContext( - rules, - variables, - supplementaryData, - constants, - ) - RuleEngineContextData( - ruleEngineContext = ruleEngineContext, - ruleEnrollment = null, - ruleEvents = events, - ) - }.toFlowable() - } - .cacheWithInitialCapacity(1) - } - - override fun restartRuleEngine(): Flowable { - val orgUnit = d2.enrollmentModule().enrollments().uid(enrollmentUid).blockingGet()!! - .organisationUnit() - return enrollmentProgram() - .switchMap { program -> - Single.zip( - rulesRepository.rulesNew(program, null), - rulesRepository.ruleVariables(program), - rulesRepository.enrollmentEvents(enrollmentUid), - rulesRepository.queryConstants(), - rulesRepository.supplementaryData(orgUnit!!), - ) { rules, variables, events, constants, supplementaryData -> - val ruleEngineContext = RuleEngineContext( - rules, - variables, - supplementaryData, - constants, - ) - RuleEngineContextData( - ruleEngineContext = ruleEngineContext, - ruleEnrollment = null, - ruleEvents = events, - ) - }.toFlowable() - } - .cacheWithInitialCapacity(1).also { cachedRuleEngineFlowable = it } - } - - override fun ruleEngine(): Flowable { - return cachedRuleEngineFlowable - } - - private fun enrollmentProgram(): Flowable { - return d2.enrollmentModule().enrollments().uid(enrollmentUid).get() - .map { it.program()!! } - .toFlowable() - } -} diff --git a/app/src/main/java/org/dhis2/data/forms/EventRepository.java b/app/src/main/java/org/dhis2/data/forms/EventRepository.java deleted file mode 100644 index ff5e99b0fa..0000000000 --- a/app/src/main/java/org/dhis2/data/forms/EventRepository.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.dhis2.data.forms; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.dhis2.commons.rules.RuleEngineContextData; -import org.dhis2.form.data.RulesRepository; -import org.hisp.dhis.android.core.D2; -import org.hisp.dhis.rules.api.RuleEngineContext; - -import io.reactivex.Flowable; -import io.reactivex.Single; - -public class EventRepository implements FormRepository { - - private final String programUid; - private final String orgUnit; - - @NonNull - private Flowable cachedRuleEngineFlowable; - - private RuleEngineContextData ruleEngineContextData = null; - - @Nullable - private final String eventUid; - private final RulesRepository rulesRepository; - - public EventRepository( - @NonNull RulesRepository rulesRepository, - @Nullable String eventUid, - @NonNull D2 d2) { - this.eventUid = eventUid != null ? eventUid : ""; - this.rulesRepository = rulesRepository; - this.programUid = eventUid != null ? d2.eventModule().events().uid(eventUid).blockingGet().program() : ""; - this.orgUnit = !this.eventUid.isEmpty() ? d2.eventModule().events().uid(eventUid).blockingGet().organisationUnit() : ""; - // We don't want to rebuild RuleEngine on each request, since metadata of - // the event is not changing throughout lifecycle of FormComponent. - this.cachedRuleEngineFlowable = Single.zip( - rulesRepository.rulesNew(programUid, eventUid), - rulesRepository.ruleVariables(programUid), - rulesRepository.otherEvents(this.eventUid), - rulesRepository.enrollment(this.eventUid), - rulesRepository.queryConstants(), - rulesRepository.supplementaryData(orgUnit), - (rules, variables, events, enrollment, constants, supplementaryData) -> { - RuleEngineContext ruleEngineContext = new RuleEngineContext( - rules, - variables, - supplementaryData, - constants - ); - - return new RuleEngineContextData( - ruleEngineContext, - enrollment.getEnrollment().isEmpty() ? null : enrollment, - events - ); - }) - .doOnSuccess(contextData -> this.ruleEngineContextData = contextData) - .toFlowable() - .cacheWithInitialCapacity(1); - } - - - @Override - public Flowable restartRuleEngine() { - return this.cachedRuleEngineFlowable = Single.zip( - rulesRepository.rulesNew(programUid, eventUid), - rulesRepository.ruleVariables(programUid), - rulesRepository.otherEvents(this.eventUid), - rulesRepository.enrollment(this.eventUid), - rulesRepository.queryConstants(), - rulesRepository.supplementaryData(orgUnit), - (rules, variables, events, enrollment, constants, supplementaryData) -> { - RuleEngineContext ruleEngineContext = new RuleEngineContext( - rules, - variables, - supplementaryData, - constants - ); - - return new RuleEngineContextData( - ruleEngineContext, - enrollment.getEnrollment().isEmpty() ? null : enrollment, - events - ); - }) - .doOnSuccess(contextData -> this.ruleEngineContextData = contextData) - .toFlowable() - .cacheWithInitialCapacity(1); - } - - @NonNull - @Override - public Flowable ruleEngine() { - return ruleEngineContextData != null ? Flowable.just(ruleEngineContextData) : cachedRuleEngineFlowable; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/data/forms/FormRepository.java b/app/src/main/java/org/dhis2/data/forms/FormRepository.java deleted file mode 100644 index 78425ea42f..0000000000 --- a/app/src/main/java/org/dhis2/data/forms/FormRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.dhis2.data.forms; - -import androidx.annotation.NonNull; - -import org.dhis2.commons.rules.RuleEngineContextData; - -import io.reactivex.Flowable; - -public interface FormRepository { - - Flowable restartRuleEngine(); - - @NonNull - Flowable ruleEngine(); - -} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/data/forms/ReportStatus.java b/app/src/main/java/org/dhis2/data/forms/ReportStatus.java deleted file mode 100644 index febd75857e..0000000000 --- a/app/src/main/java/org/dhis2/data/forms/ReportStatus.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.dhis2.data.forms; - -import org.hisp.dhis.android.core.enrollment.EnrollmentStatus; -import org.hisp.dhis.android.core.event.EventStatus; - -enum ReportStatus { - ACTIVE, COMPLETED; - - static ReportStatus fromEnrollmentStatus(EnrollmentStatus enrollmentStatus) { - if (enrollmentStatus == EnrollmentStatus.ACTIVE) { - return ACTIVE; - } - return COMPLETED; - } - - static ReportStatus fromEventStatus(EventStatus eventStatus) { - if (eventStatus == EventStatus.COMPLETED) { - return COMPLETED; - } - return ACTIVE; - } - - static EventStatus toEventStatus(ReportStatus reportStatus) { - if (reportStatus == ACTIVE) { - return EventStatus.ACTIVE; - } - return EventStatus.COMPLETED; - } - - static EnrollmentStatus toEnrollmentStatus(ReportStatus reportStatus) { - if (reportStatus == ACTIVE) { - return EnrollmentStatus.ACTIVE; - } - return EnrollmentStatus.COMPLETED; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/data/forms/dataentry/DataEntryArguments.java b/app/src/main/java/org/dhis2/data/forms/dataentry/DataEntryArguments.java deleted file mode 100644 index fc75dbfbb7..0000000000 --- a/app/src/main/java/org/dhis2/data/forms/dataentry/DataEntryArguments.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.dhis2.data.forms.dataentry; - -import android.os.Parcelable; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.auto.value.AutoValue; - -@AutoValue -public abstract class DataEntryArguments implements Parcelable { - - @NonNull - public abstract String event(); - - @NonNull - public abstract String section(); - - @NonNull - public abstract String enrollment(); - - @Nullable - public abstract String renderType(); - - @NonNull - public static DataEntryArguments forEvent(@NonNull String event,String renderType) { - return new AutoValue_DataEntryArguments(event, "", "",renderType); - } - - @NonNull - public static DataEntryArguments forEventSection(@NonNull String event, @NonNull String section, String renderType) { - return new AutoValue_DataEntryArguments(event, section, "",renderType); - } - - @NonNull - public static DataEntryArguments forEnrollment(@NonNull String enrollment) { - return new AutoValue_DataEntryArguments("", "", enrollment,null); - } -} diff --git a/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/age/AgeView.java b/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/age/AgeView.java index 0ea89852ae..463c87762f 100644 --- a/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/age/AgeView.java +++ b/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/age/AgeView.java @@ -23,6 +23,8 @@ import org.dhis2.R; import org.dhis2.bindings.StringExtensionsKt; +import org.dhis2.commons.Constants; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.dialogs.CustomDialog; import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker; import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener; @@ -30,8 +32,6 @@ import org.dhis2.commons.resources.ColorUtils; import org.dhis2.databinding.AgeCustomViewAccentBinding; import org.dhis2.databinding.AgeCustomViewBinding; -import org.dhis2.commons.Constants; -import org.dhis2.utils.DateUtils; import org.dhis2.utils.customviews.FieldLayout; import org.jetbrains.annotations.NotNull; diff --git a/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/coordinate/CoordinatesView.java b/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/coordinate/CoordinatesView.java index a849984db9..4ca9a2c6b8 100644 --- a/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/coordinate/CoordinatesView.java +++ b/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/coordinate/CoordinatesView.java @@ -394,9 +394,11 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d public void onMapPositionClick() { subscribe(); ((FragmentActivity) getContext()).startActivityForResult(MapSelectorActivity.Companion.create( - (FragmentActivity) getContext(), + getContext(), + null, getFeatureType(), - currentCoordinates()), + currentCoordinates(), + null), RQ_MAP_LOCATION_VIEW); } diff --git a/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/datetime/OnDateSelected.java b/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/datetime/OnDateSelected.java deleted file mode 100644 index ddbe163c46..0000000000 --- a/app/src/main/java/org/dhis2/data/forms/dataentry/tablefields/datetime/OnDateSelected.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.dhis2.data.forms.dataentry.tablefields.datetime; - -import java.util.Date; - -/** - * QUADRAM. Created by ppajuelo on 23/04/2018. - */ - -public interface OnDateSelected { - void onDateSelected(Date date); -} diff --git a/app/src/main/java/org/dhis2/data/jira/ClickedIssueData.kt b/app/src/main/java/org/dhis2/data/jira/ClickedIssueData.kt deleted file mode 100644 index 15922bf0f8..0000000000 --- a/app/src/main/java/org/dhis2/data/jira/ClickedIssueData.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.dhis2.data.jira - -data class ClickedIssueData( - val uriString: String, - val auth: String, -) { - fun authHeader() = "Authorization" - fun basicAuth() = auth.toBasicAuth() -} diff --git a/app/src/main/java/org/dhis2/data/jira/IssueRequest.kt b/app/src/main/java/org/dhis2/data/jira/IssueRequest.kt deleted file mode 100644 index 462ef1344e..0000000000 --- a/app/src/main/java/org/dhis2/data/jira/IssueRequest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.dhis2.data.jira - -import org.dhis2.BuildConfig - -data class IssueRequest(private val fields: Fields) { - constructor(summary: String, description: String) : this( - Fields( - summary, - arrayListOf(Component(ANDROIDAPP_COMPONENT)), - description.jiraBugFormated(), - DEFAULT_ENVIRONMENT, - arrayListOf(FixVersion(BuildConfig.VERSION_NAME)), - Project(JIRA_PROJECT_NUMBER), - Issue(ISSUE_TYPE_BUG), - ), - ) -} - -data class Fields( - private val summary: String, - private val components: List, - private val description: String, - private val environment: String, - private val versions: List, - private val project: Project, - private val issuetype: Issue, -) - -data class Project(val id: String) -data class Issue(val name: String) -data class Component(val name: String) -data class FixVersion(val name: String) - -const val ANDROIDAPP_COMPONENT = "AndroidApp" -const val DEFAULT_ENVIRONMENT = "." -const val DEFAULT_BUG_TEMPLATE = "{panel:title=Bug description}\n%s\n{panel}" -const val JIRA_PROJECT_NUMBER = "10200" -const val ISSUE_TYPE_BUG = "Bug" diff --git a/app/src/main/java/org/dhis2/data/jira/JiraExtensions.kt b/app/src/main/java/org/dhis2/data/jira/JiraExtensions.kt deleted file mode 100644 index 7ee527c4af..0000000000 --- a/app/src/main/java/org/dhis2/data/jira/JiraExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.dhis2.data.jira - -const val BASIC = "Basic %s" - -fun String.jiraBugFormated() = DEFAULT_BUG_TEMPLATE.format(this) - -fun String.toJiraJql() = "(project=10200 AND reporter=$this AND issueType=10006) order by created" - -fun String.toJiraIssueUri() = "https://jira.dhis2.org/browse/$this" - -fun String.toBasicAuth() = BASIC.format(this) diff --git a/app/src/main/java/org/dhis2/data/jira/JiraIssue.kt b/app/src/main/java/org/dhis2/data/jira/JiraIssue.kt deleted file mode 100644 index 91a868d5bf..0000000000 --- a/app/src/main/java/org/dhis2/data/jira/JiraIssue.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.dhis2.data.jira - -data class JiraIssue( - val id: Int = 0, - val key: String, - val fields: JiraIssueField? = null, -) - -data class JiraIssueField( - val issuetype: JiraField? = null, - var summary: String? = null, - val status: JiraField? = null, -) - -data class JiraField( - val name: String? = null, - val id: Int = 0, -) diff --git a/app/src/main/java/org/dhis2/data/jira/JiraIssueListRequest.kt b/app/src/main/java/org/dhis2/data/jira/JiraIssueListRequest.kt deleted file mode 100644 index 1feb8a226c..0000000000 --- a/app/src/main/java/org/dhis2/data/jira/JiraIssueListRequest.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.dhis2.data.jira - -data class JiraIssueListRequest(val jql: String, val maxResults: Int) diff --git a/app/src/main/java/org/dhis2/data/jira/JiraIssueListResponse.kt b/app/src/main/java/org/dhis2/data/jira/JiraIssueListResponse.kt deleted file mode 100644 index 95746e9f30..0000000000 --- a/app/src/main/java/org/dhis2/data/jira/JiraIssueListResponse.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.dhis2.data.jira - -data class JiraIssueListResponse( - val maxResults: Int, - val total: Int, - val issues: List, -) diff --git a/app/src/main/java/org/dhis2/data/jira/JiraIssuesResult.kt b/app/src/main/java/org/dhis2/data/jira/JiraIssuesResult.kt deleted file mode 100644 index 338ff52532..0000000000 --- a/app/src/main/java/org/dhis2/data/jira/JiraIssuesResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.dhis2.data.jira - -data class JiraIssuesResult( - val issues: List = emptyList(), - val errorMessage: String? = null, -) { - fun isSuccess(): Boolean { - return errorMessage == null - } -} diff --git a/app/src/main/java/org/dhis2/data/qr/QRCodeGenerator.java b/app/src/main/java/org/dhis2/data/qr/QRCodeGenerator.java index 9df6226110..d8d1aec932 100644 --- a/app/src/main/java/org/dhis2/data/qr/QRCodeGenerator.java +++ b/app/src/main/java/org/dhis2/data/qr/QRCodeGenerator.java @@ -1,5 +1,14 @@ package org.dhis2.data.qr; +import static org.dhis2.data.qr.QRjson.ATTR_JSON; +import static org.dhis2.data.qr.QRjson.DATA_JSON; +import static org.dhis2.data.qr.QRjson.DATA_JSON_WO_REGISTRATION; +import static org.dhis2.data.qr.QRjson.ENROLLMENT_JSON; +import static org.dhis2.data.qr.QRjson.EVENTS_JSON; +import static org.dhis2.data.qr.QRjson.EVENT_JSON; +import static org.dhis2.data.qr.QRjson.TEI_JSON; +import static java.util.zip.Deflater.BEST_COMPRESSION; + import android.graphics.Bitmap; import android.text.TextUtils; import android.util.Base64; @@ -12,8 +21,8 @@ import com.google.zxing.common.BitMatrix; import com.journeyapps.barcodescanner.BarcodeEncoder; +import org.dhis2.commons.date.DateUtils; import org.dhis2.usescases.qrCodes.QrViewModel; -import org.dhis2.utils.DateUtils; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.common.Coordinates; import org.hisp.dhis.android.core.common.FeatureType; @@ -44,15 +53,6 @@ import io.reactivex.Observable; import timber.log.Timber; -import static java.util.zip.Deflater.BEST_COMPRESSION; -import static org.dhis2.data.qr.QRjson.ATTR_JSON; -import static org.dhis2.data.qr.QRjson.DATA_JSON; -import static org.dhis2.data.qr.QRjson.DATA_JSON_WO_REGISTRATION; -import static org.dhis2.data.qr.QRjson.ENROLLMENT_JSON; -import static org.dhis2.data.qr.QRjson.EVENTS_JSON; -import static org.dhis2.data.qr.QRjson.EVENT_JSON; -import static org.dhis2.data.qr.QRjson.TEI_JSON; - /** * QUADRAM. Created by ppajuelo on 22/05/2018. */ diff --git a/app/src/main/java/org/dhis2/data/server/ServerModule.kt b/app/src/main/java/org/dhis2/data/server/ServerModule.kt index f78931d708..75d2462eb2 100644 --- a/app/src/main/java/org/dhis2/data/server/ServerModule.kt +++ b/app/src/main/java/org/dhis2/data/server/ServerModule.kt @@ -16,9 +16,11 @@ import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.reporting.CrashReportController import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.service.SyncStatusController import org.dhis2.data.service.VersionRepository import org.dhis2.form.data.FileController @@ -122,8 +124,8 @@ class ServerModule { @Provides @PerServer - fun providesSyncStatusController(): SyncStatusController { - return SyncStatusController() + fun providesSyncStatusController(dispatcherProvider: DispatcherProvider): SyncStatusController { + return SyncStatusController(dispatcherProvider) } @Provides @@ -200,4 +202,13 @@ class ServerModule { fun provideOptionsRepository(d2: D2): OptionsRepository { return OptionsRepository(d2) } + + @Provides + @PerServer + fun provideEventResourceProvider( + d2: D2, + resourceManager: ResourceManager, + ): EventResourcesProvider { + return EventResourcesProvider(d2, resourceManager) + } } diff --git a/app/src/main/java/org/dhis2/data/service/SyncGranularRxWorker.java b/app/src/main/java/org/dhis2/data/service/SyncGranularRxWorker.java index 8e0bfab080..df402571fa 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncGranularRxWorker.java +++ b/app/src/main/java/org/dhis2/data/service/SyncGranularRxWorker.java @@ -1,5 +1,12 @@ package org.dhis2.data.service; +import static org.dhis2.commons.Constants.ATTRIBUTE_OPTION_COMBO; +import static org.dhis2.commons.Constants.CATEGORY_OPTION_COMBO; +import static org.dhis2.commons.Constants.CONFLICT_TYPE; +import static org.dhis2.commons.Constants.ORG_UNIT; +import static org.dhis2.commons.Constants.PERIOD_ID; +import static org.dhis2.commons.Constants.UID; + import android.content.Context; import androidx.annotation.NonNull; @@ -7,8 +14,8 @@ import androidx.work.RxWorker; import androidx.work.WorkerParameters; +import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.sync.ConflictType; -import org.dhis2.utils.DateUtils; import org.hisp.dhis.android.core.imports.TrackerImportConflict; import org.jetbrains.annotations.NotNull; @@ -21,13 +28,6 @@ import io.reactivex.Single; import timber.log.Timber; -import static org.dhis2.commons.Constants.ATTRIBUTE_OPTION_COMBO; -import static org.dhis2.commons.Constants.CATEGORY_OPTION_COMBO; -import static org.dhis2.commons.Constants.CONFLICT_TYPE; -import static org.dhis2.commons.Constants.ORG_UNIT; -import static org.dhis2.commons.Constants.PERIOD_ID; -import static org.dhis2.commons.Constants.UID; - public class SyncGranularRxWorker extends RxWorker { @Inject diff --git a/app/src/main/java/org/dhis2/data/service/SyncPresenter.java b/app/src/main/java/org/dhis2/data/service/SyncPresenter.java index 0f4f1c81ca..507a0fabd4 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncPresenter.java +++ b/app/src/main/java/org/dhis2/data/service/SyncPresenter.java @@ -23,7 +23,7 @@ interface SyncPresenter { SyncResult checkSyncStatus(); - Observable syncGranularEvent(String eventUid); + Observable syncGranularEvent(String eventUid); ListenableWorker.Result blockSyncGranularProgram(String programUid); @@ -35,7 +35,7 @@ interface SyncPresenter { Observable syncGranularProgram(String uid); - Observable syncGranularTEI(String uid); + Observable syncGranularTEI(String uid); Observable syncGranularDataSet(String uid); diff --git a/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt b/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt index 9e1acd36e8..d5205fb81c 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt +++ b/app/src/main/java/org/dhis2/data/service/SyncPresenterImpl.kt @@ -7,6 +7,8 @@ import androidx.work.ListenableWorker import io.reactivex.Completable import io.reactivex.Observable import org.dhis2.bindings.toSeconds +import org.dhis2.commons.bindings.enrollment +import org.dhis2.commons.bindings.program import org.dhis2.commons.prefs.Preference.Companion.DATA import org.dhis2.commons.prefs.Preference.Companion.EVENT_MAX import org.dhis2.commons.prefs.Preference.Companion.EVENT_MAX_DEFAULT @@ -36,7 +38,6 @@ import org.hisp.dhis.android.core.settings.GeneralSettings import org.hisp.dhis.android.core.settings.LimitScope import org.hisp.dhis.android.core.settings.ProgramSettings import org.hisp.dhis.android.core.systeminfo.DHISVersion -import org.hisp.dhis.android.core.tracker.exporter.TrackerD2Progress import timber.log.Timber import java.util.Calendar import kotlin.math.ceil @@ -80,6 +81,7 @@ class SyncPresenterImpl( val programEventUids = d2.programModule().programs() .byProgramType().eq(ProgramType.WITHOUT_REGISTRATION) .blockingGetUids() + syncStatusController.startDownloadingEvents() Completable.fromObservable(d2.eventModule().events().upload()) .andThen( Completable.fromObservable( @@ -148,6 +150,8 @@ class SyncPresenterImpl( .byProgramType().eq(ProgramType.WITH_REGISTRATION) .blockingGetUids() + syncStatusController.startDownloadingTracker() + Completable.fromObservable(d2.trackedEntityModule().trackedEntityInstances().upload()) .andThen( Completable.fromObservable( @@ -182,6 +186,7 @@ class SyncPresenterImpl( override fun syncAndDownloadDataValues() { if (!d2.dataSetModule().dataSets().blockingIsEmpty()) { + syncStatusController.startDownloadingDataSets() Completable.fromObservable(d2.dataValueModule().dataValues().upload()) .andThen( Completable.fromObservable( @@ -190,9 +195,12 @@ class SyncPresenterImpl( ) .andThen( Completable.fromObservable( - d2.aggregatedModule().data().download().doOnNext { - syncStatusController.updateDownloadProcess(it.dataSets()) - }, + d2.aggregatedModule().data().download() + .doOnNext { + syncStatusController.updateDownloadProcess(it.dataSets()) + }.doOnComplete { + syncStatusController.finishDownloadingDataSets() + }, ), ).blockingAwait() } @@ -209,7 +217,6 @@ class SyncPresenterImpl( updateProyectAnalytics() setUpSMS() }, - ).andThen( d2.mapsModule().mapLayersDownloader().downloadMetadata(), ).andThen( @@ -290,10 +297,11 @@ class SyncPresenterImpl( return SyncResult.ERROR } - override fun syncGranularEvent(eventUid: String): Observable { - Completable.fromObservable(d2.eventModule().events().byUid().eq(eventUid).upload()) - .blockingAwait() - return d2.eventModule().eventDownloader().byUid().eq(eventUid).download() + override fun syncGranularEvent(eventUid: String): Observable { + Completable.fromObservable(syncRepository.uploadEvent(eventUid)).blockingAwait() + return syncRepository.downLoadEvent(eventUid) + .map { it as D2Progress } + .mergeWith(syncRepository.downloadEventFiles(eventUid)) } override fun blockSyncGranularProgram(programUid: String): ListenableWorker.Result { @@ -390,37 +398,35 @@ class SyncPresenterImpl( } override fun syncGranularProgram(uid: String): Observable { - return d2.programModule().programs().uid(uid).get().toObservable() - .flatMap { program -> - if (program.programType() == ProgramType.WITH_REGISTRATION) { - Completable.fromObservable( - d2.trackedEntityModule().trackedEntityInstances().byProgramUids(listOf(uid)) - .upload(), - ).blockingAwait() + return when (d2.program(uid)?.programType()) { + null -> null + ProgramType.WITH_REGISTRATION -> { + Completable.fromObservable(syncRepository.uploadTrackerProgram(uid)).blockingAwait() + syncRepository.downloadTrackerProgram(uid) + } - d2.trackedEntityModule().trackedEntityInstanceDownloader().byProgramUid(uid) - .download() - } else { - Completable.fromObservable( - d2.eventModule().events().byProgramUid().eq(uid).upload(), - ).blockingAwait() - d2.eventModule().eventDownloader().byProgramUid(uid).download() - } + ProgramType.WITHOUT_REGISTRATION -> { + Completable.fromObservable(syncRepository.uploadEventProgram(uid)).blockingAwait() + syncRepository.downloadEventProgram(uid) } + } + ?.map { it as D2Progress } + ?.mergeWith(syncRepository.downloadProgramFiles(uid)) + ?: Observable.empty() } - override fun syncGranularTEI(uid: String): Observable { - val enrollment = d2.enrollmentModule().enrollments().uid(uid).blockingGet() + override fun syncGranularTEI(uid: String): Observable { + val enrollment = d2.enrollment(uid) + val teiUid = enrollment?.trackedEntityInstance() ?: return Observable.empty() + val programUid = enrollment.program() Completable.fromObservable( - d2.trackedEntityModule().trackedEntityInstances() - .byUid().eq(enrollment?.trackedEntityInstance()) - .byProgramUids(enrollment?.program()?.let { listOf(it) } ?: emptyList()) - .upload(), + syncRepository.uploadTei(teiUid, programUid), ).blockingAwait() - return d2.trackedEntityModule().trackedEntityInstanceDownloader() - .byUid().eq(enrollment?.trackedEntityInstance()) - .byProgramUid(enrollment?.program() ?: "") - .download() + return syncRepository.downloadTei(teiUid, programUid) + .map { it as D2Progress } + .mergeWith( + syncRepository.downloadTeiFiles(teiUid, programUid), + ) } override fun syncGranularDataSet(uid: String): Observable { diff --git a/app/src/main/java/org/dhis2/data/service/SyncRepository.kt b/app/src/main/java/org/dhis2/data/service/SyncRepository.kt index d844f92101..446462f44c 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncRepository.kt +++ b/app/src/main/java/org/dhis2/data/service/SyncRepository.kt @@ -1,11 +1,25 @@ package org.dhis2.data.service +import io.reactivex.Observable +import org.hisp.dhis.android.core.arch.call.D2Progress import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.hisp.dhis.android.core.tracker.exporter.TrackerD2Progress interface SyncRepository { fun getTeiByNotInStates(uid: String, states: List): List fun getTeiByInStates(uid: String, states: List): List fun getEventsFromEnrollmentByNotInSyncState(uid: String, states: List): List + fun uploadEvent(eventUid: String): Observable + fun downLoadEvent(eventUid: String): Observable + fun downloadEventFiles(eventUid: String): Observable + fun uploadTrackerProgram(programUid: String): Observable + fun downloadTrackerProgram(programUid: String): Observable + fun uploadEventProgram(programUid: String): Observable + fun downloadEventProgram(programUid: String): Observable + fun downloadProgramFiles(programUid: String): Observable + fun uploadTei(teiUid: String, programUid: String?): Observable + fun downloadTei(teiUid: String, programUid: String?): Observable + fun downloadTeiFiles(teiUid: String, programUid: String?): Observable } diff --git a/app/src/main/java/org/dhis2/data/service/SyncRepositoryImpl.kt b/app/src/main/java/org/dhis2/data/service/SyncRepositoryImpl.kt index ac436b3c99..3e1cb62920 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/data/service/SyncRepositoryImpl.kt @@ -1,6 +1,8 @@ package org.dhis2.data.service +import io.reactivex.Observable import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.call.D2Progress import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance @@ -33,4 +35,58 @@ class SyncRepositoryImpl(private val d2: D2) : SyncRepository { .byAggregatedSyncState().notIn(State.SYNCED) .blockingGet() } + + override fun uploadEvent(eventUid: String) = + d2.eventModule().events().byUid().eq(eventUid).upload() + + override fun downLoadEvent(eventUid: String): Observable = + d2.eventModule().eventDownloader() + .byUid().eq(eventUid) + .download() + + override fun downloadEventFiles(eventUid: String) = + d2.fileResourceModule().fileResourceDownloader() + .byEventUid().eq(eventUid) + .download() + + override fun uploadTrackerProgram(programUid: String) = + d2.trackedEntityModule().trackedEntityInstances() + .byProgramUids(listOf(programUid)) + .upload() + + override fun downloadTrackerProgram(programUid: String) = + d2.trackedEntityModule().trackedEntityInstanceDownloader() + .byProgramUid(programUid) + .download() + + override fun uploadEventProgram(programUid: String) = + d2.eventModule().events().byProgramUid().eq(programUid).upload() + + override fun downloadEventProgram(programUid: String) = + d2.eventModule().eventDownloader() + .byProgramUid(programUid) + .download() + + override fun downloadProgramFiles(programUid: String) = + d2.fileResourceModule().fileResourceDownloader() + .byProgramUid().eq(programUid) + .download() + + override fun uploadTei(teiUid: String, programUid: String?) = + d2.trackedEntityModule().trackedEntityInstances() + .byUid().eq(teiUid) + .byProgramUids(programUid?.let { listOf(it) } ?: emptyList()) + .upload() + + override fun downloadTei(teiUid: String, programUid: String?) = + d2.trackedEntityModule().trackedEntityInstanceDownloader() + .byUid().eq(teiUid) + .byProgramUid(programUid ?: "") + .download() + + override fun downloadTeiFiles(teiUid: String, programUid: String?) = + d2.fileResourceModule().fileResourceDownloader() + .byTrackedEntityUid().eq(teiUid) + .byProgramUid().eq(programUid ?: "") + .download() } diff --git a/app/src/main/java/org/dhis2/data/service/SyncStatusController.kt b/app/src/main/java/org/dhis2/data/service/SyncStatusController.kt index 712b04f075..3c551b6b25 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncStatusController.kt +++ b/app/src/main/java/org/dhis2/data/service/SyncStatusController.kt @@ -1,21 +1,35 @@ package org.dhis2.data.service -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import org.dhis2.commons.viewmodel.DispatcherProvider import org.hisp.dhis.android.core.arch.call.D2ProgressStatus import org.hisp.dhis.android.core.arch.call.D2ProgressSyncStatus import timber.log.Timber -class SyncStatusController { +class SyncStatusController(private val dispatcher: DispatcherProvider) { private var progressStatusMap: Map = emptyMap() - private val downloadStatus = MutableLiveData(SyncStatusData(isInitialSync = true)) + private val downloadStatus = MutableStateFlow(SyncStatusData(isInitialSync = true)) - fun observeDownloadProcess(): LiveData = downloadStatus + fun observeDownloadProcess(): StateFlow = downloadStatus fun initDownloadProcess(programDownload: Map) { Timber.tag("SYNC").d("INIT DATA SYNC") progressStatusMap = programDownload - downloadStatus.postValue(SyncStatusData(true, false, progressStatusMap)) + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + SyncStatusData( + running = true, + downloadingEvents = false, + downloadingTracker = false, + downloadingDataSetValues = false, + false, + progressStatusMap, + ), + ) + } } fun updateDownloadProcess(programDownload: Map) { @@ -23,17 +37,24 @@ class SyncStatusController { progressStatusMap = progressStatusMap.toMutableMap().also { it.putAll(programDownload) } - downloadStatus.postValue( - SyncStatusData(true, false, progressStatusMap), - ) + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + downloadStatus.value.copy(programSyncStatusMap = progressStatusMap), + ) + } } fun finishSync() { Timber.tag("SYNC").d("FINISH DATA SYNC") progressStatusMap = progressStatusMap.toMutableMap() - downloadStatus.postValue( - SyncStatusData(false, false, progressStatusMap), - ) + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + downloadStatus.value.copy( + running = false, + programSyncStatusMap = progressStatusMap, + ), + ) + } } fun onNetworkUnavailable() { @@ -44,9 +65,19 @@ class SyncStatusController { entry.value.copy(isComplete = true, D2ProgressSyncStatus.ERROR) } } - downloadStatus.postValue( - SyncStatusData(true, false, progressStatusMap), - ) + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + SyncStatusData(true, programSyncStatusMap = progressStatusMap), + ) + } + } + + fun startDownloadingEvents() { + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + downloadStatus.value.copy(running = true, downloadingEvents = true), + ) + } } fun finishDownloadingEvents(eventProgramUids: List) { @@ -58,9 +89,22 @@ class SyncStatusController { entry.value.copy(isComplete = true, D2ProgressSyncStatus.ERROR) } } - downloadStatus.postValue( - SyncStatusData(true, false, progressStatusMap), - ) + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + downloadStatus.value.copy( + downloadingEvents = false, + programSyncStatusMap = progressStatusMap, + ), + ) + } + } + + fun startDownloadingTracker() { + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + downloadStatus.value.copy(downloadingTracker = true), + ) + } } fun finishDownloadingTracker(trackerProgramUids: List) { @@ -73,9 +117,14 @@ class SyncStatusController { entry.value.copy(isComplete = true, D2ProgressSyncStatus.ERROR) } } - downloadStatus.postValue( - SyncStatusData(true, false, progressStatusMap), - ) + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + downloadStatus.value.copy( + downloadingTracker = false, + programSyncStatusMap = progressStatusMap, + ), + ) + } } fun updateSingleProgramToSuccess(programUid: String) { @@ -86,20 +135,41 @@ class SyncStatusController { entry.value.copy(isComplete = true, D2ProgressSyncStatus.SUCCESS) } } - downloadStatus.postValue( - SyncStatusData(false, false, progressStatusMap), - ) + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + SyncStatusData(false, programSyncStatusMap = progressStatusMap), + ) + } } fun initDownloadMedia() { Timber.tag("SYNC").d("INIT FILES") - - downloadStatus.postValue( - SyncStatusData(true, true, progressStatusMap), - ) + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + downloadStatus.value.copy(downloadingMedia = true), + ) + } } fun restore() { - downloadStatus.postValue(SyncStatusData()) + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit(SyncStatusData()) + } + } + + fun startDownloadingDataSets() { + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + downloadStatus.value.copy(downloadingDataSetValues = true), + ) + } + } + + fun finishDownloadingDataSets() { + CoroutineScope(dispatcher.io()).launch { + downloadStatus.emit( + downloadStatus.value.copy(downloadingDataSetValues = false), + ) + } } } diff --git a/app/src/main/java/org/dhis2/data/service/SyncStatusData.kt b/app/src/main/java/org/dhis2/data/service/SyncStatusData.kt index 245d514153..f3184604c4 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncStatusData.kt +++ b/app/src/main/java/org/dhis2/data/service/SyncStatusData.kt @@ -5,6 +5,9 @@ import org.hisp.dhis.android.core.arch.call.D2ProgressSyncStatus data class SyncStatusData( val running: Boolean? = null, + val downloadingEvents: Boolean = false, + val downloadingTracker: Boolean = false, + val downloadingDataSetValues: Boolean = false, val downloadingMedia: Boolean = false, val programSyncStatusMap: Map = emptyMap(), val isInitialSync: Boolean = false, @@ -16,11 +19,22 @@ data class SyncStatusData( fun hasDownloadError(uid: String): Boolean { return programSyncStatusMap.isNotEmpty() && - programSyncStatusMap[uid]?.syncStatus == D2ProgressSyncStatus.ERROR + ( + programSyncStatusMap[uid]?.syncStatus == D2ProgressSyncStatus.ERROR || + programSyncStatusMap[uid]?.syncStatus == D2ProgressSyncStatus.PARTIAL_ERROR + ) } - fun wasProgramDownloading(lastStatus: SyncStatusData?, uid: String): Boolean { - return lastStatus?.programSyncStatusMap?.get(uid)?.isComplete == false && - programSyncStatusMap[uid]?.isComplete == true + fun isProgramDownloaded(uid: String): Boolean { + return programSyncStatusMap[uid]?.isComplete == true && running == true + } + + fun canDisplayMessage() = when { + running == false or + downloadingEvents or + downloadingTracker or + downloadingDataSetValues or + downloadingMedia -> true + else -> false } } diff --git a/app/src/main/java/org/dhis2/data/sorting/SearchSortingValueSetter.kt b/app/src/main/java/org/dhis2/data/sorting/SearchSortingValueSetter.kt index a4ddd48bf6..108f07d87d 100644 --- a/app/src/main/java/org/dhis2/data/sorting/SearchSortingValueSetter.kt +++ b/app/src/main/java/org/dhis2/data/sorting/SearchSortingValueSetter.kt @@ -1,10 +1,10 @@ package org.dhis2.data.sorting -import org.dhis2.commons.data.SearchTeiModel import org.dhis2.commons.filters.Filters import org.dhis2.commons.filters.sorting.SortingItem import org.dhis2.commons.filters.sorting.SortingStatus import org.dhis2.data.enrollment.EnrollmentUiDataHelper +import org.dhis2.usescases.searchTrackEntity.SearchTeiModel import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.event.EventStatus diff --git a/app/src/main/java/org/dhis2/ui/icons/DHIS2Icons.kt b/app/src/main/java/org/dhis2/ui/icons/DHIS2Icons.kt new file mode 100644 index 0000000000..4657eaffc5 --- /dev/null +++ b/app/src/main/java/org/dhis2/ui/icons/DHIS2Icons.kt @@ -0,0 +1,3 @@ +package org.dhis2.ui.icons + +object DHIS2Icons diff --git a/app/src/main/java/org/dhis2/ui/icons/DataEntryFilled.kt b/app/src/main/java/org/dhis2/ui/icons/DataEntryFilled.kt new file mode 100644 index 0000000000..2a37c7ded1 --- /dev/null +++ b/app/src/main/java/org/dhis2/ui/icons/DataEntryFilled.kt @@ -0,0 +1,77 @@ +package org.dhis2.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val DHIS2Icons.DataEntryFilled: ImageVector + get() { + if (dataEntryFilled != null) { + return dataEntryFilled!! + } + dataEntryFilled = Builder( + name = "DataEntryFilled", defaultWidth = 24.0.dp, + defaultHeight = + 24.0.dp, + viewportWidth = 24.0f, viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(4.6f, 2.04f) + curveTo(4.73f, 2.01f, 4.86f, 2.0f, 5.0f, 2.0f) + horizontalLineTo(12.0f) + verticalLineTo(4.0f) + horizontalLineTo(5.0f) + verticalLineTo(20.0f) + horizontalLineTo(19.0f) + verticalLineTo(12.0f) + horizontalLineTo(21.0f) + verticalLineTo(20.0f) + curveTo(21.0f, 21.1f, 20.1f, 22.0f, 19.0f, 22.0f) + horizontalLineTo(5.0f) + curveTo(4.86f, 22.0f, 4.73f, 21.99f, 4.6f, 21.97f) + curveTo(4.21f, 21.89f, 3.86f, 21.69f, 3.59f, 21.42f) + curveTo(3.41f, 21.23f, 3.26f, 21.02f, 3.16f, 20.78f) + curveTo(3.06f, 20.54f, 3.0f, 20.27f, 3.0f, 20.0f) + verticalLineTo(4.0f) + curveTo(3.0f, 3.72f, 3.06f, 3.46f, 3.16f, 3.23f) + curveTo(3.26f, 2.99f, 3.41f, 2.77f, 3.59f, 2.59f) + curveTo(3.86f, 2.32f, 4.21f, 2.12f, 4.6f, 2.04f) + close() + } + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(17.5809f, 3.0977f) + lineTo(18.3583f, 2.3203f) + curveTo(18.7854f, 1.8932f, 19.4752f, 1.8932f, 19.9023f, 2.3203f) + lineTo(20.6797f, 3.0977f) + curveTo(21.1068f, 3.5248f, 21.1068f, 4.2146f, 20.6797f, 4.6417f) + lineTo(19.9023f, 5.4191f) + lineTo(17.5809f, 3.0977f) + close() + moveTo(16.8034f, 3.8752f) + lineTo(11.0f, 9.6786f) + verticalLineTo(12.0f) + horizontalLineTo(13.3214f) + lineTo(19.1248f, 6.1966f) + lineTo(16.8034f, 3.8752f) + close() + } + } + .build() + return dataEntryFilled!! + } + +private var dataEntryFilled: ImageVector? = null diff --git a/app/src/main/java/org/dhis2/ui/icons/DataEntryOutline.kt b/app/src/main/java/org/dhis2/ui/icons/DataEntryOutline.kt new file mode 100644 index 0000000000..da9b74eca3 --- /dev/null +++ b/app/src/main/java/org/dhis2/ui/icons/DataEntryOutline.kt @@ -0,0 +1,81 @@ +package org.dhis2.ui.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap.Companion.Butt +import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.ImageVector.Builder +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val DHIS2Icons.DataEntryOutline: ImageVector + get() { + if (dataEntryOutline != null) { + return dataEntryOutline!! + } + dataEntryOutline = Builder( + name = "DataEntryOutline", defaultWidth = 24.0.dp, + defaultHeight = + 24.0.dp, + viewportWidth = 24.0f, viewportHeight = 24.0f, + ).apply { + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(4.6f, 2.04f) + curveTo(4.73f, 2.01f, 4.86f, 2.0f, 5.0f, 2.0f) + horizontalLineTo(12.0f) + verticalLineTo(4.0f) + horizontalLineTo(5.0f) + verticalLineTo(20.0f) + horizontalLineTo(19.0f) + verticalLineTo(12.0f) + horizontalLineTo(21.0f) + verticalLineTo(20.0f) + curveTo(21.0f, 21.1f, 20.1f, 22.0f, 19.0f, 22.0f) + horizontalLineTo(5.0f) + curveTo(4.86f, 22.0f, 4.73f, 21.99f, 4.6f, 21.97f) + curveTo(4.21f, 21.89f, 3.86f, 21.69f, 3.59f, 21.42f) + curveTo(3.41f, 21.23f, 3.26f, 21.02f, 3.16f, 20.78f) + curveTo(3.06f, 20.54f, 3.0f, 20.27f, 3.0f, 20.0f) + verticalLineTo(4.0f) + curveTo(3.0f, 3.72f, 3.06f, 3.46f, 3.16f, 3.23f) + curveTo(3.26f, 2.99f, 3.41f, 2.77f, 3.59f, 2.59f) + curveTo(3.86f, 2.32f, 4.21f, 2.12f, 4.6f, 2.04f) + close() + } + path( + fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, + strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, + pathFillType = NonZero, + ) { + moveTo(18.4257f, 5.5504f) + lineTo(17.4524f, 4.577f) + lineTo(12.4037f, 9.6257f) + verticalLineTo(10.6082f) + horizontalLineTo(13.3861f) + lineTo(18.4257f, 5.5504f) + close() + moveTo(17.6252f, 2.4029f) + curveTo(17.7513f, 2.2752f, 17.9015f, 2.1739f, 18.067f, 2.1048f) + curveTo(18.2325f, 2.0356f, 18.4101f, 2.0f, 18.5895f, 2.0f) + curveTo(18.7689f, 2.0f, 18.9465f, 2.0356f, 19.112f, 2.1048f) + curveTo(19.2775f, 2.1739f, 19.4277f, 2.2752f, 19.5538f, 2.4029f) + lineTo(20.5999f, 3.449f) + curveTo(21.1366f, 3.9857f, 21.1366f, 4.8499f, 20.5999f, 5.3775f) + lineTo(13.9774f, 12.0f) + horizontalLineTo(10.9937f) + verticalLineTo(9.0344f) + lineTo(17.6252f, 2.4029f) + close() + } + } + .build() + return dataEntryOutline!! + } + +private var dataEntryOutline: ImageVector? = null diff --git a/app/src/main/java/org/dhis2/usescases/crash/CrashActivity.kt b/app/src/main/java/org/dhis2/usescases/crash/CrashActivity.kt index a30099e284..00b07f1b59 100644 --- a/app/src/main/java/org/dhis2/usescases/crash/CrashActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/crash/CrashActivity.kt @@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -25,11 +26,8 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -45,6 +43,9 @@ import cat.ereza.customactivityoncrash.config.CaocConfig import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.BuildConfig import org.dhis2.R +import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ColorStyle import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -197,6 +198,7 @@ fun CrashStackTraceInfo(stackTrace: String, onCopy: (textToCopy: String) -> Unit .fillMaxWidth() .wrapContentHeight() .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = spacedBy(8.dp), ) { Column( @@ -214,14 +216,10 @@ fun CrashStackTraceInfo(stackTrace: String, onCopy: (textToCopy: String) -> Unit color = Color.DarkGray, ) } - TextButton(onClick = { onCopy(stackTrace) }) { - Text( - text = stringResource( - id = R.string.customactivityoncrash_error_activity_error_details_copy, - ).uppercase(), - color = colorResource(id = R.color.colorPrimary), - ) - } + Button( + text = stringResource(id = R.string.customactivityoncrash_error_activity_error_details_copy), + onClick = { onCopy(stackTrace) }, + ) } } @@ -233,18 +231,13 @@ fun CrashGoBackButton(onGoBack: () -> Unit) { horizontalArrangement = Arrangement.Center, ) { Button( - onClick = { onGoBack() }, - colors = ButtonDefaults.buttonColors( - backgroundColor = colorResource(id = R.color.colorPrimary), + text = stringResource( + id = R.string.customactivityoncrash_error_activity_restart_app, ), - ) { - Text( - text = stringResource( - id = R.string.customactivityoncrash_error_activity_restart_app, - ).uppercase(), - color = colorResource(id = R.color.primaryBgTextColor), - ) - } + onClick = { onGoBack() }, + colorStyle = ColorStyle.DEFAULT, + style = ButtonStyle.FILLED, + ) } } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt index c0f162192b..b16f784076 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/DataSetTableActivity.kt @@ -9,8 +9,14 @@ import android.os.Looper import android.view.MenuItem import android.view.View import android.view.animation.OvershootInterpolator -import android.widget.PopupMenu import android.widget.Toast +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.LockReset +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.core.content.res.ResourcesCompat import androidx.databinding.DataBindingUtil import androidx.lifecycle.Lifecycle @@ -36,7 +42,6 @@ import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.dialogs.AlertBottomDialog.Companion.instance import org.dhis2.commons.extensions.closeKeyboard import org.dhis2.commons.matomo.Labels.Companion.CLICK -import org.dhis2.commons.popupmenu.AppMenuHelper import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext import org.dhis2.databinding.ActivityDatasetTableBinding @@ -45,11 +50,15 @@ import org.dhis2.usescases.datasets.dataSetTable.dataSetSection.DataSetSection import org.dhis2.usescases.datasets.dataSetTable.dataSetSection.DataSetSectionFragment.Companion.create import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.utils.analytics.SHOW_HELP +import org.dhis2.utils.customviews.MoreOptionsWithDropDownMenuButton import org.dhis2.utils.granularsync.OPEN_ERROR_LOCATION import org.dhis2.utils.granularsync.SyncStatusDialog import org.dhis2.utils.granularsync.shouldLaunchSyncDialog import org.dhis2.utils.validationrules.ValidationResultViolationsAdapter import org.dhis2.utils.validationrules.Violation +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import javax.inject.Inject class DataSetTableActivity : ActivityGlobalAbstract(), DataSetTableContract.View { @@ -110,6 +119,7 @@ class DataSetTableActivity : ActivityGlobalAbstract(), DataSetTableContract.View binding.tabLayout.visibility = View.GONE openDetails() } + R.id.navigation_data_entry -> { binding.syncButton.visibility = View.GONE binding.tabLayout.visibility = View.VISIBLE @@ -132,6 +142,7 @@ class DataSetTableActivity : ActivityGlobalAbstract(), DataSetTableContract.View if (intent.shouldLaunchSyncDialog()) { showGranularSync() } + setupMoreOptionsMenu() } private fun openDetails() { @@ -326,17 +337,14 @@ class DataSetTableActivity : ActivityGlobalAbstract(), DataSetTableContract.View .translationY(0f) .start() } + BottomSheetBehavior.STATE_COLLAPSED -> { animateArrowUp() binding.saveButton.animate() .translationY(-48.dp.toFloat()) .start() } - BottomSheetBehavior.STATE_DRAGGING, - BottomSheetBehavior.STATE_HALF_EXPANDED, - BottomSheetBehavior.STATE_HIDDEN, - BottomSheetBehavior.STATE_SETTLING, - -> {} + else -> {} } } @@ -433,24 +441,50 @@ class DataSetTableActivity : ActivityGlobalAbstract(), DataSetTableContract.View .start() } - override fun showMoreOptions(view: View) { - AppMenuHelper.Builder() - .menu(this, R.menu.dataset_menu) - .anchor(view) - .onMenuInflated { popupMenu: PopupMenu -> - popupMenu.menu.findItem(R.id.reopen).isVisible = presenter.isComplete() - } - .onMenuItemClicked { itemId: Int -> - if (itemId == R.id.showHelp) { - analyticsHelper().setEvent(SHOW_HELP, CLICK, SHOW_HELP) - showTutorial(true) - } else if (itemId == R.id.reopen) { - showReopenDialog() + private fun setupMoreOptionsMenu() { + binding.moreOptions.setContent { + var expanded by remember { mutableStateOf(false) } + + MoreOptionsWithDropDownMenuButton( + getMenuItems(), + expanded, + onMenuToggle = { expanded = it }, + ) { itemId -> + when (itemId) { + DataSetMenuItem.SHOW_HELP -> { + analyticsHelper().setEvent(SHOW_HELP, CLICK, SHOW_HELP) + showTutorial(true) + } + + DataSetMenuItem.RE_OPEN -> showReopenDialog() } - true } - .build() - .show() + } + } + + private fun getMenuItems(): List> { + return buildList { + add( + MenuItemData( + id = DataSetMenuItem.SHOW_HELP, + label = getString(R.string.showHelp), + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.HelpOutline), + ), + ) + if (presenter.isComplete()) { + add( + MenuItemData( + id = DataSetMenuItem.RE_OPEN, + label = getString(R.string.re_open), + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.LockReset, + defaultTintColor = SurfaceColor.Warning, + selectedTintColor = SurfaceColor.Warning, + ), + ), + ) + } + } } private fun showReopenDialog() { @@ -525,3 +559,8 @@ class DataSetTableActivity : ActivityGlobalAbstract(), DataSetTableContract.View } } } + +enum class DataSetMenuItem { + SHOW_HELP, + RE_OPEN, +} diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt index 84f22b818c..b2c7b0f4f3 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataSetSectionFragment.kt @@ -464,7 +464,6 @@ class DataSetSectionFragment : FragmentGlobalAbstract(), DataValueContract.View updateCellValue: (TableCell) -> Unit, ) { OUTreeFragment.Builder() - .showAsDialog() .singleSelection() .withPreselectedOrgUnits(cell.value?.let { listOf(it) } ?: emptyList()) .onSelection { selectedOrgUnits -> diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java index e0529c293a..2daf359f0a 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/DataSetDetailActivity.java @@ -27,15 +27,13 @@ import org.dhis2.commons.filters.FilterManager; import org.dhis2.commons.filters.FiltersAdapter; import org.dhis2.commons.orgunitselector.OUTreeFragment; -import org.dhis2.commons.sync.ConflictType; -import org.dhis2.commons.sync.OnNoConnectionListener; +import org.dhis2.commons.sync.SyncContext; import org.dhis2.databinding.ActivityDatasetDetailBinding; import org.dhis2.ui.ThemeManager; import org.dhis2.usescases.datasets.datasetDetail.datasetList.DataSetListFragment; import org.dhis2.usescases.general.ActivityGlobalAbstract; import org.dhis2.utils.DateUtils; import org.dhis2.utils.category.CategoryDialog; -import org.dhis2.commons.sync.SyncContext; import org.dhis2.utils.granularsync.SyncStatusDialog; import org.dhis2.utils.granularsync.SyncStatusDialogNavigatorKt; import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; @@ -136,8 +134,10 @@ private void configureBottomNavigation() { @Override protected void onResume() { super.onResume(); - presenter.init(); - binding.setTotalFilters(FilterManager.getInstance().getTotalFilters()); + if(sessionManagerServiceImpl.isUserLoggedIn()){ + presenter.init(); + binding.setTotalFilters(FilterManager.getInstance().getTotalFilters()); + } } @Override @@ -180,7 +180,6 @@ public void updateFilters(int totalFilters) { @Override public void openOrgUnitTreeSelector() { new OUTreeFragment.Builder() - .showAsDialog() .withPreselectedOrgUnits(FilterManager.getInstance().getOrgUnitUidsFilters()) .onSelection(selectedOrgUnits -> { presenter.setOrgUnitFilters((List) selectedOrgUnits); @@ -232,7 +231,9 @@ public void hideFilters() { @Override protected void onDestroy() { - presenter.clearFilterIfDatasetConfig(); + if(sessionManagerServiceImpl.isUserLoggedIn()) { + presenter.clearFilterIfDatasetConfig(); + } super.onDestroy(); } diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialActivity.java b/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialActivity.java index a18a4602d6..b4541e3137 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialActivity.java +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetInitial/DataSetInitialActivity.java @@ -69,7 +69,9 @@ public void onCreate(@Nullable Bundle savedInstanceState) { @Override protected void onResume() { super.onResume(); - presenter.init(); + if(sessionManagerServiceImpl.isUserLoggedIn()){ + presenter.init(); + } } @Override @@ -111,7 +113,6 @@ public void showOrgUnitDialog(List data) { preselectedOrgUnits.add(selectedOrgUnit.uid()); } new OUTreeFragment.Builder() - .showAsDialog() .singleSelection() .withPreselectedOrgUnits(preselectedOrgUnits) .orgUnitScope(new OrgUnitSelectorScope.DataSetCaptureScope(dataSetUid)) diff --git a/app/src/main/java/org/dhis2/usescases/development/ConflictGenerator.kt b/app/src/main/java/org/dhis2/usescases/development/ConflictGenerator.kt index b8978a6640..bf9c01c221 100644 --- a/app/src/main/java/org/dhis2/usescases/development/ConflictGenerator.kt +++ b/app/src/main/java/org/dhis2/usescases/development/ConflictGenerator.kt @@ -305,7 +305,7 @@ class ConflictGenerator(private val d2: D2) { return event.uid() } - private fun generateConflictInEvent(eventUid: String, importStatus: ImportStatus) { + fun generateConflictInEvent(eventUid: String, importStatus: ImportStatus) { val build = TrackerImportConflict.builder().conflict("Generated error conflict in event") .event(eventUid).displayDescription("Generated error description in event") .status(importStatus).build() @@ -318,6 +318,33 @@ class ConflictGenerator(private val d2: D2) { } } + fun generateStatusConflictInDataSet(importStatus: ImportStatus) { + val attributeValue = d2.dataValueModule().dataValues() + .bySyncState().eq(State.TO_UPDATE) + .blockingGet().first() + val build = DataValueConflict.builder().conflict("Generated error conflict in data value") + .value(attributeValue.value()).dataElement(attributeValue.dataElement()) + .period(attributeValue.period()).orgUnit(attributeValue.organisationUnit()) + .attributeOptionCombo(attributeValue.attributeOptionCombo()) + .categoryOptionCombo(attributeValue.categoryOptionCombo()) + .displayDescription("Generated error description in data value") + .status(importStatus).build() + val cv = build.toContentValues() + val updatedDataValueCV = + attributeValue.toBuilder().syncState(importStatus.toSyncState()).build().toContentValues() + try { + d2.databaseAdapter().insert("DataValueConflict", null, cv) + d2.databaseAdapter().update( + DataValueTableInfo.TABLE_INFO.name(), + updatedDataValueCV, + "_id = ${attributeValue.id()}", + emptyArray(), + ) + } catch (e: Exception) { + Timber.e(e) + } + } + private fun generateConflictInDataSetValue(importStatus: ImportStatus) { val attributeValue = d2.dataValueModule().dataValues().blockingGet()?.let { attributeValues -> diff --git a/app/src/main/java/org/dhis2/usescases/development/DevelopmentActivity.java b/app/src/main/java/org/dhis2/usescases/development/DevelopmentActivity.java index ce34945dc9..097ba30565 100644 --- a/app/src/main/java/org/dhis2/usescases/development/DevelopmentActivity.java +++ b/app/src/main/java/org/dhis2/usescases/development/DevelopmentActivity.java @@ -245,6 +245,7 @@ private void loadFeatureConfig() { @Override public void onBackPressed() { + super.onBackPressed(); setResult(RESULT_OK); finish(); } diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/DateEditionWarningHandler.kt b/app/src/main/java/org/dhis2/usescases/enrollment/DateEditionWarningHandler.kt new file mode 100644 index 0000000000..05c9a1b3b6 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/enrollment/DateEditionWarningHandler.kt @@ -0,0 +1,39 @@ +package org.dhis2.usescases.enrollment + +import org.dhis2.R +import org.dhis2.commons.resources.EventResourcesProvider +import org.dhis2.form.data.EnrollmentRepository +import org.dhis2.form.data.metadata.EnrollmentConfiguration + +class DateEditionWarningHandler( + private val conf: EnrollmentConfiguration?, + private val eventResourcesProvider: EventResourcesProvider, +) { + private var hasShownIncidentDateEditionWarning = false + private var hasShownEnrollmentDateEditionWarning = false + + fun shouldShowWarning( + fieldUid: String, + showWarning: (message: String) -> Unit, + ) { + if (fieldUid == EnrollmentRepository.ENROLLMENT_DATE_UID && + conf?.hasEventsGeneratedByEnrollmentDate() == true && + !hasShownEnrollmentDateEditionWarning + ) { + hasShownEnrollmentDateEditionWarning = true + showWarning(buildMessage()) + } else if (fieldUid == EnrollmentRepository.INCIDENT_DATE_UID && + conf?.hasEventsGeneratedByIncidentDate() == true && + !hasShownIncidentDateEditionWarning + ) { + hasShownIncidentDateEditionWarning = true + showWarning(buildMessage()) + } + } + + private fun buildMessage() = eventResourcesProvider.formatWithProgramEventLabel( + R.string.enrollment_date_edition_warning_event_label, + conf?.program()?.uid(), + 2, + ) +} diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt index e9f318a053..5e35d7c5c9 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentActivity.kt @@ -15,16 +15,13 @@ import org.dhis2.commons.Constants.PROGRAM_UID import org.dhis2.commons.Constants.TEI_UID import org.dhis2.commons.data.TeiAttributesInfo import org.dhis2.commons.dialogs.imagedetail.ImageDetailActivity -import org.dhis2.commons.featureconfig.data.FeatureConfigRepository -import org.dhis2.commons.featureconfig.model.Feature import org.dhis2.commons.resources.ResourceManager import org.dhis2.databinding.EnrollmentActivityBinding import org.dhis2.form.data.GeometryController import org.dhis2.form.data.GeometryParserImpl -import org.dhis2.form.model.EnrollmentRecords import org.dhis2.form.model.EventMode import org.dhis2.form.ui.FormView -import org.dhis2.form.ui.provider.EnrollmentResultDialogUiProvider +import org.dhis2.form.ui.provider.FormResultDialogProvider import org.dhis2.maps.views.MapSelectorActivity import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel @@ -52,10 +49,10 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { lateinit var presenter: EnrollmentPresenterImpl @Inject - lateinit var enrollmentResultDialogUiProvider: EnrollmentResultDialogUiProvider + lateinit var dateEditionWarningHandler: DateEditionWarningHandler @Inject - lateinit var featureConfig: FeatureConfigRepository + lateinit var enrollmentResultDialogProvider: FormResultDialogProvider lateinit var binding: EnrollmentActivityBinding lateinit var mode: EnrollmentMode @@ -93,7 +90,7 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { val enrollmentMode = intent.getStringExtra(MODE_EXTRA)?.let { EnrollmentMode.valueOf(it) } ?: EnrollmentMode.NEW val openErrorLocation = intent.getBooleanExtra(OPEN_ERROR_LOCATION, false) - (applicationContext as App).userComponent()!!.plus( + (applicationContext as App).userComponent()?.plus( EnrollmentModule( this, enrollmentUid, @@ -101,35 +98,7 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { enrollmentMode, context, ), - ).inject(this) - - formView = FormView.Builder() - .locationProvider(locationProvider) - .onItemChangeListener { action -> presenter.updateFields(action) } - .onLoadingListener { loading -> - if (loading) { - showProgress() - } else { - hideProgress() - presenter.showOrHideSaveButton() - } - } - .onFinishDataEntry { presenter.finish(mode) } - .resultDialogUiProvider(enrollmentResultDialogUiProvider) - .factory(supportFragmentManager) - .setRecords( - EnrollmentRecords( - enrollmentUid = enrollmentUid, - enrollmentMode = org.dhis2.form.model.EnrollmentMode.valueOf( - enrollmentMode.name, - ), - ), - ) - .openErrorLocation(openErrorLocation) - .useComposeForm( - featureConfig.isFeatureEnable(Feature.COMPOSE_FORMS), - ) - .build() + )?.inject(this) super.onCreate(savedInstanceState) @@ -140,17 +109,29 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { } forRelationship = intent.getBooleanExtra(FOR_RELATIONSHIP, false) - binding = DataBindingUtil.setContentView(this, R.layout.enrollment_activity) - binding.view = this - mode = enrollmentMode - val fragmentTransaction = supportFragmentManager.beginTransaction() - fragmentTransaction.replace(R.id.formViewContainer, formView) - fragmentTransaction.commit() + binding = DataBindingUtil.setContentView(this, R.layout.enrollment_activity) + binding.view = this - binding.save.setOnClickListener { - performSaveClick() + formView = buildEnrollmentForm( + config = EnrollmentFormBuilderConfig( + enrollmentUid = enrollmentUid, + programUid = programUid, + enrollmentMode = org.dhis2.form.model.EnrollmentMode.valueOf( + enrollmentMode.name, + ), + hasWriteAccess = presenter.hasWriteAccess(), + openErrorLocation = openErrorLocation, + containerId = R.id.formViewContainer, + loadingView = binding.toolbarProgress, + saveButton = binding.save, + ), + locationProvider = locationProvider, + dateEditionWarningHandler = dateEditionWarningHandler, + enrollmentResultDialogProvider = enrollmentResultDialogProvider, + ) { + presenter.finish(enrollmentMode) } presenter.init() @@ -172,27 +153,30 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { when (requestCode) { RQ_INCIDENT_GEOMETRY, RQ_ENROLLMENT_GEOMETRY -> { if (data?.hasExtra(MapSelectorActivity.DATA_EXTRA) == true) { - handleGeometry( - FeatureType.valueOfFeatureType( - data.getStringExtra(MapSelectorActivity.LOCATION_TYPE_EXTRA), - ), - data.getStringExtra(MapSelectorActivity.DATA_EXTRA)!!, - requestCode, - ) + data.getStringExtra(MapSelectorActivity.DATA_EXTRA)?.let { + handleGeometry( + FeatureType.valueOfFeatureType( + data.getStringExtra(MapSelectorActivity.LOCATION_TYPE_EXTRA), + ), + it, + requestCode, + ) + } } } - RQ_EVENT -> openDashboard(presenter.getEnrollment()!!.uid()!!) + RQ_EVENT -> presenter.getEnrollment()?.uid()?.let { openDashboard(it) } } } super.onActivityResult(requestCode, resultCode, data) } override fun openEvent(eventUid: String) { - if (presenter.isEventScheduleOrSkipped(eventUid)) { + val suggestedEventDateIsNotFutureDate = presenter.suggestedReportDateIsNotFutureDate(eventUid) + if (presenter.isEventScheduleOrSkipped(eventUid) && suggestedEventDateIsNotFutureDate) { val scheduleEventIntent = ScheduledEventActivity.getIntent(this, eventUid) openEventForResult.launch(scheduleEventIntent) - } else { + } else if (suggestedEventDateIsNotFutureDate) { val eventCreationIntent = Intent(abstracContext, EventCaptureActivity::class.java) eventCreationIntent.putExtras( EventCaptureActivity.getActivityBundle( @@ -202,25 +186,27 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { ), ) startActivityForResult(eventCreationIntent, RQ_EVENT) + } else { + openDashboard(presenter.getEnrollment()?.uid()!!) } } private val openEventForResult = registerForActivityResult( ActivityResultContracts.StartActivityForResult(), ) { - openDashboard(presenter.getEnrollment()!!.uid()!!) + presenter.getEnrollment()?.uid()?.let { it1 -> openDashboard(it1) } } override fun openDashboard(enrollmentUid: String) { if (forRelationship) { val intent = Intent() - intent.putExtra("TEI_A_UID", presenter.getEnrollment()!!.trackedEntityInstance()) + intent.putExtra("TEI_A_UID", presenter.getEnrollment()?.trackedEntityInstance()) setResult(Activity.RESULT_OK, intent) finish() } else { val bundle = Bundle() bundle.putString(PROGRAM_UID, presenter.getProgram()?.uid()) - bundle.putString(TEI_UID, presenter.getEnrollment()!!.trackedEntityInstance()) + bundle.putString(TEI_UID, presenter.getEnrollment()?.trackedEntityInstance()) bundle.putString(ENROLLMENT_UID, enrollmentUid) startActivity(TeiDashboardMobileActivity::class.java, bundle, true, false, null) } @@ -232,7 +218,6 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { @Deprecated("Deprecated in Java") override fun onBackPressed() { - formView.onEditionFinish() attemptFinish() } @@ -291,7 +276,7 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { if (mode != EnrollmentMode.NEW) { binding.title.text = resourceManager.defaultEnrollmentLabel( - programUid = presenter.getProgram()?.uid()!!, + programUid = presenter.getProgram()?.uid(), true, 1, ) @@ -348,21 +333,9 @@ class EnrollmentActivity : ActivityGlobalAbstract(), EnrollmentView { formView.onSaveClick() } - override fun showProgress() { - runOnUiThread { - binding.toolbarProgress.show() - } - } - - override fun hideProgress() { - runOnUiThread { - binding.toolbarProgress.hide() - } - } - - override fun showDateEditionWarning() { + override fun showDateEditionWarning(message: String?) { val dialog = MaterialAlertDialogBuilder(this, R.style.DhisMaterialDialog) - .setMessage(R.string.enrollment_date_edition_warning) + .setMessage(message) .setPositiveButton(R.string.button_ok, null) dialog.show() } diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepository.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepository.kt index b20dd134a7..b2ed498876 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepository.kt @@ -7,4 +7,5 @@ interface EnrollmentFormRepository { fun generateEvents(): Single> fun getProfilePicture(): String fun getProgramStageUidFromEvent(eventUi: String): String? + fun hasWriteAccess(): Boolean } diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepositoryImpl.kt index 8ab48b2bfb..14c9a82a5d 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentFormRepositoryImpl.kt @@ -5,6 +5,7 @@ import org.dhis2.bindings.profilePicturePath import org.dhis2.data.dhislogic.DhisEnrollmentUtils import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.repositories.`object`.ReadOnlyOneObjectRepositoryFinalImpl +import org.hisp.dhis.android.core.enrollment.EnrollmentAccess import org.hisp.dhis.android.core.enrollment.EnrollmentObjectRepository import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance @@ -33,4 +34,11 @@ class EnrollmentFormRepositoryImpl( override fun getProgramStageUidFromEvent(eventUi: String) = d2.eventModule().events().uid(eventUi).blockingGet()?.programStage() + + override fun hasWriteAccess(): Boolean { + return d2.enrollmentModule().enrollmentService().blockingGetEnrollmentAccess( + tei.uid(), + programUid, + ) == EnrollmentAccess.WRITE_ACCESS + } } diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt index 5f11242dfe..16790d2b35 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentModule.kt @@ -11,8 +11,8 @@ import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.network.NetworkUtils import org.dhis2.commons.prefs.PreferenceProviderImpl import org.dhis2.commons.reporting.CrashReportController -import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider @@ -30,18 +30,15 @@ import org.dhis2.form.model.EnrollmentMode import org.dhis2.form.model.RowAction import org.dhis2.form.ui.FieldViewModelFactory import org.dhis2.form.ui.FieldViewModelFactoryImpl -import org.dhis2.form.ui.LayoutProviderImpl import org.dhis2.form.ui.provider.AutoCompleteProviderImpl import org.dhis2.form.ui.provider.DisplayNameProviderImpl import org.dhis2.form.ui.provider.EnrollmentFormLabelsProvider -import org.dhis2.form.ui.provider.EnrollmentResultDialogUiProvider +import org.dhis2.form.ui.provider.FormResultDialogProvider +import org.dhis2.form.ui.provider.FormResultDialogResourcesProvider import org.dhis2.form.ui.provider.HintProviderImpl import org.dhis2.form.ui.provider.KeyboardActionProviderImpl import org.dhis2.form.ui.provider.LegendValueProviderImpl import org.dhis2.form.ui.provider.UiEventTypesProviderImpl -import org.dhis2.form.ui.provider.UiStyleProviderImpl -import org.dhis2.form.ui.style.FormUiModelColorFactoryImpl -import org.dhis2.form.ui.style.LongTextUiColorFactoryImpl import org.dhis2.form.ui.validation.FieldErrorMessageProvider import org.dhis2.usescases.teiDashboard.TeiAttributesProvider import org.dhis2.utils.analytics.AnalyticsHelper @@ -85,15 +82,21 @@ class EnrollmentModule( @Provides @PerActivity - fun provideDataEntryRepository( + fun provideEnrollmentConfiguration( d2: D2, + metadataIconProvider: MetadataIconProvider, + ) = EnrollmentConfiguration(d2, enrollmentUid, metadataIconProvider) + + @Provides + @PerActivity + fun provideDataEntryRepository( modelFactory: FieldViewModelFactory, enrollmentFormLabelsProvider: EnrollmentFormLabelsProvider, - metadataIconProvider: MetadataIconProvider, + enrollmentConfiguration: EnrollmentConfiguration, ): EnrollmentRepository { return EnrollmentRepository( fieldFactory = modelFactory, - conf = EnrollmentConfiguration(d2, enrollmentUid, metadataIconProvider), + conf = enrollmentConfiguration, enrollmentMode = EnrollmentMode.valueOf(enrollmentMode.name), enrollmentFormLabelsProvider = enrollmentFormLabelsProvider, ) @@ -116,16 +119,9 @@ class EnrollmentModule( context: Context, d2: D2, resourceManager: ResourceManager, - colorUtils: ColorUtils, periodUtils: DhisPeriodUtils, ): FieldViewModelFactory { return FieldViewModelFactoryImpl( - UiStyleProviderImpl( - FormUiModelColorFactoryImpl(activityContext, colorUtils), - LongTextUiColorFactoryImpl(activityContext, colorUtils), - true, - ), - LayoutProviderImpl(), HintProviderImpl(context), DisplayNameProviderImpl( OptionSetConfiguration(d2), @@ -140,12 +136,21 @@ class EnrollmentModule( ) } + @Provides + @PerActivity + fun provideDateEditionWarningHandler( + enrollmentConfiguration: EnrollmentConfiguration, + eventResourcesProvider: EventResourcesProvider, + ) = DateEditionWarningHandler( + enrollmentConfiguration, + eventResourcesProvider, + ) + @Provides @PerActivity fun providePresenter( d2: D2, enrollmentObjectRepository: EnrollmentObjectRepository, - dataEntryRepository: EnrollmentRepository, teiRepository: TrackedEntityInstanceObjectRepository, programRepository: ReadOnlyOneObjectRepositoryFinalImpl, schedulerProvider: SchedulerProvider, @@ -154,12 +159,12 @@ class EnrollmentModule( matomoAnalyticsController: MatomoAnalyticsController, eventCollectionRepository: EventCollectionRepository, teiAttributesProvider: TeiAttributesProvider, + dateEditionWarningHandler: DateEditionWarningHandler, ): EnrollmentPresenterImpl { return EnrollmentPresenterImpl( enrollmentView, d2, enrollmentObjectRepository, - dataEntryRepository, teiRepository, programRepository, schedulerProvider, @@ -168,6 +173,7 @@ class EnrollmentModule( matomoAnalyticsController, eventCollectionRepository, teiAttributesProvider, + dateEditionWarningHandler, ) } @@ -227,10 +233,20 @@ class EnrollmentModule( @Provides @PerActivity - fun provideDataEntryResultDialogProvider( + fun provideResultDialogProvider( + resourceManager: ResourceManager, + ): FormResultDialogProvider { + return FormResultDialogProvider( + FormResultDialogResourcesProvider(resourceManager), + ) + } + + @Provides + @PerActivity + fun provideDialogResourcesProvider( resourceManager: ResourceManager, - ): EnrollmentResultDialogUiProvider { - return EnrollmentResultDialogUiProvider(resourceManager) + ): FormResultDialogResourcesProvider { + return FormResultDialogResourcesProvider(resourceManager) } @Provides diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentPresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentPresenterImpl.kt index a87db3bff1..2908e511fa 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentPresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentPresenterImpl.kt @@ -6,13 +6,13 @@ import io.reactivex.processors.PublishProcessor import org.dhis2.bindings.profilePicturePath import org.dhis2.commons.bindings.trackedEntityTypeForTei import org.dhis2.commons.data.TeiAttributesInfo +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.matomo.Actions.Companion.CREATE_TEI import org.dhis2.commons.matomo.Categories.Companion.TRACKER_LIST import org.dhis2.commons.matomo.Labels.Companion.CLICK import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.schedulers.defaultSubscribe -import org.dhis2.form.data.EnrollmentRepository import org.dhis2.form.model.RowAction import org.dhis2.usescases.teiDashboard.TeiAttributesProvider import org.dhis2.utils.analytics.AnalyticsHelper @@ -32,6 +32,8 @@ import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstanceObjectRepository import timber.log.Timber +import java.util.Calendar.DAY_OF_YEAR +import java.util.Date private const val TAG = "EnrollmentPresenter" @@ -39,7 +41,6 @@ class EnrollmentPresenterImpl( val view: EnrollmentView, val d2: D2, private val enrollmentObjectRepository: EnrollmentObjectRepository, - private val dataEntryRepository: EnrollmentRepository, private val teiRepository: TrackedEntityInstanceObjectRepository, private val programRepository: ReadOnlyOneObjectRepositoryFinalImpl, private val schedulerProvider: SchedulerProvider, @@ -48,11 +49,11 @@ class EnrollmentPresenterImpl( private val matomoAnalyticsController: MatomoAnalyticsController, private val eventCollectionRepository: EventCollectionRepository, private val teiAttributesProvider: TeiAttributesProvider, + private val dateEditionWarningHandler: DateEditionWarningHandler, ) { + private val disposable = CompositeDisposable() private val backButtonProcessor: FlowableProcessor = PublishProcessor.create() - private var hasShownIncidentDateEditionWarning = false - private var hasShownEnrollmentDateEditionWarning = false fun init() { view.setSaveButtonVisible(false) @@ -120,24 +121,6 @@ class EnrollmentPresenterImpl( ) } - private fun shouldShowDateEditionWarning(uid: String): Boolean { - return if (uid == EnrollmentRepository.ENROLLMENT_DATE_UID && - dataEntryRepository.hasEventsGeneratedByEnrollmentDate() && - !hasShownEnrollmentDateEditionWarning - ) { - hasShownEnrollmentDateEditionWarning = true - true - } else if (uid == EnrollmentRepository.INCIDENT_DATE_UID && - dataEntryRepository.hasEventsGeneratedByIncidentDate() && - !hasShownIncidentDateEditionWarning - ) { - hasShownIncidentDateEditionWarning = true - true - } else { - false - } - } - fun subscribeToBackButton() { disposable.add( backButtonProcessor @@ -173,8 +156,8 @@ class EnrollmentPresenterImpl( fun updateFields(action: RowAction? = null) { action?.let { - if (shouldShowDateEditionWarning(it.id)) { - view.showDateEditionWarning() + dateEditionWarningHandler.shouldShowWarning(fieldUid = it.id) { message -> + view.showDateEditionWarning(message) } } } @@ -238,6 +221,8 @@ class EnrollmentPresenterImpl( } } + fun hasWriteAccess() = enrollmentFormRepository.hasWriteAccess() + fun showOrHideSaveButton() { val teiUid = teiRepository.blockingGet()?.uid() ?: "" val programUid = getProgram()?.uid() ?: "" @@ -256,4 +241,22 @@ class EnrollmentPresenterImpl( event?.status() == EventStatus.SKIPPED || event?.status() == EventStatus.OVERDUE } + + fun suggestedReportDateIsNotFutureDate(eventUid: String): Boolean { + return try { + val event = eventCollectionRepository.uid(eventUid).blockingGet() + val programStage = d2.programModule().programStages().uid(event?.programStage()).blockingGet() + val enrollment = enrollmentObjectRepository.blockingGet() + val generatedByEnrollment = programStage?.generatedByEnrollmentDate() ?: false + val startDate = if (generatedByEnrollment) enrollment?.enrollmentDate() else enrollment?.incidentDate() + val calendar = DateUtils.getInstance().getCalendarByDate(startDate) + calendar.add(DAY_OF_YEAR, programStage?.minDaysFromStart() ?: 0) + val minStartReportEventDate = calendar.time + val currentDate = DateUtils.getInstance().getStartOfDay(Date()) + return minStartReportEventDate.before(currentDate) || minStartReportEventDate == currentDate + } catch (e: Exception) { + Timber.d(e.message) + true + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentView.kt b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentView.kt index 9a953b3bd7..66aaf0dacd 100644 --- a/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentView.kt +++ b/app/src/main/java/org/dhis2/usescases/enrollment/EnrollmentView.kt @@ -19,8 +19,6 @@ interface EnrollmentView : AbstractActivityContracts.View { fun setResultAndFinish() fun requestFocus() fun performSaveClick() - fun showProgress() - fun hideProgress() fun displayTeiPicture(picturePath: String) - fun showDateEditionWarning() + fun showDateEditionWarning(message: String?) } diff --git a/app/src/main/java/org/dhis2/usescases/enrollment/FormInjector.kt b/app/src/main/java/org/dhis2/usescases/enrollment/FormInjector.kt new file mode 100644 index 0000000000..cb9f215972 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/enrollment/FormInjector.kt @@ -0,0 +1,102 @@ +package org.dhis2.usescases.enrollment + +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +import androidx.core.widget.ContentLoadingProgressBar +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.floatingactionbutton.FloatingActionButton +import org.dhis2.R +import org.dhis2.commons.locationprovider.LocationProvider +import org.dhis2.form.model.EnrollmentMode +import org.dhis2.form.model.EnrollmentRecords +import org.dhis2.form.ui.FormView +import org.dhis2.form.ui.provider.FormResultDialogProvider + +data class EnrollmentFormBuilderConfig( + val enrollmentUid: String, + val programUid: String, + val enrollmentMode: EnrollmentMode, + val hasWriteAccess: Boolean, + val openErrorLocation: Boolean, + @IdRes val containerId: Int, + val loadingView: ContentLoadingProgressBar, + val saveButton: FloatingActionButton, +) + +fun AppCompatActivity.buildEnrollmentForm( + config: EnrollmentFormBuilderConfig, + locationProvider: LocationProvider, + dateEditionWarningHandler: DateEditionWarningHandler, + enrollmentResultDialogProvider: FormResultDialogProvider, + onFinish: () -> Unit, +): FormView { + return FormView.Builder() + .locationProvider(locationProvider) + .onItemChangeListener { action -> + dateEditionWarningHandler.shouldShowWarning( + fieldUid = action.id, + showWarning = ::showDateEditionWarning, + ) + } + .onLoadingListener { loading -> + runOnUiThread { + handleLoading( + hasWriteAccess = config.hasWriteAccess, + loading = loading, + loadingView = config.loadingView, + saveButton = config.saveButton, + ) + } + } + .onFinishDataEntry(onFinish) + .eventCompletionResultDialogProvider(enrollmentResultDialogProvider) + .factory(supportFragmentManager) + .setRecords( + EnrollmentRecords( + enrollmentUid = config.enrollmentUid, + enrollmentMode = config.enrollmentMode, + ), + ) + .openErrorLocation(config.openErrorLocation) + .setProgramUid(config.programUid) + .build().also { formView -> + + config.saveButton.setOnClickListener { formView.onSaveClick() } + + val fragmentTransition = supportFragmentManager.beginTransaction() + fragmentTransition.replace( + config.containerId, + formView, + ) + fragmentTransition.commit() + } +} + +private fun AppCompatActivity.showDateEditionWarning(message: String) { + val dialog = MaterialAlertDialogBuilder(this, R.style.DhisMaterialDialog) + .setMessage(message) + .setPositiveButton(R.string.button_ok, null) + dialog.show() +} + +private fun handleLoading( + hasWriteAccess: Boolean, + loading: Boolean, + loadingView: ContentLoadingProgressBar, + saveButton: FloatingActionButton, +) { + if (loading) { + loadingView.show() + } else { + loadingView.hide() + handleSaveButtonVisibility(hasWriteAccess, saveButton) + } +} + +private fun handleSaveButtonVisibility(hasWriteAccess: Boolean, saveButton: FloatingActionButton) { + if (hasWriteAccess) { + saveButton.show() + } else { + saveButton.hide() + } +} diff --git a/app/src/main/java/org/dhis2/usescases/events/EventInfoProvider.kt b/app/src/main/java/org/dhis2/usescases/events/EventInfoProvider.kt new file mode 100644 index 0000000000..a2da03a56f --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/events/EventInfoProvider.kt @@ -0,0 +1,437 @@ +package org.dhis2.usescases.events + +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Event +import androidx.compose.material.icons.outlined.EventBusy +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.SyncDisabled +import androidx.compose.material.icons.outlined.SyncProblem +import androidx.compose.material.icons.outlined.Visibility +import org.dhis2.R +import org.dhis2.bindings.userFriendlyValue +import org.dhis2.commons.bindings.enrollment +import org.dhis2.commons.bindings.fromCache +import org.dhis2.commons.bindings.tei +import org.dhis2.commons.date.DateLabelProvider +import org.dhis2.commons.date.toOverdueOrScheduledUiText +import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.maps.model.RelatedInfo +import org.dhis2.tracker.data.ProfilePictureProvider +import org.dhis2.ui.avatar.AvatarProviderConfiguration +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.event.Event +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.program.ProgramStage +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItemColor +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import java.util.Date + +class EventInfoProvider( + private val d2: D2, + private val resourceManager: ResourceManager, + private val dateLabelProvider: DateLabelProvider, + private val metadataIconProvider: MetadataIconProvider, + private val profilePictureProvider: ProfilePictureProvider, +) { + private val cachedPrograms = mutableMapOf() + private val cachedDisplayOrgUnit = mutableMapOf() + private val cachedStages = mutableMapOf() + + fun getEventTitle(event: Event): String { + return when (event.status()) { + EventStatus.SCHEDULE -> { + dateLabelProvider.scheduleFormat( + event.eventDate(), + R.string.scheduled_for, + ) + } + + else -> dateLabelProvider.format(event.eventDate()) + } + } + + fun getAvatar(event: Event, useMetadataIcon: Boolean = false): AvatarProviderConfiguration { + val stage = event.programStage()?.let { getStage(it) } + val enrollment = event.enrollment()?.let { d2.enrollment(it) } + val tei = enrollment?.trackedEntityInstance()?.let { d2.tei(it) } + + return if (tei != null && !useMetadataIcon) { + val profilePath = profilePictureProvider(tei, event.program()) + val firstAttributeValue = d2.trackedEntityModule().trackedEntitySearch() + .uid(tei.uid()) + .blockingGet() + ?.attributeValues + ?.firstOrNull() + if (profilePath.isNotEmpty()) { + AvatarProviderConfiguration.ProfilePic( + profilePicturePath = profilePath, + ) + } else { + AvatarProviderConfiguration.MainValueLabel( + firstMainValue = firstAttributeValue?.value?.firstOrNull()?.toString() + ?: "", + ) + } + } else { + AvatarProviderConfiguration.Metadata( + metadataIconData = metadataIconProvider( + stage?.style() ?: ObjectStyle.builder().build(), + ), + ) + } + } + + fun getEventDescription(event: Event): String? { + return event.programStage()?.let { getStage(it)?.displayDescription() } + } + + fun getEventLastUpdated(event: Event): String { + return dateLabelProvider.span(event.lastUpdated()) + } + + fun getAdditionInfoList( + event: Event, + ): MutableList { + val program = event.program()?.let { getProgram(it) } + + val displayOrgUnit = program?.uid()?.let { getDisplayOrgUnit(it) } == true + + val list = getEventValues(event.uid(), event.programStage()).filter { + it.second.isNotEmpty() && it.second != "-" + }.map { + AdditionalInfoItem( + key = it.first, + value = it.second, + ) + }.toMutableList() + + if (displayOrgUnit) { + checkRegisteredIn( + list = list, + orgUnitUid = event.organisationUnit(), + ) + } + + checkCategoryCombination( + list = list, + event = event, + programCatComboUid = program?.categoryComboUid(), + ) + + checkEventStatus( + list = list, + status = event.status(), + dueDate = event.dueDate(), + ) + + checkSyncStatus( + list = list, + state = event.aggregatedSyncState(), + ) + + checkViewOnly( + list = list, + event = event, + ) + + return list + } + + fun getRelatedInfo(event: Event): RelatedInfo? { + val stage = event.programStage()?.let { getStage(it) } + val enrollment = event.enrollment()?.let { d2.enrollment(it) } + return if (stage != null) { + RelatedInfo( + event = RelatedInfo.Event( + stageUid = stage.uid(), + stageDisplayName = stage.displayName() ?: stage.uid(), + teiUid = enrollment?.uid(), + ), + ) + } else { + null + } + } + + private fun getEventValues( + eventUid: String, + eventStageUid: String? = null, + ): List> { + val stageUid = eventStageUid + ?: d2.eventModule().events() + .uid(eventUid) + .blockingGet() + ?.programStage() + + val displayInListDataElements = d2.programModule().programStageDataElements() + .byProgramStage().eq(stageUid) + .byDisplayInReports().isTrue + .blockingGet().map { + it.dataElement()?.uid()!! + } + + return if (displayInListDataElements.isNotEmpty()) { + displayInListDataElements.mapNotNull { + val valueRepo = d2.trackedEntityModule().trackedEntityDataValues() + .value(eventUid, it) + val de = d2.dataElementModule().dataElements() + .uid(it).blockingGet() + if (isAcceptedValueType(de?.valueType())) { + Pair( + de?.displayFormName() ?: de?.displayName() ?: "-", + if (valueRepo.blockingExists()) { + valueRepo.blockingGet().userFriendlyValue(d2) ?: "-" + } else { + "-" + }, + ) + } else { + null + } + } + } else { + emptyList() + } + } + + private fun isAcceptedValueType(valueType: ValueType?): Boolean { + return when (valueType) { + ValueType.IMAGE, ValueType.COORDINATE, ValueType.FILE_RESOURCE -> false + else -> true + } + } + + private fun checkRegisteredIn( + list: MutableList, + orgUnitUid: String?, + ) { + val orgUnit = d2.organisationUnitModule().organisationUnits() + .uid(orgUnitUid) + .blockingGet() + list.add( + AdditionalInfoItem( + key = resourceManager.getString(R.string.registered_in), + value = orgUnit?.displayName() ?: "-", + isConstantItem = true, + ), + ) + } + + private fun checkCategoryCombination( + list: MutableList, + event: Event, + programCatComboUid: String?, + ) { + val programCatCombo = d2.categoryModule().categoryCombos() + .uid(programCatComboUid) + .blockingGet() + programCatCombo?.let { categoryCombo -> + val catOptCombo = d2.categoryModule().categoryOptionCombos() + .uid(event.attributeOptionCombo()) + .blockingGet() + + catOptCombo?.displayName().takeIf { displayName -> + !displayName.isNullOrEmpty() && displayName != "default" + }?.let { displayName -> + list.add( + AdditionalInfoItem( + key = categoryCombo.displayName(), + value = displayName, + isConstantItem = true, + ), + ) + } + } + } + + private fun checkEventStatus( + list: MutableList, + status: EventStatus?, + dueDate: Date?, + ) { + val item = when (status) { + EventStatus.ACTIVE -> { + AdditionalInfoItem( + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = resourceManager.getString(R.string.event_not_completed), + tint = SurfaceColor.Primary, + ) + }, + value = resourceManager.getString(R.string.event_not_completed), + isConstantItem = true, + color = SurfaceColor.Primary, + ) + } + + EventStatus.SCHEDULE -> { + val text = dueDate.toOverdueOrScheduledUiText(resourceManager) + + AdditionalInfoItem( + icon = { + Icon( + imageVector = Icons.Outlined.Event, + contentDescription = text, + tint = AdditionalInfoItemColor.SUCCESS.color, + ) + }, + value = text, + isConstantItem = true, + color = AdditionalInfoItemColor.SUCCESS.color, + ) + } + + EventStatus.SKIPPED -> { + AdditionalInfoItem( + icon = { + Icon( + imageVector = Icons.Outlined.EventBusy, + contentDescription = resourceManager.getString(R.string.skipped), + tint = AdditionalInfoItemColor.DISABLED.color, + ) + }, + value = resourceManager.getString(R.string.skipped), + isConstantItem = true, + color = AdditionalInfoItemColor.DISABLED.color, + ) + } + + EventStatus.OVERDUE -> { + val overdueText = dueDate.toOverdueOrScheduledUiText(resourceManager) + + AdditionalInfoItem( + icon = { + Icon( + imageVector = Icons.Outlined.EventBusy, + contentDescription = overdueText, + tint = AdditionalInfoItemColor.ERROR.color, + ) + }, + value = overdueText, + isConstantItem = true, + color = AdditionalInfoItemColor.ERROR.color, + ) + } + + else -> null + } + item?.let { list.add(it) } + } + + private fun checkSyncStatus( + list: MutableList, + state: State?, + ) { + val item = when (state) { + State.TO_POST, + State.TO_UPDATE, + -> { + AdditionalInfoItem( + icon = { + Icon( + imageVector = Icons.Outlined.SyncDisabled, + contentDescription = resourceManager.getString(R.string.not_synced), + tint = AdditionalInfoItemColor.DISABLED.color, + ) + }, + value = resourceManager.getString(R.string.not_synced), + color = AdditionalInfoItemColor.DISABLED.color, + isConstantItem = true, + ) + } + + State.UPLOADING -> { + AdditionalInfoItem( + icon = { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) + }, + value = resourceManager.getString(R.string.syncing), + color = SurfaceColor.Primary, + isConstantItem = true, + ) + } + + State.ERROR -> { + AdditionalInfoItem( + icon = { + Icon( + imageVector = Icons.Outlined.SyncProblem, + contentDescription = resourceManager.getString(R.string.sync_error_title), + tint = AdditionalInfoItemColor.ERROR.color, + ) + }, + value = resourceManager.getString(R.string.sync_error_title), + color = AdditionalInfoItemColor.ERROR.color, + isConstantItem = true, + ) + } + + State.WARNING -> { + AdditionalInfoItem( + icon = { + Icon( + imageVector = Icons.Outlined.SyncProblem, + contentDescription = resourceManager.getString(R.string.sync_dialog_title_warning), + tint = AdditionalInfoItemColor.WARNING.color, + ) + }, + value = resourceManager.getString(R.string.sync_dialog_title_warning), + color = AdditionalInfoItemColor.WARNING.color, + isConstantItem = true, + ) + } + + else -> null + } + item?.let { list.add(it) } + } + + private fun checkViewOnly(list: MutableList, event: Event) { + val editable = d2.eventModule().eventService().blockingIsEditable(event.uid()) + if (!editable) { + list.add( + AdditionalInfoItem( + icon = { + Icon( + imageVector = Icons.Outlined.Visibility, + contentDescription = resourceManager.getString(R.string.view_only), + tint = AdditionalInfoItemColor.DISABLED.color, + ) + }, + value = resourceManager.getString(R.string.view_only), + isConstantItem = true, + color = AdditionalInfoItemColor.DISABLED.color, + ), + ) + } + } + + private fun getStage(programStageUid: String) = fromCache(cachedStages, programStageUid) { + d2.programModule().programStages() + .uid(programStageUid) + .blockingGet() + } + + private fun getProgram(programUid: String) = fromCache(cachedPrograms, programUid) { + d2.programModule().programs() + .uid(programUid) + .blockingGet() + } + + private fun getDisplayOrgUnit(programUid: String) = + fromCache(cachedDisplayOrgUnit, programUid) { + d2.organisationUnitModule().organisationUnits() + .byProgramUids(listOf(programUid)) + .blockingGet().size > 1 + } == true +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt index 0c58a25de5..51c13c95d7 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureActivity.kt @@ -1,60 +1,87 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventCapture +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.Looper -import android.view.MenuItem import android.view.View -import android.widget.PopupMenu +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.databinding.DataBindingUtil import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModelProvider +import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.commons.Constants +import org.dhis2.commons.animations.hide +import org.dhis2.commons.animations.show import org.dhis2.commons.dialogs.AlertBottomDialog import org.dhis2.commons.dialogs.CustomDialog import org.dhis2.commons.dialogs.DialogClickListener -import org.dhis2.commons.popupmenu.AppMenuHelper -import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.resources.EventResourcesProvider +import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext import org.dhis2.databinding.ActivityEventCaptureBinding import org.dhis2.form.model.EventMode -import org.dhis2.ui.ErrorFieldList +import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState import org.dhis2.ui.ThemeManager import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle.DiscardButton import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle.MainButton -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment.OnEditionListener -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCompletionDialog +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment.EventCaptureFormFragment import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponent import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponentProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsModule import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity import org.dhis2.usescases.general.ActivityGlobalAbstract +import org.dhis2.usescases.teiDashboard.DashboardViewModel import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.MapButtonObservable +import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataActivityContract +import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataFragment.Companion.newInstance +import org.dhis2.usescases.teiDashboard.ui.RelationshipTopBarIcon import org.dhis2.utils.analytics.CLICK import org.dhis2.utils.analytics.DELETE_EVENT import org.dhis2.utils.analytics.SHOW_HELP -import org.dhis2.utils.customviews.FormBottomDialog -import org.dhis2.utils.customviews.FormBottomDialog.Companion.instance +import org.dhis2.utils.customviews.MoreOptionsWithDropDownMenuButton +import org.dhis2.utils.customviews.navigationbar.NavigationPage import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.dhis2.utils.granularsync.OPEN_ERROR_LOCATION import org.dhis2.utils.granularsync.SyncStatusDialog import org.dhis2.utils.granularsync.shouldLaunchSyncDialog +import org.dhis2.utils.isLandscape +import org.dhis2.utils.isPortrait +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBar +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import javax.inject.Inject class EventCaptureActivity : ActivityGlobalAbstract(), EventCaptureContract.View, MapButtonObservable, - EventDetailsComponentProvider { + EventDetailsComponentProvider, + TEIDataActivityContract { private lateinit var binding: ActivityEventCaptureBinding @Inject @@ -68,57 +95,76 @@ class EventCaptureActivity : @Inject var themeManager: ThemeManager? = null private var isEventCompleted = false - private var eventMode: EventMode? = null + private lateinit var eventMode: EventMode @Inject - lateinit var resourceManager: ResourceManager + lateinit var eventResourcesProvider: EventResourcesProvider @JvmField var eventCaptureComponent: EventCaptureComponent? = null var programUid: String? = null var eventUid: String? = null + private var teiUid: String? = null + private var enrollmentUid: String? = null private val relationshipMapButton: LiveData = MutableLiveData(false) - private var onEditionListener: OnEditionListener? = null private var adapter: EventCapturePagerAdapter? = null + private var eventViewPager: ViewPager2? = null + private var dashboardViewModel: DashboardViewModel? = null override fun onCreate(savedInstanceState: Bundle?) { eventUid = intent.getStringExtra(Constants.EVENT_UID) - eventCaptureComponent = this.app().userComponent()!!.plus( - EventCaptureModule( - this, - eventUid, - ), - ) - eventCaptureComponent!!.inject(this) + programUid = intent.getStringExtra(Constants.PROGRAM_UID) + setUpEventCaptureComponent(eventUid!!) + teiUid = presenter.getTeiUid() + enrollmentUid = presenter.getEnrollmentUid() themeManager!!.setProgramTheme(intent.getStringExtra(Constants.PROGRAM_UID)!!) super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_event_capture) binding.presenter = presenter - eventMode = intent.getSerializableExtra(Constants.EVENT_MODE) as EventMode? + eventViewPager = when { + this.isLandscape() -> binding.eventViewLandPager + else -> binding.eventViewPager + } + eventMode = intent.getSerializableExtra(Constants.EVENT_MODE) as EventMode setUpViewPagerAdapter() setUpNavigationBar() + setupMoreOptionsMenu() + + setUpEventCaptureFormLandscape(eventUid ?: "") + if (this.isLandscape() && areTeiUidAndEnrollmentUidNotNull()) { + val viewModelFactory = this.app().dashboardComponent()?.dashboardViewModelFactory() + + viewModelFactory?.let { + dashboardViewModel = + ViewModelProvider(this, viewModelFactory)[DashboardViewModel::class.java] + supportFragmentManager.beginTransaction() + .replace(R.id.tei_column, newInstance(programUid, teiUid, enrollmentUid)) + .commit() + dashboardViewModel?.updateSelectedEventUid(eventUid) + } + } showProgress() presenter.initNoteCounter() presenter.init() - binding.syncButton.setOnClickListener { showSyncDialog() } + binding.syncButton.setOnClickListener { showSyncDialog(EVENT_SYNC) } if (intent.shouldLaunchSyncDialog()) { - showSyncDialog() + showSyncDialog(EVENT_SYNC) } } private fun setUpViewPagerAdapter() { - binding.eventViewPager.isUserInputEnabled = false + eventViewPager?.isUserInputEnabled = false adapter = EventCapturePagerAdapter( this, - intent.getStringExtra(Constants.PROGRAM_UID), - intent.getStringExtra(Constants.EVENT_UID), + intent.getStringExtra(Constants.PROGRAM_UID) ?: "", + intent.getStringExtra(Constants.EVENT_UID) ?: "", pageConfigurator!!.displayAnalytics(), pageConfigurator!!.displayRelationships(), intent.getBooleanExtra(OPEN_ERROR_LOCATION, false), eventMode, ) - binding.eventViewPager.adapter = adapter - binding.eventViewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() { + eventViewPager?.adapter = adapter + eventViewPager?.registerOnPageChangeCallback(object : OnPageChangeCallback() { override fun onPageSelected(position: Int) { super.onPageSelected(position) if (position == 0 && eventMode !== EventMode.NEW) { @@ -134,15 +180,84 @@ class EventCaptureActivity : } private fun setUpNavigationBar() { - binding.navigationBar.pageConfiguration(pageConfigurator!!) - binding.navigationBar.setOnItemSelectedListener { item: MenuItem -> - binding.eventViewPager.currentItem = adapter!!.getDynamicTabIndex(item.itemId) - true + eventViewPager?.registerOnPageChangeCallback( + object : OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + presenter.onSetNavigationPage(position) + } + }, + ) + binding.navigationBar.setContent { + DHIS2Theme { + val uiState by presenter.observeNavigationBarUIState() + val selectedItemIndex by remember(uiState) { + mutableIntStateOf( + uiState.items.indexOfFirst { + it.id == uiState.selectedItem + }, + ) + } + + AnimatedVisibility( + visible = uiState.items.isNotEmpty(), + enter = slideInVertically { it }, + exit = slideOutVertically { it }, + ) { + NavigationBar( + modifier = Modifier.fillMaxWidth(), + items = uiState.items, + selectedItemIndex = selectedItemIndex, + ) { page -> + presenter.onNavigationPageChanged(page) + eventViewPager?.currentItem = adapter!!.getDynamicTabIndex(page) + } + } + } + } + } + + private fun setUpEventCaptureFormLandscape(eventUid: String) { + if (this.isLandscape()) { + supportFragmentManager.beginTransaction() + .replace( + R.id.event_form, + EventCaptureFormFragment.newInstance(eventUid, false, eventMode), + ) + .commit() + } + } + + private fun setUpEventCaptureComponent(eventUid: String) { + eventCaptureComponent = app().userComponent()!!.plus( + EventCaptureModule( + this, + eventUid, + this.isPortrait(), + ), + ) + eventCaptureComponent!!.inject(this) + } + + private fun updateLandscapeViewsOnEventChange(newEventUid: String) { + if (newEventUid != this.eventUid) { + this.eventUid = newEventUid + setUpEventCaptureComponent(newEventUid) + setUpViewPagerAdapter() + setUpNavigationBar() + setUpEventCaptureFormLandscape(newEventUid) + showProgress() + presenter.initNoteCounter() + presenter.init() } } + private fun areTeiUidAndEnrollmentUidNotNull(): Boolean { + return teiUid != null && enrollmentUid != null + } + fun openDetails() { - binding.navigationBar.selectItemAt(0) + presenter.onNavigationPageChanged(NavigationPage.DETAILS) } fun openForm() { @@ -151,12 +266,16 @@ class EventCaptureActivity : it.dismiss() } } - binding.navigationBar.selectItemAt(1) + presenter.onNavigationPageChanged(NavigationPage.DATA_ENTRY) } override fun onResume() { super.onResume() presenter.refreshTabCounters() + with(dashboardViewModel) { + this?.selectedEventUid() + ?.observe(this@EventCaptureActivity, ::updateLandscapeViewsOnEventChange) + } } override fun onDestroy() { @@ -168,16 +287,14 @@ class EventCaptureActivity : onBackPressed() } + @SuppressLint("MissingSuperCall") @Deprecated("Deprecated in Java") override fun onBackPressed() { - if (onEditionListener != null) { - onEditionListener!!.onEditionListener() - } finishEditMode() } private fun finishEditMode() { - if (binding.navigationBar.isHidden()) { + if (binding.navigationBar.visibility == View.GONE) { showNavigationBar() } else { attemptFinish() @@ -209,7 +326,11 @@ class EventCaptureActivity : } private fun isFormScreen(): Boolean { - return adapter?.isFormScreenShown(binding.eventViewPager.currentItem) == true + return if (this.isPortrait()) { + adapter?.isFormScreenShown(binding.eventViewPager?.currentItem) == true + } else { + true + } } override fun updatePercentage(primaryValue: Float) { @@ -219,80 +340,17 @@ class EventCaptureActivity : } } - override fun showCompleteActions(eventCompletionDialog: EventCompletionDialog) { - if (binding.navigationBar.selectedItemId == R.id.navigation_data_entry) { - val dialog = BottomSheetDialog( - bottomSheetDialogUiModel = eventCompletionDialog.bottomSheetDialogUiModel, - onMainButtonClicked = { - setAction(eventCompletionDialog.mainButtonAction) - }, - onSecondaryButtonClicked = { - eventCompletionDialog.secondaryButtonAction?.let { setAction(it) } - }, - content = if (eventCompletionDialog.fieldsWithIssues.isNotEmpty()) { - { bottomSheetDialog -> - ErrorFieldList(eventCompletionDialog.fieldsWithIssues) { - bottomSheetDialog.dismiss() - } - } - } else { - null - }, - ) - dialog.show(supportFragmentManager, SHOW_OPTIONS) - } - } - override fun saveAndFinish() { displayMessage(getString(R.string.saved)) - setAction(FormBottomDialog.ActionType.FINISH) - } - - override fun attemptToSkip() { - instance - .setAccessDataWrite(presenter.canWrite()) - .setIsExpired(presenter.hasExpired()) - .setSkip(true) - .setListener { actionType: FormBottomDialog.ActionType -> setAction(actionType) } - .show(supportFragmentManager, SHOW_OPTIONS) - } - - override fun attemptToReschedule() { - instance - .setAccessDataWrite(presenter.canWrite()) - .setIsExpired(presenter.hasExpired()) - .setReschedule(true) - .setListener { actionType: FormBottomDialog.ActionType -> setAction(actionType) } - .show(supportFragmentManager, SHOW_OPTIONS) - } - - private fun setAction(actionType: FormBottomDialog.ActionType) { - when (actionType) { - FormBottomDialog.ActionType.COMPLETE -> { - isEventCompleted = true - presenter.completeEvent(false) - } - - FormBottomDialog.ActionType.COMPLETE_ADD_NEW -> presenter.completeEvent(true) - FormBottomDialog.ActionType.FINISH_ADD_NEW -> restartDataEntry() - FormBottomDialog.ActionType.SKIP -> presenter.skipEvent() - FormBottomDialog.ActionType.RESCHEDULE -> { // Do nothing - } - - FormBottomDialog.ActionType.CHECK_FIELDS -> { // Do nothing - } - - FormBottomDialog.ActionType.FINISH -> finishDataEntry() - FormBottomDialog.ActionType.NONE -> { // Do nothing - } - } + finishDataEntry() } override fun showSnackBar(messageId: Int, programStage: String) { showToast( - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( messageId, programStage, + programUid, ), ) } @@ -320,28 +378,47 @@ class EventCaptureActivity : binding.programStageName.text = stageName } - override fun showMoreOptions(view: View) { - AppMenuHelper.Builder().menu(this, R.menu.event_menu).anchor(view) - .onMenuInflated { popupMenu: PopupMenu -> - popupMenu.menu.findItem(R.id.menu_delete).isVisible = - presenter.canWrite() && presenter.isEnrollmentOpen() - popupMenu.menu.findItem(R.id.menu_share).isVisible = false - } - .onMenuItemClicked { itemId: Int? -> + private fun setupMoreOptionsMenu() { + binding.moreOptions.setContent { + var expanded by remember { mutableStateOf(false) } + + MoreOptionsWithDropDownMenuButton( + getMenuItems(), + expanded, + onMenuToggle = { expanded = it }, + ) { itemId -> when (itemId) { - R.id.showHelp -> { + EventCaptureMenuItem.SHOW_HELP -> { analyticsHelper().setEvent(SHOW_HELP, CLICK, SHOW_HELP) showTutorial(false) } - R.id.menu_delete -> confirmDeleteEvent() - else -> { // Do nothing - } + EventCaptureMenuItem.DELETE -> confirmDeleteEvent() } - false } - .build() - .show() + } + } + + private fun getMenuItems(): List> { + return buildList { + add( + MenuItemData( + id = EventCaptureMenuItem.SHOW_HELP, + label = getString(R.string.showHelp), + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.HelpOutline), + ), + ) + if (presenter.canWrite() && presenter.isEnrollmentOpen()) { + add( + MenuItemData( + id = EventCaptureMenuItem.DELETE, + label = getString(R.string.delete), + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteForever), + ), + ) + } + } } override fun showTutorial(shaked: Boolean) { @@ -352,13 +429,15 @@ class EventCaptureActivity : presenter.programStage().let { CustomDialog( this, - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.delete_event_label, programStageUid = it, + programUid, ), - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.confirm_delete_event_label, programStageUid = it, + programUid, ), getString(R.string.delete), getString(R.string.cancel), @@ -381,21 +460,24 @@ class EventCaptureActivity : MaterialAlertDialogBuilder(this, R.style.DhisMaterialDialog) .setTitle(R.string.conflict) .setMessage( - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.event_label_date_in_future_message, programStageUid = presenter.programStage(), + programUid = programUid, ), ) .setPositiveButton( R.string.change_event_date, - ) { _, _ -> binding.navigationBar.selectItemAt(0) } + ) { _, _ -> + presenter.onSetNavigationPage(0) + } .setNegativeButton(R.string.go_back) { _, _ -> back() } .setCancelable(false) .show() } override fun updateNoteBadge(numberOfNotes: Int) { - binding.navigationBar.updateBadge(R.id.navigation_notes, numberOfNotes) + presenter.updateNotesBadge(numberOfNotes) } override fun showProgress() { @@ -425,31 +507,89 @@ class EventCaptureActivity : // there are no relationships on events } - fun setFormEditionListener(onEditionListener: OnEditionListener?) { - this.onEditionListener = onEditionListener + override fun updateRelationshipsTopBarIconState(topBarIconState: RelationshipTopBarIconState) { + when (topBarIconState) { + is RelationshipTopBarIconState.Selecting -> { + binding.relationshipIcon.visibility = View.VISIBLE + binding.relationshipIcon.setContent { + RelationshipTopBarIcon( + relationshipTopBarIconState = topBarIconState, + ) { + topBarIconState.onClickListener() + } + } + } + + else -> { + binding.relationshipIcon.visibility = View.GONE + } + } } override fun provideEventDetailsComponent(module: EventDetailsModule?): EventDetailsComponent? { return eventCaptureComponent!!.plus(module) } - private fun showSyncDialog() { - SyncStatusDialog.Builder() - .withContext(this) - .withSyncContext(SyncContext.Event(eventUid!!)) - .onNoConnectionListener { - val contextView = findViewById(R.id.navigationBar) - Snackbar.make( - contextView, - R.string.sync_offline_check_connection, - Snackbar.LENGTH_SHORT, - ).show() - } - .show("EVENT_SYNC") + private fun showSyncDialog(syncType: String) { + val syncContext = when (syncType) { + TEI_SYNC -> enrollmentUid?.let { SyncContext.Enrollment(it) } + EVENT_SYNC -> SyncContext.Event(eventUid!!) + else -> null + } + + syncContext?.let { + SyncStatusDialog.Builder() + .withContext(this) + .withSyncContext(it) + .onDismissListener(object : OnDismissListener { + override fun onDismiss(hasChanged: Boolean) { + if (hasChanged && syncType == TEI_SYNC) { + dashboardViewModel?.updateDashboard() + } + } + }) + .onNoConnectionListener { + val contextView = findViewById(R.id.navigationBar) + Snackbar.make( + contextView, + R.string.sync_offline_check_connection, + Snackbar.LENGTH_SHORT, + ).show() + } + .show(syncType) + } + } + + override fun openSyncDialog() { + showSyncDialog(TEI_SYNC) + } + + override fun finishActivity() { + finish() + } + + override fun restoreAdapter(programUid: String, teiUid: String, enrollmentUid: String) { + // we do not restore adapter in events + } + + override fun executeOnUIThread() { + activity.runOnUiThread { + showDescription(getString(R.string.error_applying_rule_effects)) + } + } + + override fun getContext(): Context { + return this + } + + override fun activityTeiUid(): String? { + return teiUid } companion object { private const val SHOW_OPTIONS = "SHOW_OPTIONS" + private const val TEI_SYNC = "SYNC_TEI" + private const val EVENT_SYNC = "EVENT_SYNC" @JvmStatic fun getActivityBundle(eventUid: String, programUid: String, eventMode: EventMode): Bundle { @@ -474,3 +614,8 @@ class EventCaptureActivity : } } } + +enum class EventCaptureMenuItem { + SHOW_HELP, + DELETE, +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.kt index 91e9fff76c..9a20fc0968 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureContract.kt @@ -1,13 +1,13 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventCapture +import androidx.compose.runtime.State import androidx.lifecycle.LiveData import io.reactivex.Flowable import io.reactivex.Observable import io.reactivex.Single -import org.dhis2.form.model.EventMode -import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCompletionDialog +import org.dhis2.tracker.NavigationBarUIState import org.dhis2.usescases.general.AbstractActivityContracts +import org.dhis2.utils.customviews.navigationbar.NavigationPage import org.hisp.dhis.android.core.common.ValidationStrategy import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.organisationunit.OrganisationUnit @@ -18,14 +18,10 @@ class EventCaptureContract { fun renderInitialInfo(stageName: String) val presenter: Presenter fun updatePercentage(primaryValue: Float) - fun showCompleteActions(eventCompletionDialog: EventCompletionDialog) - fun restartDataEntry() fun finishDataEntry() fun saveAndFinish() fun showSnackBar(messageId: Int, programStage: String) - fun attemptToSkip() - fun attemptToReschedule() fun showEventIntegrityAlert() fun updateNoteBadge(numberOfNotes: Int) fun goBack() @@ -39,15 +35,8 @@ class EventCaptureContract { fun observeActions(): LiveData fun init() fun onBackClick() - fun attemptFinish( - canComplete: Boolean, - onCompleteMessage: String?, - errorFields: List, - emptyMandatoryFields: Map, - warningFields: List, - eventMode: EventMode? = null, - ) + fun saveAndExit(eventStatus: EventStatus?) fun isEnrollmentOpen(): Boolean fun completeEvent(addNew: Boolean) fun deleteEvent() @@ -62,6 +51,13 @@ class EventCaptureContract { fun getCompletionPercentageVisibility(): Boolean fun emitAction(onBack: EventCaptureAction) fun programStage(): String + fun getTeiUid(): String? + fun getEnrollmentUid(): String? + fun observeNavigationBarUIState(): State> + fun onNavigationPageChanged(page: NavigationPage) + fun onSetNavigationPage(index: Int) + fun isDataEntrySelected(): Boolean + fun updateNotesBadge(numberOfNotes: Int) } interface EventCaptureRepository { @@ -85,5 +81,7 @@ class EventCaptureContract { fun hasAnalytics(): Boolean fun hasRelationships(): Boolean fun validationStrategy(): ValidationStrategy + fun getTeiUid(): String? + fun getEnrollmentUid(): String? } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.java deleted file mode 100644 index c7b9f31d8d..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import org.dhis2.R; -import org.dhis2.commons.data.EntryMode; -import org.dhis2.commons.di.dagger.PerActivity; -import org.dhis2.commons.network.NetworkUtils; -import org.dhis2.commons.prefs.PreferenceProvider; -import org.dhis2.commons.reporting.CrashReportController; -import org.dhis2.commons.reporting.CrashReportControllerImpl; -import org.dhis2.commons.resources.ResourceManager; -import org.dhis2.commons.schedulers.SchedulerProvider; -import org.dhis2.data.dhislogic.DhisEnrollmentUtils; -import org.dhis2.data.forms.EventRepository; -import org.dhis2.data.forms.FormRepository; -import org.dhis2.data.forms.dataentry.SearchTEIRepository; -import org.dhis2.data.forms.dataentry.SearchTEIRepositoryImpl; -import org.dhis2.mobileProgramRules.EvaluationType; -import org.dhis2.mobileProgramRules.RuleEngineHelper; -import org.dhis2.form.data.FileController; -import org.dhis2.form.data.FormValueStore; -import org.dhis2.form.data.RulesRepository; -import org.dhis2.form.data.UniqueAttributeController; -import org.dhis2.form.model.RowAction; -import org.dhis2.form.ui.FieldViewModelFactory; -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ConfigureEventCompletionDialog; -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.provider.EventCaptureResourcesProvider; -import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator; -import org.hisp.dhis.android.core.D2; - -import dagger.Module; -import dagger.Provides; -import io.reactivex.processors.FlowableProcessor; -import io.reactivex.processors.PublishProcessor; - -@Module -public class EventCaptureModule { - - private final String eventUid; - private final EventCaptureContract.View view; - - public EventCaptureModule(EventCaptureContract.View view, String eventUid) { - this.view = view; - this.eventUid = eventUid; - } - - @Provides - @PerActivity - EventCaptureContract.Presenter providePresenter(@NonNull EventCaptureContract.EventCaptureRepository eventCaptureRepository, - SchedulerProvider schedulerProvider, - PreferenceProvider preferences, - ConfigureEventCompletionDialog configureEventCompletionDialog) { - return new EventCapturePresenterImpl( - view, - eventUid, - eventCaptureRepository, - schedulerProvider, - preferences, - configureEventCompletionDialog); - } - - @Provides - @PerActivity - EventFieldMapper provideFieldMapper(Context context, FieldViewModelFactory fieldFactory) { - return new EventFieldMapper(fieldFactory, context.getString(R.string.field_is_mandatory)); - } - - @Provides - @PerActivity - EventCaptureContract.EventCaptureRepository provideRepository(D2 d2) { - return new EventCaptureRepositoryImpl(eventUid, d2); - } - - @Provides - @PerActivity - RulesRepository rulesRepository(@NonNull D2 d2) { - return new RulesRepository(d2); - } - - @Provides - @PerActivity - RuleEngineHelper ruleEngineRepository(D2 d2) { - if(eventUid == null) return null; - return new RuleEngineHelper( - new EvaluationType.Event(eventUid), - new org.dhis2.mobileProgramRules.RulesRepository(d2) - ); - } - - @Provides - @PerActivity - FormRepository formRepository(@NonNull RulesRepository rulesRepository, - @NonNull D2 d2) { - return new EventRepository(rulesRepository, eventUid, d2); - } - - @Provides - @PerActivity - FormValueStore valueStore( - @NonNull D2 d2, - CrashReportController crashReportController, - NetworkUtils networkUtils, - ResourceManager resourceManager, - FileController fileController, - UniqueAttributeController uniqueAttributeController - ) { - return new FormValueStore( - d2, - eventUid, - EntryMode.DE, - null, - null, - crashReportController, - networkUtils, - resourceManager, - fileController, - uniqueAttributeController - ); - } - - @Provides - @PerActivity - SearchTEIRepository searchTEIRepository(D2 d2) { - return new SearchTEIRepositoryImpl(d2, new DhisEnrollmentUtils(d2), new CrashReportControllerImpl()); - } - - @Provides - @PerActivity - FlowableProcessor getProcessor() { - return PublishProcessor.create(); - } - - @Provides - @PerActivity - NavigationPageConfigurator pageConfigurator( - EventCaptureContract.EventCaptureRepository repository - ) { - return new EventPageConfigurator(repository); - } - - @Provides - @PerActivity - ConfigureEventCompletionDialog provideConfigureEventCompletionDialog( - EventCaptureResourcesProvider eventCaptureResourcesProvider - ) { - return new ConfigureEventCompletionDialog(eventCaptureResourcesProvider); - } - - @Provides - @PerActivity - EventCaptureResourcesProvider provideEventCaptureResourcesProvider( - ResourceManager resourceManager - ) { - return new EventCaptureResourcesProvider(resourceManager); - } -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.kt new file mode 100644 index 0000000000..8c3ef9e74f --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureModule.kt @@ -0,0 +1,124 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventCapture + +import android.content.Context +import dagger.Module +import dagger.Provides +import io.reactivex.processors.FlowableProcessor +import io.reactivex.processors.PublishProcessor +import org.dhis2.R +import org.dhis2.commons.data.EntryMode +import org.dhis2.commons.di.dagger.PerActivity +import org.dhis2.commons.network.NetworkUtils +import org.dhis2.commons.prefs.PreferenceProvider +import org.dhis2.commons.reporting.CrashReportController +import org.dhis2.commons.reporting.CrashReportControllerImpl +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.data.dhislogic.DhisEnrollmentUtils +import org.dhis2.data.forms.dataentry.SearchTEIRepository +import org.dhis2.data.forms.dataentry.SearchTEIRepositoryImpl +import org.dhis2.form.data.FileController +import org.dhis2.form.data.FormValueStore +import org.dhis2.form.data.UniqueAttributeController +import org.dhis2.form.model.RowAction +import org.dhis2.form.ui.FieldViewModelFactory +import org.dhis2.mobileProgramRules.EvaluationType +import org.dhis2.mobileProgramRules.RuleEngineHelper +import org.dhis2.mobileProgramRules.RulesRepository +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureContract.EventCaptureRepository +import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator +import org.hisp.dhis.android.core.D2 + +@Module +class EventCaptureModule( + private val view: EventCaptureContract.View, + private val eventUid: String, + private val isPortrait: Boolean, +) { + @Provides + @PerActivity + fun providePresenter( + eventCaptureRepository: EventCaptureRepository, + schedulerProvider: SchedulerProvider, + preferences: PreferenceProvider, + pageConfigurator: NavigationPageConfigurator, + resourceManager: ResourceManager, + ): EventCaptureContract.Presenter { + return EventCapturePresenterImpl( + view, + eventUid, + eventCaptureRepository, + schedulerProvider, + preferences, + pageConfigurator, + resourceManager, + ) + } + + @Provides + @PerActivity + fun provideFieldMapper( + context: Context, + fieldFactory: FieldViewModelFactory, + ): EventFieldMapper { + return EventFieldMapper(fieldFactory, context.getString(R.string.field_is_mandatory)) + } + + @Provides + @PerActivity + fun provideRepository(d2: D2?): EventCaptureRepository { + return EventCaptureRepositoryImpl(eventUid, d2) + } + + @Provides + @PerActivity + fun ruleEngineRepository(d2: D2): RuleEngineHelper { + return RuleEngineHelper( + EvaluationType.Event(eventUid), + RulesRepository(d2), + ) + } + + @Provides + @PerActivity + fun valueStore( + d2: D2, + crashReportController: CrashReportController, + networkUtils: NetworkUtils, + resourceManager: ResourceManager, + fileController: FileController, + uniqueAttributeController: UniqueAttributeController, + ): FormValueStore { + return FormValueStore( + d2, + eventUid, + EntryMode.DE, + null, + null, + crashReportController, + networkUtils, + resourceManager, + fileController, + uniqueAttributeController, + ) + } + + @Provides + @PerActivity + fun searchTEIRepository(d2: D2): SearchTEIRepository { + return SearchTEIRepositoryImpl(d2, DhisEnrollmentUtils(d2), CrashReportControllerImpl()) + } + + @get:PerActivity + @get:Provides + val processor: FlowableProcessor + get() = PublishProcessor.create() + + @Provides + @PerActivity + fun pageConfigurator( + repository: EventCaptureRepository, + ): NavigationPageConfigurator { + return EventPageConfigurator(repository, isPortrait) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.java deleted file mode 100644 index e8e044d83e..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture; - -import static org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.IndicatorsFragmentKt.VISUALIZATION_TYPE; - -import android.os.Bundle; - -import androidx.annotation.IntegerRes; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; - -import org.dhis2.R; -import org.dhis2.form.model.EventMode; -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment.EventCaptureFormFragment; -import org.dhis2.usescases.notes.NotesFragment; -import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.IndicatorsFragment; -import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.VisualizationType; -import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipFragment; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.List; - -public class EventCapturePagerAdapter extends FragmentStateAdapter { - - private final String programUid; - private final String eventUid; - private final List pages; - - private final boolean shouldOpenErrorSection; - - private final EventMode eventMode; - - public boolean isFormScreenShown(@Nullable Integer currentItem) { - return currentItem != null && pages.get(currentItem) == EventPageType.DATA_ENTRY; - } - - private enum EventPageType { - DATA_ENTRY, ANALYTICS, RELATIONSHIPS, NOTES - } - - public EventCapturePagerAdapter(FragmentActivity fragmentActivity, - String programUid, - String eventUid, - boolean displayAnalyticScreen, - boolean displayRelationshipScreen, - boolean openErrorSection, - EventMode eventMode - - ) { - super(fragmentActivity); - this.programUid = programUid; - this.eventUid = eventUid; - this.shouldOpenErrorSection = openErrorSection; - this.eventMode = eventMode; - pages = new ArrayList<>(); - pages.add(EventPageType.DATA_ENTRY); - - if (displayAnalyticScreen) { - pages.add(EventPageType.ANALYTICS); - } - - if (displayRelationshipScreen) { - pages.add(EventPageType.RELATIONSHIPS); - } - pages.add(EventPageType.NOTES); - } - - public int getDynamicTabIndex(@IntegerRes int tabClicked) { - if (tabClicked == R.id.navigation_data_entry) { - return pages.indexOf(EventPageType.DATA_ENTRY); - } else if (tabClicked == R.id.navigation_analytics) { - return pages.indexOf(EventPageType.ANALYTICS); - } else if (tabClicked == R.id.navigation_relationships) { - return pages.indexOf(EventPageType.RELATIONSHIPS); - } else if (tabClicked == R.id.navigation_notes) { - return pages.indexOf(EventPageType.NOTES); - } - return 0; - } - - @NonNull - @Override - public Fragment createFragment(int position) { - switch (pages.get(position)) { - default: - case DATA_ENTRY: - return EventCaptureFormFragment.newInstance( - eventUid, - shouldOpenErrorSection, - eventMode - ); - case ANALYTICS: - Fragment indicatorFragment = new IndicatorsFragment(); - Bundle arguments = new Bundle(); - arguments.putString(VISUALIZATION_TYPE, VisualizationType.EVENTS.name()); - indicatorFragment.setArguments(arguments); - return indicatorFragment; - case RELATIONSHIPS: - Fragment relationshipFragment = new RelationshipFragment(); - relationshipFragment.setArguments( - RelationshipFragment.withArguments(programUid, - null, - null, - eventUid - ) - ); - return relationshipFragment; - case NOTES: - return NotesFragment.newEventInstance(programUid, eventUid); - } - } - - @Override - public int getItemCount() { - return pages.size(); - } -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt new file mode 100644 index 0000000000..e39e99f4ef --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePagerAdapter.kt @@ -0,0 +1,129 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventCapture + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import org.dhis2.form.model.EventMode +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment.EventCaptureFormFragment +import org.dhis2.usescases.notes.NotesFragment.Companion.newEventInstance +import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.IndicatorsFragment +import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.VISUALIZATION_TYPE +import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.VisualizationType +import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipFragment +import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipFragment.Companion.withArguments +import org.dhis2.utils.customviews.navigationbar.NavigationPage + +class EventCapturePagerAdapter( + private val fragmentActivity: FragmentActivity, + private val programUid: String, + private val eventUid: String, + displayAnalyticScreen: Boolean, + displayRelationshipScreen: Boolean, + private val shouldOpenErrorSection: Boolean, + private val eventMode: EventMode, + +) : FragmentStateAdapter(fragmentActivity) { + private val landscapePages: MutableList = ArrayList() + private val portraitPages: MutableList = ArrayList() + + fun isFormScreenShown(currentItem: Int?): Boolean { + return currentItem != null && portraitPages[currentItem] == EventPageType.DATA_ENTRY + } + + private enum class EventPageType { + DATA_ENTRY, ANALYTICS, RELATIONSHIPS, NOTES + } + + init { + + portraitPages.add(EventPageType.DATA_ENTRY) + + if (displayAnalyticScreen) { + portraitPages.add(EventPageType.ANALYTICS) + landscapePages.add(EventPageType.ANALYTICS) + } + + if (displayRelationshipScreen) { + portraitPages.add(EventPageType.RELATIONSHIPS) + landscapePages.add(EventPageType.RELATIONSHIPS) + } + portraitPages.add(EventPageType.NOTES) + landscapePages.add(EventPageType.NOTES) + } + + override fun createFragment(position: Int): Fragment { + return createFragmentForPage( + if (isPortrait) portraitPages[position] else landscapePages[position], + ) + } + + private fun createFragmentForPage(pageType: EventPageType): Fragment { + return when (pageType) { + EventPageType.ANALYTICS -> { + val indicatorFragment: Fragment = IndicatorsFragment() + val arguments = Bundle() + arguments.putString(VISUALIZATION_TYPE, VisualizationType.EVENTS.name) + indicatorFragment.arguments = arguments + indicatorFragment + } + + EventPageType.RELATIONSHIPS -> { + val relationshipFragment: Fragment = RelationshipFragment() + relationshipFragment.arguments = withArguments( + programUid, + null, + null, + eventUid, + ) + relationshipFragment + } + + EventPageType.NOTES -> { + newEventInstance(programUid, eventUid) + } + + else -> { + EventCaptureFormFragment.newInstance( + eventUid, + shouldOpenErrorSection, + eventMode, + ) + } + } + } + + fun getDynamicTabIndex(navigationPage: NavigationPage?): Int { + val pageType = when (navigationPage) { + NavigationPage.ANALYTICS -> EventPageType.ANALYTICS + NavigationPage.RELATIONSHIPS -> EventPageType.RELATIONSHIPS + NavigationPage.NOTES -> EventPageType.NOTES + else -> null + } + + return if (pageType != null) { + if (isPortrait) { + portraitPages.indexOf(pageType) + } else { + landscapePages.indexOf(pageType) + } + } else { + NO_POSITION + } + } + + override fun getItemCount(): Int { + return if (isPortrait) { + portraitPages.size + } else { + landscapePages.size + } + } + + val isPortrait: Boolean + get() = fragmentActivity.resources.configuration.orientation == 1 + + companion object { + const val NO_POSITION: Int = -1 + } +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt index 5ce1b1f769..55231f182a 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCapturePresenterImpl.kt @@ -1,26 +1,40 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventCapture +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.StickyNote2 +import androidx.compose.material.icons.automirrored.outlined.StickyNote2 +import androidx.compose.material.icons.filled.BarChart +import androidx.compose.material.icons.filled.Hub +import androidx.compose.material.icons.outlined.BarChart +import androidx.compose.material.icons.outlined.Hub +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import io.reactivex.Flowable import io.reactivex.disposables.CompositeDisposable import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.launch import org.dhis2.R import org.dhis2.commons.prefs.Preference import org.dhis2.commons.prefs.PreferenceProvider +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.schedulers.defaultSubscribe -import org.dhis2.form.data.EventRepository -import org.dhis2.form.model.EventMode -import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue +import org.dhis2.tracker.NavigationBarUIState +import org.dhis2.ui.icons.DHIS2Icons +import org.dhis2.ui.icons.DataEntryFilled +import org.dhis2.ui.icons.DataEntryOutline import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureContract.EventCaptureRepository -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ConfigureEventCompletionDialog import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCaptureInitialInfo +import org.dhis2.utils.customviews.navigationbar.NavigationPage +import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.hisp.dhis.android.core.common.Unit -import org.hisp.dhis.android.core.common.ValidationStrategy import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBarItem import timber.log.Timber import java.util.Date @@ -30,7 +44,8 @@ class EventCapturePresenterImpl( private val eventCaptureRepository: EventCaptureRepository, private val schedulerProvider: SchedulerProvider, private val preferences: PreferenceProvider, - private val configureEventCompletionDialog: ConfigureEventCompletionDialog, + private val pageConfigurator: NavigationPageConfigurator, + private val resourceManager: ResourceManager, ) : ViewModel(), EventCaptureContract.Presenter { var compositeDisposable: CompositeDisposable = CompositeDisposable() @@ -39,6 +54,12 @@ class EventCapturePresenterImpl( val actions = MutableLiveData() + private val _navigationBarUIState = mutableStateOf(NavigationBarUIState()) + + override fun observeNavigationBarUIState(): State> { + return _navigationBarUIState + } + override fun observeActions(): LiveData = actions override fun emitAction(onBack: EventCaptureAction) { @@ -75,6 +96,104 @@ class EventCapturePresenterImpl( ), ) checkExpiration() + + viewModelScope.launch { + loadBottomBarItems() + } + } + + private fun loadBottomBarItems() { + val navItems = mutableListOf>() + + if (pageConfigurator.displayDataEntry()) { + navItems.add( + NavigationBarItem( + id = NavigationPage.DATA_ENTRY, + icon = DHIS2Icons.DataEntryOutline, + selectedIcon = DHIS2Icons.DataEntryFilled, + label = resourceManager.getString(R.string.navigation_form), + ), + ) + } + + if (pageConfigurator.displayAnalytics()) { + navItems.add( + NavigationBarItem( + id = NavigationPage.ANALYTICS, + icon = Icons.Outlined.BarChart, + selectedIcon = Icons.Filled.BarChart, + label = resourceManager.getString(R.string.navigation_charts), + ), + ) + } + + if (pageConfigurator.displayRelationships()) { + navItems.add( + NavigationBarItem( + id = NavigationPage.RELATIONSHIPS, + icon = Icons.Outlined.Hub, + selectedIcon = Icons.Filled.Hub, + label = resourceManager.getString(R.string.navigation_relations), + ), + ) + } + + if (pageConfigurator.displayNotes()) { + navItems.add( + NavigationBarItem( + id = NavigationPage.NOTES, + icon = Icons.AutoMirrored.Outlined.StickyNote2, + selectedIcon = Icons.AutoMirrored.Filled.StickyNote2, + label = resourceManager.getString(R.string.navigation_notes), + ), + ) + } + + _navigationBarUIState.value = _navigationBarUIState.value.copy( + items = navItems.takeIf { it.size > 1 }.orEmpty(), + ) + } + + override fun onNavigationPageChanged(page: NavigationPage) { + _navigationBarUIState.value = _navigationBarUIState.value.copy(selectedItem = page) + } + + override fun onSetNavigationPage(index: Int) { + val navigationPageAtIndex = _navigationBarUIState + .value + .items + .getOrNull(index) + ?.id + + if (navigationPageAtIndex != null) { + onNavigationPageChanged(navigationPageAtIndex) + } + } + + override fun isDataEntrySelected(): Boolean { + return _navigationBarUIState.value.selectedItem == NavigationPage.DATA_ENTRY + } + + override fun updateNotesBadge(numberOfNotes: Int) { + val navigationBarUIState = _navigationBarUIState.value + val indexOfNotesNavigationItem = navigationBarUIState + .items + .indexOfFirst { it.id == NavigationPage.NOTES } + + val notesNavigationItem = navigationBarUIState + .items + .getOrNull(indexOfNotesNavigationItem) + + if (notesNavigationItem != null) { + val updatedList = navigationBarUIState.items.toMutableList() + updatedList[indexOfNotesNavigationItem] = notesNavigationItem.copy( + showBadge = numberOfNotes > 0, + ) + + _navigationBarUIState.value = _navigationBarUIState.value.copy( + items = updatedList, + ) + } } private fun checkExpiration() { @@ -105,77 +224,7 @@ class EventCapturePresenterImpl( view.goBack() } - override fun attemptFinish( - canComplete: Boolean, - onCompleteMessage: String?, - errorFields: List, - emptyMandatoryFields: Map, - warningFields: List, - eventMode: EventMode?, - ) { - when (eventStatus) { - EventStatus.ACTIVE, EventStatus.COMPLETED -> { - var canSkipErrorFix = canSkipErrorFix( - hasErrorFields = errorFields.isNotEmpty(), - hasEmptyMandatoryFields = emptyMandatoryFields.isNotEmpty(), - hasEmptyEventCreationMandatoryFields = with(emptyMandatoryFields) { - containsValue(EventRepository.EVENT_DETAILS_SECTION_UID) || - containsValue(EventRepository.EVENT_CATEGORY_COMBO_SECTION_UID) - }, - eventMode = eventMode, - validationStrategy = eventCaptureRepository.validationStrategy(), - ) - if (eventStatus == EventStatus.COMPLETED) canSkipErrorFix = false - val eventCompletionDialog = configureEventCompletionDialog.invoke( - errorFields, - emptyMandatoryFields, - warningFields, - canComplete, - onCompleteMessage, - canSkipErrorFix, - eventStatus, - ) - - if (eventStatus == EventStatus.COMPLETED && eventCompletionDialog.fieldsWithIssues.isEmpty()) { - finishCompletedEvent() - } else { - view.showCompleteActions(eventCompletionDialog) - } - } - else -> { - setUpActionByStatus(eventStatus) - } - } - view.showNavigationBar() - } - - private fun canSkipErrorFix( - hasErrorFields: Boolean, - hasEmptyMandatoryFields: Boolean, - hasEmptyEventCreationMandatoryFields: Boolean, - eventMode: EventMode?, - validationStrategy: ValidationStrategy, - ): Boolean { - return when (validationStrategy) { - ValidationStrategy.ON_COMPLETE -> when (eventMode) { - EventMode.NEW -> !hasEmptyEventCreationMandatoryFields - else -> true - } - ValidationStrategy.ON_UPDATE_AND_INSERT -> !hasErrorFields && !hasEmptyMandatoryFields - } - } - - private fun setUpActionByStatus(eventStatus: EventStatus) { - when (eventStatus) { - EventStatus.OVERDUE -> view.attemptToSkip() - EventStatus.SKIPPED -> view.attemptToReschedule() - else -> { - // No actions for the remaining cases - } - } - } - - private fun finishCompletedEvent() { + override fun saveAndExit(eventStatus: EventStatus?) { if (!hasExpired && !eventCaptureRepository.isEnrollmentCancelled) { view.saveAndFinish() } else { @@ -310,4 +359,12 @@ class EventCapturePresenterImpl( get() = eventCaptureRepository.eventStatus().blockingFirst() override fun programStage(): String = eventCaptureRepository.programStage().blockingFirst() + + override fun getEnrollmentUid(): String? { + return eventCaptureRepository.getEnrollmentUid() + } + + override fun getTeiUid(): String? { + return eventCaptureRepository.getTeiUid() + } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java index a27e7d0aeb..d0bb92f2a7 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventCaptureRepositoryImpl.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Objects; +import javax.annotation.Nullable; + import io.reactivex.Flowable; import io.reactivex.Observable; import io.reactivex.Single; @@ -158,7 +160,8 @@ public Flowable eventIntegrityCheck() { Event currentEvent = getCurrentEvent(); return Flowable.just(currentEvent).map(event -> (event.status() == EventStatus.COMPLETED || - event.status() == EventStatus.ACTIVE) && + event.status() == EventStatus.ACTIVE || + event.status() == EventStatus.SKIPPED) && (event.eventDate() == null || !event.eventDate().after(new Date())) ); } @@ -216,5 +219,18 @@ public ValidationStrategy validationStrategy() { return validationStrategy != null ? validationStrategy : ValidationStrategy.ON_COMPLETE; } + + @Override + @Nullable + public String getEnrollmentUid() { + return getCurrentEvent().enrollment(); + } + + @Override + @Nullable + public String getTeiUid() { + Enrollment enrollment = d2.enrollmentModule().enrollments().uid(getEnrollmentUid()).blockingGet(); + return enrollment != null ? enrollment.trackedEntityInstance() : null; + } } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt index ed3d15822f..2f4a9dcdc8 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/EventPageConfigurator.kt @@ -4,13 +4,14 @@ import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator class EventPageConfigurator( private val eventCaptureRepository: EventCaptureContract.EventCaptureRepository, + val isPortrait: Boolean, ) : NavigationPageConfigurator { override fun displayDetails(): Boolean { return true } override fun displayDataEntry(): Boolean { - return true + return isPortrait } override fun displayAnalytics(): Boolean { diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt deleted file mode 100644 index a4b1869d06..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/domain/ConfigureEventCompletionDialog.kt +++ /dev/null @@ -1,182 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain - -import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel -import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle.CompleteButton -import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle.MainButton -import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle.SecondaryButton -import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue -import org.dhis2.ui.dialogs.bottomsheet.IssueType -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ConfigureEventCompletionDialog.DialogType.COMPLETE_ERROR -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ConfigureEventCompletionDialog.DialogType.ERROR -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ConfigureEventCompletionDialog.DialogType.MANDATORY -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ConfigureEventCompletionDialog.DialogType.SUCCESSFUL -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ConfigureEventCompletionDialog.DialogType.WARNING -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCompletionButtons -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model.EventCompletionDialog -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.provider.EventCaptureResourcesProvider -import org.dhis2.utils.customviews.FormBottomDialog -import org.hisp.dhis.android.core.event.EventStatus - -class ConfigureEventCompletionDialog( - val provider: EventCaptureResourcesProvider, -) { - - operator fun invoke( - errorFields: List, - mandatoryFields: Map, - warningFields: List, - canComplete: Boolean, - onCompleteMessage: String?, - canSkipErrorFix: Boolean, - eventState: EventStatus, - ): EventCompletionDialog { - val dialogType = getDialogType( - errorFields, - mandatoryFields, - warningFields, - !canComplete && onCompleteMessage != null, - ) - val mainButton = getMainButton(dialogType, eventState) - val secondaryButton = if (canSkipErrorFix || dialogType == WARNING) { - EventCompletionButtons( - SecondaryButton(provider.provideNotNow()), - FormBottomDialog.ActionType.FINISH, - ) - } else { - null - } - val bottomSheetDialogUiModel = BottomSheetDialogUiModel( - title = getTitle(dialogType), - message = getSubtitle(dialogType, eventState), - iconResource = getIcon(dialogType), - mainButton = mainButton.buttonStyle, - secondaryButton = secondaryButton?.buttonStyle, - ) - - return EventCompletionDialog( - bottomSheetDialogUiModel = bottomSheetDialogUiModel, - mainButtonAction = mainButton.action, - secondaryButtonAction = secondaryButton?.action, - fieldsWithIssues = getFieldsWithIssues( - errorFields = errorFields, - mandatoryFields = mandatoryFields.keys.toList(), - warningFields = warningFields, - onCompleteField = getOnCompleteMessage(canComplete, onCompleteMessage), - ), - ) - } - - private fun getTitle(type: DialogType) = when (type) { - ERROR -> provider.provideNotSavedText() - else -> provider.provideSavedText() - } - - private fun getSubtitle(type: DialogType, eventState: EventStatus) = when (type) { - ERROR -> provider.provideErrorInfo() - MANDATORY -> provider.provideMandatoryInfo() - WARNING -> if (eventState == EventStatus.COMPLETED) provider.provideWarningInfoCompletedEvent() else provider.provideWarningInfo() - SUCCESSFUL -> provider.provideCompleteInfo() - COMPLETE_ERROR -> provider.provideOnCompleteErrorInfo() - } - - private fun getIcon(type: DialogType) = when (type) { - ERROR, COMPLETE_ERROR -> provider.provideRedAlertIcon() - MANDATORY -> provider.provideSavedIcon() - WARNING -> provider.provideYellowAlertIcon() - SUCCESSFUL -> provider.provideSavedIcon() - } - - private fun getMainButton(type: DialogType, eventState: EventStatus) = when (type) { - ERROR, - MANDATORY, - COMPLETE_ERROR, - -> EventCompletionButtons( - MainButton(provider.provideReview()), - FormBottomDialog.ActionType.CHECK_FIELDS, - ) - - WARNING -> if (eventState == EventStatus.COMPLETED) { - EventCompletionButtons( - MainButton(provider.provideReview()), - FormBottomDialog.ActionType.CHECK_FIELDS, - ) - } else { - EventCompletionButtons( - CompleteButton, - FormBottomDialog.ActionType.COMPLETE, - ) - } - SUCCESSFUL, - -> EventCompletionButtons( - CompleteButton, - FormBottomDialog.ActionType.COMPLETE, - ) - } - - private fun getFieldsWithIssues( - errorFields: List, - mandatoryFields: List, - warningFields: List, - onCompleteField: List, - ): List { - return onCompleteField - .plus(errorFields) - .plus( - mandatoryFields.map { - FieldWithIssue( - "uid", - it, - IssueType.MANDATORY, - provider.provideMandatoryField(), - ) - }, - ).plus(warningFields) - } - - private fun getOnCompleteMessage( - canComplete: Boolean, - onCompleteMessage: String?, - ): List { - val issueOnComplete = onCompleteMessage?.let { - FieldWithIssue( - fieldUid = "", - fieldName = it, - issueType = when (canComplete) { - false -> IssueType.ERROR_ON_COMPLETE - else -> IssueType.WARNING_ON_COMPLETE - }, - message = "", - ) - } - return issueOnComplete?.let { listOf(it) } ?: emptyList() - } - - private fun getDialogType( - errorFields: List, - mandatoryFields: Map, - warningFields: List, - errorOnComplete: Boolean, - ) = when { - errorOnComplete -> { - COMPLETE_ERROR - } - - errorFields.isNotEmpty() -> { - ERROR - } - - mandatoryFields.isNotEmpty() -> { - MANDATORY - } - - warningFields.isNotEmpty() -> { - WARNING - } - - else -> { - SUCCESSFUL - } - } - - private enum class DialogType { ERROR, MANDATORY, WARNING, SUCCESSFUL, COMPLETE_ERROR } -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java deleted file mode 100644 index 560eee6ff1..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.java +++ /dev/null @@ -1,224 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment; - -import static org.dhis2.commons.Constants.EVENT_MODE; -import static org.dhis2.commons.extensions.ViewExtensionsKt.closeKeyboard; -import static org.dhis2.form.data.EventRepository.EVENT_ORG_UNIT_UID; -import static org.dhis2.usescases.eventsWithoutRegistration.eventCapture.ui.NonEditableReasonBlockKt.showNonEditableReasonMessage; -import static org.dhis2.utils.granularsync.SyncStatusDialogNavigatorKt.OPEN_ERROR_LOCATION; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.databinding.DataBindingUtil; -import androidx.fragment.app.FragmentTransaction; - -import org.dhis2.R; -import org.dhis2.commons.Constants; -import org.dhis2.commons.featureconfig.data.FeatureConfigRepository; -import org.dhis2.commons.featureconfig.model.Feature; -import org.dhis2.databinding.SectionSelectorFragmentBinding; -import org.dhis2.form.model.ActionType; -import org.dhis2.form.model.EventMode; -import org.dhis2.form.model.EventRecords; -import org.dhis2.form.ui.FormView; -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureAction; -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity; -import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureContract; -import org.dhis2.usescases.general.FragmentGlobalAbstract; -import org.hisp.dhis.android.core.common.ValueType; -import org.jetbrains.annotations.NotNull; - -import javax.inject.Inject; - -import kotlin.Unit; - -public class EventCaptureFormFragment extends FragmentGlobalAbstract implements EventCaptureFormView, - OnEditionListener { - - @Inject - EventCaptureFormPresenter presenter; - - @Inject - FeatureConfigRepository featureConfig; - - private EventCaptureActivity activity; - private SectionSelectorFragmentBinding binding; - private FormView formView; - - public static EventCaptureFormFragment newInstance( - String eventUid, - Boolean openErrorSection, - EventMode eventMode - ) { - EventCaptureFormFragment fragment = new EventCaptureFormFragment(); - Bundle args = new Bundle(); - args.putString(Constants.EVENT_UID, eventUid); - args.putBoolean(OPEN_ERROR_LOCATION, openErrorSection); - args.putString(EVENT_MODE, eventMode.name()); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onAttach(@NotNull Context context) { - super.onAttach(context); - this.activity = (EventCaptureActivity) context; - activity.eventCaptureComponent.plus( - new EventCaptureFormModule( - this, - getArguments().getString(Constants.EVENT_UID)) - ).inject(this); - setRetainInstance(true); - } - - @Override - public void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { - String eventUid = getArguments().getString(Constants.EVENT_UID, ""); - EventMode eventMode = EventMode.valueOf(getArguments().getString(EVENT_MODE)); - loadForm(eventUid, eventMode); - - activity.setFormEditionListener(this); - super.onCreate(savedInstanceState); - } - - private void loadForm(String eventUid, EventMode eventMode) { - formView = new FormView.Builder() - .locationProvider(locationProvider) - .onLoadingListener(loading -> { - if (loading) { - activity.showProgress(); - } else { - activity.hideProgress(); - } - return Unit.INSTANCE; - }).onItemChangeListener( action -> { - if(action.isEventDetailsRow()){ - presenter.showOrHideSaveButton(); - } - return Unit.INSTANCE; - - }) - .onFocused(() -> { - activity.hideNavigationBar(); - return Unit.INSTANCE; - }) - .onPercentageUpdate(percentage -> { - activity.updatePercentage(percentage); - return Unit.INSTANCE; - }) - .onDataIntegrityResult(result -> { - presenter.handleDataIntegrityResult(result, eventMode); - return Unit.INSTANCE; - }) - .factory(activity.getSupportFragmentManager()) - .setRecords(new EventRecords(eventUid, eventMode)) - .openErrorLocation(getArguments().getBoolean(OPEN_ERROR_LOCATION, false)) - .useComposeForm( - featureConfig.isFeatureEnable(Feature.COMPOSE_FORMS) - ) - .build(); - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - binding = DataBindingUtil.inflate(inflater, R.layout.section_selector_fragment, container, false); - EventCaptureContract.Presenter activityPresenter = activity.getPresenter(); - binding.setPresenter(activityPresenter); - - activityPresenter.observeActions().observe(getViewLifecycleOwner(), action -> - { - if (action == EventCaptureAction.ON_BACK) { - formView.onSaveClick(); - activityPresenter.emitAction(EventCaptureAction.NONE); - } - }); - - binding.actionButton.setOnClickListener(view -> { - closeKeyboard(view); - performSaveClick(); - }); - - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull @NotNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); - transaction.replace(R.id.formViewContainer, formView).commit(); - formView.setScrollCallback(isSectionVisible -> { - animateFabButton(isSectionVisible); - return Unit.INSTANCE; - }); - } - - @Override - public void onResume() { - super.onResume(); - presenter.showOrHideSaveButton(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - } - - private void animateFabButton(boolean sectionIsVisible) { - int translationX = 1000; - if (sectionIsVisible) translationX = 0; - - binding.actionButton.animate().translationX(translationX).setDuration(500).start(); - } - - @Override - public void performSaveClick() { - formView.onSaveClick(); - } - - @Override - public void onEditionListener() { - formView.onEditionFinish(); - } - - @Override - public void hideSaveButton() { - binding.actionButton.setVisibility(View.GONE); - } - - @Override - public void showSaveButton() { - binding.actionButton.setVisibility(View.VISIBLE); - } - - @Override - public void onReopen() { - formView.reload(); - } - - @Override - public void showNonEditableMessage(@NonNull String reason, boolean canBeReOpened) { - binding.editableReasonContainer.setVisibility(View.VISIBLE); - - showNonEditableReasonMessage( - binding.editableReasonContainer, - reason, - canBeReOpened, - () -> { - presenter.reOpenEvent(); - return Unit.INSTANCE; - } - ); - } - - @Override - public void hideNonEditableMessage() { - binding.editableReasonContainer.setVisibility(View.GONE); - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.kt new file mode 100644 index 0000000000..412b078b95 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormFragment.kt @@ -0,0 +1,178 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import org.dhis2.R +import org.dhis2.commons.Constants +import org.dhis2.commons.extensions.closeKeyboard +import org.dhis2.databinding.SectionSelectorFragmentBinding +import org.dhis2.form.model.EventMode +import org.dhis2.form.model.EventRecords +import org.dhis2.form.model.RowAction +import org.dhis2.form.ui.FormView +import org.dhis2.form.ui.provider.FormResultDialogProvider +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureAction +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.ui.showNonEditableReasonMessage +import org.dhis2.usescases.general.FragmentGlobalAbstract +import org.dhis2.utils.granularsync.OPEN_ERROR_LOCATION +import javax.inject.Inject + +class EventCaptureFormFragment : FragmentGlobalAbstract(), EventCaptureFormView { + @Inject + lateinit var presenter: EventCaptureFormPresenter + + @Inject + lateinit var eventResultDialogUiProvider: FormResultDialogProvider + + private lateinit var activity: EventCaptureActivity + + private lateinit var binding: SectionSelectorFragmentBinding + + private var formView: FormView? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + this.activity = context as EventCaptureActivity + activity.eventCaptureComponent?.plus( + EventCaptureFormModule( + this, + arguments?.getString(Constants.EVENT_UID)!!, + ), + )?.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + val eventUid = requireArguments().getString(Constants.EVENT_UID, "") + val eventMode = arguments?.getString(Constants.EVENT_MODE)?.let { EventMode.valueOf(it) } + val eventStatus = presenter.getEventStatus(eventUid) + formView = FormView.Builder() + .locationProvider(locationProvider) + .onLoadingListener { loading: Boolean -> + if (loading) { + activity.showProgress() + } else { + activity.hideProgress() + } + }.onItemChangeListener { action: RowAction -> + if (action.isEventDetailsRow) { + presenter.showOrHideSaveButton() + } + Unit + }.onFinishDataEntry { presenter.saveAndExit(eventStatus) } + .onFocused { + activity.hideNavigationBar() + } + .onPercentageUpdate { percentage: Float? -> + activity.updatePercentage(percentage!!) + } + .eventCompletionResultDialogProvider(eventResultDialogUiProvider = eventResultDialogUiProvider) + .factory(activity.supportFragmentManager) + .setRecords(EventRecords(eventUid, eventMode ?: EventMode.CHECK)) + .openErrorLocation(requireArguments().getBoolean(OPEN_ERROR_LOCATION, false)) + .setProgramUid(presenter.getEvent()?.program()) + .build() + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = + DataBindingUtil.inflate(inflater, R.layout.section_selector_fragment, container, false) + val activityPresenter = activity.presenter + binding.setPresenter(activityPresenter) + + activityPresenter.observeActions().observe( + viewLifecycleOwner, + ) { action: EventCaptureAction -> + if (action == EventCaptureAction.ON_BACK) { + formView?.onBackPressed() + activityPresenter.emitAction(EventCaptureAction.NONE) + } + } + + binding.actionButton.setOnClickListener { view -> + view.closeKeyboard() + performSaveClick() + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val transaction = childFragmentManager.beginTransaction() + transaction.replace(R.id.formViewContainer, formView!!).commit() + formView!!.scrollCallback = { isSectionVisible: Boolean -> + animateFabButton(isSectionVisible) + } + } + + override fun onResume() { + super.onResume() + presenter.showOrHideSaveButton() + } + + private fun animateFabButton(sectionIsVisible: Boolean) { + var translationX = 1000 + if (sectionIsVisible) translationX = 0 + + binding.actionButton.animate().translationX(translationX.toFloat()).setDuration(500) + .start() + } + + override fun performSaveClick() { + formView?.onSaveClick() + } + + override fun hideSaveButton() { + binding.actionButton.visibility = View.GONE + } + + override fun showSaveButton() { + binding.actionButton.visibility = View.VISIBLE + } + + override fun onReopen() { + formView?.reload() + } + + override fun showNonEditableMessage(reason: String, canBeReOpened: Boolean) { + binding.editableReasonContainer.visibility = View.VISIBLE + + showNonEditableReasonMessage( + binding.editableReasonContainer, + reason, + canBeReOpened, + ) { + presenter.reOpenEvent() + } + } + + override fun hideNonEditableMessage() { + binding.editableReasonContainer.visibility = View.GONE + } + + companion object { + fun newInstance( + eventUid: String?, + openErrorSection: Boolean?, + eventMode: EventMode, + ): EventCaptureFormFragment { + val fragment = EventCaptureFormFragment() + val args = Bundle() + args.putString(Constants.EVENT_UID, eventUid) + openErrorSection?.let { args.putBoolean(OPEN_ERROR_LOCATION, it) } + args.putString(Constants.EVENT_MODE, eventMode.name) + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormModule.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormModule.kt index e6fd091a46..3b5a6edd13 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormModule.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormModule.kt @@ -4,6 +4,8 @@ import dagger.Module import dagger.Provides import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.resources.ResourceManager +import org.dhis2.form.ui.provider.FormResultDialogProvider +import org.dhis2.form.ui.provider.FormResultDialogResourcesProvider import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureContract import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ReOpenEventUseCase import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.injection.EventDispatchers @@ -35,6 +37,22 @@ class EventCaptureFormModule( ) } + @Provides + @PerFragment + fun provideResultDialogProvider( + resourceProvider: FormResultDialogResourcesProvider, + ): FormResultDialogProvider { + return FormResultDialogProvider(resourceProvider) + } + + @Provides + @PerFragment + fun provideCompleteEventDialogResourcesProvider( + resourceManager: ResourceManager, + ): FormResultDialogResourcesProvider { + return FormResultDialogResourcesProvider(resourceManager) + } + @Provides @PerFragment fun provideReOpenEventUseCase( diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt index 1196bbe2f8..6804fb2a6c 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/EventCaptureFormPresenter.kt @@ -7,13 +7,6 @@ import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.dhislogic.AUTH_ALL import org.dhis2.data.dhislogic.AUTH_UNCOMPLETE_EVENT -import org.dhis2.form.data.DataIntegrityCheckResult -import org.dhis2.form.data.FieldsWithErrorResult -import org.dhis2.form.data.FieldsWithWarningResult -import org.dhis2.form.data.MissingMandatoryResult -import org.dhis2.form.data.NotSavedResult -import org.dhis2.form.data.SuccessfulResult -import org.dhis2.form.model.EventMode import org.dhis2.usescases.eventsWithoutRegistration.EventIdlingResourceSingleton import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureContract import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.domain.ReOpenEventUseCase @@ -33,49 +26,6 @@ class EventCaptureFormPresenter( private val dispatcherProvider: DispatcherProvider, ) { - fun handleDataIntegrityResult(result: DataIntegrityCheckResult, eventMode: EventMode? = null) { - when (result) { - is FieldsWithErrorResult -> activityPresenter.attemptFinish( - result.canComplete, - result.onCompleteMessage, - result.fieldUidErrorList, - result.mandatoryFields, - result.warningFields, - eventMode, - ) - - is FieldsWithWarningResult -> activityPresenter.attemptFinish( - result.canComplete, - result.onCompleteMessage, - emptyList(), - emptyMap(), - result.fieldUidWarningList, - eventMode, - ) - - is MissingMandatoryResult -> activityPresenter.attemptFinish( - result.canComplete, - result.onCompleteMessage, - result.errorFields, - result.mandatoryFields, - result.warningFields, - eventMode, - ) - - is SuccessfulResult -> activityPresenter.attemptFinish( - result.canComplete, - result.onCompleteMessage, - emptyList(), - emptyMap(), - emptyList(), - ) - - NotSavedResult -> { - // Nothing to do in this case - } - } - } - fun showOrHideSaveButton() { val isEditable = d2.eventModule().eventService().getEditableStatus(eventUid = eventUid).blockingGet() @@ -93,6 +43,10 @@ class EventCaptureFormPresenter( } } + fun saveAndExit(eventStatus: EventStatus?) { + activityPresenter.saveAndExit(eventStatus) + } + private fun configureNonEditableMessage(eventNonEditableReason: EventNonEditableReason) { val (reason, canBeReOpened) = when (eventNonEditableReason) { EventNonEditableReason.BLOCKED_BY_COMPLETION -> resourceManager.getString(R.string.blocked_by_completion) to canReopen() @@ -133,10 +87,14 @@ class EventCaptureFormPresenter( it.status() == EventStatus.COMPLETED && hasReopenAuthority() } ?: false - private fun getEvent(): Event? { + fun getEvent(): Event? { return d2.eventModule().events().uid(eventUid).blockingGet() } + fun getEventStatus(eventUid: String): EventStatus? { + return d2.eventModule().events().uid(eventUid).blockingGet()?.status() + } + private fun hasReopenAuthority(): Boolean = d2.userModule().authorities() .byName().`in`(AUTH_UNCOMPLETE_EVENT, AUTH_ALL) .one() diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/OnEditionListener.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/OnEditionListener.kt deleted file mode 100644 index f41decda96..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/eventCaptureFragment/OnEditionListener.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.eventCaptureFragment - -interface OnEditionListener { - fun onEditionListener() -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCompletionButtons.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCompletionButtons.kt deleted file mode 100644 index 1dc9e28eb4..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCompletionButtons.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model - -import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle -import org.dhis2.utils.customviews.FormBottomDialog - -data class EventCompletionButtons( - val buttonStyle: DialogButtonStyle, - val action: FormBottomDialog.ActionType, -) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCompletionDialog.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCompletionDialog.kt deleted file mode 100644 index d23b291eed..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventCapture/model/EventCompletionDialog.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventCapture.model - -import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel -import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue -import org.dhis2.utils.customviews.FormBottomDialog - -data class EventCompletionDialog( - val bottomSheetDialogUiModel: BottomSheetDialogUiModel, - val mainButtonAction: FormBottomDialog.ActionType, - val secondaryButtonAction: FormBottomDialog.ActionType?, - val fieldsWithIssues: List, -) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt index 4026469dd4..3e5127595c 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventDetails.kt @@ -3,7 +3,6 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import org.dhis2.commons.data.EventCreationType -import org.dhis2.commons.data.EventCreationType.REFERAL import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.ui.toColor import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository @@ -32,13 +31,11 @@ class ConfigureEventDetails( catOptionComboUid: String?, isCatComboCompleted: Boolean, coordinates: String?, - tempCreate: String?, ): Flow { val isEventCompleted = isCompleted( selectedDate = selectedDate, selectedOrgUnit = selectedOrgUnit, isCatComboCompleted = isCatComboCompleted, - tempCreate = tempCreate, ) val storedEvent = repository.getEvent() val programStage = repository.getProgramStage() @@ -56,7 +53,6 @@ class ConfigureEventDetails( enabled = isEnable(storedEvent), isEditable = isEditable(), editableReason = getEditableReason(), - temCreate = tempCreate, selectedDate = selectedDate, selectedOrgUnit = selectedOrgUnit, catOptionComboUid = catOptionComboUid, @@ -95,11 +91,9 @@ class ConfigureEventDetails( selectedDate: Date?, selectedOrgUnit: String?, isCatComboCompleted: Boolean, - tempCreate: String?, ) = selectedDate != null && !selectedOrgUnit.isNullOrEmpty() && - isCatComboCompleted && - (creationType != REFERAL || tempCreate != null) + isCatComboCompleted private fun isEditable(): Boolean { return getEditableReason() == null diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt index c464e213ab..00066b1009 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventReportDate.kt @@ -34,7 +34,7 @@ class ConfigureEventReportDate( label = getLabel(), dateValue = getDateValue(selectedDate), currentDate = getDate(selectedDate), - minDate = getMinDate(), + minDate = getMinDate(selectedDate), maxDate = getMaxDate(), scheduleInterval = getScheduleInterval(), allowFutureDates = getAllowFutureDates(), @@ -49,12 +49,18 @@ class ConfigureEventReportDate( private fun getLabel(): String { val programStage = getProgramStage() + val event = repository.getEvent() + return when (creationType) { SCHEDULE -> - programStage?.dueDateLabel() ?: resourceProvider.provideDueDate() + programStage?.displayDueDateLabel() ?: resourceProvider.provideDueDate() else -> { - programStage?.executionDateLabel() ?: resourceProvider.provideEventDate() + if (event == null) { + resourceProvider.provideNextEventDate(programStage?.displayEventLabel()) + } else { + programStage?.displayExecutionDateLabel() ?: resourceProvider.provideEventDate() + } } } } @@ -62,8 +68,7 @@ class ConfigureEventReportDate( private fun getDate(selectedDate: Date?) = when { selectedDate != null -> selectedDate repository.getEvent() != null -> repository.getEvent()?.eventDate() - periodType != null -> getDateBasedOnPeriodType() - creationType == SCHEDULE -> getNextScheduleDate() + periodType != null || creationType == SCHEDULE -> getNextScheduleDate() else -> getCurrentDay() } @@ -78,14 +83,15 @@ class ConfigureEventReportDate( private fun getProgramStage(): ProgramStage? = repository.getProgramStage() - private fun getDateBasedOnPeriodType(): Date { + private fun getDateBasedOnPeriodType(startDate: Date?): Date { + val initialDate = startDate ?: DateUtils.getInstance().today + val calendar = DateUtils.getInstance().calendar + calendar.time = initialDate getProgramStage()?.hideDueDate()?.let { hideDueDate -> if (creationType == SCHEDULE && hideDueDate) { return if (periodType != null) { - DateUtils.getInstance().today + calendar.time } else { - val calendar = DateUtils.getInstance().calendar - calendar.add(DAY_OF_YEAR, getScheduleInterval()) DateUtils.getInstance().getNextPeriod( null, calendar.time, @@ -94,16 +100,15 @@ class ConfigureEventReportDate( } } } - return DateUtils.getInstance() .getNextPeriod( periodType, - DateUtils.getInstance().today, + initialDate, if (creationType != SCHEDULE) 0 else 1, ) } - private fun getNextScheduleDate(): Date { + fun getNextScheduleDate(): Date { val scheduleDate = repository.getStageLastDate(enrollmentId)?.let { val lastStageDate = DateUtils.getInstance().getCalendarByDate(it) lastStageDate.add(DAY_OF_YEAR, getScheduleInterval()) @@ -119,27 +124,30 @@ class ConfigureEventReportDate( val date = DateUtils.getInstance().getCalendarByDate(enrollmentDate) val minDateFromStart = repository.getMinDaysFromStartByProgramStage() date.add(DAY_OF_YEAR, minDateFromStart) - date + periodType?.let { + return getDateBasedOnPeriodType(date.time) + } + return date.time } - return DateUtils.getInstance().getNextPeriod(null, scheduleDate.time, 0) + return DateUtils.getInstance().getNextPeriod(periodType, scheduleDate.time, if (periodType != null) 1 else 0) } - private fun getCurrentDay() = DateUtils.getInstance().today + private fun getCurrentDay() = DateUtils.getInstance().getStartOfDay(Date()) - private fun getMinDate(): Date? { + private fun getMinDate(initialDate: Date?): Date? { repository.getProgram()?.let { program -> if (periodType == null) { if (program.expiryPeriodType() != null) { val expiryDays = program.expiryDays() ?: 0 return DateUtils.getInstance().expDate( - null, + initialDate, expiryDays, program.expiryPeriodType(), ) } } else { var minDate = DateUtils.getInstance().expDate( - null, + initialDate, program.expiryDays() ?: 0, periodType, ) @@ -180,7 +188,7 @@ class ConfigureEventReportDate( when (creationType) { ADDNEW, DEFAULT, - -> DateUtils.getInstance().today + -> DateUtils.getInstance().getStartOfDay(Date()) else -> null } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventTemp.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventTemp.kt deleted file mode 100644 index 3cba5a77ac..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/domain/ConfigureEventTemp.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain - -import org.dhis2.commons.data.EventCreationType -import org.dhis2.commons.data.EventCreationType.REFERAL -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTempStatus - -class ConfigureEventTemp( - private val creationType: EventCreationType, -) { - - operator fun invoke(status: EventTempStatus? = null): EventTemp { - return EventTemp( - active = isActive(), - status = status, - completed = isCompleted(status), - ) - } - - private fun isCompleted(status: EventTempStatus?) = when (creationType) { - REFERAL -> status != null - else -> true - } - - private fun isActive(): Boolean { - return creationType == REFERAL - } -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt index daf4ee8309..bb12ddfcd2 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/injection/EventDetailsModule.kt @@ -8,8 +8,8 @@ import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.locationprovider.LocationProvider import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.prefs.PreferenceProviderImpl -import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.data.GeometryController @@ -18,22 +18,17 @@ import org.dhis2.form.data.metadata.FileResourceConfiguration import org.dhis2.form.data.metadata.OptionSetConfiguration import org.dhis2.form.data.metadata.OrgUnitConfiguration import org.dhis2.form.ui.FieldViewModelFactoryImpl -import org.dhis2.form.ui.LayoutProviderImpl import org.dhis2.form.ui.provider.AutoCompleteProviderImpl import org.dhis2.form.ui.provider.DisplayNameProviderImpl import org.dhis2.form.ui.provider.HintProviderImpl import org.dhis2.form.ui.provider.KeyboardActionProviderImpl import org.dhis2.form.ui.provider.LegendValueProviderImpl import org.dhis2.form.ui.provider.UiEventTypesProviderImpl -import org.dhis2.form.ui.provider.UiStyleProviderImpl -import org.dhis2.form.ui.style.FormUiModelColorFactoryImpl -import org.dhis2.form.ui.style.LongTextUiColorFactoryImpl import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCoordinates import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider @@ -60,8 +55,14 @@ class EventDetailsModule( @PerFragment fun provideEventDetailResourceProvider( resourceManager: ResourceManager, + eventResourcesProvider: EventResourcesProvider, ): EventDetailResourcesProvider { - return EventDetailResourcesProvider(programUid, programStageUid, resourceManager) + return EventDetailResourcesProvider( + programUid, + programStageUid, + resourceManager, + eventResourcesProvider, + ) } @Provides @@ -75,7 +76,6 @@ class EventDetailsModule( fun provideEventDetailsRepository( d2: D2, resourceManager: ResourceManager, - colorUtils: ColorUtils, periodUtils: DhisPeriodUtils, ): EventDetailsRepository { return EventDetailsRepository( @@ -85,12 +85,6 @@ class EventDetailsModule( programStageUid = programStageUid, eventCreationType = eventCreationType, fieldFactory = FieldViewModelFactoryImpl( - UiStyleProviderImpl( - FormUiModelColorFactoryImpl(context, colorUtils), - LongTextUiColorFactoryImpl(context, colorUtils), - true, - ), - LayoutProviderImpl(), HintProviderImpl(context), DisplayNameProviderImpl( OptionSetConfiguration(d2), @@ -149,9 +143,6 @@ class EventDetailsModule( ConfigureEventCatCombo( repository = eventDetailsRepository, ), - ConfigureEventTemp( - creationType = eventCreationType, - ), periodType = periodType, eventUid = eventUid, geometryController = geometryController, diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventTemp.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventTemp.kt deleted file mode 100644 index e986454a1e..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventTemp.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models - -data class EventTemp( - val active: Boolean = false, - val status: EventTempStatus? = null, - val completed: Boolean = true, -) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventTempStatus.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventTempStatus.kt deleted file mode 100644 index 722196df34..0000000000 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/models/EventTempStatus.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models - -enum class EventTempStatus { - ONE_TIME, - PERMANENT, -} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/EventDetailResourcesProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/EventDetailResourcesProvider.kt index d661fc0341..2c66231116 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/EventDetailResourcesProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/EventDetailResourcesProvider.kt @@ -1,6 +1,7 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers import org.dhis2.R +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.ResourceManager import org.hisp.dhis.android.core.event.EventNonEditableReason @@ -8,14 +9,24 @@ class EventDetailResourcesProvider( private val programUid: String, private val programStage: String?, private val resourceManager: ResourceManager, + private val eventResourcesProvider: EventResourcesProvider, ) { fun provideDueDate() = resourceManager.getString(R.string.due_date) - fun provideEventDate() = resourceManager.formatWithEventLabel( + fun provideEventDate() = eventResourcesProvider.formatWithProgramStageEventLabel( R.string.event_label_date, programStage, + programUid, ) + fun provideNextEventDate(label: String?): String { + val defaultEventLabel = resourceManager.getString(R.string.event) + return resourceManager.getString( + R.string.next_event, + label ?: defaultEventLabel, + ) + } + fun provideEditionStatus(reason: EventNonEditableReason): String { return when (reason) { EventNonEditableReason.BLOCKED_BY_COMPLETION -> @@ -45,14 +56,16 @@ class EventDetailResourcesProvider( fun provideButtonCheck() = resourceManager.getString(R.string.check_event) - fun provideEventCreatedMessage() = resourceManager.formatWithEventLabel( + fun provideEventCreatedMessage() = eventResourcesProvider.formatWithProgramStageEventLabel( R.string.event_label_updated, programStage, + programUid, ) - fun provideEventCreationError() = resourceManager.formatWithEventLabel( + fun provideEventCreationError() = eventResourcesProvider.formatWithProgramStageEventLabel( R.string.failed_insert_event_label, programStage, + programUid, ) fun provideReOpened() = resourceManager.getString(R.string.re_opened) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt index 3c0719ea6c..73acff526a 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt @@ -23,8 +23,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCa import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventInputDateUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOrgUnit -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTempStatus import org.hisp.dhis.android.core.arch.helpers.GeometryHelper import org.hisp.dhis.android.core.arch.helpers.Result import org.hisp.dhis.android.core.common.FeatureType @@ -41,12 +39,9 @@ import org.hisp.dhis.mobile.ui.designsystem.component.InputDateTimeModel import org.hisp.dhis.mobile.ui.designsystem.component.InputDropDown import org.hisp.dhis.mobile.ui.designsystem.component.InputOrgUnit import org.hisp.dhis.mobile.ui.designsystem.component.InputPolygon -import org.hisp.dhis.mobile.ui.designsystem.component.InputRadioButton import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState -import org.hisp.dhis.mobile.ui.designsystem.component.Orientation -import org.hisp.dhis.mobile.ui.designsystem.component.RadioButtonData import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates -import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation +import org.hisp.dhis.mobile.ui.designsystem.component.model.DateTransformation import java.time.LocalDate import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException @@ -143,6 +138,7 @@ fun manageActionBasedOnValue(uiModel: EventInputDateUiModel, dateString: String) } } } + private fun isValid(valueString: String) = valueString.length == 8 private fun formatStoredDateToUI(dateValue: String): String? { @@ -390,56 +386,6 @@ fun mapGeometry(value: String?, featureType: FeatureType): Coordinates? { } } -@Composable -fun ProvideRadioButtons( - eventTemp: EventTemp, - detailsEnabled: Boolean, - resources: ResourceManager, - onEventTempSelected: (status: EventTempStatus?) -> Unit, - showField: Boolean = true, -) { - if (showField) { - Spacer(modifier = Modifier.height(16.dp)) - val radioButtonData = listOf( - RadioButtonData( - uid = EventTempStatus.ONE_TIME.name, - selected = eventTemp.status == EventTempStatus.ONE_TIME, - enabled = true, - textInput = resources.getString(R.string.one_time), - ), - RadioButtonData( - uid = EventTempStatus.PERMANENT.name, - selected = eventTemp.status == EventTempStatus.PERMANENT, - enabled = true, - textInput = resources.getString(R.string.permanent), - ), - ) - - InputRadioButton( - title = resources.getString(R.string.referral), - radioButtonData = radioButtonData, - orientation = Orientation.HORIZONTAL, - state = getInputState(detailsEnabled), - itemSelected = radioButtonData.find { it.selected }, - onItemChange = { data -> - when (data?.uid) { - EventTempStatus.ONE_TIME.name -> { - onEventTempSelected(EventTempStatus.ONE_TIME) - } - - EventTempStatus.PERMANENT.name -> { - onEventTempSelected(EventTempStatus.PERMANENT) - } - - else -> { - onEventTempSelected(null) - } - } - }, - ) - } -} - fun willShowCalendar(periodType: PeriodType?): Boolean { return (periodType == null || periodType == PeriodType.Daily) } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt index 036a3fe9f7..3475546966 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsFragment.kt @@ -42,14 +42,12 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDa import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventInputDateUiModel import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOrgUnit -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideCategorySelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideCoordinates import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideEmptyCategorySelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvidePeriodSelector -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideRadioButtons import org.dhis2.usescases.general.FragmentGlobalAbstract import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.enrollment.EnrollmentStatus @@ -141,7 +139,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { val orgUnit by viewModel.eventOrgUnit.collectAsState() val catCombo by viewModel.eventCatCombo.collectAsState() val coordinates by viewModel.eventCoordinates.collectAsState() - val eventTemp by viewModel.eventTemp.collectAsState() ProvideNewEventForm( date = date, @@ -149,7 +146,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { orgUnit = orgUnit, catCombo = catCombo, coordinates = coordinates, - eventTemp = eventTemp, ) } return binding.root @@ -185,9 +181,11 @@ class EventDetailsFragment : FragmentGlobalAbstract() { viewModel.requestLocationByMap = { featureType, initCoordinate -> requestLocationByMap.launch( MapSelectorActivity.create( - requireActivity(), - FeatureType.valueOfFeatureType(featureType), - initCoordinate, + activity = requireActivity(), + fieldUid = null, + locationType = FeatureType.valueOfFeatureType(featureType), + initialData = initCoordinate, + programUid = requireArguments().getString(PROGRAM_UID), ), ) } @@ -229,7 +227,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { orgUnit: EventOrgUnit, catCombo: EventCatCombo, coordinates: EventCoordinates, - eventTemp: EventTemp, ) { Column { if (viewModel.getPeriodType() == null || (viewModel.getPeriodType() != null && viewModel.getPeriodType() == PeriodType.Daily)) { @@ -313,15 +310,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { resources = resourceManager, showField = coordinates.active, ) - ProvideRadioButtons( - eventTemp = eventTemp, - detailsEnabled = details.enabled, - resources = resourceManager, - onEventTempSelected = { - viewModel.setUpEventTemp(it) - }, - showField = eventTemp.active, - ) } } @@ -338,7 +326,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { private fun showOrgUnitDialog() { OUTreeFragment.Builder() - .showAsDialog() .withPreselectedOrgUnits( viewModel.eventOrgUnit.value.selectedOrgUnit ?.let { listOf(it.uid()) } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt index e7f6233cd0..3c4f1c9e8b 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt @@ -15,7 +15,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCoordinates import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo @@ -23,8 +22,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOrgUnit -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTempStatus import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MAX_DATE import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.DEFAULT_MIN_DATE import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider @@ -46,7 +43,6 @@ class EventDetailsViewModel( private val configureOrgUnit: ConfigureOrgUnit, private val configureEventCoordinates: ConfigureEventCoordinates, private val configureEventCatCombo: ConfigureEventCatCombo, - private val configureEventTemp: ConfigureEventTemp, private val periodType: PeriodType?, private val eventUid: String?, private val geometryController: GeometryController, @@ -82,9 +78,6 @@ class EventDetailsViewModel( private val _eventCatCombo: MutableStateFlow = MutableStateFlow(EventCatCombo()) val eventCatCombo: StateFlow get() = _eventCatCombo - private val _eventTemp: MutableStateFlow = MutableStateFlow(EventTemp()) - val eventTemp: StateFlow get() = _eventTemp - init { loadEventDetails() } @@ -123,17 +116,12 @@ class EventDetailsViewModel( _eventCoordinates.value = eventCoordinates } - configureEventTemp().apply { - _eventTemp.value = this - } - configureEventDetails( selectedDate = eventDate.value.currentDate, selectedOrgUnit = eventOrgUnit.value.selectedOrgUnit?.uid(), catOptionComboUid = eventCatCombo.value.uid, isCatComboCompleted = eventCatCombo.value.isCompleted, coordinates = eventCoordinates.value.model?.value, - tempCreate = eventTemp.value.status?.name, ) .collect { _eventDetails.value = it @@ -150,7 +138,6 @@ class EventDetailsViewModel( catOptionComboUid = eventCatCombo.value.uid, isCatComboCompleted = eventCatCombo.value.isCompleted, coordinates = eventCoordinates.value.model?.value, - tempCreate = eventTemp.value.status?.name, ) .flowOn(Dispatchers.IO) .collect { @@ -239,17 +226,6 @@ class EventDetailsViewModel( } } - fun setUpEventTemp(status: EventTempStatus? = null, isChecked: Boolean = true) { - EventIdlingResourceSingleton.increment() - if (isChecked) { - configureEventTemp(status).apply { - _eventTemp.value = this - setUpEventDetails() - } - } - EventIdlingResourceSingleton.decrement() - } - fun getSelectableDates(eventDate: EventDate): SelectableDates { return if (eventDate.allowFutureDates) { SelectableDates(DEFAULT_MIN_DATE, DEFAULT_MAX_DATE) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt index 502ce7fc16..5910ee7d71 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModelFactory.kt @@ -8,7 +8,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCoordinates import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider @@ -21,7 +20,6 @@ class EventDetailsViewModelFactory( private val configureOrgUnit: ConfigureOrgUnit, private val configureEventCoordinates: ConfigureEventCoordinates, private val configureEventCatCombo: ConfigureEventCatCombo, - private val configureEventTemp: ConfigureEventTemp, private val periodType: PeriodType?, private val eventUid: String?, private val geometryController: GeometryController, @@ -37,7 +35,6 @@ class EventDetailsViewModelFactory( configureOrgUnit, configureEventCoordinates, configureEventCatCombo, - configureEventTemp, periodType, eventUid, geometryController, diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/ReopenButton.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/ReopenButton.kt index a89ba452fe..504dd80d09 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/ReopenButton.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/ReopenButton.kt @@ -44,6 +44,7 @@ fun ReopenButton(visible: Boolean, onReopenClickListener: () -> Unit) { contentPadding = PaddingValues(10.dp), modifier = Modifier .height(40.dp) + .wrapContentWidth() .wrapContentWidth(), ) { Icon( diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.kt index 8ff12343c6..c33abdd694 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialActivity.kt @@ -6,7 +6,14 @@ import android.os.Handler import android.os.Looper import android.util.SparseBooleanArray import android.view.View -import android.widget.PopupMenu +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.Share +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.databinding.DataBindingUtil import io.reactivex.disposables.CompositeDisposable import org.dhis2.App @@ -15,8 +22,7 @@ import org.dhis2.commons.Constants import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.dialogs.CustomDialog import org.dhis2.commons.dialogs.DialogClickListener -import org.dhis2.commons.popupmenu.AppMenuHelper -import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.schedulers.SingleEventEnforcer import org.dhis2.commons.schedulers.SingleEventEnforcerImpl import org.dhis2.databinding.ActivityEventInitialBinding @@ -36,11 +42,15 @@ import org.dhis2.utils.analytics.CREATE_EVENT import org.dhis2.utils.analytics.DATA_CREATION import org.dhis2.utils.analytics.DELETE_EVENT import org.dhis2.utils.analytics.SHOW_HELP +import org.dhis2.utils.customviews.MoreOptionsWithDropDownMenuButton import org.hisp.dhis.android.core.common.Geometry import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuLeadingElement import java.util.Objects import javax.inject.Inject @@ -53,7 +63,7 @@ class EventInitialActivity : lateinit var presenter: EventInitialPresenter @Inject - lateinit var resourceManager: ResourceManager + lateinit var eventResourcesProvider: EventResourcesProvider private lateinit var binding: ActivityEventInitialBinding @@ -110,7 +120,6 @@ class EventInitialActivity : this, eventUid, programStageUid, - context, ), ) eventInitialComponent!!.inject(this) @@ -120,6 +129,7 @@ class EventInitialActivity : binding.setPresenter(presenter) initProgressBar() + setupMoreOptionsMenu() val bundle = Bundle() bundle.putString(Constants.EVENT_UID, eventUid) @@ -168,18 +178,7 @@ class EventInitialActivity : if (eventUid == null) { // This is a new Event presenter.onEventCreated() analyticsHelper().setEvent(CREATE_EVENT, DATA_CREATION, CREATE_EVENT) - if (eventCreationType == EventCreationType.REFERAL && eventDetails.temCreate != null && eventDetails.temCreate == Constants.PERMANENT) { - presenter.scheduleEventPermanent( - enrollmentUid, - getTrackedEntityInstance, - programStageModelUid, - eventDetails.selectedDate, - eventDetails.selectedOrgUnit, - null, - eventDetails.catOptionComboUid, - geometry, - ) - } else if (eventCreationType == EventCreationType.SCHEDULE || eventCreationType == EventCreationType.REFERAL) { + if (eventCreationType == EventCreationType.SCHEDULE || eventCreationType == EventCreationType.REFERAL) { presenter.scheduleEvent( enrollmentUid, programStageModelUid, @@ -231,9 +230,10 @@ class EventInitialActivity : getString(R.string.referral) } else { if (eventUid == null) { - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.new_event_label, programStageUid, + programUid, 1, false, ) @@ -246,9 +246,10 @@ class EventInitialActivity : override fun onEventCreated(eventUid: String) { showToast( - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.event_label_created, programStageUid, + programUid, 1, false, ), @@ -318,46 +319,75 @@ class EventInitialActivity : }, 500) } - override fun showMoreOptions(view: View) { - AppMenuHelper.Builder().menu(this, R.menu.event_menu).anchor(view) - .onMenuInflated { popupMenu: PopupMenu -> - popupMenu.menu.findItem(R.id.menu_delete).setVisible( - accessData!! && presenter.isEnrollmentOpen, - ) - popupMenu.menu.findItem(R.id.menu_share).setVisible(eventUid != null) - Unit - } - .onMenuItemClicked { itemId: Int? -> + private fun setupMoreOptionsMenu() { + binding.moreOptions.setContent { + var expanded by remember { mutableStateOf(false) } + + MoreOptionsWithDropDownMenuButton( + getMenuItems(), + expanded, + onMenuToggle = { expanded = it }, + ) { itemId -> when (itemId) { - R.id.showHelp -> { + EventInitialMenuItem.SHOW_HELP -> { analyticsHelper().setEvent(SHOW_HELP, CLICK, SHOW_HELP) setTutorial() } - R.id.menu_delete -> confirmDeleteEvent() - R.id.menu_share -> presenter.onShareClick() - else -> { - // do nothing - } + EventInitialMenuItem.SHARE -> presenter.onShareClick() + EventInitialMenuItem.DELETE -> confirmDeleteEvent() } - false } - .build() - .show() + } + } + + private fun getMenuItems(): List> { + return buildList { + add( + MenuItemData( + id = EventInitialMenuItem.SHOW_HELP, + label = getString(R.string.showHelp), + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.HelpOutline), + ), + ) + if (eventUid != null) { + add( + MenuItemData( + id = EventInitialMenuItem.SHARE, + label = getString(R.string.share), + showDivider = true, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Share), + ), + ) + } + + if (accessData!! && presenter.isEnrollmentOpen) { + add( + MenuItemData( + id = EventInitialMenuItem.DELETE, + label = getString(R.string.delete), + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteForever), + ), + ) + } + } } fun confirmDeleteEvent() { CustomDialog( this, - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.delete_event_label, programStageUid, + programUid, 1, false, ), - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.confirm_delete_event_label, programStageUid, + programUid, 1, false, ), @@ -379,9 +409,10 @@ class EventInitialActivity : override fun showEventWasDeleted() { showToast( - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.event_label_was_deleted, programStageUid, + programUid, 1, false, ), @@ -391,9 +422,10 @@ class EventInitialActivity : override fun showDeleteEventError() { showToast( - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.delete_event_label_error, programStageUid, + programUid, 1, false, ), @@ -432,3 +464,9 @@ class EventInitialActivity : } } } + +enum class EventInitialMenuItem { + SHOW_HELP, + SHARE, + DELETE, +} diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialModule.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialModule.java index 949159bca4..83de6a364a 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialModule.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialModule.java @@ -10,30 +10,22 @@ import org.dhis2.commons.matomo.MatomoAnalyticsController; import org.dhis2.commons.prefs.PreferenceProvider; import org.dhis2.commons.prefs.PreferenceProviderImpl; -import org.dhis2.commons.resources.ColorUtils; -import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.DhisPeriodUtils; +import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.commons.schedulers.SchedulerProvider; -import org.dhis2.data.forms.EventRepository; -import org.dhis2.data.forms.FormRepository; -import org.dhis2.form.data.RulesRepository; import org.dhis2.form.data.RulesUtilsProvider; import org.dhis2.form.data.metadata.FileResourceConfiguration; import org.dhis2.form.data.metadata.OptionSetConfiguration; import org.dhis2.form.data.metadata.OrgUnitConfiguration; import org.dhis2.form.ui.FieldViewModelFactory; import org.dhis2.form.ui.FieldViewModelFactoryImpl; -import org.dhis2.form.ui.LayoutProviderImpl; import org.dhis2.form.ui.provider.AutoCompleteProviderImpl; import org.dhis2.form.ui.provider.DisplayNameProviderImpl; import org.dhis2.form.ui.provider.HintProviderImpl; import org.dhis2.form.ui.provider.KeyboardActionProviderImpl; import org.dhis2.form.ui.provider.LegendValueProviderImpl; import org.dhis2.form.ui.provider.UiEventTypesProviderImpl; -import org.dhis2.form.ui.provider.UiStyleProviderImpl; -import org.dhis2.form.ui.style.FormUiModelColorFactoryImpl; -import org.dhis2.form.ui.style.LongTextUiColorFactoryImpl; import org.dhis2.mobileProgramRules.EvaluationType; import org.dhis2.mobileProgramRules.RuleEngineHelper; import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventFieldMapper; @@ -50,16 +42,13 @@ public class EventInitialModule { private final String stageUid; @Nullable private final String eventUid; - private final Context activityContext; public EventInitialModule(@NonNull EventInitialContract.View view, @Nullable String eventUid, - String stageUid, - Context context) { + String stageUid) { this.view = view; this.eventUid = eventUid; this.stageUid = stageUid; - this.activityContext = context; } @Provides @@ -94,16 +83,9 @@ FieldViewModelFactory fieldFactory( Context context, D2 d2, ResourceManager resourceManager, - ColorUtils colorUtils, DhisPeriodUtils periodUtils ) { return new FieldViewModelFactoryImpl( - new UiStyleProviderImpl( - new FormUiModelColorFactoryImpl(activityContext, colorUtils), - new LongTextUiColorFactoryImpl(activityContext, colorUtils), - true - ), - new LayoutProviderImpl(), new HintProviderImpl(context), new DisplayNameProviderImpl( new OptionSetConfiguration(d2), @@ -118,17 +100,6 @@ FieldViewModelFactory fieldFactory( ); } - @Provides - FormRepository formRepository(@NonNull RulesRepository rulesRepository, - @NonNull D2 d2) { - return new EventRepository(rulesRepository, eventUid, d2); - } - - @Provides - RulesRepository rulesRepository(@NonNull D2 d2) { - return new RulesRepository(d2); - } - @Provides @PerActivity EventInitialRepository eventDetailRepository( diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialPresenter.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialPresenter.java index 28a7035617..6af6babb85 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialPresenter.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialPresenter.java @@ -199,31 +199,6 @@ public void createEvent(String enrollmentUid, String programStageModel, Date dat } } - public void scheduleEventPermanent(String enrollmentUid, String trackedEntityInstanceUid, String programStageModel, - Date dueDate, String orgUnitUid, String categoryOptionComboUid, String categoryOptionsUid, Geometry geometry) { - if (program != null) { - preferences.setValue(Preference.CURRENT_ORG_UNIT, orgUnitUid); - compositeDisposable.add( - eventInitialRepository.permanentReferral( - enrollmentUid, - trackedEntityInstanceUid, - program.uid(), - programStageModel, - dueDate, - orgUnitUid, - categoryOptionComboUid, - categoryOptionsUid, - geometry) - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - view::onEventCreated, - t -> view.renderError(t.getMessage()) - ) - ); - } - } - public void scheduleEvent(String enrollmentUid, String programStageModel, Date dueDate, String orgUnitUid, String categoryOptionComboUid, String categoryOptionsUid, Geometry geometry) { if (program != null) { diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepository.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepository.java index ff5ee2af92..3469130c21 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepository.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepository.java @@ -62,16 +62,4 @@ Observable scheduleEvent(String enrollmentUid, @Nullable String trackedE Flowable> calculate(); Flowable getEditableStatus(); - - Observable permanentReferral( - String enrollmentUid, - @NonNull String teiUid, - @NonNull String programUid, - @NonNull String programStage, - @NonNull Date dueDate, - @NonNull String orgUnitUid, - @Nullable String categoryOptionsUid, - @Nullable String categoryOptionComboUid, - @NonNull Geometry geometry - ); } diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java index 010541dad7..ab345fd56a 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventInitial/EventInitialRepositoryImpl.java @@ -387,34 +387,4 @@ private String searchValueDataElement(String dataElement, List getEditableStatus() { return d2.eventModule().eventService().getEditableStatus(eventUid).toFlowable(); } - - @Override - public Observable permanentReferral( - String enrollmentUid, - @NonNull String teiUid, - @NonNull String programUid, - @NonNull String programStage, - @NonNull Date dueDate, - @NonNull String orgUnitUid, - @Nullable String categoryOptionsUid, - @Nullable String categoryOptionComboUid, - @NonNull Geometry geometry - ) { - - d2.trackedEntityModule().ownershipManager() - .blockingTransfer(teiUid, programUid, orgUnitUid); - return scheduleEvent( - enrollmentUid, - teiUid, - programUid, - programStage, - dueDate, - orgUnitUid, - categoryOptionsUid, - categoryOptionComboUid, - geometry - ); - - } - } diff --git a/app/src/main/java/org/dhis2/usescases/general/ActivityGlobalAbstract.java b/app/src/main/java/org/dhis2/usescases/general/ActivityGlobalAbstract.java index 12e73eba95..5c461f6ce9 100644 --- a/app/src/main/java/org/dhis2/usescases/general/ActivityGlobalAbstract.java +++ b/app/src/main/java/org/dhis2/usescases/general/ActivityGlobalAbstract.java @@ -2,81 +2,44 @@ import static org.dhis2.utils.analytics.AnalyticsConstants.CLICK; import static org.dhis2.utils.analytics.AnalyticsConstants.SHOW_HELP; -import static org.dhis2.utils.session.PinDialogKt.PIN_DIALOG_TAG; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.ActivityInfo; -import android.os.Bundle; import android.view.View; -import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityOptionsCompat; -import androidx.core.content.ContextCompat; - import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.dhis2.App; import org.dhis2.R; -import org.dhis2.bindings.ExtensionsKt; import org.dhis2.commons.ActivityResultObservable; -import org.dhis2.commons.ActivityResultObserver; import org.dhis2.commons.Constants; import org.dhis2.commons.dialogs.CustomDialog; -import org.dhis2.commons.locationprovider.LocationProvider; import org.dhis2.commons.popupmenu.AppMenuHelper; -import org.dhis2.data.server.ServerComponent; -import org.dhis2.usescases.login.LoginActivity; -import org.dhis2.usescases.login.accounts.AccountsActivity; -import org.dhis2.usescases.main.MainActivity; -import org.dhis2.usescases.qrScanner.ScanActivity; -import org.dhis2.usescases.splash.SplashActivity; +import org.dhis2.commons.reporting.CrashReportController; import org.dhis2.utils.HelpManager; import org.dhis2.utils.OnDialogClickListener; -import org.dhis2.utils.analytics.AnalyticsConstants; import org.dhis2.utils.analytics.AnalyticsHelper; import org.dhis2.utils.granularsync.SyncStatusDialog; -import org.dhis2.commons.reporting.CrashReportController; -import org.dhis2.utils.session.PinDialog; -import org.jetbrains.annotations.NotNull; import javax.inject.Inject; -import io.reactivex.Observable; -import io.reactivex.subjects.BehaviorSubject; import kotlin.Unit; -public abstract class ActivityGlobalAbstract extends AppCompatActivity +public abstract class ActivityGlobalAbstract extends SessionManagerActivity implements AbstractActivityContracts.View, ActivityResultObservable { private static final String FRAGMENT_TAG = "SYNC"; - private BehaviorSubject lifeCycleObservable = BehaviorSubject.create(); public String uuid; - @Inject - public AnalyticsHelper analyticsHelper; + @Inject public CrashReportController crashReportController; - @Inject - public LocationProvider locationProvider; - private PinDialog pinDialog; - private boolean comesFromImageSource = false; - - private ActivityResultObserver activityResultObserver; private CustomDialog descriptionDialog; - public enum Status { - ON_PAUSE, - ON_RESUME - } @Override protected void attachBaseContext(Context newBase) { @@ -89,130 +52,13 @@ protected void attachBaseContext(Context newBase) { ); } - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - ServerComponent serverComponent = ((App) getApplicationContext()).getServerComponent(); - if (serverComponent != null) { - serverComponent.openIdSession().setSessionCallback(this, logOutReason -> { - startActivity(LoginActivity.class, LoginActivity.Companion.bundle(true, -1, false, logOutReason), true, true, null); - return Unit.INSTANCE; - }); - if (serverComponent.userManager().isUserLoggedIn().blockingFirst() && - !serverComponent.userManager().allowScreenShare()) { - getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); - } - } - - if (!getResources().getBoolean(R.bool.is_tablet)) - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); - - SharedPreferences prefs = getSharedPreferences(); - if (this instanceof MainActivity || this instanceof LoginActivity || this instanceof SplashActivity || this instanceof AccountsActivity) { - if (serverComponent != null) { - serverComponent.themeManager().clearProgramTheme(); - } - prefs.edit().remove(Constants.PROGRAM_THEME).apply(); - } - - if (!(this instanceof SplashActivity) && - !(this instanceof LoginActivity) && - !(this instanceof AccountsActivity) && - !(this instanceof ScanActivity) - ) { - if (serverComponent != null) { - setTheme(serverComponent.themeManager().getProgramTheme()); - } else { - setTheme(R.style.AppTheme); - } - } - - super.onCreate(savedInstanceState); - } - - private void initPinDialog() { - pinDialog = new PinDialog(PinDialog.Mode.ASK, - (this instanceof LoginActivity), - () -> { - startActivity(MainActivity.class, null, true, true, null); - return null; - }, - () -> { - analyticsHelper.setEvent(AnalyticsConstants.FORGOT_CODE, AnalyticsConstants.CLICK, AnalyticsConstants.FORGOT_CODE); - if (!(this instanceof LoginActivity)) { - startActivity(LoginActivity.class, null, true, true, null); - } - return null; - } - ); - } - - @Override - protected void onResume() { - super.onResume(); - lifeCycleObservable.onNext(Status.ON_RESUME); - shouldCheckPIN(); - } - private void shouldCheckPIN() { - if (comesFromImageSource) { - ExtensionsKt.app(this).disableBackGroundFlag(); - comesFromImageSource = false; - } else { - if (ExtensionsKt.app(this).isSessionBlocked() && !(this instanceof SplashActivity)) { - if (getPinDialog() == null) { - initPinDialog(); - showPinDialog(); - } - } - } - } - - @Override - protected void onPause() { - super.onPause(); - lifeCycleObservable.onNext(Status.ON_PAUSE); - if (locationProvider != null) { - locationProvider.stopLocationUpdates(); - } - } - - @Override - protected void onStop() { - super.onStop(); - PinDialog dialog = getPinDialog(); - if (dialog != null) { - dialog.dismissAllowingStateLoss(); - } - } - - - @Override - protected void onDestroy() { - super.onDestroy(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (activityResultObserver != null) { - activityResultObserver.onRequestPermissionsResult(requestCode, permissions, grantResults); - activityResultObserver = null; - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } @Override public void setTutorial() { } - public void showPinDialog() { - pinDialog.show(getSupportFragmentManager(), PIN_DIALOG_TAG); - } - - public PinDialog getPinDialog() { - return (PinDialog) getSupportFragmentManager().findFragmentByTag(PIN_DIALOG_TAG); - } - @Override public void showTutorial(boolean shaked) { if (HelpManager.getInstance().isReady()) { @@ -246,19 +92,7 @@ public ActivityGlobalAbstract getActivity() { return ActivityGlobalAbstract.this; } - public void startActivity(@NonNull Class destination, @Nullable Bundle bundle, boolean finishCurrent, boolean finishAll, @Nullable ActivityOptionsCompat transition) { - Intent intent = new Intent(this, destination); - if (finishAll) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - if (bundle != null) - intent.putExtras(bundle); - if (transition != null) - ContextCompat.startActivity(this, intent, transition.toBundle()); - else - ContextCompat.startActivity(this, intent, null); - if (finishCurrent) - finish(); - } + public ActivityGlobalAbstract getAbstracContext() { return this; @@ -285,10 +119,6 @@ public SharedPreferences getSharedPreferences() { return getSharedPreferences(Constants.SHARE_PREFS, MODE_PRIVATE); } - public Observable observableLifeCycle() { - return lifeCycleObservable; - } - public void hideKeyboard() { if (getCurrentFocus() != null) { InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); @@ -312,12 +142,12 @@ public void showInfoDialog(String title, String message) { showInfoDialog(title, message, new OnDialogClickListener() { @Override public void onPositiveClick() { - + // no-op } @Override public void onNegativeClick() { - + // no-op } }); } @@ -343,26 +173,8 @@ public void showInfoDialog(String title, String message, String positiveButtonTe } } - @Override - public void subscribe(@NotNull ActivityResultObserver activityResultObserver) { - this.activityResultObserver = activityResultObserver; - } - @Override - public void unsubscribe() { - this.activityResultObserver = null; - } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (activityResultObserver != null) { - comesFromImageSource = true; - activityResultObserver.onActivityResult(requestCode, resultCode, data); - activityResultObserver = null; - } - - super.onActivityResult(requestCode, resultCode, data); - } @Override public void showDescription(String description) { diff --git a/app/src/main/java/org/dhis2/usescases/general/FragmentGlobalAbstract.java b/app/src/main/java/org/dhis2/usescases/general/FragmentGlobalAbstract.java index b64a006c97..e3f3b3da19 100644 --- a/app/src/main/java/org/dhis2/usescases/general/FragmentGlobalAbstract.java +++ b/app/src/main/java/org/dhis2/usescases/general/FragmentGlobalAbstract.java @@ -10,12 +10,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityOptionsCompat; -import androidx.fragment.app.Fragment; -import org.dhis2.utils.granularsync.SyncStatusDialog; import org.dhis2.commons.locationprovider.LocationProvider; import org.dhis2.utils.OnDialogClickListener; import org.dhis2.utils.analytics.AnalyticsHelper; +import org.dhis2.utils.granularsync.SyncStatusDialog; import javax.inject.Inject; @@ -23,7 +22,7 @@ * QUADRAM. Created by ppajuelo on 18/10/2017. */ -public abstract class FragmentGlobalAbstract extends Fragment implements AbstractActivityContracts.View { +public abstract class FragmentGlobalAbstract extends SessionManagerFragment implements AbstractActivityContracts.View { @Inject public LocationProvider locationProvider; @@ -34,7 +33,6 @@ public abstract class FragmentGlobalAbstract extends Fragment implements Abstrac @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return super.onCreateView(inflater, container, savedInstanceState); - } //endregion diff --git a/app/src/main/java/org/dhis2/usescases/general/SessionManagerActivity.kt b/app/src/main/java/org/dhis2/usescases/general/SessionManagerActivity.kt new file mode 100644 index 0000000000..fecc90d4d5 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/general/SessionManagerActivity.kt @@ -0,0 +1,283 @@ +package org.dhis2.usescases.general + +import android.content.Intent +import android.content.SharedPreferences +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.view.WindowManager +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import kotlinx.coroutines.Dispatchers +import org.dhis2.App +import org.dhis2.R +import org.dhis2.bindings.app +import org.dhis2.commons.ActivityResultObservable +import org.dhis2.commons.ActivityResultObserver +import org.dhis2.commons.Constants +import org.dhis2.commons.locationprovider.LocationProvider +import org.dhis2.commons.service.SessionManagerServiceImpl +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.server.OpenIdSession.LogOutReason +import org.dhis2.data.service.SyncStatusController +import org.dhis2.data.service.workManager.WorkManagerController +import org.dhis2.usescases.login.LoginActivity +import org.dhis2.usescases.login.LoginActivity.Companion.bundle +import org.dhis2.usescases.login.accounts.AccountsActivity +import org.dhis2.usescases.main.MainActivity +import org.dhis2.usescases.qrScanner.ScanActivity +import org.dhis2.usescases.splash.SplashActivity +import org.dhis2.utils.analytics.AnalyticsHelper +import org.dhis2.utils.analytics.CLICK +import org.dhis2.utils.analytics.FORGOT_CODE +import org.dhis2.utils.session.PIN_DIALOG_TAG +import org.dhis2.utils.session.PinDialog +import javax.inject.Inject + +abstract class SessionManagerActivity : AppCompatActivity(), ActivityResultObservable { + + @Inject + lateinit var sessionManagerServiceImpl: SessionManagerServiceImpl + + @Inject + lateinit var workManagerController: WorkManagerController + + @Inject + lateinit var locationProvider: LocationProvider + + fun observableLifeCycle(): Observable { + return lifeCycleObservable + } + + @Inject + lateinit var analyticsHelper: AnalyticsHelper + + private var pinDialog: PinDialog? = null + + private var lifeCycleObservable: BehaviorSubject = + BehaviorSubject.create() + + var syncStatusController: SyncStatusController = SyncStatusController( + object : DispatcherProvider { + override fun io() = Dispatchers.IO + override fun computation() = Dispatchers.Default + override fun ui() = Dispatchers.Main + }, + ) + + override fun onCreate(savedInstanceState: Bundle?) { + val serverComponent = (applicationContext as App).serverComponent + if (serverComponent != null) { + serverComponent.openIdSession() + .setSessionCallback(this) { logOutReason: LogOutReason? -> + startActivity( + LoginActivity::class.java, + bundle(true, -1, false, logOutReason), + true, + true, + null, + ) + Unit + } + if (serverComponent.userManager().isUserLoggedIn().blockingFirst() && + !serverComponent.userManager().allowScreenShare() + ) { + window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE, + ) + } + } + + if (!resources.getBoolean(R.bool.is_tablet)) { + requestedOrientation = + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + } + + val prefs = getSharedPreferences() + if (this is MainActivity || this is LoginActivity || this is SplashActivity || this is AccountsActivity) { + serverComponent?.themeManager()?.clearProgramTheme() + prefs?.edit()?.remove(Constants.PROGRAM_THEME)?.apply() + } + + if (this !is SplashActivity && + this !is LoginActivity && + this !is AccountsActivity && + this !is ScanActivity + ) { + if (serverComponent != null) { + setTheme(serverComponent.themeManager().getProgramTheme()) + } else { + setTheme(R.style.AppTheme) + } + } + + super.onCreate(savedInstanceState) + } + + private fun getSharedPreferences(): SharedPreferences? { + return getSharedPreferences(Constants.SHARE_PREFS, MODE_PRIVATE) + } + + override fun onUserInteraction() { + if (::sessionManagerServiceImpl.isInitialized && this !is SplashActivity) sessionManagerServiceImpl.onUserInteraction() + } + + private var comesFromImageSource: Boolean = false + private var activityResultObserver: ActivityResultObserver? = null + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + ) { + if (activityResultObserver != null) { + activityResultObserver!!.onRequestPermissionsResult( + requestCode, + permissions, + grantResults, + ) + activityResultObserver = null + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun subscribe(activityResultObserver: ActivityResultObserver) { + this.activityResultObserver = activityResultObserver + } + + private fun initPinDialog() { + pinDialog = PinDialog( + PinDialog.Mode.ASK, + (this is LoginActivity), + { + startActivity(MainActivity::class.java, null, true, true, null) + null + }, + { + analyticsHelper.setEvent(FORGOT_CODE, CLICK, FORGOT_CODE) + if (this !is LoginActivity) { + startActivity(LoginActivity::class.java, null, true, true, null) + } + null + }, + ) + } + + override fun unsubscribe() { + this.activityResultObserver = null + } + + override fun onPause() { + super.onPause() + lifeCycleObservable.onNext(Status.ON_PAUSE) + if (::locationProvider.isInitialized) { + locationProvider.stopLocationUpdates() + } + } + + fun startActivity( + destination: Class<*>, + bundle: Bundle?, + finishCurrent: Boolean, + finishAll: Boolean, + transition: ActivityOptionsCompat?, + ) { + val intent = Intent(this, destination) + if (finishAll) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + if (bundle != null) intent.putExtras(bundle) + if (transition != null) { + ContextCompat.startActivity(this, intent, transition.toBundle()) + } else { + ContextCompat.startActivity(this, intent, null) + } + if (finishCurrent) finish() + } + + private fun showPinDialog() { + pinDialog!!.show(supportFragmentManager, PIN_DIALOG_TAG) + } + + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (activityResultObserver != null && sessionManagerServiceImpl.isUserLoggedIn()) { + comesFromImageSource = true + activityResultObserver!!.onActivityResult(requestCode, resultCode, data) + activityResultObserver = null + } + super.onActivityResult(requestCode, resultCode, data) + } + + private fun checkSessionTimeout() { + if (::sessionManagerServiceImpl.isInitialized && sessionManagerServiceImpl.checkSessionTimeout({ accountsCount -> sessionAction(accountsCount) }, lifecycleScope) && this !is LoginActivity) { + workManagerController.cancelAllWork() + syncStatusController.restore() + } + } + + override fun onStop() { + super.onStop() + val dialog = pinDialog + dialog?.dismissAllowingStateLoss() + } + + override fun onDestroy() { + super.onDestroy() + } + + override fun onResume() { + super.onResume() + lifeCycleObservable.onNext(Status.ON_RESUME) + shouldCheckPIN() + } + + enum class Status { + ON_PAUSE, + ON_RESUME, + } + + private fun shouldCheckPIN() { + if (comesFromImageSource) { + this.app().disableBackGroundFlag() + comesFromImageSource = false + } else { + if (this.app().isSessionBlocked && this !is SplashActivity) { + if (pinDialog == null) { + initPinDialog() + showPinDialog() + } + } else { + if (this !is LoginActivity && this !is SplashActivity) { + checkSessionTimeout() + } + } + } + } + + private fun sessionAction(accountsCount: Int) { + if (this.app().isSessionBlocked && this !is SplashActivity) { + if (pinDialog == null) { + initPinDialog() + showPinDialog() + } + } else { + navigateToLogin(accountsCount) + } + } + + private fun navigateToLogin(accountsCount: Int) { + startActivity( + LoginActivity::class.java, + LoginActivity.bundle( + accountsCount = accountsCount, + isDeletion = false, + ), + true, + true, + null, + ) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/general/SessionManagerFragment.kt b/app/src/main/java/org/dhis2/usescases/general/SessionManagerFragment.kt new file mode 100644 index 0000000000..aa14ec1976 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/general/SessionManagerFragment.kt @@ -0,0 +1,15 @@ +package org.dhis2.usescases.general + +import androidx.fragment.app.Fragment +import org.dhis2.commons.service.SessionManagerServiceImpl +import javax.inject.Inject + +abstract class SessionManagerFragment : Fragment() { + + @Inject + lateinit var sessionManagerServiceImpl: SessionManagerServiceImpl + + fun isUserLoggedIn(): Boolean { + return (::sessionManagerServiceImpl.isInitialized && sessionManagerServiceImpl.isUserLoggedIn()) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/jira/JiraComponent.kt b/app/src/main/java/org/dhis2/usescases/jira/JiraComponent.kt deleted file mode 100644 index 666fbbd31d..0000000000 --- a/app/src/main/java/org/dhis2/usescases/jira/JiraComponent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.dhis2.usescases.jira - -import dagger.Subcomponent -import org.dhis2.commons.di.dagger.PerFragment - -@PerFragment -@Subcomponent(modules = [JiraModule::class]) -interface JiraComponent { - fun inject(jiraFragment: JiraFragment) -} diff --git a/app/src/main/java/org/dhis2/usescases/jira/JiraFragment.kt b/app/src/main/java/org/dhis2/usescases/jira/JiraFragment.kt deleted file mode 100644 index 47f72688f7..0000000000 --- a/app/src/main/java/org/dhis2/usescases/jira/JiraFragment.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.dhis2.usescases.jira - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.provider.Browser -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.viewModels -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration -import org.dhis2.data.jira.ClickedIssueData -import org.dhis2.data.jira.JiraIssuesResult -import org.dhis2.databinding.FragmentJiraBinding -import org.dhis2.usescases.general.FragmentGlobalAbstract -import org.dhis2.usescases.main.MainActivity -import org.dhis2.utils.NetworkUtils -import javax.inject.Inject - -class JiraFragment : FragmentGlobalAbstract() { - @Inject - lateinit var jiraViewModelFactory: JiraViewModelFactory - private val jiraModel: JiraViewModel by viewModels { - jiraViewModelFactory - } - private val jiraIssueAdapter by lazy { - JiraIssueAdapter { jiraModel.onJiraIssueClick(it) } - } - - override fun onAttach(context: Context) { - super.onAttach(context) - if (context is MainActivity) { - context.mainComponent.plus(JiraModule()).inject(this) - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View? { - return FragmentJiraBinding.inflate(inflater, container, false).apply { - jiraViewModel = jiraModel - rememberCheck.setOnCheckedChangeListener { _, isChecked -> - jiraModel.onCheckedChanged(isChecked) - } - sendReportButton.isEnabled = NetworkUtils.isOnline(context) - issueRecycler.apply { - adapter = jiraIssueAdapter - addItemDecoration( - DividerItemDecoration( - context, - DividerItemDecoration.VERTICAL, - ), - ) - } - jiraModel.apply { - init() - issueListResponse.observe(viewLifecycleOwner, Observer { handleListResponse(it) }) - issueMessage.observe(viewLifecycleOwner, Observer { handleMessage(it) }) - clickedIssueData.observe( - viewLifecycleOwner, - Observer { openJiraTicketInBrowser(it) }, - ) - } - }.root - } - - private fun handleListResponse(result: JiraIssuesResult) { - if (result.isSuccess()) { - jiraIssueAdapter.submitList(result.issues) - } else { - displayMessage(result.errorMessage) - } - } - - private fun handleMessage(message: String) { - displayMessage(message) - } - - private fun openJiraTicketInBrowser(clickedIssueData: ClickedIssueData) { - val browserIntent = Intent( - Intent.ACTION_VIEW, - Uri.parse(clickedIssueData.uriString), - ).apply { - val bundle = Bundle().apply { - putString(clickedIssueData.authHeader(), clickedIssueData.basicAuth()) - } - putExtra(Browser.EXTRA_HEADERS, bundle) - } - startActivity(browserIntent) - } -} diff --git a/app/src/main/java/org/dhis2/usescases/jira/JiraIssueAdapter.kt b/app/src/main/java/org/dhis2/usescases/jira/JiraIssueAdapter.kt deleted file mode 100644 index dc64e74501..0000000000 --- a/app/src/main/java/org/dhis2/usescases/jira/JiraIssueAdapter.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.dhis2.usescases.jira - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import org.dhis2.data.jira.JiraIssue -import org.dhis2.databinding.JiraIssueItemBinding - -class JiraIssueAdapter(private val onJiraIssueClick: (String) -> Unit) : - ListAdapter( - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: JiraIssue, newItem: JiraIssue): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: JiraIssue, newItem: JiraIssue): Boolean { - return oldItem == newItem - } - }, - ) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): JiraIssueHolder { - return JiraIssueHolder( - JiraIssueItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false, - ), - onJiraIssueClick, - ) - } - - override fun onBindViewHolder(holder: JiraIssueHolder, position: Int) { - holder.bind(getItem(position)) - } -} diff --git a/app/src/main/java/org/dhis2/usescases/jira/JiraIssueHolder.kt b/app/src/main/java/org/dhis2/usescases/jira/JiraIssueHolder.kt deleted file mode 100644 index e7d6f604f7..0000000000 --- a/app/src/main/java/org/dhis2/usescases/jira/JiraIssueHolder.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.dhis2.usescases.jira - -import androidx.recyclerview.widget.RecyclerView -import org.dhis2.data.jira.JiraIssue -import org.dhis2.databinding.JiraIssueItemBinding - -class JiraIssueHolder( - private val binding: JiraIssueItemBinding, - private val onJiraIssueClick: (String) -> Unit, -) : RecyclerView.ViewHolder(binding.root) { - fun bind(jiraIssue: JiraIssue) { - binding.apply { - issue = jiraIssue - jiraCard.setOnClickListener { onJiraIssueClick(jiraIssue.key) } - } - } -} diff --git a/app/src/main/java/org/dhis2/usescases/jira/JiraIssueService.kt b/app/src/main/java/org/dhis2/usescases/jira/JiraIssueService.kt deleted file mode 100644 index 9ffe574b32..0000000000 --- a/app/src/main/java/org/dhis2/usescases/jira/JiraIssueService.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.dhis2.usescases.jira - -import io.reactivex.Single -import okhttp3.RequestBody -import okhttp3.ResponseBody -import org.dhis2.data.jira.JiraIssueListResponse -import retrofit2.http.Body -import retrofit2.http.Header -import retrofit2.http.POST - -interface JiraIssueService { - @POST("rest/api/2/issue") - fun createIssue( - @Header("Authorization") - auth: String?, - @Body issueRequest: RequestBody, - ): Single - - @POST("rest/api/2/search") - fun getJiraIssues( - @Header("Authorization") - auth: String?, - @Body issueRequest: RequestBody, - ): Single -} diff --git a/app/src/main/java/org/dhis2/usescases/jira/JiraModule.kt b/app/src/main/java/org/dhis2/usescases/jira/JiraModule.kt deleted file mode 100644 index 65e34eedd9..0000000000 --- a/app/src/main/java/org/dhis2/usescases/jira/JiraModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.dhis2.usescases.jira - -import dagger.Module -import dagger.Provides -import org.dhis2.commons.di.dagger.PerFragment -import org.dhis2.commons.prefs.PreferenceProvider -import org.dhis2.commons.resources.ResourceManager -import org.dhis2.commons.schedulers.SchedulerProvider - -@Module -class JiraModule { - @Provides - @PerFragment - fun jiraViewModelFactory( - preferenceProvider: PreferenceProvider, - resourceManager: ResourceManager, - schedulerProvider: SchedulerProvider, - ): JiraViewModelFactory { - return JiraViewModelFactory(preferenceProvider, resourceManager, schedulerProvider) - } -} diff --git a/app/src/main/java/org/dhis2/usescases/jira/JiraRepository.kt b/app/src/main/java/org/dhis2/usescases/jira/JiraRepository.kt deleted file mode 100644 index ba138143b2..0000000000 --- a/app/src/main/java/org/dhis2/usescases/jira/JiraRepository.kt +++ /dev/null @@ -1,67 +0,0 @@ -package org.dhis2.usescases.jira - -import android.util.Base64 -import com.google.gson.Gson -import io.reactivex.Single -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.ResponseBody -import org.dhis2.commons.Constants -import org.dhis2.commons.prefs.PreferenceProvider -import org.dhis2.data.jira.IssueRequest -import org.dhis2.data.jira.JiraIssueListRequest -import org.dhis2.data.jira.JiraIssueListResponse -import org.dhis2.data.jira.toBasicAuth -import org.dhis2.data.jira.toJiraJql - -class JiraRepository( - private val jiraApi: JiraIssueService, - private val prefs: PreferenceProvider, -) { - private var session: String? = prefs.getString(Constants.JIRA_AUTH, null) - private var userName: String? = prefs.getString(Constants.JIRA_USER, null) - - fun hasJiraSessionSaved(): Boolean { - return prefs.contains(Constants.JIRA_USER) - } - - fun getJiraIssues(userName: String? = null): Single { - userName?.let { this.userName = userName } - val basic = session?.toBasicAuth() - val request = JiraIssueListRequest( - prefs.getString(Constants.JIRA_USER, this.userName)!!.toJiraJql(), - MAX_RESULTS, - ) - - val requestBody = Gson().toJson(request) - .toRequestBody(MEDIA_TYPE_APPLICATION_JSON.toMediaTypeOrNull()) - return jiraApi.getJiraIssues(basic, requestBody) - } - - fun sendJiraIssue(summary: String, description: String): Single { - val basic = session?.toBasicAuth() - val issueRequest = IssueRequest(summary, description) - val requestBody = Gson().toJson(issueRequest) - .toRequestBody(MEDIA_TYPE_APPLICATION_JSON.toMediaTypeOrNull()) - return jiraApi.createIssue(basic, requestBody) - } - - fun saveCredentials() { - session?.let { prefs.saveJiraCredentials(it) } - } - - fun setAuth(userName: String, pass: String) { - session = Base64.encodeToString( - BASIC_AUTH_CODE.format(userName, pass).toByteArray(), - Base64.NO_WRAP, - ) - } - - fun getSession(): String? { - return session - } - - fun closeSession() { - session = null - } -} diff --git a/app/src/main/java/org/dhis2/usescases/jira/JiraViewModel.kt b/app/src/main/java/org/dhis2/usescases/jira/JiraViewModel.kt deleted file mode 100644 index bb28a895ad..0000000000 --- a/app/src/main/java/org/dhis2/usescases/jira/JiraViewModel.kt +++ /dev/null @@ -1,155 +0,0 @@ -package org.dhis2.usescases.jira - -import androidx.databinding.ObservableField -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import io.reactivex.disposables.CompositeDisposable -import org.dhis2.commons.resources.ResourceManager -import org.dhis2.commons.schedulers.SchedulerProvider -import org.dhis2.data.jira.ClickedIssueData -import org.dhis2.data.jira.JiraIssuesResult -import org.dhis2.data.jira.toJiraIssueUri -import timber.log.Timber - -const val MEDIA_TYPE_APPLICATION_JSON = "application/json" -const val BASIC_AUTH_CODE = "%s:%s" -const val MAX_RESULTS = 20 - -class JiraViewModel( - private val jiraRepository: JiraRepository, - private val resources: ResourceManager, - private val schedulerProvider: SchedulerProvider, -) : ViewModel() { - - private var disposable = CompositeDisposable() - - private var userName: String? = null - private var pass: String? = null - - private var rememberCredentials = jiraRepository.hasJiraSessionSaved() - private var summary: String = "" - private var description: String = "" - - val formCompleted = ObservableField(false) - val isSessionOpen = ObservableField(false) - - val issueListResponse = MutableLiveData() - var issueMessage: MutableLiveData = MutableLiveData() - val clickedIssueData: MutableLiveData = MutableLiveData() - - fun init() { - if (jiraRepository.hasJiraSessionSaved()) { - getJiraTickets() - } - } - - fun openSession() { - if (!userName.isNullOrEmpty() && !pass.isNullOrEmpty()) { - jiraRepository.setAuth(userName!!, pass!!) - getJiraTickets() - } - } - - fun closeSession() { - jiraRepository.closeSession() - rememberCredentials = false - isSessionOpen.set(false) - } - - fun sendIssue() { - disposable.add( - jiraRepository.sendJiraIssue(summary, description) - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { handleSendResponse(true) }, - { handleThrowableResponse(it) }, - ), - ) - } - - fun getJiraTickets() { - disposable.add( - jiraRepository.getJiraIssues(userName) - .map { JiraIssuesResult(it.issues) } - .onErrorReturn { JiraIssuesResult(errorMessage = it.message) } - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { handleResponse(it) }, - { t -> Timber.e(t) }, - ), - ) - } - - fun onSummaryChanged(s: CharSequence, start: Int, before: Int, count: Int) { - summary = s.toString() - checkFormCompletion() - } - - fun onDescriptionChanged(s: CharSequence, start: Int, before: Int, count: Int) { - description = s.toString() - checkFormCompletion() - } - - fun onJiraUserChanged(s: CharSequence, start: Int, before: Int, count: Int) { - userName = s.toString() - } - - fun onJiraPassChanged(s: CharSequence, start: Int, before: Int, count: Int) { - pass = s.toString() - } - - fun onCheckedChanged(isChecked: Boolean) { - rememberCredentials = isChecked - } - - fun onJiraIssueClick(jiraKey: String) { - clickedIssueData.value = ClickedIssueData( - jiraKey.toJiraIssueUri(), - jiraRepository.getSession() ?: "", - ) - } - - private fun saveCredentialsIfNeeded() { - if (rememberCredentials) { - jiraRepository.saveCredentials() - } - } - - private fun handleSendResponse(responseIsSuccessful: Boolean) { - if (responseIsSuccessful) { - issueMessage.value = resources.jiraIssueSentMessage() - getJiraTickets() - } else { - issueMessage.value = resources.jiraIssueSentErrorMessage() - } - } - - private fun handleThrowableResponse(throwable: Throwable) { - issueMessage.value = throwable.localizedMessage - } - - private fun handleResponse(jiraIssuesResult: JiraIssuesResult) { - if (jiraIssuesResult.isSuccess()) { - saveCredentialsIfNeeded() - isSessionOpen.set(true) - } else { - closeSession() - } - issueListResponse.value = jiraIssuesResult - } - - private fun checkFormCompletion() { - val check = summary.isNotEmpty() && - description.isNotEmpty() && - isSessionOpen.get() ?: false - - formCompleted.set(check) - } - - override fun onCleared() { - disposable.clear() - super.onCleared() - } -} diff --git a/app/src/main/java/org/dhis2/usescases/jira/JiraViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/jira/JiraViewModelFactory.kt deleted file mode 100644 index 13a0962be1..0000000000 --- a/app/src/main/java/org/dhis2/usescases/jira/JiraViewModelFactory.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.dhis2.usescases.jira - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import okhttp3.OkHttpClient -import org.dhis2.commons.prefs.PreferenceProvider -import org.dhis2.commons.resources.ResourceManager -import org.dhis2.commons.schedulers.SchedulerProvider -import retrofit2.Retrofit -import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory -import retrofit2.converter.gson.GsonConverterFactory - -const val JIRA_URL = "https://jira.dhis2.org/" - -@Suppress("UNCHECKED_CAST") -class JiraViewModelFactory( - val preferenceProvider: PreferenceProvider, - val resourceManager: ResourceManager, - val schedulerProvider: SchedulerProvider, -) : ViewModelProvider.Factory { - - private fun jiraService(): JiraIssueService { - val retrofit = Retrofit.Builder() - .baseUrl(JIRA_URL) - .client(OkHttpClient()) - .addConverterFactory(GsonConverterFactory.create()) - .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .validateEagerly(true) - .build() - - return retrofit.create(JiraIssueService::class.java) - } - - override fun create(modelClass: Class): T { - return JiraViewModel( - JiraRepository(jiraService(), preferenceProvider), - resourceManager, - schedulerProvider, - ) as T - } -} diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt index 0983a16df2..a1570e4382 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt @@ -15,8 +15,14 @@ import android.webkit.URLUtil import android.widget.ArrayAdapter import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.databinding.DataBindingUtil import com.google.android.material.composethemeadapter.MdcTheme import com.google.gson.Gson @@ -33,14 +39,13 @@ import org.dhis2.commons.Constants.SESSION_DIALOG_RQ import org.dhis2.commons.dialogs.CustomDialog import org.dhis2.commons.extensions.closeKeyboard import org.dhis2.commons.resources.ResourceManager -import org.dhis2.data.fingerprint.FingerPrintResult -import org.dhis2.data.fingerprint.Type import org.dhis2.data.server.OpenIdSession import org.dhis2.data.server.UserManager import org.dhis2.databinding.ActivityLoginBinding import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle +import org.dhis2.ui.theme.Dhis2Theme import org.dhis2.usescases.about.PolicyView import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.usescases.login.accounts.AccountsActivity @@ -73,6 +78,7 @@ const val EXTRA_SESSION_EXPIRED = "EXTRA_SESSION_EXPIRED" const val EXTRA_ACCOUNT_DISABLED = "EXTRA_ACCOUNT_DISABLED" const val IS_DELETION = "IS_DELETION" const val ACCOUNTS_COUNT = "ACCOUNTS_COUNT" +const val FROM_SPLASH = "FROM_SPLASH" const val RESULT_ACCOUNT_SERVER = "RESULT_ACCOUNT_SERVER" const val RESULT_ACCOUNT_USERNAME = "RESULT_ACCOUNT_USERNAME" const val RESULT_ACCOUNT_CLICKED = "RESULT_ACCOUNT_CLICKED" @@ -135,11 +141,13 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { accountsCount: Int = -1, isDeletion: Boolean = false, logOutReason: OpenIdSession.LogOutReason? = null, + fromSplash: Boolean = false, ): Bundle { return Bundle().apply { putBoolean(EXTRA_SKIP_SYNC, skipSync) putBoolean(IS_DELETION, isDeletion) putInt(ACCOUNTS_COUNT, accountsCount) + putBoolean(FROM_SPLASH, fromSplash) when (logOutReason) { OpenIdSession.LogOutReason.OPEN_ID -> putBoolean(EXTRA_SESSION_EXPIRED, true) OpenIdSession.LogOutReason.DISABLED_ACCOUNT -> putBoolean( @@ -161,6 +169,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { ): Intent = Intent().apply { serverUrl?.let { putExtra(RESULT_ACCOUNT_SERVER, serverUrl) } userName?.let { putExtra(RESULT_ACCOUNT_USERNAME, userName) } + putExtra(FROM_SPLASH, false) putExtra(RESULT_ACCOUNT_CLICKED, wasAccountClicked) } } @@ -181,6 +190,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { super.onCreate(savedInstanceState) val accountsCount = intent.getIntExtra(ACCOUNTS_COUNT, -1) val isDeletion = intent.getBooleanExtra(IS_DELETION, false) + val fromSplash = intent.getBooleanExtra(FROM_SPLASH, false) if ((isDeletion && accountsCount >= 1)) { openAccountsActivity() @@ -207,6 +217,8 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { } } + provideBiometricButton() + binding.presenter = presenter setLoginVisibility(false) @@ -260,6 +272,11 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { checkMessage() presenter.apply { checkServerInfoAndShowBiometricButton() + canLoginWithBiometrics.observe(this@LoginActivity) { + if (it && fromSplash) { + presenter.authenticateWithBiometric() + } + } } if (!isDeletion && accountsCount == 1) { @@ -267,6 +284,27 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { } } + private fun provideBiometricButton() { + binding.biometricButton.setContent { + val displayBiometric by presenter.canLoginWithBiometrics.observeAsState(false) + if (displayBiometric) { + Dhis2Theme { + IconButton( + onClick = { + presenter.authenticateWithBiometric() + }, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_fingerprint), + tint = MaterialTheme.colorScheme.primary, + contentDescription = stringResource(id = R.string.fingerprint_title), + ) + } + } + } + } + } + private fun checkUrl(urlString: String): Boolean { return URLUtil.isValidUrl(urlString) && Patterns.WEB_URL.matcher(urlString).matches() && urlString.toHttpUrlOrNull() != null @@ -321,11 +359,11 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { return NetworkUtils.isOnline(this) } - override fun setUrl(url: String) { + override fun setUrl(url: String?) { binding.serverUrlEdit.setText(if (!isEmpty(qrUrl)) qrUrl else url) } - override fun setUser(user: String) { + override fun setUser(user: String?) { binding.userNameEdit.setText(user) binding.userNameEdit.setSelectAllOnFocus(true) } @@ -378,8 +416,9 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { message = getString(R.string.improve_app_msg_text), clickableWord = getString(R.string.improve_app_msg_clickable_word), iconResource = R.drawable.ic_line_chart, + headerTextAlignment = TextAlign.Start, mainButton = DialogButtonStyle.MainButton(textResource = R.string.yes), - secondaryButton = DialogButtonStyle.SecondaryButton(textResource = R.string.no), + secondaryButton = DialogButtonStyle.SecondaryButton(textResource = R.string.not_now), ), onMainButtonClicked = { presenter.grantTrackingPermissions(true) @@ -442,45 +481,32 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { private fun onLoginDataUpdated(displayTrackingMessage: Boolean) { when { displayTrackingMessage -> showCrashlyticsDialog() - !presenter.areSameCredentials() -> { - handleFingerPrint() + presenter.shouldAskForBiometrics() -> showBiometricDialog() + else -> { + presenter.saveUserCredentials() goToNextScreen() } - - else -> goToNextScreen() } } - private fun handleFingerPrint() { - // This is commented until fingerprint login for multiuser is supported - /* if (presenter.canHandleBiometrics() == true) { - showInfoDialog( - getString(R.string.biometrics_security_title), - getString(R.string.biometrics_security_text), - object : OnDialogClickListener { - override fun onPositiveClick() { - presenter.saveUserCredentials( - binding.serverUrlEdit.text.toString(), - binding.userNameEdit.text.toString(), - binding.userPassEdit.text.toString() - ) - goToNextScreen() - } - - override fun onNegativeClick() { - goToNextScreen() - } - } - ) - goToNextScreen() - } else { - presenter.saveUserCredentials( - binding.serverUrlEdit.text.toString(), - binding.userNameEdit.text.toString(), - "" - ) - goToNextScreen() - } */ + private fun showBiometricDialog() { + BottomSheetDialog( + BottomSheetDialogUiModel( + title = getString(R.string.biometrics_login_title), + message = getString(R.string.biometrics_login_text), + iconResource = R.drawable.ic_fingerprint, + mainButton = DialogButtonStyle.MainButton(textResource = R.string.yes), + secondaryButton = DialogButtonStyle.SecondaryButton(textResource = R.string.not_now), + ), + onMainButtonClicked = { + presenter.saveUserCredentials(binding.userPassEdit.text.toString()) + onLoginDataUpdated(false) + }, + onSecondaryButtonClicked = { + presenter.saveUserCredentials() + onLoginDataUpdated(false) + }, + ).show(supportFragmentManager, BottomSheetDialog::class.simpleName) } @Deprecated("Deprecated in Java") @@ -509,8 +535,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { } override fun showBiometricButton() { - // This is commented until fingerprint login for multiuser is supported - // binding.biometricButton.visibility = View.VISIBLE + binding.biometricButton.visibility = View.VISIBLE } private val requestQRScanner = registerForActivityResult( @@ -578,14 +603,10 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { } } - override fun showCredentialsData(result: FingerPrintResult, vararg args: String) { - if (result.type == Type.SUCCESS) { - binding.serverUrlEdit.setText(args[0]) - binding.userNameEdit.setText(args[1]) - binding.userPassEdit.setText(args[2]) - } else if (result.type == Type.ERROR && args[0] != getString(R.string.cancel)) { - showInfoDialog(getString(R.string.biometrics_dialog_title), args[0]) - } + override fun showCredentialsData(vararg args: String) { + binding.serverUrlEdit.setText(args[0]) + binding.userNameEdit.setText(args[1]) + binding.userPassEdit.setText(args[2]) } override fun showEmptyCredentialsMessage() { diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginComponent.kt b/app/src/main/java/org/dhis2/usescases/login/LoginComponent.kt index cf9057ce09..0624776e34 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginComponent.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginComponent.kt @@ -2,10 +2,10 @@ package org.dhis2.usescases.login import dagger.Subcomponent import org.dhis2.commons.di.dagger.PerActivity -import org.dhis2.data.fingerprint.FingerPrintModule +import org.dhis2.data.biometric.BiometricModule @PerActivity -@Subcomponent(modules = [LoginModule::class, FingerPrintModule::class]) +@Subcomponent(modules = [LoginModule::class, BiometricModule::class]) interface LoginComponent { fun inject(loginActivity: LoginActivity) } diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginContracts.kt b/app/src/main/java/org/dhis2/usescases/login/LoginContracts.kt index cdb1882b88..ea2c459215 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginContracts.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginContracts.kt @@ -1,7 +1,6 @@ package org.dhis2.usescases.login import androidx.annotation.UiThread -import org.dhis2.data.fingerprint.FingerPrintResult import org.dhis2.data.server.UserManager import org.dhis2.usescases.general.AbstractActivityContracts import org.hisp.dhis.android.core.user.openid.IntentWithRequestCode @@ -30,9 +29,9 @@ class LoginContracts { fun goToNextScreen() - fun setUrl(url: String) + fun setUrl(url: String?) - fun setUser(user: String) + fun setUser(user: String?) fun navigateToQRActivity() @@ -46,7 +45,7 @@ class LoginContracts { fun openAccountRecovery() fun alreadyAuthenticated() - fun showCredentialsData(result: FingerPrintResult, vararg args: String) + fun showCredentialsData(vararg args: String) fun showEmptyCredentialsMessage() fun setTestingCredentials() fun getDefaultServerProtocol(): String diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginModule.kt b/app/src/main/java/org/dhis2/usescases/login/LoginModule.kt index 136c7f1557..a507e62ecd 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginModule.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginModule.kt @@ -13,8 +13,9 @@ import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.fingerprint.FingerPrintController +import org.dhis2.data.biometric.BiometricController import org.dhis2.data.server.UserManager +import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.usescases.login.auth.OpenIdProviders import org.dhis2.utils.analytics.AnalyticsHelper @@ -25,6 +26,9 @@ class LoginModule( private val userManager: UserManager?, ) { + @Provides + fun provideActivity(): ActivityGlobalAbstract = view.abstractActivity + @Provides @PerActivity fun provideResourceManager( @@ -38,7 +42,7 @@ class LoginModule( resourceManager: ResourceManager, schedulerProvider: SchedulerProvider, dispatcherProvider: DispatcherProvider, - fingerPrintController: FingerPrintController, + biometricController: BiometricController, analyticsHelper: AnalyticsHelper, crashReportController: CrashReportController, networkUtils: NetworkUtils, @@ -51,7 +55,7 @@ class LoginModule( resourceManager, schedulerProvider, dispatcherProvider, - fingerPrintController, + biometricController, analyticsHelper, crashReportController, networkUtils, diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt index e50a123ff2..d2cded6460 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt @@ -1,7 +1,6 @@ package org.dhis2.usescases.login import android.content.Intent -import android.os.Build import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -29,8 +28,7 @@ import org.dhis2.commons.reporting.CrashReportController import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.fingerprint.FingerPrintController -import org.dhis2.data.fingerprint.Type +import org.dhis2.data.biometric.BiometricController import org.dhis2.data.server.UserManager import org.dhis2.usescases.main.MainActivity import org.dhis2.utils.TestingCredential @@ -45,7 +43,6 @@ import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.hisp.dhis.android.core.systeminfo.SystemInfo import org.hisp.dhis.android.core.user.openid.OpenIDConnectConfig -import retrofit2.Response import timber.log.Timber import java.io.File @@ -57,7 +54,7 @@ class LoginViewModel( private val resourceManager: ResourceManager, private val schedulers: SchedulerProvider, private val dispatchers: DispatcherProvider, - private val fingerPrintController: FingerPrintController, + private val biometricController: BiometricController, private val analyticsHelper: AnalyticsHelper, private val crashReportController: CrashReportController, private val network: NetworkUtils, @@ -67,8 +64,6 @@ class LoginViewModel( private val syncIsPerformedInteractor = SyncIsPerformedInteractor(userManager) var disposable: CompositeDisposable = CompositeDisposable() - private var canHandleBiometrics: Boolean? = null - val serverUrl = MutableLiveData() val userName = MutableLiveData() val password = MutableLiveData() @@ -81,6 +76,9 @@ class LoginViewModel( private val _hasAccounts = MutableLiveData() val hasAccounts: LiveData = _hasAccounts + private val _canLoginWithBiometrics = MutableLiveData() + val canLoginWithBiometrics: LiveData = _canLoginWithBiometrics + private val _displayMoreActions = MutableLiveData(true) val displayMoreActions: LiveData = _displayMoreActions @@ -107,17 +105,16 @@ class LoginViewModel( ) val user = preferenceProvider.getString(SECURE_USER_NAME, "") if (!serverUrl.isNullOrEmpty() && !user.isNullOrEmpty()) { - view.setUrl(serverUrl) - view.setUser(user) + setAccountInfo(serverUrl, user) } else { - view.setUrl(view.getDefaultServerProtocol()) + setAccountInfo(view.getDefaultServerProtocol(), null) } } }, { exception -> Timber.e(exception) }, ), ) - } ?: view.setUrl(view.getDefaultServerProtocol()) + } ?: setAccountInfo(view.getDefaultServerProtocol(), null) displayManageAccount() } @@ -134,34 +131,34 @@ class LoginViewModel( .observeOn(schedulers.ui()) .subscribe( { systemInfo -> - if (systemInfo.contextPath() != null) { - view.setUrl(systemInfo.contextPath() ?: "") - view.setUser(userManager.userName().blockingGet()) - } else { - val isSessionLocked = - preferenceProvider.getBoolean(SESSION_LOCKED, false) - if (!isSessionLocked) { - val serverUrl = - preferenceProvider.getString( - SECURE_SERVER_URL, - view.getDefaultServerProtocol(), - ) - val user = preferenceProvider.getString(SECURE_USER_NAME, "") - if (!serverUrl.isNullOrEmpty() && !user.isNullOrEmpty()) { - view.setUrl(serverUrl) - view.setUser(user) - } - } else { - view.setUrl(view.getDefaultServerProtocol()) - } - } + setServerAndUserInfo(systemInfo.contextPath()) + checkBiometricVisibility() }, { Timber.e(it) }, ), ) - } ?: view.setUrl(view.getDefaultServerProtocol()) + } ?: setAccountInfo(view.getDefaultServerProtocol(), null) + } + + private fun setServerAndUserInfo(contextPath: String?) { + contextPath?.let { + setAccountInfo(it, userManager?.userName()?.blockingGet()) + } ?: { + val isSessionLocked = + preferenceProvider.getBoolean(SESSION_LOCKED, false) + val serverUrl = + preferenceProvider.getString( + SECURE_SERVER_URL, + view.getDefaultServerProtocol(), + ) + val user = preferenceProvider.getString(SECURE_USER_NAME, "") - showBiometricButtonIfVersionIsGreaterThanM(view) + if (!isSessionLocked && !serverUrl.isNullOrEmpty() && !user.isNullOrEmpty()) { + setAccountInfo(serverUrl, user) + } else { + setAccountInfo(view.getDefaultServerProtocol(), null) + } + } } private fun getSystemInfoIfUserIsLogged(userManager: UserManager): SystemInfo { @@ -175,31 +172,19 @@ class LoginViewModel( } } - private fun showBiometricButtonIfVersionIsGreaterThanM(view: LoginContracts.View) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - disposable.add( - Observable.just(fingerPrintController.hasFingerPrint()) - .filter { canHandleBiometrics -> - this.canHandleBiometrics = canHandleBiometrics - canHandleBiometrics && preferenceProvider.contains(SECURE_SERVER_URL) - } - .subscribeOn(schedulers.io()) - .observeOn(schedulers.ui()) - .subscribe( - { view.showBiometricButton() }, - { Timber.e(it) }, - ), - ) - } + fun checkBiometricVisibility() { + _canLoginWithBiometrics.value = + biometricController.hasBiometric() && + userManager?.d2?.userModule()?.accountManager()?.getAccounts()?.count() == 1 && + preferenceProvider.getString(SECURE_SERVER_URL)?.let { it == serverUrl.value } ?: false && + preferenceProvider.contains(SECURE_PASS) } fun onLoginButtonClick() { try { view.hideKeyboard() analyticsHelper.setEvent(LOGIN, CLICK, LOGIN) - increment() logIn() - decrement() } catch (throwable: Throwable) { Timber.e(throwable) handleError(throwable) @@ -208,6 +193,7 @@ class LoginViewModel( private fun logIn() { _loginProgressVisible.postValue(true) + increment() disposable.add( Observable.just(view.initLogin()) .flatMap { userManager -> @@ -223,11 +209,14 @@ class LoginViewModel( setValue(SESSION_LOCKED, false) } deletePin() - Response.success(null) + Result.success(null) } } } - .doOnTerminate { _loginProgressVisible.postValue(false) } + .doOnTerminate { + decrement() + _loginProgressVisible.postValue(false) + } .subscribeOn(schedulers.io()) .observeOn(schedulers.ui()) .subscribe( @@ -266,13 +255,13 @@ class LoginViewModel( userManager?.let { userManager -> disposable.add( userManager.handleAuthData(serverUrl, data, requestCode) - .map> { + .map { run { with(preferenceProvider) { setValue(SESSION_LOCKED, false) } deletePin() - Response.success(null) + Result.success(null) } }.subscribeOn(schedulers.io()) .observeOn(schedulers.ui()) @@ -318,8 +307,8 @@ class LoginViewModel( } @VisibleForTesting - fun handleResponse(userResponse: Response<*>) { - if (userResponse.isSuccessful) { + fun handleResponse(userResponse: Result<*>) { + if (userResponse.isSuccess) { updateServerUrls() updateLoginUsers() val displayTrackingMessage = hasToDisplayTrackingMessage() @@ -366,80 +355,21 @@ class LoginViewModel( ?.blockingGet()?.value() == null } - fun canHandleBiometrics(): Boolean? { - return canHandleBiometrics - } - - fun areSameCredentials(): Boolean { - return ( - preferenceProvider.areCredentialsSet() && - preferenceProvider.areSameCredentials( - serverUrl.value!!, - userName.value!!, - password.value!!, - ) - ).also { areSameCredentials -> if (!areSameCredentials) saveUserCredentials() } - } - - private fun saveUserCredentials() { - preferenceProvider.saveUserCredentials( - serverUrl.value!!, - userName.value!!, - "", - ) + fun saveUserCredentials(userPass: String? = null) { + if (!preferenceProvider.areSameCredentials(serverUrl.value!!, userName.value!!)) { + preferenceProvider.saveUserCredentials( + serverUrl.value!!, + userName.value!!, + userPass, + ) + } } - fun onFingerprintClick() { - disposable.add( - - fingerPrintController.authenticate() - .map { result -> - if (preferenceProvider.contains( - SECURE_SERVER_URL, - SECURE_USER_NAME, - SECURE_PASS, - ) - ) { - Result.success(result) - } else { - Result.failure(Exception(EMPTY_CREDENTIALS)) - } - } - .observeOn(schedulers.ui()) - .subscribe( - { - it.fold( - onSuccess = { data -> - when (data.type) { - Type.SUCCESS -> - view.showCredentialsData( - data, - preferenceProvider.getString(SECURE_SERVER_URL)!!, - preferenceProvider.getString(SECURE_USER_NAME)!!, - preferenceProvider.getString(SECURE_PASS)!!, - ) - - Type.INFO -> { - /*Do nothing*/ - } - - Type.ERROR -> - view.showCredentialsData( - data, - it.getOrNull()?.message!!, - ) - } - }, - onFailure = { - view.showEmptyCredentialsMessage() - }, - ) - }, - { - view.displayMessage(AUTH_ERROR) - }, - ), - ) + fun authenticateWithBiometric() { + biometricController.authenticate { + password.value = preferenceProvider.getString(SECURE_PASS) + logIn() + } } fun onAccountRecovery() { @@ -540,6 +470,7 @@ class LoginViewModel( if (isDataComplete.value == null || isDataComplete.value != newValue) { isDataComplete.value = newValue } + checkBiometricVisibility() } private fun checkTestingEnvironment(serverUrl: String) { @@ -562,6 +493,8 @@ class LoginViewModel( fun setAccountInfo(serverUrl: String?, userName: String?) { this.serverUrl.value = serverUrl this.userName.value = userName + view.setUrl(serverUrl) + view.setUser(userName) } fun onImportDataBase(file: File) { @@ -582,8 +515,6 @@ class LoginViewModel( result.fold( onSuccess = { setAccountInfo(it.serverUrl, it.username) - view.setUrl(it.serverUrl) - view.setUser(it.username) displayManageAccount() view.displayMessage(resourceManager.getString(R.string.importing_successful)) }, @@ -601,4 +532,9 @@ class LoginViewModel( fun setDisplayMoreActions(shouldDisplayMoreActions: Boolean) { _displayMoreActions.postValue(shouldDisplayMoreActions) } + + fun shouldAskForBiometrics(): Boolean = + biometricController.hasBiometric() && + !preferenceProvider.areCredentialsSet() && + hasAccounts.value == false } diff --git a/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt index cf1b998e88..c47f1ce719 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt @@ -8,7 +8,7 @@ import org.dhis2.commons.reporting.CrashReportController import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.fingerprint.FingerPrintController +import org.dhis2.data.biometric.BiometricController import org.dhis2.data.server.UserManager import org.dhis2.utils.analytics.AnalyticsHelper @@ -18,7 +18,7 @@ class LoginViewModelFactory( private val resources: ResourceManager, private val schedulerProvider: SchedulerProvider, private val dispatcherProvider: DispatcherProvider, - private val fingerPrintController: FingerPrintController, + private val biometricController: BiometricController, private val analyticsHelper: AnalyticsHelper, private val crashReportController: CrashReportController, private val networkUtils: NetworkUtils, @@ -31,7 +31,7 @@ class LoginViewModelFactory( resources, schedulerProvider, dispatcherProvider, - fingerPrintController, + biometricController, analyticsHelper, crashReportController, networkUtils, diff --git a/app/src/main/java/org/dhis2/usescases/main/HomeNavigator.kt b/app/src/main/java/org/dhis2/usescases/main/HomeNavigator.kt index 4e4cb43f49..dabf807a65 100644 --- a/app/src/main/java/org/dhis2/usescases/main/HomeNavigator.kt +++ b/app/src/main/java/org/dhis2/usescases/main/HomeNavigator.kt @@ -8,7 +8,7 @@ import org.dhis2.android.rtsm.data.AppConfig import org.dhis2.android.rtsm.ui.home.HomeActivity import org.dhis2.commons.Constants import org.dhis2.usescases.datasets.datasetDetail.DataSetDetailActivity -import org.dhis2.usescases.main.program.ProgramViewModel +import org.dhis2.usescases.main.program.ProgramUiModel import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.hisp.dhis.android.core.program.ProgramType @@ -39,7 +39,7 @@ sealed class HomeItemData( ) : HomeItemData(uid, label, accessDataWrite) } -fun ProgramViewModel.toHomeItemData(): HomeItemData { +fun ProgramUiModel.toHomeItemData(): HomeItemData { return when (programType) { ProgramType.WITHOUT_REGISTRATION.name -> HomeItemData.EventProgram( diff --git a/app/src/main/java/org/dhis2/usescases/main/HomePageConfigurator.kt b/app/src/main/java/org/dhis2/usescases/main/HomePageConfigurator.kt index d921ea0bb2..2cb51d8435 100644 --- a/app/src/main/java/org/dhis2/usescases/main/HomePageConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/main/HomePageConfigurator.kt @@ -1,13 +1,18 @@ package org.dhis2.usescases.main +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BarChart +import org.dhis2.R +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.ui.icons.imagevectors.Form +import org.dhis2.utils.customviews.navigationbar.NavigationPage import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBarItem class HomePageConfigurator( private val homeRepository: HomeRepository, + private val resourceManager: ResourceManager, ) : NavigationPageConfigurator { - override fun displayTasks(): Boolean { - return super.displayTasks() - } override fun displayPrograms(): Boolean { return true @@ -16,4 +21,25 @@ class HomePageConfigurator( override fun displayAnalytics(): Boolean { return homeRepository.hasHomeAnalytics() } + + override fun navigationItems(): List> { + return buildList { + add( + NavigationBarItem( + id = NavigationPage.PROGRAMS, + icon = Icons.Filled.Form, + label = resourceManager.getString(R.string.navigation_programs), + ), + ) + if (displayAnalytics()) { + add( + NavigationBarItem( + id = NavigationPage.ANALYTICS, + icon = Icons.Filled.BarChart, + label = resourceManager.getString(R.string.navigation_charts), + ), + ) + } + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt index a66a3bf8b2..c7a4d6679d 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt @@ -9,27 +9,31 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings -import android.transition.ChangeBounds -import android.transition.TransitionManager import android.view.View import android.webkit.MimeTypeMap import android.widget.TextView import android.widget.Toast +import android.window.OnBackInvokedDispatcher +import androidx.activity.addCallback import androidx.activity.result.contract.ActivityResultContracts -import androidx.constraintlayout.widget.ConstraintSet +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.app.NotificationCompat -import androidx.core.view.ViewCompat import androidx.databinding.DataBindingUtil import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.launch import org.dhis2.BuildConfig import org.dhis2.R import org.dhis2.bindings.app import org.dhis2.bindings.hasPermissions -import org.dhis2.commons.filters.FilterItem -import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.filters.FiltersAdapter +import org.dhis2.commons.animations.hide +import org.dhis2.commons.animations.show import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext import org.dhis2.databinding.ActivityMainBinding @@ -38,14 +42,15 @@ import org.dhis2.ui.model.ButtonUiModel import org.dhis2.usescases.development.DevelopmentActivity import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.usescases.login.LoginActivity -import org.dhis2.utils.DateUtils import org.dhis2.utils.analytics.CLICK import org.dhis2.utils.analytics.CLOSE_SESSION +import org.dhis2.utils.customviews.navigationbar.NavigationPage import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.dhis2.utils.extension.navigateTo import org.dhis2.utils.granularsync.SyncStatusDialog import org.dhis2.utils.session.PIN_DIALOG_TAG import org.dhis2.utils.session.PinDialog +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBar import java.io.File import javax.inject.Inject @@ -67,19 +72,14 @@ class MainActivity : @Inject lateinit var presenter: MainPresenter - @Inject - lateinit var newAdapter: FiltersAdapter - @Inject lateinit var pageConfigurator: NavigationPageConfigurator - var notification: Boolean = false - var forceToNotSynced = false private var singleProgramNavigationDone = false private val getDevActivityContent = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - binding.navigationBar.pageConfiguration(pageConfigurator) + // no-op } private val requestWritePermissions = @@ -93,18 +93,11 @@ class MainActivity : private var isPinLayoutVisible = false - private var backDropActive = false - private var elevation = 0f private val mainNavigator = MainNavigator( supportFragmentManager, - { - if (backDropActive) { - showHideFilter() - } - }, - ) { titleRes, showFilterButton, showBottomNavigation -> + { /*no-op*/ }, + ) { titleRes, _, showBottomNavigation -> setTitle(getString(titleRes)) - setFilterButtonVisibility(showFilterButton) setBottomNavigationVisibility(showBottomNavigation) } @@ -137,13 +130,17 @@ class MainActivity : //region LIFECYCLE override fun onCreate(savedInstanceState: Bundle?) { app().userComponent()?.let { - mainComponent = it.plus(MainModule(this)).apply { - inject(this@MainActivity) - } + mainComponent = it.plus( + MainModule( + view = this, + forceToNotSynced = intent.getBooleanExtra(AVOID_SYNC, false), + ), + ) + mainComponent.inject(this@MainActivity) } ?: navigateTo(true) super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_main) - forceToNotSynced = intent.getBooleanExtra(AVOID_SYNC, false) + if (::presenter.isInitialized) { binding.presenter = presenter } else { @@ -157,37 +154,12 @@ class MainActivity : binding.mainDrawerLayout.addDrawerListener(this) - binding.filterRecycler.adapter = newAdapter - - binding.navigationBar.pageConfiguration(pageConfigurator) - binding.navigationBar.setOnNavigationItemSelectedListener { - when (it.itemId) { - R.id.navigation_tasks -> { - } - - R.id.navigation_programs -> { - mainNavigator.openPrograms() - } - - R.id.navigation_analytics -> { - presenter.trackHomeAnalytics() - mainNavigator.openVisualizations() - } - } - true - } - - if (BuildConfig.DEBUG) { - binding.menu.setOnLongClickListener { - getDevActivityContent.launch(Intent(this, DevelopmentActivity::class.java)) - false - } - } - - elevation = ViewCompat.getElevation(binding.toolbar) + setUpNavigationBar() + setUpDevelopmentMode() val restoreScreenName = savedInstanceState?.getString(FRAGMENT) - singleProgramNavigationDone = savedInstanceState?.getBoolean(SINGLE_PROGRAM_NAVIGATION) ?: false + singleProgramNavigationDone = + savedInstanceState?.getBoolean(SINGLE_PROGRAM_NAVIGATION) ?: false val openScreen = intent.getStringExtra(FRAGMENT) when { @@ -221,10 +193,14 @@ class MainActivity : } checkNotificationPermission() + + registerOnBackPressedCallback() } private fun checkNotificationPermission() { - if (!hasPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS))) { + if (!hasPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS)) and + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + ) { requestNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS) } } @@ -237,11 +213,9 @@ class MainActivity : override fun onResume() { super.onResume() - - presenter.init() - presenter.initFilters() - - binding.totalFilters = FilterManager.getInstance().totalFilters + if (sessionManagerServiceImpl.isUserLoggedIn()) { + presenter.init() + } } override fun onPause() { @@ -250,23 +224,84 @@ class MainActivity : super.onPause() } - private fun observeSyncState() { - presenter.observeDataSync().observe(this) { - when (it.running) { - true -> { - setFilterButtonVisibility(false) - setBottomNavigationVisibility(false) + private fun setUpDevelopmentMode() { + if (BuildConfig.DEBUG) { + binding.menu.setOnLongClickListener { + getDevActivityContent.launch(Intent(this, DevelopmentActivity::class.java)) + false + } + } + } + + private fun registerOnBackPressedCallback() { + if (Build.VERSION.SDK_INT >= 33) { + onBackInvokedDispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT) { + backPressed() + } + } else { + onBackPressedDispatcher.addCallback(this) { + backPressed() + } + } + } + + private fun setUpNavigationBar() { + binding.navigationBar.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val currentScreen by mainNavigator.selectedScreen.observeAsState() + val selectedItemIndex by remember { + derivedStateOf { + when (currentScreen) { + MainNavigator.MainScreen.PROGRAMS -> 0 + MainNavigator.MainScreen.VISUALIZATIONS -> 1 + else -> null + } + } } - false -> { - setFilterButtonVisibility(true) - setBottomNavigationVisibility(true) - presenter.onDataSuccess() - if (presenter.hasOneHomeItem()) { - navigateToSingleProgram() + if (pageConfigurator.navigationItems().size > 1) { + NavigationBar( + items = pageConfigurator.navigationItems(), + selectedItemIndex = selectedItemIndex ?: 0, + ) { navigationPage -> + when (navigationPage) { + NavigationPage.ANALYTICS -> { + presenter.trackHomeAnalytics() + mainNavigator.openVisualizations() + } + + NavigationPage.PROGRAMS -> mainNavigator.openPrograms() + else -> { + /*no-op*/ + } + } } } - else -> { - // no action + } + } + } + + private fun observeSyncState() { + lifecycleScope.launch { + presenter.observeDataSync().collect { + when (it.running) { + true -> { + binding.syncActionButton.visibility = View.GONE + setBottomNavigationVisibility(false) + } + + false -> { + binding.syncActionButton.visibility = View.VISIBLE + setBottomNavigationVisibility(true) + presenter.onDataSuccess() + if (presenter.hasOneHomeItem()) { + navigateToSingleProgram() + } + } + + else -> { + // no action + } } } } @@ -300,7 +335,7 @@ class MainActivity : object : OnDismissListener { override fun onDismiss(hasChanged: Boolean) { if (hasChanged) { - mainNavigator.getCurrentIfProgram()?.presenter?.updateProgramQueries() + mainNavigator.getCurrentIfProgram()?.programViewModel?.updateProgramQueries() } } }, @@ -344,36 +379,6 @@ class MainActivity : } } - override fun showHideFilter() { - val transition = ChangeBounds() - transition.duration = 200 - TransitionManager.beginDelayedTransition(binding.backdropLayout, transition) - backDropActive = !backDropActive - val initSet = ConstraintSet() - initSet.clone(binding.backdropLayout) - if (backDropActive) { - initSet.connect( - R.id.fragment_container, - ConstraintSet.TOP, - R.id.filterRecycler, - ConstraintSet.BOTTOM, - 50, - ) - binding.navigationBar.hide() - } else { - initSet.connect( - R.id.fragment_container, - ConstraintSet.TOP, - R.id.toolbar, - ConstraintSet.BOTTOM, - 0, - ) - binding.navigationBar.show() - } - initSet.applyTo(binding.backdropLayout) - mainNavigator.getCurrentIfProgram()?.openFilter(backDropActive) - } - override fun onLockClick() { if (!presenter.isPinStored()) { binding.mainDrawerLayout.closeDrawers() @@ -389,17 +394,15 @@ class MainActivity : } } - @Deprecated("Deprecated in Java") - override fun onBackPressed() { + private fun backPressed() { when { !mainNavigator.isHome() -> presenter.onNavigateBackToHome() isPinLayoutVisible -> isPinLayoutVisible = false - else -> super.onBackPressed() } } override fun goToHome() { - mainNavigator.openHome(binding.navigationBar) + mainNavigator.openHome() } override fun changeFragment(id: Int) { @@ -407,41 +410,10 @@ class MainActivity : binding.mainDrawerLayout.closeDrawers() } - override fun updateFilters(totalFilters: Int) { - binding.totalFilters = totalFilters - } - - override fun showPeriodRequest(periodRequest: FilterManager.PeriodRequest) { - if (periodRequest == FilterManager.PeriodRequest.FROM_TO) { - DateUtils.getInstance() - .fromCalendarSelector(this) { FilterManager.getInstance().addPeriod(it) } - } else { - DateUtils.getInstance() - .showPeriodDialog( - this, - { datePeriods -> FilterManager.getInstance().addPeriod(datePeriods) }, - true, - ) - } - } - fun setTitle(title: String) { binding.title.text = title } - private fun setFilterButtonVisibility(showFilterButton: Boolean) { - binding.filterActionButton.visibility = if (showFilterButton) { - View.VISIBLE - } else { - View.GONE - } - binding.syncActionButton.visibility = if (showFilterButton) { - View.VISIBLE - } else { - View.GONE - } - } - private fun setBottomNavigationVisibility(showBottomNavigation: Boolean) { if (showBottomNavigation) { binding.navigationBar.show() @@ -450,14 +422,6 @@ class MainActivity : } } - override fun setFilters(filters: List) { - newAdapter.submitList(filters) - } - - override fun hideFilters() { - binding.filterActionButton.visibility = View.GONE - } - override fun onDrawerStateChanged(newState: Int) { } @@ -466,9 +430,6 @@ class MainActivity : override fun onDrawerClosed(drawerView: View) { initCurrentScreen() - if (mainNavigator.isPrograms() && !isNotificationRunning()) { - presenter.initFilters() - } } override fun onDrawerOpened(drawerView: View) { @@ -486,11 +447,6 @@ class MainActivity : mainNavigator.openQR() } - R.id.menu_jira -> { - presenter.trackJiraReport() - mainNavigator.openJira() - } - R.id.menu_about -> { mainNavigator.openAbout() } @@ -506,7 +462,7 @@ class MainActivity : } R.id.menu_home -> { - mainNavigator.openHome(binding.navigationBar) + mainNavigator.openHome() } R.id.menu_troubleshooting -> { @@ -517,10 +473,6 @@ class MainActivity : confirmAccountDelete() } } - - if (backDropActive && mainNavigator.isPrograms()) { - showHideFilter() - } } private fun confirmAccountDelete() { @@ -538,7 +490,6 @@ class MainActivity : } override fun showProgressDeleteNotification() { - notification = true val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -564,14 +515,6 @@ class MainActivity : return this.cacheDir } - private fun isNotificationRunning(): Boolean { - return notification - } - - override fun hasToNotSync(): Boolean { - return forceToNotSynced - } - override fun cancelNotifications() { val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/app/src/main/java/org/dhis2/usescases/main/MainComponent.kt b/app/src/main/java/org/dhis2/usescases/main/MainComponent.kt index 93bac5f353..85209d4421 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainComponent.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainComponent.kt @@ -2,8 +2,6 @@ package org.dhis2.usescases.main import dagger.Subcomponent import org.dhis2.commons.di.dagger.PerActivity -import org.dhis2.usescases.jira.JiraComponent -import org.dhis2.usescases.jira.JiraModule import org.dhis2.usescases.troubleshooting.TroubleshootingComponent import org.dhis2.usescases.troubleshooting.TroubleshootingModule @@ -11,6 +9,5 @@ import org.dhis2.usescases.troubleshooting.TroubleshootingModule @Subcomponent(modules = [MainModule::class]) interface MainComponent { fun inject(mainActivity: MainActivity) - fun plus(jiraModule: JiraModule): JiraComponent fun plus(troubleShootingModule: TroubleshootingModule): TroubleshootingComponent } diff --git a/app/src/main/java/org/dhis2/usescases/main/MainModule.kt b/app/src/main/java/org/dhis2/usescases/main/MainModule.kt index 1fbd73d6a8..95077a043c 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainModule.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainModule.kt @@ -6,10 +6,11 @@ import dhis2.org.analytics.charts.Charts import org.dhis2.commons.di.dagger.PerActivity import org.dhis2.commons.featureconfig.data.FeatureConfigRepository import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.filters.FiltersAdapter import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.prefs.PreferenceProvider +import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.server.UserManager @@ -22,7 +23,7 @@ import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.hisp.dhis.android.core.D2 @Module -class MainModule(val view: MainView) { +class MainModule(val view: MainView, private val forceToNotSynced: Boolean) { @Provides @PerActivity @@ -31,7 +32,6 @@ class MainModule(val view: MainView) { schedulerProvider: SchedulerProvider, preferences: PreferenceProvider, workManagerController: WorkManagerController, - filterManager: FilterManager, filterRepository: FilterRepository, matomoAnalyticsController: MatomoAnalyticsController, userManager: UserManager, @@ -47,7 +47,6 @@ class MainModule(val view: MainView) { schedulerProvider, preferences, workManagerController, - filterManager, filterRepository, matomoAnalyticsController, userManager, @@ -56,6 +55,7 @@ class MainModule(val view: MainView) { syncStatusController, versionRepository, dispatcherProvider, + forceToNotSynced, ) } @@ -77,14 +77,10 @@ class MainModule(val view: MainView) { @Provides @PerActivity - fun providePageConfigurator(homeRepository: HomeRepository): NavigationPageConfigurator { - return HomePageConfigurator(homeRepository) - } - - @Provides - @PerActivity - fun providesNewFilterAdapter(): FiltersAdapter { - return FiltersAdapter() + fun providePageConfigurator( + homeRepository: HomeRepository, + ): NavigationPageConfigurator { + return HomePageConfigurator(homeRepository, ResourceManager(view.context, ColorUtils())) } @Provides @@ -92,10 +88,11 @@ class MainModule(val view: MainView) { fun provideDeleteUserData( workManagerController: WorkManagerController, preferencesProvider: PreferenceProvider, + filterManager: FilterManager, ): DeleteUserData { return DeleteUserData( workManagerController, - FilterManager.getInstance(), + filterManager, preferencesProvider, ) } diff --git a/app/src/main/java/org/dhis2/usescases/main/MainNavigator.kt b/app/src/main/java/org/dhis2/usescases/main/MainNavigator.kt index 076ab2d322..8a3fa8cd72 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainNavigator.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainNavigator.kt @@ -7,15 +7,15 @@ import androidx.annotation.StringRes import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import dhis2.org.analytics.charts.ui.GroupAnalyticsFragment import org.dhis2.R import org.dhis2.usescases.about.AboutFragment -import org.dhis2.usescases.jira.JiraFragment import org.dhis2.usescases.main.program.ProgramFragment import org.dhis2.usescases.qrReader.QrReaderFragment import org.dhis2.usescases.settings.SyncManagerFragment import org.dhis2.usescases.troubleshooting.TroubleshootingFragment -import org.dhis2.utils.customviews.navigationbar.NavigationBottomBar class MainNavigator( private val fragmentManager: FragmentManager, @@ -32,32 +32,32 @@ class MainNavigator( QR(R.string.QR_SCANNER, R.id.qr_scan), SETTINGS(R.string.SYNC_MANAGER, R.id.sync_manager), TROUBLESHOOTING(R.string.main_menu_troubleshooting, R.id.menu_troubleshooting), - JIRA(R.string.jira_report, R.id.menu_jira), ABOUT(R.string.about, R.id.menu_about), } - private var currentScreen: MainScreen? = null + private var currentScreen = MutableLiveData(null) + var selectedScreen: LiveData = currentScreen private var currentFragment: Fragment? = null fun isHome(): Boolean = isPrograms() || isVisualizations() - fun isPrograms(): Boolean = currentScreen == MainScreen.PROGRAMS + fun isPrograms(): Boolean = currentScreen.value == MainScreen.PROGRAMS - fun isVisualizations(): Boolean = currentScreen == MainScreen.VISUALIZATIONS + fun isVisualizations(): Boolean = currentScreen.value == MainScreen.VISUALIZATIONS fun getCurrentIfProgram(): ProgramFragment? { return currentFragment?.takeIf { it is ProgramFragment } as ProgramFragment } - fun currentScreenName() = currentScreen?.name + fun currentScreenName() = currentScreen.value?.name fun currentNavigationViewItemId(screenName: String): Int = MainScreen.valueOf(screenName).navViewId - fun openHome(navigationBottomBar: NavigationBottomBar) { + fun openHome() { when { - isVisualizations() -> navigationBottomBar.selectedItemId = R.id.navigation_analytics - else -> navigationBottomBar.selectedItemId = R.id.navigation_programs + isVisualizations() -> openVisualizations() + else -> openPrograms() } } @@ -85,28 +85,13 @@ class MainNavigator( MainScreen.VISUALIZATIONS -> openVisualizations() MainScreen.QR -> openQR() MainScreen.SETTINGS -> openSettings() - MainScreen.JIRA -> openJira() MainScreen.ABOUT -> openAbout() MainScreen.TROUBLESHOOTING -> openTroubleShooting(languageSelectorOpened) } } fun openVisualizations() { - val visualizationFragment = GroupAnalyticsFragment.forHome() - val sharedView = if (isPrograms()) { - (currentFragment as ProgramFragment).sharedView() - } else { - null - } - if (sharedView != null) { - visualizationFragment.sharedElementEnterTransition = ChangeBounds() - visualizationFragment.sharedElementReturnTransition = ChangeBounds() - } - beginTransaction( - visualizationFragment, - MainScreen.VISUALIZATIONS, - sharedView, - ) + beginTransaction(GroupAnalyticsFragment.forHome(), MainScreen.VISUALIZATIONS) } fun openSettings() { @@ -123,13 +108,6 @@ class MainNavigator( ) } - fun openJira() { - beginTransaction( - JiraFragment(), - MainScreen.JIRA, - ) - } - fun openAbout() { beginTransaction( AboutFragment(), @@ -151,9 +129,9 @@ class MainNavigator( sharedView: View? = null, useFadeInTransition: Boolean = false, ) { - if (currentScreen != screen) { + if (currentScreen.value != screen) { onTransitionStart() - currentScreen = screen + currentScreen.value = screen currentFragment = fragment val transaction: FragmentTransaction = fragmentManager.beginTransaction() transaction.apply { diff --git a/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt b/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt index 4fdfbf577c..fe2bb73058 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt @@ -3,22 +3,20 @@ package org.dhis2.usescases.main import android.content.Context import android.net.Uri import android.view.Gravity -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import androidx.work.ExistingWorkPolicy import io.reactivex.Completable -import io.reactivex.Flowable import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.dhis2.BuildConfig import org.dhis2.commons.Constants import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.matomo.Actions.Companion.BLOCK_SESSION_PIN -import org.dhis2.commons.matomo.Actions.Companion.JIRA_REPORT import org.dhis2.commons.matomo.Actions.Companion.OPEN_ANALYTICS import org.dhis2.commons.matomo.Actions.Companion.QR_SCANNER import org.dhis2.commons.matomo.Actions.Companion.SETTINGS @@ -59,7 +57,6 @@ class MainPresenter( private val schedulerProvider: SchedulerProvider, private val preferences: PreferenceProvider, private val workManagerController: WorkManagerController, - private val filterManager: FilterManager, private val filterRepository: FilterRepository, private val matomoAnalyticsController: MatomoAnalyticsController, private val userManager: UserManager, @@ -68,6 +65,7 @@ class MainPresenter( private val syncStatusController: SyncStatusController, private val versionRepository: VersionRepository, private val dispatcherProvider: DispatcherProvider, + private val forceToNotSynced: Boolean, ) : CoroutineScope { private var job = Job() @@ -119,44 +117,6 @@ class MainPresenter( trackDhis2Server() } - fun initFilters() { - disposable.add( - Flowable.just(filterRepository.homeFilters()) - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { filters -> - if (filters.isEmpty()) { - view.hideFilters() - } else { - view.setFilters(filters) - } - }, - { Timber.e(it) }, - ), - ) - - disposable.add( - filterManager.asFlowable() - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { filterManager -> view.updateFilters(filterManager.totalFilters) }, - { Timber.e(it) }, - ), - ) - - disposable.add( - filterManager.periodRequest - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { periodRequest -> view.showPeriodRequest(periodRequest.first) }, - { Timber.e(it) }, - ), - ) - } - fun trackDhis2Server() { disposable.add( repository.getServerVersion() @@ -200,6 +160,7 @@ class MainPresenter( syncStatusController.restore() FilterManager.getInstance().clearAllFilters() preferences.setValue(Preference.SESSION_LOCKED, false) + preferences.setValue(Preference.PIN_ENABLED, false) userManager.d2.dataStoreModule().localDataStore().value(PIN).blockingDeleteIfExist() }.andThen( repository.logOut(), @@ -240,10 +201,6 @@ class MainPresenter( view.back() } - fun showFilter() { - view.showHideFilter() - } - fun onDetach() { disposable.clear() } @@ -262,7 +219,6 @@ class MainPresenter( fun onNavigateBackToHome() { view.goToHome() - initFilters() } fun onClickSyncManager() { @@ -281,12 +237,12 @@ class MainPresenter( .syncDataForWorker(Constants.DATA_NOW, Constants.INITIAL_SYNC) } - fun observeDataSync(): LiveData { + fun observeDataSync(): StateFlow { return syncStatusController.observeDownloadProcess() } fun wasSyncAlreadyDone(): Boolean { - if (view.hasToNotSync()) { + if (forceToNotSynced) { return true } return syncIsPerformedInteractor.execute() @@ -311,10 +267,6 @@ class MainPresenter( matomoAnalyticsController.trackEvent(HOME, QR_SCANNER, CLICK) } - fun trackJiraReport() { - matomoAnalyticsController.trackEvent(HOME, JIRA_REPORT, CLICK) - } - fun checkVersionUpdate() { launch { versionRepository.checkVersionUpdates() diff --git a/app/src/main/java/org/dhis2/usescases/main/MainView.kt b/app/src/main/java/org/dhis2/usescases/main/MainView.kt index 008f2a3fe9..1d590f3c84 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainView.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainView.kt @@ -26,8 +26,6 @@ package org.dhis2.usescases.main import androidx.annotation.UiThread -import org.dhis2.commons.filters.FilterItem -import org.dhis2.commons.filters.FilterManager import org.dhis2.usescases.general.AbstractActivityContracts import java.io.File @@ -38,22 +36,12 @@ interface MainView : AbstractActivityContracts.View { fun openDrawer(gravity: Int) - fun showHideFilter() - fun onLockClick() fun changeFragment(id: Int) - fun updateFilters(totalFilters: Int) - - fun showPeriodRequest(periodRequest: FilterManager.PeriodRequest) - fun goToHome() - fun setFilters(filters: List) - - fun hideFilters() - fun showGranularSync() fun goToLogin(accountsCount: Int, isDeletion: Boolean) @@ -63,6 +51,4 @@ interface MainView : AbstractActivityContracts.View { fun obtainFileView(): File? fun cancelNotifications() - - fun hasToNotSync(): Boolean } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramFragment.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramFragment.kt index 4e0d1653d3..065d766713 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramFragment.kt @@ -2,7 +2,6 @@ package org.dhis2.usescases.main.program import android.content.Context import android.content.Intent -import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.os.Handler import android.os.Looper @@ -11,27 +10,22 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.core.view.ViewCompat -import androidx.databinding.DataBindingUtil +import androidx.fragment.app.viewModels import com.google.android.material.snackbar.Snackbar import org.dhis2.App import org.dhis2.R import org.dhis2.android.rtsm.commons.Constants.INTENT_EXTRA_APP_CONFIG import org.dhis2.android.rtsm.data.AppConfig import org.dhis2.android.rtsm.ui.home.HomeActivity -import org.dhis2.bindings.Bindings -import org.dhis2.bindings.clipWithRoundedCorners -import org.dhis2.bindings.dp -import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext -import org.dhis2.databinding.FragmentProgramBinding import org.dhis2.usescases.general.FragmentGlobalAbstract -import org.dhis2.usescases.main.MainActivity import org.dhis2.usescases.main.navigateTo import org.dhis2.usescases.main.toHomeItemData import org.dhis2.utils.HelpManager @@ -43,10 +37,12 @@ import javax.inject.Inject class ProgramFragment : FragmentGlobalAbstract(), ProgramView { - private lateinit var binding: FragmentProgramBinding - @Inject - lateinit var presenter: ProgramPresenter + lateinit var programViewModelFactory: ProgramViewModelFactory + + val programViewModel: ProgramViewModel by viewModels { + programViewModelFactory + } @Inject lateinit var animation: ProgramAnimation @@ -70,34 +66,22 @@ class ProgramFragment : FragmentGlobalAbstract(), ProgramView { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - binding = DataBindingUtil.inflate(inflater, R.layout.fragment_program, container, false) - ViewCompat.setTransitionName(binding.drawerLayout, "contenttest") - binding.lifecycleOwner = viewLifecycleOwner - (binding.drawerLayout.background as GradientDrawable).cornerRadius = 0f - return binding.apply { - presenter = this@ProgramFragment.presenter - drawerLayout.clipWithRoundedCorners(16.dp) - initList() - }.also { - presenter.downloadState().observe(viewLifecycleOwner) { - presenter.setIsDownloading() - } - }.root - } - - private fun initList() { - binding.programList.apply { + return ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, ) setContent { - val items by presenter.programs.observeAsState() - val state by presenter.downloadState().observeAsState() + val items by programViewModel.programs.observeAsState() + val downloadState by programViewModel.downloadState().collectAsState() + + LaunchedEffect(downloadState) { + programViewModel.setIsDownloading() + } ProgramList( - downLoadState = state, + downLoadState = downloadState, programs = items, onItemClick = { - presenter.onItemClick(it) + programViewModel.onItemClick(it) }, onGranularSyncClick = { showSyncDialog(it) @@ -109,42 +93,11 @@ class ProgramFragment : FragmentGlobalAbstract(), ProgramView { override fun onResume() { super.onResume() - presenter.init() - animation.initBackdropCorners( - binding.drawerLayout.background.mutate() as GradientDrawable, - ) - } - - override fun onPause() { - animation.reverseBackdropCorners( - binding.drawerLayout.background.mutate() as GradientDrawable, - ) - presenter.dispose() - super.onPause() + programViewModel.init() } //endregion - override fun showFilterProgress() { - Bindings.setViewVisibility( - binding.clearFilter, - FilterManager.getInstance().totalFilters > 0, - ) - } - - override fun openOrgUnitTreeSelector() { - OUTreeFragment.Builder() - .showAsDialog() - .withPreselectedOrgUnits( - FilterManager.getInstance().orgUnitFilters.map { it.uid() }.toMutableList(), - ) - .onSelection { selectedOrgUnits -> - presenter.setOrgUnitFilters(selectedOrgUnits) - } - .build() - .show(childFragmentManager, "OUTreeFragment") - } - override fun setTutorial() { try { if (context != null && isAdded) { @@ -154,7 +107,7 @@ class ProgramFragment : FragmentGlobalAbstract(), ProgramView { val stepCondition = SparseBooleanArray() stepCondition.put( 7, - (presenter.programs.value?.size ?: 0) > 0, + (programViewModel.programs.value?.size ?: 0) > 0, ) HelpManager.getInstance().show( abstractActivity, @@ -171,19 +124,7 @@ class ProgramFragment : FragmentGlobalAbstract(), ProgramView { } } - fun openFilter(open: Boolean) { - binding.filter.visibility = if (open) View.VISIBLE else View.GONE - } - - override fun showHideFilter() { - (activity as MainActivity).showHideFilter() - } - - override fun clearFilters() { - (activity as MainActivity).newAdapter.notifyDataSetChanged() - } - - override fun navigateTo(program: ProgramViewModel) { + override fun navigateTo(program: ProgramUiModel) { abstractActivity.analyticsHelper.setEvent( TYPE_PROGRAM_SELECTED, program.programType.ifEmpty { program.typeName }, @@ -203,7 +144,7 @@ class ProgramFragment : FragmentGlobalAbstract(), ProgramView { } } - override fun showSyncDialog(program: ProgramViewModel) { + override fun showSyncDialog(program: ProgramUiModel) { SyncStatusDialog.Builder() .withContext(this) .withSyncContext( @@ -217,7 +158,7 @@ class ProgramFragment : FragmentGlobalAbstract(), ProgramView { object : OnDismissListener { override fun onDismiss(hasChanged: Boolean) { if (hasChanged) { - presenter.updateProgramQueries() + programViewModel.updateProgramQueries() } } }, @@ -233,8 +174,6 @@ class ProgramFragment : FragmentGlobalAbstract(), ProgramView { .show(FRAGMENT_TAG) } - fun sharedView() = binding.drawerLayout - companion object { const val FRAGMENT_TAG = "SYNC" } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt index 4f5a6de9dd..8f22319228 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt @@ -3,15 +3,15 @@ package org.dhis2.usescases.main.program import dagger.Module import dagger.Provides import org.dhis2.commons.di.dagger.PerFragment -import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.featureconfig.data.FeatureConfigRepository import org.dhis2.commons.filters.data.FilterPresenter import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.dhislogic.DhisProgramUtils -import org.dhis2.data.dhislogic.DhisTrackedEntityInstanceUtils import org.dhis2.data.service.SyncStatusController import org.hisp.dhis.android.core.D2 @@ -20,18 +20,18 @@ class ProgramModule(private val view: ProgramView) { @Provides @PerFragment - internal fun programPresenter( + internal fun programViewModelFactory( programRepository: ProgramRepository, - schedulerProvider: SchedulerProvider, - filterManager: FilterManager, + dispatcherProvider: DispatcherProvider, + featureConfigRepository: FeatureConfigRepository, matomoAnalyticsController: MatomoAnalyticsController, syncStatusController: SyncStatusController, - ): ProgramPresenter { - return ProgramPresenter( + ): ProgramViewModelFactory { + return ProgramViewModelFactory( view, programRepository, - schedulerProvider, - filterManager, + featureConfigRepository, + dispatcherProvider, matomoAnalyticsController, syncStatusController, ) @@ -43,7 +43,6 @@ class ProgramModule(private val view: ProgramView) { d2: D2, filterPresenter: FilterPresenter, dhisProgramUtils: DhisProgramUtils, - dhisTrackedEntityInstanceUtils: DhisTrackedEntityInstanceUtils, schedulerProvider: SchedulerProvider, colorUtils: ColorUtils, metadataIconProvider: MetadataIconProvider, @@ -52,7 +51,6 @@ class ProgramModule(private val view: ProgramView) { d2, filterPresenter, dhisProgramUtils, - dhisTrackedEntityInstanceUtils, ResourceManager(view.context, colorUtils), metadataIconProvider, schedulerProvider, diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramPresenter.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramPresenter.kt deleted file mode 100644 index c9c0eda051..0000000000 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramPresenter.kt +++ /dev/null @@ -1,128 +0,0 @@ -package org.dhis2.usescases.main.program - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.processors.PublishProcessor -import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.matomo.Actions.Companion.SYNC_BTN -import org.dhis2.commons.matomo.Categories.Companion.HOME -import org.dhis2.commons.matomo.Labels.Companion.CLICK_ON -import org.dhis2.commons.matomo.MatomoAnalyticsController -import org.dhis2.commons.schedulers.SchedulerProvider -import org.dhis2.data.service.SyncStatusController -import org.hisp.dhis.android.core.organisationunit.OrganisationUnit -import timber.log.Timber -import java.util.concurrent.TimeUnit - -class ProgramPresenter internal constructor( - private val view: ProgramView, - private val programRepository: ProgramRepository, - private val schedulerProvider: SchedulerProvider, - private val filterManager: FilterManager, - private val matomoAnalyticsController: MatomoAnalyticsController, - private val syncStatusController: SyncStatusController, -) { - - private val _programs = MutableLiveData>() - val programs: LiveData> = _programs - private val refreshData = PublishProcessor.create() - var disposable: CompositeDisposable = CompositeDisposable() - - fun init() { - val applyFiler = PublishProcessor.create() - programRepository.clearCache() - - disposable.add( - applyFiler - .switchMap { - refreshData.debounce( - 500, - TimeUnit.MILLISECONDS, - schedulerProvider.io(), - ).startWith(Unit).switchMap { - programRepository.homeItems( - syncStatusController.observeDownloadProcess().value!!, - ) - } - } - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { programs -> - _programs.postValue(programs) - }, - { throwable -> Timber.d(throwable) }, - { Timber.tag("INIT DATA").d("LOADING ENDED") }, - ), - ) - - disposable.add( - filterManager.asFlowable() - .startWith(filterManager) - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { - view.showFilterProgress() - applyFiler.onNext(filterManager) - }, - { Timber.e(it) }, - ), - ) - - disposable.add( - filterManager.ouTreeFlowable() - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { view.openOrgUnitTreeSelector() }, - { Timber.e(it) }, - ), - ) - } - - fun onSyncStatusClick(program: ProgramViewModel) { - val programTitle = "$CLICK_ON${program.title}" - matomoAnalyticsController.trackEvent(HOME, SYNC_BTN, programTitle) - view.showSyncDialog(program) - } - - fun updateProgramQueries() { - programRepository.clearCache() - filterManager.publishData() - } - - fun onItemClick(programModel: ProgramViewModel) { - view.navigateTo(programModel) - } - - fun showDescription(description: String?) { - if (!description.isNullOrEmpty()) { - view.showDescription(description) - } - } - - fun showHideFilterClick() { - view.showHideFilter() - } - - fun clearFilterClick() { - filterManager.clearAllFilters() - view.clearFilters() - } - - fun dispose() { - disposable.clear() - } - - fun setOrgUnitFilters(selectedOrgUnits: List) { - filterManager.addOrgUnits(selectedOrgUnits) - } - - fun downloadState() = syncStatusController.observeDownloadProcess() - - fun setIsDownloading() { - refreshData.onNext(Unit) - } -} diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepository.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepository.kt index ebecbf4c31..6256fa30af 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepository.kt @@ -3,9 +3,9 @@ package org.dhis2.usescases.main.program import io.reactivex.Flowable import org.dhis2.data.service.SyncStatusData -internal interface ProgramRepository { - fun homeItems(syncStatusData: SyncStatusData): Flowable> - fun programModels(syncStatusData: SyncStatusData): Flowable> - fun aggregatesModels(syncStatusData: SyncStatusData): Flowable> +interface ProgramRepository { + fun homeItems(syncStatusData: SyncStatusData): Flowable> + fun programModels(syncStatusData: SyncStatusData): Flowable> + fun aggregatesModels(syncStatusData: SyncStatusData): Flowable> fun clearCache() } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt index 17ca1caa75..e19c0ca348 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt @@ -9,10 +9,8 @@ import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.data.dhislogic.DhisProgramUtils -import org.dhis2.data.dhislogic.DhisTrackedEntityInstanceUtils import org.dhis2.data.service.SyncStatusData import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.arch.call.D2ProgressSyncStatus import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramType.WITHOUT_REGISTRATION @@ -23,7 +21,6 @@ internal class ProgramRepositoryImpl( private val d2: D2, private val filterPresenter: FilterPresenter, private val dhisProgramUtils: DhisProgramUtils, - private val dhisTeiUtils: DhisTrackedEntityInstanceUtils, private val resourceManager: ResourceManager, private val metadataIconProvider: MetadataIconProvider, private val schedulerProvider: SchedulerProvider, @@ -31,9 +28,9 @@ internal class ProgramRepositoryImpl( private val programViewModelMapper = ProgramViewModelMapper() private var lastSyncStatus: SyncStatusData? = null - private var baseProgramCache: List = emptyList() + private var baseProgramCache: List = emptyList() - override fun homeItems(syncStatusData: SyncStatusData): Flowable> { + override fun homeItems(syncStatusData: SyncStatusData): Flowable> { return programModels(syncStatusData).onErrorReturn { arrayListOf() } .mergeWith(aggregatesModels(syncStatusData).onErrorReturn { arrayListOf() }) .flatMapIterable { data -> data } @@ -48,7 +45,7 @@ internal class ProgramRepositoryImpl( override fun aggregatesModels( syncStatusData: SyncStatusData, - ): Flowable> { + ): Flowable> { return Flowable.fromCallable { aggregatesModels().blockingFirst() .applySync(syncStatusData) @@ -59,7 +56,7 @@ internal class ProgramRepositoryImpl( baseProgramCache = emptyList() } - private fun aggregatesModels(): Flowable> { + private fun aggregatesModels(): Flowable> { return filterPresenter.filteredDataSetInstances().get() .toFlowable() .map { dataSetSummaries -> @@ -70,13 +67,8 @@ internal class ProgramRepositoryImpl( programViewModelMapper.map( dataSet, it, - if (filterPresenter.isAssignedToMeApplied()) { - 0 - } else { - it.dataSetInstanceCount() - }, + it.dataSetInstanceCount(), resourceManager.defaultDataSetLabel(), - filterPresenter.areFiltersActive(), metadataIconProvider(dataSet.style(), SurfaceColor.Primary), ) } @@ -84,7 +76,7 @@ internal class ProgramRepositoryImpl( } } - override fun programModels(syncStatusData: SyncStatusData): Flowable> { + override fun programModels(syncStatusData: SyncStatusData): Flowable> { return Flowable.fromCallable { baseProgramCache.ifEmpty { baseProgramCache = basePrograms() @@ -94,7 +86,7 @@ internal class ProgramRepositoryImpl( } } - private fun basePrograms(): List { + private fun basePrograms(): List { return dhisProgramUtils.getProgramsInCaptureOrgUnits() .flatMap { programs -> ParallelFlowable.from(Flowable.fromIterable(programs)) @@ -115,8 +107,6 @@ internal class ProgramRepositoryImpl( 0, recordLabel, state, - hasOverdue = false, - filtersAreActive = false, metadataIconData = metadataIconProvider(program.style(), SurfaceColor.Primary), ).copy( stockConfig = if (d2.isStockProgram(program.uid())) { @@ -128,28 +118,24 @@ internal class ProgramRepositoryImpl( }.toList().toFlowable().blockingFirst() } - private fun List.applyFilters(): List { + private fun List.applyFilters(): List { return map { programModel -> val program = d2.programModule().programs().uid(programModel.uid).blockingGet() - val (count, hasOverdue) = + val count = if (program?.programType() == WITHOUT_REGISTRATION) { getSingleEventCount(program) } else if (program?.programType() == WITH_REGISTRATION) { - getTrackerTeiCountAndOverdue(program) + getTrackerTeiCount(program) } else { - Pair(0, false) + 0 } - programModel.copy( - count = count, - hasOverdueEvent = hasOverdue, - filtersAreActive = filterPresenter.areFiltersActive(), - ) + programModel.copy(count = count) } } - private fun List.applySync( + private fun List.applySync( syncStatusData: SyncStatusData, - ): List { + ): List { return map { programModel -> programModel.copy( downloadState = when { @@ -159,15 +145,8 @@ internal class ProgramRepositoryImpl( syncStatusData.isProgramDownloading(programModel.uid) -> ProgramDownloadState.DOWNLOADING - syncStatusData.wasProgramDownloading(lastSyncStatus, programModel.uid) -> - when (syncStatusData.programSyncStatusMap[programModel.uid]?.syncStatus) { - D2ProgressSyncStatus.SUCCESS -> ProgramDownloadState.DOWNLOADED - D2ProgressSyncStatus.ERROR, - D2ProgressSyncStatus.PARTIAL_ERROR, - -> ProgramDownloadState.ERROR - - null -> ProgramDownloadState.DOWNLOADED - } + syncStatusData.isProgramDownloaded(programModel.uid) -> + ProgramDownloadState.DOWNLOADED else -> ProgramDownloadState.NONE @@ -177,20 +156,13 @@ internal class ProgramRepositoryImpl( } } - private fun getSingleEventCount(program: Program): Pair { - return Pair( - filterPresenter.filteredEventProgram(program) - .blockingGet().filter { event -> event.syncState() != State.RELATIONSHIP }.size, - false, - ) + private fun getSingleEventCount(program: Program): Int { + return filterPresenter.filteredEventProgram(program) + .blockingGet().filter { event -> event.syncState() != State.RELATIONSHIP }.size } - private fun getTrackerTeiCountAndOverdue(program: Program): Pair { - val teiIds = filterPresenter.filteredTrackerProgram(program) - .offlineFirst().blockingGetUids() - val mCount = teiIds.size - val mOverdue = dhisTeiUtils.hasOverdueInProgram(teiIds, program) - - return Pair(mCount, mOverdue) + private fun getTrackerTeiCount(program: Program): Int { + return filterPresenter.filteredTrackerProgram(program) + .offlineFirst().blockingCount() } } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt index 8504795eeb..bb2ac6334d 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt @@ -1,44 +1,33 @@ package org.dhis2.usescases.main.program -import android.content.res.Configuration import android.os.Handler import android.os.Looper import android.view.animation.OvershootInterpolator import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween -import androidx.compose.animation.expandIn -import androidx.compose.animation.shrinkOut -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.AlertDialog import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Celebration import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Sync import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -47,207 +36,231 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex import org.dhis2.R +import org.dhis2.commons.bindings.addIf +import org.dhis2.commons.date.toDateSpan import org.dhis2.commons.resources.ColorType import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.ui.icons.toIconData import org.dhis2.data.service.SyncStatusData -import org.dhis2.ui.MetadataIcon import org.dhis2.ui.MetadataIconData import org.dhis2.ui.toColor import org.hisp.dhis.android.core.common.State +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem +import org.hisp.dhis.mobile.ui.designsystem.component.Avatar +import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyleData +import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ExpandableItemColumn +import org.hisp.dhis.mobile.ui.designsystem.component.ImageCardData import org.hisp.dhis.mobile.ui.designsystem.component.InfoBar import org.hisp.dhis.mobile.ui.designsystem.component.InfoBarData -import org.hisp.dhis.mobile.ui.designsystem.component.internal.ImageCardData +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardDescriptionModel +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.component.MetadataAvatarSize +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType +import org.hisp.dhis.mobile.ui.designsystem.component.VerticalInfoListCard +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberAdditionalInfoColumnState +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberListCardState import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import java.util.Date + +enum class ProgramLayout { + DEFAULT, MEDIUM, LARGE; + + fun metadataAvatarSize() = when (this) { + DEFAULT -> MetadataAvatarSize.S() + MEDIUM -> MetadataAvatarSize.L() + LARGE -> MetadataAvatarSize.XL() + } +} @Composable fun ProgramList( - programs: List?, - onItemClick: (programViewModel: ProgramViewModel) -> Unit, - onGranularSyncClick: (programViewModel: ProgramViewModel) -> Unit, + programs: List?, + onItemClick: (programUiModel: ProgramUiModel) -> Unit, + onGranularSyncClick: (programUiModel: ProgramUiModel) -> Unit, downLoadState: SyncStatusData?, ) { - val conf = LocalConfiguration.current - Column { - AnimatedVisibility( - visible = downLoadState?.downloadingMedia == true, - enter = expandIn( - expandFrom = Alignment.Center, - animationSpec = tween( - easing = { - OvershootInterpolator().getInterpolation(it) - }, - ), - ), - exit = shrinkOut(shrinkTowards = Alignment.Center), - ) { - DownloadMedia() - } + Column( + modifier = Modifier + .fillMaxSize() + .testTag(HOME_ITEMS) + .semantics { + HasPrograms = programs?.isNotEmpty() ?: false + }, + ) { + DownloadMessage( + downLoadState = downLoadState, + isDownloading = programs?.any { it.isDownloading() } ?: false, + ) programs?.let { if (programs.isEmpty()) { NoAccessMessage() } - when (conf.orientation) { - Configuration.ORIENTATION_LANDSCAPE -> - LazyVerticalGrid( - columns = GridCells.Fixed(3), - contentPadding = PaddingValues(bottom = 56.dp), - ) { - items(items = programs) { program -> - ProgramItem( - programViewModel = program, - onItemClick = onItemClick, - onGranularSyncClick = onGranularSyncClick, - ) - Divider( - color = colorResource(id = R.color.divider_bg), - thickness = 1.dp, - startIndent = 72.dp, - ) - } - } + ExpandableItemColumn( + modifier = Modifier + .fillMaxSize(), + itemList = programs, + ) { program, verticalPadding, onSizeChanged -> - else -> - LazyColumn( - modifier = Modifier.testTag(HOME_ITEMS), - contentPadding = PaddingValues(bottom = 56.dp), - ) { - itemsIndexed(items = programs) { index, program -> - ProgramItem( - modifier = Modifier.semantics { testTag = HOME_ITEM.format(index) }, - programViewModel = program, - onItemClick = onItemClick, - onGranularSyncClick = onGranularSyncClick, - ) - Divider( - color = colorResource(id = R.color.divider_bg), - thickness = 1.dp, - startIndent = 72.dp, - ) + ProgramItem( + modifier = Modifier, + program = program, + programLayout = getProgramLayout(programs), + verticalPadding = verticalPadding, + onSizeChanged = { size -> + if (!program.isDownloading()) { + onSizeChanged(size) } - } + }, + onItemClick = onItemClick, + onGranularSyncClick = onGranularSyncClick, + ) + } + } ?: run { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) } } } } +private fun getProgramLayout(programs: List) = when { + programs.size < 3 -> ProgramLayout.LARGE + programs.size < 4 -> ProgramLayout.MEDIUM + else -> ProgramLayout.DEFAULT +} + @Composable -fun ProgramItem( - modifier: Modifier = Modifier, - programViewModel: ProgramViewModel, - onItemClick: (programViewModel: ProgramViewModel) -> Unit = {}, - onGranularSyncClick: (programViewModel: ProgramViewModel) -> Unit = {}, -) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable(enabled = !programViewModel.isDownloading()) { - onItemClick(programViewModel) - } - .background(color = Color.White) - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, +private fun DownloadMessage(downLoadState: SyncStatusData?, isDownloading: Boolean) { + val visibility = when { + downLoadState?.running == false -> timeVisibility(isDownloading) + else -> downLoadState?.canDisplayMessage() == true + } + + AnimatedVisibility( + visible = visibility, + enter = expandVertically( + expandFrom = Alignment.Top, + animationSpec = tween( + easing = { + OvershootInterpolator().getInterpolation(it) + }, + ), + ), + exit = shrinkVertically(shrinkTowards = Alignment.Top), ) { Box( - contentAlignment = Alignment.BottomEnd, + Modifier + .fillMaxWidth() + .padding( + top = Spacing.Spacing16, + bottom = Spacing.Spacing8, + start = Spacing.Spacing16, + end = Spacing.Spacing16, + ), ) { - MetadataIcon( - metadataIconData = programViewModel.metadataIconData, - ) + InfoBar( + infoBarData = InfoBarData( + text = downloadInfoText(downLoadState), + icon = { + Icon( + imageVector = when { + downLoadState?.running == false -> + Icons.Outlined.Celebration - var openDescriptionDialog by remember { - mutableStateOf(false) // Initially dialog is closed - } + else -> Icons.Outlined.Info + }, + contentDescription = "error", + tint = when { + downLoadState?.running == false -> + SurfaceColor.CustomGreen - if (programViewModel.description != null) { - ProgramDescriptionIcon { - openDescriptionDialog = true - } - } - - if (openDescriptionDialog) { - ProgramDescriptionDialog(programViewModel.description ?: "") { - openDescriptionDialog = false - } - } - } + else -> TextColor.OnSurfaceLight + }, + ) + }, + color = when { + downLoadState?.running == false -> + SurfaceColor.CustomGreen - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier - .weight(1f) - .alpha(programViewModel.getAlphaValue()), - ) { - Text( - text = programViewModel.title, - color = colorResource(id = R.color.textPrimary), - fontSize = 14.sp, - style = LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(R.font.rubik_regular)), - ), - ) - Text( - text = if (programViewModel.downloadState == ProgramDownloadState.DOWNLOADING) { - stringResource(R.string.syncing_resource, programViewModel.typeName.lowercase()) - } else { - programViewModel.countDescription() - }, - color = colorResource(id = R.color.textSecondary), - fontSize = 12.sp, - style = LocalTextStyle.current.copy( - fontFamily = FontFamily(Font(R.font.rubik_regular)), + else -> TextColor.OnSurfaceLight + }, + backgroundColor = SurfaceColor.Surface, ), ) + if (downLoadState?.running == true) { + ProgressIndicator( + modifier = Modifier + .zIndex(1f) + .align(Alignment.CenterEnd) + .padding(Spacing.Spacing8) + .size(Spacing.Spacing24), + type = ProgressIndicatorType.CIRCULAR_SMALL, + ) + } } + } +} - when (programViewModel.downloadState) { - ProgramDownloadState.DOWNLOADING -> DownloadingProgress() - ProgramDownloadState.DOWNLOADED -> DownloadedIcon(programViewModel) - ProgramDownloadState.NONE -> StateIcon(programViewModel.state) { - onGranularSyncClick(programViewModel) - } +@Composable +private fun downloadInfoText(downLoadState: SyncStatusData?) = when { + downLoadState?.running == false -> stringResource(R.string.successful_sync) + downLoadState?.downloadingEvents == true -> stringResource( + id = R.string.syncing_something, + stringResource(id = R.string.events).lowercase(), + ) - ProgramDownloadState.ERROR -> DownloadErrorIcon { - onGranularSyncClick(programViewModel) - } - } + downLoadState?.downloadingTracker == true -> stringResource( + id = R.string.syncing_something, + stringResource(id = R.string.programs).lowercase(), + ) - if (programViewModel.hasOverdueEvent) { - Icon( - painter = painterResource(id = R.drawable.ic_overdue), - contentDescription = "Overdue", - tint = Color.Unspecified, - ) - } - } + downLoadState?.downloadingDataSetValues == true -> stringResource( + id = R.string.syncing_something, + stringResource(id = R.string.data_sets).lowercase(), + ) + + downLoadState?.downloadingMedia == true -> stringResource( + id = R.string.syncing_something, + stringResource(id = R.string.file_resources).lowercase(), + ) + + else -> "" } @Composable -fun StateIcon(state: State, onClick: () -> Unit) { +fun StateIcon( + state: State, + enabled: Boolean, + onClick: () -> Unit, +) { if (state != State.RELATIONSHIP && state != State.SYNCED) { - IconButton(onClick = onClick) { + IconButton(onClick = onClick, enabled = enabled) { val (iconResource, tintColor) = state.toIconData() Icon( imageVector = iconResource, @@ -271,104 +284,39 @@ fun DownloadingProgress() { ) } -@OptIn(ExperimentalAnimationApi::class) @Composable -fun DownloadedIcon(programViewModel: ProgramViewModel) { - val visible = visibility(programViewModel) - AnimatedVisibility( - visible = visible, - enter = expandIn( - expandFrom = Alignment.Center, - animationSpec = tween( - easing = { - OvershootInterpolator().getInterpolation(it) - }, - ), - ), - exit = shrinkOut(shrinkTowards = Alignment.Center), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_download_done), - contentDescription = "downloaded", - tint = Color.Unspecified, - ) - } +fun DownloadedIcon() { + Icon( + painter = painterResource(id = R.drawable.ic_download_done), + contentDescription = "downloaded", + tint = Color.Unspecified, + ) } @Composable -fun DownloadErrorIcon(onClick: () -> Unit) { +fun DownloadErrorIcon() { Icon( - modifier = Modifier.clickable { onClick() }, - painter = painterResource(id = R.drawable.ic_download_error), + modifier = Modifier, + painter = painterResource(id = R.drawable.ic_download_off), contentDescription = "download error", tint = Color.Unspecified, ) } @Composable -fun visibility(viewModel: ProgramViewModel): Boolean { - var visible by remember { mutableStateOf(!viewModel.hasShowCompleteSyncAnimation()) } +fun timeVisibility(initialVisibility: Boolean, hideAfterMillis: Long = 3000): Boolean { + var visible by remember { mutableStateOf(initialVisibility) } DisposableEffect(Unit) { val handler = Handler(Looper.getMainLooper()) val runnable = { visible = false - viewModel.setCompleteSyncAnimation() } - handler.postDelayed(runnable, 3000) + handler.postDelayed(runnable, hideAfterMillis) onDispose { handler.removeCallbacks(runnable) } } return visible } -@Composable -fun ProgramDescriptionIcon(onClick: () -> Unit) { - Box( - modifier = Modifier - .clip( - RoundedCornerShape( - topStart = 15.dp, - topEnd = 10.dp, - bottomEnd = 4.dp, - bottomStart = 15.dp, - ), - ) - .background(Color.White) - .clickable { onClick() }, - ) { - Icon( - modifier = Modifier - .size(16.dp) - .padding(1.dp), - painter = painterResource(id = R.drawable.ic_info), - contentDescription = stringResource(id = R.string.program_description), - tint = Color.Unspecified, - ) - } -} - -@Composable -fun ProgramDescriptionDialog(description: String, onDismiss: () -> Unit) { - AlertDialog( - onDismissRequest = { onDismiss() }, - title = { - Text(text = stringResource(R.string.info)) - }, - text = { - Text(text = description) - }, - confirmButton = { - TextButton( - onClick = { onDismiss() }, - ) { - Text( - text = stringResource(id = R.string.action_close).uppercase(), - color = colorResource(id = R.color.black_de0), - ) - } - }, - ) -} - @Composable @Preview fun DownloadMedia() { @@ -427,46 +375,268 @@ fun NoAccessMessage() { ) } -@Preview @Composable -fun ProgramTest() { - ProgramItem( - programViewModel = testingProgramModel(), +fun ProgramItem( + modifier: Modifier, + program: ProgramUiModel, + programLayout: ProgramLayout, + verticalPadding: Dp, + onSizeChanged: (IntSize) -> Unit, + onItemClick: (programUiModel: ProgramUiModel) -> Unit, + onGranularSyncClick: (programUiModel: ProgramUiModel) -> Unit, +) { + val title = ListCardTitleModel( + text = program.title, + color = TextColor.OnPrimaryContainer.copy( + alpha = program.getAlphaValue(), + ), ) + + val lastUpdated = program.lastUpdated.toDateSpan(LocalContext.current) + + val description = ListCardDescriptionModel(text = program.countDescription()) + + when (programLayout) { + ProgramLayout.DEFAULT -> + ListCard( + modifier = modifier, + listCardState = rememberListCardState( + title = title, + lastUpdated = lastUpdated, + description = description, + loading = program.isDownloading(), + additionalInfoColumnState = rememberAdditionalInfoColumnState( + additionalInfoList = buildList { + program.description?.let { description -> + add( + AdditionalInfoItem( + value = description, + color = TextColor.OnSurfaceLight, + truncate = false, + ), + ) + } + addIf( + !program.isDownloading() && + listOf( + State.TO_POST, + State.TO_UPDATE, + State.ERROR, + State.WARNING, + ).contains(program.state), + stateAdditionalInfoItem(program.state), + ) + }, + syncProgressItem = syncingAdditionalInfoItem(program), + expandLabelText = stringResource(R.string.show_description), + shrinkLabelText = stringResource(R.string.hide_description), + minItemsToShow = 0, + ), + expandable = true, + itemVerticalPadding = verticalPadding, + ), + listAvatar = { + ProgramAvatar( + program = program, + avatarSize = programLayout.metadataAvatarSize(), + ) + }, + + onCardClick = { + if (!program.isDownloading()) { + onItemClick(program) + } + }, + actionButton = { + if (!program.isDownloading()) { + ProvideSyncButton(state = program.state) { + onGranularSyncClick(program) + } + } + }, + onSizeChanged = onSizeChanged, + ) + + else -> + VerticalInfoListCard( + modifier = modifier, + listCardState = rememberListCardState( + title = title, + lastUpdated = lastUpdated, + description = description, + additionalInfoColumnState = rememberAdditionalInfoColumnState( + additionalInfoList = buildList { + program.description?.let { description -> + add( + AdditionalInfoItem( + value = description, + color = TextColor.OnSurfaceLight, + truncate = false, + ), + ) + } + addIf( + !program.isDownloading() && + listOf( + State.TO_POST, + State.TO_UPDATE, + State.ERROR, + State.WARNING, + ).contains(program.state), + stateAdditionalInfoItem(program.state), + ) + }, + expandLabelText = stringResource(R.string.show_description), + shrinkLabelText = stringResource(R.string.hide_description), + syncProgressItem = syncingAdditionalInfoItem(program), + minItemsToShow = 0, + ), + expandable = true, + itemVerticalPadding = verticalPadding, + loading = program.isDownloading(), + ), + listAvatar = { + ProgramAvatar( + program = program, + avatarSize = programLayout.metadataAvatarSize(), + ) + }, + onCardClick = { + if (!program.isDownloading()) { + onItemClick(program) + } + }, + actionButton = { + if (!program.isDownloading()) { + ProvideSyncButton(state = program.state) { + onGranularSyncClick(program) + } + } + }, + onSizeChanged = onSizeChanged, + ) + } } -@Preview @Composable -fun ProgramTestToPost() { - ProgramItem( - programViewModel = testingProgramModel().copy(state = State.TO_POST), - ) -} +private fun syncingAdditionalInfoItem(program: ProgramUiModel) = AdditionalInfoItem( + icon = { + when (program.downloadState) { + ProgramDownloadState.DOWNLOADING -> + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) + + ProgramDownloadState.DOWNLOADED -> + DownloadedIcon() + + ProgramDownloadState.ERROR -> + DownloadErrorIcon() + + ProgramDownloadState.NONE -> { +// no-op + } + } + }, + value = when (program.downloadState) { + ProgramDownloadState.DOWNLOADING -> + stringResource(id = R.string.syncing_resource, program.typeName) + + ProgramDownloadState.DOWNLOADED -> + program.countDescription() + + ProgramDownloadState.ERROR -> + program.countDescription() + + ProgramDownloadState.NONE -> + "" + }, + color = when (program.downloadState) { + ProgramDownloadState.DOWNLOADING -> + SurfaceColor.Primary + + ProgramDownloadState.DOWNLOADED -> + SurfaceColor.CustomGreen + + else -> + TextColor.OnSurfaceLight + }, + isConstantItem = true, +) + +@Composable +private fun stateAdditionalInfoItem(state: State) = AdditionalInfoItem( + icon = { + StateIcon( + state = state, + enabled = false, + ) { + // no-op + } + }, + value = when (state) { + State.TO_POST, + State.TO_UPDATE, + -> stringResource(id = R.string.not_synced) + + State.ERROR -> stringResource(id = R.string.sync_error_title) + State.WARNING -> stringResource(id = R.string.sync_warning) + else -> stringResource(id = R.string.sync_dialog_title_synced) + }, + color = when (state) { + State.ERROR -> TextColor.OnErrorContainer + State.WARNING -> TextColor.OnWarningContainer + else -> TextColor.OnSurfaceLight + }, + isConstantItem = true, +) -@Preview @Composable -fun ProgramTestWithDescription() { - ProgramItem( - programViewModel = testingProgramModel().copy(description = "Program description"), +private fun ProgramAvatar(program: ProgramUiModel, avatarSize: MetadataAvatarSize) { + Avatar( + style = AvatarStyleData.Metadata( + imageCardData = program.metadataIconData.imageCardData, + avatarSize = avatarSize, + tintColor = program.metadataIconData.color.copy( + alpha = program.getAlphaValue(), + ), + ), ) } -@Preview @Composable -fun ProgramTestDownloaded() { - var downloadState by remember { - mutableStateOf(ProgramDownloadState.DOWNLOADING) +private fun ProvideSyncButton( + state: State?, + onSyncIconClick: () -> Unit, +) { + val buttonText = when (state) { + State.TO_POST, + State.TO_UPDATE, + -> { + stringResource(R.string.sync) + } + + State.ERROR, + State.WARNING, + -> { + stringResource(R.string.sync_retry) + } + + else -> null + } + buttonText?.let { + Button( + style = ButtonStyle.TONAL, + text = it, + icon = { + Icon( + imageVector = Icons.Outlined.Sync, + contentDescription = it, + tint = TextColor.OnPrimaryContainer, + ) + }, + onClick = { onSyncIconClick() }, + modifier = Modifier.fillMaxWidth(), + ) } - ProgramItem( - programViewModel = testingProgramModel().copy(downloadState = downloadState), - onItemClick = { - downloadState = if (downloadState == ProgramDownloadState.DOWNLOADING) { - ProgramDownloadState.DOWNLOADED - } else { - ProgramDownloadState.DOWNLOADING - } - }, - ) } @Preview(showBackground = true) @@ -484,24 +654,22 @@ fun ListPreview() { ), onItemClick = {}, onGranularSyncClick = {}, - downLoadState = SyncStatusData(true, true, emptyMap()), + downLoadState = SyncStatusData( + true, + downloadingMedia = true, + programSyncStatusMap = emptyMap(), + ), ) } -@Preview(showBackground = true) -@Composable -fun ProgramDescriptionDialogPReview() { - ProgramDescriptionDialog(description = "Program description") { } -} - -private fun testingProgramModel() = ProgramViewModel( +private fun testingProgramModel() = ProgramUiModel( uid = "qweqwe", title = "Program title", metadataIconData = MetadataIconData( imageCardData = ImageCardData.IconCardData( uid = "", label = "", - iconRes = "ic_positive_negative", + iconRes = "dhis2_positive_negative", iconTint = "#00BCD4".toColor(), ), color = "#00BCD4".toColor(), @@ -514,11 +682,13 @@ private fun testingProgramModel() = ProgramViewModel( onlyEnrollOnce = false, accessDataWrite = true, state = State.SYNCED, - hasOverdueEvent = true, - false, downloadState = ProgramDownloadState.NONE, stockConfig = null, + lastUpdated = Date(), ) +val HasPrograms = SemanticsPropertyKey("HasPrograms") +var SemanticsPropertyReceiver.HasPrograms by HasPrograms + const val HOME_ITEMS = "HOME_ITEMS" const val HOME_ITEM = "HOME_ITEMS_%s" diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt new file mode 100644 index 0000000000..c1440bcf30 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt @@ -0,0 +1,38 @@ +package org.dhis2.usescases.main.program + +import org.dhis2.android.rtsm.data.AppConfig +import org.dhis2.ui.MetadataIconData +import org.hisp.dhis.android.core.common.State +import java.util.Date + +data class ProgramUiModel( + val uid: String, + val title: String, + val metadataIconData: MetadataIconData, + val count: Int, + val type: String?, + val typeName: String, + val programType: String, + val description: String?, + val onlyEnrollOnce: Boolean, + val accessDataWrite: Boolean, + val state: State, + val downloadState: ProgramDownloadState, + val downloadActive: Boolean = false, + val stockConfig: AppConfig?, + val lastUpdated: Date, +) { + fun countDescription() = "%s %s".format(count, typeName) + + fun isDownloading() = downloadActive || downloadState == ProgramDownloadState.DOWNLOADING + + fun getAlphaValue() = if (isDownloading()) { + 0.4f + } else { + 1f + } +} + +enum class ProgramDownloadState { + DOWNLOADING, DOWNLOADED, ERROR, NONE +} diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramView.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramView.kt index c2adb0a32d..cb5a075130 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramView.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramView.kt @@ -5,17 +5,9 @@ import org.dhis2.usescases.general.AbstractActivityContracts interface ProgramView : AbstractActivityContracts.View { - fun showFilterProgress() - - fun openOrgUnitTreeSelector() - - fun showHideFilter() - - fun clearFilters() - - fun navigateTo(program: ProgramViewModel) + fun navigateTo(program: ProgramUiModel) fun navigateToStockManagement(config: AppConfig) - fun showSyncDialog(program: ProgramViewModel) + fun showSyncDialog(program: ProgramUiModel) } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt index 0122cf8289..26f010bb11 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt @@ -1,50 +1,92 @@ package org.dhis2.usescases.main.program -import org.dhis2.android.rtsm.data.AppConfig -import org.dhis2.ui.MetadataIconData -import org.hisp.dhis.android.core.common.State - -data class ProgramViewModel( - val uid: String, - val title: String, - val metadataIconData: MetadataIconData, - val count: Int, - val type: String?, - val typeName: String, - val programType: String, - val description: String?, - val onlyEnrollOnce: Boolean, - val accessDataWrite: Boolean, - val state: State, - val hasOverdueEvent: Boolean, - val filtersAreActive: Boolean, - val downloadState: ProgramDownloadState, - val downloadActive: Boolean = false, - val stockConfig: AppConfig?, -) { - private var hasShownCompleteSyncAnimation = false - - fun setCompleteSyncAnimation() { - hasShownCompleteSyncAnimation = true +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import org.dhis2.commons.featureconfig.data.FeatureConfigRepository +import org.dhis2.commons.featureconfig.model.Feature +import org.dhis2.commons.featureconfig.model.FeatureOptions +import org.dhis2.commons.matomo.Actions.Companion.SYNC_BTN +import org.dhis2.commons.matomo.Categories.Companion.HOME +import org.dhis2.commons.matomo.Labels.Companion.CLICK_ON +import org.dhis2.commons.matomo.MatomoAnalyticsController +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.service.SyncStatusController +import timber.log.Timber + +class ProgramViewModel internal constructor( + private val view: ProgramView, + private val programRepository: ProgramRepository, + private val featureConfigRepository: FeatureConfigRepository, + private val dispatchers: DispatcherProvider, + private val matomoAnalyticsController: MatomoAnalyticsController, + private val syncStatusController: SyncStatusController, +) : ViewModel() { + + private val _programs = MutableLiveData>() + val programs: LiveData> = _programs + + var disposable: CompositeDisposable = CompositeDisposable() + + fun init() { + programRepository.clearCache() + fetchPrograms() } - fun hasShowCompleteSyncAnimation() = hasShownCompleteSyncAnimation + private fun fetchPrograms() { + viewModelScope.launch { + val result = async(dispatchers.io()) { + val programs = programRepository.homeItems( + syncStatusController.observeDownloadProcess().value, + ).blockingLast() + if (featureConfigRepository.isFeatureEnable(Feature.RESPONSIVE_HOME)) { + val feature = featureConfigRepository.featuresList.find { it.feature == Feature.RESPONSIVE_HOME } + val totalItems = feature?.extras?.takeIf { it is FeatureOptions.ResponsiveHome }?.let { + it as FeatureOptions.ResponsiveHome + it.totalItems + } + programs.take( + totalItems ?: programs.size, + ) + } else { + programs + } + } + try { + _programs.postValue(result.await()) + } catch (e: Exception) { + Timber.d(e) + } + } + } - fun translucent(): Boolean { - return (filtersAreActive && count == 0) || downloadState == ProgramDownloadState.DOWNLOADING + fun onSyncStatusClick(program: ProgramUiModel) { + val programTitle = "$CLICK_ON${program.title}" + matomoAnalyticsController.trackEvent(HOME, SYNC_BTN, programTitle) + view.showSyncDialog(program) } - fun countDescription() = "%s %s".format(count, typeName) + fun updateProgramQueries() { + init() + } - fun isDownloading() = downloadActive || downloadState == ProgramDownloadState.DOWNLOADING + fun onItemClick(programModel: ProgramUiModel) { + view.navigateTo(programModel) + } - fun getAlphaValue() = if (isDownloading()) { - 0.5f - } else { - 1f + fun dispose() { + disposable.clear() } -} -enum class ProgramDownloadState { - DOWNLOADING, DOWNLOADED, ERROR, NONE + fun downloadState() = syncStatusController.observeDownloadProcess() + + fun setIsDownloading() { + viewModelScope.launch(dispatchers.io()) { + fetchPrograms() + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt new file mode 100644 index 0000000000..f302ba11bf --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt @@ -0,0 +1,29 @@ +package org.dhis2.usescases.main.program + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.dhis2.commons.featureconfig.data.FeatureConfigRepository +import org.dhis2.commons.matomo.MatomoAnalyticsController +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.service.SyncStatusController + +@Suppress("UNCHECKED_CAST") +class ProgramViewModelFactory( + private val view: ProgramView, + private val programRepository: ProgramRepository, + private val featureConfigRepository: FeatureConfigRepository, + private val dispatchers: DispatcherProvider, + private val matomoAnalyticsController: MatomoAnalyticsController, + private val syncStatusController: SyncStatusController, +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return ProgramViewModel( + view, + programRepository, + featureConfigRepository, + dispatchers, + matomoAnalyticsController, + syncStatusController, + ) as T + } +} diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt index 365ab15007..a632bfdf50 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt @@ -5,6 +5,7 @@ import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.dataset.DataSet import org.hisp.dhis.android.core.dataset.DataSetInstanceSummary import org.hisp.dhis.android.core.program.Program +import java.util.Date class ProgramViewModelMapper() { fun map( @@ -12,11 +13,9 @@ class ProgramViewModelMapper() { recordCount: Int, recordLabel: String, state: State, - hasOverdue: Boolean, - filtersAreActive: Boolean, metadataIconData: MetadataIconData, - ): ProgramViewModel { - return ProgramViewModel( + ): ProgramUiModel { + return ProgramUiModel( uid = program.uid(), title = program.displayName()!!, metadataIconData = metadataIconData, @@ -32,10 +31,9 @@ class ProgramViewModelMapper() { onlyEnrollOnce = program.onlyEnrollOnce() == true, accessDataWrite = program.access().data().write(), state = State.valueOf(state.name), - hasOverdueEvent = hasOverdue, - filtersAreActive = filtersAreActive, downloadState = ProgramDownloadState.NONE, stockConfig = null, + lastUpdated = program.lastUpdated() ?: Date(), ) } @@ -44,10 +42,9 @@ class ProgramViewModelMapper() { dataSetInstanceSummary: DataSetInstanceSummary, recordCount: Int, dataSetLabel: String, - filtersAreActive: Boolean, metadataIconData: MetadataIconData, - ): ProgramViewModel { - return ProgramViewModel( + ): ProgramUiModel { + return ProgramUiModel( uid = dataSetInstanceSummary.dataSetUid(), title = dataSetInstanceSummary.dataSetDisplayName(), metadataIconData = metadataIconData, @@ -59,18 +56,17 @@ class ProgramViewModelMapper() { onlyEnrollOnce = false, accessDataWrite = dataSet.access().data().write(), state = dataSetInstanceSummary.state(), - hasOverdueEvent = false, - filtersAreActive = filtersAreActive, downloadState = ProgramDownloadState.NONE, stockConfig = null, + lastUpdated = dataSet.lastUpdated() ?: Date(), ) } fun map( - programViewModel: ProgramViewModel, + programUiModel: ProgramUiModel, downloadState: ProgramDownloadState, - ): ProgramViewModel { - return programViewModel.copy( + ): ProgramUiModel { + return programUiModel.copy( downloadState = downloadState, ) } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt index b8c292896e..642fbe283d 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt @@ -6,9 +6,20 @@ import android.os.Bundle import android.transition.ChangeBounds import android.transition.Transition import android.transition.TransitionManager -import android.view.MenuItem import android.view.View import androidx.activity.viewModels +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.constraintlayout.widget.ConstraintSet import androidx.databinding.DataBindingUtil import androidx.lifecycle.viewModelScope @@ -49,11 +60,13 @@ import org.dhis2.utils.analytics.DATA_CREATION import org.dhis2.utils.category.CategoryDialog import org.dhis2.utils.category.CategoryDialog.Companion.TAG import org.dhis2.utils.customviews.RxDateDialog -import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator +import org.dhis2.utils.customviews.navigationbar.NavigationPage import org.dhis2.utils.granularsync.SyncStatusDialog import org.dhis2.utils.granularsync.shouldLaunchSyncDialog import org.hisp.dhis.android.core.period.DatePeriod import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBar +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import timber.log.Timber import java.util.Date import javax.inject.Inject @@ -70,9 +83,6 @@ class ProgramEventDetailActivity : @Inject lateinit var filtersAdapter: FiltersAdapter - @Inject - lateinit var pageConfigurator: NavigationPageConfigurator - @Inject lateinit var networkUtils: NetworkUtils @@ -105,38 +115,8 @@ class ProgramEventDetailActivity : binding = DataBindingUtil.setContentView(this, R.layout.activity_program_event_detail) binding.presenter = presenter binding.totalFilters = FilterManager.getInstance().totalFilters - binding.navigationBar.pageConfiguration(pageConfigurator) - binding.navigationBar.setOnNavigationItemSelectedListener { item: MenuItem -> - when (item.itemId) { - R.id.navigation_list_view -> { - programEventsViewModel.showList() - return@setOnNavigationItemSelectedListener true - } - R.id.navigation_map_view -> { - networkUtils.performIfOnline( - this, - { - presenter.trackEventProgramMap() - programEventsViewModel.showMap() - }, - { - binding.navigationBar.selectItemAt(0) - }, - getString(R.string.msg_network_connection_maps), - ) - return@setOnNavigationItemSelectedListener true - } - - R.id.navigation_analytics -> { - presenter.trackEventProgramAnalytics() - programEventsViewModel.showAnalytics() - return@setOnNavigationItemSelectedListener true - } - - else -> return@setOnNavigationItemSelectedListener false - } - } + setupBottomNavigation() binding.fragmentContainer.clipWithRoundedCorners(16.dp) binding.filterLayout.adapter = filtersAdapter presenter.init() @@ -160,6 +140,67 @@ class ProgramEventDetailActivity : } } + private fun setupBottomNavigation() { + binding.navigationBar.setContent { + DHIS2Theme { + val uiState by programEventsViewModel.navigationBarUIState + val isBackdropActive by programEventsViewModel.backdropActive.observeAsState(false) + var selectedItemIndex by remember(uiState) { + mutableIntStateOf( + uiState.items.indexOfFirst { + it.id == uiState.selectedItem + }, + ) + } + + LaunchedEffect(uiState.selectedItem) { + when (uiState.selectedItem) { + NavigationPage.LIST_VIEW -> { + programEventsViewModel.showList() + } + + NavigationPage.MAP_VIEW -> { + networkUtils.performIfOnline( + context = this@ProgramEventDetailActivity, + action = { + presenter.trackEventProgramMap() + programEventsViewModel.showMap() + }, + onDialogDismissed = { + selectedItemIndex = 0 + }, + noNetworkMessage = getString(R.string.msg_network_connection_maps), + ) + } + + NavigationPage.ANALYTICS -> { + presenter.trackEventProgramAnalytics() + programEventsViewModel.showAnalytics() + } + + else -> { + // no-op + } + } + } + + AnimatedVisibility( + visible = uiState.items.size > 1 && isBackdropActive.not(), + enter = slideInVertically(animationSpec = tween(200)) { it }, + exit = slideOutVertically(animationSpec = tween(200)) { it }, + ) { + NavigationBar( + modifier = Modifier.fillMaxWidth(), + items = uiState.items, + selectedItemIndex = selectedItemIndex, + ) { page -> + programEventsViewModel.onNavigationPageChanged(page) + } + } + } + } + } + private fun initExtras() { programUid = intent.getStringExtra(EXTRA_PROGRAM_UID) ?: "" } @@ -169,7 +210,7 @@ class ProgramEventDetailActivity : ?.plus( ProgramEventDetailModule( this, - programUid, + this, programUid, OrgUnitSelectorScope.ProgramCaptureScope(programUid), ), ) @@ -251,14 +292,16 @@ class ProgramEventDetailActivity : override fun onDestroy() { super.onDestroy() - presenter.setOpeningFilterToNone() - presenter.onDettach() - FilterManager.getInstance().clearEventStatus() - FilterManager.getInstance().clearCatOptCombo() - FilterManager.getInstance().clearWorkingList(true) - FilterManager.getInstance().clearAssignToMe() - FilterManager.getInstance().clearFlow() - presenter.clearOtherFiltersIfWebAppIsConfig() + if (sessionManagerServiceImpl.isUserLoggedIn()) { + presenter.setOpeningFilterToNone() + presenter.onDettach() + FilterManager.getInstance().clearEventStatus() + FilterManager.getInstance().clearCatOptCombo() + FilterManager.getInstance().clearWorkingList(true) + FilterManager.getInstance().clearAssignToMe() + FilterManager.getInstance().clearFlow() + presenter.clearOtherFiltersIfWebAppIsConfig() + } } override fun setProgram(programModel: Program) { @@ -279,13 +322,13 @@ class ProgramEventDetailActivity : val transition: Transition = ChangeBounds() transition.addListener(object : Transition.TransitionListener { override fun onTransitionStart(transition: Transition) { + programEventsViewModel.updateBackdrop(backDropActive) if (!backDropActive) { binding.clearFilters.hide() } } override fun onTransitionEnd(transition: Transition) { - programEventsViewModel.updateBackdrop(backDropActive) if (backDropActive) { binding.clearFilters.show() } @@ -303,9 +346,11 @@ class ProgramEventDetailActivity : /*No action needed*/ } }) + backDropActive = !backDropActive + transition.duration = 200 TransitionManager.beginDelayedTransition(binding.backdropLayout, transition) - backDropActive = !backDropActive + val initSet = ConstraintSet() initSet.clone(binding.backdropLayout) if (backDropActive) { @@ -316,7 +361,20 @@ class ProgramEventDetailActivity : ConstraintSet.BOTTOM, 16.dp, ) - binding.navigationBar.hide() + initSet.connect( + R.id.fragmentContainer, + ConstraintSet.BOTTOM, + ConstraintSet.PARENT_ID, + ConstraintSet.BOTTOM, + 0, + ) + initSet.connect( + R.id.addEventButton, + ConstraintSet.BOTTOM, + R.id.fragmentContainer, + ConstraintSet.BOTTOM, + 16.dp, + ) } else { initSet.connect( R.id.fragmentContainer, @@ -325,7 +383,20 @@ class ProgramEventDetailActivity : ConstraintSet.BOTTOM, 0, ) - binding.navigationBar.show() + initSet.connect( + R.id.fragmentContainer, + ConstraintSet.BOTTOM, + R.id.navigationBar, + ConstraintSet.TOP, + 0, + ) + initSet.connect( + R.id.addEventButton, + ConstraintSet.BOTTOM, + R.id.navigationBar, + ConstraintSet.TOP, + 16.dp, + ) } initSet.applyTo(binding.backdropLayout) } @@ -342,7 +413,6 @@ class ProgramEventDetailActivity : } } else { OUTreeFragment.Builder() - .showAsDialog() .singleSelection() .withPreselectedOrgUnits( listOf(sharedPreferences.getString(CURRENT_ORG_UNIT, "") ?: ""), @@ -383,12 +453,15 @@ class ProgramEventDetailActivity : override fun showPeriodRequest(periodRequest: PeriodRequest) { if (periodRequest == PeriodRequest.FROM_TO) { - DateUtils.getInstance().fromCalendarSelector(this.context) { datePeriod: List? -> - FilterManager.getInstance().addPeriod(datePeriod) - } + DateUtils.getInstance() + .fromCalendarSelector(this.context) { datePeriod: List? -> + FilterManager.getInstance().addPeriod(datePeriod) + } } else { val onFromToSelector = - OnFromToSelector { datePeriods -> FilterManager.getInstance().addPeriod(datePeriods) } + OnFromToSelector { datePeriods -> + FilterManager.getInstance().addPeriod(datePeriods) + } DateUtils.getInstance().showPeriodDialog( this, @@ -414,7 +487,6 @@ class ProgramEventDetailActivity : override fun openOrgUnitTreeSelector() { OUTreeFragment.Builder() - .showAsDialog() .withPreselectedOrgUnits(FilterManager.getInstance().orgUnitUidsFilters) .onSelection { selectedOrgUnits -> presenter.setOrgUnitFilters(selectedOrgUnits) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt index ea6332d292..e95fb9b5b3 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailModule.kt @@ -4,7 +4,8 @@ import android.content.Context import dagger.Module import dagger.Provides import dhis2.org.analytics.charts.Charts -import org.dhis2.animations.CarouselViewAnimations +import org.dhis2.commons.data.ProgramConfigurationRepository +import org.dhis2.commons.date.DateLabelProvider import org.dhis2.commons.date.DateUtils import org.dhis2.commons.di.dagger.PerActivity import org.dhis2.commons.filters.DisableHomeFiltersFromSettingsApp @@ -22,24 +23,19 @@ import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.maps.geometry.bound.GetBoundingBox -import org.dhis2.maps.geometry.mapper.MapGeometryToFeature -import org.dhis2.maps.geometry.mapper.feature.MapCoordinateFieldToFeature -import org.dhis2.maps.geometry.mapper.featurecollection.MapAttributeToFeature -import org.dhis2.maps.geometry.mapper.featurecollection.MapCoordinateFieldToFeatureCollection -import org.dhis2.maps.geometry.mapper.featurecollection.MapDataElementToFeature -import org.dhis2.maps.geometry.mapper.featurecollection.MapEventToFeatureCollection -import org.dhis2.maps.geometry.point.MapPointToFeature -import org.dhis2.maps.geometry.polygon.MapPolygonToFeature import org.dhis2.maps.usecases.MapStyleConfiguration import org.dhis2.maps.utils.DhisMapUtils +import org.dhis2.tracker.data.ProfilePictureProvider +import org.dhis2.tracker.events.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCaseRepository +import org.dhis2.usescases.events.EventInfoProvider import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.hisp.dhis.android.core.D2 @Module class ProgramEventDetailModule( + private val context: Context, private val view: ProgramEventDetailView, private val programUid: String, private val orgUnitSelectorScope: OrgUnitSelectorScope, @@ -79,51 +75,29 @@ class ProgramEventDetailModule( eventDetailRepository: ProgramEventDetailRepository, dispatcher: DispatcherProvider, createEventUseCase: CreateEventUseCase, + pageConfigurator: NavigationPageConfigurator, + resourceManager: ResourceManager, + programConfigurationRepository: ProgramConfigurationRepository, ): ProgramEventDetailViewModelFactory { return ProgramEventDetailViewModelFactory( - MapStyleConfiguration(d2), + MapStyleConfiguration( + d2, + programUid, + programConfigurationRepository, + ), eventDetailRepository, dispatcher, createEventUseCase, + pageConfigurator, + resourceManager, ) } @Provides @PerActivity - fun provideMapGeometryToFeature(): MapGeometryToFeature { - return MapGeometryToFeature(MapPointToFeature(), MapPolygonToFeature()) - } - - @Provides - @PerActivity - fun provideMapEventToFeatureCollection( - mapGeometryToFeature: MapGeometryToFeature, - ): MapEventToFeatureCollection { - return MapEventToFeatureCollection( - mapGeometryToFeature, - GetBoundingBox(), - ) - } - - @Provides - @PerActivity - fun provideMapDataElementToFeatureCollection( - attributeToFeatureMapper: MapAttributeToFeature, - dataElementToFeatureMapper: MapDataElementToFeature, - ): MapCoordinateFieldToFeatureCollection { - return MapCoordinateFieldToFeatureCollection( - dataElementToFeatureMapper, - attributeToFeatureMapper, - ) - } - - @Provides - @PerActivity - fun provideMapCoordinateFieldToFeature( - mapGeometryToFeature: MapGeometryToFeature, - ): MapCoordinateFieldToFeature { - return MapCoordinateFieldToFeature(mapGeometryToFeature) - } + fun provideProgramConfigurationRepository( + d2: D2, + ) = ProgramConfigurationRepository(d2) @Provides @PerActivity @@ -142,29 +116,36 @@ class ProgramEventDetailModule( fun eventDetailRepository( d2: D2, mapper: ProgramEventMapper, - mapEventToFeatureCollection: MapEventToFeatureCollection, - mapCoordinateFieldToFeatureCollection: MapCoordinateFieldToFeatureCollection, dhisMapUtils: DhisMapUtils, filterPresenter: FilterPresenter, charts: Charts, + eventInfoProvider: EventInfoProvider, ): ProgramEventDetailRepository { return ProgramEventDetailRepositoryImpl( programUid, d2, mapper, - mapEventToFeatureCollection, - mapCoordinateFieldToFeatureCollection, dhisMapUtils, filterPresenter, charts, + eventInfoProvider, ) } @Provides @PerActivity - fun animations(): CarouselViewAnimations { - return CarouselViewAnimations() - } + fun eventInfoProvider( + d2: D2, + resourceManager: ResourceManager, + metadataIconProvider: MetadataIconProvider, + profilePictureProvider: ProfilePictureProvider, + ) = EventInfoProvider( + d2, + resourceManager, + DateLabelProvider(context, resourceManager), + metadataIconProvider, + profilePictureProvider, + ) @Provides @PerActivity @@ -201,10 +182,18 @@ class ProgramEventDetailModule( @PerActivity fun provideCreateEventUseCase( dispatcher: DispatcherProvider, - d2: D2, - dateUtils: DateUtils, + repository: CreateEventUseCaseRepository, ) = CreateEventUseCase( dispatcher = dispatcher, + repository = repository, + ) + + @Provides + @PerActivity + fun provideCreateEventUseCaseRepository( + d2: D2, + dateUtils: DateUtils, + ) = CreateEventUseCaseRepository( d2 = d2, dateUtils = dateUtils, ) @@ -217,4 +206,8 @@ class ProgramEventDetailModule( @PerActivity fun provideOURepositoryConfiguration(d2: D2) = OURepositoryConfiguration(d2, orgUnitSelectorScope) + + @Provides + @PerActivity + fun provideProfilePictureProvider(d2: D2) = ProfilePictureProvider(d2) } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt index b3d127cae6..c5904da34d 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt @@ -5,6 +5,7 @@ import io.reactivex.Flowable import io.reactivex.Single import kotlinx.coroutines.flow.Flow import org.dhis2.commons.data.ProgramEventViewModel +import org.dhis2.maps.layer.MapLayer import org.hisp.dhis.android.core.category.CategoryOptionCombo import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.event.Event @@ -14,7 +15,7 @@ import org.hisp.dhis.android.core.program.ProgramStage interface ProgramEventDetailRepository { fun filteredProgramEvents(): Flow> - fun filteredEventsForMap(): Flowable + fun filteredEventsForMap(layersVisibility: Map): Flowable fun program(): Single fun getAccessDataWrite(): Boolean fun getInfoForEvent(eventUid: String): Flowable diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt index 7834dfca3b..4300aecc9a 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt @@ -8,14 +8,17 @@ import io.reactivex.Single import kotlinx.coroutines.flow.Flow import org.dhis2.commons.data.ProgramEventViewModel import org.dhis2.commons.filters.data.FilterPresenter -import org.dhis2.maps.geometry.mapper.featurecollection.MapCoordinateFieldToFeatureCollection -import org.dhis2.maps.geometry.mapper.featurecollection.MapEventToFeatureCollection +import org.dhis2.maps.extensions.filterEventsByLayerVisibility +import org.dhis2.maps.layer.MapLayer import org.dhis2.maps.managers.EventMapManager +import org.dhis2.maps.model.MapItemModel import org.dhis2.maps.utils.DhisMapUtils +import org.dhis2.usescases.events.EventInfoProvider import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.helpers.UidsHelper.getUidsList import org.hisp.dhis.android.core.category.CategoryOptionCombo import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.EventFilter import org.hisp.dhis.android.core.program.Program @@ -25,11 +28,10 @@ class ProgramEventDetailRepositoryImpl internal constructor( private val programUid: String, private val d2: D2, private val mapper: ProgramEventMapper, - private val mapEventToFeatureCollection: MapEventToFeatureCollection, - private val mapCoordinateFieldToFeatureCollection: MapCoordinateFieldToFeatureCollection, private val mapUtils: DhisMapUtils, private val filterPresenter: FilterPresenter, private val charts: Charts?, + private val eventInfoProvider: EventInfoProvider, ) : ProgramEventDetailRepository { private val programRepository = d2.programModule().programs().uid(programUid) @@ -45,18 +47,43 @@ class ProgramEventDetailRepositoryImpl internal constructor( .getPagingData(10) } - override fun filteredEventsForMap(): Flowable { + override fun filteredEventsForMap( + layersVisibility: Map, + ): Flowable { return filterRepository?.get() ?.map { listEvents -> - val (first, second) = mapEventToFeatureCollection.map(listEvents) + val (first, second) = mapUtils.eventsToFeatureCollection(listEvents) val programEventFeatures = HashMap() programEventFeatures[EventMapManager.EVENTS] = first - val deFeatureCollection = mapCoordinateFieldToFeatureCollection.map( - mapUtils.getCoordinateDataElementInfo(getUidsList(listEvents)), - ) + val coordinateDataElements = + mapUtils.getCoordinateDataElementInfo(getUidsList(listEvents)) + val deFeatureCollection = + mapUtils.coordinateFieldsToFeatureCollection(coordinateDataElements) + programEventFeatures.putAll(deFeatureCollection) + + val mapEvents = listEvents.map { event -> + with(eventInfoProvider) { + MapItemModel( + uid = event.uid(), + avatarProviderConfiguration = getAvatar(event), + title = getEventTitle(event), + description = getEventDescription(event), + lastUpdated = getEventLastUpdated(event), + additionalInfoList = getAdditionInfoList(event), + isOnline = false, + geometry = event.geometry(), + relatedInfo = getRelatedInfo(event), + state = event.aggregatedSyncState() ?: State.SYNCED, + ) + } + } + ProgramEventMapData( - mapper.eventsToProgramEvents(listEvents), + mapEvents.filterEventsByLayerVisibility( + layersVisibility, + coordinateDataElements, + ), programEventFeatures, second, ) @@ -66,9 +93,7 @@ class ProgramEventDetailRepositoryImpl internal constructor( override fun getInfoForEvent(eventUid: String): Flowable { return d2.eventModule().events().withTrackedEntityDataValues().uid(eventUid).get() - .map { event -> - mapper.eventToProgramEvent(event) - } + .map(mapper::eventToProgramEvent) .toFlowable() } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt index 6a7a7b0752..425587c116 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt @@ -1,5 +1,14 @@ package org.dhis2.usescases.programEventDetail +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.automirrored.outlined.List +import androidx.compose.material.icons.filled.BarChart +import androidx.compose.material.icons.filled.Map +import androidx.compose.material.icons.outlined.BarChart +import androidx.compose.material.icons.outlined.Map +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -8,16 +17,24 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch +import org.dhis2.R +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.maps.layer.basemaps.BaseMapStyle import org.dhis2.maps.usecases.MapStyleConfiguration -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.NavigationBarUIState +import org.dhis2.tracker.events.CreateEventUseCase +import org.dhis2.utils.customviews.navigationbar.NavigationPage +import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBarItem class ProgramEventDetailViewModel( private val mapStyleConfig: MapStyleConfiguration, val eventRepository: ProgramEventDetailRepository, val dispatcher: DispatcherProvider, val createEventUseCase: CreateEventUseCase, + private val pageConfigurator: NavigationPageConfigurator, + private val resourceManager: ResourceManager, ) : ViewModel() { private val progress = MutableLiveData(false) val writePermission = MutableLiveData(false) @@ -43,6 +60,63 @@ class ProgramEventDetailViewModel( val shouldNavigateToEventDetails: SharedFlow get() = _shouldNavigateToEventDetails + private val _navigationBarUIState = mutableStateOf(NavigationBarUIState()) + val navigationBarUIState: State> = _navigationBarUIState + + init { + viewModelScope.launch { loadBottomBarItems() } + } + + private fun loadBottomBarItems() { + val navItems = mutableListOf>() + + if (pageConfigurator.displayListView()) { + navItems.add( + NavigationBarItem( + id = NavigationPage.LIST_VIEW, + icon = Icons.AutoMirrored.Outlined.List, + selectedIcon = Icons.AutoMirrored.Filled.List, + label = resourceManager.getString(R.string.navigation_list_view), + ), + ) + } + + if (pageConfigurator.displayMapView()) { + navItems.add( + NavigationBarItem( + id = NavigationPage.MAP_VIEW, + icon = Icons.Outlined.Map, + selectedIcon = Icons.Filled.Map, + label = resourceManager.getString(R.string.navigation_map_view), + ), + ) + } + + if (pageConfigurator.displayAnalytics()) { + navItems.add( + NavigationBarItem( + id = NavigationPage.ANALYTICS, + icon = Icons.Outlined.BarChart, + selectedIcon = Icons.Filled.BarChart, + label = resourceManager.getString(R.string.navigation_charts), + ), + ) + } + + _navigationBarUIState.value = _navigationBarUIState.value.copy( + items = navItems, + selectedItem = navItems.firstOrNull()?.id, + ) + + if (_navigationBarUIState.value.selectedItem != null) { + onNavigationPageChanged(navigationBarUIState.value.items.first().id) + } + } + + fun onNavigationPageChanged(page: NavigationPage) { + _navigationBarUIState.value = _navigationBarUIState.value.copy(selectedItem = page) + } + fun setProgress(showProgress: Boolean) { progress.value = showProgress } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt index 2d3695614b..0b0d55bf23 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModelFactory.kt @@ -2,9 +2,11 @@ package org.dhis2.usescases.programEventDetail import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.maps.usecases.MapStyleConfiguration -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCase +import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator @Suppress("UNCHECKED_CAST") class ProgramEventDetailViewModelFactory( @@ -12,6 +14,8 @@ class ProgramEventDetailViewModelFactory( private val eventRepository: ProgramEventDetailRepository, private val dispatcher: DispatcherProvider, private val createEventUseCase: CreateEventUseCase, + private val pageConfigurator: NavigationPageConfigurator, + private val resourceManager: ResourceManager, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -20,6 +24,8 @@ class ProgramEventDetailViewModelFactory( eventRepository, dispatcher, createEventUseCase, + pageConfigurator, + resourceManager, ) as T } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventMapData.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventMapData.kt index 24b485194e..38c7de8bed 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventMapData.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventMapData.kt @@ -2,10 +2,10 @@ package org.dhis2.usescases.programEventDetail import com.mapbox.geojson.BoundingBox import com.mapbox.geojson.FeatureCollection -import org.dhis2.commons.data.ProgramEventViewModel +import org.dhis2.maps.model.MapItemModel data class ProgramEventMapData( - val events: List, + val mapItems: List, val featureCollectionMap: MutableMap, val boundingBox: BoundingBox, ) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt index dfd4766d67..882e42bc41 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt @@ -4,12 +4,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import org.dhis2.commons.filters.workingLists.WorkingListViewModel import org.dhis2.commons.filters.workingLists.WorkingListViewModelFactory +import org.dhis2.databinding.FragmentComposeHolderBinding import org.dhis2.usescases.general.FragmentGlobalAbstract import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity import org.dhis2.usescases.programEventDetail.ProgramEventDetailViewModel @@ -48,16 +48,18 @@ class EventListFragment : FragmentGlobalAbstract() { programEventsViewModel.eventClicked.value = Pair(eventUid, orgUnitUid) } - return ComposeView(requireContext()).apply { + val binding = FragmentComposeHolderBinding.inflate(inflater, container, false) + binding.composeView.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { val workingListViewModel by viewModels { workingListViewModelFactory } EventListScreen( - eventListViewModel, - workingListViewModel, + eventListViewModel = eventListViewModel, + workingListViewModel = workingListViewModel, ) } } + return binding.root } override fun onResume() { diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragment.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragment.kt index eca2a6ab5d..61271eccb1 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragment.kt @@ -4,40 +4,60 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.updateLayoutParams +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Icon +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView import androidx.fragment.app.activityViewModels -import com.mapbox.mapboxsdk.geometry.LatLng -import com.mapbox.mapboxsdk.maps.MapboxMap -import org.dhis2.animations.CarouselViewAnimations -import org.dhis2.bindings.dp +import com.mapbox.mapboxsdk.maps.MapView +import org.dhis2.R +import org.dhis2.commons.bindings.launchImageDetail import org.dhis2.commons.data.ProgramEventViewModel import org.dhis2.commons.locationprovider.LocationSettingLauncher -import org.dhis2.databinding.FragmentProgramEventDetailMapBinding -import org.dhis2.maps.carousel.CarouselAdapter +import org.dhis2.commons.ui.SyncButtonProvider +import org.dhis2.maps.camera.centerCameraOnFeatures import org.dhis2.maps.layer.MapLayerDialog +import org.dhis2.maps.location.MapLocationEngine +import org.dhis2.maps.managers.EventMapManager +import org.dhis2.maps.views.LocationIcon +import org.dhis2.maps.views.MapScreen +import org.dhis2.maps.views.OnMapClickListener +import org.dhis2.ui.avatar.AvatarProvider +import org.dhis2.ui.theme.Dhis2Theme import org.dhis2.usescases.general.FragmentGlobalAbstract import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity import org.dhis2.usescases.programEventDetail.ProgramEventDetailViewModel -import org.dhis2.usescases.programEventDetail.ProgramEventMapData +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem +import org.hisp.dhis.mobile.ui.designsystem.component.IconButton +import org.hisp.dhis.mobile.ui.designsystem.component.IconButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardDescriptionModel +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberAdditionalInfoColumnState +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberListCardState +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import javax.inject.Inject class EventMapFragment : FragmentGlobalAbstract(), - EventMapFragmentView, - MapboxMap.OnMapClickListener { - - private lateinit var binding: FragmentProgramEventDetailMapBinding - - @Inject - lateinit var animations: CarouselViewAnimations + EventMapFragmentView { @Inject lateinit var mapNavigation: org.dhis2.maps.ExternalMapNavigation - private var eventMapManager: org.dhis2.maps.managers.EventMapManager? = null - - private val fragmentLifeCycle = lifecycle + private var eventMapManager: EventMapManager? = null private val programEventsViewModel: ProgramEventDetailViewModel by activityViewModels() @@ -53,62 +73,148 @@ class EventMapFragment : ?.plus(EventMapModule(this)) ?.inject(this) programEventsViewModel.setProgress(true) - binding = FragmentProgramEventDetailMapBinding.inflate(inflater, container, false) - binding.apply { - eventMapManager = org.dhis2.maps.managers.EventMapManager(mapView) - eventMapManager?.let { fragmentLifeCycle.addObserver(it) } - eventMapManager?.onCreate(savedInstanceState) - eventMapManager?.featureType = presenter.programFeatureType() - eventMapManager?.onMapClickListener = this@EventMapFragment - eventMapManager?.init( - mapStyles = programEventsViewModel.fetchMapStyles(), - onInitializationFinished = { - presenter.init() - }, - onMissingPermission = { permissionsManager -> - if (locationProvider.hasLocationEnabled()) { - permissionsManager?.requestLocationPermissions(requireActivity()) - } else { - LocationSettingLauncher.requestEnableLocationSetting(requireContext()) + + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + Dhis2Theme { + val listState = rememberLazyListState() + val eventMapData by presenter.eventMapData.observeAsState(initial = null) + val items by remember(eventMapData) { + derivedStateOf { eventMapData?.mapItems ?: emptyList() } } - }, - ) - mapLayerButton.setOnClickListener { - eventMapManager?.let { - MapLayerDialog(it) - .show(childFragmentManager, MapLayerDialog::class.java.name) - } - } - mapPositionButton.setOnClickListener { - if (locationProvider.hasLocationEnabled()) { - eventMapManager?.centerCameraOnMyPosition { permissionManager -> - permissionManager?.requestLocationPermissions(requireActivity()) + val clickedItem by presenter.mapItemClicked.observeAsState(initial = null) + val locationState = eventMapManager?.locationState?.collectAsState() + + LaunchedEffect(key1 = clickedItem) { + clickedItem?.let { + listState.animateScrollToItem( + items.indexOfFirst { it.uid == clickedItem }, + ) + } + } + + LaunchedEffect(key1 = items) { + eventMapData?.let { data -> + eventMapManager?.takeIf { it.isMapReady() }?.update( + data.featureCollectionMap, + data.boundingBox, + ) + } } - } else { - LocationSettingLauncher.requestEnableLocationSetting(requireContext()) - } - } - } - programEventsViewModel.backdropActive.observe(viewLifecycleOwner) { backdropActive -> - binding.mapView.updateLayoutParams { - val bottomMargin = if (backdropActive) { - 0 - } else { - 40.dp + MapScreen( + items = items, + listState = listState, + onItemScrolled = { item -> + with(eventMapManager) { + this?.requestMapLayerManager()?.selectFeature(null) + this?.findFeatures(item.uid) + ?.takeIf { it.isNotEmpty() }?.let { features -> + map?.centerCameraOnFeatures(features) + } + } + }, + onNavigate = { item -> + eventMapManager?.findFeature(item.uid)?.let { feature -> + startActivity(mapNavigation.navigateToMapIntent(feature)) + } + }, + actionButtons = { + IconButton( + style = IconButtonStyle.TONAL, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_layers), + contentDescription = "", + tint = TextColor.OnPrimaryContainer, + ) + }, + ) { + MapLayerDialog(eventMapManager!!, presenter.programUid()) { layersVisibility -> + presenter.filterVisibleMapItems(layersVisibility) + }.show( + childFragmentManager, + MapLayerDialog::class.java.name, + ) + } + locationState?.let { + LocationIcon( + locationState = it.value, + onLocationButtonClicked = ::onLocationButtonClicked, + ) + } + }, + map = { + AndroidView( + factory = { context -> + MapView(context).also { + loadMap(it, savedInstanceState) + } + }, + modifier = Modifier + .fillMaxSize() + .testTag("MAP"), + ) {} + }, + onItem = { item -> + ListCard( + modifier = Modifier + .fillParentMaxWidth() + .testTag("MAP_ITEM"), + listCardState = rememberListCardState( + title = ListCardTitleModel(text = item.title), + description = item.description?.let { + ListCardDescriptionModel( + text = it, + ) + }, + lastUpdated = item.lastUpdated, + additionalInfoColumnState = rememberAdditionalInfoColumnState( + additionalInfoList = item.additionalInfoList, + syncProgressItem = AdditionalInfoItem( + key = stringResource(id = R.string.syncing), + value = "", + ), + expandLabelText = stringResource(id = R.string.show_more), + shrinkLabelText = stringResource(id = R.string.show_less), + scrollableContent = true, + ), + ), + actionButton = { + SyncButtonProvider(state = item.state) { + programEventsViewModel.eventSyncClicked.value = item.uid + } + }, + onCardClick = { + programEventsViewModel.eventClicked.value = + Pair(item.uid, "") + }, + listAvatar = { + AvatarProvider( + avatarProviderConfiguration = item.avatarProviderConfiguration, + onImageClick = ::launchImageDetail, + ) + }, + ) + }, + ) } - setMargins(0, 0, 0, bottomMargin) } } + } - return binding.root + private fun onLocationButtonClicked() { + eventMapManager?.onLocationButtonClicked( + locationProvider.hasLocationEnabled(), + requireActivity(), + ) } override fun onResume() { super.onResume() programEventsViewModel.updateEvent?.let { eventUid -> - animations.initMapLoading(binding.mapCarousel) programEventsViewModel.setProgress(true) presenter.getEventInfo(eventUid) } @@ -142,61 +248,34 @@ class EventMapFragment : ) } - override fun setMap(mapData: ProgramEventMapData) { - eventMapManager?.update( - mapData.featureCollectionMap, - mapData.boundingBox, - ) - if (binding.mapCarousel.adapter == null) { - val carouselAdapter = - CarouselAdapter.Builder() - .addOnSyncClickListener { teiUid: String? -> - if (binding.mapCarousel.carouselEnabled) { - programEventsViewModel.eventSyncClicked.value = teiUid - } - true - } - .addOnEventClickListener { teiUid: String?, orgUnit: String?, _: String? -> - if (binding.mapCarousel.carouselEnabled) { - programEventsViewModel.eventClicked.value = Pair(teiUid!!, orgUnit!!) - } - true - } - .addOnNavigateClickListener { uid -> - eventMapManager?.findFeature(uid)?.let { feature -> - startActivity(mapNavigation.navigateToMapIntent(feature)) - } + private fun loadMap(mapView: MapView, savedInstanceState: Bundle?) { + eventMapManager = EventMapManager(mapView, MapLocationEngine(requireContext())) + eventMapManager?.also { + lifecycle.addObserver(it) + it.onCreate(savedInstanceState) + it.featureType = presenter.programFeatureType() + it.onMapClickListener = OnMapClickListener(it, presenter::onFeatureClicked) + it.init( + mapStyles = programEventsViewModel.fetchMapStyles(), + onInitializationFinished = { + presenter.filterVisibleMapItems( + it.mapLayerManager.mapLayers.toMap(), + ) + presenter.init() + }, + onMissingPermission = { permissionsManager -> + if (locationProvider.hasLocationEnabled()) { + permissionsManager?.requestLocationPermissions(requireActivity()) + } else { + LocationSettingLauncher.requestEnableLocationSetting(requireContext()) } - .addMapManager(eventMapManager!!) - .build() - binding.mapCarousel.setAdapter(carouselAdapter) - eventMapManager?.carouselAdapter = carouselAdapter - eventMapManager?.let { binding.mapCarousel.attachToMapManager(eventMapManager!!) } - carouselAdapter.setAllItems(mapData.events) - carouselAdapter.updateLayers(eventMapManager?.mapLayerManager?.mapLayers) - } else { - eventMapManager?.let { - (binding.mapCarousel.adapter as CarouselAdapter?)?.setItems(mapData.events) - } + }, + ) } - - eventMapManager?.mapLayerManager?.selectFeature(null) - - animations.endMapLoading(binding.mapCarousel) - programEventsViewModel.setProgress(false) } override fun updateEventCarouselItem(programEventViewModel: ProgramEventViewModel) { - (binding.mapCarousel.adapter as CarouselAdapter).updateItem(programEventViewModel) - animations.endMapLoading(binding.mapCarousel) programEventsViewModel.setProgress(false) programEventsViewModel.updateEvent = null } - - override fun onMapClick(point: LatLng): Boolean { - eventMapManager?.markFeatureAsSelected(point, null)?.let { - binding.mapCarousel.scrollToFeature(it) - return true - } ?: return false - } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragmentView.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragmentView.kt index 1a9ddca2b8..888eaa2f46 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragmentView.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapFragmentView.kt @@ -1,9 +1,7 @@ package org.dhis2.usescases.programEventDetail.eventMap import org.dhis2.commons.data.ProgramEventViewModel -import org.dhis2.usescases.programEventDetail.ProgramEventMapData -interface EventMapFragmentView { - fun setMap(mapData: ProgramEventMapData) +fun interface EventMapFragmentView { fun updateEventCarouselItem(programEventViewModel: ProgramEventViewModel) } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapPresenter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapPresenter.kt index b4c8cf7ee5..b3dd75d856 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventMap/EventMapPresenter.kt @@ -1,5 +1,8 @@ package org.dhis2.usescases.programEventDetail.eventMap +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.mapbox.geojson.Feature import io.reactivex.disposables.CompositeDisposable import io.reactivex.processors.FlowableProcessor import io.reactivex.processors.PublishProcessor @@ -8,7 +11,10 @@ import org.dhis2.commons.prefs.Preference import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.schedulers.defaultSubscribe +import org.dhis2.maps.extensions.toStringProperty +import org.dhis2.maps.layer.MapLayer import org.dhis2.usescases.programEventDetail.ProgramEventDetailRepository +import org.dhis2.usescases.programEventDetail.ProgramEventMapData import org.hisp.dhis.android.core.common.FeatureType import timber.log.Timber @@ -20,16 +26,24 @@ class EventMapPresenter( val schedulerProvider: SchedulerProvider, ) { + private var layersVisibility: Map = emptyMap() + + private val _eventMapData = MutableLiveData() + val eventMapData: LiveData = _eventMapData + + private val _mapItemClicked = MutableLiveData() + val mapItemClicked: LiveData = _mapItemClicked + val disposable = CompositeDisposable() private val eventInfoProcessor: FlowableProcessor = PublishProcessor.create() fun init() { disposable.add( filterManager.asFlowable().startWith(filterManager) - .switchMap { eventRepository.filteredEventsForMap() } + .switchMap { eventRepository.filteredEventsForMap(layersVisibility) } .defaultSubscribe( schedulerProvider, - { view.setMap(it) }, + { _eventMapData.postValue(it) }, { Timber.e(it) }, ), ) @@ -59,4 +73,19 @@ class EventMapPresenter( fun onDestroy() { disposable.clear() } + + fun onFeatureClicked(feature: Feature) { + feature.toStringProperty()?.let { + _mapItemClicked.postValue(it) + } + } + + fun filterVisibleMapItems(layersVisibility: Map) { + this.layersVisibility = layersVisibility + filterManager.publishData() + } + + fun programUid(): String? { + return eventRepository.program().blockingGet()?.uid() + } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCase.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCase.kt deleted file mode 100644 index e053a02c0e..0000000000 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/usecase/CreateEventUseCase.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.dhis2.usescases.programEventDetail.usecase - -import kotlinx.coroutines.withContext -import org.dhis2.commons.date.DateUtils -import org.dhis2.commons.viewmodel.DispatcherProvider -import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.event.EventCreateProjection -import org.hisp.dhis.android.core.maintenance.D2Error - -class CreateEventUseCase( - private val dispatcher: DispatcherProvider, - private val d2: D2, - private val dateUtils: DateUtils, -) { - suspend operator fun invoke( - programUid: String, - orgUnitUid: String, - programStageUid: String, - enrollmentUid: String?, - ): Result = withContext(dispatcher.io()) { - try { - val eventUid = d2.eventModule().events().blockingAdd( - EventCreateProjection.builder().apply { - enrollmentUid?.let { enrollment(enrollmentUid) } - program(programUid) - programStage(programStageUid) - organisationUnit(orgUnitUid) - }.build(), - ) - - val eventRepository = d2.eventModule().events().uid(eventUid) - eventRepository.setEventDate(dateUtils.today) - - Result.success(eventUid) - } catch (error: D2Error) { - Result.failure(error) - } - } -} diff --git a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionActivity.kt b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionActivity.kt index b791ad64c7..0bb7334185 100644 --- a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionActivity.kt @@ -12,6 +12,7 @@ import org.dhis2.commons.Constants import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.databinding.ActivityProgramStageSelectionBinding import org.dhis2.form.model.EventMode import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity @@ -26,6 +27,9 @@ class ProgramStageSelectionActivity : ActivityGlobalAbstract(), ProgramStageSele @Inject lateinit var presenter: ProgramStageSelectionPresenter + @Inject + lateinit var eventResourcesProvider: EventResourcesProvider + private lateinit var adapter: ProgramStageSelectionAdapter public override fun onCreate(savedInstanceState: Bundle?) { @@ -38,6 +42,10 @@ class ProgramStageSelectionActivity : ActivityGlobalAbstract(), ProgramStageSele presenter.onProgramStageClick(programStage) } binding.recyclerView.adapter = adapter + binding.title.text = eventResourcesProvider.formatWithProgramEventLabel( + stringResource = R.string.new_event_label, + programUid = intent.getStringExtra(Constants.PROGRAM_UID), + ) } override fun onResume() { @@ -97,7 +105,6 @@ class ProgramStageSelectionActivity : ActivityGlobalAbstract(), ProgramStageSele enrollmentUid: String?, ) { OUTreeFragment.Builder() - .showAsDialog() .singleSelection() .orgUnitScope( OrgUnitSelectorScope.ProgramCaptureScope(programUid), diff --git a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionInjector.kt b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionInjector.kt index 551383a75c..f8b3b34302 100644 --- a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionInjector.kt +++ b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionInjector.kt @@ -11,7 +11,8 @@ import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.data.RulesUtilsProvider -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCaseRepository import org.hisp.dhis.android.core.D2 @PerActivity @@ -73,9 +74,15 @@ class ProgramStageSelectionModule( @PerActivity fun provideCreateEventUseCase( dispatcherProvider: DispatcherProvider, + repository: CreateEventUseCaseRepository, + ) = CreateEventUseCase(dispatcherProvider, repository) + + @Provides + @PerActivity + fun provideCreateEventUseCaseRepository( d2: D2, dateUtils: DateUtils, - ) = CreateEventUseCase(dispatcherProvider, d2, dateUtils) + ) = CreateEventUseCaseRepository(d2, dateUtils) @Provides @PerActivity diff --git a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionPresenter.kt b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionPresenter.kt index e19a9e7843..76d2e68299 100644 --- a/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/programStageSelection/ProgramStageSelectionPresenter.kt @@ -11,7 +11,7 @@ import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.form.data.RulesUtilsProvider import org.dhis2.form.model.EventMode -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.utils.Result import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor diff --git a/app/src/main/java/org/dhis2/usescases/qrScanner/ScanActivity.kt b/app/src/main/java/org/dhis2/usescases/qrScanner/ScanActivity.kt index 35a73c5c34..e78cbbc67f 100644 --- a/app/src/main/java/org/dhis2/usescases/qrScanner/ScanActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/qrScanner/ScanActivity.kt @@ -93,9 +93,10 @@ class ScanActivity : ActivityGlobalAbstract() { override fun onRequestPermissionsResult( requestCode: Int, - permissions: Array, + permissions: Array, grantResults: IntArray, ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) capture.onRequestPermissionsResult(requestCode, permissions, grantResults) } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/MapDataRepository.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/MapDataRepository.kt index b91f400a80..978b9367f3 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/MapDataRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/MapDataRepository.kt @@ -1,89 +1,69 @@ package org.dhis2.usescases.searchTrackEntity import com.mapbox.geojson.FeatureCollection -import org.dhis2.commons.data.SearchTeiModel -import org.dhis2.commons.data.uids import org.dhis2.data.search.SearchParametersModel +import org.dhis2.maps.extensions.filterRelationshipsByLayerVisibility +import org.dhis2.maps.extensions.filterTeiByLayerVisibility +import org.dhis2.maps.extensions.filterTrackerEventsByLayerVisibility import org.dhis2.maps.geometry.mapper.featurecollection.MapCoordinateFieldToFeatureCollection import org.dhis2.maps.geometry.mapper.featurecollection.MapTeiEventsToFeatureCollection import org.dhis2.maps.geometry.mapper.featurecollection.MapTeisToFeatureCollection -import org.dhis2.maps.mapper.EventToEventUiComponent -import org.dhis2.maps.utils.CoordinateAttributeInfo -import org.dhis2.maps.utils.CoordinateDataElementInfo +import org.dhis2.maps.layer.MapLayer import org.dhis2.maps.utils.DhisMapUtils -import org.dhis2.usescases.searchTrackEntity.adapters.uids import org.hisp.dhis.android.core.program.Program class MapDataRepository( - private val searchRepository: SearchRepository, + private val searchRepositoryKt: SearchRepositoryKt, private val mapTeisToFeatureCollection: MapTeisToFeatureCollection, private val mapTeiEventsToFeatureCollection: MapTeiEventsToFeatureCollection, private val mapCoordinateFieldToFeatureCollection: MapCoordinateFieldToFeatureCollection, - private val eventToEventUiComponent: EventToEventUiComponent, private val mapUtils: DhisMapUtils, ) { + fun getTrackerMapData( selectedProgram: Program?, queryData: MutableMap, + layersVisibility: Map = emptyMap(), ): TrackerMapData { - val teis = searchRepository.searchTeiForMap( + val mapTeis = searchRepositoryKt.searchTeiForMap( SearchParametersModel( selectedProgram, queryData, ), true, - ).blockingFirst() - val events = searchRepository.getEventsForMap(teis) + ) + + val mapEvents = + searchRepositoryKt.searchEventForMap(mapTeis.map { it.uid }, selectedProgram) + + val mapRelationships = + searchRepositoryKt.searchRelationshipsForMap(mapTeis, selectedProgram) + + val coordinateDataElements = mapUtils.getCoordinateDataElementInfo(mapEvents.map { it.uid }) - val coordinateDataElements = mapUtils.getCoordinateDataElementInfo(events.uids()) val dataElements = mapCoordinateFieldToFeatureCollection.map(coordinateDataElements) - val coordinateAttributes = mapUtils.getCoordinateAttributeInfo(teis.uids()) + val coordinateAttributes = mapUtils.getCoordinateAttributeInfo(mapTeis.map { it.uid }) val attributes = mapCoordinateFieldToFeatureCollection.map(coordinateAttributes) val coordinateFields = mutableMapOf().apply { putAll(dataElements) putAll(attributes) } - val eventsUi = eventToEventUiComponent.mapList(events, teis) val teiFeatureCollection = - mapTeisToFeatureCollection.map(teis, selectedProgram != null) + mapTeisToFeatureCollection.map(mapTeis, selectedProgram != null, mapRelationships) val eventsByProgramStage = - mapTeiEventsToFeatureCollection.map(eventsUi).component1() + mapTeiEventsToFeatureCollection.map(mapEvents).component1() + return TrackerMapData( - teiModels = teis.filter { - hasCoordinates(it) or - hasEnrollmentCoordinates(it) or - hasAttributeCoordinates(it, coordinateAttributes) or - hasDataElementCoordinates(it, coordinateDataElements) - }.toMutableList(), + mapItems = mapTeis.filterTeiByLayerVisibility(layersVisibility, coordinateAttributes) + + mapEvents.filterTrackerEventsByLayerVisibility( + layersVisibility, + coordinateDataElements, + ) + + mapRelationships.filterRelationshipsByLayerVisibility(layersVisibility), eventFeatures = eventsByProgramStage, teiFeatures = teiFeatureCollection.first, teiBoundingBox = teiFeatureCollection.second, - eventModels = eventsUi.filter { it.event.geometry() != null }.toMutableList(), dataElementFeaturess = coordinateFields, ) } - - private fun hasCoordinates(searchTeiModel: SearchTeiModel): Boolean { - return searchTeiModel.tei.geometry() != null - } - - private fun hasEnrollmentCoordinates(searchTeiModel: SearchTeiModel): Boolean { - return searchTeiModel.selectedEnrollment?.geometry() != null - } - - private fun hasAttributeCoordinates( - searchTeiModel: SearchTeiModel, - coordinateAttributes: List, - ): Boolean { - return coordinateAttributes.find { it.tei.uid() == searchTeiModel.uid() } != null - } - - private fun hasDataElementCoordinates( - searchTeiModel: SearchTeiModel, - coordinateDataElements: List, - ): Boolean { - return coordinateDataElements.find { - it.enrollment?.uid() == searchTeiModel.selectedEnrollment.uid() - } != null - } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java index 8691eea1fd..d38d34b130 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepository.java @@ -4,7 +4,6 @@ import androidx.annotation.Nullable; import org.dhis2.commons.data.EventViewModel; -import org.dhis2.commons.data.SearchTeiModel; import org.dhis2.commons.data.tuples.Pair; import org.dhis2.commons.filters.FilterManager; import org.dhis2.commons.filters.sorting.SortingItem; diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java index 72633ee737..7aecc48978 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -12,10 +12,6 @@ import org.dhis2.commons.data.EntryMode; import org.dhis2.commons.data.EventViewModel; import org.dhis2.commons.data.EventViewModelType; -import org.dhis2.commons.data.RelationshipDirection; -import org.dhis2.commons.data.RelationshipOwnerType; -import org.dhis2.commons.data.RelationshipViewModel; -import org.dhis2.commons.data.SearchTeiModel; import org.dhis2.commons.data.tuples.Pair; import org.dhis2.commons.data.tuples.Trio; import org.dhis2.commons.date.DateUtils; @@ -37,7 +33,9 @@ import org.dhis2.metadata.usecases.FileResourceConfiguration; import org.dhis2.metadata.usecases.ProgramConfiguration; import org.dhis2.metadata.usecases.TrackedEntityInstanceConfiguration; -import org.dhis2.ui.MetadataIconData; +import org.dhis2.tracker.relationships.model.RelationshipDirection; +import org.dhis2.tracker.relationships.model.RelationshipModel; +import org.dhis2.tracker.relationships.model.RelationshipOwnerType; import org.dhis2.ui.ThemeManager; import org.dhis2.usescases.teiDownload.TeiDownloader; import org.dhis2.utils.ValueUtils; @@ -303,7 +301,7 @@ public Observable> saveToEnroll(@NonNull String teiType, .organisationUnit(orgUnit) .build()) .map(enrollmentUid -> { - d2.enrollmentModule().enrollments().uid(enrollmentUid).setEnrollmentDate(DateUtils.getInstance().getToday()); + d2.enrollmentModule().enrollments().uid(enrollmentUid).setEnrollmentDate(DateUtils.getInstance().getStartOfDay(new Date())); d2.enrollmentModule().enrollments().uid(enrollmentUid).setFollowUp(false); return Pair.create(enrollmentUid, uid); }) @@ -368,7 +366,7 @@ private void setAttributeValue(SearchTeiModel searchTei, TrackedEntitySearchItem String value = attribute.getValue(); String transformedValue; if (value != null) { - transformedValue = ValueUtils.Companion.transformValue(d2, value, attribute.getValueType(), attribute.getOptionSet()); + transformedValue = ValueUtils.Companion.transformValue(d2, value, attribute.getValueType(), attribute.getOptionSet()); } else { transformedValue = sortingValueSetter.getUnknownLabel(); } @@ -410,7 +408,7 @@ private void setOverdueEvents(@NonNull SearchTeiModel tei, Program selectedProgr if (count > 0) { tei.setHasOverdue(true); Date scheduleDate = !scheduleList.isEmpty() ? scheduleList.get(0).dueDate() : null; - Date overdueDate = !overdueList.isEmpty() ? overdueList.get(0).dueDate() : null; + Date overdueDate = !overdueList.isEmpty() ? overdueList.get(0).dueDate() : null; Date dateToShow = null; if (scheduleDate != null && overdueDate != null) { if (scheduleDate.before(overdueDate)) { @@ -428,7 +426,7 @@ private void setOverdueEvents(@NonNull SearchTeiModel tei, Program selectedProgr } private void setRelationshipsInfo(@NonNull SearchTeiModel searchTeiModel, Program selectedProgram) { - List relationshipViewModels = new ArrayList<>(); + List relationshipModels = new ArrayList<>(); List relationships = d2.relationshipModule().relationships().getByItem( RelationshipItem.builder().trackedEntityInstance( RelationshipItemTrackedEntityInstance.builder() @@ -467,7 +465,7 @@ private void setRelationshipsInfo(@NonNull SearchTeiModel searchTeiModel, Progra for (TrackedEntityAttributeValue attributeValue : toAttr) { toValues.add(new kotlin.Pair<>(attributeValue.trackedEntityAttribute(), attributeValue.value())); } - relationshipViewModels.add(new RelationshipViewModel( + relationshipModels.add(new RelationshipModel( relationship, fromTei.geometry(), toTei.geometry(), @@ -481,13 +479,17 @@ private void setRelationshipsInfo(@NonNull SearchTeiModel searchTeiModel, Progra profilePicturePath(toTei, selectedProgram.uid()), getTeiDefaultRes(fromTei), getTeiDefaultRes(toTei), - MetadataIconData.Companion.defaultIcon(), - true + null, + true, + null, + null, + null, + null )); } } - searchTeiModel.setRelationships(relationshipViewModels); + searchTeiModel.setRelationships(relationshipModels); } private String profilePicturePath(TrackedEntityInstance tei, String programUid) { @@ -734,7 +736,7 @@ public SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Pr .orderByEnrollmentDate(RepositoryScope.OrderByDirection.DESC) .blockingGet(); - if (!enrollmentsInProgram.isEmpty()) { + if (selectedProgram != null && !enrollmentsInProgram.isEmpty()) { for (Enrollment enrollment : enrollmentsInProgram) { if (enrollment.status() == EnrollmentStatus.ACTIVE) { searchTei.setCurrentEnrollment(enrollment); @@ -745,12 +747,12 @@ public SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable Pr searchTei.setCurrentEnrollment(enrollmentsInProgram.get(0)); } } - - searchTei.setOnline(!searchItem.isOnline()); - + // set online parameter from the tei search item + searchTei.setOnline(searchItem.isOnline()); + // If search is being conducted offline only, set the search as offline if (offlineOnly) - searchTei.setOnline(!offlineOnly); - + searchTei.setOnline(false); + // if the local database tei is deleted, set search as online if (Boolean.TRUE.equals(dbTei.deleted())) { searchTei.setOnline(true); } @@ -937,6 +939,6 @@ public boolean canCreateInProgramWithoutSearch() { private boolean displayOrgUnit() { return d2.organisationUnitModule().organisationUnits() .byProgramUids(Collections.singletonList(currentProgram)) - .blockingGet().size() > 1; + .blockingCount() > 1; } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt index bee7a918e3..6abcc4278d 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImplKt.kt @@ -10,17 +10,27 @@ import org.dhis2.data.search.SearchParametersModel import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.OptionSetConfiguration import org.dhis2.form.ui.FieldViewModelFactory +import org.dhis2.maps.model.MapItemModel import org.dhis2.ui.toColor +import org.dhis2.usescases.events.EventInfoProvider +import org.dhis2.usescases.tracker.TrackedEntityInstanceInfoProvider import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.event.EventStatus +import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramTrackedEntityAttribute import org.hisp.dhis.android.core.program.SectionRenderingType +import org.hisp.dhis.android.core.relationship.RelationshipItem +import org.hisp.dhis.android.core.relationship.RelationshipItemTrackedEntityInstance import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchCollectionRepository import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemHelper.toTrackedEntityInstance import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import timber.log.Timber class SearchRepositoryImplKt( private val searchRepositoryJava: SearchRepository, @@ -28,10 +38,18 @@ class SearchRepositoryImplKt( private val dispatcher: DispatcherProvider, private val fieldViewModelFactory: FieldViewModelFactory, private val metadataIconProvider: MetadataIconProvider, + private val trackedEntityInstanceInfoProvider: TrackedEntityInstanceInfoProvider, + private val eventInfoProvider: EventInfoProvider, ) : SearchRepositoryKt { + private lateinit var savedSearchParameters: SearchParametersModel + + private lateinit var savedFilters: FilterManager + private lateinit var trackedEntityInstanceQuery: TrackedEntitySearchCollectionRepository + private val fetchedTeiUids = HashSet() + override fun searchTrackedEntities( searchParametersModel: SearchParametersModel, isOnline: Boolean, @@ -44,27 +62,33 @@ class SearchRepositoryImplKt( searchParametersModel: SearchParametersModel, isOnline: Boolean, ): TrackedEntitySearchCollectionRepository { - trackedEntityInstanceQuery = - searchRepositoryJava.getFilteredRepository(searchParametersModel) - - val allowCache = !( - searchParametersModel != searchRepositoryJava.savedSearchParameters || - !FilterManager.getInstance().sameFilters(searchRepositoryJava.savedFilters) - ) + var allowCache = false + savedSearchParameters = searchParametersModel.copy() + savedFilters = FilterManager.getInstance().copy() - if ( - searchRepositoryJava.fetchedTeiUIDs.isNotEmpty() && - searchParametersModel.selectedProgram == null + if (searchParametersModel != savedSearchParameters || !FilterManager.getInstance() + .sameFilters(savedFilters) ) { - trackedEntityInstanceQuery = trackedEntityInstanceQuery.excludeUids() - .`in`(searchRepositoryJava.fetchedTeiUIDs.toList()) + trackedEntityInstanceQuery = + searchRepositoryJava.getFilteredRepository(searchParametersModel) + } else { + trackedEntityInstanceQuery = + searchRepositoryJava.getFilteredRepository(searchParametersModel) + allowCache = true } - return if (isOnline && FilterManager.getInstance().stateFilters.isEmpty()) { + if (fetchedTeiUids.isNotEmpty() && searchParametersModel.selectedProgram == null) { + trackedEntityInstanceQuery = + trackedEntityInstanceQuery.excludeUids().`in`(fetchedTeiUids.toList()) + } + + val pagerFlow = if (isOnline && FilterManager.getInstance().stateFilters.isEmpty()) { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineFirst() } else { trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineOnly() } + + return pagerFlow } override suspend fun searchParameters( @@ -72,11 +96,28 @@ class SearchRepositoryImplKt( teiTypeUid: String, ): List = withContext(dispatcher.io()) { - programUid?.let { + val searchParameters = programUid?.let { programTrackedEntityAttributes(programUid) } ?: trackedEntitySearchFields(teiTypeUid) + + sortSearchParameters(searchParameters) } + fun sortSearchParameters(parameters: List): List { + return parameters.sortedWith( + compareByDescending { + it.renderingType?.isQROrBarcode() == true && isUnique(it.uid) + }.thenByDescending { + it.renderingType?.isQROrBarcode() == true + }.thenByDescending { isUnique(it.uid) }, + ) + } + + private fun isUnique(teaUid: String): Boolean { + return d2.trackedEntityModule().trackedEntityAttributes().uid(teaUid) + .blockingGet()?.unique() ?: false + } + override suspend fun searchTrackedEntitiesImmediate( searchParametersModel: SearchParametersModel, isOnline: Boolean, @@ -85,6 +126,171 @@ class SearchRepositoryImplKt( .blockingGet() } + override fun searchTeiForMap( + searchParametersModel: SearchParametersModel, + isOnline: Boolean, + ): List { + var allowCache = false + if (searchParametersModel != savedSearchParameters || FilterManager.getInstance() != savedFilters) { + trackedEntityInstanceQuery = + searchRepositoryJava.getFilteredRepository(searchParametersModel) + } else { + allowCache = true + } + + return if (isOnline && FilterManager.getInstance().stateFilters.isEmpty()) { + trackedEntityInstanceQuery.allowOnlineCache() + .eq(allowCache).offlineFirst().blockingGet() + .map { tei -> + transformForMap( + tei, + searchParametersModel.selectedProgram, + ) + } + } else { + trackedEntityInstanceQuery.allowOnlineCache().eq(allowCache).offlineOnly() + .blockingGet() + .map { tei -> + transformForMap( + tei, + searchParametersModel.selectedProgram, + ) + } + } + } + + private fun transformForMap( + searchItem: TrackedEntitySearchItem, + selectedProgram: Program?, + ): MapItemModel { + fetchedTeiUids.add(searchItem.uid()) + val tei = if (searchItem.isOnline) { + d2.trackedEntityModule().trackedEntityInstances() + .uid(searchItem.uid()).blockingGet()!! + } else { + toTrackedEntityInstance(searchItem) + } + + val attributeValues = trackedEntityInstanceInfoProvider.getTeiAdditionalInfoList( + searchItem.attributeValues ?: emptyList(), + ) + + return MapItemModel( + uid = searchItem.uid, + avatarProviderConfiguration = trackedEntityInstanceInfoProvider.getAvatar( + tei, + selectedProgram?.uid(), + attributeValues.firstOrNull(), + ), + title = trackedEntityInstanceInfoProvider.getTeiTitle( + searchItem.header, + attributeValues, + ), + description = null, + lastUpdated = trackedEntityInstanceInfoProvider.getTeiLastUpdated(searchItem), + additionalInfoList = attributeValues, + isOnline = d2.trackedEntityModule().trackedEntityInstances().uid(searchItem.uid) + .blockingGet() == null, + geometry = searchItem.geometry, + relatedInfo = trackedEntityInstanceInfoProvider.getRelatedInfo( + searchItem, + selectedProgram, + ), + state = searchItem.syncState ?: State.SYNCED, + ) + } + + override fun searchRelationshipsForMap( + teis: List, + selectedProgram: Program?, + ): List { + return buildList { + teis.forEach { tei -> + d2.relationshipModule().relationships().getByItem( + searchItem = RelationshipItem.builder().trackedEntityInstance( + RelationshipItemTrackedEntityInstance.builder() + .trackedEntityInstance(tei.uid) + .build(), + ).build(), + includeDeleted = false, + onlyAccessible = false, + ) + .forEach { relationship -> + add( + trackedEntityInstanceInfoProvider.updateRelationshipInfo( + tei, + relationship, + ), + ) + + val relationshipTarget = if (relationship.to()?.trackedEntityInstance() + ?.trackedEntityInstance() == tei.uid + ) { + relationship.from() + } else { + relationship.to() + } + + when { + relationshipTarget?.trackedEntityInstance() != null && + teis.none { it.uid == relationshipTarget.elementUid() } -> { + val trackedEntityType = + d2.trackedEntityModule().trackedEntityInstances() + .uid(relationshipTarget.elementUid()) + .blockingGet() + ?.trackedEntityType() + val relationshipTei = d2.trackedEntityModule().trackedEntitySearch() + .byTrackedEntityType().eq(trackedEntityType) + .uid(relationshipTarget.elementUid()) + .blockingGet() + + relationshipTei?.let { + add( + trackedEntityInstanceInfoProvider.updateRelationshipInfo( + transformForMap(it, null), + relationship, + ), + ) + } + } + + relationshipTarget?.event() != null -> { + Timber.tag("MAP RELATIONSHIP BUILDER") + .d("Event need to be added and updated with relationship info") + } + } + } + } + } + } + + override fun searchEventForMap( + teiUids: List, + selectedProgram: Program?, + ): List { + return d2.eventModule().events() + .byTrackedEntityInstanceUids(teiUids) + .byProgramUid().eq(selectedProgram?.uid()) + .byStatus().`in`(listOf(EventStatus.ACTIVE, EventStatus.OVERDUE, EventStatus.COMPLETED)) + .byDeleted().isFalse + .blockingGet().map { event -> + with(eventInfoProvider) { + MapItemModel( + uid = event.uid(), + avatarProviderConfiguration = getAvatar(event), + title = getEventTitle(event), + description = getEventDescription(event), + lastUpdated = getEventLastUpdated(event), + additionalInfoList = getAdditionInfoList(event), + isOnline = false, + geometry = event.geometry(), + relatedInfo = getRelatedInfo(event), + state = event.aggregatedSyncState() ?: State.SYNCED, + ) + } + } + } + private fun programTrackedEntityAttributes(programUid: String): List { val searchableAttributes = d2.programModule().programTrackedEntityAttributes() .withRenderType() diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryKt.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryKt.kt index 0b93ebd1d0..720f91bcf8 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryKt.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryKt.kt @@ -4,6 +4,8 @@ import androidx.paging.PagingData import kotlinx.coroutines.flow.Flow import org.dhis2.data.search.SearchParametersModel import org.dhis2.form.model.FieldUiModel +import org.dhis2.maps.model.MapItemModel +import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem interface SearchRepositoryKt { @@ -15,5 +17,23 @@ interface SearchRepositoryKt { suspend fun searchParameters(programUid: String?, teiTypeUid: String): List - suspend fun searchTrackedEntitiesImmediate(searchParametersModel: SearchParametersModel, isOnline: Boolean): List + suspend fun searchTrackedEntitiesImmediate( + searchParametersModel: SearchParametersModel, + isOnline: Boolean, + ): List + + fun searchTeiForMap( + searchParametersModel: SearchParametersModel, + isOnline: Boolean, + ): List + + fun searchEventForMap( + teiUids: List, + selectedProgram: Program?, + ): List + + fun searchRelationshipsForMap( + teis: List, + selectedProgram: Program?, + ): List } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java deleted file mode 100644 index 0566e41af7..0000000000 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.java +++ /dev/null @@ -1,751 +0,0 @@ -package org.dhis2.usescases.searchTrackEntity; - -import static android.view.View.GONE; -import static org.dhis2.commons.extensions.ViewExtensionsKt.closeKeyboard; -import static org.dhis2.usescases.searchTrackEntity.searchparameters.SearchParametersScreenKt.initSearchScreen; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.content.res.Configuration; -import android.os.Bundle; -import android.os.PersistableBundle; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.OptIn; -import androidx.compose.animation.ExperimentalAnimationApi; -import androidx.databinding.DataBindingUtil; -import androidx.fragment.app.FragmentTransaction; -import androidx.lifecycle.ViewModelProvider; - -import com.google.android.material.snackbar.BaseTransientBottomBar; -import com.google.android.material.snackbar.Snackbar; - -import org.dhis2.App; -import org.dhis2.R; -import org.dhis2.bindings.ExtensionsKt; -import org.dhis2.bindings.ViewExtensionsKt; -import org.dhis2.commons.Constants; -import org.dhis2.commons.date.DateUtils; -import org.dhis2.commons.date.Period; -import org.dhis2.commons.featureconfig.data.FeatureConfigRepository; -import org.dhis2.commons.filters.FilterItem; -import org.dhis2.commons.filters.FilterManager; -import org.dhis2.commons.filters.Filters; -import org.dhis2.commons.filters.FiltersAdapter; -import org.dhis2.commons.network.NetworkUtils; -import org.dhis2.commons.orgunitselector.OUTreeFragment; -import org.dhis2.commons.resources.ResourceManager; -import org.dhis2.commons.sync.SyncContext; -import org.dhis2.data.forms.dataentry.ProgramAdapter; -import org.dhis2.databinding.ActivitySearchBinding; -import org.dhis2.form.ui.intent.FormIntent; -import org.dhis2.ui.ThemeManager; -import org.dhis2.usescases.general.ActivityGlobalAbstract; -import org.dhis2.usescases.searchTrackEntity.listView.SearchTEList; -import org.dhis2.usescases.searchTrackEntity.mapView.SearchTEMap; -import org.dhis2.usescases.searchTrackEntity.ui.SearchScreenConfigurator; -import org.dhis2.utils.OrientationUtilsKt; -import org.dhis2.utils.customviews.BreakTheGlassBottomDialog; -import org.dhis2.utils.customviews.RxDateDialog; -import org.dhis2.utils.granularsync.SyncStatusDialog; -import org.dhis2.utils.granularsync.SyncStatusDialogNavigatorKt; -import org.hisp.dhis.android.core.arch.call.D2Progress; -import org.hisp.dhis.android.core.common.ValueType; -import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.inject.Inject; - -import dhis2.org.analytics.charts.ui.GroupAnalyticsFragment; -import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Consumer; -import kotlin.Pair; -import kotlin.Unit; -import timber.log.Timber; - -public class SearchTEActivity extends ActivityGlobalAbstract implements SearchTEContractsModule.View { - - ActivitySearchBinding binding; - SearchScreenConfigurator searchScreenConfigurator; - - @Inject - SearchTEContractsModule.Presenter presenter; - - @Inject - FiltersAdapter filtersAdapter; - - @Inject - SearchTeiViewModelFactory viewModelFactory; - - @Inject - SearchNavigator searchNavigator; - - @Inject - NetworkUtils networkUtils; - - @Inject - ThemeManager themeManager; - - @Inject - FeatureConfigRepository featureConfig; - - @Inject - ResourceManager resourceManager; - - private static final String INITIAL_PAGE = "initialPage"; - - private String initialProgram; - private String tEType; - private Map initialQuery; - - private boolean fromRelationship = false; - private String fromRelationshipTeiUid; - private boolean fromAnalytics = false; - - private SearchTEIViewModel viewModel; - - private boolean initSearchNeeded = true; - public SearchTEComponent searchComponent; - private int initialPage = 0; - - public enum Extra { - TEI_UID("TRACKED_ENTITY_UID"), - PROGRAM_UID("PROGRAM_UID"), - QUERY_ATTR("QUERY_DATA_ATTR"), - QUERY_VALUES("QUERY_DATA_VALUES"); - private final String key; - - Extra(String key) { - this.key = key; - } - - public String key() { - return key; - } - } - - private enum Content { - LIST, - MAP, - ANALYTICS - } - - private Content currentContent = null; - - private String CURRENT_SCREEN = "current_screen"; - - public static Intent getIntent(Context context, String programUid, String teiTypeToAdd, String teiUid, boolean fromRelationship) { - Intent intent = new Intent(context, SearchTEActivity.class); - Bundle extras = new Bundle(); - extras.putBoolean("FROM_RELATIONSHIP", fromRelationship); - extras.putString("FROM_RELATIONSHIP_TEI", teiUid); - extras.putString(Extra.TEI_UID.key, teiTypeToAdd); - extras.putString(Extra.PROGRAM_UID.key, programUid); - intent.putExtras(extras); - return intent; - } - - @SuppressLint("ClickableViewAccessibility") - @Override - @OptIn(markerClass = ExperimentalAnimationApi.class) - protected void onCreate(@Nullable Bundle savedInstanceState) { - - initializeVariables(savedInstanceState); - inject(); - - if (initialProgram != null) { - themeManager.setProgramTheme(initialProgram); - } - super.onCreate(savedInstanceState); - - viewModel = new ViewModelProvider(this, viewModelFactory).get(SearchTEIViewModel.class); - - binding = DataBindingUtil.setContentView(this, R.layout.activity_search); - if(savedInstanceState!=null && savedInstanceState.getString(CURRENT_SCREEN)!=null){ - currentContent = Content.valueOf(savedInstanceState.getString(CURRENT_SCREEN)); - } - initSearchParameters(); - - searchScreenConfigurator = new SearchScreenConfigurator( - binding, - isOpen -> { - viewModel.setFiltersOpened(isOpen); - return Unit.INSTANCE; - }); - if (savedInstanceState != null && savedInstanceState.containsKey(INITIAL_PAGE)) { - initialPage = savedInstanceState.getInt(INITIAL_PAGE); - binding.setNavigationInitialPage(initialPage); - } - binding.setPresenter(presenter); - binding.setTotalFilters(FilterManager.getInstance().getTotalFilters()); - - if (OrientationUtilsKt.isLandscape()) { - viewModel.getFiltersOpened().observe(this, isOpened -> { - if (Boolean.TRUE.equals(isOpened)) { - ViewExtensionsKt.clipWithRoundedCorners(binding.mainComponent, ExtensionsKt.getDp(16)); - } else { - ViewExtensionsKt.clipWithTopRightRoundedCorner(binding.mainComponent, ExtensionsKt.getDp(16)); - } - }); - } else { - ViewExtensionsKt.clipWithRoundedCorners(binding.mainComponent, ExtensionsKt.getDp(16)); - } - - binding.filterRecyclerLayout.setAdapter(filtersAdapter); - - binding.executePendingBindings(); - - binding.syncButton.setVisibility(initialProgram != null ? View.VISIBLE : GONE); - binding.syncButton.setOnClickListener(v -> openSyncDialog()); - - SearchJavaToComposeKt.setLandscapeOpenSearchButton( - binding.landOpenSearchButton, - viewModel, - () -> { - viewModel.setSearchScreen(); - return Unit.INSTANCE; - } - ); - - configureBottomNavigation(); - observeScreenState(); - observeDownload(); - observeLegacyInteractions(); - - if (SyncStatusDialogNavigatorKt.shouldLaunchSyncDialog(getIntent())) { - openSyncDialog(); - } - } - - private void initializeVariables(Bundle savedInstanceState) { - tEType = getIntent().getStringExtra("TRACKED_ENTITY_UID"); - initialProgram = getIntent().getStringExtra("PROGRAM_UID"); - try { - fromRelationship = getIntent().getBooleanExtra("FROM_RELATIONSHIP", false); - fromRelationshipTeiUid = getIntent().getStringExtra("FROM_RELATIONSHIP_TEI"); - } catch (Exception e) { - Timber.d(e); - } - initialQuery = SearchTEExtraKt.queryDataExtra(this, savedInstanceState); - } - - private void inject() { - searchComponent = ((App) getApplicationContext()).userComponent().plus( - new SearchTEModule(this, - tEType, - initialProgram, - getContext(), - initialQuery - )); - searchComponent.inject(this); - } - - @Override - protected void onResume() { - super.onResume(); - FilterManager.getInstance().clearUnsupportedFilters(); - - if (initSearchNeeded) { - presenter.init(); - } else { - initSearchNeeded = true; - } - - binding.setTotalFilters(FilterManager.getInstance().getTotalFilters()); - } - - @Override - protected void onPause() { - presenter.setOpeningFilterToNone(); - if (initSearchNeeded) { - presenter.onDestroy(); - } - super.onPause(); - } - - @Override - protected void onDestroy() { - presenter.onDestroy(); - - FilterManager.getInstance().clearEnrollmentStatus(); - FilterManager.getInstance().clearEventStatus(); - FilterManager.getInstance().clearEnrollmentDate(); - FilterManager.getInstance().clearWorkingList(true); - FilterManager.getInstance().clearSorting(); - FilterManager.getInstance().clearAssignToMe(); - FilterManager.getInstance().clearFollowUp(); - presenter.clearOtherFiltersIfWebAppIsConfig(); - - super.onDestroy(); - } - - @Override - public void onBackPressed() { - viewModel.onBackPressed( - OrientationUtilsKt.isPortrait(), - viewModel.searchOrFilterIsOpen(), - ExtensionsKt.isKeyboardOpened(this), - () -> { - super.onBackPressed(); - return Unit.INSTANCE; - }, - () -> { - if (viewModel.filterIsOpen()) { - showHideFilterGeneral(); - } - viewModel.setPreviousScreen(); - return Unit.INSTANCE; - }, - () -> { - hideKeyboard(); - return Unit.INSTANCE; - } - ); - } - - @Override - public void onBackClicked() { - onBackPressed(); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putSerializable(Constants.QUERY_DATA, (Serializable) viewModel.getQueryData()); - outState.putInt(INITIAL_PAGE, binding.navigationBar.currentPage()); - outState.putString(CURRENT_SCREEN, currentContent.name()); - } - - private void openSyncDialog() { - View contextView = findViewById(R.id.navigationBar); - new SyncStatusDialog.Builder() - .withContext(this, null) - .withSyncContext( - new SyncContext.TrackerProgram(initialProgram) - ) - .onDismissListener(hasChanged -> { - if (hasChanged) viewModel.refreshData(); - }) - .onNoConnectionListener(() -> - Snackbar.make( - contextView, - R.string.sync_offline_check_connection, - Snackbar.LENGTH_SHORT - ).show() - ) - .show("PROGRAM_SYNC"); - } - - @Override - public void updateFilters(int totalFilters) { - binding.setTotalFilters(totalFilters); - binding.executePendingBindings(); - viewModel.updateActiveFilters(totalFilters > 0); - viewModel.refreshData(); - } - - private void initSearchParameters() { - initSearchScreen( - binding.searchContainer, - viewModel, - initialProgram, - tEType, - resourceManager, - (uid, preselectedOrgUnits, orgUnitScope, label) -> { - new OUTreeFragment.Builder() - .showAsDialog() - .withPreselectedOrgUnits(preselectedOrgUnits) - .singleSelection() - .onSelection(selectedOrgUnits -> { - String selectedOrgUnit = null; - if (!selectedOrgUnits.isEmpty()) { - selectedOrgUnit = selectedOrgUnits.get(0).uid(); - } - viewModel.onParameterIntent( - new FormIntent.OnSave( - uid, - selectedOrgUnit, - ValueType.ORGANISATION_UNIT, - null, - true - ) - ); - return Unit.INSTANCE; - }) - .orgUnitScope(orgUnitScope) - .build() - .show(getSupportFragmentManager(), label); - return Unit.INSTANCE; - }, - () -> { - closeKeyboard(binding.root); - presenter.onClearClick(); - return Unit.INSTANCE; - } - ); - } - - private void configureBottomNavigation() { - binding.navigationBar.setOnNavigationItemSelectedListener(item -> { - if (viewModel.searchOrFilterIsOpen()) { - searchScreenConfigurator.closeBackdrop(); - } - binding.mainComponent.setVisibility(View.VISIBLE); - switch (item.getItemId()) { - case R.id.navigation_list_view -> { - viewModel.setListScreen(); - showList(); - showSearchAndFilterButtons(); - } - case R.id.navigation_map_view -> networkUtils.performIfOnline( - this, - () -> { - presenter.trackSearchMapVisualization(); - showMap(); - showSearchAndFilterButtons(); - return null; - }, - () -> { - binding.navigationBar.selectItemAt(0); - return null; - }, - getString(R.string.msg_network_connection_maps) - ); - case R.id.navigation_analytics -> { - presenter.trackSearchAnalytics(); - viewModel.setAnalyticsScreen(); - fromAnalytics = true; - showAnalytics(); - hideSearchAndFilterButtons(); - } - } - return true; - }); - - viewModel.getPageConfiguration().observe(this, pageConfigurator -> { - if (initialPage == 0) { - showList(); - } - binding.navigationBar.setOnConfigurationFinishListener(() -> { - if (viewModel.searchOrFilterIsOpen()) { - binding.navigationBar.hide(); - } else { - binding.navigationBar.show(); - } - return Unit.INSTANCE; - }); - binding.navigationBar.pageConfiguration(pageConfigurator); - }); - } - - private void showList() { - if (currentContent != Content.LIST) { - currentContent = Content.LIST; - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.mainComponent, SearchTEList.Companion.get(fromRelationship)).commit(); - } - viewModel.getRefreshData().observe(this, refresh -> { - closeKeyboard(binding.root); - }); - } - - private void showMap() { - if (currentContent != Content.MAP) { - currentContent = Content.MAP; - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.mainComponent, SearchTEMap.Companion.get(fromRelationship, tEType)).commit(); - observeMapLoading(); - } - } - - private void showAnalytics() { - if (currentContent != Content.ANALYTICS) { - currentContent = Content.ANALYTICS; - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.replace(R.id.mainComponent, GroupAnalyticsFragment.Companion.forProgram(initialProgram)).commit(); - } - } - - private void hideSearchAndFilterButtons() { - binding.searchFilterGeneral.setVisibility(GONE); - binding.filterCounter.setVisibility(GONE); - } - - private void showSearchAndFilterButtons() { - if (fromAnalytics) { - fromAnalytics = false; - binding.searchFilterGeneral.setVisibility(View.VISIBLE); - binding.filterCounter.setVisibility(binding.getTotalFilters() > 0 ? View.VISIBLE : View.GONE); - } - } - - private void observeScreenState() { - viewModel.getScreenState().observe(this, screenState -> - searchScreenConfigurator.configure(screenState)); - } - - private void observeDownload() { - viewModel.getDownloadResult().observe(this, result -> - result.handleResult( - (teiUid, programUid, enrollmentUid) -> { - openDashboard(teiUid, - programUid, - enrollmentUid); - return Unit.INSTANCE; - }, - (teiUid, enrollmentUid) -> { - showBreakTheGlass(teiUid, enrollmentUid); - return Unit.INSTANCE; - }, - teiUid -> { - couldNotDownload(presenter.getTrackedEntityName().displayName()); - return Unit.INSTANCE; - }, - errorMessage -> { - displayMessage(errorMessage); - return Unit.INSTANCE; - } - )); - } - - private void observeLegacyInteractions() { - - viewModel.getLegacyInteraction().observe(this, legacyInteraction -> { - if (legacyInteraction != null) { - switch (legacyInteraction.getId()) { - case ON_ENROLL_CLICK -> { - LegacyInteraction.OnEnrollClick interaction = (LegacyInteraction.OnEnrollClick) legacyInteraction; - presenter.onEnrollClick(new HashMap<>(interaction.getQueryData())); - - } - case ON_ADD_RELATIONSHIP -> { - LegacyInteraction.OnAddRelationship interaction = (LegacyInteraction.OnAddRelationship) legacyInteraction; - presenter.addRelationship(interaction.getTeiUid(), interaction.getRelationshipTypeUid(), interaction.getOnline()); - } - case ON_SYNC_CLICK -> { - LegacyInteraction.OnSyncIconClick interaction = (LegacyInteraction.OnSyncIconClick) legacyInteraction; - presenter.onSyncIconClick(interaction.getTeiUid()); - } - case ON_ENROLL -> { - LegacyInteraction.OnEnroll interaction = (LegacyInteraction.OnEnroll) legacyInteraction; - presenter.enroll( - interaction.getInitialProgramUid(), - interaction.getTeiUid(), - new HashMap<>(interaction.getQueryData()) - ); - } - case ON_TEI_CLICK -> { - LegacyInteraction.OnTeiClick interaction = (LegacyInteraction.OnTeiClick) legacyInteraction; - presenter.onTEIClick( - interaction.getTeiUid(), - interaction.getEnrollmentUid(), - interaction.getOnline() - ); - } - } - - viewModel.onLegacyInteractionConsumed(); - } - }); - } - - private void observeMapLoading() { - viewModel.getRefreshData().observe(this, refresh -> { - if (currentContent == Content.MAP) { - binding.toolbarProgress.show(); - } - }); - viewModel.getMapResults().observe(this, result -> binding.toolbarProgress.hide()); - } - - @Override - public void clearList(String uid) { - this.initialProgram = uid; - if (uid == null) - binding.programSpinner.setSelection(0); - } - - @Override - public void setPrograms(List programs) { - binding.programSpinner.setAdapter(new ProgramAdapter(this, - R.layout.spinner_program_layout, - R.id.spinner_text, - programs, - presenter.getTrackedEntityName().displayName())); - if (initialProgram != null && !initialProgram.isEmpty()) - setInitialProgram(programs); - else - binding.programSpinner.setSelection(0); - - ViewExtensionsKt.overrideHeight(binding.programSpinner, 500); - ViewExtensionsKt.doOnItemSelected(binding.programSpinner, selectedIndex -> { - viewModel.onProgramSelected(selectedIndex, programs, selectedProgram -> { - changeProgram(selectedProgram); - return Unit.INSTANCE; - }); - return Unit.INSTANCE; - }); - } - - @Override - public void showSyncDialog(String enrollmentUid) { - View contextView = findViewById(R.id.navigationBar); - new SyncStatusDialog.Builder() - .withContext(this, null) - .withSyncContext( - new SyncContext.TrackerProgramTei(enrollmentUid) - ) - .onDismissListener(hasChanged -> { - if (hasChanged) viewModel.refreshData(); - }) - .onNoConnectionListener(() -> - Snackbar.make( - contextView, - R.string.sync_offline_check_connection, - Snackbar.LENGTH_SHORT - ).show() - ).show("TEI_SYNC"); - } - - private void setInitialProgram(List programs) { - for (int i = 0; i < programs.size(); i++) { - if (programs.get(i).getUid().equals(initialProgram)) { - binding.programSpinner.setSelection(i + 1); - } - } - } - - public void changeProgram(@Nullable String programUid) { - searchNavigator.changeProgram( - programUid, - viewModel.queryDataByProgram(programUid), - fromRelationshipTeiUid - ); - } - - @Override - public String fromRelationshipTEI() { - return fromRelationshipTeiUid; - } - - @Override - public void showHideFilterGeneral() { - viewModel.onFiltersClick(OrientationUtilsKt.isLandscape()); - } - - @Override - public void setInitialFilters(List filtersToDisplay) { - filtersAdapter.submitList(filtersToDisplay); - } - - @Override - public void hideFilter() { - binding.searchFilterGeneral.setVisibility(GONE); - } - - @Override - public void clearFilters() { - if (viewModel.filterIsOpen()) { - filtersAdapter.notifyDataSetChanged(); - FilterManager.getInstance().clearAllFilters(); - } - } - - @Override - public void openOrgUnitTreeSelector() { - new OUTreeFragment.Builder() - .showAsDialog() - .withPreselectedOrgUnits( - FilterManager.getInstance().getOrgUnitUidsFilters() - ) - .onSelection(selectedOrgUnits -> { - presenter.setOrgUnitFilters((List) selectedOrgUnits); - return Unit.INSTANCE; - }) - .build() - .show(getSupportFragmentManager(), "OUTreeFragment"); - } - - @Override - public void showPeriodRequest(Pair periodRequest) { - if (periodRequest.getFirst() == FilterManager.PeriodRequest.FROM_TO) { - DateUtils.getInstance().fromCalendarSelector(this, datePeriod -> { - if (periodRequest.getSecond() == Filters.PERIOD) { - FilterManager.getInstance().addPeriod(datePeriod); - } else { - FilterManager.getInstance().addEnrollmentPeriod(datePeriod); - } - }); - } else { - - DateUtils.OnFromToSelector onFromToSelector = datePeriods -> { - if (periodRequest.getSecond() == Filters.PERIOD) { - FilterManager.getInstance().addPeriod(datePeriods); - } else { - FilterManager.getInstance().addEnrollmentPeriod(datePeriods); - } - }; - - DateUtils.OnNextSelected onNextSelected = () -> { - Disposable disposable = new RxDateDialog(this, Period.WEEKLY) - .createForFilter().show() - .subscribe( - selectedDates -> onFromToSelector.onFromToSelected(DateUtils.getInstance().getDatePeriodListFor( - selectedDates.val1(), - selectedDates.val0()) - ), - Timber::e - ); - }; - - DateUtils.getInstance().showPeriodDialog(this,onFromToSelector, - true, onNextSelected); - } - } - - @Override - public void openDashboard(String teiUid, String programUid, String enrollmentUid) { - searchNavigator.openDashboard(teiUid, programUid, enrollmentUid); - } - - public void refreshData() { - viewModel.refreshData(); - } - - @Override - public void couldNotDownload(String typeName) { - displayMessage(getString(R.string.download_tei_error, typeName)); - } - - @Override - public void showBreakTheGlass(String teiUid, String enrollmentUid) { - new BreakTheGlassBottomDialog() - .setProgram(presenter.getProgram().uid()) - .setPositiveButton(reason -> { - viewModel.onDownloadTei(teiUid, enrollmentUid, reason); - return Unit.INSTANCE; - }) - .show(getSupportFragmentManager(), BreakTheGlassBottomDialog.class.getName()); - } - - @Override - public void goToEnrollment(String enrollmentUid, String programUid) { - searchNavigator.goToEnrollment(enrollmentUid, programUid, fromRelationshipTEI()); - } - - @Override - public Consumer downloadProgress() { - return progress -> Snackbar.make( - binding.getRoot(), - getString(R.string.downloading), - BaseTransientBottomBar.LENGTH_SHORT - ).show(); - } -} diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt new file mode 100644 index 0000000000..0a813a057b --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEActivity.kt @@ -0,0 +1,801 @@ +package org.dhis2.usescases.searchTrackEntity + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.activity.viewModels +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.databinding.DataBindingUtil +import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.Snackbar +import dhis2.org.analytics.charts.ui.GroupAnalyticsFragment.Companion.forProgram +import io.reactivex.functions.Consumer +import org.dhis2.App +import org.dhis2.R +import org.dhis2.bindings.clipWithRoundedCorners +import org.dhis2.bindings.clipWithTopRightRoundedCorner +import org.dhis2.bindings.doOnItemSelected +import org.dhis2.bindings.dp +import org.dhis2.bindings.isKeyboardOpened +import org.dhis2.bindings.overrideHeight +import org.dhis2.commons.Constants +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.date.DateUtils.OnNextSelected +import org.dhis2.commons.date.Period +import org.dhis2.commons.extensions.closeKeyboard +import org.dhis2.commons.featureconfig.data.FeatureConfigRepository +import org.dhis2.commons.filters.FilterItem +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.filters.FilterManager.PeriodRequest +import org.dhis2.commons.filters.Filters +import org.dhis2.commons.filters.FiltersAdapter +import org.dhis2.commons.network.NetworkUtils +import org.dhis2.commons.orgunitselector.OUTreeFragment +import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.sync.OnDismissListener +import org.dhis2.commons.sync.SyncContext +import org.dhis2.commons.sync.SyncContext.TrackerProgramTei +import org.dhis2.data.forms.dataentry.ProgramAdapter +import org.dhis2.databinding.ActivitySearchBinding +import org.dhis2.form.ui.intent.FormIntent.OnSave +import org.dhis2.tracker.NavigationBarUIState +import org.dhis2.ui.ThemeManager +import org.dhis2.usescases.general.ActivityGlobalAbstract +import org.dhis2.usescases.searchTrackEntity.LegacyInteraction.OnAddRelationship +import org.dhis2.usescases.searchTrackEntity.LegacyInteraction.OnEnroll +import org.dhis2.usescases.searchTrackEntity.LegacyInteraction.OnEnrollClick +import org.dhis2.usescases.searchTrackEntity.LegacyInteraction.OnSyncIconClick +import org.dhis2.usescases.searchTrackEntity.LegacyInteraction.OnTeiClick +import org.dhis2.usescases.searchTrackEntity.listView.SearchTEList.Companion.get +import org.dhis2.usescases.searchTrackEntity.mapView.SearchTEMap.Companion.get +import org.dhis2.usescases.searchTrackEntity.searchparameters.initSearchScreen +import org.dhis2.usescases.searchTrackEntity.ui.SearchScreenConfigurator +import org.dhis2.utils.customviews.BreakTheGlassBottomDialog +import org.dhis2.utils.customviews.RxDateDialog +import org.dhis2.utils.customviews.navigationbar.NavigationPage +import org.dhis2.utils.granularsync.SyncStatusDialog +import org.dhis2.utils.granularsync.shouldLaunchSyncDialog +import org.dhis2.utils.isLandscape +import org.dhis2.utils.isPortrait +import org.hisp.dhis.android.core.arch.call.D2Progress +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.organisationunit.OrganisationUnit +import org.hisp.dhis.android.core.period.DatePeriod +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBar +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme +import timber.log.Timber +import java.io.Serializable +import java.util.Date +import javax.inject.Inject + +class SearchTEActivity : ActivityGlobalAbstract(), SearchTEContractsModule.View { + + private lateinit var binding: ActivitySearchBinding + private lateinit var searchScreenConfigurator: SearchScreenConfigurator + + @Inject + lateinit var presenter: SearchTEContractsModule.Presenter + + @Inject + lateinit var filtersAdapter: FiltersAdapter + + @Inject + lateinit var viewModelFactory: SearchTeiViewModelFactory + + @Inject + lateinit var searchNavigator: SearchNavigator + + @Inject + lateinit var networkUtils: NetworkUtils + + @Inject + lateinit var themeManager: ThemeManager + + @Inject + lateinit var featureConfig: FeatureConfigRepository + + @Inject + lateinit var resourceManager: ResourceManager + + private var initialProgram: String? = null + private var initialQuery: Map? = null + + private var fromRelationship = false + private var fromRelationshipTeiUid: String? = null + private var fromAnalytics = false + + private lateinit var tEType: String + + private val viewModel: SearchTEIViewModel by viewModels { viewModelFactory } + + private var initSearchNeeded = true + var searchComponent: SearchTEComponent? = null + + enum class Extra(val key: String) { + TEI_UID("TRACKED_ENTITY_UID"), + PROGRAM_UID("PROGRAM_UID"), + QUERY_ATTR("QUERY_DATA_ATTR"), + QUERY_VALUES("QUERY_DATA_VALUES"), + ; + + fun key(): String { + return key + } + } + + private enum class Content { + LIST, + MAP, + ANALYTICS, + } + + private var currentContent: Content? = null + + @OptIn(ExperimentalAnimationApi::class) + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + initializeVariables(savedInstanceState) + inject() + + if (initialProgram != null) { + themeManager.setProgramTheme(initialProgram!!) + } + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_search) + val currentScreen = savedInstanceState?.getString(CURRENT_SCREEN).orEmpty() + if (currentScreen.isNotBlank()) { + currentContent = Content.valueOf(currentScreen) + } + initSearchParameters() + + searchScreenConfigurator = SearchScreenConfigurator( + binding, + ) { isOpen: Boolean -> + viewModel.setFiltersOpened(isOpen) + } + + binding.setPresenter(presenter) + binding.setTotalFilters(FilterManager.getInstance().totalFilters) + + if (isLandscape()) { + viewModel.filtersOpened.observe(this) { isOpened: Boolean -> + if (java.lang.Boolean.TRUE == isOpened) { + binding.mainComponent.clipWithRoundedCorners(16.dp) + } else { + binding.mainComponent.clipWithTopRightRoundedCorner(16.dp) + } + } + } else { + binding.mainComponent.clipWithRoundedCorners(16.dp) + } + + binding.filterRecyclerLayout.adapter = filtersAdapter + + binding.executePendingBindings() + + binding.syncButton.visibility = if (initialProgram != null) View.VISIBLE else View.GONE + binding.syncButton.setOnClickListener { openSyncDialog() } + + binding.landOpenSearchButton + .setLandscapeOpenSearchButton( + viewModel, + ) { + viewModel.setSearchScreen() + } + + setupBottomNavigation() + observeScreenState() + observeDownload() + observeLegacyInteractions() + + if (intent.shouldLaunchSyncDialog()) { + openSyncDialog() + } + } + + private fun initializeVariables(savedInstanceState: Bundle?) { + tEType = intent.getStringExtra("TRACKED_ENTITY_UID").orEmpty() + initialProgram = intent.getStringExtra("PROGRAM_UID") + try { + fromRelationship = intent.getBooleanExtra("FROM_RELATIONSHIP", false) + fromRelationshipTeiUid = intent.getStringExtra("FROM_RELATIONSHIP_TEI") + } catch (e: Exception) { + Timber.d(e) + } + initialQuery = this.queryDataExtra(savedInstanceState) + } + + private fun inject() { + searchComponent = + (applicationContext as App).userComponent()?.plus( + SearchTEModule( + this, + tEType, + initialProgram, + context, + initialQuery, + ), + ) + searchComponent?.inject(this) + } + + override fun onResume() { + super.onResume() + if (sessionManagerServiceImpl.isUserLoggedIn()) { + FilterManager.getInstance().clearUnsupportedFilters() + if (initSearchNeeded) { + presenter.init() + } else { + initSearchNeeded = true + } + binding.totalFilters = FilterManager.getInstance().totalFilters + } + } + + override fun onPause() { + if (sessionManagerServiceImpl.isUserLoggedIn()) { + presenter.setOpeningFilterToNone() + if (initSearchNeeded) { + presenter.onDestroy() + } + } + super.onPause() + } + + override fun onDestroy() { + if (sessionManagerServiceImpl.isUserLoggedIn()) { + presenter.onDestroy() + FilterManager.getInstance().clearEnrollmentStatus() + FilterManager.getInstance().clearEventStatus() + FilterManager.getInstance().clearEnrollmentDate() + FilterManager.getInstance().clearWorkingList(true) + FilterManager.getInstance().clearSorting() + FilterManager.getInstance().clearAssignToMe() + FilterManager.getInstance().clearFollowUp() + presenter.clearOtherFiltersIfWebAppIsConfig() + } + super.onDestroy() + } + + override fun onBackPressed() { + viewModel.onBackPressed( + isPortrait(), + viewModel.backdropActive.value ?: false, + this.isKeyboardOpened(), + { + super.onBackPressed() + }, + { + if (viewModel.filterIsOpen()) { + showHideFilterGeneral() + } + viewModel.setPreviousScreen() + }, + { + hideKeyboard() + }, + ) + } + + override fun onBackClicked() { + onBackPressed() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putSerializable(Constants.QUERY_DATA, viewModel.queryData as Serializable) + outState.putString(CURRENT_SCREEN, currentContent?.name) + } + + private fun openSyncDialog() { + val contextView = findViewById(R.id.navigationBar) + SyncStatusDialog.Builder() + .withContext(this, null) + .withSyncContext( + SyncContext.TrackerProgram(initialProgram!!), + ) + .onDismissListener(object : OnDismissListener { + override fun onDismiss(hasChanged: Boolean) { + if (hasChanged) viewModel.refreshData() + } + }) + .onNoConnectionListener { + Snackbar.make( + contextView, + R.string.sync_offline_check_connection, + Snackbar.LENGTH_SHORT, + ).show() + } + .show("PROGRAM_SYNC") + } + + override fun updateFilters(totalFilters: Int) { + binding.totalFilters = totalFilters + binding.executePendingBindings() + viewModel.updateActiveFilters(totalFilters > 0) + viewModel.refreshData() + } + + private fun initSearchParameters() { + initSearchScreen( + binding.searchContainer, + viewModel, + initialProgram, + tEType, + resourceManager, + { uid: String, preselectedOrgUnits: List, orgUnitScope: OrgUnitSelectorScope, label: String -> + OUTreeFragment.Builder() + .withPreselectedOrgUnits(preselectedOrgUnits) + .singleSelection() + .onSelection { selectedOrgUnits: List -> + var selectedOrgUnit: String? = null + if (selectedOrgUnits.isNotEmpty()) { + selectedOrgUnit = selectedOrgUnits[0].uid() + } + viewModel.onParameterIntent( + OnSave( + uid, + selectedOrgUnit, + ValueType.ORGANISATION_UNIT, + null, + true, + ), + ) + } + .orgUnitScope(orgUnitScope) + .build() + .show(supportFragmentManager, label) + }, + { + binding.root.closeKeyboard() + presenter.onClearClick() + }, + ) + } + + private fun setupBottomNavigation() { + binding.navigationBar.setContent { + DHIS2Theme { + val uiState by viewModel.navigationBarUIState + val isBackdropActive by viewModel.backdropActive.observeAsState(false) + var selectedItemIndex by remember(uiState) { + mutableIntStateOf( + uiState.items.indexOfFirst { + it.id == uiState.selectedItem + }, + ) + } + + LaunchedEffect(uiState.selectedItem) { + handleBottomNavigation( + uiState = uiState, + onDialogDismissed = { + selectedItemIndex = 0 + }, + ) + } + + AnimatedVisibility( + visible = (isBackdropActive.not() && uiState.items.isNotEmpty()) || isLandscape(), + enter = slideInVertically(animationSpec = tween(200)) { it }, + exit = slideOutVertically(animationSpec = tween(200)) { it }, + ) { + NavigationBar( + modifier = Modifier.fillMaxWidth(), + items = uiState.items, + selectedItemIndex = selectedItemIndex, + ) { page -> + selectedItemIndex = uiState.items.indexOfFirst { it.id == page } + if (sessionManagerServiceImpl.isUserLoggedIn().not()) return@NavigationBar + + if (viewModel.backdropActive.value == true) { + searchScreenConfigurator.closeBackdrop() + } + + viewModel.onNavigationPageChanged(page) + } + } + } + } + } + + private fun handleBottomNavigation( + uiState: NavigationBarUIState, + onDialogDismissed: () -> Unit, + ) { + when (uiState.selectedItem) { + NavigationPage.LIST_VIEW -> { + viewModel.setListScreen() + showList() + showSearchAndFilterButtons() + } + + NavigationPage.MAP_VIEW -> { + networkUtils.performIfOnline( + context = this@SearchTEActivity, + action = { + presenter.trackSearchMapVisualization() + showMap() + showSearchAndFilterButtons() + }, + onDialogDismissed = onDialogDismissed, + noNetworkMessage = getString(R.string.msg_network_connection_maps), + ) + } + + NavigationPage.ANALYTICS -> { + if (sessionManagerServiceImpl.isUserLoggedIn()) { + presenter.trackSearchAnalytics() + viewModel.setAnalyticsScreen() + fromAnalytics = true + showAnalytics() + hideSearchAndFilterButtons() + } + } + + else -> { + // no-op + } + } + } + + private fun showList() { + if (currentContent != Content.LIST) { + currentContent = Content.LIST + supportFragmentManager.beginTransaction().run { + replace(R.id.mainComponent, get(fromRelationship)) + commit() + } + hideToolbarProgressBar() + } + viewModel.refreshData.observe(this) { + binding.root.closeKeyboard() + } + } + + private fun showMap() { + if (currentContent != Content.MAP) { + currentContent = Content.MAP + supportFragmentManager.beginTransaction().run { + replace(R.id.mainComponent, get(fromRelationship, tEType)) + commit() + } + observeMapLoading() + } + } + + private fun showAnalytics() { + if (currentContent != Content.ANALYTICS) { + currentContent = Content.ANALYTICS + supportFragmentManager.beginTransaction().run { + replace( + R.id.mainComponent, + forProgram( + initialProgram!!, + ), + ) + commit() + } + hideToolbarProgressBar() + } + } + + private fun hideToolbarProgressBar() { + if (binding.toolbarProgress.isShown) { + binding.toolbarProgress.hide() + } + } + + private fun hideSearchAndFilterButtons() { + binding.searchFilterGeneral.visibility = View.GONE + binding.filterCounter.visibility = View.GONE + } + + private fun showSearchAndFilterButtons() { + if (fromAnalytics) { + fromAnalytics = false + binding.searchFilterGeneral.visibility = View.VISIBLE + binding.filterCounter.visibility = + if ((binding.totalFilters ?: 0) > 0) View.VISIBLE else View.GONE + } + } + + private fun observeScreenState() { + viewModel.screenState.observe(this, searchScreenConfigurator::configure) + viewModel.screenState.observe(this, viewModel::updateBackdrop) + } + + private fun observeDownload() { + viewModel.downloadResult.observe(this) { result: TeiDownloadResult -> + result.handleResult( + { teiUid: String, programUid: String?, enrollmentUid: String? -> + openDashboard( + teiUid, + programUid, + enrollmentUid, + ) + }, + { teiUid: String, enrollmentUid: String? -> + showBreakTheGlass(teiUid, enrollmentUid) + }, + { + couldNotDownload(presenter.trackedEntityName.displayName()) + }, + { errorMessage: String? -> + displayMessage(errorMessage) + }, + ) + } + } + + private fun observeLegacyInteractions() { + viewModel.legacyInteraction.observe(this) { legacyInteraction -> + if (legacyInteraction != null) { + when (legacyInteraction.id) { + LegacyInteractionID.ON_ENROLL_CLICK -> { + val interaction = legacyInteraction as OnEnrollClick + presenter.onEnrollClick(HashMap(interaction.queryData)) + } + + LegacyInteractionID.ON_ADD_RELATIONSHIP -> { + val interaction = legacyInteraction as OnAddRelationship + presenter.addRelationship( + interaction.teiUid, + interaction.relationshipTypeUid, + interaction.online, + ) + } + + LegacyInteractionID.ON_SYNC_CLICK -> { + val interaction = legacyInteraction as OnSyncIconClick + presenter.onSyncIconClick(interaction.teiUid) + } + + LegacyInteractionID.ON_ENROLL -> { + val interaction = legacyInteraction as OnEnroll + presenter.enroll( + interaction.initialProgramUid, + interaction.teiUid, + HashMap(interaction.queryData), + ) + } + + LegacyInteractionID.ON_TEI_CLICK -> { + val interaction = legacyInteraction as OnTeiClick + presenter.onTEIClick( + interaction.teiUid, + interaction.enrollmentUid, + interaction.online, + ) + } + } + viewModel.onLegacyInteractionConsumed() + } + } + } + + private fun observeMapLoading() { + viewModel.refreshData.observe(this) { + if (currentContent == Content.MAP) { + binding.toolbarProgress.show() + } + } + } + + override fun clearList(uid: String?) { + this.initialProgram = uid + if (uid == null) binding.programSpinner.setSelection(0) + } + + override fun setPrograms(programs: List) { + binding.programSpinner.adapter = ProgramAdapter( + this, + R.layout.spinner_program_layout, + R.id.spinner_text, + programs, + presenter.trackedEntityName.displayName(), + ) + if (initialProgram != null && initialProgram!!.isNotEmpty()) { + setInitialProgram(programs) + } else { + binding.programSpinner.setSelection(0) + } + + binding.programSpinner.overrideHeight(500) + binding.programSpinner.doOnItemSelected { selectedIndex: Int -> + viewModel.onProgramSelected(selectedIndex, programs) { selectedProgram: String? -> + changeProgram(selectedProgram) + } + } + } + + override fun showSyncDialog(enrollmentUid: String) { + val contextView = findViewById(R.id.navigationBar) + SyncStatusDialog.Builder() + .withContext(this, null) + .withSyncContext( + TrackerProgramTei(enrollmentUid), + ) + .onDismissListener(object : OnDismissListener { + override fun onDismiss(hasChanged: Boolean) { + if (hasChanged) viewModel.refreshData() + } + }) + .onNoConnectionListener { + Snackbar.make( + contextView, + R.string.sync_offline_check_connection, + Snackbar.LENGTH_SHORT, + ).show() + }.show("TEI_SYNC") + } + + private fun setInitialProgram(programs: List) { + for (i in programs.indices) { + if (programs[i].uid == initialProgram) { + binding.programSpinner.setSelection(i + 1) + } + } + } + + private fun changeProgram(programUid: String?) { + searchNavigator.changeProgram( + programUid, + viewModel.queryDataByProgram(programUid), + fromRelationshipTeiUid, + ) + } + + override fun fromRelationshipTEI(): String? { + return fromRelationshipTeiUid + } + + override fun showHideFilterGeneral() { + viewModel.onFiltersClick(isLandscape()) + } + + override fun setInitialFilters(filtersToDisplay: List) { + filtersAdapter.submitList(filtersToDisplay) + } + + override fun hideFilter() { + binding.searchFilterGeneral.visibility = View.GONE + } + + @SuppressLint("NotifyDataSetChanged") + override fun clearFilters() { + if (viewModel.filterIsOpen()) { + filtersAdapter.notifyDataSetChanged() + FilterManager.getInstance().clearAllFilters() + } + } + + override fun openOrgUnitTreeSelector() { + OUTreeFragment.Builder() + .withPreselectedOrgUnits( + FilterManager.getInstance().orgUnitUidsFilters, + ) + .onSelection { selectedOrgUnits: List? -> + presenter.setOrgUnitFilters( + selectedOrgUnits, + ) + } + .build() + .show(supportFragmentManager, "OUTreeFragment") + } + + override fun showPeriodRequest(periodRequest: Pair) { + if (periodRequest.first == PeriodRequest.FROM_TO) { + DateUtils.getInstance().fromCalendarSelector(this) { datePeriod: List? -> + if (periodRequest.second == Filters.PERIOD) { + FilterManager.getInstance().addPeriod(datePeriod) + } else { + FilterManager.getInstance().addEnrollmentPeriod(datePeriod) + } + } + } else { + val onFromToSelector = DateUtils.OnFromToSelector { datePeriods: List? -> + if (periodRequest.second == Filters.PERIOD) { + FilterManager.getInstance().addPeriod(datePeriods) + } else { + FilterManager.getInstance().addEnrollmentPeriod(datePeriods) + } + } + + val onNextSelected = OnNextSelected { + RxDateDialog(this, Period.WEEKLY) + .createForFilter().show() + .subscribe( + { selectedDates: org.dhis2.commons.data.tuples.Pair?> -> + onFromToSelector.onFromToSelected( + DateUtils.getInstance().getDatePeriodListFor( + selectedDates.val1(), + selectedDates.val0(), + ), + ) + }, + { t: Throwable? -> Timber.e(t) }, + ) + } + + DateUtils.getInstance().showPeriodDialog( + this, + onFromToSelector, + true, + onNextSelected, + ) + } + } + + override fun openDashboard(teiUid: String, programUid: String?, enrollmentUid: String?) { + searchNavigator.openDashboard(teiUid, programUid, enrollmentUid) + } + + fun refreshData() { + viewModel.refreshData() + } + + override fun couldNotDownload(typeName: String?) { + displayMessage(getString(R.string.download_tei_error, typeName)) + } + + override fun showBreakTheGlass(teiUid: String, enrollmentUid: String?) { + BreakTheGlassBottomDialog() + .setProgram(presenter.program.uid()) + .setPositiveButton { reason: String? -> + viewModel.onDownloadTei(teiUid, enrollmentUid, reason) + } + .show(supportFragmentManager, BreakTheGlassBottomDialog::class.java.name) + } + + override fun goToEnrollment(enrollmentUid: String, programUid: String) { + searchNavigator.goToEnrollment(enrollmentUid, programUid, fromRelationshipTEI()) + } + + override fun downloadProgress(): Consumer { + return Consumer { + Snackbar.make( + binding.getRoot(), + getString(R.string.downloading), + BaseTransientBottomBar.LENGTH_SHORT, + ).show() + } + } + + companion object { + private const val CURRENT_SCREEN = "current_screen" + + fun getIntent( + context: Context?, + programUid: String?, + teiTypeToAdd: String?, + teiUid: String?, + fromRelationship: Boolean, + ): Intent { + val intent = Intent(context, SearchTEActivity::class.java) + val extras = Bundle() + extras.putBoolean("FROM_RELATIONSHIP", fromRelationship) + extras.putString("FROM_RELATIONSHIP_TEI", teiUid) + extras.putString(Extra.TEI_UID.key, teiTypeToAdd) + extras.putString(Extra.PROGRAM_UID.key, programUid) + intent.putExtras(extras) + return intent + } + } +} diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt index 795d132bd5..f9ac63a0ec 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEIViewModel.kt @@ -1,5 +1,13 @@ package org.dhis2.usescases.searchTrackEntity +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.automirrored.outlined.List +import androidx.compose.material.icons.filled.BarChart +import androidx.compose.material.icons.filled.Map +import androidx.compose.material.icons.outlined.BarChart +import androidx.compose.material.icons.outlined.Map +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -10,14 +18,17 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map +import com.mapbox.geojson.Feature import kotlinx.coroutines.Job import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.dhis2.R -import org.dhis2.commons.data.SearchTeiModel import org.dhis2.commons.extensions.toFriendlyDate import org.dhis2.commons.extensions.toFriendlyDateTime import org.dhis2.commons.extensions.toPercentage @@ -30,21 +41,26 @@ import org.dhis2.data.search.SearchParametersModel import org.dhis2.form.model.FieldUiModelImpl import org.dhis2.form.ui.intent.FormIntent import org.dhis2.form.ui.provider.DisplayNameProvider +import org.dhis2.maps.extensions.toStringProperty +import org.dhis2.maps.layer.MapLayer import org.dhis2.maps.layer.basemaps.BaseMapStyle import org.dhis2.maps.usecases.MapStyleConfiguration +import org.dhis2.tracker.NavigationBarUIState import org.dhis2.usescases.searchTrackEntity.listView.SearchResult import org.dhis2.usescases.searchTrackEntity.searchparameters.model.SearchParametersUiState import org.dhis2.usescases.searchTrackEntity.ui.UnableToSearchOutsideData +import org.dhis2.utils.customviews.navigationbar.NavigationPage import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.hisp.dhis.android.core.arch.helpers.Result import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.maintenance.D2ErrorCode +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBarItem import timber.log.Timber const val TEI_TYPE_SEARCH_MAX_RESULTS = 5 class SearchTEIViewModel( - private val initialProgramUid: String?, + val initialProgramUid: String?, initialQuery: MutableMap?, private val searchRepository: SearchRepository, private val searchRepositoryKt: SearchRepositoryKt, @@ -58,8 +74,15 @@ class SearchTEIViewModel( private val filterManager: FilterManager, ) : ViewModel() { + private var layersVisibility: Map = emptyMap() + private val _pageConfiguration = MutableLiveData() - val pageConfiguration: LiveData = _pageConfiguration + + private val _navigationBarUIState = mutableStateOf( + NavigationBarUIState(), + ) + val navigationBarUIState: MutableState> = + _navigationBarUIState val queryData = mutableMapOf().apply { initialQuery?.let { putAll(it) } @@ -71,8 +94,11 @@ class SearchTEIViewModel( private val _refreshData = MutableLiveData(Unit) val refreshData: LiveData = _refreshData - private val _mapResults = MutableLiveData() - val mapResults: LiveData = _mapResults + private val _mapResults = Channel() + val mapResults: Flow = _mapResults.receiveAsFlow() + + private val _mapItemClicked = MutableSharedFlow() + val mapItemClicked: Flow = _mapItemClicked private val _screenState = MutableLiveData() val screenState: LiveData = _screenState @@ -92,6 +118,9 @@ class SearchTEIViewModel( private val _filtersOpened = MutableLiveData(false) val filtersOpened: LiveData = _filtersOpened + private val _backdropActive = MutableLiveData() + val backdropActive: LiveData get() = _backdropActive + private val _teTypeName = MutableLiveData("") val teTypeName: LiveData = _teTypeName @@ -104,7 +133,7 @@ class SearchTEIViewModel( createButtonScrollVisibility.postValue( searchRepository.canCreateInProgramWithoutSearch(), ) - _pageConfiguration.postValue(searchNavPageConfigurator.initVariables()) + loadNavigationBarItems() _teTypeName.postValue( searchRepository.trackedEntityType.displayName(), @@ -112,6 +141,59 @@ class SearchTEIViewModel( } } + private fun loadNavigationBarItems() { + val pageConfigurator = searchNavPageConfigurator.initVariables() + _pageConfiguration.postValue(pageConfigurator) + + val enrollmentItems = mutableListOf>() + + if (pageConfigurator.displayListView()) { + enrollmentItems.add( + NavigationBarItem( + id = NavigationPage.LIST_VIEW, + icon = Icons.AutoMirrored.Outlined.List, + selectedIcon = Icons.AutoMirrored.Filled.List, + label = resourceManager.getString(R.string.navigation_list_view), + ), + ) + } + + if (pageConfigurator.displayMapView()) { + enrollmentItems.add( + NavigationBarItem( + id = NavigationPage.MAP_VIEW, + icon = Icons.Outlined.Map, + selectedIcon = Icons.Filled.Map, + label = resourceManager.getString(R.string.navigation_map_view), + ), + ) + } + + if (pageConfigurator.displayAnalytics()) { + enrollmentItems.add( + NavigationBarItem( + id = NavigationPage.ANALYTICS, + icon = Icons.Outlined.BarChart, + selectedIcon = Icons.Filled.BarChart, + label = resourceManager.getString(R.string.navigation_charts), + ), + ) + } + + _navigationBarUIState.value = _navigationBarUIState.value.copy( + items = enrollmentItems.takeIf { it.size > 1 }.orEmpty(), + selectedItem = enrollmentItems.firstOrNull()?.id, + ) + + if (enrollmentItems.isNotEmpty()) { + onNavigationPageChanged(enrollmentItems.first().id) + } + } + + fun onNavigationPageChanged(page: NavigationPage) { + _navigationBarUIState.value = _navigationBarUIState.value.copy(selectedItem = page) + } + fun setListScreen() { _screenState.value.takeIf { it?.screenState == SearchScreenState.MAP }?.let { searching = (it as SearchList).isSearching @@ -303,6 +385,7 @@ class SearchTEIViewModel( } fun fetchListResults(onPagedListReady: (Flow>?) -> Unit) { + SearchIdlingResourceSingleton.increment() viewModelScope.launch(dispatchers.io()) { val resultPagedList = async { when { @@ -311,7 +394,13 @@ class SearchTEIViewModel( else -> null } } - onPagedListReady(resultPagedList.await()) + try { + onPagedListReady(resultPagedList.await()) + } catch (e: Exception) { + Timber.e(e) + } finally { + SearchIdlingResourceSingleton.decrement() + } } } @@ -416,17 +505,23 @@ class SearchTEIViewModel( } fun fetchMapResults() { + SearchIdlingResourceSingleton.increment() viewModelScope.launch { val result = async(context = dispatchers.io()) { mapDataRepository.getTrackerMapData( searchRepository.getProgram(initialProgramUid), queryData, + layersVisibility, ) } + try { - _mapResults.postValue(result.await()) + val data = result.await() + _mapResults.send(data) } catch (e: Exception) { Timber.e(e) + } finally { + SearchIdlingResourceSingleton.decrement() } searching = false } @@ -439,45 +534,52 @@ class SearchTEIViewModel( private fun performSearch() { viewModelScope.launch(dispatchers.io()) { - if (canPerformSearch()) { - searching = queryData.isNotEmpty() - uiState = uiState.copy( - clearSearchEnabled = queryData.isNotEmpty(), - searchedItems = getFriendlyQueryData(), - ) - when (_screenState.value?.screenState) { - SearchScreenState.LIST -> { - SearchIdlingResourceSingleton.increment() - setListScreen() - fetchListResults { flow -> - flow?.let { - _refreshData.postValue(Unit) - SearchIdlingResourceSingleton.decrement() + try { + if (canPerformSearch()) { + searching = queryData.isNotEmpty() + uiState = uiState.copy( + clearSearchEnabled = queryData.isNotEmpty(), + searchedItems = getFriendlyQueryData(), + ) + + when (_screenState.value?.screenState) { + SearchScreenState.LIST -> { + setListScreen() + fetchListResults { flow -> + flow?.let { + fetchListResults { flow -> + flow?.let { + _refreshData.postValue(Unit) + SearchIdlingResourceSingleton.decrement() + } + } + } } } - } - SearchScreenState.MAP -> { - SearchIdlingResourceSingleton.increment() - _refreshData.postValue(Unit) - setMapScreen() - fetchMapResults() - } + SearchScreenState.MAP -> { + _refreshData.postValue(Unit) + setMapScreen() + fetchMapResults() + } - else -> searching = false + else -> searching = false + } + } else { + val minAttributesToSearch = searchRepository.getProgram(initialProgramUid) + ?.minAttributesRequiredToSearch() + ?: 0 + val message = resourceManager.getString( + R.string.search_min_num_attr, + minAttributesToSearch, + ) + uiState = uiState.copy(minAttributesMessage = message) + uiState.updateMinAttributeWarning(true) + setSearchScreen() + _refreshData.postValue(Unit) } - } else { - val minAttributesToSearch = searchRepository.getProgram(initialProgramUid) - ?.minAttributesRequiredToSearch() - ?: 0 - val message = resourceManager.getString( - R.string.search_min_num_attr, - minAttributesToSearch, - ) - uiState = uiState.copy(minAttributesMessage = message) - uiState.updateMinAttributeWarning(true) - setSearchScreen() - _refreshData.postValue(Unit) + } catch (e: Exception) { + Timber.d(e.message) } } } @@ -589,8 +691,6 @@ class SearchTEIViewModel( } else { handleInitWithoutData() } - - SearchIdlingResourceSingleton.decrement() } private fun handleDisplayInListResult(hasProgramResults: Boolean) { @@ -630,7 +730,6 @@ class SearchTEIViewModel( SearchResult( SearchResult.SearchResultType.SEARCH_OUTSIDE, searchRepository.getProgram(initialProgramUid)?.displayName(), - ), ) } @@ -717,10 +816,6 @@ class SearchTEIViewModel( } ?: false } - fun mapDataFetched() { - SearchIdlingResourceSingleton.decrement() - } - fun onProgramSelected( programIndex: Int, programs: List, @@ -772,11 +867,13 @@ class SearchTEIViewModel( } } - fun searchOrFilterIsOpen(): Boolean { - return _screenState.value?.takeIf { it is SearchList }?.let { - val currentScreen = it as SearchList - currentScreen.searchForm.isOpened || currentScreen.searchFilters.isOpened - } ?: false + fun updateBackdrop(screenState: SearchTEScreenState) { + _backdropActive.postValue( + screenState.takeIf { it is SearchList }?.let { + val currentScreen = it as SearchList + currentScreen.searchForm.isOpened || currentScreen.searchFilters.isOpened + } ?: false, + ) } fun filterIsOpen(): Boolean { @@ -975,4 +1072,17 @@ class SearchTEIViewModel( } return map } + + fun onFeatureClicked(feature: Feature) { + feature.toStringProperty()?.let { + viewModelScope.launch { + _mapItemClicked.emit(it) + } + } + } + + fun filterVisibleMapItems(layersVisibility: Map) { + this.layersVisibility = layersVisibility + fetchMapResults() + } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java index 26e9b0be02..1dec62ca98 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEModule.java @@ -5,7 +5,8 @@ import androidx.annotation.NonNull; import org.dhis2.R; -import org.dhis2.animations.CarouselViewAnimations; +import org.dhis2.commons.data.ProgramConfigurationRepository; +import org.dhis2.commons.date.DateLabelProvider; import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.di.dagger.PerActivity; import org.dhis2.commons.filters.DisableHomeFiltersFromSettingsApp; @@ -37,7 +38,6 @@ import org.dhis2.form.data.metadata.OrgUnitConfiguration; import org.dhis2.form.ui.FieldViewModelFactory; import org.dhis2.form.ui.FieldViewModelFactoryImpl; -import org.dhis2.form.ui.LayoutProviderImpl; import org.dhis2.form.ui.provider.AutoCompleteProviderImpl; import org.dhis2.form.ui.provider.DisplayNameProvider; import org.dhis2.form.ui.provider.DisplayNameProviderImpl; @@ -45,9 +45,6 @@ import org.dhis2.form.ui.provider.KeyboardActionProviderImpl; import org.dhis2.form.ui.provider.LegendValueProviderImpl; import org.dhis2.form.ui.provider.UiEventTypesProviderImpl; -import org.dhis2.form.ui.provider.UiStyleProviderImpl; -import org.dhis2.form.ui.style.FormUiModelColorFactoryImpl; -import org.dhis2.form.ui.style.LongTextUiColorFactoryImpl; import org.dhis2.maps.geometry.bound.BoundsGeometry; import org.dhis2.maps.geometry.bound.GetBoundingBox; import org.dhis2.maps.geometry.line.MapLineRelationshipToFeature; @@ -62,12 +59,13 @@ import org.dhis2.maps.geometry.point.MapPointToFeature; import org.dhis2.maps.geometry.polygon.MapPolygonPointToFeature; import org.dhis2.maps.geometry.polygon.MapPolygonToFeature; -import org.dhis2.maps.mapper.EventToEventUiComponent; -import org.dhis2.maps.mapper.MapRelationshipToRelationshipMapModel; import org.dhis2.maps.usecases.MapStyleConfiguration; import org.dhis2.maps.utils.DhisMapUtils; +import org.dhis2.tracker.data.ProfilePictureProvider; import org.dhis2.ui.ThemeManager; +import org.dhis2.usescases.events.EventInfoProvider; import org.dhis2.usescases.searchTrackEntity.ui.mapper.TEICardMapper; +import org.dhis2.usescases.tracker.TrackedEntityInstanceInfoProvider; import org.dhis2.utils.analytics.AnalyticsHelper; import org.hisp.dhis.android.core.D2; @@ -127,7 +125,6 @@ filterRepository, new DisableHomeFiltersFromSettingsApp(), MapTeisToFeatureCollection provideMapTeisToFeatureCollection() { return new MapTeisToFeatureCollection(new BoundsGeometry(), new MapPointToFeature(), new MapPolygonToFeature(), new MapPolygonPointToFeature(), - new MapRelationshipToRelationshipMapModel(), new MapRelationshipsToFeatureCollection( new MapLineRelationshipToFeature(), new MapPointToFeature(), @@ -180,14 +177,32 @@ SearchRepositoryKt searchRepositoryKt( D2 d2, DispatcherProvider dispatcherProvider, FieldViewModelFactory fieldViewModelFactory, - MetadataIconProvider metadataIconProvider + MetadataIconProvider metadataIconProvider, + ColorUtils colorUtils ) { + ResourceManager resourceManager = new ResourceManager(moduleContext, colorUtils); + DateLabelProvider dateLabelProvider = new DateLabelProvider(moduleContext, new ResourceManager(moduleContext, colorUtils)); + ProfilePictureProvider profilePictureProvider = new ProfilePictureProvider(d2); + return new SearchRepositoryImplKt( searchRepository, d2, dispatcherProvider, fieldViewModelFactory, - metadataIconProvider + metadataIconProvider, + new TrackedEntityInstanceInfoProvider( + d2, + profilePictureProvider, + dateLabelProvider, + metadataIconProvider + ), + new EventInfoProvider( + d2, + resourceManager, + dateLabelProvider, + metadataIconProvider, + profilePictureProvider + ) ); } @@ -203,16 +218,9 @@ FieldViewModelFactory fieldViewModelFactory( Context context, D2 d2, ResourceManager resourceManager, - ColorUtils colorUtils, DhisPeriodUtils periodUtils ) { return new FieldViewModelFactoryImpl( - new UiStyleProviderImpl( - new FormUiModelColorFactoryImpl(moduleContext, colorUtils), - new LongTextUiColorFactoryImpl(moduleContext, colorUtils), - false - ), - new LayoutProviderImpl(), new HintProviderImpl(context), new DisplayNameProviderImpl( new OptionSetConfiguration(d2), @@ -280,12 +288,6 @@ SearchSortingValueSetter searchSortingValueSetter( enrollmentUiDataHelper); } - @Provides - @PerActivity - CarouselViewAnimations animations() { - return new CarouselViewAnimations(); - } - @Provides @PerActivity FiltersAdapter provideNewFiltersAdapter() { @@ -302,7 +304,8 @@ SearchTeiViewModelFactory providesViewModelFactory( D2 d2, ResourceManager resourceManager, DisplayNameProvider displayNameProvider, - FilterManager filterManager + FilterManager filterManager, + ProgramConfigurationRepository programConfigurationRepository ) { return new SearchTeiViewModelFactory( searchRepository, @@ -313,13 +316,21 @@ SearchTeiViewModelFactory providesViewModelFactory( mapDataRepository, networkUtils, new SearchDispatchers(), - new MapStyleConfiguration(d2), + new MapStyleConfiguration(d2, initialProgram, programConfigurationRepository), resourceManager, displayNameProvider, filterManager ); } + @Provides + @PerActivity + ProgramConfigurationRepository provideProgramConfigurationRepository( + D2 d2 + ) { + return new ProgramConfigurationRepository(d2); + } + @Provides @PerActivity DisplayNameProvider provideDisplayNameProvider( @@ -337,18 +348,17 @@ DisplayNameProvider provideDisplayNameProvider( @Provides @PerActivity MapDataRepository mapDataRepository( - SearchRepository searchRepository, + SearchRepositoryKt searchRepositoryKt, MapTeisToFeatureCollection mapTeisToFeatureCollection, MapTeiEventsToFeatureCollection mapTeiEventsToFeatureCollection, MapCoordinateFieldToFeatureCollection mapCoordinateFieldToFeatureCollection, DhisMapUtils mapUtils ) { return new MapDataRepository( - searchRepository, + searchRepositoryKt, mapTeisToFeatureCollection, mapTeiEventsToFeatureCollection, mapCoordinateFieldToFeatureCollection, - new EventToEventUiComponent(), mapUtils); } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEPresenter.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEPresenter.java index 2fe724992b..039ae6ebee 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEPresenter.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTEPresenter.java @@ -31,6 +31,7 @@ import org.dhis2.commons.prefs.Preference; import org.dhis2.commons.prefs.PreferenceProvider; import org.dhis2.commons.resources.ColorUtils; +import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.ObjectStyleUtils; import org.dhis2.commons.resources.ResourceManager; import org.dhis2.commons.schedulers.SchedulerProvider; @@ -303,7 +304,6 @@ public void enroll(String programUid, String uid, HashMap queryD allOrgUnits -> { if (allOrgUnits.size() > 1) { new OUTreeFragment.Builder() - .showAsDialog() .singleSelection() .onSelection(selectedOrgUnits -> { if (!selectedOrgUnits.isEmpty()) diff --git a/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiModel.java similarity index 92% rename from commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java rename to app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiModel.java index ba8d33d93c..91280b02b2 100644 --- a/commons/src/main/java/org/dhis2/commons/data/SearchTeiModel.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchTeiModel.java @@ -1,6 +1,8 @@ -package org.dhis2.commons.data; +package org.dhis2.usescases.searchTrackEntity; +import org.dhis2.commons.data.CarouselItemModel; import org.dhis2.commons.data.tuples.Trio; +import org.dhis2.tracker.relationships.model.RelationshipModel; import org.dhis2.ui.MetadataIconData; import org.hisp.dhis.android.core.enrollment.Enrollment; import org.hisp.dhis.android.core.maintenance.D2ErrorCode; @@ -37,7 +39,7 @@ public class SearchTeiModel implements CarouselItemModel { private Enrollment selectedEnrollment; private List enrollments; private Date overdueDate; - private List relationships; + private List relationships; private boolean openedAttributeList = false; private String sortingKey; private String sortingValue; @@ -197,6 +199,15 @@ public MetadataIconData getMetadataIconData(@Nullable String programUid) { } } + public Boolean isMetadataIconDataAvailable(@Nullable String programUid) { + MetadataIconData iconData = metadataIconDataMap.get(programUid); + if (iconData != null) { + return !iconData.getIconRes().isEmpty(); + } else { + return false; + } + } + public void setOverdueDate(Date dateToShow) { this.overdueDate = dateToShow; } @@ -205,11 +216,11 @@ public Date getOverdueDate() { return overdueDate; } - public List getRelationships() { + public List getRelationships() { return relationships; } - public void setRelationships(List relationships) { + public void setRelationships(List relationships) { this.relationships = relationships; } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/TrackerMapData.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/TrackerMapData.kt index d87fb1ffa2..3010517bd0 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/TrackerMapData.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/TrackerMapData.kt @@ -2,26 +2,13 @@ package org.dhis2.usescases.searchTrackEntity import com.mapbox.geojson.BoundingBox import com.mapbox.geojson.FeatureCollection -import org.dhis2.commons.data.CarouselItemModel -import org.dhis2.commons.data.SearchTeiModel -import org.dhis2.maps.mapper.MapRelationshipToRelationshipMapModel +import org.dhis2.maps.model.MapItemModel import java.util.HashMap data class TrackerMapData( - val teiModels: MutableList, val eventFeatures: org.dhis2.maps.geometry.mapper.EventsByProgramStage, + val mapItems: List, val teiFeatures: HashMap, val teiBoundingBox: BoundingBox, - val eventModels: MutableList, val dataElementFeaturess: MutableMap, -) { - fun allItems() = mutableListOf().apply { - addAll(teiModels) - addAll(eventModels) - teiModels.forEach { - addAll( - MapRelationshipToRelationshipMapModel().mapList(it.relationships), - ) - } - } -} +) diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/BaseTeiViewHolder.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/BaseTeiViewHolder.kt index 352a764d5c..0a40bf9f65 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/BaseTeiViewHolder.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/BaseTeiViewHolder.kt @@ -8,12 +8,11 @@ import org.dhis2.bindings.hasFollowUp import org.dhis2.bindings.paintAllEnrollmentIcons import org.dhis2.bindings.setAttributeList import org.dhis2.bindings.setStatusText -import org.dhis2.bindings.setTeiImage import org.dhis2.commons.data.EnrollmentIconData -import org.dhis2.commons.data.SearchTeiModel import org.dhis2.commons.date.toDateSpan import org.dhis2.commons.resources.ColorUtils import org.dhis2.databinding.ItemSearchTrackedEntityBinding +import org.dhis2.usescases.searchTrackEntity.SearchTeiModel abstract class BaseTeiViewHolder( private val binding: ItemSearchTrackedEntityBinding, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchAdapterDiffCallback.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchAdapterDiffCallback.kt index 10bb606d35..0d98c1921e 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchAdapterDiffCallback.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchAdapterDiffCallback.kt @@ -1,7 +1,7 @@ package org.dhis2.usescases.searchTrackEntity.adapters import androidx.recyclerview.widget.DiffUtil -import org.dhis2.commons.data.SearchTeiModel +import org.dhis2.usescases.searchTrackEntity.SearchTeiModel class SearchAdapterDiffCallback() : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SearchTeiModel, newItem: SearchTeiModel): Boolean { diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchErrorViewHolder.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchErrorViewHolder.kt index fecc901853..dc0935983d 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchErrorViewHolder.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchErrorViewHolder.kt @@ -1,8 +1,8 @@ package org.dhis2.usescases.searchTrackEntity.adapters import androidx.recyclerview.widget.RecyclerView -import org.dhis2.commons.data.SearchTeiModel import org.dhis2.databinding.ItemSearchErrorBinding +import org.dhis2.usescases.searchTrackEntity.SearchTeiModel class SearchErrorViewHolder( private val binding: ItemSearchErrorBinding, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt index 73a787f48c..72113bb888 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiLiveAdapter.kt @@ -13,10 +13,10 @@ import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView import org.dhis2.R -import org.dhis2.commons.data.SearchTeiModel import org.dhis2.commons.resources.ColorUtils import org.dhis2.databinding.ItemSearchErrorBinding import org.dhis2.databinding.ItemSearchTrackedEntityBinding +import org.dhis2.usescases.searchTrackEntity.SearchTeiModel import org.dhis2.usescases.searchTrackEntity.ui.mapper.TEICardMapper import org.hisp.dhis.mobile.ui.designsystem.component.ListCard import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel @@ -67,8 +67,12 @@ class SearchTeiLiveAdapter( } override fun getItemViewType(position: Int): Int { + var onlineErrorMessage: String? = null + if (position < snapshot().size) { + onlineErrorMessage = snapshot()[position]?.onlineErrorMessage + } return when { - snapshot()[position]?.onlineErrorMessage != null -> SearchItem.ONLINE_ERROR.ordinal + onlineErrorMessage != null -> SearchItem.ONLINE_ERROR.ordinal fromRelationship -> SearchItem.RELATIONSHIP_TEI.ordinal else -> SearchItem.TEI.ordinal } @@ -129,12 +133,6 @@ class SearchTeiLiveAdapter( ) } } - holder.bind(it, { - getItem(holder.absoluteAdapterPosition)?.toggleAttributeList() - notifyItemChanged(holder.absoluteAdapterPosition) - }) { path: String? -> - path?.let { onImageClick(path) } - } } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiModelExtensions.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiModelExtensions.kt index f161729bda..0209858785 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiModelExtensions.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/adapters/SearchTeiModelExtensions.kt @@ -1,7 +1,128 @@ package org.dhis2.usescases.searchTrackEntity.adapters -import org.dhis2.commons.data.SearchTeiModel +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CircleCrop +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import org.dhis2.commons.R +import org.dhis2.commons.resources.ColorType +import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.usescases.searchTrackEntity.SearchTeiModel +import java.io.File -fun List.uids(): List { - return map { it.tei.uid() } +fun SearchTeiModel.setTeiImage( + context: Context, + teiImageView: ImageView, + teiTextImageView: TextView, + colorUtils: ColorUtils, + pictureListener: (String) -> Unit, +) { + val imageBg = AppCompatResources.getDrawable( + context, + R.drawable.photo_temp_gray, + ) + imageBg?.colorFilter = PorterDuffColorFilter( + colorUtils.getPrimaryColor( + context, + ColorType.PRIMARY, + ), + PorterDuff.Mode.SRC_IN, + ) + teiImageView.background = imageBg + val file = File(profilePicturePath) + val placeHolderId = ResourceManager(context, colorUtils) + .getObjectStyleDrawableResource(defaultTypeIcon, -1) + teiImageView.setOnClickListener(null) + when { + file.exists() -> { + teiTextImageView.visibility = View.GONE + Glide.with(context) + .load(file) + .error(placeHolderId) + .transition(DrawableTransitionOptions.withCrossFade()) + .transform(CircleCrop()) + .into(teiImageView) + teiImageView.setOnClickListener { pictureListener(profilePicturePath) } + } + + textAttributeValues != null && + textAttributeValues.values.isNotEmpty() && + ArrayList(textAttributeValues.values)[0].value() != "-" -> { + teiImageView.setImageDrawable(null) + teiTextImageView.visibility = View.VISIBLE + val valueToShow = ArrayList(textAttributeValues.values) + if (valueToShow[0]?.value()?.isEmpty() != false) { + teiTextImageView.text = "?" + } else { + teiTextImageView.text = valueToShow[0].value()?.first().toString().uppercase() + } + teiTextImageView.setTextColor( + colorUtils.getContrastColor( + colorUtils.getPrimaryColor( + context, + ColorType.PRIMARY, + ), + ), + ) + } + + isOnline && attributeValues.isNotEmpty() && + ArrayList(attributeValues.values).firstOrNull()?.value()?.isNotEmpty() == true -> { + teiImageView.setImageDrawable(null) + teiTextImageView.visibility = View.VISIBLE + val valueToShow = ArrayList(attributeValues.values) + if (valueToShow[0] == null) { + teiTextImageView.text = "?" + } else { + teiTextImageView.text = valueToShow[0].value()?.first().toString().uppercase() + } + teiTextImageView.setTextColor( + colorUtils.getContrastColor( + colorUtils.getPrimaryColor( + context, + ColorType.PRIMARY, + ), + ), + ) + } + + placeHolderId != -1 -> { + teiTextImageView.visibility = View.GONE + val icon = AppCompatResources.getDrawable( + context, + placeHolderId, + ) + icon?.colorFilter = PorterDuffColorFilter( + colorUtils.getContrastColor( + colorUtils.getPrimaryColor( + context, + ColorType.PRIMARY, + ), + ), + PorterDuff.Mode.SRC_IN, + ) + teiImageView.setImageDrawable(icon) + } + + else -> { + teiImageView.setImageDrawable(null) + teiTextImageView.visibility = View.VISIBLE + teiTextImageView.text = "?" + teiTextImageView.setTextColor( + colorUtils.getContrastColor( + colorUtils.getPrimaryColor( + context, + ColorType.PRIMARY, + ), + ), + ) + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt index 96a7833549..a819f98edf 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/listView/SearchTEList.kt @@ -129,9 +129,9 @@ class SearchTEList : FragmentGlobalAbstract() { override fun onAttach(context: Context) { super.onAttach(context) - (context as SearchTEActivity).searchComponent.plus( + (context as SearchTEActivity).searchComponent?.plus( SearchTEListModule(), - ).inject(this) + )?.inject(this) } @ExperimentalAnimationApi diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt index 727b704650..c601bb59a7 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/mapView/SearchTEMap.kt @@ -5,40 +5,63 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.updateLayoutParams +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import androidx.fragment.app.activityViewModels -import com.mapbox.mapboxsdk.geometry.LatLng -import com.mapbox.mapboxsdk.maps.MapboxMap -import org.dhis2.animations.CarouselViewAnimations -import org.dhis2.bindings.dp -import org.dhis2.commons.bindings.clipWithRoundedCorners -import org.dhis2.commons.data.RelationshipOwnerType -import org.dhis2.commons.dialogs.imagedetail.ImageDetailActivity -import org.dhis2.commons.locationprovider.LocationSettingLauncher +import com.mapbox.mapboxsdk.maps.MapView +import org.dhis2.R +import org.dhis2.commons.bindings.launchImageDetail import org.dhis2.commons.resources.ColorType import org.dhis2.commons.resources.ColorUtils -import org.dhis2.databinding.FragmentSearchMapBinding +import org.dhis2.commons.ui.SyncButtonProvider import org.dhis2.maps.ExternalMapNavigation -import org.dhis2.maps.carousel.CarouselAdapter +import org.dhis2.maps.camera.centerCameraOnFeatures import org.dhis2.maps.layer.MapLayerDialog +import org.dhis2.maps.location.MapLocationEngine import org.dhis2.maps.managers.TeiMapManager import org.dhis2.maps.model.MapStyle +import org.dhis2.maps.views.LocationIcon +import org.dhis2.maps.views.MapScreen +import org.dhis2.maps.views.OnMapClickListener +import org.dhis2.ui.avatar.AvatarProvider +import org.dhis2.ui.theme.Dhis2Theme import org.dhis2.usescases.general.FragmentGlobalAbstract -import org.dhis2.usescases.searchTrackEntity.SearchList -import org.dhis2.usescases.searchTrackEntity.SearchScreenState import org.dhis2.usescases.searchTrackEntity.SearchTEActivity import org.dhis2.usescases.searchTrackEntity.SearchTEContractsModule import org.dhis2.usescases.searchTrackEntity.SearchTEIViewModel import org.dhis2.usescases.searchTrackEntity.SearchTeiViewModelFactory -import org.dhis2.utils.NetworkUtils -import org.dhis2.utils.isPortrait +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem +import org.hisp.dhis.mobile.ui.designsystem.component.IconButton +import org.hisp.dhis.mobile.ui.designsystem.component.IconButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardDescriptionModel +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberAdditionalInfoColumnState +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberListCardState +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import javax.inject.Inject const val ARG_FROM_RELATIONSHIP = "ARG_FROM_RELATIONSHIP" const val ARG_TE_TYPE = "ARG_TE_TYPE" -class SearchTEMap : FragmentGlobalAbstract(), MapboxMap.OnMapClickListener { +class SearchTEMap : FragmentGlobalAbstract() { @Inject lateinit var mapNavigation: ExternalMapNavigation @@ -49,17 +72,12 @@ class SearchTEMap : FragmentGlobalAbstract(), MapboxMap.OnMapClickListener { @Inject lateinit var viewModelFactory: SearchTeiViewModelFactory - @Inject - lateinit var animations: CarouselViewAnimations - @Inject lateinit var colorUtils: ColorUtils private val viewModel by activityViewModels { viewModelFactory } private var teiMapManager: TeiMapManager? = null - private var carouselAdapter: CarouselAdapter? = null - lateinit var binding: FragmentSearchMapBinding private val fromRelationship by lazy { arguments?.getBoolean(ARG_FROM_RELATIONSHIP) ?: false @@ -86,9 +104,9 @@ class SearchTEMap : FragmentGlobalAbstract(), MapboxMap.OnMapClickListener { override fun onAttach(context: Context) { super.onAttach(context) - (context as SearchTEActivity).searchComponent.plus( + (context as SearchTEActivity).searchComponent?.plus( SearchTEMapModule(), - ).inject(this) + )?.inject(this) viewModel.setMapScreen() } @@ -97,82 +115,205 @@ class SearchTEMap : FragmentGlobalAbstract(), MapboxMap.OnMapClickListener { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - binding = FragmentSearchMapBinding.inflate(inflater, container, false) - - binding.mapLayerButton.setOnClickListener { - MapLayerDialog(teiMapManager!!) - .show(childFragmentManager, MapLayerDialog::class.java.name) - } + return ComposeView(requireContext()).apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnDetachedFromWindow, + ) + setContent { + val listState = rememberLazyListState() - binding.mapPositionButton.setOnClickListener { - if (locationProvider.hasLocationEnabled()) { - teiMapManager?.centerCameraOnMyPosition { permissionManager -> - permissionManager?.requestLocationPermissions(requireActivity()) + val trackerMapData by viewModel.mapResults.collectAsState(initial = null) + val items by remember { + derivedStateOf { trackerMapData?.mapItems ?: emptyList() } } - } else { - LocationSettingLauncher.requestEnableLocationSetting(requireContext()) - } - } - binding.openSearchButton.setOnClickListener { - viewModel.setSearchScreen() - } + val clickedItem by viewModel.mapItemClicked.collectAsState(initial = null) - teiMapManager = TeiMapManager(binding.mapView) - teiMapManager?.let { lifecycle.addObserver(it) } - teiMapManager?.onCreate(savedInstanceState) - teiMapManager?.teiFeatureType = presenter.getTrackedEntityType(tEType).featureType() - teiMapManager?.enrollmentFeatureType = - if (presenter.program != null) presenter.program.featureType() else null - teiMapManager?.onMapClickListener = this - teiMapManager?.mapStyle = - MapStyle( - presenter.teiColor, - presenter.symbolIcon, - presenter.enrollmentColor, - presenter.enrollmentSymbolIcon, - presenter.programStageStyle, - colorUtils.getPrimaryColor( - requireContext(), - ColorType.PRIMARY_DARK, - ), - ) - initializeCarousel() - teiMapManager?.init( - viewModel.fetchMapStyles(), - onInitializationFinished = { - presenter.getMapData() + val locationState = teiMapManager?.locationState?.collectAsState() - observeMapResults() + LaunchedEffect(key1 = clickedItem) { + if (clickedItem != null) { + listState.animateScrollToItem( + items.indexOfFirst { it.uid == clickedItem }, + ) + } + } - viewModel.fetchMapResults() - }, - onMissingPermission = { permissionsManager -> - permissionsManager?.requestLocationPermissions(requireActivity()) - }, - ) - binding.content.clipWithRoundedCorners() + LaunchedEffect(key1 = items) { + trackerMapData?.let { data -> + teiMapManager?.takeIf { it.isMapReady() }?.update( + data.teiFeatures, + data.eventFeatures, + data.dataElementFeaturess, + data.teiBoundingBox, + ) + } + } - viewModel.screenState.observe(viewLifecycleOwner) { - if (it.screenState == SearchScreenState.MAP) { - val backdropActive = isPortrait() && (it as SearchList).searchFilters.isOpened - binding.mapView.updateLayoutParams { - val bottomMargin = if (backdropActive) { - 0 - } else { - 40.dp + Dhis2Theme { + Box( + modifier = Modifier + .fillMaxSize() + .clip(shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)), + ) { + MapScreen( + items = items, + listState = listState, + map = { + AndroidView(factory = { context -> + val map = MapView(context) + loadMap(map, savedInstanceState) + map + }) { + } + }, + actionButtons = { + IconButton( + style = IconButtonStyle.TONAL, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = "", + tint = TextColor.OnPrimaryContainer, + ) + }, + ) { + viewModel.setSearchScreen() + } + IconButton( + style = IconButtonStyle.TONAL, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_layers), + contentDescription = "", + tint = TextColor.OnPrimaryContainer, + ) + }, + ) { + MapLayerDialog(teiMapManager!!, viewModel.initialProgramUid) { layersVisibility -> + viewModel.filterVisibleMapItems(layersVisibility) + } + .show(childFragmentManager, MapLayerDialog::class.java.name) + } + locationState?.let { + LocationIcon( + locationState = it.value, + onLocationButtonClicked = ::onLocationButtonClicked, + ) + } + }, + onItemScrolled = { item -> + with(teiMapManager) { + this?.requestMapLayerManager()?.selectFeature(null) + this?.findFeatures(item.uid) + ?.takeIf { it.isNotEmpty() }?.let { features -> + map?.centerCameraOnFeatures(features) + } + } + }, + onNavigate = { item -> + teiMapManager?.findFeature(item.uid)?.let { feature -> + startActivity(mapNavigation.navigateToMapIntent(feature)) + } + }, + onItem = { item -> + + ListCard( + modifier = Modifier.fillParentMaxWidth().testTag("MAP_ITEM"), + listCardState = rememberListCardState( + title = ListCardTitleModel(text = item.title), + description = item.description?.let { + ListCardDescriptionModel( + text = it, + ) + }, + lastUpdated = item.lastUpdated, + additionalInfoColumnState = rememberAdditionalInfoColumnState( + additionalInfoList = item.additionalInfoList, + syncProgressItem = AdditionalInfoItem( + key = stringResource(id = R.string.syncing), + value = "", + ), + expandLabelText = stringResource(id = R.string.show_more), + shrinkLabelText = stringResource(id = R.string.show_less), + scrollableContent = true, + ), + ), + actionButton = { + SyncButtonProvider(state = item.state) { + presenter.onSyncIconClick(item.uid) + } + }, + onCardClick = { + if (fromRelationship) { + viewModel.onAddRelationship( + item.uid, + item.relatedInfo?.relationship?.relationshipTypeUid, + item.isOnline, + ) + } else { + viewModel.onTeiClick( + item.uid, + item.relatedInfo?.enrollment?.uid, + item.isOnline, + ) + } + }, + listAvatar = { + AvatarProvider( + avatarProviderConfiguration = item.avatarProviderConfiguration, + onImageClick = ::launchImageDetail, + ) + }, + ) + }, + ) } - setMargins(0, 0, 0, bottomMargin) } } } + } - return binding.root + private fun onLocationButtonClicked() { + teiMapManager?.onLocationButtonClicked( + locationProvider.hasLocationEnabled(), + requireActivity(), + ) } - override fun onDestroy() { - super.onDestroy() - teiMapManager?.onDestroy() + private fun loadMap(mapView: MapView, savedInstanceState: Bundle?) { + teiMapManager = TeiMapManager(mapView, MapLocationEngine(requireContext())).also { + lifecycle.addObserver(it) + it.onCreate(savedInstanceState) + it.teiFeatureType = presenter.getTrackedEntityType(tEType).featureType() + it.enrollmentFeatureType = + if (presenter.program != null) presenter.program.featureType() else null + it.onMapClickListener = OnMapClickListener(it, viewModel::onFeatureClicked) + it.mapStyle = + MapStyle( + presenter.teiColor, + presenter.symbolIcon, + presenter.enrollmentColor, + presenter.enrollmentSymbolIcon, + presenter.programStageStyle, + colorUtils.getPrimaryColor( + requireContext(), + ColorType.PRIMARY_DARK, + ), + ) + it.init( + viewModel.fetchMapStyles(), + onInitializationFinished = { + presenter.getMapData() + viewModel.filterVisibleMapItems( + it.mapLayerManager.mapLayers.toMap(), + ) + }, + onMissingPermission = { permissionsManager -> + permissionsManager?.requestLocationPermissions(requireActivity()) + }, + ) + } } override fun onLowMemory() { @@ -198,101 +339,4 @@ class SearchTEMap : FragmentGlobalAbstract(), MapboxMap.OnMapClickListener { grantResults, ) } - - private fun observeMapResults() { - animations.initMapLoading(binding.mapCarousel) - - viewModel.mapResults.removeObservers(viewLifecycleOwner) - viewModel.mapResults.observe(viewLifecycleOwner) { trackerMapData -> - teiMapManager?.update( - trackerMapData.teiFeatures, - trackerMapData.eventFeatures, - trackerMapData.dataElementFeaturess, - trackerMapData.teiBoundingBox, - ) - carouselAdapter?.setAllItems(trackerMapData.allItems()) - carouselAdapter?.updateLayers(teiMapManager?.mapLayerManager?.mapLayers) - animations.endMapLoading(binding.mapCarousel) - viewModel.mapDataFetched() - } - } - - private fun initializeCarousel() { - carouselAdapter = CarouselAdapter.Builder() - .addOnTeiClickListener { teiUid: String, enrollmentUid: String?, isDeleted: Boolean? -> - if (binding.mapCarousel.carouselEnabled) { - if (fromRelationship) { - presenter.addRelationship( - teiUid, - null, - NetworkUtils.isOnline(requireContext()), - ) - } else { - presenter.onTEIClick(teiUid, enrollmentUid, isDeleted!!) - } - } - true - } - .addOnSyncClickListener { teiUid: String? -> - if (binding.mapCarousel.carouselEnabled) { - presenter.onSyncIconClick(teiUid) - } - true - } - .addOnDeleteRelationshipListener { relationshipUid: String? -> - if (binding.mapCarousel.carouselEnabled) { - presenter.deleteRelationship(relationshipUid) - viewModel.refreshData() - } - true - } - .addOnRelationshipClickListener { teiUid: String?, ownerType: RelationshipOwnerType -> - if (binding.mapCarousel.carouselEnabled) { - presenter.onTEIClick(teiUid, null, false) - } - true - } - .addOnEventClickListener { teiUid: String?, enrollmentUid: String?, eventUid: String? -> - if (binding.mapCarousel.carouselEnabled) { - presenter.onTEIClick(teiUid, enrollmentUid, false) - } - true - } - .addOnProfileImageClickListener { path: String? -> - if (binding.mapCarousel.carouselEnabled && !path.isNullOrBlank()) { - val intent = ImageDetailActivity.intent( - context = requireContext(), - title = null, - imagePath = path, - ) - - startActivity(intent) - } - Unit - } - .addOnNavigateClickListener { uuid: String? -> - val feature = teiMapManager!!.findFeature( - uuid!!, - ) - if (feature != null) { - startActivity(mapNavigation.navigateToMapIntent(feature)) - } - Unit - } - .addProgram(presenter.program) - .addMapManager(teiMapManager!!) - .build() - teiMapManager?.carouselAdapter = carouselAdapter - binding.mapCarousel.setAdapter(carouselAdapter) - teiMapManager?.let { binding.mapCarousel.attachToMapManager(it) } - } - - override fun onMapClick(point: LatLng): Boolean { - val featureFound = teiMapManager!!.markFeatureAsSelected(point, null) - if (featureFound != null) { - binding.mapCarousel.scrollToFeature(featureFound) - return true - } - return false - } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/searchparameters/SearchParametersScreen.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/searchparameters/SearchParametersScreen.kt index af52d14a5b..7131eeaa61 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/searchparameters/SearchParametersScreen.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/searchparameters/SearchParametersScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material.SnackbarDuration import androidx.compose.material.SnackbarHost import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -50,8 +51,11 @@ import org.dhis2.usescases.searchTrackEntity.SearchTEIViewModel import org.dhis2.usescases.searchTrackEntity.searchparameters.model.SearchParametersUiState import org.dhis2.usescases.searchTrackEntity.searchparameters.provider.provideParameterSelectorItem import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItemColor import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.InfoBar +import org.hisp.dhis.mobile.ui.designsystem.component.InfoBarData import org.hisp.dhis.mobile.ui.designsystem.component.parameter.ParameterSelectorItem import org.hisp.dhis.mobile.ui.designsystem.theme.Radius import org.hisp.dhis.mobile.ui.designsystem.theme.Shape @@ -196,24 +200,50 @@ fun SearchParametersScreen( .weight(1F) .verticalScroll(rememberScrollState()), ) { - uiState.items.forEachIndexed { index, fieldUiModel -> - fieldUiModel.setCallback(callback) - ParameterSelectorItem( - modifier = Modifier - .testTag("SEARCH_PARAM_ITEM"), - model = provideParameterSelectorItem( - resources = resourceManager, - focusManager = focusManager, - fieldUiModel = fieldUiModel, - callback = callback, - onNextClicked = { - val nextIndex = index + 1 - if (nextIndex < uiState.items.size) { - uiState.items[nextIndex].onItemClick() - } - }, - ), - ) + if (uiState.items.isEmpty()) { + Column( + modifier = Modifier.fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + InfoBar( + infoBarData = InfoBarData( + text = resourceManager.getString(R.string.empty_search_attributes_message), + icon = { + Icon( + imageVector = Icons.Outlined.ErrorOutline, + contentDescription = "warning", + tint = AdditionalInfoItemColor.WARNING.color, + ) + }, + color = AdditionalInfoItemColor.WARNING.color, + backgroundColor = AdditionalInfoItemColor.WARNING.color.copy(alpha = 0.1f), + actionText = null, + onClick = {}, + ), + Modifier.testTag("EMPTY_SEARCH_ATTRIBUTES_TEXT_TAG"), + ) + } + } else { + uiState.items.forEachIndexed { index, fieldUiModel -> + fieldUiModel.setCallback(callback) + ParameterSelectorItem( + modifier = Modifier + .testTag("SEARCH_PARAM_ITEM"), + model = provideParameterSelectorItem( + resources = resourceManager, + focusManager = focusManager, + fieldUiModel = fieldUiModel, + callback = callback, + onNextClicked = { + val nextIndex = index + 1 + if (nextIndex < uiState.items.size) { + uiState.items[nextIndex].onItemClick() + } + }, + ), + ) + } } if (uiState.clearSearchEnabled) { @@ -275,7 +305,6 @@ fun SearchFormPreview() { items = listOf( FieldUiModelImpl( uid = "uid1", - layoutId = 1, label = "Label 1", autocompleteList = emptyList(), optionSetConfiguration = null, @@ -283,7 +312,6 @@ fun SearchFormPreview() { ), FieldUiModelImpl( uid = "uid2", - layoutId = 2, label = "Label 2", autocompleteList = emptyList(), optionSetConfiguration = null, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/BackdropManager.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/BackdropManager.kt index 81261b2a44..eb2c4ea1b3 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/BackdropManager.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/BackdropManager.kt @@ -10,7 +10,12 @@ import org.dhis2.R object BackdropManager { private const val changeBoundDuration = 200L - private fun changeBounds(backdropLayout: ConstraintLayout, endID: Int, margin: Int) { + private fun changeBounds( + isNavigationBarVisible: Boolean, + backdropLayout: ConstraintLayout, + endID: Int, + margin: Int, + ) { val transition: Transition = ChangeBounds() transition.duration = changeBoundDuration TransitionManager.beginDelayedTransition(backdropLayout, transition) @@ -19,15 +24,33 @@ object BackdropManager { initSet.clone(backdropLayout) initSet.connect(R.id.mainComponent, ConstraintSet.TOP, endID, ConstraintSet.BOTTOM, margin) + if (isNavigationBarVisible) { + initSet.connect( + R.id.mainComponent, + ConstraintSet.BOTTOM, + R.id.navigationBar, + ConstraintSet.TOP, + 0, + ) + } else { + initSet.connect( + R.id.mainComponent, + ConstraintSet.BOTTOM, + ConstraintSet.PARENT_ID, + ConstraintSet.BOTTOM, + 0, + ) + } initSet.applyTo(backdropLayout) } fun changeBoundsIf( condition: Boolean, + isNavigationBarVisible: Boolean, backdropLayout: ConstraintLayout, endID: Int, margin: Int, ) { - if (condition) changeBounds(backdropLayout, endID, margin) + if (condition) changeBounds(isNavigationBarVisible, backdropLayout, endID, margin) } } diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt index 9e0c5ea837..9a07f35cf5 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchScreenConfigurator.kt @@ -81,9 +81,8 @@ class SearchScreenConfigurator( } binding.filterRecyclerLayout.visibility = View.VISIBLE binding.searchContainer.visibility = View.GONE - if (isPortrait()) binding.navigationBar.hide() filterIsOpenCallback(true) - changeBounds(R.id.filterRecyclerLayout, 16.dp) + changeBounds(false, R.id.filterRecyclerLayout, 16.dp) } fun closeBackdrop() { @@ -93,9 +92,8 @@ class SearchScreenConfigurator( } binding.filterRecyclerLayout.visibility = View.GONE binding.searchContainer.visibility = View.GONE - if (isPortrait()) binding.navigationBar.show() filterIsOpenCallback(false) - changeBounds(R.id.backdropGuideTop, 0) + changeBounds(true, R.id.backdropGuideTop, 0) } private fun openSearch() { @@ -105,14 +103,14 @@ class SearchScreenConfigurator( binding.title.visibility = View.VISIBLE } binding.searchContainer.visibility = View.VISIBLE - if (isPortrait()) binding.navigationBar.hide() filterIsOpenCallback(false) - changeBounds(R.id.searchContainer, 0) + changeBounds(false, R.id.searchContainer, 0) } - private fun changeBounds(endID: Int, margin: Int) { + private fun changeBounds(isNavigationBarVisible: Boolean, endID: Int, margin: Int) { changeBoundsIf( isPortrait(), + isNavigationBarVisible, binding.backdropLayout, endID, margin, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt index 516d63d134..174ca0bd22 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/SearchTEUi.kt @@ -32,6 +32,7 @@ import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -196,7 +197,7 @@ fun SearchButtonWithQuery( .background(Color.Unspecified) .clickable( onClick = onClick, - interactionSource = MutableInteractionSource(), + interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple( true, color = SurfaceColor.Primary, diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt index 5bbf5f5e8f..28c270ed10 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt @@ -18,11 +18,11 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import org.dhis2.R import org.dhis2.bindings.hasFollowUp -import org.dhis2.commons.data.SearchTeiModel import org.dhis2.commons.date.toDateSpan import org.dhis2.commons.date.toOverdueOrScheduledUiText import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.ui.model.ListCardUiModel +import org.dhis2.usescases.searchTrackEntity.SearchTeiModel import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.enrollment.Enrollment import org.hisp.dhis.android.core.enrollment.EnrollmentStatus @@ -30,9 +30,10 @@ import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItemColor import org.hisp.dhis.mobile.ui.designsystem.component.Avatar -import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyle +import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyleData import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.MetadataAvatarSize import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import java.io.File @@ -63,20 +64,32 @@ class TEICardMapper( @Composable private fun ProvideAvatar(item: SearchTeiModel, onImageClick: ((String) -> Unit)) { + val programUid: String? = if (item.selectedEnrollment != null) { + item.selectedEnrollment.program().toString() + } else { + null + } + if (item.profilePicturePath.isNotEmpty()) { val file = File(item.profilePicturePath) val bitmap = BitmapFactory.decodeFile(file.absolutePath).asImageBitmap() val painter = BitmapPainter(bitmap) Avatar( - imagePainter = painter, - style = AvatarStyle.IMAGE, + style = AvatarStyleData.Image(painter), onImageClick = { onImageClick(item.profilePicturePath) }, ) + } else if (item.isMetadataIconDataAvailable(programUid)) { + Avatar( + style = AvatarStyleData.Metadata( + imageCardData = item.getMetadataIconData(programUid).imageCardData, + avatarSize = MetadataAvatarSize.S(), + tintColor = item.getMetadataIconData(programUid).color, + ), + ) } else { Avatar( - textAvatar = getTitleFirstLetter(item), - style = AvatarStyle.TEXT, + style = AvatarStyleData.Text(getTitleFirstLetter(item)), ) } } diff --git a/app/src/main/java/org/dhis2/usescases/settings/bindings/SyncManagerBindings.kt b/app/src/main/java/org/dhis2/usescases/settings/bindings/SyncManagerBindings.kt index 66d332f047..4c1aafd7f3 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/bindings/SyncManagerBindings.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/bindings/SyncManagerBindings.kt @@ -3,16 +3,20 @@ package org.dhis2.usescases.settings.bindings import androidx.compose.ui.platform.ComposeView import androidx.databinding.BindingAdapter import org.dhis2.ui.Dhis2ProgressIndicator -import org.dhis2.ui.buttons.Dhis2TextButton import org.dhis2.ui.model.ButtonUiModel import org.dhis2.ui.theme.Dhis2Theme +import org.hisp.dhis.mobile.ui.designsystem.component.Button @BindingAdapter("addTextButton") fun ComposeView.addTextButton(model: ButtonUiModel?) { model?.let { setContent { Dhis2Theme { - Dhis2TextButton(model = it) + Button( + text = it.text, + enabled = it.enabled, + onClick = it.onClick, + ) } } } diff --git a/app/src/main/java/org/dhis2/usescases/splash/SplashActivity.kt b/app/src/main/java/org/dhis2/usescases/splash/SplashActivity.kt index 622b49c74f..4885aab1cd 100644 --- a/app/src/main/java/org/dhis2/usescases/splash/SplashActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/splash/SplashActivity.kt @@ -139,6 +139,7 @@ class SplashActivity : ActivityGlobalAbstract(), SplashView { LoginActivity::class.java, LoginActivity.bundle( accountsCount = presenter.getAccounts(), + fromSplash = true, ), true, true, diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.kt index 57393ff42b..6da5e08409 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardProgramModel.kt @@ -17,6 +17,7 @@ sealed class DashboardModel( open val orgUnits: List, open val teiHeader: String?, open val avatarPath: String?, + open val ownerOrgUnit: OrganisationUnit?, ) { open fun getTrackedEntityAttributeValueBySortOrder(sortOrder: Int): String? { return if (sortOrder <= trackedEntityAttributeValues.size) { @@ -37,6 +38,7 @@ data class DashboardEnrollmentModel( override val orgUnits: List, override val teiHeader: String?, override val avatarPath: String?, + override val ownerOrgUnit: OrganisationUnit?, ) : DashboardModel( trackedEntityInstance, trackedEntityAttributeValues, @@ -44,6 +46,7 @@ data class DashboardEnrollmentModel( orgUnits, teiHeader, avatarPath, + ownerOrgUnit, ) { fun currentProgram(): Program { return enrollmentPrograms.first { it.first.uid() == currentEnrollment.program() }.first @@ -67,6 +70,7 @@ data class DashboardTEIModel( override val orgUnits: List, override val teiHeader: String?, override val avatarPath: String?, + override val ownerOrgUnit: OrganisationUnit?, ) : DashboardModel( trackedEntityInstance, trackedEntityAttributeValues, @@ -74,6 +78,7 @@ data class DashboardTEIModel( orgUnits, teiHeader, avatarPath, + ownerOrgUnit, ) { fun getProgramsWithActiveEnrollment(): List? { return enrollmentPrograms.sortedBy { it.first.displayName()?.lowercase() } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt index e6d6353c20..6bb4e94218 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepository.kt @@ -105,9 +105,14 @@ interface DashboardRepository { teiUid: String, ): Observable>> - fun getDashboardModel(): DashboardModel + fun getDashboardModel(): DashboardModel? fun getGrouping(): Boolean fun setGrouping(groupEvent: Boolean) + + fun transferTei(newOrgUnitId: String) + + fun teiCanBeTransferred(): Boolean + fun enrollmentHasWriteAccess(): Boolean } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt index d81101aa2d..6673a21ffc 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardRepositoryImpl.kt @@ -20,6 +20,7 @@ import org.hisp.dhis.android.core.category.CategoryOptionCombo import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.enrollment.EnrollmentAccess import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.EventStatus @@ -208,7 +209,10 @@ class DashboardRepositoryImpl( return attributeValues } - private fun mapRelationShipTypes(list: List, teType: String): MutableList> { + private fun mapRelationShipTypes( + list: List, + teType: String, + ): MutableList> { val relTypeList: MutableList> = java.util.ArrayList() for (relationshipType in list) { @@ -374,6 +378,17 @@ class DashboardRepositoryImpl( } } + private fun getOwnerOrgUnit(teiUid: String): OrganisationUnit? { + val orgUnitId = d2.trackedEntityModule() + .trackedEntityInstances().withProgramOwners() + .uid(teiUid).blockingGet() + ?.programOwners()?.first { it.trackedEntityInstance() == teiUid }?.ownerOrgUnit() + + return d2.organisationUnitModule().organisationUnits() + .uid(orgUnitId) + .blockingGet() + } + override fun getDashboardModel(): DashboardModel { return if (programUid.isNullOrEmpty()) { DashboardTEIModel( @@ -384,6 +399,7 @@ class DashboardRepositoryImpl( getTeiOrgUnits(teiUid, null).blockingFirst(), getTeiHeader(), getTeiProfilePath(), + getOwnerOrgUnit(teiUid), ) } else { DashboardEnrollmentModel( @@ -396,6 +412,7 @@ class DashboardRepositoryImpl( getTeiOrgUnits(teiUid, programUid).blockingFirst(), getTeiHeader(), getTeiProfilePath(), + getOwnerOrgUnit(teiUid), ) } } @@ -574,6 +591,39 @@ class DashboardRepositoryImpl( ) } + override fun transferTei(newOrgUnitId: String) { + d2.trackedEntityModule() + .ownershipManager() + .blockingTransfer(teiUid, programUid!!, newOrgUnitId) + } + + override fun teiCanBeTransferred(): Boolean { + if (programUid == null) { + return false + } + + val orgUnits = d2.organisationUnitModule().organisationUnits() + .byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_TEI_SEARCH) + .byProgramUids(listOf(programUid)) + .blockingGet() + + if (orgUnits.isEmpty()) { + return false + } + + return orgUnits.size > 1 || + orgUnits.first().uid() != getOwnerOrgUnit(teiUid)?.uid() + } + + override fun enrollmentHasWriteAccess(): Boolean { + return programUid?.let { + d2.enrollmentModule().enrollmentService().blockingGetEnrollmentAccess( + teiUid, + it, + ) + } == EnrollmentAccess.WRITE_ACCESS + } + private fun getGroupingOptions(): HashMap { val typeToken: TypeToken> = object : TypeToken>() {} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt index 7523decc33..a972e26f35 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModel.kt @@ -1,5 +1,14 @@ package org.dhis2.usescases.teiDashboard +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Assignment +import androidx.compose.material.icons.automirrored.filled.StickyNote2 +import androidx.compose.material.icons.automirrored.outlined.Assignment +import androidx.compose.material.icons.automirrored.outlined.StickyNote2 +import androidx.compose.material.icons.filled.BarChart +import androidx.compose.material.icons.filled.Hub +import androidx.compose.material.icons.outlined.BarChart +import androidx.compose.material.icons.outlined.Hub import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -9,24 +18,36 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.dhis2.R +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.tracker.NavigationBarUIState +import org.dhis2.tracker.TEIDashboardItems +import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState import org.dhis2.utils.AuthorityException import org.dhis2.utils.analytics.ACTIVE_FOLLOW_UP import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.analytics.FOLLOW_UP +import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator +import org.dhis2.utils.isPortrait import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.common.State.SYNCED import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBarItem import timber.log.Timber class DashboardViewModel( private val repository: DashboardRepository, private val analyticsHelper: AnalyticsHelper, private val dispatcher: DispatcherProvider, + private val pageConfigurator: NavigationPageConfigurator, + private val resourcesManager: ResourceManager, ) : ViewModel() { private val eventUid = MutableLiveData() + private val selectedEventUid = MutableLiveData() + val showStatusErrorMessages = MutableLiveData(StatusChangeResultCode.CHANGED) private var _showFollowUpBar = MutableStateFlow(false) @@ -41,8 +62,11 @@ class DashboardViewModel( private var _state = MutableStateFlow(null) val state = _state.asStateFlow() - private val _dashboardModel = MutableLiveData() - var dashboardModel: LiveData = _dashboardModel + private val _isLoading = MutableLiveData(false) + val isLoading: LiveData = _isLoading + + private val _dashboardModel = MutableLiveData() + var dashboardModel: LiveData = _dashboardModel private val _groupByStage = MutableLiveData() val groupByStage: LiveData = _groupByStage @@ -50,6 +74,14 @@ class DashboardViewModel( private val _noEnrollmentSelected = MutableLiveData(false) val noEnrollmentSelected: LiveData = _noEnrollmentSelected + private val _navigationBarUIState = + MutableStateFlow>(NavigationBarUIState()) + val navigationBarUIState = _navigationBarUIState.asStateFlow() + + private val _relationshipTopBarIconState = + MutableStateFlow(RelationshipTopBarIconState.List()) + val relationshipTopBarIconState = _relationshipTopBarIconState.asStateFlow() + init { fetchDashboardModel() fetchGrouping() @@ -73,6 +105,7 @@ class DashboardViewModel( _state.value = model.currentEnrollment.aggregatedSyncState() _noEnrollmentSelected.value = false + loadNavigationBarItems() } else { _noEnrollmentSelected.value = true } @@ -83,6 +116,59 @@ class DashboardViewModel( } } + private fun loadNavigationBarItems() { + val enrollmentItems = mutableListOf>() + + if (isPortrait()) { + enrollmentItems.add( + NavigationBarItem( + id = TEIDashboardItems.DETAILS, + icon = Icons.AutoMirrored.Outlined.Assignment, + + selectedIcon = Icons.AutoMirrored.Filled.Assignment, + label = resourcesManager.getString(R.string.navigation_tei_data), + ), + ) + } + + if (pageConfigurator.displayAnalytics()) { + enrollmentItems.add( + NavigationBarItem( + id = TEIDashboardItems.ANALYTICS, + icon = Icons.Outlined.BarChart, + selectedIcon = Icons.Filled.BarChart, + label = resourcesManager.getString(R.string.navigation_analytics), + ), + ) + } + + if (pageConfigurator.displayRelationships()) { + enrollmentItems.add( + NavigationBarItem( + id = TEIDashboardItems.RELATIONSHIPS, + icon = Icons.Outlined.Hub, + selectedIcon = Icons.Filled.Hub, + label = resourcesManager.getString(R.string.navigation_relations), + ), + ) + } + + enrollmentItems.add( + NavigationBarItem( + id = TEIDashboardItems.NOTES, + icon = Icons.AutoMirrored.Outlined.StickyNote2, + selectedIcon = Icons.AutoMirrored.Filled.StickyNote2, + label = resourcesManager.getString(R.string.navigation_notes), + ), + ) + + _navigationBarUIState.value = _navigationBarUIState.value.copy(items = enrollmentItems) + + if (navigationBarUIState.value.items.none { it.id == navigationBarUIState.value.selectedItem }) { + onNavigationItemSelected(navigationBarUIState.value.items.first().id) + } + } + private fun fetchGrouping() { viewModelScope.launch(dispatcher.io()) { val result = async { @@ -169,4 +255,60 @@ class DashboardViewModel( } } } + + fun selectedEventUid(): LiveData { + return selectedEventUid + } + + fun updateSelectedEventUid(uid: String?) { + if (selectedEventUid.value != uid) { + this.selectedEventUid.value = uid + } + } + + fun updateNoteCounter(numberOfNotes: Int) { + _navigationBarUIState.value = _navigationBarUIState.value.copy( + items = _navigationBarUIState.value.items.map { + if (it.id == TEIDashboardItems.NOTES) { + it.copy(showBadge = numberOfNotes > 0) + } else { + it + } + }, + ) + } + + fun onNavigationItemSelected(itemId: TEIDashboardItems) { + _navigationBarUIState.value = _navigationBarUIState.value.copy(selectedItem = itemId) + } + + fun checkIfTeiCanBeTransferred(): Boolean { + return repository.teiCanBeTransferred() + } + + fun transferTei( + newOrgUnitId: String, + onCompletion: () -> Unit, + ) { + _isLoading.value = true + viewModelScope.launch(dispatcher.io()) { + try { + repository.transferTei(newOrgUnitId) + withContext(dispatcher.ui()) { + updateDashboard() + onCompletion() + } + } catch (ex: Exception) { + Timber.e(ex) + } finally { + withContext(dispatcher.ui()) { + _isLoading.value = false + } + } + } + } + + fun updateRelationshipsTopBarIconState(state: RelationshipTopBarIconState) { + _relationshipTopBarIconState.value = state + } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt index 5f632375b8..285951d4ef 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/DashboardViewModelFactory.kt @@ -2,14 +2,18 @@ package org.dhis2.usescases.teiDashboard import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.utils.analytics.AnalyticsHelper +import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator @Suppress("UNCHECKED_CAST") class DashboardViewModelFactory( val repository: DashboardRepository, val analyticsHelper: AnalyticsHelper, val dispatcher: DispatcherProvider, + val pageConfigurator: NavigationPageConfigurator, + val resourceManager: ResourceManager, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @@ -17,6 +21,8 @@ class DashboardViewModelFactory( repository, analyticsHelper, dispatcher, + pageConfigurator, + resourceManager, ) as T } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardComponent.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardComponent.java index 50c9278d86..82a0bc0cca 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardComponent.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardComponent.java @@ -29,5 +29,7 @@ public interface TeiDashboardComponent { @NonNull TEIDataComponent plus(TEIDataModule teiDataModule); + DashboardViewModelFactory dashboardViewModelFactory(); + void inject(TeiDashboardMobileActivity mobileActivity); } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardContracts.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardContracts.java index c86529df1d..89463c3d98 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardContracts.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardContracts.java @@ -25,6 +25,8 @@ public interface View extends AbstractActivityContracts.View { void showTabsAndEnableSwipe(); void displayStatusError(StatusChangeResultCode statusCode); + + void showOrgUnitSelector(String programUid); } public interface Presenter { @@ -66,5 +68,9 @@ public interface Presenter { Boolean checkIfTEICanBeDeleted(); Boolean checkIfEnrollmentCanBeDeleted(String enrollmentUid); + + void onTransferClick(); + + boolean hasWriteAccess(); } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt index dc0db0ec6e..db58f8aba3 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardMobileActivity.kt @@ -8,43 +8,66 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.TypedValue -import android.view.MenuItem import android.view.View import android.view.WindowManager -import android.widget.PopupMenu import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.MoveDown +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.style.TextAlign import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.databinding.DataBindingUtil import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModelProvider -import androidx.viewpager2.widget.ViewPager2 +import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import org.dhis2.App import org.dhis2.R import org.dhis2.commons.Constants import org.dhis2.commons.Constants.TEI_UID +import org.dhis2.commons.featureconfig.data.FeatureConfigRepository import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.filters.Filters import org.dhis2.commons.network.NetworkUtils -import org.dhis2.commons.popupmenu.AppMenuHelper +import org.dhis2.commons.orgunitselector.OUTreeFragment +import org.dhis2.commons.orgunitselector.OUTreeModel +import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext import org.dhis2.databinding.ActivityDashboardMobileBinding +import org.dhis2.form.model.EnrollmentMode +import org.dhis2.form.ui.provider.FormResultDialogProvider +import org.dhis2.tracker.TEIDashboardItems +import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState import org.dhis2.ui.ThemeManager import org.dhis2.ui.dialogs.bottomsheet.DeleteBottomSheetDialog +import org.dhis2.usescases.enrollment.DateEditionWarningHandler import org.dhis2.usescases.enrollment.EnrollmentActivity import org.dhis2.usescases.enrollment.EnrollmentActivity.Companion.getIntent +import org.dhis2.usescases.enrollment.EnrollmentFormBuilderConfig +import org.dhis2.usescases.enrollment.buildEnrollmentForm import org.dhis2.usescases.general.ActivityGlobalAbstract +import org.dhis2.usescases.notes.NotesFragment import org.dhis2.usescases.qrCodes.QrActivity -import org.dhis2.usescases.teiDashboard.adapters.DashboardPagerAdapter -import org.dhis2.usescases.teiDashboard.adapters.DashboardPagerAdapter.Companion.NO_POSITION -import org.dhis2.usescases.teiDashboard.adapters.DashboardPagerAdapter.DashboardPageType +import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.IndicatorsFragment +import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.VISUALIZATION_TYPE +import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.VisualizationType import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.MapButtonObservable +import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipFragment +import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataActivityContract import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataFragment.Companion.newInstance import org.dhis2.usescases.teiDashboard.teiProgramList.TeiProgramListActivity +import org.dhis2.usescases.teiDashboard.ui.RelationshipTopBarIcon +import org.dhis2.usescases.teiDashboard.ui.getEnrollmentMenuList import org.dhis2.usescases.teiDashboard.ui.setButtonContent import org.dhis2.utils.HelpManager import org.dhis2.utils.analytics.CLICK @@ -52,23 +75,36 @@ import org.dhis2.utils.analytics.SHARE_TEI import org.dhis2.utils.analytics.SHOW_HELP import org.dhis2.utils.analytics.TYPE_QR import org.dhis2.utils.analytics.TYPE_SHARE +import org.dhis2.utils.customviews.MoreOptionsWithDropDownMenuButton import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.dhis2.utils.granularsync.SyncStatusDialog import org.dhis2.utils.granularsync.shouldLaunchSyncDialog import org.dhis2.utils.isLandscape import org.dhis2.utils.isPortrait import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBar +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import javax.inject.Inject class TeiDashboardMobileActivity : ActivityGlobalAbstract(), TeiDashboardContracts.View, - MapButtonObservable { + MapButtonObservable, + TEIDataActivityContract { private var currentOrientation = -1 @Inject lateinit var presenter: TeiDashboardContracts.Presenter + @Inject + lateinit var dateEditionWarningHandler: DateEditionWarningHandler + + @Inject + lateinit var enrollmentResultDialogProvider: FormResultDialogProvider + + var featureConfig: FeatureConfigRepository? = null + @Inject set + @Inject lateinit var filterManager: FilterManager @@ -87,14 +123,15 @@ class TeiDashboardMobileActivity : @Inject lateinit var resourceManager: ResourceManager + @Inject + lateinit var eventResourcesProvider: EventResourcesProvider + lateinit var programModel: DashboardProgramModel var teiUid: String? = null var programUid: String? = null var enrollmentUid: String? = null lateinit var binding: ActivityDashboardMobileBinding - var adapter: DashboardPagerAdapter? = null private lateinit var dashboardViewModel: DashboardViewModel - private var fromRelationship = false private var relationshipMap: MutableLiveData = MutableLiveData(false) @@ -165,39 +202,114 @@ class TeiDashboardMobileActivity : filterManager.setUnsupportedFilters(Filters.ENROLLMENT_DATE, Filters.ENROLLMENT_STATUS) presenter.prefSaveCurrentProgram(programUid) elevation = ViewCompat.getElevation(binding.toolbar) - binding.relationshipMapIcon.setOnClickListener { - networkUtils.performIfOnline( - this, - { - if (java.lang.Boolean.FALSE == relationshipMap.value) { - binding.relationshipMapIcon.setImageResource(R.drawable.ic_list) - } else { - binding.relationshipMapIcon.setImageResource(R.drawable.ic_map) - } - val showMap = !relationshipMap.value!! - if (showMap) { - binding.toolbarProgress.visibility = View.VISIBLE - binding.toolbarProgress.hide() - } - relationshipMap.value = showMap - }, - {}, - getString(R.string.msg_network_connection_maps), - ) + + setRelationshipMapIcon() + setSyncButtonListener() + setFormViewForLandScape() + setEditButton() + observeErrorMessages() + observeProgressBar() + observeDashboardModel() + setUpNavigationBar() + showLoadingProgress(false) + setupMoreOptionsMenu() + } + + private fun observeErrorMessages() { + dashboardViewModel.showStatusErrorMessages.observe(this) { + displayStatusError(it) } + } + + private fun observeProgressBar() { + dashboardViewModel.isLoading.observe(this) { + showLoadingProgress(it) + } + } + + private fun setSyncButtonListener() { binding.syncButton.setOnClickListener { openSyncDialog() } if (intent.shouldLaunchSyncDialog()) { openSyncDialog() } - setNavigationBar() - setEditButton() - dashboardViewModel.showStatusErrorMessages.observe(this) { - displayStatusError(it) - } + } + + private fun observeDashboardModel() { dashboardViewModel.dashboardModel.observe(this) { - when (it) { - is DashboardEnrollmentModel -> setData(it) - is DashboardTEIModel -> setDataWithOutProgram(it) + if (sessionManagerServiceImpl.isUserLoggedIn()) { + when (it) { + is DashboardEnrollmentModel -> setData(it) + is DashboardTEIModel -> setDataWithOutProgram(it) + else -> // Do nothing + Unit + } + } + } + } + + private fun setRelationshipMapIcon() { + binding.relationshipIcon.setContent { + val relationshipTopBarIconState by dashboardViewModel.relationshipTopBarIconState.collectAsState() + RelationshipTopBarIcon( + relationshipTopBarIconState = relationshipTopBarIconState, + ) { + when (val uiState = relationshipTopBarIconState) { + is RelationshipTopBarIconState.Selecting -> { + uiState.onClickListener() + } + + is RelationshipTopBarIconState.List -> { + networkUtils.performIfOnline( + context = this, + action = { + dashboardViewModel.updateRelationshipsTopBarIconState( + RelationshipTopBarIconState.Map(), + ) + }, + noNetworkMessage = getString(R.string.msg_network_connection_maps), + ) + + binding.toolbarProgress.visibility = View.VISIBLE + binding.toolbarProgress.hide() + relationshipMap.value = true + } + + is RelationshipTopBarIconState.Map -> { + networkUtils.performIfOnline( + context = this, + action = { + dashboardViewModel.updateRelationshipsTopBarIconState( + RelationshipTopBarIconState.List(), + ) + }, + noNetworkMessage = getString(R.string.msg_network_connection_maps), + ) + relationshipMap.value = false + } + } + } + } + } + + private fun setFormViewForLandScape() { + if (isLandscape() && enrollmentUid != null) { + val saveButton = findViewById(R.id.saveLand) as FloatingActionButton + buildEnrollmentForm( + config = EnrollmentFormBuilderConfig( + enrollmentUid = enrollmentUid!!, + programUid = programUid!!, + enrollmentMode = EnrollmentMode.CHECK, + hasWriteAccess = presenter.hasWriteAccess(), + openErrorLocation = false, + containerId = R.id.tei_form_view, + loadingView = binding.toolbarProgress, + saveButton = saveButton, + ), + locationProvider = locationProvider, + dateEditionWarningHandler = dateEditionWarningHandler, + enrollmentResultDialogProvider = enrollmentResultDialogProvider, + ) { + dashboardViewModel.updateDashboard() } } } @@ -220,46 +332,102 @@ class TeiDashboardMobileActivity : } } - private fun setNavigationBar() { - if (programUid != null) { - binding.navigationBar.visibility = View.VISIBLE - binding.navigationBar.pageConfiguration(pageConfigurator) - binding.navigationBar.setOnItemSelectedListener { item: MenuItem -> - adapter?.let { pagerAdapter -> - when (item.itemId) { - R.id.navigation_analytics -> presenter.trackDashboardAnalytics() - R.id.navigation_relationships -> presenter.trackDashboardRelationships() - R.id.navigation_notes -> presenter.trackDashboardNotes() + private fun setUpNavigationBar() { + binding.navigationBar.setContent { + DHIS2Theme { + val uiState by dashboardViewModel.navigationBarUIState.collectAsState() + var selectedHomeItemIndex by remember(uiState) { + mutableIntStateOf( + uiState.items.indexOfFirst { + it.id == uiState.selectedItem + }, + ) + } + + NavigationBar( + items = uiState.items, + selectedItemIndex = selectedHomeItemIndex, + ) { itemId -> + selectedHomeItemIndex = uiState.items.indexOfFirst { it.id == itemId } + dashboardViewModel.onNavigationItemSelected(itemId) + } + + uiState.selectedItem?.let { + navigateToFragment(it) + } + } + } + } + + private fun navigateToFragment(item: TEIDashboardItems) { + val fragment = when (item) { + TEIDashboardItems.DETAILS -> newInstance( + programUid, + teiUid, + enrollmentUid, + ) + + TEIDashboardItems.ANALYTICS -> { + presenter.trackDashboardAnalytics() + IndicatorsFragment().apply { + arguments = Bundle().apply { + putString(VISUALIZATION_TYPE, VisualizationType.TRACKER.name) } - pagerAdapter.getNavigationPagePosition(item.itemId) - .takeIf { it != NO_POSITION } - ?.let { - when { - this.isLandscape() -> binding.teiTablePager?.currentItem = it - else -> binding.teiPager?.currentItem = it - } - } } - true } + + TEIDashboardItems.RELATIONSHIPS -> { + presenter.trackDashboardRelationships() + RelationshipFragment().apply { + arguments = RelationshipFragment.withArguments( + programUid, + teiUid, + enrollmentUid, + null, + ) + } + } + + TEIDashboardItems.NOTES -> { + presenter.trackDashboardNotes() + NotesFragment.newTrackerInstance(programUid!!, teiUid!!) + } + } + + supportFragmentManager.beginTransaction() + .replace(R.id.fragmentContainer, fragment, item.name) + .commit() + + updateTopBar(item) + } + + private fun updateTopBar(item: TEIDashboardItems) { + if (item === TEIDashboardItems.RELATIONSHIPS) { + binding.relationshipIcon.visibility = View.VISIBLE } else { - binding.navigationBar.visibility = View.GONE + binding.relationshipIcon.visibility = View.GONE + } + + if (this.isPortrait()) { + if (item == TEIDashboardItems.DETAILS && programUid != null) { + binding.toolbarTitle?.visibility = View.GONE + binding.editButton?.visibility = View.VISIBLE + binding.syncButton.visibility = View.GONE + } else { + binding.toolbarTitle?.visibility = View.VISIBLE + binding.editButton?.visibility = View.GONE + binding.syncButton.visibility = View.VISIBLE + } } } override fun onResume() { super.onResume() - if (currentOrientation != -1) { - val nextOrientation = if (this.isLandscape()) 1 else 0 - if (currentOrientation != nextOrientation && adapter != null) { - adapter?.notifyDataSetChanged() - } - } - currentOrientation = if (this.isLandscape()) 1 else 0 - if (adapter == null) { - restoreAdapter(programUid) + if (sessionManagerServiceImpl.isUserLoggedIn()) { + currentOrientation = if (this.isLandscape()) 1 else 0 + presenter.refreshTabCounters() + dashboardViewModel.updateDashboard() } - presenter.refreshTabCounters() } override fun onPause() { @@ -272,7 +440,7 @@ class TeiDashboardMobileActivity : super.onDestroy() } - fun openSyncDialog() { + override fun openSyncDialog() { enrollmentUid?.let { enrollmentUid -> SyncStatusDialog.Builder() .withContext(this, null) @@ -307,73 +475,6 @@ class TeiDashboardMobileActivity : } } - private fun setViewpagerAdapter() { - adapter = teiUid?.let { - DashboardPagerAdapter( - this, - programUid, - it, - enrollmentUid, - pageConfigurator.displayAnalytics(), - pageConfigurator.displayRelationships(), - ) - } - when { - this.isPortrait() -> setPortraitPager() - else -> setLandscapePager() - } - } - - private fun setPortraitPager() { - binding.teiPager?.adapter = null - binding.teiPager?.isUserInputEnabled = false - binding.teiPager?.adapter = adapter - binding.teiPager?.registerOnPageChangeCallback( - object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - showLoadingProgress(false) - val pageType = adapter?.pageType(position) - if (pageType === DashboardPageType.RELATIONSHIPS) { - binding.relationshipMapIcon.visibility = View.VISIBLE - } else { - binding.relationshipMapIcon.visibility = View.GONE - } - if (pageType == DashboardPageType.TEI_DETAIL && programUid != null) { - binding.toolbarTitle.visibility = View.GONE - binding.editButton.visibility = View.VISIBLE - binding.syncButton.visibility = View.GONE - } else { - binding.toolbarTitle.visibility = View.VISIBLE - binding.editButton.visibility = View.GONE - binding.syncButton.visibility = View.VISIBLE - } - binding.navigationBar.selectItemAt(position) - } - }, - ) - if (fromRelationship) { - binding.teiPager?.setCurrentItem(2, false) - } - } - - private fun setLandscapePager() { - binding.teiTablePager?.adapter = adapter - binding.teiTablePager?.isUserInputEnabled = false - binding.teiTablePager?.registerOnPageChangeCallback( - object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - showLoadingProgress(false) - binding.relationshipMapIcon.visibility = when (adapter?.pageType(position)) { - DashboardPageType.RELATIONSHIPS -> View.VISIBLE - else -> View.GONE - } - binding.navigationBar.selectItemAt(position) - } - }, - ) - if (fromRelationship) binding.teiTablePager?.setCurrentItem(1, false) - } - private fun showLoadingProgress(showProgress: Boolean) { if (showProgress) { binding.toolbarProgress.show() @@ -402,16 +503,9 @@ class TeiDashboardMobileActivity : binding.executePendingBindings() enrollmentUid = dashboardModel.currentEnrollment.uid() if (this.isLandscape()) { - if (binding.teiTablePager?.adapter == null) { - setViewpagerAdapter() - supportFragmentManager.beginTransaction() - .replace(R.id.tei_main_view, newInstance(programUid, teiUid, enrollmentUid)) - .commitAllowingStateLoss() - } - } else { - if (binding.teiPager?.adapter == null) { - setViewpagerAdapter() - } + supportFragmentManager.beginTransaction() + .replace(R.id.tei_main_view, newInstance(programUid, teiUid, enrollmentUid)) + .commitAllowingStateLoss() } val enrollmentStatus = dashboardModel.currentEnrollment.status() == EnrollmentStatus.ACTIVE @@ -424,7 +518,6 @@ class TeiDashboardMobileActivity : } override fun restoreAdapter(programUid: String?) { - adapter = null this.programUid = programUid } @@ -432,7 +525,7 @@ class TeiDashboardMobileActivity : finish() } - fun handleEnrollmentDeletion(hasMoreEnrollments: Boolean) { + private fun handleEnrollmentDeletion(hasMoreEnrollments: Boolean) { if (hasMoreEnrollments) { startActivity(intent(this, teiUid, null, null)) finish() @@ -470,14 +563,13 @@ class TeiDashboardMobileActivity : ) binding.title = title binding.executePendingBindings() - setViewpagerAdapter() - binding.relationshipMapIcon.visibility = View.GONE if (this.isLandscape()) { supportFragmentManager.beginTransaction() .replace(R.id.tei_main_view, newInstance(programUid, teiUid, enrollmentUid)) .commitAllowingStateLoss() + } else { + navigateToFragment(TEIDashboardItems.DETAILS) } - showLoadingProgress(false) } override fun goToEnrollmentList() { @@ -502,20 +594,19 @@ class TeiDashboardMobileActivity : if (this.isLandscape()) { setTutorial() } else { - if (binding.teiPager?.currentItem == 0) setTutorial() else showToast(getString(R.string.no_intructions)) + if (binding.editButton?.visibility == View.VISIBLE) { + setTutorial() + } else { + showToast(getString(R.string.no_intructions)) + } } } - fun toRelationships() { - fromRelationship = true - } - private fun setProgramColor(programUid: String?) { themeManager.getThemePrimaryColor( programUid, { programColor: Int -> binding.toolbar.setBackgroundColor(programColor) - binding.navigationBar.setIconsColor(programColor) }, ) { themeColorRes: Int -> binding.toolbar.setBackgroundColor( @@ -524,12 +615,6 @@ class TeiDashboardMobileActivity : themeColorRes, ), ) - binding.navigationBar.setIconsColor( - ContextCompat.getColor( - this@TeiDashboardMobileActivity, - themeColorRes, - ), - ) } binding.executePendingBindings() setTheme(themeManager.getProgramTheme()) @@ -544,100 +629,8 @@ class TeiDashboardMobileActivity : } } - override fun showMoreOptions(view: View?) { - val menu: Int = if (enrollmentUid == null) { - R.menu.dashboard_tei_menu - } else if (dashboardViewModel.groupByStage.value != false) { - R.menu.dashboard_menu_group - } else { - R.menu.dashboard_menu - } - AppMenuHelper.Builder() - .anchor(view!!) - .menu(this, menu) - .onMenuInflated { popupMenu: PopupMenu -> - val deleteTeiItem = popupMenu.menu.findItem(R.id.deleteTei) - val showDeleteTeiItem = presenter.checkIfTEICanBeDeleted() - if (showDeleteTeiItem) { - deleteTeiItem.isVisible = true - deleteTeiItem.title = - String.format(deleteTeiItem.title.toString(), presenter.teType) - } else { - deleteTeiItem.isVisible = false - } - - if (enrollmentUid != null) { - popupMenu.menu.findItem(R.id.deleteEnrollment).let { deleteEnrollmentItem -> - deleteEnrollmentItem.isVisible = - presenter.checkIfEnrollmentCanBeDeleted(enrollmentUid) - deleteEnrollmentItem.title = resourceManager.formatWithEnrollmentLabel( - programUid!!, - R.string.dashboard_menu_delete_enrollment_V2, - 1, - ) - } - - val status = presenter.getEnrollmentStatus(enrollmentUid) - if (status == EnrollmentStatus.COMPLETED) { - popupMenu.menu.findItem(R.id.complete).isVisible = false - } else if (status == EnrollmentStatus.CANCELLED) { - popupMenu.menu.findItem(R.id.deactivate).isVisible = false - } else { - popupMenu.menu.findItem(R.id.activate).isVisible = false - } - if (dashboardViewModel.showFollowUpBar.value) { - popupMenu.menu.findItem(R.id.markForFollowUp).isVisible = false - } - } - popupMenu.menu.findItem(R.id.programSelector).let { programSelectorItem -> - programSelectorItem.title = resourceManager.formatWithEnrollmentLabel( - programUid ?: "", - R.string.program_selector_V2, - 3, - ) - } - Unit - } - .onMenuItemClicked { itemId: Int? -> - when (itemId) { - R.id.showHelp -> { - analyticsHelper.setEvent(SHOW_HELP, CLICK, SHOW_HELP) - showTutorial(true) - } - - R.id.markForFollowUp -> dashboardViewModel.onFollowUp() - R.id.deleteTei -> showDeleteTEIConfirmationDialog() - R.id.deleteEnrollment -> showRemoveEnrollmentConfirmationDialog() - R.id.programSelector -> presenter.onEnrollmentSelectorClick() - R.id.groupEvents -> dashboardViewModel.setGrouping(true) - R.id.showTimeline -> dashboardViewModel.setGrouping(false) - R.id.complete -> { - dashboardViewModel.updateEnrollmentStatus( - EnrollmentStatus.COMPLETED, - ) - } - - R.id.activate -> dashboardViewModel.updateEnrollmentStatus( - EnrollmentStatus.ACTIVE, - ) - - R.id.deactivate -> dashboardViewModel.updateEnrollmentStatus( - EnrollmentStatus.CANCELLED, - ) - - R.id.share -> startQRActivity() - } - true - } - .build().show() - } - override fun updateNoteBadge(numberOfNotes: Int) { - binding.navigationBar.updateBadge(R.id.navigation_notes, numberOfNotes) - } - - fun observeFilters(): LiveData? { - return null + dashboardViewModel.updateNoteCounter(numberOfNotes) } override fun relationshipMap(): LiveData { @@ -663,6 +656,50 @@ class TeiDashboardMobileActivity : } } + override fun showOrgUnitSelector( + programUid: String, + ) { + val ownerOrgUnit = dashboardViewModel.dashboardModel.value?.ownerOrgUnit + OUTreeFragment.Builder() + .singleSelection() + .withModel( + OUTreeModel( + title = getString( + R.string.transfer_tei_org_sheet_title, + presenter.teType.lowercase(), + ), + subtitle = getString( + R.string.transfer_tei_org_sheet_description, + ownerOrgUnit?.displayName(), + ), + headerAlignment = TextAlign.Start, + showClearButton = false, + doneButtonText = getString(R.string.transfer), + doneButtonIcon = Icons.Outlined.MoveDown, + hideOrgUnits = ownerOrgUnit?.let { listOf(it) }, + ), + ) + .orgUnitScope( + OrgUnitSelectorScope.ProgramSearchScope(programUid), + ) + .onSelection { selectedOrgUnits -> + if (selectedOrgUnits.isNotEmpty()) { + dashboardViewModel.transferTei( + selectedOrgUnits.first().uid(), + ) { + val contextView = findViewById(R.id.navigationBar) + Snackbar.make( + contextView, + R.string.successfully_transferred, + Snackbar.LENGTH_SHORT, + ).show() + } + } + } + .build() + .show(supportFragmentManager, "ORG_UNIT_DIALOG") + } + private fun showDeleteTEIConfirmationDialog() { DeleteBottomSheetDialog( title = getString(R.string.delete_tei_dialog_title).format(presenter.teType), @@ -719,6 +756,39 @@ class TeiDashboardMobileActivity : startActivity(intent) } + override fun updateRelationshipsTopBarIconState(topBarIconState: RelationshipTopBarIconState) { + dashboardViewModel.updateRelationshipsTopBarIconState(topBarIconState) + } + + override fun finishActivity() { + finish() + } + + override fun restoreAdapter(programUid: String, teiUid: String, enrollmentUid: String) { + startActivity( + intent( + this, + teiUid, + programUid, + enrollmentUid, + ), + ) + } + + override fun executeOnUIThread() { + activity.runOnUiThread { + showDescription(getString(R.string.error_applying_rule_effects)) + } + } + + override fun getContext(): Context { + return this + } + + override fun activityTeiUid(): String? { + return teiUid + } + companion object { private const val TEI_SYNC = "SYNC_TEI" @@ -736,4 +806,70 @@ class TeiDashboardMobileActivity : return intent } } + + private fun setupMoreOptionsMenu() { + binding.moreOptions.setContent { + val menuItems = getEnrollmentMenuList( + enrollmentUid = enrollmentUid, + resourceManager = resourceManager, + presenter = presenter, + dashboardViewModel = dashboardViewModel, + ) + + var expanded by remember { mutableStateOf(false) } + + MoreOptionsWithDropDownMenuButton( + menuItems, + expanded, + onMenuToggle = { expanded = it }, + ) { itemId -> + when (itemId) { + EnrollmentMenuItem.SYNC -> openSyncDialog() + EnrollmentMenuItem.TRANSFER -> presenter.onTransferClick() + EnrollmentMenuItem.FOLLOW_UP -> dashboardViewModel.onFollowUp() + EnrollmentMenuItem.GROUP_BY_STAGE -> dashboardViewModel.setGrouping(true) + EnrollmentMenuItem.VIEW_TIMELINE -> dashboardViewModel.setGrouping(false) + EnrollmentMenuItem.HELP -> { + analyticsHelper.setEvent(SHOW_HELP, CLICK, SHOW_HELP) + showTutorial(true) + } + + EnrollmentMenuItem.ENROLLMENTS -> presenter.onEnrollmentSelectorClick() + EnrollmentMenuItem.SHARE -> startQRActivity() + EnrollmentMenuItem.ACTIVATE -> dashboardViewModel.updateEnrollmentStatus( + EnrollmentStatus.ACTIVE, + ) + + EnrollmentMenuItem.DEACTIVATE -> dashboardViewModel.updateEnrollmentStatus( + EnrollmentStatus.CANCELLED, + ) + + EnrollmentMenuItem.COMPLETE -> { + dashboardViewModel.updateEnrollmentStatus( + EnrollmentStatus.COMPLETED, + ) + } + + EnrollmentMenuItem.DELETE -> showDeleteTEIConfirmationDialog() + EnrollmentMenuItem.REMOVE -> showRemoveEnrollmentConfirmationDialog() + } + } + } + } +} + +enum class EnrollmentMenuItem { + SYNC, + TRANSFER, + FOLLOW_UP, + GROUP_BY_STAGE, + VIEW_TIMELINE, + HELP, + ENROLLMENTS, + SHARE, + ACTIVATE, + DEACTIVATE, + COMPLETE, + DELETE, + REMOVE, } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt index 617c21cfc2..6e9afede90 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardModule.kt @@ -6,14 +6,17 @@ import dhis2.org.analytics.charts.Charts import org.dhis2.commons.di.dagger.PerActivity import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.prefs.PreferenceProvider +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider -import org.dhis2.data.forms.EnrollmentFormRepository -import org.dhis2.data.forms.FormRepository -import org.dhis2.form.data.RulesRepository +import org.dhis2.form.data.metadata.EnrollmentConfiguration +import org.dhis2.form.ui.provider.FormResultDialogProvider +import org.dhis2.form.ui.provider.FormResultDialogResourcesProvider import org.dhis2.mobileProgramRules.EvaluationType import org.dhis2.mobileProgramRules.RuleEngineHelper +import org.dhis2.usescases.enrollment.DateEditionWarningHandler import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.customviews.navigationbar.NavigationPageConfigurator import org.hisp.dhis.android.core.D2 @@ -52,6 +55,33 @@ class TeiDashboardModule( ) } + @Provides + @PerActivity + fun provideEnrollmentConfiguration( + d2: D2, + metadataIconProvider: MetadataIconProvider, + ) = enrollmentUid?.let { EnrollmentConfiguration(d2, it, metadataIconProvider) } + + @Provides + @PerActivity + fun provideDateEditionWarningHandler( + enrollmentConfiguration: EnrollmentConfiguration?, + eventResourcesProvider: EventResourcesProvider, + ) = DateEditionWarningHandler( + enrollmentConfiguration, + eventResourcesProvider, + ) + + @Provides + @PerActivity + fun provideResultDialogProvider( + resourceManager: ResourceManager, + ): FormResultDialogProvider { + return FormResultDialogProvider( + FormResultDialogResourcesProvider(resourceManager), + ) + } + @Provides @PerActivity fun dashboardRepository( @@ -73,26 +103,6 @@ class TeiDashboardModule( ) } - @Provides - @PerActivity - fun rulesRepository(d2: D2): RulesRepository { - return RulesRepository(d2) - } - - @Provides - @PerActivity - fun formRepository( - rulesRepository: RulesRepository, - d2: D2, - ): FormRepository { - val enrollmentUidToUse = enrollmentUid ?: "" - return EnrollmentFormRepository( - rulesRepository, - enrollmentUidToUse, - d2, - ) - } - @Provides @PerActivity fun ruleEngineRepository( @@ -125,7 +135,15 @@ class TeiDashboardModule( repository: DashboardRepository, analyticsHelper: AnalyticsHelper, dispatcher: DispatcherProvider, + pageConfigurator: NavigationPageConfigurator, + resourcesManager: ResourceManager, ): DashboardViewModelFactory { - return DashboardViewModelFactory(repository, analyticsHelper, dispatcher) + return DashboardViewModelFactory( + repository, + analyticsHelper, + dispatcher, + pageConfigurator, + resourcesManager, + ) } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenter.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenter.java index 3c93f4d6c1..8a9460bfda 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenter.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/TeiDashboardPresenter.java @@ -192,4 +192,14 @@ public void updateEnrollmentStatus(String enrollmentUid, EnrollmentStatus status }, Timber::e) ); } + + @Override + public void onTransferClick() { + view.showOrgUnitSelector(programUid); + } + + @Override + public boolean hasWriteAccess() { + return dashboardRepository.enrollmentHasWriteAccess(); + } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/adapters/DashboardPagerAdapter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/adapters/DashboardPagerAdapter.kt deleted file mode 100644 index 62a32c6ece..0000000000 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/adapters/DashboardPagerAdapter.kt +++ /dev/null @@ -1,124 +0,0 @@ -package org.dhis2.usescases.teiDashboard.adapters - -import android.os.Bundle -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.viewpager2.adapter.FragmentStateAdapter -import org.dhis2.R -import org.dhis2.usescases.notes.NotesFragment -import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.IndicatorsFragment -import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.VISUALIZATION_TYPE -import org.dhis2.usescases.teiDashboard.dashboardfragments.indicators.VisualizationType -import org.dhis2.usescases.teiDashboard.dashboardfragments.relationships.RelationshipFragment -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataFragment -import org.dhis2.utils.isLandscape - -class DashboardPagerAdapter( - private val fragmentActivity: FragmentActivity, - private val currentProgram: String?, - private val teiUid: String, - private val enrollmentUid: String?, - private val displayAnalyticScreen: Boolean = true, - private val displayRelationshipScreen: Boolean, -) : FragmentStateAdapter(fragmentActivity) { - - enum class DashboardPageType { - TEI_DETAIL, ANALYTICS, RELATIONSHIPS, NOTES - } - - private var indicatorsFragment: IndicatorsFragment? = null - private var relationshipFragment: RelationshipFragment? = null - private val landscapePages: List - private val portraitPages: List - - init { - landscapePages = mutableListOf().apply { - if (displayAnalyticScreen) add(DashboardPageType.ANALYTICS) - if (displayRelationshipScreen) add(DashboardPageType.RELATIONSHIPS) - if (currentProgram != null) add(DashboardPageType.NOTES) - } - portraitPages = mutableListOf().apply { - add(DashboardPageType.TEI_DETAIL) - if (displayAnalyticScreen) add(DashboardPageType.ANALYTICS) - if (displayRelationshipScreen) add(DashboardPageType.RELATIONSHIPS) - if (currentProgram != null) add(DashboardPageType.NOTES) - } - } - - override fun createFragment(position: Int): Fragment { - return createFragmentForPage( - if (fragmentActivity.isLandscape()) { - landscapePages[position] - } else { - portraitPages[position] - }, - ) - } - - private fun createFragmentForPage(pageType: DashboardPageType): Fragment { - return when (pageType) { - DashboardPageType.TEI_DETAIL -> TEIDataFragment.newInstance( - currentProgram, - teiUid, - enrollmentUid, - ) - DashboardPageType.ANALYTICS -> { - if (indicatorsFragment == null) { - indicatorsFragment = IndicatorsFragment().apply { - arguments = Bundle().apply { - putString(VISUALIZATION_TYPE, VisualizationType.TRACKER.name) - } - } - } - indicatorsFragment!! - } - DashboardPageType.RELATIONSHIPS -> { - if (relationshipFragment == null) { - relationshipFragment = RelationshipFragment().apply { - arguments = RelationshipFragment.withArguments( - currentProgram, - teiUid, - enrollmentUid, - null, - ) - } - } - relationshipFragment!! - } - DashboardPageType.NOTES -> NotesFragment.newTrackerInstance(currentProgram!!, teiUid) - } - } - - override fun getItemCount() = - if (fragmentActivity.isLandscape()) landscapePages.size else portraitPages.size - - fun getNavigationPagePosition(navigationId: Int): Int { - val pageType = when (navigationId) { - R.id.navigation_details -> DashboardPageType.TEI_DETAIL - R.id.navigation_analytics -> DashboardPageType.ANALYTICS - R.id.navigation_relationships -> DashboardPageType.RELATIONSHIPS - R.id.navigation_notes -> DashboardPageType.NOTES - else -> null - } - - return pageType?.let { - if (fragmentActivity.isLandscape()) { - landscapePages.indexOf(pageType) - } else { - portraitPages.indexOf(pageType) - } - } ?: NO_POSITION - } - - fun pageType(position: Int): DashboardPageType { - return if (fragmentActivity.isLandscape()) { - landscapePages[position] - } else { - portraitPages[position] - } - } - - companion object { - const val NO_POSITION = -1 - } -} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/indicators/IndicatorsFragment.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/indicators/IndicatorsFragment.kt index a80f613c06..f21235dad5 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/indicators/IndicatorsFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/indicators/IndicatorsFragment.kt @@ -141,7 +141,6 @@ class IndicatorsFragment : FragmentGlobalAbstract(), IndicatorsView { lineListingColumnId: Int?, ) { OUTreeFragment.Builder() - .showAsDialog() .withPreselectedOrgUnits( chartModel.graph.orgUnitsSelected(lineListingColumnId).toMutableList(), ) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/MapButtonObservable.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/MapButtonObservable.kt index 04ccd837db..0ed182698b 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/MapButtonObservable.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/MapButtonObservable.kt @@ -1,8 +1,10 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships import androidx.lifecycle.LiveData +import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState interface MapButtonObservable { fun relationshipMap(): LiveData fun onRelationshipMapLoaded() + fun updateRelationshipsTopBarIconState(topBarIconState: RelationshipTopBarIconState) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipAdapter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipAdapter.kt deleted file mode 100644 index a2e189f500..0000000000 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipAdapter.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import org.dhis2.R -import org.dhis2.commons.data.RelationshipViewModel -import org.dhis2.commons.resources.ColorUtils -import org.dhis2.databinding.ItemRelationshipBinding - -class RelationshipAdapter( - private val presenter: RelationshipPresenter, - private val colorUtils: ColorUtils, -) : - ListAdapter(object : - DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: RelationshipViewModel, - newItem: RelationshipViewModel, - ): Boolean { - return oldItem.relationship.uid() == newItem.relationship.uid() - } - - override fun areContentsTheSame( - oldItem: RelationshipViewModel, - newItem: RelationshipViewModel, - ): Boolean { - return oldItem == newItem - } - }) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RelationshipViewHolder { - val binding: ItemRelationshipBinding = DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.item_relationship, - parent, - false, - ) - return RelationshipViewHolder(binding, colorUtils) - } - - override fun onBindViewHolder(holder: RelationshipViewHolder, position: Int) { - holder.bind(presenter, getItem(position)) - } -} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipContracts.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipContracts.java deleted file mode 100644 index db047d3f14..0000000000 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipContracts.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships; - -import android.content.Intent; - -import org.dhis2.commons.data.RelationshipViewModel; -import org.dhis2.commons.data.tuples.Trio; -import org.dhis2.usescases.general.AbstractActivityContracts; -import org.hisp.dhis.android.core.relationship.Relationship; -import org.hisp.dhis.android.core.relationship.RelationshipType; -import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue; - -import java.util.List; - -import io.reactivex.Observable; -import io.reactivex.functions.Consumer; - -/** - * QUADRAM. Created by ppajuelo on 09/04/2019. - */ -public class RelationshipContracts { - - public interface View extends AbstractActivityContracts.View { - - Consumer> setRelationships(); - - Consumer>> setRelationshipTypes(); - - void goToAddRelationship(Intent intent); - } - - public interface Presenter extends AbstractActivityContracts.Presenter { - - void init(View view); - - void goToAddRelationship(String teiTypeToAdd); - - void deleteRelationship(Relationship relationship); - - void addRelationship(String trackEntityInstance_A, String relationshipType); - - void openDashboard(String teiUid); - - Observable> getTEIMainAttributes(String teiUid); - - String getTeiUid(); - } - -} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipFragment.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipFragment.kt index 6a1eb2f5d1..a2799ef335 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipFragment.kt @@ -2,49 +2,73 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships import android.content.Context import android.content.Intent -import android.graphics.RectF import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import com.mapbox.geojson.BoundingBox -import com.mapbox.geojson.FeatureCollection -import com.mapbox.mapboxsdk.geometry.LatLng +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.mapbox.mapboxsdk.location.permissions.PermissionsManager -import com.mapbox.mapboxsdk.maps.MapboxMap.OnMapClickListener +import com.mapbox.mapboxsdk.maps.MapView +import kotlinx.coroutines.launch import org.dhis2.R -import org.dhis2.animations.CarouselViewAnimations import org.dhis2.bindings.app -import org.dhis2.commons.data.RelationshipViewModel -import org.dhis2.commons.data.tuples.Trio -import org.dhis2.commons.locationprovider.LocationSettingLauncher.requestEnableLocationSetting +import org.dhis2.commons.bindings.launchImageDetail +import org.dhis2.commons.locationprovider.LocationSettingLauncher import org.dhis2.commons.resources.ColorUtils -import org.dhis2.databinding.FragmentRelationshipsBinding import org.dhis2.form.model.EventMode import org.dhis2.maps.ExternalMapNavigation -import org.dhis2.maps.carousel.CarouselAdapter -import org.dhis2.maps.geometry.mapper.featurecollection.MapRelationshipsToFeatureCollection +import org.dhis2.maps.camera.centerCameraOnFeatures import org.dhis2.maps.layer.MapLayerDialog +import org.dhis2.maps.location.MapLocationEngine import org.dhis2.maps.managers.RelationshipMapManager -import org.dhis2.maps.model.RelationshipUiComponentModel +import org.dhis2.maps.views.LocationIcon +import org.dhis2.maps.views.MapScreen +import org.dhis2.maps.views.OnMapClickListener +import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState +import org.dhis2.tracker.relationships.ui.DeleteRelationshipsConfirmation +import org.dhis2.tracker.relationships.ui.RelationShipsScreen +import org.dhis2.tracker.relationships.ui.RelationshipsUiState +import org.dhis2.tracker.relationships.ui.RelationshipsViewModel import org.dhis2.ui.ThemeManager +import org.dhis2.ui.avatar.AvatarProvider +import org.dhis2.ui.theme.Dhis2Theme import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.general.FragmentGlobalAbstract import org.dhis2.usescases.teiDashboard.TeiDashboardMobileActivity -import org.dhis2.usescases.teiDashboard.ui.NoRelationships import org.dhis2.utils.OnDialogClickListener -import org.dhis2.utils.dialFloatingActionButton.DialItem import org.hisp.dhis.android.core.relationship.RelationshipType +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem +import org.hisp.dhis.mobile.ui.designsystem.component.IconButton +import org.hisp.dhis.mobile.ui.designsystem.component.IconButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardDescriptionModel +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberAdditionalInfoColumnState +import org.hisp.dhis.mobile.ui.designsystem.component.state.rememberListCardState +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor import javax.inject.Inject -class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapClickListener { +class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView { @Inject lateinit var presenter: RelationshipPresenter - @Inject - lateinit var animations: CarouselViewAnimations - @Inject lateinit var mapNavigation: ExternalMapNavigation @@ -54,11 +78,11 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl @Inject lateinit var colorUtils: ColorUtils - private lateinit var binding: FragmentRelationshipsBinding - private lateinit var relationshipAdapter: RelationshipAdapter + @Inject + lateinit var relationShipsViewModel: RelationshipsViewModel + private var relationshipType: RelationshipType? = null - private lateinit var relationshipMapManager: RelationshipMapManager - private var sources: Set? = null + private var relationshipMapManager: RelationshipMapManager? = null private lateinit var mapButtonObservable: MapButtonObservable private val addRelationshipLauncher = registerForActivityResult(AddRelationshipContract()) { @@ -66,6 +90,7 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl when (it) { is RelationshipResult.Error -> { // Unused } + is RelationshipResult.Success -> { presenter.addRelationship(it.teiUidToAddAsRelationship, relationshipType!!.uid()) } @@ -78,16 +103,21 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl override fun onAttach(context: Context) { super.onAttach(context) - mapButtonObservable = context as MapButtonObservable - app().userComponent()?.plus( - RelationshipModule( - this, - programUid(), - requireArguments().getString("ARG_TEI_UID"), - requireArguments().getString("ARG_ENROLLMENT_UID"), - requireArguments().getString("ARG_EVENT_UID"), - ), - )?.inject(this) + if (context is MapButtonObservable) { + mapButtonObservable = context + app().userComponent()?.plus( + RelationshipModule( + requireContext(), + this, + programUid(), + requireArguments().getString("ARG_TEI_UID"), + requireArguments().getString("ARG_ENROLLMENT_UID"), + requireArguments().getString("ARG_EVENT_UID"), + ), + )?.inject(this) + } else { + throw ClassCastException("$context must implement MapButtonObservable") + } } override fun onCreateView( @@ -95,49 +125,224 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl container: ViewGroup?, savedInstanceState: Bundle?, ): View { - binding = - DataBindingUtil.inflate(inflater, R.layout.fragment_relationships, container, false) - relationshipAdapter = RelationshipAdapter(presenter, colorUtils) - binding.relationshipRecycler.adapter = relationshipAdapter - relationshipMapManager = RelationshipMapManager(binding.mapView) - lifecycle.addObserver(relationshipMapManager) - relationshipMapManager.onCreate(savedInstanceState) - relationshipMapManager.onMapClickListener = this - relationshipMapManager.init( - presenter.fetchMapStyles(), - ) { permissionManager -> - handleMissingPermission(permissionManager) + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + Dhis2Theme { + val showMap by mapButtonObservable.relationshipMap().observeAsState() + + val uiState by relationShipsViewModel.relationshipsUiState.collectAsState() + val relationshipSelectionState by relationShipsViewModel.relationshipSelectionState.collectAsState() + val showDeleteConfirmation by relationShipsViewModel.showDeleteConfirmation.collectAsState() + + when (showMap) { + true -> RelationshipMapScreen(savedInstanceState) + else -> RelationShipsScreen( + uiState = uiState, + relationshipSelectionState = relationshipSelectionState, + onCreateRelationshipClick = { + it.teiTypeUid?.let { teiTypeUid -> + goToRelationShip( + relationshipTypeModel = it.relationshipType, + teiTypeUid = teiTypeUid, + ) + } + }, + onRelationshipClick = { + presenter.onRelationshipClicked( + ownerType = it.ownerType, + ownerUid = it.ownerUid, + ) + }, + onRelationShipSelected = relationShipsViewModel::updateSelectedList, + ) + } + + if (showDeleteConfirmation) { + (uiState as? RelationshipsUiState.Success)?.let { state -> + DeleteRelationshipsConfirmation( + relationships = + relationshipSelectionState.selectedItems.map { selectedUid -> + state.data.first { + it.relationships.any { it.uid == selectedUid } + }.title + }, + onDelete = { + relationShipsViewModel.deleteSelectedRelationships() + }, + onDismiss = { + relationShipsViewModel.onDismissDelete() + }, + ) + } + } + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observeRelationshipTopBarIcon() + } + + private fun observeRelationshipTopBarIcon() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + relationShipsViewModel.relationshipSelectionState.collect { selectionState -> + val topBarIconState = if (selectionState.selectingMode) { + RelationshipTopBarIconState.Selecting { + relationShipsViewModel.onDeleteClick() + } + } else { + RelationshipTopBarIconState.List() + } + mapButtonObservable.updateRelationshipsTopBarIconState(topBarIconState) + } + } + } + } + + @Composable + private fun RelationshipMapScreen(savedInstanceState: Bundle?) { + val listState = rememberLazyListState() + + val mapData by presenter.relationshipMapData.observeAsState() + val items by remember { + derivedStateOf { mapData?.mapItems ?: emptyList() } } - mapButtonObservable.relationshipMap().observe(viewLifecycleOwner) { showMap -> - val mapVisibility = if (showMap) View.VISIBLE else View.GONE - val listVisibility = if (showMap) View.GONE else View.VISIBLE - binding.relationshipRecycler.visibility = listVisibility - binding.mapView.visibility = mapVisibility - binding.mapLayerButton.visibility = mapVisibility - binding.mapPositionButton.visibility = mapVisibility - binding.mapCarousel.visibility = mapVisibility - binding.dialFabLayout.setFabVisible(!showMap) + + val clickedItem by presenter.mapItemClicked.observeAsState(initial = null) + val locationState = relationshipMapManager?.locationState?.collectAsState() + + LaunchedEffect(key1 = items) { + mapData?.let { data -> + relationshipMapManager.takeIf { it?.isMapReady() == true } + ?.update(data.relationshipFeatures, data.boundingBox) + } } - binding.mapLayerButton.setOnClickListener { - val layerDialog = MapLayerDialog( - relationshipMapManager, + + LaunchedEffect(key1 = clickedItem) { + listState.takeIf { clickedItem != null }?.animateScrollToItem( + items.indexOfFirst { it.uid == clickedItem }, ) - layerDialog.show(childFragmentManager, MapLayerDialog::class.java.name) } - binding.mapPositionButton.setOnClickListener { handleMapPositionClick() } - binding.emptyRelationships.setContent { NoRelationships() } - return binding.root - } - private fun handleMapPositionClick() { - if (locationProvider.hasLocationEnabled()) { - relationshipMapManager.centerCameraOnMyPosition { permissionManager -> - permissionManager?.requestLocationPermissions( - activity, + MapScreen( + items = items, + listState = listState, + onItemScrolled = { item -> + with(relationshipMapManager) { + this?.requestMapLayerManager()?.selectFeature(null) + this?.findFeatures(item.uid) + ?.takeIf { it.isNotEmpty() }?.let { features -> + map?.centerCameraOnFeatures(features) + } + } + }, + onNavigate = { item -> + relationshipMapManager?.findFeature(item.uid)?.let { feature -> + startActivity(mapNavigation.navigateToMapIntent(feature)) + } + }, + map = { + AndroidView(factory = { context -> + val map = MapView(context) + loadMap(map, savedInstanceState) + map + }) { + } + }, + actionButtons = { + IconButton( + style = IconButtonStyle.TONAL, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_layers), + contentDescription = "", + tint = TextColor.OnPrimaryContainer, + ) + }, + ) { + relationshipMapManager?.let { + MapLayerDialog(it, programUid()) { layersVisibility -> + presenter.filterVisibleMapItems(layersVisibility) + }.show( + childFragmentManager, + MapLayerDialog::class.java.name, + ) + } + } + locationState?.let { + LocationIcon( + locationState = it.value, + onLocationButtonClicked = ::onLocationButtonClicked, + ) + } + }, + onItem = { item -> + ListCard( + modifier = Modifier.fillParentMaxWidth(), + listCardState = rememberListCardState( + title = ListCardTitleModel(text = item.title), + description = item.description?.let { + ListCardDescriptionModel( + text = it, + ) + }, + lastUpdated = item.lastUpdated, + additionalInfoColumnState = rememberAdditionalInfoColumnState( + additionalInfoList = item.additionalInfoList, + syncProgressItem = AdditionalInfoItem( + key = stringResource(id = R.string.syncing), + value = "", + ), + expandLabelText = stringResource(id = R.string.show_more), + shrinkLabelText = stringResource(id = R.string.show_less), + scrollableContent = true, + ), + ), + onCardClick = { + presenter.onMapRelationshipClicked(item.uid) + }, + listAvatar = { + AvatarProvider( + avatarProviderConfiguration = item.avatarProviderConfiguration, + onImageClick = ::launchImageDetail, + ) + }, ) + }, + ) + } + + private fun onLocationButtonClicked() { + relationshipMapManager?.onLocationButtonClicked( + locationProvider.hasLocationEnabled(), + requireActivity(), + ) + } + + private fun loadMap(mapView: MapView, savedInstanceState: Bundle?) { + relationshipMapManager = + RelationshipMapManager(mapView, MapLocationEngine(requireContext())) + relationshipMapManager?.also { + lifecycle.addObserver(it) + it.onCreate(savedInstanceState) + it.onMapClickListener = OnMapClickListener( + it, + presenter::onFeatureClicked, + ) + it.init( + presenter.fetchMapStyles(), + onInitializationFinished = { + presenter.relationshipMapData.value?.let { data -> + relationshipMapManager?.update(data.relationshipFeatures, data.boundingBox) + } + }, + ) { permissionManager -> + handleMissingPermission(permissionManager) } - } else { - requestEnableLocationSetting(requireContext()) } } @@ -145,13 +350,13 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl if (locationProvider.hasLocationEnabled()) { permissionManager?.requestLocationPermissions(activity) } else { - requestEnableLocationSetting(requireContext()) + LocationSettingLauncher.requestEnableLocationSetting(requireContext()) } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - relationshipMapManager.onSaveInstanceState(outState) + relationshipMapManager?.onSaveInstanceState(outState) } @Deprecated("Deprecated in Java") @@ -161,10 +366,10 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl grantResults: IntArray, ) { if ( - binding.mapView.visibility == View.VISIBLE && - relationshipMapManager.permissionsManager != null + mapButtonObservable.relationshipMap().value == true && + relationshipMapManager?.permissionsManager != null ) { - relationshipMapManager.permissionsManager?.onRequestPermissionsResult( + relationshipMapManager?.permissionsManager?.onRequestPermissionsResult( requestCode, permissions, grantResults, @@ -174,10 +379,8 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl override fun onResume() { super.onResume() - if (binding.mapView.visibility == View.VISIBLE) { - animations.initMapLoading(binding.mapCarousel) - } presenter.init() + relationShipsViewModel.refreshRelationships() } override fun onPause() { @@ -187,22 +390,10 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl override fun onLowMemory() { super.onLowMemory() - relationshipMapManager.onLowMemory() - } - - override fun setRelationships(relationships: List) { - relationshipAdapter.submitList(relationships) - if (relationships.isNotEmpty()) { - binding.emptyRelationships.visibility = View.GONE - } else { - binding.emptyRelationships.visibility = View.VISIBLE - } + relationshipMapManager?.onLowMemory() } override fun goToAddRelationship(teiUid: String, teiTypeUidToAdd: String) { - if (activity is TeiDashboardMobileActivity) { - (activity as TeiDashboardMobileActivity?)?.toRelationships() - } addRelationshipLauncher.launch( RelationshipInput( teiUid, @@ -211,26 +402,6 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl ) } - override fun initFab(relationshipTypes: MutableList>) { - val items: MutableList = ArrayList() - var dialItemIndex = 1 - for (trio in relationshipTypes) { - val relationshipType = trio.val0() - val resource = trio.val2()!! - items.add( - DialItem( - dialItemIndex++, - relationshipType!!.displayName()!!, - resource, - ), - ) - } - binding.dialFabLayout.addDialItems(items) { clickedId: Int -> - val selectedRelationShip = relationshipTypes[clickedId - 1] - goToRelationShip(selectedRelationShip.val0()!!, selectedRelationShip.val1()!!) - } - } - private fun goToRelationShip(relationshipTypeModel: RelationshipType, teiTypeUid: String) { relationshipType = relationshipTypeModel presenter.goToAddRelationship(teiTypeUid, relationshipType!!) @@ -300,67 +471,6 @@ class RelationshipFragment : FragmentGlobalAbstract(), RelationshipView, OnMapCl ) } - override fun setFeatureCollection( - currentTei: String?, - relationshipsMapModels: List, - map: Pair, BoundingBox>, - ) { - relationshipMapManager.update(map.first, map.second) - sources = map.first.keys - val carouselAdapter = CarouselAdapter.Builder() - .addCurrentTei(currentTei) - .addOnDeleteRelationshipListener { relationshipUid -> - if (binding.mapCarousel.carouselEnabled) { - presenter.deleteRelationship(relationshipUid) - } - true - } - .addOnRelationshipClickListener { teiUid, ownerType -> - if (binding.mapCarousel.carouselEnabled) { - presenter.onRelationshipClicked(ownerType, teiUid) - } - true - } - .addOnNavigateClickListener { uid -> - val feature = relationshipMapManager.findFeature(uid) - if (feature != null) { - startActivity(mapNavigation.navigateToMapIntent(feature)) - } - } - .build() - binding.mapCarousel.setAdapter(carouselAdapter) - binding.mapCarousel.attachToMapManager(relationshipMapManager) - carouselAdapter.addItems(relationshipsMapModels) - animations.endMapLoading(binding.mapCarousel) - mapButtonObservable.onRelationshipMapLoaded() - } - - override fun onMapClick(point: LatLng): Boolean { - val pointf = relationshipMapManager.map!!.projection.toScreenLocation(point) - val rectF = RectF(pointf.x - 10, pointf.y - 10, pointf.x + 10, pointf.y + 10) - sources?.forEach { sourceId -> - val lineLayerId = "RELATIONSHIP_LINE_LAYER_ID_$sourceId" - val pointLayerId = "RELATIONSHIP_LINE_LAYER_ID_$sourceId" - val features = relationshipMapManager.map - ?.queryRenderedFeatures(rectF, lineLayerId, pointLayerId) - if (features?.isNotEmpty() == true) { - relationshipMapManager.mapLayerManager.selectFeature(null) - val selectedFeature = relationshipMapManager.findFeature( - sourceId, - MapRelationshipsToFeatureCollection.RELATIONSHIP_UID, - features[0].getStringProperty( - MapRelationshipsToFeatureCollection.RELATIONSHIP_UID, - ), - ) - relationshipMapManager.mapLayerManager.getLayer(sourceId, true) - ?.setSelectedItem(selectedFeature) - binding.mapCarousel.scrollToFeature(features[0]) - return true - } - } - return false - } - companion object { const val TEI_A_UID = "TEI_A_UID" diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipMapData.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipMapData.kt new file mode 100644 index 0000000000..1948f412ea --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipMapData.kt @@ -0,0 +1,11 @@ +package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships + +import com.mapbox.geojson.BoundingBox +import com.mapbox.geojson.FeatureCollection +import org.dhis2.maps.model.MapItemModel + +data class RelationshipMapData( + val mapItems: List, + val relationshipFeatures: Map, + val boundingBox: BoundingBox, +) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipMapsRepository.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipMapsRepository.kt new file mode 100644 index 0000000000..7672d0c236 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipMapsRepository.kt @@ -0,0 +1,16 @@ +package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships + +import org.dhis2.maps.model.MapItemModel +import org.dhis2.maps.model.RelatedInfo +import org.dhis2.tracker.relationships.model.RelationshipOwnerType +import org.hisp.dhis.android.core.relationship.Relationship + +interface RelationshipMapsRepository { + fun getEventProgram(eventUid: String?): String + fun getRelatedInfo( + ownerType: RelationshipOwnerType, + ownerUid: String, + ): RelatedInfo? + + fun addRelationshipInfo(mapItem: MapItemModel, relationship: Relationship): MapItemModel +} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipMapsRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipMapsRepositoryImpl.kt new file mode 100644 index 0000000000..d30cbdf150 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipMapsRepositoryImpl.kt @@ -0,0 +1,79 @@ +package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships + +import org.dhis2.commons.bindings.event +import org.dhis2.commons.bindings.program +import org.dhis2.maps.model.MapItemModel +import org.dhis2.maps.model.RelatedInfo +import org.dhis2.tracker.relationships.model.RelationshipOwnerType +import org.dhis2.usescases.events.EventInfoProvider +import org.dhis2.usescases.tracker.TrackedEntityInstanceInfoProvider +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.relationship.Relationship + +class RelationshipMapsRepositoryImpl( + private val d2: D2, + private val config: RelationshipConfiguration, + private val trackedEntityInfoProvider: TrackedEntityInstanceInfoProvider, + private val eventInfoProvider: EventInfoProvider, +) : RelationshipMapsRepository { + + override fun getRelatedInfo( + ownerType: RelationshipOwnerType, + ownerUid: String, + ): RelatedInfo? { + return when (ownerType) { + RelationshipOwnerType.EVENT -> { + val event = d2.event(ownerUid) + return event?.let { + eventInfoProvider.getRelatedInfo(it) + } + } + + RelationshipOwnerType.TEI -> { + val tei = d2.trackedEntityModule().trackedEntityInstances() + .uid(ownerUid).blockingGet() + + val searchItem = d2.trackedEntityModule().trackedEntitySearch() + .byTrackedEntityType().eq(tei?.trackedEntityType()) + .uid(ownerUid) + .blockingGet() + + searchItem?.let { + if (config is TrackerRelationshipConfiguration) { + val programUid = d2.enrollmentModule().enrollments() + .uid(config.enrollmentUid) + .blockingGet() + ?.program() + trackedEntityInfoProvider.getRelatedInfo( + searchItem = searchItem, + selectedProgram = programUid?.let { d2.program(programUid) }, + ) + } else { + null + } + } + } + } + } + + override fun addRelationshipInfo( + mapItem: MapItemModel, + relationship: Relationship, + ): MapItemModel { + return trackedEntityInfoProvider.updateRelationshipInfo(mapItem, relationship) + } + + override fun getEventProgram(eventUid: String?): String { + return d2.eventModule().events().uid(eventUid).blockingGet()?.program() ?: "" + } +} + +sealed class RelationshipConfiguration +data class TrackerRelationshipConfiguration( + val enrollmentUid: String, + val teiUid: String, +) : RelationshipConfiguration() + +data class EventRelationshipConfiguration( + val eventUid: String, +) : RelationshipConfiguration() diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipModule.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipModule.java index 03b64fe698..119258cb81 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipModule.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipModule.java @@ -1,18 +1,30 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships; -import org.dhis2.animations.CarouselViewAnimations; +import android.content.Context; + +import org.dhis2.commons.data.ProgramConfigurationRepository; +import org.dhis2.commons.date.DateLabelProvider; import org.dhis2.commons.di.dagger.PerFragment; import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.commons.resources.ResourceManager; -import org.dhis2.commons.schedulers.SchedulerProvider; +import org.dhis2.commons.viewmodel.DispatcherProvider; import org.dhis2.maps.geometry.bound.GetBoundingBox; import org.dhis2.maps.geometry.line.MapLineRelationshipToFeature; import org.dhis2.maps.geometry.mapper.featurecollection.MapRelationshipsToFeatureCollection; import org.dhis2.maps.geometry.point.MapPointToFeature; import org.dhis2.maps.geometry.polygon.MapPolygonToFeature; -import org.dhis2.maps.mapper.MapRelationshipToRelationshipMapModel; import org.dhis2.maps.usecases.MapStyleConfiguration; +import org.dhis2.tracker.data.ProfilePictureProvider; +import org.dhis2.tracker.relationships.data.EventRelationshipsRepository; +import org.dhis2.tracker.relationships.data.RelationshipsRepository; +import org.dhis2.tracker.relationships.data.TrackerRelationshipsRepository; +import org.dhis2.tracker.relationships.domain.DeleteRelationships; +import org.dhis2.tracker.relationships.domain.GetRelationshipsByType; +import org.dhis2.tracker.relationships.ui.RelationshipsViewModel; +import org.dhis2.tracker.ui.AvatarProvider; +import org.dhis2.usescases.events.EventInfoProvider; import org.dhis2.usescases.teiDashboard.TeiAttributesProvider; +import org.dhis2.usescases.tracker.TrackedEntityInstanceInfoProvider; import org.dhis2.utils.analytics.AnalyticsHelper; import org.hisp.dhis.android.core.D2; @@ -27,12 +39,16 @@ public class RelationshipModule { private final String enrollmentUid; private final String eventUid; private final RelationshipView view; + private final Context moduleContext; - public RelationshipModule(RelationshipView view, - String programUid, - String teiUid, - String enrollmentUid, - String eventUid) { + public RelationshipModule( + Context moduleContext, + RelationshipView view, + String programUid, + String teiUid, + String enrollmentUid, + String eventUid) { + this.moduleContext = moduleContext; this.programUid = programUid; this.teiUid = teiUid; this.enrollmentUid = enrollmentUid; @@ -43,36 +59,69 @@ public RelationshipModule(RelationshipView view, @Provides @PerFragment RelationshipPresenter providesPresenter(D2 d2, - RelationshipRepository relationshipRepository, - SchedulerProvider schedulerProvider, + RelationshipMapsRepository relationshipMapsRepository, AnalyticsHelper analyticsHelper, - MapRelationshipsToFeatureCollection mapRelationshipsToFeatureCollection) { - return new RelationshipPresenter(view, + MapRelationshipsToFeatureCollection mapRelationshipsToFeatureCollection, + RelationshipsRepository relationshipsRepository, + AvatarProvider avatarProvider, + DateLabelProvider dateLabelProvider, + DispatcherProvider dispatcherProvider, + ProgramConfigurationRepository programConfigurationRepository + ) { + return new RelationshipPresenter( + view, d2, - programUid, teiUid, eventUid, - relationshipRepository, - schedulerProvider, + relationshipMapsRepository, analyticsHelper, - new MapRelationshipToRelationshipMapModel(), mapRelationshipsToFeatureCollection, - new MapStyleConfiguration(d2)); + new MapStyleConfiguration(d2, programUid, programConfigurationRepository), + relationshipsRepository, + avatarProvider, + dateLabelProvider, + dispatcherProvider + ); + } + + @Provides + @PerFragment + ProgramConfigurationRepository providesProgramConfigurationRepository(D2 d2) { + return new ProgramConfigurationRepository(d2); } @Provides @PerFragment - RelationshipRepository providesRepository(D2 d2, - ResourceManager resourceManager, - TeiAttributesProvider attributesProvider, - MetadataIconProvider metadataIconProvider) { + RelationshipMapsRepository providesRepository( + D2 d2, + ResourceManager resourceManager, + MetadataIconProvider metadataIconProvider, + DateLabelProvider dateLabelProvider + ) { RelationshipConfiguration config; if (teiUid != null) { config = new TrackerRelationshipConfiguration(enrollmentUid, teiUid); } else { config = new EventRelationshipConfiguration(eventUid); } - return new RelationshipRepositoryImpl(d2, config, resourceManager, attributesProvider, metadataIconProvider); + ProfilePictureProvider profilePictureProvider = new ProfilePictureProvider(d2); + return new RelationshipMapsRepositoryImpl( + d2, + config, + new TrackedEntityInstanceInfoProvider( + d2, + profilePictureProvider, + dateLabelProvider, + metadataIconProvider + ), + new EventInfoProvider( + d2, + resourceManager, + dateLabelProvider, + metadataIconProvider, + profilePictureProvider + ) + ); } @Provides @@ -88,13 +137,89 @@ MapRelationshipsToFeatureCollection provideMapRelationshipToFeatureCollection() @Provides @PerFragment - CarouselViewAnimations animations() { - return new CarouselViewAnimations(); + TeiAttributesProvider teiAttributesProvider(D2 d2) { + return new TeiAttributesProvider(d2); } @Provides @PerFragment - TeiAttributesProvider teiAttributesProvider(D2 d2) { - return new TeiAttributesProvider(d2); + RelationshipsViewModel provideRelationshipsViewModel( + GetRelationshipsByType getRelationshipsByType, + DeleteRelationships deleteRelationships, + DispatcherProvider dispatcherProvider + ) { + return new RelationshipsViewModel( + getRelationshipsByType, + deleteRelationships, + dispatcherProvider + ); + } + + @Provides + @PerFragment + GetRelationshipsByType provideGetRelationshipsByType( + RelationshipsRepository relationshipsRepository, + DateLabelProvider dateLabelProvider, + AvatarProvider avatarProvider + ) { + return new GetRelationshipsByType( + relationshipsRepository, + dateLabelProvider, + avatarProvider + ); + } + + @Provides + @PerFragment + DeleteRelationships provideDeleteRelationships( + RelationshipsRepository relationshipsRepository + ) { + return new DeleteRelationships(relationshipsRepository); + } + + @Provides + @PerFragment + RelationshipsRepository provideRelationshipsRepository( + D2 d2, + ResourceManager resourceManager, + ProfilePictureProvider profilePictureProvider + ) { + if (teiUid != null) { + return new TrackerRelationshipsRepository( + d2, + resourceManager, + teiUid, + enrollmentUid, + profilePictureProvider + ); + } else { + return new EventRelationshipsRepository( + d2, + resourceManager, + eventUid, + profilePictureProvider + ); + } + + } + + @Provides + @PerFragment + DateLabelProvider provideDateLabelProvider(ResourceManager resourceManager) { + return new DateLabelProvider(moduleContext, resourceManager); + } + + @Provides + @PerFragment + ProfilePictureProvider provideProfilePictureProvider(D2 d2) { + return new ProfilePictureProvider(d2); + } + + @Provides + @PerFragment + AvatarProvider provideAvatarProvider( + MetadataIconProvider metadataIconProvider + ) { + return new AvatarProvider(metadataIconProvider); } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenter.kt index 16396c09ed..b1f1db0065 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipPresenter.kt @@ -1,15 +1,27 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.mapbox.geojson.Feature import io.reactivex.disposables.CompositeDisposable import io.reactivex.processors.FlowableProcessor import io.reactivex.processors.PublishProcessor -import org.dhis2.commons.data.RelationshipOwnerType -import org.dhis2.commons.data.tuples.Trio -import org.dhis2.commons.schedulers.SchedulerProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.dhis2.commons.date.DateLabelProvider +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.maps.extensions.filterRelationshipsByLayerVisibility +import org.dhis2.maps.extensions.toStringProperty import org.dhis2.maps.geometry.mapper.featurecollection.MapRelationshipsToFeatureCollection +import org.dhis2.maps.layer.MapLayer import org.dhis2.maps.layer.basemaps.BaseMapStyle -import org.dhis2.maps.mapper.MapRelationshipToRelationshipMapModel +import org.dhis2.maps.model.MapItemModel import org.dhis2.maps.usecases.MapStyleConfiguration +import org.dhis2.tracker.relationships.data.RelationshipsRepository +import org.dhis2.tracker.relationships.model.RelationshipModel +import org.dhis2.tracker.relationships.model.RelationshipOwnerType +import org.dhis2.tracker.ui.AvatarProvider import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.analytics.CLICK import org.dhis2.utils.analytics.DELETE_RELATIONSHIP @@ -19,71 +31,99 @@ import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.relationship.RelationshipHelper import org.hisp.dhis.android.core.relationship.RelationshipType +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem import timber.log.Timber class RelationshipPresenter internal constructor( private val view: RelationshipView, private val d2: D2, - private val programUid: String?, private val teiUid: String?, private val eventUid: String?, - private val relationshipRepository: RelationshipRepository, - private val schedulerProvider: SchedulerProvider, + private val relationshipMapsRepository: RelationshipMapsRepository, private val analyticsHelper: AnalyticsHelper, - private val mapRelationshipToRelationshipMapModel: MapRelationshipToRelationshipMapModel, private val mapRelationshipsToFeatureCollection: MapRelationshipsToFeatureCollection, private val mapStyleConfig: MapStyleConfiguration, + private val relationshipsRepository: RelationshipsRepository, + private val avatarProvider: AvatarProvider, + private val dateLabelProvider: DateLabelProvider, + dispatcherProvider: DispatcherProvider, ) { + private lateinit var layersVisibility: Map + private val compositeDisposable: CompositeDisposable = CompositeDisposable() private val teiType: String? = d2.trackedEntityModule().trackedEntityInstances() .withTrackedEntityAttributeValues() .uid(teiUid) .blockingGet()?.trackedEntityType() - private val programStageUid = - d2.eventModule().events().uid(eventUid).blockingGet()?.programStage() + private var updateRelationships: FlowableProcessor = PublishProcessor.create() - var updateRelationships: FlowableProcessor = PublishProcessor.create() + private val _relationshipsModels = MutableLiveData>() + private val _relationshipMapData: MutableLiveData = MutableLiveData() + val relationshipMapData: LiveData = _relationshipMapData + private val _mapItemClicked = MutableLiveData() + val mapItemClicked: LiveData = _mapItemClicked + private val job = Job() + private val scope = CoroutineScope(dispatcherProvider.ui() + job) fun init() { - compositeDisposable.add( - updateRelationships.startWith(true) - .flatMapSingle { relationshipRepository.relationships() } - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { - view.setRelationships(it) - val relationshipModel = mapRelationshipToRelationshipMapModel.mapList(it) - view.setFeatureCollection( - teiUid, - relationshipModel, - mapRelationshipsToFeatureCollection.map(relationshipModel), + scope.launch { + relationshipsRepository.getRelationships().collect { + _relationshipsModels.postValue(it) + + val mapItems = it.map { relationship -> + val mapItem = MapItemModel( + uid = relationship.ownerUid, + avatarProviderConfiguration = avatarProvider.getAvatar( + style = relationship.ownerStyle, + profilePath = relationship.getPicturePath(), + firstAttributeValue = relationship.firstMainValue(), + ), + title = relationship.displayRelationshipName(), + description = relationship.displayDescription(), + lastUpdated = dateLabelProvider.span(relationship.displayLastUpdated()), + additionalInfoList = relationship.displayAttributes().map { + AdditionalInfoItem( + key = it.first, + value = it.second, + ) + }, + isOnline = false, + geometry = relationship.displayGeometry(), + relatedInfo = relationshipMapsRepository.getRelatedInfo( + ownerType = relationship.ownerType, + ownerUid = relationship.ownerUid, + ), + state = relationship.relationship.syncState() ?: State.SYNCED, + ) + relationshipMapsRepository.addRelationshipInfo( + mapItem, + relationship.relationship, + ) + } + + mapItems.let { + val featureCollection = mapRelationshipsToFeatureCollection.map(mapItems) + val relationshipMapData = if (::layersVisibility.isInitialized) { + RelationshipMapData( + mapItems = mapItems.filterRelationshipsByLayerVisibility( + layersVisibility, + ), + relationshipFeatures = featureCollection.first, + boundingBox = featureCollection.second, + ) + } else { + RelationshipMapData( + mapItems = mapItems, + relationshipFeatures = featureCollection.first, + boundingBox = featureCollection.second, ) - }, - { Timber.d(it) }, - ), - ) - - compositeDisposable.add( - relationshipRepository.relationshipTypes() - .map { list -> - val finalList = ArrayList>() - for (rType in list) { - val iconResId = - relationshipRepository.getTeiTypeDefaultRes(rType.second) - finalList.add(Trio.create(rType.first, rType.second, iconResId)) } - finalList + _relationshipMapData.postValue(relationshipMapData) } - .subscribeOn(schedulerProvider.io()) - .observeOn(schedulerProvider.ui()) - .subscribe( - { view.initFab(it.toMutableList()) }, - { Timber.e(it) }, - ), - ) + } + } } fun goToAddRelationship(teiTypeToAdd: String, relationshipType: RelationshipType) { @@ -197,8 +237,9 @@ class RelationshipPresenter internal constructor( when (ownerType) { RelationshipOwnerType.EVENT -> openEvent( ownerUid, - relationshipRepository.getEventProgram(ownerUid), + relationshipMapsRepository.getEventProgram(ownerUid), ) + RelationshipOwnerType.TEI -> openDashboard(ownerUid) } } @@ -206,4 +247,24 @@ class RelationshipPresenter internal constructor( fun fetchMapStyles(): List { return mapStyleConfig.fetchMapStyles() } + + fun onFeatureClicked(feature: Feature) { + feature.toStringProperty()?.let { + _mapItemClicked.postValue(it) + } + } + + fun filterVisibleMapItems(layersVisibility: Map) { + this.layersVisibility = layersVisibility + } + + fun onMapRelationshipClicked(uid: String) { + val relationship = _relationshipsModels.value?.firstOrNull { uid == it.ownerUid } + relationship?.let { + onRelationshipClicked( + ownerType = it.ownerType, + ownerUid = it.ownerUid, + ) + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepository.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepository.kt deleted file mode 100644 index fb40f54774..0000000000 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships - -import io.reactivex.Single -import org.dhis2.commons.data.RelationshipViewModel -import org.hisp.dhis.android.core.relationship.RelationshipType - -interface RelationshipRepository { - fun relationshipTypes(): Single>> - fun relationships(): Single> - fun getTeiTypeDefaultRes(teiTypeUid: String?): Int - fun getEventProgram(eventUid: String?): String -} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepositoryImpl.kt deleted file mode 100644 index 05dfccae53..0000000000 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipRepositoryImpl.kt +++ /dev/null @@ -1,497 +0,0 @@ -package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships - -import io.reactivex.Single -import org.dhis2.R -import org.dhis2.bindings.profilePicturePath -import org.dhis2.bindings.userFriendlyValue -import org.dhis2.commons.data.RelationshipDirection -import org.dhis2.commons.data.RelationshipOwnerType -import org.dhis2.commons.data.RelationshipViewModel -import org.dhis2.commons.resources.MetadataIconProvider -import org.dhis2.commons.resources.ResourceManager -import org.dhis2.ui.MetadataIconData -import org.dhis2.usescases.teiDashboard.TeiAttributesProvider -import org.hisp.dhis.android.core.D2 -import org.hisp.dhis.android.core.common.Geometry -import org.hisp.dhis.android.core.common.State -import org.hisp.dhis.android.core.event.Event -import org.hisp.dhis.android.core.organisationunit.OrganisationUnit -import org.hisp.dhis.android.core.program.ProgramType -import org.hisp.dhis.android.core.relationship.RelationshipItem -import org.hisp.dhis.android.core.relationship.RelationshipItemEvent -import org.hisp.dhis.android.core.relationship.RelationshipItemTrackedEntityInstance -import org.hisp.dhis.android.core.relationship.RelationshipType -import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance -import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor - -class RelationshipRepositoryImpl( - private val d2: D2, - private val config: RelationshipConfiguration, - private val resources: ResourceManager, - private val teiAttributesProvider: TeiAttributesProvider, - private val metadataIconProvider: MetadataIconProvider, -) : RelationshipRepository { - - override fun relationshipTypes(): Single>> { - return when (config) { - is EventRelationshipConfiguration -> stageRelationshipTypes() - is TrackerRelationshipConfiguration -> trackerRelationshipTypes() - } - } - - override fun relationships(): Single> { - return when (config) { - is EventRelationshipConfiguration -> eventRelationships() - is TrackerRelationshipConfiguration -> enrollmentRelationships() - } - } - - private fun trackerRelationshipTypes(): Single>> { - // TODO: Limit link to only TEI - val teTypeUid = d2.trackedEntityModule().trackedEntityInstances() - .uid((config as TrackerRelationshipConfiguration).teiUid) - .blockingGet()?.trackedEntityType() ?: return Single.just(emptyList()) - - return d2.relationshipModule().relationshipTypes() - .withConstraints() - .byAvailableForTrackedEntityInstance(config.teiUid) - .get().map { relationshipTypes -> - relationshipTypes.mapNotNull { relationshipType -> - val secondaryTeTypeUid = when { - relationshipType.fromConstraint()?.trackedEntityType() - ?.uid() == teTypeUid -> - relationshipType.toConstraint()?.trackedEntityType()?.uid() - relationshipType.bidirectional() == true && relationshipType.toConstraint() - ?.trackedEntityType()?.uid() == teTypeUid -> - relationshipType.fromConstraint()?.trackedEntityType()?.uid() - else -> null - } - secondaryTeTypeUid?.let { - Pair(relationshipType, secondaryTeTypeUid) - } - } - } - } - - private fun stageRelationshipTypes(): Single>> { - // TODO: Limit links to TEI - val event = d2.eventModule().events().uid( - (config as EventRelationshipConfiguration).eventUid, - ).blockingGet() - val programStageUid = event?.programStage() ?: "" - val programUid = event?.program() ?: "" - return d2.relationshipModule().relationshipTypes() - .withConstraints() - .byAvailableForEvent(event?.uid() ?: "") - .get().map { relationshipTypes -> - relationshipTypes.mapNotNull { relationshipType -> - val secondaryUid = when { - relationshipType.fromConstraint()?.programStage() - ?.uid() == programStageUid -> - relationshipType.toConstraint()?.trackedEntityType()?.uid() - relationshipType.fromConstraint()?.program()?.uid() == programUid -> - relationshipType.toConstraint()?.trackedEntityType()?.uid() - relationshipType.bidirectional() == true && relationshipType.toConstraint() - ?.programStage()?.uid() == programStageUid -> - relationshipType.fromConstraint()?.trackedEntityType()?.uid() - relationshipType.bidirectional() == true && relationshipType.toConstraint() - ?.program()?.uid() == programUid -> - relationshipType.fromConstraint()?.trackedEntityType()?.uid() - else -> null - } - secondaryUid?.let { Pair(relationshipType, secondaryUid) } - } - } - } - - fun eventRelationships(): Single> { - val eventUid = (config as EventRelationshipConfiguration).eventUid - return Single.fromCallable { - d2.relationshipModule().relationships().getByItem( - RelationshipItem.builder().event( - RelationshipItemEvent.builder().event(eventUid).build(), - ).build(), - ).mapNotNull { relationship -> - val relationshipType = - d2.relationshipModule().relationshipTypes() - .uid(relationship.relationshipType()) - .blockingGet() ?: return@mapNotNull null - - val relationshipOwnerUid: String? - val direction: RelationshipDirection - if (eventUid != relationship.from()?.event()?.event()) { - relationshipOwnerUid = - relationship.from()?.trackedEntityInstance()?.trackedEntityInstance() - direction = RelationshipDirection.FROM - } else { - relationshipOwnerUid = - relationship.to()?.trackedEntityInstance()?.trackedEntityInstance() - direction = RelationshipDirection.TO - } - if (relationshipOwnerUid == null) return@mapNotNull null - - val event = d2.eventModule().events() - .withTrackedEntityDataValues().uid(eventUid).blockingGet() - val tei = d2.trackedEntityModule().trackedEntityInstances() - .withTrackedEntityAttributeValues().uid(relationshipOwnerUid).blockingGet() - - val (fromGeometry, toGeometry) = - if (direction == RelationshipDirection.FROM) { - Pair( - tei?.geometry(), - event?.geometry(), - ) - } else { - Pair( - event?.geometry(), - tei?.geometry(), - ) - } - val (fromValues, toValues) = - if (direction == RelationshipDirection.FROM) { - Pair( - getTeiAttributesForRelationship(relationshipOwnerUid), - getEventValuesForRelationship(eventUid), - ) - } else { - Pair( - getEventValuesForRelationship(eventUid), - getTeiAttributesForRelationship(relationshipOwnerUid), - ) - } - - val (fromProfilePic, toProfilePic) = - if (direction == RelationshipDirection.FROM) { - Pair( - tei?.profilePicturePath(d2, null), - null, - ) - } else { - Pair( - null, - tei?.profilePicturePath(d2, null), - ) - } - - val (fromDefaultPic, toDefaultPic) = - if (direction == RelationshipDirection.FROM) { - Pair( - getTeiDefaultRes(tei), - getEventDefaultRes(event), - ) - } else { - Pair( - getEventDefaultRes(event), - getTeiDefaultRes(tei), - ) - } - - val canBeOpened = if (direction == RelationshipDirection.FROM) { - tei?.syncState() != State.RELATIONSHIP && - orgUnitInScope(tei?.organisationUnit()) - } else { - event?.syncState() != State.RELATIONSHIP && - orgUnitInScope(event?.organisationUnit()) - } - - RelationshipViewModel( - relationship, - fromGeometry, - toGeometry, - relationshipType, - direction, - relationshipOwnerUid, - RelationshipOwnerType.TEI, - fromValues, - toValues, - fromProfilePic, - toProfilePic, - fromDefaultPic, - toDefaultPic, - getOwnerColor(relationshipOwnerUid, RelationshipOwnerType.TEI), - canBeOpened, - ) - } - } - } - - fun enrollmentRelationships(): Single> { - val teiUid = (config as TrackerRelationshipConfiguration).teiUid - val tei = d2.trackedEntityModule().trackedEntityInstances() - .uid(teiUid).blockingGet() - val programUid = d2.enrollmentModule().enrollments() - .uid(config.enrollmentUid).blockingGet()?.program() - return Single.fromCallable { - d2.relationshipModule().relationships().getByItem( - RelationshipItem.builder().trackedEntityInstance( - RelationshipItemTrackedEntityInstance.builder().trackedEntityInstance(teiUid) - .build(), - ).build(), - ).mapNotNull { relationship -> - val relationshipType = - d2.relationshipModule().relationshipTypes() - .uid(relationship.relationshipType()) - .blockingGet() ?: return@mapNotNull null - val direction: RelationshipDirection - val relationshipOwnerUid: String? - val relationshipOwnerType: RelationshipOwnerType? - val fromGeometry: Geometry? - val toGeometry: Geometry? - val fromValues: List> - val toValues: List> - val fromProfilePic: String? - val toProfilePic: String? - val fromDefaultPicRes: Int - val toDefaultPicRes: Int - val canBoOpened: Boolean - - when (teiUid) { - relationship.from()?.trackedEntityInstance()?.trackedEntityInstance() -> { - direction = RelationshipDirection.TO - fromGeometry = tei?.geometry() - fromValues = getTeiAttributesForRelationship(teiUid) - fromProfilePic = tei?.profilePicturePath(d2, programUid) - fromDefaultPicRes = getTeiDefaultRes(tei) - if (relationship.to()?.trackedEntityInstance() != null) { - relationshipOwnerType = RelationshipOwnerType.TEI - relationshipOwnerUid = - relationship.to()?.trackedEntityInstance()?.trackedEntityInstance() - val toTei = d2.trackedEntityModule().trackedEntityInstances() - .uid(relationshipOwnerUid).blockingGet() - toGeometry = toTei?.geometry() - toValues = getTeiAttributesForRelationship(toTei?.uid()) - toProfilePic = toTei?.profilePicturePath(d2, programUid) - toDefaultPicRes = getTeiDefaultRes(toTei) - canBoOpened = toTei?.syncState() != State.RELATIONSHIP && - orgUnitInScope(toTei?.organisationUnit()) - } else { - relationshipOwnerType = RelationshipOwnerType.EVENT - relationshipOwnerUid = - relationship.to()?.event()?.event() - val toEvent = d2.eventModule().events() - .uid(relationshipOwnerUid).blockingGet() - toGeometry = toEvent?.geometry() - toValues = getEventValuesForRelationship(toEvent?.uid()) - toProfilePic = "" - toDefaultPicRes = getEventDefaultRes(toEvent) - canBoOpened = toEvent?.syncState() != State.RELATIONSHIP && - orgUnitInScope(toEvent?.organisationUnit()) - } - } - relationship.to()?.trackedEntityInstance()?.trackedEntityInstance() -> { - direction = RelationshipDirection.FROM - toGeometry = tei?.geometry() - toValues = getTeiAttributesForRelationship(teiUid) - toProfilePic = tei?.profilePicturePath(d2, programUid) - toDefaultPicRes = getTeiDefaultRes(tei) - if (relationship.from()?.trackedEntityInstance() != null) { - relationshipOwnerType = RelationshipOwnerType.TEI - relationshipOwnerUid = - relationship.from()?.trackedEntityInstance() - ?.trackedEntityInstance() - val fromTei = d2.trackedEntityModule().trackedEntityInstances() - .uid(relationshipOwnerUid).blockingGet() - fromGeometry = fromTei?.geometry() - fromValues = getTeiAttributesForRelationship(fromTei?.uid()) - fromProfilePic = fromTei?.profilePicturePath(d2, programUid) - fromDefaultPicRes = getTeiDefaultRes(fromTei) - canBoOpened = fromTei?.syncState() != State.RELATIONSHIP && - orgUnitInScope(fromTei?.organisationUnit()) - } else { - relationshipOwnerType = RelationshipOwnerType.EVENT - relationshipOwnerUid = - relationship.from()?.event()?.event() - val fromEvent = d2.eventModule().events() - .uid(relationshipOwnerUid).blockingGet() - fromGeometry = fromEvent?.geometry() - fromValues = getEventValuesForRelationship(fromEvent?.uid()) - fromProfilePic = "" - fromDefaultPicRes = getEventDefaultRes(fromEvent) - canBoOpened = fromEvent?.syncState() != State.RELATIONSHIP && - orgUnitInScope(fromEvent?.organisationUnit()) - } - } - else -> return@mapNotNull null - } - - if (relationshipOwnerUid == null) return@mapNotNull null - - RelationshipViewModel( - relationship, - fromGeometry, - toGeometry, - relationshipType, - direction, - relationshipOwnerUid, - relationshipOwnerType, - fromValues, - toValues, - fromProfilePic, - toProfilePic, - fromDefaultPicRes, - toDefaultPicRes, - getOwnerColor(relationshipOwnerUid, relationshipOwnerType), - canBoOpened, - ) - } - } - } - - private fun orgUnitInScope(orgUnitUid: String?): Boolean { - return orgUnitUid?.let { - val inCaptureScope = d2.organisationUnitModule().organisationUnits() - .byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE) - .uid(orgUnitUid) - .blockingExists() - val inSearchScope = d2.organisationUnitModule().organisationUnits() - .byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_TEI_SEARCH) - .uid(orgUnitUid) - .blockingExists() - inCaptureScope || inSearchScope - } ?: false - } - - private fun getOwnerColor(uid: String, relationshipOwnerType: RelationshipOwnerType): MetadataIconData { - return when (relationshipOwnerType) { - RelationshipOwnerType.EVENT -> { - val event = d2.eventModule().events().uid(uid).blockingGet() - val program = d2.programModule().programs().uid(event?.program()).blockingGet() - if (program?.programType() == ProgramType.WITHOUT_REGISTRATION) { - metadataIconProvider.invoke(program.style(), SurfaceColor.Primary) - } else { - val programStage = - d2.programModule().programStages().uid(event?.programStage()).blockingGet() - metadataIconProvider(programStage!!.style(), SurfaceColor.Primary) - } - } - RelationshipOwnerType.TEI -> { - val tei = d2.trackedEntityModule().trackedEntityInstances() - .uid(uid).blockingGet() - val teType = d2.trackedEntityModule().trackedEntityTypes() - .uid(tei?.trackedEntityType()).blockingGet() - return metadataIconProvider(teType!!.style(), SurfaceColor.Primary) - } - } - } - - private fun getTeiAttributesForRelationship(teiUid: String?): List> { - val teiTypeUid = d2.trackedEntityModule() - .trackedEntityInstances().uid(teiUid).blockingGet()?.trackedEntityType() - - val attrValuesFromType = mutableListOf>() - teiUid?.let { - teiAttributesProvider.getValuesFromTrackedEntityTypeAttributes(teiTypeUid, it) - .mapNotNull { attributeValue -> - val fieldName = d2.trackedEntityModule().trackedEntityAttributes() - .uid(attributeValue.trackedEntityAttribute()).blockingGet() - ?.displayFormName() - val value = attributeValue.userFriendlyValue(d2) - if (fieldName != null && value != null) { - attrValuesFromType.add(Pair(fieldName, value)) - } else { - null - } - } - } - - val attrValueFromProgramTrackedEntityAttribute = mutableListOf>() - val teiTypeName = d2.trackedEntityModule().trackedEntityTypes() - .uid(teiTypeUid).blockingGet()?.name() ?: "" - - if (attrValuesFromType.isEmpty()) { - teiUid?.let { - teiAttributesProvider.getValuesFromProgramTrackedEntityAttributes(teiTypeUid, it) - .mapNotNull { attributeValue -> - val fieldName = d2.trackedEntityModule().trackedEntityAttributes() - .uid(attributeValue.trackedEntityAttribute()) - .blockingGet()?.displayFormName() - val value = attributeValue.userFriendlyValue(d2) - if (fieldName != null && value != null) { - attrValueFromProgramTrackedEntityAttribute.add(Pair(fieldName, value)) - } else { - null - } - } - } - } - - return when { - attrValuesFromType.isNotEmpty() -> { - attrValuesFromType - } - attrValuesFromType.isEmpty() -> { - attrValueFromProgramTrackedEntityAttribute - } - else -> { - listOf(Pair("uid", teiTypeName)) - } - } - } - - private fun getEventValuesForRelationship(eventUid: String?): List> { - val event = - d2.eventModule().events().withTrackedEntityDataValues().uid(eventUid).blockingGet() - val deFromEvent = d2.programModule().programStageDataElements() - .byProgramStage().eq(event?.programStage()) - .byDisplayInReports().isTrue.blockingGetUids() - - val valuesFromEvent = event?.trackedEntityDataValues()?.mapNotNull { - if (!deFromEvent.contains(it.dataElement())) return@mapNotNull null - val formName = d2.dataElementModule().dataElements().uid(it.dataElement()).blockingGet() - ?.displayName() - val value = it.userFriendlyValue(d2) - if (formName != null && value != null) { - Pair(formName, value) - } else { - null - } - } ?: emptyList() - - return if (valuesFromEvent.isNotEmpty()) { - valuesFromEvent - } else { - val stage = d2.programModule().programStages().uid(event?.programStage()).blockingGet() - listOf(Pair("displayName", stage?.displayName() ?: event?.uid() ?: "")) - } - } - - private fun getTeiDefaultRes(tei: TrackedEntityInstance?): Int { - val teiType = - d2.trackedEntityModule().trackedEntityTypes() - .uid(tei?.trackedEntityType()) - .blockingGet() - return getTeiTypeDefaultRes(teiType?.uid()) - } - - override fun getTeiTypeDefaultRes(teiTypeUid: String?): Int { - val teiType = - d2.trackedEntityModule().trackedEntityTypes().uid(teiTypeUid).blockingGet() - return resources.getObjectStyleDrawableResource( - teiType?.style()?.icon(), - R.drawable.photo_temp_gray, - ) - } - - override fun getEventProgram(eventUid: String?): String { - return d2.eventModule().events().uid(eventUid).blockingGet()?.program() ?: "" - } - - private fun getEventDefaultRes(event: Event?): Int { - val stage = d2.programModule().programStages().uid(event?.programStage()).blockingGet() - val program = d2.programModule().programs().uid(event?.program()).blockingGet() - return resources.getObjectStyleDrawableResource( - stage?.style()?.icon() ?: program?.style()?.icon(), - R.drawable.photo_temp_gray, - ) - } -} - -sealed class RelationshipConfiguration -data class TrackerRelationshipConfiguration( - val enrollmentUid: String, - val teiUid: String, -) : RelationshipConfiguration() - -data class EventRelationshipConfiguration( - val eventUid: String, -) : RelationshipConfiguration() diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipView.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipView.kt index 35d354c599..921438986c 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipView.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipView.kt @@ -1,26 +1,13 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships -import com.mapbox.geojson.BoundingBox -import com.mapbox.geojson.FeatureCollection -import org.dhis2.commons.data.RelationshipViewModel -import org.dhis2.commons.data.tuples.Trio import org.dhis2.usescases.general.AbstractActivityContracts -import org.hisp.dhis.android.core.relationship.RelationshipType interface RelationshipView : AbstractActivityContracts.View { - fun setRelationships(relationships: List) fun goToAddRelationship(teiUid: String, teiTypeUidToAdd: String) fun showPermissionError() fun openDashboardFor(teiUid: String) fun showTeiWithoutEnrollmentError(teiTypeName: String) fun showRelationshipNotFoundError(teiTypeName: String) - fun initFab(relationshipTypes: MutableList>) - fun setFeatureCollection( - currentTei: String?, - relationshipsMapModels: List, - map: Pair, BoundingBox>, - ) - fun openEventFor(eventUid: String, programUid: String) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipViewHolder.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipViewHolder.kt deleted file mode 100644 index 6519ebc9bc..0000000000 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/relationships/RelationshipViewHolder.kt +++ /dev/null @@ -1,67 +0,0 @@ -package org.dhis2.usescases.teiDashboard.dashboardfragments.relationships - -import android.view.View -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.recyclerview.widget.RecyclerView -import org.dhis2.commons.data.RelationshipViewModel -import org.dhis2.commons.resources.ColorUtils -import org.dhis2.commons.resources.setItemPic -import org.dhis2.databinding.ItemRelationshipBinding -import org.dhis2.ui.setUpMetadataIcon - -class RelationshipViewHolder( - private val binding: ItemRelationshipBinding, - private val colorUtils: ColorUtils, -) : - RecyclerView.ViewHolder(binding.root) { - - init { - binding.composeToImage.setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, - ) - } - - fun bind(presenter: RelationshipPresenter, relationships: RelationshipViewModel) { - binding.apply { - relationshipCard.setOnClickListener { - if (relationships.canBeOpened) { - presenter.onRelationshipClicked( - relationships.ownerType, - relationships.ownerUid, - ) - } - } - clearButton.apply { - visibility = if (relationships.canBeOpened) { - View.VISIBLE - } else { - View.GONE - } - setOnClickListener { - relationships.relationship.uid()?.let { presenter.deleteRelationship(it) } - } - } - relationshipTypeName.text = relationships.displayRelationshipTypeName() - toRelationshipName.text = relationships.displayRelationshipName() - relationships.displayImage().let { (imagePath, defaultRes) -> - if (relationships.isEvent()) { - binding.composeToImage.setUpMetadataIcon( - relationships.ownerDefaultColorResource, - false, - ) - } else { - toTeiImage.setItemPic( - imagePath, - defaultRes, - relationships.ownerDefaultColorResource.color.toArgb(), - relationships.displayRelationshipName(), - relationships.isEvent(), - binding.imageText, - colorUtils, - ) - } - } - } - } -} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt index a9ba0d848f..42c4b013e6 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/EventCreationOptionsMapper.kt @@ -1,5 +1,8 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowForward +import androidx.compose.material.icons.outlined.Event import org.dhis2.R import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.data.EventCreationType.ADDNEW @@ -7,42 +10,44 @@ import org.dhis2.commons.data.EventCreationType.DEFAULT import org.dhis2.commons.data.EventCreationType.REFERAL import org.dhis2.commons.data.EventCreationType.SCHEDULE import org.dhis2.commons.resources.ResourceManager -import org.dhis2.usescases.teiDashboard.ui.EventCreationOptions -import org.dhis2.utils.dialFloatingActionButton.DialItem +import org.dhis2.ui.icons.DHIS2Icons +import org.dhis2.ui.icons.DataEntryFilled +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuLeadingElement class EventCreationOptionsMapper(val resources: ResourceManager) { companion object { - const val REFERAL_ID = 3 + const val REFERRAL_ID = 3 const val ADD_NEW_ID = 2 const val SCHEDULE_ID = 1 } - fun mapToEventsByStage(availableOptions: List): List { + fun mapToEventsByStage(availableOptions: List, displayEventLabel: String?): List> { return availableOptions.map { item -> - EventCreationOptions( - item, - getOptionName(item), + MenuItemData( + id = item, + label = getOptionName(item, displayEventLabel ?: resources.getString(R.string.event)), + leadingElement = getMenuItemIcon(item), ) } } - private fun getOptionName(item: EventCreationType): String { + private fun getMenuItemIcon(item: EventCreationType): MenuLeadingElement { return when (item) { - SCHEDULE -> resources.getString(R.string.schedule_new) - ADDNEW -> resources.getString(R.string.add_new) - REFERAL -> resources.getString(R.string.referral) - DEFAULT -> resources.getString(R.string.add_new) + ADDNEW -> MenuLeadingElement.Icon(icon = DHIS2Icons.DataEntryFilled) + SCHEDULE -> MenuLeadingElement.Icon(icon = Icons.Outlined.Event) + REFERAL -> MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.ArrowForward) + DEFAULT -> MenuLeadingElement.Icon(icon = Icons.Outlined.Event) } } - fun mapToEventsByTimeLine(availableOptions: List): List { - return availableOptions.map { item -> - DialItem( - id = getItemId(item), - label = getOptionName(item), - icon = getIconResource(item), - ) + private fun getOptionName(item: EventCreationType, displayEventLabel: String): String { + return when (item) { + SCHEDULE -> resources.getString(R.string.schedule) + " " + displayEventLabel + ADDNEW -> resources.getString(R.string.enter) + " " + displayEventLabel + REFERAL -> resources.getString(R.string.refer) + " " + displayEventLabel + DEFAULT -> resources.getString(R.string.enter) + " " + displayEventLabel } } @@ -50,7 +55,7 @@ class EventCreationOptionsMapper(val resources: ResourceManager) { return when (eventCreationId) { SCHEDULE_ID -> SCHEDULE ADD_NEW_ID -> ADDNEW - REFERAL_ID -> REFERAL + REFERRAL_ID -> REFERAL else -> throw UnsupportedOperationException( "id %s is not supported as an event creation".format( eventCreationId, @@ -58,22 +63,4 @@ class EventCreationOptionsMapper(val resources: ResourceManager) { ) } } - - private fun getItemId(item: EventCreationType): Int { - return when (item) { - SCHEDULE -> SCHEDULE_ID - ADDNEW -> ADD_NEW_ID - REFERAL -> REFERAL_ID - DEFAULT -> ADD_NEW_ID - } - } - - private fun getIconResource(item: EventCreationType): Int { - return when (item) { - SCHEDULE -> R.drawable.ic_date_range - ADDNEW -> R.drawable.ic_note_add - REFERAL -> R.drawable.ic_arrow_forward - DEFAULT -> R.drawable.ic_note_add - } - } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataActivityContract.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataActivityContract.kt new file mode 100644 index 0000000000..e689ee3855 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataActivityContract.kt @@ -0,0 +1,12 @@ +package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata + +import android.content.Context + +interface TEIDataActivityContract { + fun restoreAdapter(programUid: String, teiUid: String, enrollmentUid: String) + fun finishActivity() + fun openSyncDialog() + fun executeOnUIThread() + fun getContext(): Context + fun activityTeiUid(): String? +} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt index 7928e18686..551455ec00 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataContracts.kt @@ -18,7 +18,8 @@ class TEIDataContracts { interface View : AbstractActivityContracts.View { fun viewLifecycleOwner(): LifecycleOwner fun setEvents(events: List) - fun displayScheduleEvent() + fun displayScheduleEvent(programStage: ProgramStage?, showYesNoOptions: Boolean, eventCreationType: EventCreationType) + fun displayEnterEvent(eventUid: String, showYesNoOptions: Boolean, eventCreationType: EventCreationType) fun showDialogCloseProgram() fun areEventsCompleted(): Consumer> fun displayGenerateEvent(eventUid: String) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt index fe8ec48195..657a53312c 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataFragment.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.LifecycleOwner @@ -38,6 +39,7 @@ import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext.EnrollmentEvent @@ -54,14 +56,18 @@ import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.Eve import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventCatComboOptionSelector import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.ui.mapper.TEIEventCardMapper import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.EVENT_LABEL import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.PROGRAM_STAGE_UID import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.SCHEDULING_DIALOG import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.SCHEDULING_DIALOG_RESULT +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.SCHEDULING_EVENT_DUE_DATE_UPDATED +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.Companion.SCHEDULING_EVENT_SKIPPED import org.dhis2.usescases.teiDashboard.ui.TeiDetailDashboard import org.dhis2.usescases.teiDashboard.ui.mapper.InfoBarMapper import org.dhis2.usescases.teiDashboard.ui.mapper.TeiDashboardCardMapper import org.dhis2.usescases.teiDashboard.ui.model.InfoBarType import org.dhis2.usescases.teiDashboard.ui.model.TimelineEventsHeaderModel +import org.dhis2.utils.extension.setIcon import org.dhis2.utils.granularsync.SyncStatusDialog import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.program.Program @@ -91,6 +97,9 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { @Inject lateinit var resourceManager: ResourceManager + @Inject + lateinit var eventResourcesProvider: EventResourcesProvider + @Inject lateinit var cardMapper: TEIEventCardMapper @@ -99,9 +108,8 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { private var programStageFromEvent: ProgramStage? = null private var eventCatComboOptionSelector: EventCatComboOptionSelector? = null private val dashboardViewModel: DashboardViewModel by activityViewModels() - private val dashboardActivity: TeiDashboardMobileActivity by lazy { context as TeiDashboardMobileActivity } + private val dashboardActivity: TEIDataActivityContract by lazy { context as TEIDataActivityContract } - private var showAllEnrollment = false private var programUid: String? = null override fun onAttach(context: Context) { @@ -131,7 +139,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { return FragmentTeiDataBinding.inflate(inflater, container, false).also { binding -> this.binding = binding dashboardViewModel.groupByStage.observe(viewLifecycleOwner) { group -> - showLoadingProgress(false) + showLoadingProgress(true) presenter.onGroupingChanged(group) } @@ -139,7 +147,6 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { eventUid().observe(viewLifecycleOwner, ::displayGenerateEvent) noEnrollmentSelected.observe(viewLifecycleOwner) { noEnrollmentSelected -> if (noEnrollmentSelected) { - showAllEnrollment = true showLegacyCard(dashboardModel.value as DashboardTEIModel) } else { showDetailCard() @@ -158,107 +165,146 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { } } - presenter.events.observe(viewLifecycleOwner) { - setEvents(it) - showLoadingProgress(false) + if (::presenter.isInitialized) { + presenter.events.observe(viewLifecycleOwner) { + setEvents(it) + showLoadingProgress(false) + } } setFragmentResultListener(SCHEDULING_DIALOG_RESULT) { _, bundle -> showToast( - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.event_label_created, bundle.getString(PROGRAM_STAGE_UID), + programUid, ), ) presenter.fetchEvents() } + + setFragmentResultListener(SCHEDULING_EVENT_SKIPPED) { _, bundle -> + val eventLabel = bundle.getString(EVENT_LABEL) ?: getString(R.string.event) + val snackbar = Snackbar.make( + binding.teiRootView, + requireContext().getString(R.string.event_cancelled, eventLabel.replaceFirstChar { it.uppercaseChar() }), + Snackbar.LENGTH_LONG, + ) + + snackbar.setIcon( + drawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_close)!!, + ) { + snackbar.dismiss() + } + snackbar.show() + presenter.fetchEvents() + } + + setFragmentResultListener(SCHEDULING_EVENT_DUE_DATE_UPDATED) { _, _ -> + val snackbar = Snackbar.make( + binding.teiRootView, + requireContext().getString(R.string.due_date_updated), + Snackbar.LENGTH_LONG, + ) + + snackbar.setIcon( + drawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_close)!!, + ) { + snackbar.dismiss() + } + snackbar.show() + presenter.fetchEvents() + } }.root } private fun showDetailCard() { binding.detailCard.setContent { - val dashboardModel by dashboardViewModel.dashboardModel.observeAsState() - val followUp by dashboardViewModel.showFollowUpBar.collectAsState() - val syncNeeded by dashboardViewModel.syncNeeded.collectAsState() - val enrollmentStatus by dashboardViewModel.showStatusBar.collectAsState() - val groupingEvents by dashboardViewModel.groupByStage.observeAsState() - val displayEventCreationButton by presenter.shouldDisplayEventCreationButton.observeAsState( - false, - ) - val eventCount by presenter.events.map { it.count() }.observeAsState(0) - - val syncInfoBar = dashboardModel.takeIf { it is DashboardEnrollmentModel }?.let { - infoBarMapper.map( - infoBarType = InfoBarType.SYNC, - item = dashboardModel as DashboardEnrollmentModel, - actionCallback = { dashboardActivity.openSyncDialog() }, - showInfoBar = syncNeeded, + if (isUserLoggedIn()) { + val dashboardModel by dashboardViewModel.dashboardModel.observeAsState() + val followUp by dashboardViewModel.showFollowUpBar.collectAsState() + val syncNeeded by dashboardViewModel.syncNeeded.collectAsState() + val enrollmentStatus by dashboardViewModel.showStatusBar.collectAsState() + val groupingEvents by dashboardViewModel.groupByStage.observeAsState() + val displayEventCreationButton by presenter.shouldDisplayEventCreationButton.observeAsState( + false, ) - } + val eventCount by presenter.events.map { it.count() }.observeAsState(0) - val followUpInfoBar = - dashboardModel.takeIf { it is DashboardEnrollmentModel }?.let { - infoBarMapper.map( - infoBarType = InfoBarType.FOLLOW_UP, - item = dashboardModel as DashboardEnrollmentModel, - actionCallback = { - dashboardViewModel.onFollowUp() - }, - showInfoBar = followUp, - ) - } - val enrollmentInfoBar = - dashboardModel.takeIf { it is DashboardEnrollmentModel }?.let { + val syncInfoBar = dashboardModel.takeIf { it is DashboardEnrollmentModel }?.let { infoBarMapper.map( - infoBarType = InfoBarType.ENROLLMENT_STATUS, + infoBarType = InfoBarType.SYNC, item = dashboardModel as DashboardEnrollmentModel, - actionCallback = { }, - showInfoBar = enrollmentStatus != EnrollmentStatus.ACTIVE, + actionCallback = { dashboardActivity.openSyncDialog() }, + showInfoBar = syncNeeded, ) } - val card = dashboardModel?.let { - teiDashboardCardMapper.map( - dashboardModel = it, - onImageClick = { fileToShow -> - val intent = ImageDetailActivity.intent( - context = requireActivity(), - title = null, - imagePath = fileToShow.path, + val followUpInfoBar = + dashboardModel.takeIf { it is DashboardEnrollmentModel }?.let { + infoBarMapper.map( + infoBarType = InfoBarType.FOLLOW_UP, + item = dashboardModel as DashboardEnrollmentModel, + actionCallback = { + dashboardViewModel.onFollowUp() + }, + showInfoBar = followUp, ) - - startActivity(intent) - }, - phoneCallback = { openChooser(it, Intent.ACTION_DIAL) }, - emailCallback = { openChooser(it, Intent.ACTION_SENDTO) }, - programsCallback = { - startActivity( - TeiDashboardMobileActivity.intent( - dashboardActivity.context, - dashboardActivity.teiUid, - null, - null, - ), + } + val enrollmentInfoBar = + dashboardModel.takeIf { it is DashboardEnrollmentModel }?.let { + infoBarMapper.map( + infoBarType = InfoBarType.ENROLLMENT_STATUS, + item = dashboardModel as DashboardEnrollmentModel, + actionCallback = { }, + showInfoBar = enrollmentStatus != EnrollmentStatus.ACTIVE, ) + } + + val card = dashboardModel?.let { + teiDashboardCardMapper.map( + dashboardModel = it, + onImageClick = { fileToShow -> + val intent = ImageDetailActivity.intent( + context = requireActivity(), + title = null, + imagePath = fileToShow.path, + ) + + startActivity(intent) + }, + phoneCallback = { openChooser(it, Intent.ACTION_DIAL) }, + emailCallback = { openChooser(it, Intent.ACTION_SENDTO) }, + programsCallback = { + startActivity( + TeiDashboardMobileActivity.intent( + dashboardActivity.getContext(), + dashboardActivity.activityTeiUid(), + null, + null, + ), + ) + }, + ) + } + + TeiDetailDashboard( + syncData = syncInfoBar, + followUpData = followUpInfoBar, + enrollmentData = enrollmentInfoBar, + card = card, + isGrouped = groupingEvents ?: true, + timelineEventHeaderModel = TimelineEventsHeaderModel( + displayEventCreationButton, + eventCount, + eventResourcesProvider.programEventLabel(programUid, eventCount), + presenter.getNewEventOptionsByStages(null), + ), + timelineOnEventCreationOptionSelected = { + presenter.onAddNewEventOptionSelected(it, null) }, ) } - - TeiDetailDashboard( - syncData = syncInfoBar, - followUpData = followUpInfoBar, - enrollmentData = enrollmentInfoBar, - card = card, - isGrouped = groupingEvents ?: true, - timelineEventHeaderModel = TimelineEventsHeaderModel( - displayEventCreationButton, - eventCount, - presenter.getNewEventOptionsByStages(null), - ), - timelineOnEventCreationOptionSelected = { - presenter.onAddNewEventOptionSelected(it, null) - }, - ) } } @@ -306,9 +352,6 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { override fun onResume() { super.onResume() presenter.init() - if (!showAllEnrollment) { - dashboardViewModel.updateDashboard() - } } override fun onPause() { @@ -347,6 +390,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { currentProgram, colorUtils, cardMapper, + initialSelectedEventUid = dashboardViewModel.selectedEventUid().value, ) binding.teiRecycler.adapter = eventAdapter } @@ -359,9 +403,17 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { binding.teiRecycler.visibility = View.GONE if (presenter.shouldDisplayEventCreationButton.value == true) { - binding.emptyTeis.setText(R.string.empty_tei_add) + binding.emptyTeis.text = eventResourcesProvider.formatWithProgramEventLabel( + R.string.empty_tei_event_label_add, + programUid, + 2, + ) } else { - binding.emptyTeis.setText(R.string.empty_tei_no_add) + binding.emptyTeis.text = eventResourcesProvider.formatWithProgramEventLabel( + R.string.empty_tei_event_label_no_add, + programUid, + 2, + ) } } else { binding.emptyTeis.visibility = View.GONE @@ -384,22 +436,39 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { } } - override fun displayScheduleEvent() { + override fun displayScheduleEvent(programStage: ProgramStage?, showYesNoOptions: Boolean, eventCreationType: EventCreationType) { val model = dashboardViewModel.dashboardModel.value if (model is DashboardEnrollmentModel) { - SchedulingDialog.newInstance( - enrollment = model.currentEnrollment, - programStages = presenter.filterAvailableStages(model.programStages), - ).show(parentFragmentManager, SCHEDULING_DIALOG) + val programStages = programStage?.let { listOf(it.uid()) } + ?: presenter.filterAvailableStages(model.programStages) + .map { it.uid() } + + if (programStages.isNotEmpty()) { + SchedulingDialog.newSchedule( + enrollmentUid = model.currentEnrollment.uid(), + programStagesUids = programStages, + showYesNoOptions = showYesNoOptions, + eventCreationType = eventCreationType, + ).show(parentFragmentManager, SCHEDULING_DIALOG) + } } } + override fun displayEnterEvent(eventUid: String, showYesNoOptions: Boolean, eventCreationType: EventCreationType) { + SchedulingDialog.enterEvent( + eventUid = eventUid, + showYesNoOptions = showYesNoOptions, + eventCreationType = eventCreationType, + ).show(parentFragmentManager, SCHEDULING_DIALOG) + } + override fun showDialogCloseProgram() { dialog = CustomDialog( requireContext(), - resourceManager.formatWithEventLabel( + eventResourcesProvider.formatWithProgramStageEventLabel( R.string.event_label_completed, programStageFromEvent?.uid(), + programUid, ), resourceManager.formatWithEnrollmentLabel( programUid = programUid, @@ -457,15 +526,8 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { } override fun restoreAdapter(programUid: String, teiUid: String, enrollmentUid: String) { - dashboardActivity.startActivity( - TeiDashboardMobileActivity.intent( - activity, - teiUid, - programUid, - enrollmentUid, - ), - ) - dashboardActivity.finish() + dashboardActivity.restoreAdapter(programUid, teiUid, enrollmentUid) + dashboardActivity.finishActivity() } override fun openEventDetails(intent: Intent, options: ActivityOptionsCompat) = @@ -478,10 +540,17 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { presenter.fetchEvents() } - override fun openEventCapture(intent: Intent) = - contractHandler.editEvent(intent).observe(viewLifecycleOwner) { - presenter.fetchEvents() + override fun openEventCapture(intent: Intent) { + if (dashboardActivity is TeiDashboardMobileActivity) { + contractHandler.editEvent(intent).observe(viewLifecycleOwner) { + presenter.fetchEvents() + } } + if (dashboardActivity is EventCaptureActivity) { + val selectedEventUid = intent.getStringExtra(Constants.EVENT_UID) + dashboardViewModel.updateSelectedEventUid(selectedEventUid) + } + } override fun goToEventInitial( eventCreationType: EventCreationType, @@ -528,7 +597,6 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { override fun displayOrgUnitSelectorForNewEvent(programUid: String, programStageUid: String) { OUTreeFragment.Builder() - .showAsDialog() .singleSelection() .orgUnitScope( OrgUnitSelectorScope.ProgramCaptureScope(programUid), @@ -572,9 +640,7 @@ class TEIDataFragment : FragmentGlobalAbstract(), TEIDataContracts.View { } override fun showProgramRuleErrorMessage() { - dashboardActivity.runOnUiThread { - showDescription(getString(R.string.error_applying_rule_effects)) - } + dashboardActivity.executeOnUIThread() } companion object { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt index 456ca6e8a3..4c21b073f5 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataModule.kt @@ -4,6 +4,7 @@ import androidx.activity.result.ActivityResultRegistry import dagger.Module import dagger.Provides import org.dhis2.commons.data.EntryMode +import org.dhis2.commons.data.ProgramConfigurationRepository import org.dhis2.commons.date.DateUtils import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.network.NetworkUtils @@ -21,10 +22,10 @@ import org.dhis2.data.forms.dataentry.SearchTEIRepositoryImpl import org.dhis2.form.data.FormValueStore import org.dhis2.form.data.OptionsRepository import org.dhis2.mobileProgramRules.RuleEngineHelper -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCase +import org.dhis2.tracker.events.CreateEventUseCaseRepository import org.dhis2.usescases.teiDashboard.DashboardRepository import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.ui.mapper.TEIEventCardMapper -import org.dhis2.usescases.teiDashboard.data.ProgramConfigurationRepository import org.dhis2.usescases.teiDashboard.domain.GetNewEventCreationTypeOptions import org.dhis2.usescases.teiDashboard.ui.mapper.InfoBarMapper import org.dhis2.usescases.teiDashboard.ui.mapper.TeiDashboardCardMapper @@ -171,10 +172,17 @@ class TEIDataModule( @Provides fun provideCreateEventUseCase( dispatcherProvider: DispatcherProvider, - d2: D2, - dateUtils: DateUtils, + repository: CreateEventUseCaseRepository, ) = CreateEventUseCase( dispatcher = dispatcherProvider, + repository = repository, + ) + + @Provides + fun provideCreateEventUseCaseRepository( + d2: D2, + dateUtils: DateUtils, + ) = CreateEventUseCaseRepository( d2 = d2, dateUtils = dateUtils, ) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt index 15a1b0b043..c910d79cbe 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TEIDataPresenter.kt @@ -4,7 +4,6 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.annotation.VisibleForTesting -import androidx.core.app.ActivityOptionsCompat import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import io.reactivex.Completable @@ -32,17 +31,15 @@ import org.dhis2.form.data.OptionsRepository import org.dhis2.form.data.RulesUtilsProviderImpl import org.dhis2.form.model.EventMode import org.dhis2.mobileProgramRules.RuleEngineHelper -import org.dhis2.usescases.events.ScheduledEventActivity.Companion.getIntent +import org.dhis2.tracker.events.CreateEventUseCase import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity.Companion.getActivityBundle import org.dhis2.usescases.eventsWithoutRegistration.eventInitial.EventInitialActivity -import org.dhis2.usescases.programEventDetail.usecase.CreateEventUseCase import org.dhis2.usescases.programStageSelection.ProgramStageSelectionActivity import org.dhis2.usescases.teiDashboard.DashboardRepository import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton.decrement import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TeiDataIdlingResourceSingleton.increment import org.dhis2.usescases.teiDashboard.domain.GetNewEventCreationTypeOptions -import org.dhis2.usescases.teiDashboard.ui.EventCreationOptions import org.dhis2.utils.Result import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.analytics.CREATE_EVENT_TEI @@ -53,6 +50,7 @@ import org.hisp.dhis.android.core.enrollment.EnrollmentStatus import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData import org.hisp.dhis.rules.models.RuleEffect import timber.log.Timber @@ -207,7 +205,11 @@ class TEIDataPresenter( .observeOn(schedulerProvider.ui()) .subscribe({ programStage -> if (programStage.displayGenerateEventBox() == true || programStage.allowGenerateNextVisit() == true) { - view.displayScheduleEvent() + view.displayScheduleEvent( + programStage = null, + showYesNoOptions = true, + eventCreationType = EventCreationType.SCHEDULE, + ) } else if (programStage.remindCompleted() == true) { view.showDialogCloseProgram() } @@ -268,47 +270,46 @@ class TEIDataPresenter( fun onScheduleSelected(uid: String?, sharedView: View?) { uid?.let { - val intent = getIntent(view.context, uid) - val options = sharedView?.let { it1 -> - ActivityOptionsCompat.makeSceneTransitionAnimation( - view.abstractActivity, - it1, - "shared_view", - ) - } ?: ActivityOptionsCompat.makeBasic() - view.openEventDetails(intent, options) + view.displayEnterEvent( + eventUid = it, + showYesNoOptions = false, + eventCreationType = EventCreationType.SCHEDULE, + ) } } fun onEventSelected(uid: String, eventStatus: EventStatus) { - if (eventStatus == EventStatus.ACTIVE || eventStatus == EventStatus.COMPLETED) { - val intent = Intent(view.context, EventCaptureActivity::class.java) - intent.putExtras( - getActivityBundle( - eventUid = uid, - programUid = programUid ?: throw IllegalStateException(), - eventMode = EventMode.CHECK, - ), - ) - view.openEventCapture(intent) - } else { - val event = d2.event(uid) - val intent = Intent(view.context, EventInitialActivity::class.java) - intent.putExtras( - EventInitialActivity.getBundle( - programUid, - uid, - EventCreationType.DEFAULT.name, - teiUid, - null, - event?.organisationUnit(), - event?.programStage(), - enrollmentUid, - 0, - teiDataRepository.getEnrollment().blockingGet()?.status(), - ), - ) - view.openEventInitial(intent) + when (eventStatus) { + EventStatus.ACTIVE, EventStatus.COMPLETED, EventStatus.SKIPPED -> { + val intent = Intent(view.context, EventCaptureActivity::class.java) + intent.putExtras( + getActivityBundle( + eventUid = uid, + programUid = programUid ?: throw IllegalStateException(), + eventMode = EventMode.CHECK, + ), + ) + view.openEventCapture(intent) + } + else -> { + val event = d2.event(uid) + val intent = Intent(view.context, EventInitialActivity::class.java) + intent.putExtras( + EventInitialActivity.getBundle( + programUid, + uid, + EventCreationType.DEFAULT.name, + teiUid, + null, + event?.organisationUnit(), + event?.programStage(), + enrollmentUid, + 0, + teiDataRepository.getEnrollment().blockingGet()?.status(), + ), + ) + view.openEventInitial(intent) + } } } @@ -359,23 +360,45 @@ class TEIDataPresenter( } } - private fun manageAddNewEventOptionSelected(eventCreationType: EventCreationType, stage: ProgramStage?) { + private fun manageAddNewEventOptionSelected( + eventCreationType: EventCreationType, + stage: ProgramStage?, + ) { if (stage != null) { when (eventCreationType) { EventCreationType.ADDNEW -> programUid?.let { program -> checkOrgUnitCount(program, stage.uid()) } + EventCreationType.SCHEDULE -> { + view.displayScheduleEvent( + programStage = stage, + showYesNoOptions = false, + eventCreationType = eventCreationType, + ) + } + else -> view.goToEventInitial(eventCreationType, stage) } } else { - createEventInEnrollment(eventCreationType) + when (eventCreationType) { + EventCreationType.REFERAL -> { + createEventInEnrollment(eventCreationType) + } + else -> { + view.displayScheduleEvent( + programStage = null, + showYesNoOptions = false, + eventCreationType = eventCreationType, + ) + } + } } } - fun getNewEventOptionsByStages(stage: ProgramStage?): List { + fun getNewEventOptionsByStages(stage: ProgramStage?): List> { val options = programUid?.let { getNewEventCreationTypeOptions(stage, it) } - return options?.let { eventCreationOptionsMapper.mapToEventsByStage(it) } ?: emptyList() + return options?.let { eventCreationOptionsMapper.mapToEventsByStage(it, stage?.displayEventLabel()) } ?: emptyList() } fun fetchEvents() { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepositoryImpl.kt index c003fc5430..7e8227b954 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/TeiDataRepositoryImpl.kt @@ -441,7 +441,7 @@ class TeiDataRepositoryImpl( override fun displayOrganisationUnit(programUid: String): Boolean { return d2.organisationUnitModule().organisationUnits() .byProgramUids(listOf(programUid)) - .blockingGet().size > 1 + .blockingCount() > 1 } override fun enrollmentOrgUnitInCaptureScope(enrollmentOrgUnit: String): Boolean { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/EventAdapter.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/EventAdapter.kt index 5e4426397b..5d893f120c 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/EventAdapter.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/EventAdapter.kt @@ -3,8 +3,10 @@ package org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.LocalTextStyle import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView @@ -29,12 +31,15 @@ import org.dhis2.commons.resources.ColorUtils import org.dhis2.databinding.ItemEventBinding import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataPresenter import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.ui.mapper.TEIEventCardMapper +import org.dhis2.utils.isLandscape import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.mobile.ui.designsystem.component.ListCard import org.hisp.dhis.mobile.ui.designsystem.component.ListCardDescriptionModel import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.theme.Radius import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor class EventAdapter( @@ -42,6 +47,7 @@ class EventAdapter( val program: Program, val colorUtils: ColorUtils, private val cardMapper: TEIEventCardMapper, + private val initialSelectedEventUid: String? = null, ) : ListAdapter( object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: EventViewModel, newItem: EventViewModel): Boolean { @@ -66,6 +72,8 @@ class EventAdapter( private var stageSelector: FlowableProcessor = PublishProcessor.create() + private var previousSelectedPosition: Int = RecyclerView.NO_POSITION + fun stageSelector(): Flowable { return stageSelector } @@ -137,7 +145,7 @@ class EventAdapter( onCardClick = { it.event?.let { event -> when (event.status()) { - EventStatus.SCHEDULE, EventStatus.OVERDUE, EventStatus.SKIPPED -> { + EventStatus.SCHEDULE, EventStatus.OVERDUE -> { presenter.onScheduleSelected( event.uid(), composeView, @@ -149,11 +157,26 @@ class EventAdapter( event.uid(), event.status()!!, ) + + if (isLandscape()) { + if (previousSelectedPosition != RecyclerView.NO_POSITION) { + currentList[previousSelectedPosition].isClicked = false + notifyItemChanged(previousSelectedPosition) + } + previousSelectedPosition = position + getItem(position).isClicked = true + notifyItemChanged(position) + } } } } }, ) + + if (it.event?.uid() == initialSelectedEventUid && previousSelectedPosition == RecyclerView.NO_POSITION) { + it.isClicked = true + previousSelectedPosition = position + } Box( modifier = Modifier .padding( @@ -183,6 +206,13 @@ class EventAdapter( shrinkLabelText = card.shrinkLabelText, onCardClick = card.onCardCLick, ) + if (it.isClicked) { + Box( + modifier = Modifier + .matchParentSize() + .background(color = SurfaceColor.Primary.copy(alpha = 0.1f), shape = RoundedCornerShape(Radius.S)), + ) + } } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/StageViewHolder.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/StageViewHolder.kt index fd1975339a..341b56a644 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/StageViewHolder.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/StageViewHolder.kt @@ -22,15 +22,13 @@ import org.dhis2.commons.data.EventViewModel import org.dhis2.commons.data.StageSection import org.dhis2.commons.resources.ColorUtils import org.dhis2.commons.resources.ResourceManager -import org.dhis2.commons.schedulers.get -import org.dhis2.ui.MetadataIcon import org.dhis2.ui.MetadataIconData import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.TEIDataPresenter import org.dhis2.usescases.teiDashboard.ui.NewEventOptions import org.hisp.dhis.mobile.ui.designsystem.component.Avatar -import org.hisp.dhis.mobile.ui.designsystem.component.AvatarSize -import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyle +import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyleData import org.hisp.dhis.mobile.ui.designsystem.component.Description +import org.hisp.dhis.mobile.ui.designsystem.component.MetadataAvatarSize import org.hisp.dhis.mobile.ui.designsystem.component.Title import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor @@ -106,10 +104,11 @@ internal class StageViewHolder( metadataIconData: MetadataIconData, ) { Avatar( - metadataAvatar = { - MetadataIcon(metadataIconData = metadataIconData, size = AvatarSize.Large) - }, - style = AvatarStyle.METADATA, + style = AvatarStyleData.Metadata( + imageCardData = metadataIconData.imageCardData, + avatarSize = MetadataAvatarSize.M(), + tintColor = metadataIconData.color, + ), ) } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt index 14edcf7be0..1cb3755533 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt @@ -17,15 +17,15 @@ import org.dhis2.commons.data.EventViewModel import org.dhis2.commons.date.toOverdueOrScheduledUiText import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.ui.model.ListCardUiModel -import org.dhis2.ui.MetadataIcon import org.hisp.dhis.android.core.common.State import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItemColor import org.hisp.dhis.mobile.ui.designsystem.component.Avatar -import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyle +import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyleData import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.MetadataAvatarSize import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @@ -68,10 +68,11 @@ class TEIEventCardMapper( @Composable private fun ProvideAvatar(eventItem: EventViewModel) { Avatar( - metadataAvatar = { - MetadataIcon(metadataIconData = eventItem.metadataIconData) - }, - style = AvatarStyle.METADATA, + style = AvatarStyleData.Metadata( + imageCardData = eventItem.metadataIconData.imageCardData, + avatarSize = MetadataAvatarSize.M(), + tintColor = eventItem.metadataIconData.color, + ), ) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt index 987a86729d..fe1494541b 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialog.kt @@ -1,7 +1,10 @@ package org.dhis2.usescases.teiDashboard.dialogs.scheduling import android.content.Context +import android.content.Intent +import android.os.Build import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -12,43 +15,85 @@ import androidx.core.os.bundleOf import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.parcelize.Parcelize import org.dhis2.bindings.app +import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.dialogs.PeriodDialog import org.dhis2.commons.dialogs.calendarpicker.CalendarPicker import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener import org.dhis2.form.R -import org.hisp.dhis.android.core.enrollment.Enrollment -import org.hisp.dhis.android.core.program.ProgramStage +import org.dhis2.form.model.EventMode +import org.dhis2.usescases.eventsWithoutRegistration.eventCapture.EventCaptureActivity import java.util.Date import javax.inject.Inject class SchedulingDialog : BottomSheetDialogFragment() { + companion object { const val SCHEDULING_DIALOG = "SCHEDULING_DIALOG" const val SCHEDULING_DIALOG_RESULT = "SCHEDULING_DIALOG_RESULT" + const val SCHEDULING_EVENT_SKIPPED = "SCHEDULING_EVENT_SKIPPED" + const val SCHEDULING_EVENT_DUE_DATE_UPDATED = "SCHEDULING_EVENT_DUE_DATE_UPDATED" const val PROGRAM_STAGE_UID = "PROGRAM_STAGE_UID" + const val EVENT_LABEL = "EVENT_LABEL" + + private const val TAG_LAUNCH_MODE = "LAUNCH_MODE" + + fun newSchedule( + enrollmentUid: String, + programStagesUids: List, + showYesNoOptions: Boolean, + eventCreationType: EventCreationType, + ): SchedulingDialog { + val launchMode = LaunchMode.NewSchedule( + enrollmentUid = enrollmentUid, + programStagesUids = programStagesUids, + showYesNoOptions = showYesNoOptions, + eventCreationType = eventCreationType, + ) + + return SchedulingDialog().apply { + arguments = bundleOf( + TAG_LAUNCH_MODE to launchMode, + ) + } + } - fun newInstance( - enrollment: Enrollment, - programStages: List, + fun enterEvent( + eventUid: String, + showYesNoOptions: Boolean, + eventCreationType: EventCreationType, ): SchedulingDialog { + val launchMode = LaunchMode.EnterEvent( + eventUid = eventUid, + showYesNoOptions = showYesNoOptions, + eventCreationType = eventCreationType, + ) + return SchedulingDialog().apply { - this.enrollment = enrollment - this.programStages = programStages + arguments = bundleOf( + TAG_LAUNCH_MODE to launchMode, + ) } } } - var enrollment: Enrollment? = null - var programStages: List? = null + private lateinit var launchMode: LaunchMode @Inject - lateinit var factory: SchedulingViewModelFactory - val viewModel: SchedulingViewModel by viewModels { factory } + lateinit var factory: SchedulingViewModelFactory.Factory + + val viewModel: SchedulingViewModel by viewModels { + factory.build(launchMode) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NORMAL, R.style.CustomBottomSheetDialogTheme) + val arguments = arguments + if (arguments != null) { + launchMode = LaunchMode.fromBundle(arguments) + } } override fun onAttach(context: Context) { @@ -61,18 +106,35 @@ class SchedulingDialog : BottomSheetDialogFragment() { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - enrollment?.let { - viewModel.enrollment = it - } - programStages?.let { - viewModel.programStages = it - viewModel.setInitialProgramStage(it.first()) - } viewModel.onEventScheduled = { setFragmentResult(SCHEDULING_DIALOG_RESULT, bundleOf(PROGRAM_STAGE_UID to it)) dismiss() } + viewModel.onEventSkipped = { + setFragmentResult(SCHEDULING_EVENT_SKIPPED, bundleOf(EVENT_LABEL to it)) + dismiss() + } + + viewModel.onDueDateUpdated = { + setFragmentResult(SCHEDULING_EVENT_DUE_DATE_UPDATED, bundleOf()) + dismiss() + } + + viewModel.onEnterEvent = { eventUid, programUid -> + val bundle = EventCaptureActivity.getActivityBundle( + eventUid, + programUid, + EventMode.SCHEDULE, + ) + Intent(activity, EventCaptureActivity::class.java).apply { + putExtras(bundle) + startActivity(this) + } + + dismiss() + } + viewModel.showCalendar = { showCalendarDialog() } @@ -88,8 +150,7 @@ class SchedulingDialog : BottomSheetDialogFragment() { setContent { SchedulingDialogUi( viewModel = viewModel, - programStages = viewModel.programStages, - orgUnitUid = viewModel.enrollment.organisationUnit(), + launchMode = launchMode, onDismiss = { dismiss() }, ) } @@ -129,4 +190,37 @@ class SchedulingDialog : BottomSheetDialogFragment() { } .show(requireActivity().supportFragmentManager, PeriodDialog::class.java.simpleName) } + + sealed interface LaunchMode : Parcelable { + + companion object { + + fun fromBundle(args: Bundle): LaunchMode { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + args.getParcelable(TAG_LAUNCH_MODE, LaunchMode::class.java)!! + } else { + @Suppress("DEPRECATION") + args.getParcelable(TAG_LAUNCH_MODE)!! + } + } + } + + val showYesNoOptions: Boolean + val eventCreationType: EventCreationType + + @Parcelize + data class NewSchedule( + val enrollmentUid: String, + val programStagesUids: List, + override val showYesNoOptions: Boolean, + override val eventCreationType: EventCreationType, + ) : LaunchMode + + @Parcelize + data class EnterEvent( + val eventUid: String, + override val showYesNoOptions: Boolean, + override val eventCreationType: EventCreationType, + ) : LaunchMode + } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt index 4827df76ee..374c5d1ec7 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingDialogUi.kt @@ -1,8 +1,13 @@ package org.dhis2.usescases.teiDashboard.dialogs.scheduling +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.EventBusy +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf @@ -12,6 +17,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import org.dhis2.R import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatComboUiModel @@ -21,10 +30,12 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.Prov import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvidePeriodSelector import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.willShowCalendar +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.LaunchMode import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.mobile.ui.designsystem.component.BottomSheetShell import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ColorStyle import org.hisp.dhis.mobile.ui.designsystem.component.DropdownItem import org.hisp.dhis.mobile.ui.designsystem.component.InputDropDown import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState @@ -34,17 +45,19 @@ import org.hisp.dhis.mobile.ui.designsystem.component.RadioButtonBlock import org.hisp.dhis.mobile.ui.designsystem.component.RadioButtonData import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor @Composable fun SchedulingDialogUi( - programStages: List, viewModel: SchedulingViewModel, - orgUnitUid: String?, + launchMode: LaunchMode, onDismiss: () -> Unit, ) { val date by viewModel.eventDate.collectAsState() val catCombo by viewModel.eventCatCombo.collectAsState() + val programStages by viewModel.programStages.collectAsState() val selectedProgramStage by viewModel.programStage.collectAsState() + val enrollment by viewModel.enrollment.collectAsState() val yesNoOptions = InputYesNoFieldValues.entries.map { RadioButtonData( @@ -59,23 +72,22 @@ fun SchedulingDialogUi( derivedStateOf { optionSelected == yesNoOptions.first() } } - val onButtonClick = { - when { - scheduleNew -> viewModel.scheduleEvent() - else -> onDismiss() - } - } BottomSheetShell( - title = bottomSheetTitle(programStages), + title = bottomSheetTitle( + launchMode = launchMode, + programStages = programStages, + ), + subtitle = viewModel.overdueSubtitle, + headerTextAlignment = TextAlign.Start, buttonBlock = { - Button( - modifier = Modifier.fillMaxWidth(), - style = ButtonStyle.FILLED, - enabled = !scheduleNew || - !date.dateValue.isNullOrEmpty() && - catCombo.isCompleted, - text = buttonTitle(scheduleNew), - onClick = onButtonClick, + ButtonBlock( + launchMode = launchMode, + scheduleNew = scheduleNew, + date = date, + catCombo = catCombo, + selectedProgramStage = selectedProgramStage, + viewModel = viewModel, + onDismiss = onDismiss, ) }, showSectionDivider = false, @@ -83,15 +95,20 @@ fun SchedulingDialogUi( Column( modifier = Modifier.fillMaxWidth(), ) { - RadioButtonBlock( - modifier = Modifier.padding(bottom = Spacing.Spacing8), - orientation = Orientation.HORIZONTAL, - content = yesNoOptions, - itemSelected = optionSelected, - onItemChange = { - optionSelected = it - }, - ) + if (launchMode.showYesNoOptions) { + RadioButtonBlock( + modifier = Modifier + .padding(bottom = Spacing.Spacing8) + .semantics { testTag = "YES_NO_OPTIONS" }, + orientation = Orientation.HORIZONTAL, + content = yesNoOptions, + itemSelected = optionSelected, + onItemChange = { + optionSelected = it + }, + ) + } + if (scheduleNew) { ProvideScheduleNewEventForm( programStages = programStages, @@ -99,22 +116,105 @@ fun SchedulingDialogUi( selectedProgramStage = selectedProgramStage, date = date, catCombo = catCombo, - orgUnitUid = orgUnitUid, + orgUnitUid = enrollment?.organisationUnit(), + launchMode = launchMode, ) } } }, onDismiss = onDismiss, + animateHeaderOnKeyboardAppearance = false, ) } @Composable -fun bottomSheetTitle(programStages: List): String = - stringResource(id = R.string.schedule_next) + " " + - when (programStages.size) { - 1 -> programStages.first().displayName() - else -> stringResource(id = R.string.event) - } + "?" +private fun ButtonBlock( + launchMode: LaunchMode, + scheduleNew: Boolean, + date: EventDate, + catCombo: EventCatCombo, + selectedProgramStage: ProgramStage?, + viewModel: SchedulingViewModel, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier) { + when (launchMode) { + is LaunchMode.NewSchedule -> { + Button( + modifier = Modifier.fillMaxWidth(), + style = ButtonStyle.FILLED, + enabled = !scheduleNew || + !date.dateValue.isNullOrEmpty() && + catCombo.isCompleted, + text = buttonTitle(scheduleNew), + onClick = { + when { + scheduleNew -> viewModel.scheduleEvent(launchMode) + else -> onDismiss() + } + }, + ) + } + + is LaunchMode.EnterEvent -> { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + val eventLabel = selectedProgramStage?.displayEventLabel() ?: stringResource(R.string.event) + Button( + modifier = Modifier.fillMaxWidth(), + style = ButtonStyle.FILLED, + enabled = !date.dateValue.isNullOrEmpty(), + text = stringResource(R.string.enter_event, eventLabel), + onClick = { + viewModel.enterEvent(launchMode) + }, + ) + + Button( + modifier = Modifier.fillMaxWidth(), + style = ButtonStyle.OUTLINED, + colorStyle = ColorStyle.WARNING, + text = stringResource(R.string.cancel_event, eventLabel), + icon = { + Icon( + imageVector = Icons.Outlined.EventBusy, + contentDescription = null, + tint = TextColor.OnWarningContainer, + ) + }, + onClick = { + viewModel.onCancelEvent() + }, + ) + } + } + } + } +} + +@Composable +fun bottomSheetTitle( + launchMode: LaunchMode, + programStages: List, +): String { + val prefix = when (launchMode) { + is LaunchMode.NewSchedule -> stringResource(id = R.string.schedule_next) + is LaunchMode.EnterEvent -> stringResource(id = R.string.scheduled_enter_event) + } + val defaultEventName = stringResource(id = R.string.event) + val programName = when (programStages.size) { + 1 -> programStages.first().displayEventLabel() ?: defaultEventName + else -> defaultEventName + } + val terminalSymbol = when (launchMode) { + is LaunchMode.NewSchedule -> "?" + is LaunchMode.EnterEvent -> "" + } + + return "$prefix $programName$terminalSymbol" +} @Composable fun buttonTitle(scheduleNew: Boolean): String = when (scheduleNew) { @@ -130,8 +230,9 @@ fun ProvideScheduleNewEventForm( date: EventDate, catCombo: EventCatCombo, orgUnitUid: String?, + launchMode: LaunchMode, ) { - if (programStages.size > 1) { + if (programStages.size > 1 && launchMode !is LaunchMode.EnterEvent) { InputDropDown( title = stringResource(id = R.string.program_stage), state = InputShellState.UNFOCUSED, @@ -150,6 +251,7 @@ fun ProvideScheduleNewEventForm( EventInputDateUiModel( eventDate = date, detailsEnabled = true, + selectableDates = viewModel.getSelectableDates(), onDateClick = {}, onDateSelected = { viewModel.onDateSet(it.year, it.month, it.day) }, onClear = { viewModel.onClearEventReportDate() }, @@ -171,7 +273,7 @@ fun ProvideScheduleNewEventForm( ) } - if (!catCombo.isDefault) { + if (!catCombo.isDefault && launchMode !is LaunchMode.EnterEvent) { catCombo.categories.forEach { category -> ProvideCategorySelector( eventCatComboUiModel = EventCatComboUiModel( diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingModule.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingModule.kt index 3d5beb862b..6dccc64caf 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingModule.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingModule.kt @@ -2,23 +2,11 @@ package org.dhis2.usescases.teiDashboard.dialogs.scheduling import dagger.Module import dagger.Provides -import org.dhis2.commons.di.dagger.PerFragment -import org.dhis2.commons.resources.DhisPeriodUtils -import org.dhis2.commons.resources.ResourceManager -import org.hisp.dhis.android.core.D2 +import org.dhis2.commons.date.DateUtils @Module class SchedulingModule { + @Provides - @PerFragment - fun provideSchedulingViewModelFactory( - d2: D2, - resourceManager: ResourceManager, - periodUtils: DhisPeriodUtils, - ): SchedulingViewModelFactory = - SchedulingViewModelFactory( - d2, - resourceManager, - periodUtils, - ) + fun providesDateUtils(): DateUtils = DateUtils.getInstance() } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt index 33f095c82d..9c2070b1d4 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModel.kt @@ -2,22 +2,31 @@ package org.dhis2.usescases.teiDashboard.dialogs.scheduling import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch -import org.dhis2.commons.data.EventCreationType +import kotlinx.coroutines.withContext +import org.dhis2.commons.bindings.enrollment +import org.dhis2.commons.bindings.event +import org.dhis2.commons.bindings.programStage +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.date.toOverdueOrScheduledUiText import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.data.EventDetailsRepository import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureEventReportDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider +import org.dhis2.usescases.teiDashboard.dialogs.scheduling.SchedulingDialog.LaunchMode import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.enrollment.Enrollment +import org.hisp.dhis.android.core.event.Event +import org.hisp.dhis.android.core.event.EventStatus import org.hisp.dhis.android.core.program.ProgramStage import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates import java.text.SimpleDateFormat @@ -26,70 +35,143 @@ import java.util.Date import java.util.Locale class SchedulingViewModel( - val d2: D2, - val resourceManager: ResourceManager, - val periodUtils: DhisPeriodUtils, + private val d2: D2, + private val resourceManager: ResourceManager, + private val eventResourcesProvider: EventResourcesProvider, + private val periodUtils: DhisPeriodUtils, + private val dispatchersProvider: DispatcherProvider, + private val launchMode: LaunchMode, + private val dateUtils: DateUtils, ) : ViewModel() { lateinit var repository: EventDetailsRepository lateinit var configureEventReportDate: ConfigureEventReportDate lateinit var configureEventCatCombo: ConfigureEventCatCombo - lateinit var enrollment: Enrollment - lateinit var programStages: List - - private val _programStage: MutableStateFlow = MutableStateFlow(null) - val programStage: StateFlow get() = _programStage - var showCalendar: (() -> Unit)? = null var showPeriods: (() -> Unit)? = null var onEventScheduled: ((String) -> Unit)? = null + var onEventSkipped: ((String?) -> Unit)? = null + var onDueDateUpdated: (() -> Unit)? = null + var onEnterEvent: ((String, String) -> Unit)? = null private val _eventDate: MutableStateFlow = MutableStateFlow(EventDate()) - val eventDate: StateFlow get() = _eventDate + val eventDate: StateFlow = _eventDate private val _eventCatCombo: MutableStateFlow = MutableStateFlow(EventCatCombo()) - val eventCatCombo: StateFlow get() = _eventCatCombo + val eventCatCombo: StateFlow = _eventCatCombo + + private val _programStage: MutableStateFlow = MutableStateFlow(null) + val programStage: StateFlow = _programStage + + private val _programStages: MutableStateFlow> = MutableStateFlow(emptyList()) + val programStages: StateFlow> = _programStages + + private val _enrollment: MutableStateFlow = MutableStateFlow(null) + val enrollment: StateFlow = _enrollment + + val overdueSubtitle: String? + get() { + return if (launchMode is LaunchMode.NewSchedule) { + null + } else { + val eventDate = _eventDate.value.currentDate ?: return null + eventDate.toOverdueOrScheduledUiText( + resourceManager = resourceManager, + isScheduling = true, + ) + } + } + + init { + viewModelScope.launch { + val enrollment = withContext(dispatchersProvider.io()) { + when (launchMode) { + is LaunchMode.NewSchedule -> d2.enrollment(launchMode.enrollmentUid) + is LaunchMode.EnterEvent -> null + } + } + _enrollment.value = enrollment + + val programStages = withContext(dispatchersProvider.io()) { + when (launchMode) { + is LaunchMode.NewSchedule -> { + launchMode.programStagesUids.mapNotNull(d2::programStage) + } + is LaunchMode.EnterEvent -> emptyList() + } + } + _programStages.value = programStages + + val programStage = withContext(dispatchersProvider.io()) { + when (launchMode) { + is LaunchMode.NewSchedule -> programStages.first() + is LaunchMode.EnterEvent -> { + val eventProgramStageId = d2.event(launchMode.eventUid)?.programStage() + d2.programModule().programStages().uid(eventProgramStageId).blockingGet() + } + } + } + _programStage.value = programStage + + loadScheduleConfiguration(launchMode) + } + } + + private fun loadScheduleConfiguration(launchMode: LaunchMode) { + val enrollment = enrollment.value + val event = when (launchMode) { + is LaunchMode.EnterEvent -> d2.event(launchMode.eventUid) + is LaunchMode.NewSchedule -> null + } + val programId = when (launchMode) { + is LaunchMode.NewSchedule -> enrollment?.program() + is LaunchMode.EnterEvent -> event?.program() + }.orEmpty() + val enrollmentId = when (launchMode) { + is LaunchMode.NewSchedule -> enrollment?.uid() + is LaunchMode.EnterEvent -> event?.enrollment().orEmpty() + } - private fun loadConfiguration() { repository = EventDetailsRepository( d2 = d2, - programUid = enrollment.program().orEmpty(), - eventUid = null, + programUid = programId, + eventUid = event?.uid(), programStageUid = programStage.value?.uid(), fieldFactory = null, - eventCreationType = EventCreationType.SCHEDULE, + eventCreationType = launchMode.eventCreationType, onError = resourceManager::parseD2Error, ) configureEventReportDate = ConfigureEventReportDate( - creationType = EventCreationType.SCHEDULE, - resourceProvider = EventDetailResourcesProvider( - enrollment.program().orEmpty(), - programStage.value?.uid(), - resourceManager, - ), + creationType = launchMode.eventCreationType, + resourceProvider = eventDetailResourcesProvider(programId), repository = repository, periodType = programStage.value?.periodType(), periodUtils = periodUtils, - enrollmentId = enrollment.uid(), + enrollmentId = enrollmentId, scheduleInterval = programStage.value?.standardInterval() ?: 0, ) - configureEventCatCombo = ConfigureEventCatCombo( - repository = repository, - ) - loadProgramStage() + configureEventCatCombo = ConfigureEventCatCombo(repository = repository) + + loadProgramStage(event = event) } - private fun loadProgramStage() { + private fun eventDetailResourcesProvider(programId: String) = EventDetailResourcesProvider( + programUid = programId, + programStage = programStage.value?.uid(), + resourceManager = resourceManager, + eventResourcesProvider = eventResourcesProvider, + ) + private fun loadProgramStage(event: Event? = null) { viewModelScope.launch { - configureEventReportDate().collect { + val selectedDate = event?.dueDate() ?: configureEventReportDate.getNextScheduleDate() + configureEventReportDate(selectedDate = selectedDate).collect { _eventDate.value = it } - configureEventCatCombo() - .collect { - _eventCatCombo.value = it - } + configureEventCatCombo().collect { + _eventCatCombo.value = it + } } } @@ -113,21 +195,44 @@ class SchedulingViewModel( fun setUpEventReportDate(selectedDate: Date? = null) { viewModelScope.launch { configureEventReportDate(selectedDate) - .flowOn(Dispatchers.IO) + .flowOn(dispatchersProvider.io()) .collect { _eventDate.value = it + + if (launchMode is LaunchMode.EnterEvent) { + updateEventDueDate( + eventUid = launchMode.eventUid, + dueDate = it, + ) + } } } } + private fun updateEventDueDate(eventUid: String, dueDate: EventDate) { + viewModelScope.launch { + launch(dispatchersProvider.io()) { + d2.eventModule().events().uid(eventUid).run { + setDueDate(dueDate.currentDate) + setStatus(EventStatus.SCHEDULE) + } + } + + onDueDateUpdated?.invoke() + } + } + fun onClearEventReportDate() { - _eventDate.value = eventDate.value.copy(currentDate = null) + _eventDate.value = eventDate.value.copy( + currentDate = null, + dateValue = null, + ) } fun setUpCategoryCombo(categoryOption: Pair? = null) { viewModelScope.launch { configureEventCatCombo(categoryOption) - .flowOn(Dispatchers.IO) + .flowOn(dispatchersProvider.io()) .collect { _eventCatCombo.value = it } @@ -154,29 +259,58 @@ class SchedulingViewModel( fun updateStage(stage: ProgramStage) { _programStage.value = stage - loadConfiguration() + loadScheduleConfiguration(launchMode = launchMode) } - fun scheduleEvent() { + fun scheduleEvent(launchMode: LaunchMode.NewSchedule) { viewModelScope.launch { - eventDate.value.currentDate?.let { date -> - repository.scheduleEvent( - enrollmentUid = enrollment.uid(), - dueDate = date, - orgUnitUid = enrollment.organisationUnit(), - categoryOptionComboUid = eventCatCombo.value.uid, - ).flowOn(Dispatchers.IO) - .collect { - if (it != null) { - onEventScheduled?.invoke(programStage.value?.uid() ?: "") - } + val eventDate = eventDate.value.currentDate ?: return@launch + val enrollment = enrollment.value ?: return@launch + + repository.scheduleEvent( + enrollmentUid = enrollment.uid(), + dueDate = eventDate, + orgUnitUid = enrollment.organisationUnit(), + categoryOptionComboUid = eventCatCombo.value.uid, + ).flowOn(dispatchersProvider.io()) + .collect { + if (it != null) { + onEventScheduled?.invoke(programStage.value?.uid() ?: "") } + } + } + } + + fun enterEvent(launchMode: LaunchMode.EnterEvent) { + viewModelScope.launch { + val event = withContext(dispatchersProvider.io()) { + d2.event(launchMode.eventUid) + } ?: return@launch + val programUid = event.program() ?: return@launch + + d2.eventModule().events().uid(launchMode.eventUid).run { + setEventDate(dateUtils.getStartOfDay(Date())) + setStatus(EventStatus.ACTIVE) } + + onEnterEvent?.invoke( + launchMode.eventUid, + programUid, + ) } } - fun setInitialProgramStage(programStage: ProgramStage) { - _programStage.value = programStage - loadConfiguration() + fun onCancelEvent() { + viewModelScope.launch { + when (launchMode) { + is LaunchMode.EnterEvent -> { + d2.eventModule().events().uid(launchMode.eventUid).setStatus(EventStatus.SKIPPED) + onEventSkipped?.invoke(programStage.value?.displayEventLabel()) + } + is LaunchMode.NewSchedule -> { + // no-op + } + } + } } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt index 47654437e8..dbe9523c9a 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dialogs/scheduling/SchedulingViewModelFactory.kt @@ -2,22 +2,41 @@ package org.dhis2.usescases.teiDashboard.dialogs.scheduling import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import org.dhis2.commons.date.DateUtils import org.dhis2.commons.resources.DhisPeriodUtils +import org.dhis2.commons.resources.EventResourcesProvider import org.dhis2.commons.resources.ResourceManager +import org.dhis2.commons.viewmodel.DispatcherProvider import org.hisp.dhis.android.core.D2 @Suppress("UNCHECKED_CAST") -class SchedulingViewModelFactory( +class SchedulingViewModelFactory @AssistedInject constructor( private val d2: D2, private val resourceManager: ResourceManager, + private val eventResourcesProvider: EventResourcesProvider, private val periodUtils: DhisPeriodUtils, + private val dateUtils: DateUtils, + private val dispatcherProvider: DispatcherProvider, + @Assisted private val launchMode: SchedulingDialog.LaunchMode, ) : ViewModelProvider.Factory { + @AssistedFactory + interface Factory { + fun build(launchMode: SchedulingDialog.LaunchMode): SchedulingViewModelFactory + } + override fun create(modelClass: Class): T { return SchedulingViewModel( - d2, - resourceManager, - periodUtils, + d2 = d2, + resourceManager = resourceManager, + eventResourcesProvider = eventResourcesProvider, + periodUtils = periodUtils, + dateUtils = dateUtils, + dispatchersProvider = dispatcherProvider, + launchMode = launchMode, ) as T } } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptions.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptions.kt index 8969821505..b4befe0658 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptions.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/domain/GetNewEventCreationTypeOptions.kt @@ -4,7 +4,7 @@ import org.dhis2.commons.data.EventCreationType import org.dhis2.commons.data.EventCreationType.ADDNEW import org.dhis2.commons.data.EventCreationType.REFERAL import org.dhis2.commons.data.EventCreationType.SCHEDULE -import org.dhis2.usescases.teiDashboard.data.ProgramConfigurationRepository +import org.dhis2.commons.data.ProgramConfigurationRepository import org.hisp.dhis.android.core.program.ProgramStage class GetNewEventCreationTypeOptions( @@ -16,15 +16,10 @@ class GetNewEventCreationTypeOptions( programUid: String, ): List { val options: MutableList = mutableListOf() - - programStage?.let { - if (shouldShowScheduleEvents(it)) { - options.add(SCHEDULE) - } - } ?: options.add(SCHEDULE) - options.add(ADDNEW) - + if (programStage == null || shouldShowScheduleEvents(programStage)) { + options.add(SCHEDULE) + } if (shouldShowReferralEvents(programUid)) { options.add(REFERAL) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListActivity.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListActivity.java index 46a961c591..d9535cdf9f 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListActivity.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListActivity.java @@ -9,11 +9,10 @@ import org.dhis2.App; import org.dhis2.R; -import org.dhis2.data.service.SyncStatusController; import org.dhis2.databinding.ActivityTeiProgramListBinding; import org.dhis2.ui.ThemeManager; import org.dhis2.usescases.general.ActivityGlobalAbstract; -import org.dhis2.usescases.main.program.ProgramViewModel; +import org.dhis2.usescases.main.program.ProgramUiModel; import java.util.List; @@ -31,8 +30,6 @@ public class TeiProgramListActivity extends ActivityGlobalAbstract implements Te TeiProgramListAdapter adapter; @Inject ThemeManager themeManager; - @Inject - SyncStatusController syncStatusController; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -41,8 +38,6 @@ public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_tei_program_list); binding.setPresenter(presenter); - - syncStatusController.observeDownloadProcess().observe(this, syncStatusData -> presenter.refreshData()); } @Override @@ -74,7 +69,7 @@ public void setOtherEnrollments(List enrollments) { } @Override - public void setPrograms(List programs) { + public void setPrograms(List programs) { if (binding.recycler.getAdapter() == null) { binding.recycler.setAdapter(adapter); } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListAdapter.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListAdapter.java index 82b5e7bebe..a59a6dee69 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListAdapter.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListAdapter.java @@ -10,7 +10,7 @@ import androidx.recyclerview.widget.RecyclerView; import org.dhis2.R; -import org.dhis2.usescases.main.program.ProgramViewModel; +import org.dhis2.usescases.main.program.ProgramUiModel; import java.util.ArrayList; import java.util.List; @@ -21,8 +21,8 @@ public class TeiProgramListAdapter extends RecyclerView.Adapter listItems; private List activeEnrollments; private List inactiveEnrollments; - private List programs; - private List possibleEnrollmentPrograms; + private List programs; + private List possibleEnrollmentPrograms; TeiProgramListAdapter(TeiProgramListContract.Presenter presenter) { this.presenter = presenter; @@ -128,7 +128,7 @@ void setOtherEnrollments(List enrollments) { orderList(); } - void setPrograms(List programs) { + void setPrograms(List programs) { this.programs.clear(); this.programs.addAll(programs); orderList(); @@ -163,7 +163,7 @@ private void orderList() { boolean found; boolean active; - for (ProgramViewModel programModel : programs) { + for (ProgramUiModel programModel : programs) { found = false; active = false; for (EnrollmentViewModel enrollment : activeEnrollments) { @@ -192,7 +192,7 @@ private void orderList() { TeiProgramListItem thirdTeiProgramListItem = new TeiProgramListItem(null, null, TeiProgramListItem.TeiProgramListItemViewType.THIRD_TITLE); listItems.add(thirdTeiProgramListItem); - for (ProgramViewModel programToEnroll : possibleEnrollmentPrograms) { + for (ProgramUiModel programToEnroll : possibleEnrollmentPrograms) { TeiProgramListItem teiProgramListItem = new TeiProgramListItem(null, programToEnroll, TeiProgramListItem.TeiProgramListItemViewType.PROGRAMS_TO_ENROLL); listItems.add(teiProgramListItem); } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListContract.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListContract.java index 5d5974022c..5e6bc13194 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListContract.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListContract.java @@ -1,7 +1,7 @@ package org.dhis2.usescases.teiDashboard.teiProgramList; import org.dhis2.usescases.general.AbstractActivityContracts; -import org.dhis2.usescases.main.program.ProgramViewModel; +import org.dhis2.usescases.main.program.ProgramUiModel; import org.hisp.dhis.android.core.program.Program; import java.util.List; @@ -19,7 +19,7 @@ public interface View extends AbstractActivityContracts.View { void setOtherEnrollments(List enrollments); - void setPrograms(List programs); + void setPrograms(List programs); void goToEnrollmentScreen(String enrollmentUid, String programUid); @@ -35,7 +35,7 @@ public interface Presenter extends AbstractActivityContracts.Presenter { void onBackClick(); - void onEnrollClick(ProgramViewModel program); + void onEnrollClick(ProgramUiModel program); void onActiveEnrollClick(EnrollmentViewModel enrollmentModel); diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListEnrollmentViewHolder.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListEnrollmentViewHolder.kt index 529268b8ec..2a0d7d3162 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListEnrollmentViewHolder.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListEnrollmentViewHolder.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.databinding.ViewDataBinding import androidx.recyclerview.widget.RecyclerView import org.dhis2.BR -import org.dhis2.usescases.main.program.ProgramViewModel +import org.dhis2.usescases.main.program.ProgramUiModel class TeiProgramListEnrollmentViewHolder( private val binding: ViewDataBinding, @@ -22,7 +22,7 @@ class TeiProgramListEnrollmentViewHolder( fun bind( presenter: TeiProgramListContract.Presenter, enrollment: EnrollmentViewModel?, - programModel: ProgramViewModel?, + programModel: ProgramUiModel?, ) { binding.setVariable(BR.enrollment, enrollment) binding.setVariable(BR.program, programModel) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListInteractor.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListInteractor.java index 41400efdc6..e48e8a49fe 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListInteractor.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListInteractor.java @@ -9,9 +9,8 @@ import org.dhis2.commons.orgunitselector.OUTreeFragment; import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope; import org.dhis2.data.service.SyncStatusController; -import org.dhis2.data.service.SyncStatusData; import org.dhis2.usescases.main.program.ProgramDownloadState; -import org.dhis2.usescases.main.program.ProgramViewModel; +import org.dhis2.usescases.main.program.ProgramUiModel; import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; import org.hisp.dhis.android.core.program.Program; import org.jetbrains.annotations.NotNull; @@ -43,7 +42,6 @@ public class TeiProgramListInteractor implements TeiProgramListContract.Interact private Date selectedEnrollmentDate; private PublishProcessor refreshData = PublishProcessor.create(); private SyncStatusController syncStatusController; - private SyncStatusData lastSyncData = null; TeiProgramListInteractor( TeiProgramListRepository teiProgramListRepository, @@ -142,7 +140,6 @@ private void handleCalendarResult( public void enroll(String programUid, String uid) { selectedEnrollmentDate = Calendar.getInstance().getTime(); OUTreeFragment orgUnitDialog = new OUTreeFragment.Builder() - .showAsDialog() .singleSelection() .onSelection(selectedOrgUnits -> { if (!selectedOrgUnits.isEmpty()) @@ -203,8 +200,8 @@ private void getPrograms() { refreshData.startWith(Unit.INSTANCE) .flatMap(unit -> teiProgramListRepository.allPrograms(trackedEntityId)) .map(programViewModels -> { - List programModels = new ArrayList<>(); - for (ProgramViewModel programModel : programViewModels) { + List programModels = new ArrayList<>(); + for (ProgramUiModel programModel : programViewModels) { programModels.add( teiProgramListRepository.updateProgramViewModel( programModel, @@ -222,18 +219,17 @@ private void getPrograms() { ); } - private ProgramDownloadState getSyncState(ProgramViewModel programViewModel) { + private ProgramDownloadState getSyncState(ProgramUiModel programUiModel) { ProgramDownloadState programDownloadState; if (syncStatusController.observeDownloadProcess().getValue().isProgramDownloading( - programViewModel.getUid() + programUiModel.getUid() )) { programDownloadState = ProgramDownloadState.DOWNLOADING; - } else if (syncStatusController.observeDownloadProcess().getValue().wasProgramDownloading( - lastSyncData, - programViewModel.getUid()) - ) { + } else if (syncStatusController.observeDownloadProcess().getValue().isProgramDownloaded( + programUiModel.getUid() + )) { programDownloadState = ProgramDownloadState.DOWNLOADED; - } else if (programViewModel.getDownloadState() == ProgramDownloadState.ERROR) { + } else if (programUiModel.getDownloadState() == ProgramDownloadState.ERROR) { programDownloadState = ProgramDownloadState.ERROR; } else { programDownloadState = ProgramDownloadState.NONE; @@ -241,7 +237,7 @@ private ProgramDownloadState getSyncState(ProgramViewModel programViewModel) { return programDownloadState; } - private void getAlreadyEnrolledPrograms(List programs) { + private void getAlreadyEnrolledPrograms(List programs) { compositeDisposable.add(teiProgramListRepository.alreadyEnrolledPrograms(trackedEntityId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -251,19 +247,19 @@ private void getAlreadyEnrolledPrograms(List programs) { ); } - private void deleteRepeatedPrograms(List allPrograms, List alreadyEnrolledPrograms) { - ArrayList programListToPrint = new ArrayList<>(); - for (ProgramViewModel programViewModel : allPrograms) { + private void deleteRepeatedPrograms(List allPrograms, List alreadyEnrolledPrograms) { + ArrayList programListToPrint = new ArrayList<>(); + for (ProgramUiModel programUiModel : allPrograms) { boolean isAlreadyEnrolled = false; boolean onlyEnrollOnce = false; for (Program program : alreadyEnrolledPrograms) { - if (programViewModel.getUid().equals(program.uid())) { + if (programUiModel.getUid().equals(program.uid())) { isAlreadyEnrolled = true; onlyEnrollOnce = program.onlyEnrollOnce(); } } if (!isAlreadyEnrolled || !onlyEnrollOnce) { - programListToPrint.add(programViewModel); + programListToPrint.add(programUiModel); } } Collections.sort(programListToPrint, (program1, program2) -> program1.getTitle().compareToIgnoreCase(program2.getTitle())); diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListItem.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListItem.java index 4ee0de2019..93ed2508e4 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListItem.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListItem.java @@ -2,7 +2,7 @@ import androidx.annotation.IntDef; -import org.dhis2.usescases.main.program.ProgramViewModel; +import org.dhis2.usescases.main.program.ProgramUiModel; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -30,11 +30,11 @@ public class TeiProgramListItem { } private EnrollmentViewModel enrollmentModel; - private ProgramViewModel programModel; + private ProgramUiModel programModel; private @TeiProgramListItemViewType int viewType; - public TeiProgramListItem(EnrollmentViewModel enrollmentModel, ProgramViewModel programModel, int viewType) { + public TeiProgramListItem(EnrollmentViewModel enrollmentModel, ProgramUiModel programModel, int viewType) { this.enrollmentModel = enrollmentModel; this.programModel = programModel; this.viewType = viewType; @@ -44,7 +44,7 @@ public EnrollmentViewModel getEnrollmentModel() { return enrollmentModel; } - public ProgramViewModel getProgramModel() { + public ProgramUiModel getProgramModel() { return programModel; } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenter.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenter.java index a4b9a6c4a2..cbb9daca74 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenter.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenter.java @@ -2,7 +2,7 @@ import org.dhis2.commons.prefs.Preference; import org.dhis2.commons.prefs.PreferenceProvider; -import org.dhis2.usescases.main.program.ProgramViewModel; +import org.dhis2.usescases.main.program.ProgramUiModel; import org.dhis2.utils.analytics.AnalyticsHelper; import org.hisp.dhis.android.core.enrollment.EnrollmentService; @@ -44,7 +44,7 @@ public void onBackClick() { } @Override - public void onEnrollClick(ProgramViewModel program) { + public void onEnrollClick(ProgramUiModel program) { switch (enrollmentService.blockingGetEnrollmentAccess(teiUid, program.getUid())) { case WRITE_ACCESS: default: diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepository.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepository.java index e369e28f92..a0e21b2d5b 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepository.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepository.java @@ -3,7 +3,7 @@ import androidx.annotation.NonNull; import org.dhis2.usescases.main.program.ProgramDownloadState; -import org.dhis2.usescases.main.program.ProgramViewModel; +import org.dhis2.usescases.main.program.ProgramUiModel; import org.hisp.dhis.android.core.organisationunit.OrganisationUnit; import org.hisp.dhis.android.core.program.Program; @@ -22,7 +22,7 @@ public interface TeiProgramListRepository { Observable> otherEnrollments(String trackedEntityId); @NonNull - Flowable> allPrograms(String trackedEntityId); + Flowable> allPrograms(String trackedEntityId); @NonNull Observable> alreadyEnrolledPrograms(String trackedEntityId); @@ -36,5 +36,5 @@ public interface TeiProgramListRepository { Program getProgram(String programUid); - ProgramViewModel updateProgramViewModel(ProgramViewModel programViewModel, ProgramDownloadState programDownloadState); + ProgramUiModel updateProgramViewModel(ProgramUiModel programUiModel, ProgramDownloadState programDownloadState); } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java index ece43b5b34..cca9c72c9b 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java @@ -5,7 +5,7 @@ import org.dhis2.commons.date.DateUtils; import org.dhis2.commons.resources.MetadataIconProvider; import org.dhis2.usescases.main.program.ProgramDownloadState; -import org.dhis2.usescases.main.program.ProgramViewModel; +import org.dhis2.usescases.main.program.ProgramUiModel; import org.dhis2.usescases.main.program.ProgramViewModelMapper; import org.hisp.dhis.android.core.D2; import org.hisp.dhis.android.core.common.State; @@ -85,7 +85,7 @@ public Observable> otherEnrollments(String trackedEnti @NonNull @Override - public Flowable> allPrograms(String trackedEntityId) { + public Flowable> allPrograms(String trackedEntityId) { String trackedEntityType = d2.trackedEntityModule().trackedEntityInstances().byUid().eq(trackedEntityId).one().blockingGet().trackedEntityType(); return Flowable.just(d2.organisationUnitModule().organisationUnits().byOrganisationUnitScope(OrganisationUnit.Scope.SCOPE_DATA_CAPTURE).blockingGet()) .map(captureOrgUnits -> { @@ -107,8 +107,6 @@ public Flowable> allPrograms(String trackedEntityId) { 0, "", State.SYNCED, - false, - false, metadataIconProvider.invoke(program.style()) ) ) @@ -172,7 +170,7 @@ public Program getProgram(String programUid) { } @Override - public ProgramViewModel updateProgramViewModel(ProgramViewModel programViewModel, ProgramDownloadState programDownloadState) { - return programViewModelMapper.map(programViewModel, programDownloadState); + public ProgramUiModel updateProgramViewModel(ProgramUiModel programUiModel, ProgramDownloadState programDownloadState) { + return programViewModelMapper.map(programUiModel, programDownloadState); } } \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt index 21aaff5a2c..a0411cc345 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt @@ -1,14 +1,14 @@ package org.dhis2.usescases.teiDashboard.teiProgramList.ui import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -26,49 +26,54 @@ import org.dhis2.ui.MetadataIcon import org.dhis2.ui.MetadataIconData import org.dhis2.ui.toColor import org.dhis2.usescases.main.program.ProgramDownloadState -import org.dhis2.usescases.main.program.ProgramViewModel +import org.dhis2.usescases.main.program.ProgramUiModel import org.hisp.dhis.android.core.common.State -import org.hisp.dhis.mobile.ui.designsystem.component.internal.ImageCardData +import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.ImageCardData +import java.util.Date @Composable -fun EnrollToProgram(programViewModel: ProgramViewModel, onEnrollClickListener: () -> Unit) { - Row( - modifier = Modifier - .height(86.dp) - .fillMaxWidth() - .background(color = Color.White) - .padding(horizontal = 21.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - MetadataIcon( +fun EnrollToProgram(programUiModel: ProgramUiModel, onEnrollClickListener: () -> Unit) { + Column { + Row( modifier = Modifier - .width(56.dp) - .height(56.dp) - .alpha(0.5f), - metadataIconData = programViewModel.metadataIconData, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - modifier = Modifier - .weight(2f, true) - .padding(end = 12.dp), - text = programViewModel.title, - fontSize = 14.sp, - ) - Button( + .fillMaxHeight() + .fillMaxWidth() + .background(color = Color.White) + .padding(start = 21.dp, top = 8.dp, end = 21.dp, bottom = 0.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MetadataIcon( + modifier = Modifier + .width(56.dp) + .height(56.dp) + .alpha(0.5f), + metadataIconData = programUiModel.metadataIconData, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + modifier = Modifier + .weight(2f, true) + .padding(end = 12.dp), + text = programUiModel.title, + fontSize = 14.sp, + ) + } + Row( modifier = Modifier - .semantics { testTag = PROGRAM_TO_ENROLL.format(programViewModel.title) } - .height(36.dp) - .weight(1.2f, true) - .padding(end = 16.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = Color.DarkGray, - contentColor = Color.White, - ), - enabled = !programViewModel.isDownloading(), - onClick = onEnrollClickListener, + .padding(top = 4.dp, bottom = 16.dp, end = 16.dp).fillMaxWidth(), ) { - Text(text = stringResource(id = R.string.enroll).uppercase()) + Spacer(modifier = Modifier.width(68.dp).height(0.dp)) + + Button( + text = stringResource(id = R.string.enroll), + modifier = Modifier.fillMaxWidth() + .semantics { testTag = PROGRAM_TO_ENROLL.format(programUiModel.title) }, + enabled = !programUiModel.isDownloading(), + onClick = onEnrollClickListener, + style = ButtonStyle.TONAL, + ) } } } @@ -85,14 +90,14 @@ fun EnrollToProgramDisabledPreview() { EnrollToProgram(testingProgramModel(ProgramDownloadState.DOWNLOADED)) {} } -private fun testingProgramModel(downloadState: ProgramDownloadState) = ProgramViewModel( +private fun testingProgramModel(downloadState: ProgramDownloadState) = ProgramUiModel( uid = "qweqwe", title = "A very long long long program title", metadataIconData = MetadataIconData( imageCardData = ImageCardData.IconCardData( - uid = "", - label = "", - iconRes = "ic_positive_negative", + uid = "7e0cb105-c276-4f12-9f56-a26af8314121", + label = "Stethoscope", + iconRes = "dhis2_stethoscope_positive", iconTint = "#00BCD4".toColor(), ), color = "#00BCD4".toColor(), @@ -105,10 +110,9 @@ private fun testingProgramModel(downloadState: ProgramDownloadState) = ProgramVi onlyEnrollOnce = false, accessDataWrite = true, state = State.SYNCED, - hasOverdueEvent = true, - false, downloadState = downloadState, stockConfig = null, + lastUpdated = Date(), ) const val PROGRAM_TO_ENROLL = "PROGRAM_TO_ENROLL_%s" diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/TeiProgramListBindings.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/TeiProgramListBindings.kt index 131f31ba9b..e826be1586 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/TeiProgramListBindings.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/TeiProgramListBindings.kt @@ -3,12 +3,12 @@ package org.dhis2.usescases.teiDashboard.teiProgramList.ui import androidx.compose.ui.platform.ComposeView import androidx.databinding.BindingAdapter import com.google.accompanist.themeadapter.material3.Mdc3Theme -import org.dhis2.usescases.main.program.ProgramViewModel +import org.dhis2.usescases.main.program.ProgramUiModel import org.dhis2.usescases.teiDashboard.teiProgramList.TeiProgramListContract @BindingAdapter(value = ["setProgramModel", "setPresenter"]) fun ComposeView.setProgramModel( - program: ProgramViewModel, + program: ProgramUiModel, presenter: TeiProgramListContract.Presenter, ) { setContent { diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NewEventOptionsMenu.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NewEventOptionsMenu.kt index 2adc0de062..63f6f43010 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NewEventOptionsMenu.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NewEventOptionsMenu.kt @@ -1,12 +1,9 @@ package org.dhis2.usescases.teiDashboard.ui import androidx.compose.foundation.layout.Column -import androidx.compose.material.DropdownMenu -import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface -import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -22,10 +19,12 @@ import org.dhis2.R import org.dhis2.commons.data.EventCreationType import org.hisp.dhis.mobile.ui.designsystem.component.IconButton import org.hisp.dhis.mobile.ui.designsystem.component.IconButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menu.DropDownMenu +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData @Composable fun NewEventOptions( - options: List, + options: List>, onOptionSelected: (EventCreationType) -> Unit, ) { var expanded by remember { mutableStateOf(false) } @@ -43,21 +42,15 @@ fun NewEventOptions( }, onClick = { expanded = !expanded }, ) - DropdownMenu( + DropDownMenu( + items = options, expanded = expanded, onDismissRequest = { expanded = false }, - ) { - options.forEach { - DropdownMenuItem( - modifier = Modifier.testTag(it.name), - content = { Text(it.name) }, - onClick = { - onOptionSelected.invoke(it.type) - expanded = false - }, - ) - } - } + onItemClick = { + onOptionSelected.invoke(it) + expanded = false + }, + ) } } @@ -69,9 +62,9 @@ fun NewEventOptionsPreview() { ) { NewEventOptions( listOf( - EventCreationOptions( - EventCreationType.SCHEDULE, - "Schedule", + MenuItemData( + EventCreationType.ADDNEW, + "Add new", ), ), ) {} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NoRelationships.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NoRelationships.kt deleted file mode 100644 index ba43e4e19c..0000000000 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/NoRelationships.kt +++ /dev/null @@ -1,67 +0,0 @@ -package org.dhis2.usescases.teiDashboard.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.dhis2.commons.R - -@Composable -fun NoRelationships() { - Column( - modifier = Modifier - .background(Color.White) - .padding(42.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Image( - modifier = Modifier - .padding(1.dp) - .width(139.dp) - .height(125.dp), - painter = painterResource(id = R.drawable.no_relationships), - contentDescription = stringResource(id = org.dhis2.R.string.empty_relationships), - ) - Spacer( - modifier = Modifier - .height(17.dp) - .fillMaxWidth(), - ) - Text( - text = stringResource(id = org.dhis2.R.string.empty_relationships), - style = TextStyle( - fontSize = 17.sp, - lineHeight = 24.sp, - fontWeight = FontWeight.Normal, - color = colorResource(id = R.color.gray_990), - textAlign = TextAlign.Center, - ), - ) - } -} - -@Preview -@Composable -fun NoRelationshipsPreview() { - NoRelationships() -} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TeiDashboardMenu.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TeiDashboardMenu.kt new file mode 100644 index 0000000000..622f88d0e0 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TeiDashboardMenu.kt @@ -0,0 +1,266 @@ +package org.dhis2.usescases.teiDashboard.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Assignment +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Flag +import androidx.compose.material.icons.outlined.LockReset +import androidx.compose.material.icons.outlined.MoveDown +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.outlined.Sync +import androidx.compose.material.icons.outlined.Timeline +import androidx.compose.material.icons.outlined.Workspaces +import org.dhis2.R +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.usescases.teiDashboard.DashboardEnrollmentModel +import org.dhis2.usescases.teiDashboard.DashboardViewModel +import org.dhis2.usescases.teiDashboard.EnrollmentMenuItem +import org.dhis2.usescases.teiDashboard.TeiDashboardContracts +import org.hisp.dhis.android.core.enrollment.EnrollmentStatus +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +fun getEnrollmentMenuList( + enrollmentUid: String?, + resourceManager: ResourceManager, + presenter: TeiDashboardContracts.Presenter, + dashboardViewModel: DashboardViewModel, +): List> { + return if (enrollmentUid == null) { + buildMenuForNoEnrollment(resourceManager, presenter) + } else { + buildMenuForEnrollment(enrollmentUid, resourceManager, presenter, dashboardViewModel) + } +} + +private fun buildMenuForNoEnrollment( + resourceManager: ResourceManager, + presenter: TeiDashboardContracts.Presenter, +): List> { + return buildList { + addSyncMenuItem(resourceManager) + addMoreEnrollmentsMenuItem(resourceManager) + addDeleteTeiMenuItem(presenter, resourceManager) + } +} + +private fun buildMenuForEnrollment( + enrollmentUid: String, + resourceManager: ResourceManager, + presenter: TeiDashboardContracts.Presenter, + dashboardViewModel: DashboardViewModel, +): List> { + return buildList { + addSyncMenuItem(resourceManager) + addIfTeiCanBeTransferred(dashboardViewModel, resourceManager) + addFollowUpMenuItem(dashboardViewModel, resourceManager) + addTimelineOrGroupByStageMenuItem(dashboardViewModel, resourceManager) + addHelpMenuItem(resourceManager) + addMoreEnrollmentsMenuItem(resourceManager) + addShareMenuItem(resourceManager) + addStatusMenuItems(enrollmentUid, resourceManager, presenter) + addRemoveEnrollmentItem(enrollmentUid, resourceManager, presenter, dashboardViewModel) + addDeleteTeiMenuItem(presenter, resourceManager) + } +} + +private fun MutableList>.addSyncMenuItem( + resourceManager: ResourceManager, +) { + add( + MenuItemData( + id = EnrollmentMenuItem.SYNC, + label = resourceManager.getString(R.string.refresh_this_record), + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Sync), + ), + ) +} + +private fun MutableList>.addIfTeiCanBeTransferred( + dashboardViewModel: DashboardViewModel, + resourceManager: ResourceManager, +) { + if (dashboardViewModel.checkIfTeiCanBeTransferred()) { + add( + MenuItemData( + id = EnrollmentMenuItem.TRANSFER, + label = resourceManager.getString(R.string.transfer), + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.MoveDown), + ), + ) + } +} + +private fun MutableList>.addFollowUpMenuItem( + dashboardViewModel: DashboardViewModel, + resourceManager: ResourceManager, +) { + if (!dashboardViewModel.showFollowUpBar.value) { + add( + MenuItemData( + id = EnrollmentMenuItem.FOLLOW_UP, + label = resourceManager.getString(R.string.mark_follow_up), + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Flag), + ), + ) + } +} + +private fun MutableList>.addTimelineOrGroupByStageMenuItem( + dashboardViewModel: DashboardViewModel, + resourceManager: ResourceManager, +) { + if (dashboardViewModel.groupByStage.value != false) { + add( + MenuItemData( + id = EnrollmentMenuItem.VIEW_TIMELINE, + label = resourceManager.getString(R.string.view_timeline), + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Timeline), + ), + ) + } else { + add( + MenuItemData( + id = EnrollmentMenuItem.GROUP_BY_STAGE, + label = resourceManager.getString(R.string.group_by_stage), + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Workspaces), + ), + ) + } +} + +private fun MutableList>.addHelpMenuItem( + resourceManager: ResourceManager, +) { + add( + MenuItemData( + id = EnrollmentMenuItem.HELP, + label = resourceManager.getString(R.string.showHelp), + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.HelpOutline), + ), + ) +} + +private fun MutableList>.addMoreEnrollmentsMenuItem( + resourceManager: ResourceManager, +) { + add( + MenuItemData( + id = EnrollmentMenuItem.ENROLLMENTS, + label = resourceManager.getString(R.string.more_enrollments), + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.Assignment), + ), + ) +} + +private fun MutableList>.addShareMenuItem( + resourceManager: ResourceManager, +) { + add( + MenuItemData( + id = EnrollmentMenuItem.SHARE, + label = resourceManager.getString(R.string.share), + showDivider = true, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Share), + ), + ) +} + +private fun MutableList>.addStatusMenuItems( + enrollmentUid: String, + resourceManager: ResourceManager, + presenter: TeiDashboardContracts.Presenter, +) { + val status = presenter.getEnrollmentStatus(enrollmentUid) + if (status != EnrollmentStatus.COMPLETED) { + add( + MenuItemData( + id = EnrollmentMenuItem.COMPLETE, + label = resourceManager.getString(R.string.complete), + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.CheckCircle, + defaultTintColor = SurfaceColor.CustomGreen, + selectedTintColor = SurfaceColor.CustomGreen, + ), + ), + ) + } + + if (status != EnrollmentStatus.ACTIVE) { + add( + MenuItemData( + id = EnrollmentMenuItem.ACTIVATE, + label = resourceManager.getString(R.string.re_open), + showDivider = status == EnrollmentStatus.CANCELLED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.LockReset, + defaultTintColor = SurfaceColor.Warning, + selectedTintColor = SurfaceColor.Warning, + ), + ), + ) + } + + if (status != EnrollmentStatus.CANCELLED) { + add( + MenuItemData( + id = EnrollmentMenuItem.DEACTIVATE, + label = resourceManager.getString(R.string.deactivate), + showDivider = true, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Cancel, + defaultTintColor = TextColor.OnDisabledSurface, + selectedTintColor = TextColor.OnDisabledSurface, + ), + ), + ) + } +} + +private fun MutableList>.addRemoveEnrollmentItem( + enrollmentUid: String, + resourceManager: ResourceManager, + presenter: TeiDashboardContracts.Presenter, + dashboardViewModel: DashboardViewModel, +) { + if (presenter.checkIfEnrollmentCanBeDeleted(enrollmentUid)) { + val dashboardModel = dashboardViewModel.dashboardModel.value + val programmeName = if (dashboardModel is DashboardEnrollmentModel) { + dashboardModel.currentProgram().displayName() + } else { + "" + } + add( + MenuItemData( + id = EnrollmentMenuItem.REMOVE, + label = resourceManager.getString(R.string.remove_from), + supportingText = programmeName, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteOutline), + ), + ) + } +} + +private fun MutableList>.addDeleteTeiMenuItem( + presenter: TeiDashboardContracts.Presenter, + resourceManager: ResourceManager, +) { + if (presenter.checkIfTEICanBeDeleted()) { + add( + MenuItemData( + id = EnrollmentMenuItem.DELETE, + label = resourceManager.getString(R.string.dashboard_menu_delete_tei_v2, presenter.teType), + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteForever), + ), + ) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TimelineEventsHeader.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TimelineEventsHeader.kt index 4cc30f15dd..588917e6ad 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TimelineEventsHeader.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TimelineEventsHeader.kt @@ -38,10 +38,8 @@ fun TimelineEventsHeader( ) { Title(text = stringResource(id = R.string.timeline)) Description( - text = stringResource( - id = R.string.event_count, - timelineEventsHeaderModel.eventCount, - ), + text = + "${timelineEventsHeaderModel.eventCount} ${timelineEventsHeaderModel.eventLabel}", textColor = TextColor.OnSurfaceLight, ) } @@ -55,7 +53,7 @@ fun TimelineEventsHeader( @Composable private fun TimelineEventHeaderPreview() { TimelineEventsHeader( - timelineEventsHeaderModel = TimelineEventsHeaderModel(true, 3, listOf()), + timelineEventsHeaderModel = TimelineEventsHeaderModel(true, 3, "events", listOf()), onOptionSelected = {}, ) } diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/DetailsButton.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TopBarIcons.kt similarity index 57% rename from app/src/main/java/org/dhis2/usescases/teiDashboard/ui/DetailsButton.kt rename to app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TopBarIcons.kt index 83a8f17a95..a12dc210a1 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/DetailsButton.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/TopBarIcons.kt @@ -1,14 +1,18 @@ package org.dhis2.usescases.teiDashboard.ui -import androidx.compose.material.Icon import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource import org.dhis2.R +import org.dhis2.tracker.relationships.model.RelationshipTopBarIconState import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle +import org.hisp.dhis.mobile.ui.designsystem.component.IconButton fun ComposeView?.setButtonContent(trackedEntityName: String, onButtonClicked: () -> Unit) { this?.setContent { @@ -26,3 +30,22 @@ fun ComposeView?.setButtonContent(trackedEntityName: String, onButtonClicked: () ) } } + +@Composable +fun RelationshipTopBarIcon( + relationshipTopBarIconState: RelationshipTopBarIconState, + onButtonClicked: () -> Unit, +) { + IconButton( + modifier = Modifier, + icon = { + Icon( + imageVector = relationshipTopBarIconState.icon, + contentDescription = stringResource(R.string.relationships), + tint = Color.White, + ) + }, + ) { + onButtonClicked() + } +} diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt index ededf4dca3..a4c3375397 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/mapper/TeiDashboardCardMapper.kt @@ -22,7 +22,7 @@ import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttribute import org.hisp.dhis.android.core.trackedentity.TrackedEntityAttributeValue import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem import org.hisp.dhis.mobile.ui.designsystem.component.Avatar -import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyle +import org.hisp.dhis.mobile.ui.designsystem.component.AvatarStyleData import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import java.io.File import java.util.Date @@ -68,9 +68,10 @@ class TeiDashboardCardMapper( val painter = BitmapPainter(bitmap) Avatar( - imagePainter = painter, + style = AvatarStyleData.Image( + imagePainter = painter, + ), onImageClick = { onImageClick.invoke(file) }, - style = AvatarStyle.IMAGE, ) } @@ -160,7 +161,11 @@ class TeiDashboardCardMapper( item.currentEnrollment.enrollmentDate(), ) }.also { list -> - if (item.orgUnits.isNotEmpty()) { + addOwnedBy( + list, + item.ownerOrgUnit, + ) + if (item.getCurrentOrgUnit() != item.ownerOrgUnit) { addEnrollIn( list, item.getCurrentOrgUnit(), @@ -210,6 +215,19 @@ class TeiDashboardCardMapper( ) } + private fun addOwnedBy( + list: MutableList, + ownedByOrgUnit: OrganisationUnit?, + ) { + list.add( + AdditionalInfoItem( + key = resourceManager.getString(R.string.ownedBy), + value = ownedByOrgUnit?.displayName() ?: "-", + isConstantItem = true, + ), + ) + } + private fun addIncidentDate( list: MutableList, incidentDateLabel: String?, diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/model/TimelineEventsHeaderModel.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/model/TimelineEventsHeaderModel.kt index aebf4f0094..e20ff6c3fa 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/model/TimelineEventsHeaderModel.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/ui/model/TimelineEventsHeaderModel.kt @@ -1,9 +1,11 @@ package org.dhis2.usescases.teiDashboard.ui.model -import org.dhis2.usescases.teiDashboard.ui.EventCreationOptions +import org.dhis2.commons.data.EventCreationType +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData data class TimelineEventsHeaderModel( val displayEventCreationButton: Boolean = true, val eventCount: Int, - val options: List, + val eventLabel: String, + val options: List>, ) diff --git a/app/src/main/java/org/dhis2/usescases/tracker/TrackedEntityInstanceInfoProvider.kt b/app/src/main/java/org/dhis2/usescases/tracker/TrackedEntityInstanceInfoProvider.kt new file mode 100644 index 0000000000..a411dda152 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/tracker/TrackedEntityInstanceInfoProvider.kt @@ -0,0 +1,177 @@ +package org.dhis2.usescases.tracker + +import org.dhis2.commons.date.DateLabelProvider +import org.dhis2.commons.resources.MetadataIconProvider +import org.dhis2.maps.model.MapItemModel +import org.dhis2.maps.model.RelatedInfo +import org.dhis2.maps.model.RelationshipDirection +import org.dhis2.tracker.data.ProfilePictureProvider +import org.dhis2.ui.avatar.AvatarProviderConfiguration +import org.dhis2.ui.avatar.AvatarProviderConfiguration.Metadata +import org.dhis2.ui.avatar.AvatarProviderConfiguration.ProfilePic +import org.dhis2.utils.ValueUtils +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.repositories.scope.RepositoryScope +import org.hisp.dhis.android.core.common.ObjectStyle +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.android.core.program.Program +import org.hisp.dhis.android.core.relationship.Relationship +import org.hisp.dhis.android.core.trackedentity.TrackedEntityInstance +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItem +import org.hisp.dhis.android.core.trackedentity.search.TrackedEntitySearchItemAttribute +import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor + +class TrackedEntityInstanceInfoProvider( + private val d2: D2, + private val profilePictureProvider: ProfilePictureProvider, + private val dateLabelProvider: DateLabelProvider, + private val metadataIconProvider: MetadataIconProvider, +) { + + fun getAvatar( + tei: TrackedEntityInstance, + programUid: String?, + firstAttributeValue: AdditionalInfoItem?, + ): AvatarProviderConfiguration { + val program = programUid?.let { d2.programModule().programs().uid(it).blockingGet() } + val hasIcon = d2.iconModule().icons().key(program?.style()?.icon() ?: "").blockingExists() + val profilePath = profilePictureProvider(tei, programUid) + + return when { + profilePath.isNotEmpty() -> { + ProfilePic( + profilePicturePath = profilePath, + ) + } + + hasIcon && profilePath.isEmpty() -> { + Metadata( + metadataIconData = metadataIconProvider.invoke( + program?.style() ?: ObjectStyle.builder().build(), + ), + ) + } + + else -> { + AvatarProviderConfiguration.MainValueLabel( + firstMainValue = firstAttributeValue?.value?.firstOrNull()?.toString() + ?: "", + ) + } + } + } + + fun getTeiTitle( + header: String?, + attributeValues: List, + ): String { + return when { + header != null -> header + attributeValues.isEmpty() -> "-" + else -> with(attributeValues.first()) { + "$key $value" + } + } + } + + fun getTeiLastUpdated(tei: TrackedEntitySearchItem) = dateLabelProvider.span(tei.lastUpdated) + + fun getTeiAdditionalInfoList( + attributeValues: List, + ): List { + return attributeValues.filter { attribute -> + attribute.displayInList && + !listOf( + ValueType.IMAGE, + ValueType.FILE_RESOURCE, + ValueType.COORDINATE, + ).contains(attribute.valueType) + }.map { attribute -> + AdditionalInfoItem( + key = attribute.displayFormName, + value = if (attribute.value != null) { + ValueUtils.transformValue( + d2, + attribute.value, + attribute.valueType, + attribute.optionSet, + ) ?: "" + } else { + "" + }, + ) + } + } + + fun getRelatedInfo( + searchItem: TrackedEntitySearchItem, + selectedProgram: Program?, + ): RelatedInfo { + val lastEnrollmentInProgram = d2.enrollmentModule().enrollments() + .byTrackedEntityInstance().eq(searchItem.uid) + .byProgram().eq(selectedProgram?.uid()) + .byDeleted().isFalse + .orderByEnrollmentDate(RepositoryScope.OrderByDirection.DESC) + .blockingGet() + .firstOrNull() + + return RelatedInfo( + enrollment = lastEnrollmentInProgram?.let { + RelatedInfo.Enrollment( + uid = lastEnrollmentInProgram.uid(), + geometry = lastEnrollmentInProgram.geometry(), + ) + }, + ) + } + + fun updateRelationshipInfo(model: MapItemModel, relationship: Relationship): MapItemModel { + val relationshipType = d2.relationshipModule().relationshipTypes() + .uid(relationship.relationshipType()) + .blockingGet() + + val relationshipName: String? + val relatedUid: String? + val relationshipDirection: RelationshipDirection? + when (model.uid) { + relationship.from()?.elementUid() -> { + relationshipName = relationshipType?.fromToName() + relatedUid = relationship.to()?.elementUid() + relationshipDirection = RelationshipDirection.FROM + } + + relationship.to()?.elementUid() -> { + relationshipName = relationshipType?.toFromName() + relatedUid = relationship.from()?.elementUid() + relationshipDirection = RelationshipDirection.TO + } + + else -> { + relationshipName = null + relatedUid = null + relationshipDirection = null + } + } + val relationshipInfo = RelatedInfo.Relationship( + uid = relationship.uid(), + displayName = relationshipType?.displayName() ?: "", + relationshipTypeUid = relationshipType?.uid(), + relatedUid = relatedUid, + relationshipDirection = relationshipDirection, + ) + return model.copy( + additionalInfoList = model.additionalInfoList + + AdditionalInfoItem( + value = relationshipName ?: "", + isConstantItem = true, + color = SurfaceColor.Primary, + ), + relatedInfo = model.relatedInfo?.copy( + relationship = relationshipInfo, + ) ?: RelatedInfo( + relationship = relationshipInfo, + ), + ) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingRepository.kt b/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingRepository.kt index 34a06d2656..f0b44329d9 100644 --- a/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/troubleshooting/TroubleshootingRepository.kt @@ -1,8 +1,8 @@ package org.dhis2.usescases.troubleshooting import org.dhis2.commons.resources.MetadataIconProvider -import org.dhis2.form.bindings.toRuleEngineObject -import org.dhis2.form.bindings.toRuleVariableList +import org.dhis2.mobileProgramRules.toRuleEngineObject +import org.dhis2.mobileProgramRules.toRuleVariableList import org.dhis2.usescases.development.ProgramRuleValidation import org.dhis2.usescases.development.RuleValidation import org.hisp.dhis.android.core.D2 @@ -108,7 +108,6 @@ class TroubleshootingRepository( .blockingGet().toRuleVariableList( d2.trackedEntityModule().trackedEntityAttributes(), d2.dataElementModule().dataElements(), - d2.optionModule().options(), ).mapNotNull { val ruleValueType = it.fieldType val valueKey = when (it) { diff --git a/app/src/main/java/org/dhis2/usescases/troubleshooting/ui/TroubleshootingUiPreview.kt b/app/src/main/java/org/dhis2/usescases/troubleshooting/ui/TroubleshootingUiPreview.kt index 300cff9ef4..7949ae804e 100644 --- a/app/src/main/java/org/dhis2/usescases/troubleshooting/ui/TroubleshootingUiPreview.kt +++ b/app/src/main/java/org/dhis2/usescases/troubleshooting/ui/TroubleshootingUiPreview.kt @@ -8,7 +8,7 @@ import org.dhis2.ui.MetadataIconData import org.dhis2.ui.toColor import org.dhis2.usescases.development.ProgramRuleValidation import org.dhis2.usescases.development.RuleValidation -import org.hisp.dhis.mobile.ui.designsystem.component.internal.ImageCardData +import org.hisp.dhis.mobile.ui.designsystem.component.ImageCardData import org.hisp.dhis.rules.models.Rule import java.util.Locale diff --git a/app/src/main/java/org/dhis2/utils/CatComboAdapter.java b/app/src/main/java/org/dhis2/utils/CatComboAdapter.java deleted file mode 100644 index 4a1f271f9f..0000000000 --- a/app/src/main/java/org/dhis2/utils/CatComboAdapter.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.dhis2.utils; - -import android.content.Context; -import androidx.databinding.DataBindingUtil; -import androidx.annotation.ColorRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; - -import org.dhis2.R; -import org.dhis2.databinding.SpinnerLayoutBinding; - -import org.hisp.dhis.android.core.category.CategoryOptionCombo; -import java.util.List; - -/** - * Created by ppajuelo on 12/02/2018. - * - */ - -public class CatComboAdapter extends ArrayAdapter { - - private List options; - private String catComboName; - private @ColorRes int textColor; - - public CatComboAdapter(@NonNull Context context, int resource, int textViewResourceId, - @NonNull List objects, - String categoryOptionName, - @ColorRes int textColor) { - super(context, resource, textViewResourceId, objects); - this.options = objects; - this.catComboName = categoryOptionName; - this.textColor = textColor; - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - SpinnerLayoutBinding binding = SpinnerLayoutBinding.inflate(inflater,parent,false); - if (position != 0) - binding.setOption(options.get(position - 1).displayName()); - binding.setOptionSetName(catComboName); - binding.spinnerText.setTextColor(ContextCompat.getColor(binding.spinnerText.getContext(), textColor)); - return binding.getRoot(); - - } - - @Override - public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - SpinnerLayoutBinding binding = DataBindingUtil.inflate(inflater, R.layout.spinner_layout, parent, false); - if (position != 0) - binding.setOption(options.get(position - 1).displayName()); - - binding.setOptionSetName(catComboName); - return binding.getRoot(); - } - - @Override - public int getCount() { - return super.getCount() + 1; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/utils/HelpManager.java b/app/src/main/java/org/dhis2/utils/HelpManager.java index 01fd4fd278..4c069b37e3 100644 --- a/app/src/main/java/org/dhis2/utils/HelpManager.java +++ b/app/src/main/java/org/dhis2/utils/HelpManager.java @@ -1,5 +1,7 @@ package org.dhis2.utils; +import static org.dhis2.utils.OrientationUtilsKt.isPortrait; + import android.util.SparseBooleanArray; import android.view.Gravity; @@ -140,19 +142,24 @@ private List teiSearchTutorial(ActivityGlobalAbstract activit } private List teiDashboardTutorial(ActivityGlobalAbstract activity) { + FancyShowCaseView tuto2 = null; FancyShowCaseView tuto1 = new FancyShowCaseView.Builder(activity) .title(activity.getString(R.string.tuto_dashboard_1)) .enableAutoTextPosition() .closeOnTouch(true) .build(); - FancyShowCaseView tuto2 = new FancyShowCaseView.Builder(activity) - .title(activity.getString(R.string.tuto_dashboard_2)) - .enableAutoTextPosition() - .focusOn(activity.findViewById(R.id.editButton)) - .focusShape(FocusShape.ROUNDED_RECTANGLE) - .titleGravity(Gravity.BOTTOM) - .closeOnTouch(true) - .build(); + + if (isPortrait()) { + tuto2 = new FancyShowCaseView.Builder(activity) + .title(activity.getString(R.string.tuto_dashboard_2)) + .enableAutoTextPosition() + .focusOn(activity.findViewById(R.id.editButton)) + .focusShape(FocusShape.ROUNDED_RECTANGLE) + .titleGravity(Gravity.BOTTOM) + .closeOnTouch(true) + .build(); + } + FancyShowCaseView tuto3 = new FancyShowCaseView.Builder(activity) .title(activity.getString(R.string.tuto_dashboard_6)) .enableAutoTextPosition() @@ -171,7 +178,7 @@ private List teiDashboardTutorial(ActivityGlobalAbstract acti ArrayList steps = new ArrayList<>(); steps.add(tuto1); - steps.add(tuto2); + if (tuto2 != null) steps.add(tuto2); steps.add(tuto3); steps.add(tuto4); return steps; diff --git a/app/src/main/java/org/dhis2/utils/customviews/FormBottomDialog.kt b/app/src/main/java/org/dhis2/utils/customviews/FormBottomDialog.kt deleted file mode 100644 index 7f722f1ccc..0000000000 --- a/app/src/main/java/org/dhis2/utils/customviews/FormBottomDialog.kt +++ /dev/null @@ -1,113 +0,0 @@ -package org.dhis2.utils.customviews - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import androidx.fragment.app.FragmentManager -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import org.dhis2.databinding.FormBottomDialogBinding - -class FormBottomDialog : BottomSheetDialogFragment() { - private var mListener: OnFormBottomDialogItemSelection? = null - private var mCanComplete = false - private var mReopen = false - private var mSkip = false - private var mReschedule = false - private var mIsEnrollmentOpen = true - private var mAccessDataWrite = true - private var mHasExpired = false - private var mMandatoryFields = false - private var mMessageOnComplete: String? = null - private var mFieldsWithErrors = false - private var mEmptyMandatoryFields: Map = HashMap() - private val presenter = FormBottomDialogPresenter() - - companion object { - @JvmStatic - val instance: FormBottomDialog - get() = FormBottomDialog() - } - - fun setAccessDataWrite(canWrite: Boolean): FormBottomDialog { - mAccessDataWrite = canWrite - return this - } - - fun setSkip(skip: Boolean): FormBottomDialog { - mSkip = skip - return this - } - - fun setReschedule(reschedule: Boolean): FormBottomDialog { - mReschedule = reschedule - return this - } - - fun setIsExpired(hasExpired: Boolean): FormBottomDialog { - mHasExpired = hasExpired - return this - } - - enum class ActionType { - FINISH_ADD_NEW, SKIP, RESCHEDULE, FINISH, COMPLETE_ADD_NEW, COMPLETE, CHECK_FIELDS, NONE - } - - fun setListener(listener: OnFormBottomDialogItemSelection?): FormBottomDialog { - mListener = listener - return this - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - return FormBottomDialogBinding.inflate(inflater, container, false).apply { - canWrite = mAccessDataWrite - isEnrollmentOpen = mIsEnrollmentOpen - hasExpired = mHasExpired - setListener { actionType -> - mListener!!.onActionSelected(actionType) - dismiss() - } - canComplete = mCanComplete - setReopen(mReopen) - setSkip(mSkip) - setReschedule(mReschedule) - mandatoryFields = mMandatoryFields - fieldsWithErrors = mFieldsWithErrors - messageOnComplete = mMessageOnComplete - txtMandatoryFields.text = presenter.appendMandatoryFieldList( - mMandatoryFields, - mEmptyMandatoryFields, - txtMandatoryFields.text.toString(), - ) - }.root - } - - // This is necessary to show the bottomSheet dialog with full height on landscape - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view.viewTreeObserver - .addOnGlobalLayoutListener { - val dialog = dialog as BottomSheetDialog? - val bottomSheet = - dialog!!.findViewById( - com.google.android.material.R.id.design_bottom_sheet, - ) - val behavior: BottomSheetBehavior<*> = - BottomSheetBehavior.from(bottomSheet!!) - behavior.state = BottomSheetBehavior.STATE_EXPANDED - behavior.setPeekHeight(0) - } - } - - override fun show(manager: FragmentManager, tag: String?) { - requireNotNull(mListener) { "Call this method after setting listener" } - super.show(manager, tag) - } -} diff --git a/app/src/main/java/org/dhis2/utils/customviews/FormBottomDialogPresenter.kt b/app/src/main/java/org/dhis2/utils/customviews/FormBottomDialogPresenter.kt deleted file mode 100644 index 9e44d08665..0000000000 --- a/app/src/main/java/org/dhis2/utils/customviews/FormBottomDialogPresenter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.dhis2.utils.customviews - -class FormBottomDialogPresenter { - fun appendMandatoryFieldList( - showMandatoryFields: Boolean, - emptyMandatoryFields: Map, - currentMessage: String, - ): String { - return if (showMandatoryFields) { - currentMessage + "\n" + emptyMandatoryFields.keys.joinToString(separator = "\n") - } else { - currentMessage - } - } -} diff --git a/app/src/main/java/org/dhis2/utils/customviews/LoadingViewHolder.java b/app/src/main/java/org/dhis2/utils/customviews/LoadingViewHolder.java deleted file mode 100644 index 505d260ad7..0000000000 --- a/app/src/main/java/org/dhis2/utils/customviews/LoadingViewHolder.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.dhis2.utils.customviews; - -import org.dhis2.databinding.ItemLoadingBinding; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -/** - * Created by frodriguez on 5/20/2019. - */ -public class LoadingViewHolder extends RecyclerView.ViewHolder { - public LoadingViewHolder(@NonNull ItemLoadingBinding itemView) { - super(itemView.getRoot()); - } - - public void bind(){ - } -} diff --git a/app/src/main/java/org/dhis2/utils/customviews/MoreMenuView.kt b/app/src/main/java/org/dhis2/utils/customviews/MoreMenuView.kt new file mode 100644 index 0000000000..b01a0607f2 --- /dev/null +++ b/app/src/main/java/org/dhis2/utils/customviews/MoreMenuView.kt @@ -0,0 +1,41 @@ +package org.dhis2.utils.customviews + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.hisp.dhis.mobile.ui.designsystem.component.IconButton +import org.hisp.dhis.mobile.ui.designsystem.component.menu.DropDownMenu +import org.hisp.dhis.mobile.ui.designsystem.component.menu.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor + +@Composable +fun MoreOptionsWithDropDownMenuButton( + dropDownMenuItems: List>, + expanded: Boolean, + onMenuToggle: (Boolean) -> Unit, + onItemClick: (T) -> Unit, +) { + IconButton( + modifier = Modifier, + icon = { + Icon( + imageVector = Icons.Outlined.MoreVert, + tint = SurfaceColor.SurfaceBright, + contentDescription = null, + ) + }, + ) { + onMenuToggle(!expanded) + } + + DropDownMenu( + items = dropDownMenuItems, + expanded = expanded, + onDismissRequest = { onMenuToggle(false) }, + ) { itemId -> + onMenuToggle(false) + onItemClick(itemId) + } +} diff --git a/app/src/main/java/org/dhis2/utils/customviews/OnFormBottomDialogItemSelection.java b/app/src/main/java/org/dhis2/utils/customviews/OnFormBottomDialogItemSelection.java deleted file mode 100644 index 808accbd8a..0000000000 --- a/app/src/main/java/org/dhis2/utils/customviews/OnFormBottomDialogItemSelection.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.dhis2.utils.customviews; - -/** - * QUADRAM. Created by ppajuelo on 17/01/2019. - */ -public interface OnFormBottomDialogItemSelection { - void onActionSelected(FormBottomDialog.ActionType actionType); -} diff --git a/app/src/main/java/org/dhis2/utils/customviews/navigationbar/NavigationPageConfigurator.kt b/app/src/main/java/org/dhis2/utils/customviews/navigationbar/NavigationPageConfigurator.kt index ed7857ad9a..d8202977a2 100644 --- a/app/src/main/java/org/dhis2/utils/customviews/navigationbar/NavigationPageConfigurator.kt +++ b/app/src/main/java/org/dhis2/utils/customviews/navigationbar/NavigationPageConfigurator.kt @@ -1,6 +1,7 @@ package org.dhis2.utils.customviews.navigationbar import androidx.annotation.IdRes +import org.hisp.dhis.mobile.ui.designsystem.component.navigationBar.NavigationBarItem interface NavigationPageConfigurator { fun pageVisibility(@IdRes pageId: Int): Boolean { @@ -29,4 +30,5 @@ interface NavigationPageConfigurator { fun displayTableView(): Boolean = false fun displayTasks(): Boolean = false fun displayPrograms(): Boolean = false + fun navigationItems(): List> = emptyList() } diff --git a/app/src/main/java/org/dhis2/utils/extension/SnackbarExtension.kt b/app/src/main/java/org/dhis2/utils/extension/SnackbarExtension.kt new file mode 100644 index 0000000000..b9214f7c7b --- /dev/null +++ b/app/src/main/java/org/dhis2/utils/extension/SnackbarExtension.kt @@ -0,0 +1,24 @@ +package org.dhis2.utils.extension + +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.drawable.Drawable +import android.widget.TextView +import androidx.annotation.ColorInt +import com.google.android.material.snackbar.Snackbar + +fun Snackbar.setIcon( + drawable: Drawable, + @ColorInt colorTint: Int = Color.WHITE, + action: () -> Unit, +): Snackbar { + return this.apply { + setAction(" ") { action() } + val textView = view.findViewById(com.google.android.material.R.id.snackbar_action) + textView.text = "" + + drawable.setTint(colorTint) + drawable.setTintMode(PorterDuff.Mode.SRC_ATOP) + textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) + } +} diff --git a/app/src/main/java/org/dhis2/utils/session/PinPresenter.kt b/app/src/main/java/org/dhis2/utils/session/PinPresenter.kt index c79dcc8a32..3abdbe85e9 100644 --- a/app/src/main/java/org/dhis2/utils/session/PinPresenter.kt +++ b/app/src/main/java/org/dhis2/utils/session/PinPresenter.kt @@ -25,6 +25,7 @@ class PinPresenter( when { pinStored == pin -> { preferenceProvider.setValue(Preference.SESSION_LOCKED, true) + preferenceProvider.setValue(Preference.PIN_ENABLED, true) onPinCorrect() } attempts < 2 -> onError() diff --git a/app/src/main/res/anim/anticipateovershoot_interpolator.xml b/app/src/main/res/anim/anticipateovershoot_interpolator.xml deleted file mode 100644 index 5d65716e2e..0000000000 --- a/app/src/main/res/anim/anticipateovershoot_interpolator.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/bounce_animation.xml b/app/src/main/res/anim/bounce_animation.xml deleted file mode 100644 index 12fa5d822f..0000000000 --- a/app/src/main/res/anim/bounce_animation.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/animator/flip_in_animation.xml b/app/src/main/res/animator/flip_in_animation.xml deleted file mode 100644 index e074431c8d..0000000000 --- a/app/src/main/res/animator/flip_in_animation.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/animator/flip_out_animation.xml b/app/src/main/res/animator/flip_out_animation.xml deleted file mode 100644 index 68eb8f1af0..0000000000 --- a/app/src/main/res/animator/flip_out_animation.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_done_black.xml b/app/src/main/res/drawable-anydpi/ic_done_black.xml deleted file mode 100644 index 517c3de8e0..0000000000 --- a/app/src/main/res/drawable-anydpi/ic_done_black.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-anydpi/ic_sync_black.xml b/app/src/main/res/drawable-anydpi/ic_sync_black.xml deleted file mode 100644 index 1baf24c54e..0000000000 --- a/app/src/main/res/drawable-anydpi/ic_sync_black.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-anydpi/ic_sync_error_black.xml b/app/src/main/res/drawable-anydpi/ic_sync_error_black.xml deleted file mode 100644 index 5e4446798d..0000000000 --- a/app/src/main/res/drawable-anydpi/ic_sync_error_black.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_bg_focused.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_bg_focused.9.png deleted file mode 100644 index 901a0803a0..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_bg_pressed.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_bg_pressed.9.png deleted file mode 100644 index efacbcfa64..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_bg_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_dark_bg.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_dark_bg.9.png deleted file mode 100644 index 02ee4401f9..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_dark_bg.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_dark_bg_focused.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_dark_bg_focused.9.png deleted file mode 100644 index ccc0177457..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_dark_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_c.9.png deleted file mode 100644 index 5aafacd9ca..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_l.9.png deleted file mode 100644 index ab6e8f31e8..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_r.9.png deleted file mode 100644 index 6c1b3c624e..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_c.9.png deleted file mode 100644 index 470f5c0389..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_l.9.png deleted file mode 100644 index e3aa8db1a3..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_r.9.png deleted file mode 100644 index 9e27d2fdc7..0000000000 Binary files a/app/src/main/res/drawable-hdpi-v14/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_bg_focused.9.png b/app/src/main/res/drawable-hdpi/appwidget_bg_focused.9.png deleted file mode 100644 index ee098af160..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_bg_pressed.9.png b/app/src/main/res/drawable-hdpi/appwidget_bg_pressed.9.png deleted file mode 100644 index 03ca2a1f3d..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_bg_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_dark_bg.9.png b/app/src/main/res/drawable-hdpi/appwidget_dark_bg.9.png deleted file mode 100644 index 3b29eae77b..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_dark_bg.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_dark_bg_focused.9.png b/app/src/main/res/drawable-hdpi/appwidget_dark_bg_focused.9.png deleted file mode 100644 index 9fae722ffc..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_dark_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_dark_bg_pressed.9.png b/app/src/main/res/drawable-hdpi/appwidget_dark_bg_pressed.9.png deleted file mode 100644 index 8df4c69cde..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_dark_bg_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_focused_c.9.png deleted file mode 100644 index a949bd2c3b..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_focused_l.9.png deleted file mode 100644 index 4aaca6c504..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_focused_r.9.png deleted file mode 100644 index 1fc0f900af..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_c.9.png deleted file mode 100644 index ca6f16cd1d..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_l.9.png deleted file mode 100644 index 642eb3d326..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_r.9.png deleted file mode 100644 index 5e1f70a39b..0000000000 Binary files a/app/src/main/res/drawable-hdpi/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_done_black.png b/app/src/main/res/drawable-hdpi/ic_done_black.png deleted file mode 100644 index 500dd3a829..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_done_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_sync_black.png b/app/src/main/res/drawable-hdpi/ic_sync_black.png deleted file mode 100644 index d8f5f0ac15..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_sync_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_sync_error_black.png b/app/src/main/res/drawable-hdpi/ic_sync_error_black.png deleted file mode 100644 index 67accdc494..0000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_sync_error_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_bg_focused.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_bg_focused.9.png deleted file mode 100644 index 5044f8419d..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_bg_pressed.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_bg_pressed.9.png deleted file mode 100644 index 5c03b8ab9f..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_bg_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_dark_bg.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_dark_bg.9.png deleted file mode 100644 index a245d91779..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_dark_bg.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_dark_bg_focused.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_dark_bg_focused.9.png deleted file mode 100644 index fa2d6826cc..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_dark_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_c.9.png deleted file mode 100644 index cc50fe9be6..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_l.9.png deleted file mode 100644 index feaa6c7858..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_r.9.png deleted file mode 100644 index 8d22c56178..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_c.9.png deleted file mode 100644 index aa80a7c70e..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_l.9.png deleted file mode 100644 index e49e8a9b89..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_r.9.png deleted file mode 100644 index a54ecd0da3..0000000000 Binary files a/app/src/main/res/drawable-mdpi-v14/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_bg_focused.9.png b/app/src/main/res/drawable-mdpi/appwidget_bg_focused.9.png deleted file mode 100644 index f4bbb08eb8..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_bg_pressed.9.png b/app/src/main/res/drawable-mdpi/appwidget_bg_pressed.9.png deleted file mode 100644 index d060b77556..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_bg_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_dark_bg.9.png b/app/src/main/res/drawable-mdpi/appwidget_dark_bg.9.png deleted file mode 100644 index afe41b671e..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_dark_bg.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_dark_bg_focused.9.png b/app/src/main/res/drawable-mdpi/appwidget_dark_bg_focused.9.png deleted file mode 100644 index 8b4ce3f007..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_dark_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_dark_bg_pressed.9.png b/app/src/main/res/drawable-mdpi/appwidget_dark_bg_pressed.9.png deleted file mode 100644 index ca8d5ac221..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_dark_bg_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_focused_c.9.png deleted file mode 100644 index 1450e65b11..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_focused_l.9.png deleted file mode 100644 index 6e8f100e4c..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_focused_r.9.png deleted file mode 100644 index bc8757b88c..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_c.9.png deleted file mode 100644 index bd542bac6e..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_l.9.png deleted file mode 100644 index 575ecf4e13..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_r.9.png deleted file mode 100644 index 79eaea35a6..0000000000 Binary files a/app/src/main/res/drawable-mdpi/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_done_black.png b/app/src/main/res/drawable-mdpi/ic_done_black.png deleted file mode 100644 index bef7123f2a..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_done_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_sync_black.png b/app/src/main/res/drawable-mdpi/ic_sync_black.png deleted file mode 100644 index f89286e7eb..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_sync_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_sync_error_black.png b/app/src/main/res/drawable-mdpi/ic_sync_error_black.png deleted file mode 100644 index 124e0802ab..0000000000 Binary files a/app/src/main/res/drawable-mdpi/ic_sync_error_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/example_appwidget_preview.png b/app/src/main/res/drawable-nodpi/example_appwidget_preview.png deleted file mode 100644 index 894b069a49..0000000000 Binary files a/app/src/main/res/drawable-nodpi/example_appwidget_preview.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/sms_sync_icon.png b/app/src/main/res/drawable-nodpi/sms_sync_icon.png deleted file mode 100644 index 105c3e937c..0000000000 Binary files a/app/src/main/res/drawable-nodpi/sms_sync_icon.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/widget_preview_dark.png b/app/src/main/res/drawable-nodpi/widget_preview_dark.png deleted file mode 100644 index ba4bcee1b3..0000000000 Binary files a/app/src/main/res/drawable-nodpi/widget_preview_dark.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/widget_preview_light.png b/app/src/main/res/drawable-nodpi/widget_preview_light.png deleted file mode 100644 index 9d3bd6c2a7..0000000000 Binary files a/app/src/main/res/drawable-nodpi/widget_preview_light.png and /dev/null differ diff --git a/app/src/main/res/drawable-v19/item_selected_bg.xml b/app/src/main/res/drawable-v19/item_selected_bg.xml deleted file mode 100644 index 3040935f4e..0000000000 --- a/app/src/main/res/drawable-v19/item_selected_bg.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/full_width_button_bg.xml b/app/src/main/res/drawable-v21/full_width_button_bg.xml deleted file mode 100644 index b71c1ad77f..0000000000 --- a/app/src/main/res/drawable-v21/full_width_button_bg.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/item_event_gray_ripple.xml b/app/src/main/res/drawable-v21/item_event_gray_ripple.xml deleted file mode 100644 index 095f54db90..0000000000 --- a/app/src/main/res/drawable-v21/item_event_gray_ripple.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/item_event_green_ripple.xml b/app/src/main/res/drawable-v21/item_event_green_ripple.xml deleted file mode 100644 index e09a0ef4d0..0000000000 --- a/app/src/main/res/drawable-v21/item_event_green_ripple.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/item_event_red_ripple.xml b/app/src/main/res/drawable-v21/item_event_red_ripple.xml deleted file mode 100644 index fe7b45bf01..0000000000 --- a/app/src/main/res/drawable-v21/item_event_red_ripple.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/item_event_yellow_ripple.xml b/app/src/main/res/drawable-v21/item_event_yellow_ripple.xml deleted file mode 100644 index 9606470264..0000000000 --- a/app/src/main/res/drawable-v21/item_event_yellow_ripple.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/white_circle_ripple.xml b/app/src/main/res/drawable-v21/white_circle_ripple.xml deleted file mode 100644 index e20024795f..0000000000 --- a/app/src/main/res/drawable-v21/white_circle_ripple.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_bg_focused.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_bg_focused.9.png deleted file mode 100644 index fccb4d9063..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_bg_pressed.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_bg_pressed.9.png deleted file mode 100644 index 97a3ba093f..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_bg_pressed.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_dark_bg.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_dark_bg.9.png deleted file mode 100644 index 7ccb762be0..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_dark_bg.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_dark_bg_focused.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_dark_bg_focused.9.png deleted file mode 100644 index da9289e673..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_dark_bg_focused.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_c.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_c.9.png deleted file mode 100644 index 0de253ca00..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_l.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_l.9.png deleted file mode 100644 index ce9decd19c..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_r.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_r.9.png deleted file mode 100644 index 448cd83730..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_focused_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_c.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_c.9.png deleted file mode 100644 index defdbb9c00..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_c.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_l.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_l.9.png deleted file mode 100644 index 582d0e1973..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_l.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_r.9.png b/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_r.9.png deleted file mode 100644 index 9732dd7c55..0000000000 Binary files a/app/src/main/res/drawable-xhdpi-v14/appwidget_inner_pressed_r.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_done_black.png b/app/src/main/res/drawable-xhdpi/ic_done_black.png deleted file mode 100644 index b7f59ada04..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_done_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_sync_black.png b/app/src/main/res/drawable-xhdpi/ic_sync_black.png deleted file mode 100644 index 7f841effe5..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_sync_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_sync_error_black.png b/app/src/main/res/drawable-xhdpi/ic_sync_error_black.png deleted file mode 100644 index 5b7f2bd6bb..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_sync_error_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_done_black.png b/app/src/main/res/drawable-xxhdpi/ic_done_black.png deleted file mode 100644 index 8545305c62..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_done_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_sync_black.png b/app/src/main/res/drawable-xxhdpi/ic_sync_black.png deleted file mode 100644 index 75443dd0a1..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_sync_black.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_sync_error_black.png b/app/src/main/res/drawable-xxhdpi/ic_sync_error_black.png deleted file mode 100644 index 1366b50266..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_sync_error_black.png and /dev/null differ diff --git a/app/src/main/res/drawable/animated_sync_grey.xml b/app/src/main/res/drawable/animated_sync_grey.xml deleted file mode 100644 index bcd4a8e62a..0000000000 --- a/app/src/main/res/drawable/animated_sync_grey.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/animator_sync_grey.xml b/app/src/main/res/drawable/animator_sync_grey.xml deleted file mode 100644 index 40e1a16c41..0000000000 --- a/app/src/main/res/drawable/animator_sync_grey.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/appwidget_bg_clickable.xml b/app/src/main/res/drawable/appwidget_bg_clickable.xml deleted file mode 100644 index dde1cb5bef..0000000000 --- a/app/src/main/res/drawable/appwidget_bg_clickable.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/appwidget_button_center.xml b/app/src/main/res/drawable/appwidget_button_center.xml deleted file mode 100644 index 06f5f5738f..0000000000 --- a/app/src/main/res/drawable/appwidget_button_center.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/appwidget_button_left.xml b/app/src/main/res/drawable/appwidget_button_left.xml deleted file mode 100644 index 7382f05ff3..0000000000 --- a/app/src/main/res/drawable/appwidget_button_left.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/appwidget_button_right.xml b/app/src/main/res/drawable/appwidget_button_right.xml deleted file mode 100644 index a81225917d..0000000000 --- a/app/src/main/res/drawable/appwidget_button_right.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/appwidget_dark_bg_clickable.xml b/app/src/main/res/drawable/appwidget_dark_bg_clickable.xml deleted file mode 100644 index 7cfc9d2b85..0000000000 --- a/app/src/main/res/drawable/appwidget_dark_bg_clickable.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/bg_chip.xml b/app/src/main/res/drawable/bg_chip.xml deleted file mode 100644 index 4d22691b62..0000000000 --- a/app/src/main/res/drawable/bg_chip.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_image_marker.xml b/app/src/main/res/drawable/bg_image_marker.xml deleted file mode 100644 index 48d1a72a0a..0000000000 --- a/app/src/main/res/drawable/bg_image_marker.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_rounded_corners.xml b/app/src/main/res/drawable/bg_rounded_corners.xml deleted file mode 100644 index 376d904247..0000000000 --- a/app/src/main/res/drawable/bg_rounded_corners.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_rounded_top_left.xml b/app/src/main/res/drawable/bg_rounded_top_left.xml deleted file mode 100644 index d05fbb8284..0000000000 --- a/app/src/main/res/drawable/bg_rounded_top_left.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_section_error.xml b/app/src/main/res/drawable/bg_section_error.xml deleted file mode 100644 index 2fc7930202..0000000000 --- a/app/src/main/res/drawable/bg_section_error.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_section_warning.xml b/app/src/main/res/drawable/bg_section_warning.xml deleted file mode 100644 index db075c28a5..0000000000 --- a/app/src/main/res/drawable/bg_section_warning.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_delete.xml b/app/src/main/res/drawable/button_delete.xml deleted file mode 100644 index 5368627f95..0000000000 --- a/app/src/main/res/drawable/button_delete.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_delete_pressed.xml b/app/src/main/res/drawable/button_delete_pressed.xml deleted file mode 100644 index 2cb49f4be7..0000000000 --- a/app/src/main/res/drawable/button_delete_pressed.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_delete_selector.xml b/app/src/main/res/drawable/button_delete_selector.xml deleted file mode 100644 index 750c0d2442..0000000000 --- a/app/src/main/res/drawable/button_delete_selector.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_shape.xml b/app/src/main/res/drawable/circle_shape.xml deleted file mode 100644 index 7c02d191cd..0000000000 --- a/app/src/main/res/drawable/circle_shape.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_shape_thick.xml b/app/src/main/res/drawable/circle_shape_thick.xml deleted file mode 100644 index f0be8f70d9..0000000000 --- a/app/src/main/res/drawable/circle_shape_thick.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/edit_text_hint_primary.xml b/app/src/main/res/drawable/edit_text_hint_primary.xml deleted file mode 100644 index ad2b8aaf83..0000000000 --- a/app/src/main/res/drawable/edit_text_hint_primary.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/edittext_material_border.xml b/app/src/main/res/drawable/edittext_material_border.xml deleted file mode 100644 index d0f4980d24..0000000000 --- a/app/src/main/res/drawable/edittext_material_border.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/enroll_button_disabled.xml b/app/src/main/res/drawable/enroll_button_disabled.xml deleted file mode 100644 index e313916b3e..0000000000 --- a/app/src/main/res/drawable/enroll_button_disabled.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/enroll_button_enabled.xml b/app/src/main/res/drawable/enroll_button_enabled.xml deleted file mode 100644 index c31499bd4f..0000000000 --- a/app/src/main/res/drawable/enroll_button_enabled.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/full_width_button_bg.xml b/app/src/main/res/drawable/full_width_button_bg.xml deleted file mode 100644 index e963d7e6e6..0000000000 --- a/app/src/main/res/drawable/full_width_button_bg.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/gray_border_box.xml b/app/src/main/res/drawable/gray_border_box.xml deleted file mode 100644 index 017246ae08..0000000000 --- a/app/src/main/res/drawable/gray_border_box.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/gray_border_gray_solid_box.xml b/app/src/main/res/drawable/gray_border_gray_solid_box.xml deleted file mode 100644 index 34bab877b9..0000000000 --- a/app/src/main/res/drawable/gray_border_gray_solid_box.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/gray_button_round_7.xml b/app/src/main/res/drawable/gray_button_round_7.xml deleted file mode 100644 index 8f04dfa77f..0000000000 --- a/app/src/main/res/drawable/gray_button_round_7.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/gray_button_round_7_pressed.xml b/app/src/main/res/drawable/gray_button_round_7_pressed.xml deleted file mode 100644 index db566e0cf6..0000000000 --- a/app/src/main/res/drawable/gray_button_round_7_pressed.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/green_circle.xml b/app/src/main/res/drawable/green_circle.xml deleted file mode 100644 index c6c287eec7..0000000000 --- a/app/src/main/res/drawable/green_circle.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_circle.xml b/app/src/main/res/drawable/ic_add_circle.xml deleted file mode 100644 index a4bbf9a74b..0000000000 --- a/app/src/main/res/drawable/ic_add_circle.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_alert_red.xml b/app/src/main/res/drawable/ic_alert_red.xml deleted file mode 100644 index 33a1da843e..0000000000 --- a/app/src/main/res/drawable/ic_alert_red.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_arrow_down_white.xml b/app/src/main/res/drawable/ic_arrow_down_white.xml deleted file mode 100644 index f50ba2cc5c..0000000000 --- a/app/src/main/res/drawable/ic_arrow_down_white.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_bottom_right_corner_circle.xml b/app/src/main/res/drawable/ic_bottom_right_corner_circle.xml deleted file mode 100644 index f410be4194..0000000000 --- a/app/src/main/res/drawable/ic_bottom_right_corner_circle.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bottom_sheet_dialog.xml b/app/src/main/res/drawable/ic_bottom_sheet_dialog.xml deleted file mode 100644 index e45024ad01..0000000000 --- a/app/src/main/res/drawable/ic_bottom_sheet_dialog.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml deleted file mode 100644 index 3d2ba42f3e..0000000000 --- a/app/src/main/res/drawable/ic_camera.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_cancel_white.xml b/app/src/main/res/drawable/ic_cancel_white.xml deleted file mode 100644 index e9741ac734..0000000000 --- a/app/src/main/res/drawable/ic_cancel_white.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml deleted file mode 100644 index 1241edabdb..0000000000 --- a/app/src/main/res/drawable/ic_check_circle.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_check_circle_36.xml b/app/src/main/res/drawable/ic_check_circle_36.xml deleted file mode 100644 index fe3586df18..0000000000 --- a/app/src/main/res/drawable/ic_check_circle_36.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_circle_primary.xml b/app/src/main/res/drawable/ic_circle_primary.xml deleted file mode 100644 index e12c6a2d70..0000000000 --- a/app/src/main/res/drawable/ic_circle_primary.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_circle_red.xml b/app/src/main/res/drawable/ic_circle_red.xml deleted file mode 100644 index 253dcc4caf..0000000000 --- a/app/src/main/res/drawable/ic_circle_red.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_circle_white.xml b/app/src/main/res/drawable/ic_circle_white.xml deleted file mode 100644 index 2fbebe06ba..0000000000 --- a/app/src/main/res/drawable/ic_circle_white.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_clear_red.xml b/app/src/main/res/drawable/ic_clear_red.xml deleted file mode 100644 index 891ab1b3f9..0000000000 --- a/app/src/main/res/drawable/ic_clear_red.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_cloud_download.xml b/app/src/main/res/drawable/ic_cloud_download.xml deleted file mode 100644 index d7c2fa7222..0000000000 --- a/app/src/main/res/drawable/ic_cloud_download.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_compass_ripple.xml b/app/src/main/res/drawable/ic_compass_ripple.xml index 6854eff699..b4edc7d194 100644 --- a/app/src/main/res/drawable/ic_compass_ripple.xml +++ b/app/src/main/res/drawable/ic_compass_ripple.xml @@ -1,4 +1,4 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_complete.xml b/app/src/main/res/drawable/ic_complete.xml deleted file mode 100644 index 09d822d5f3..0000000000 --- a/app/src/main/res/drawable/ic_complete.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_dhistraining.xml b/app/src/main/res/drawable/ic_dhistraining.xml deleted file mode 100644 index a1173bbc4e..0000000000 --- a/app/src/main/res/drawable/ic_dhistraining.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_done_black.xml b/app/src/main/res/drawable/ic_done_black.xml deleted file mode 100644 index 7affe9ba9f..0000000000 --- a/app/src/main/res/drawable/ic_done_black.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_download_off.xml b/app/src/main/res/drawable/ic_download_off.xml new file mode 100644 index 0000000000..db6e4615c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_download_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_duplicate.xml b/app/src/main/res/drawable/ic_duplicate.xml deleted file mode 100644 index 3d8eb210f7..0000000000 --- a/app/src/main/res/drawable/ic_duplicate.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml deleted file mode 100644 index 2d41283bc2..0000000000 --- a/app/src/main/res/drawable/ic_edit.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_edit_green.xml b/app/src/main/res/drawable/ic_edit_green.xml deleted file mode 100644 index e60b1c32c9..0000000000 --- a/app/src/main/res/drawable/ic_edit_green.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_edit_yellow.xml b/app/src/main/res/drawable/ic_edit_yellow.xml deleted file mode 100644 index 50123ad31e..0000000000 --- a/app/src/main/res/drawable/ic_edit_yellow.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_error.xml b/app/src/main/res/drawable/ic_error.xml deleted file mode 100644 index 983b548072..0000000000 --- a/app/src/main/res/drawable/ic_error.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_eye_green.xml b/app/src/main/res/drawable/ic_eye_green.xml deleted file mode 100644 index 730b53017e..0000000000 --- a/app/src/main/res/drawable/ic_eye_green.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_eye_grey.xml b/app/src/main/res/drawable/ic_eye_grey.xml deleted file mode 100644 index aa0c3e88c5..0000000000 --- a/app/src/main/res/drawable/ic_eye_grey.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_eye_red.xml b/app/src/main/res/drawable/ic_eye_red.xml deleted file mode 100644 index df5aa0a95c..0000000000 --- a/app/src/main/res/drawable/ic_eye_red.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_fab_bg.xml b/app/src/main/res/drawable/ic_fab_bg.xml deleted file mode 100644 index 48d5889ff0..0000000000 --- a/app/src/main/res/drawable/ic_fab_bg.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_fab_bg_compat.xml b/app/src/main/res/drawable/ic_fab_bg_compat.xml deleted file mode 100644 index 42f871d41f..0000000000 --- a/app/src/main/res/drawable/ic_fab_bg_compat.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_field_default.xml b/app/src/main/res/drawable/ic_field_default.xml deleted file mode 100644 index bf9b895aca..0000000000 --- a/app/src/main/res/drawable/ic_field_default.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_file_upload.xml b/app/src/main/res/drawable/ic_file_upload.xml deleted file mode 100644 index d6339722a1..0000000000 --- a/app/src/main/res/drawable/ic_file_upload.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_fingerprint.xml b/app/src/main/res/drawable/ic_fingerprint.xml index f650f74452..9ae1b0d18d 100644 --- a/app/src/main/res/drawable/ic_fingerprint.xml +++ b/app/src/main/res/drawable/ic_fingerprint.xml @@ -1,9 +1,9 @@ - + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48"> + diff --git a/app/src/main/res/drawable/ic_form_decimal.xml b/app/src/main/res/drawable/ic_form_decimal.xml deleted file mode 100644 index d733a003f4..0000000000 --- a/app/src/main/res/drawable/ic_form_decimal.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_form_document.xml b/app/src/main/res/drawable/ic_form_document.xml deleted file mode 100644 index f82b706074..0000000000 --- a/app/src/main/res/drawable/ic_form_document.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_form_image.xml b/app/src/main/res/drawable/ic_form_image.xml deleted file mode 100644 index 51e13557a7..0000000000 --- a/app/src/main/res/drawable/ic_form_image.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_form_letter.xml b/app/src/main/res/drawable/ic_form_letter.xml deleted file mode 100644 index 3280f71479..0000000000 --- a/app/src/main/res/drawable/ic_form_letter.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_form_link.xml b/app/src/main/res/drawable/ic_form_link.xml deleted file mode 100644 index 3b45c727fc..0000000000 --- a/app/src/main/res/drawable/ic_form_link.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_form_lock.xml b/app/src/main/res/drawable/ic_form_lock.xml deleted file mode 100644 index 45629c3179..0000000000 --- a/app/src/main/res/drawable/ic_form_lock.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_form_number.xml b/app/src/main/res/drawable/ic_form_number.xml deleted file mode 100644 index e0c6b4008c..0000000000 --- a/app/src/main/res/drawable/ic_form_number.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_form_person.xml b/app/src/main/res/drawable/ic_form_person.xml deleted file mode 100644 index 85676cc0af..0000000000 --- a/app/src/main/res/drawable/ic_form_person.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_form_sub.xml b/app/src/main/res/drawable/ic_form_sub.xml deleted file mode 100644 index 32c8387e1c..0000000000 --- a/app/src/main/res/drawable/ic_form_sub.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_form_text.xml b/app/src/main/res/drawable/ic_form_text.xml deleted file mode 100644 index fb8cf0a4a3..0000000000 --- a/app/src/main/res/drawable/ic_form_text.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_front_backdrop_bg_landscape.xml b/app/src/main/res/drawable/ic_front_backdrop_bg_landscape.xml deleted file mode 100644 index e65682a01f..0000000000 --- a/app/src/main/res/drawable/ic_front_backdrop_bg_landscape.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_i_hide_pss.xml b/app/src/main/res/drawable/ic_i_hide_pss.xml deleted file mode 100644 index 2c35837956..0000000000 --- a/app/src/main/res/drawable/ic_i_hide_pss.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_i_show_pss.xml b/app/src/main/res/drawable/ic_i_show_pss.xml deleted file mode 100644 index bfbacb4288..0000000000 --- a/app/src/main/res/drawable/ic_i_show_pss.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_img_marker_frame.xml b/app/src/main/res/drawable/ic_img_marker_frame.xml deleted file mode 100644 index c5c1c727fe..0000000000 --- a/app/src/main/res/drawable/ic_img_marker_frame.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_increase_width.xml b/app/src/main/res/drawable/ic_increase_width.xml deleted file mode 100644 index 59f08f114c..0000000000 --- a/app/src/main/res/drawable/ic_increase_width.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_info_layer.xml b/app/src/main/res/drawable/ic_info_layer.xml deleted file mode 100644 index 93ae6b4243..0000000000 --- a/app/src/main/res/drawable/ic_info_layer.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_insert_link.xml b/app/src/main/res/drawable/ic_insert_link.xml deleted file mode 100644 index f7a8f2f0c4..0000000000 --- a/app/src/main/res/drawable/ic_insert_link.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_left.xml b/app/src/main/res/drawable/ic_keyboard_arrow_left.xml deleted file mode 100644 index c9f7747e21..0000000000 --- a/app/src/main/res/drawable/ic_keyboard_arrow_left.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_layers.xml b/app/src/main/res/drawable/ic_layers.xml new file mode 100644 index 0000000000..f99f6eb5d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_layers.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock_white.xml b/app/src/main/res/drawable/ic_lock_white.xml deleted file mode 100644 index 37b66998a3..0000000000 --- a/app/src/main/res/drawable/ic_lock_white.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/form/src/main/res/drawable/ic_menu_info.xml b/app/src/main/res/drawable/ic_menu_info.xml similarity index 100% rename from form/src/main/res/drawable/ic_menu_info.xml rename to app/src/main/res/drawable/ic_menu_info.xml diff --git a/app/src/main/res/drawable/ic_menu_qr.xml b/app/src/main/res/drawable/ic_menu_qr.xml deleted file mode 100644 index 9b0ac1f545..0000000000 --- a/app/src/main/res/drawable/ic_menu_qr.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_my_location.xml b/app/src/main/res/drawable/ic_my_location.xml deleted file mode 100644 index 61712dafa9..0000000000 --- a/app/src/main/res/drawable/ic_my_location.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml deleted file mode 100644 index b2cb337b0d..0000000000 --- a/app/src/main/res/drawable/ic_person.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_place.xml b/app/src/main/res/drawable/ic_place.xml deleted file mode 100644 index 7bf6974e2b..0000000000 --- a/app/src/main/res/drawable/ic_place.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_place_white.xml b/app/src/main/res/drawable/ic_place_white.xml deleted file mode 100644 index cf442c3fa0..0000000000 --- a/app/src/main/res/drawable/ic_place_white.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_program.xml b/app/src/main/res/drawable/ic_program.xml deleted file mode 100644 index b92f1d96ac..0000000000 --- a/app/src/main/res/drawable/ic_program.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_read_only.xml b/app/src/main/res/drawable/ic_read_only.xml deleted file mode 100644 index c3812f4ce2..0000000000 --- a/app/src/main/res/drawable/ic_read_only.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_reduce_width.xml b/app/src/main/res/drawable/ic_reduce_width.xml deleted file mode 100644 index d09a46804e..0000000000 --- a/app/src/main/res/drawable/ic_reduce_width.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_remove_circle.xml b/app/src/main/res/drawable/ic_remove_circle.xml deleted file mode 100644 index b029878859..0000000000 --- a/app/src/main/res/drawable/ic_remove_circle.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_remove_circle_primary.xml b/app/src/main/res/drawable/ic_remove_circle_primary.xml deleted file mode 100644 index 889bb879df..0000000000 --- a/app/src/main/res/drawable/ic_remove_circle_primary.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_reset.xml b/app/src/main/res/drawable/ic_reset.xml deleted file mode 100644 index ad48ac67e8..0000000000 --- a/app/src/main/res/drawable/ic_reset.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_save_alt.xml b/app/src/main/res/drawable/ic_save_alt.xml deleted file mode 100644 index 9ccbed2bae..0000000000 --- a/app/src/main/res/drawable/ic_save_alt.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_scale.xml b/app/src/main/res/drawable/ic_scale.xml deleted file mode 100644 index ff394c0070..0000000000 --- a/app/src/main/res/drawable/ic_scale.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_search_add.xml b/app/src/main/res/drawable/ic_search_add.xml deleted file mode 100644 index 7e0e0be02f..0000000000 --- a/app/src/main/res/drawable/ic_search_add.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_searchandnew.xml b/app/src/main/res/drawable/ic_searchandnew.xml deleted file mode 100644 index dea888f60f..0000000000 --- a/app/src/main/res/drawable/ic_searchandnew.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_send.xml b/app/src/main/res/drawable/ic_send.xml deleted file mode 100644 index e145ca83cc..0000000000 --- a/app/src/main/res/drawable/ic_send.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_skipped.xml b/app/src/main/res/drawable/ic_skipped.xml deleted file mode 100644 index 14fefd5055..0000000000 --- a/app/src/main/res/drawable/ic_skipped.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_transfer.xml b/app/src/main/res/drawable/ic_transfer.xml new file mode 100644 index 0000000000..87cc3c38fc --- /dev/null +++ b/app/src/main/res/drawable/ic_transfer.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_unchecked_circle_36.xml b/app/src/main/res/drawable/ic_unchecked_circle_36.xml deleted file mode 100644 index ef7c3bac67..0000000000 --- a/app/src/main/res/drawable/ic_unchecked_circle_36.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_visibility.xml b/app/src/main/res/drawable/ic_visibility.xml deleted file mode 100644 index c3812f4ce2..0000000000 --- a/app/src/main/res/drawable/ic_visibility.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_visibility_off.xml b/app/src/main/res/drawable/ic_visibility_off.xml deleted file mode 100644 index 689f3f47c1..0000000000 --- a/app/src/main/res/drawable/ic_visibility_off.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_warning_primary.xml b/app/src/main/res/drawable/ic_warning_primary.xml deleted file mode 100644 index 24fd846a77..0000000000 --- a/app/src/main/res/drawable/ic_warning_primary.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_warning_white.xml b/app/src/main/res/drawable/ic_warning_white.xml deleted file mode 100644 index 27db1a67a0..0000000000 --- a/app/src/main/res/drawable/ic_warning_white.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_with_registration.xml b/app/src/main/res/drawable/ic_with_registration.xml deleted file mode 100644 index 0923cb7a04..0000000000 --- a/app/src/main/res/drawable/ic_with_registration.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_without_reg.xml b/app/src/main/res/drawable/ic_without_reg.xml deleted file mode 100644 index 196d9c678f..0000000000 --- a/app/src/main/res/drawable/ic_without_reg.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_zoomx1.xml b/app/src/main/res/drawable/ic_zoomx1.xml deleted file mode 100644 index e0af859dd5..0000000000 --- a/app/src/main/res/drawable/ic_zoomx1.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_zoomx2.xml b/app/src/main/res/drawable/ic_zoomx2.xml deleted file mode 100644 index 58098c8d5b..0000000000 --- a/app/src/main/res/drawable/ic_zoomx2.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_zoomx3.xml b/app/src/main/res/drawable/ic_zoomx3.xml deleted file mode 100644 index 7dc5142cba..0000000000 --- a/app/src/main/res/drawable/ic_zoomx3.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/indicator_bg.xml b/app/src/main/res/drawable/indicator_bg.xml deleted file mode 100644 index dfe1d5f3ed..0000000000 --- a/app/src/main/res/drawable/indicator_bg.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/indicator_progress_bg.xml b/app/src/main/res/drawable/indicator_progress_bg.xml deleted file mode 100644 index 46a6818605..0000000000 --- a/app/src/main/res/drawable/indicator_progress_bg.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/item_event_dark_gray_ripple.xml b/app/src/main/res/drawable/item_event_dark_gray_ripple.xml deleted file mode 100644 index 327e688f83..0000000000 --- a/app/src/main/res/drawable/item_event_dark_gray_ripple.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/item_event_gray_ripple.xml b/app/src/main/res/drawable/item_event_gray_ripple.xml deleted file mode 100644 index e477f9323c..0000000000 --- a/app/src/main/res/drawable/item_event_gray_ripple.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/item_event_green_ripple.xml b/app/src/main/res/drawable/item_event_green_ripple.xml deleted file mode 100644 index 481db70117..0000000000 --- a/app/src/main/res/drawable/item_event_green_ripple.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/item_event_red_ripple.xml b/app/src/main/res/drawable/item_event_red_ripple.xml deleted file mode 100644 index 961f99b202..0000000000 --- a/app/src/main/res/drawable/item_event_red_ripple.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/item_event_yellow_ripple.xml b/app/src/main/res/drawable/item_event_yellow_ripple.xml deleted file mode 100644 index 50c24529fc..0000000000 --- a/app/src/main/res/drawable/item_event_yellow_ripple.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/item_selected_bg.xml b/app/src/main/res/drawable/item_selected_bg.xml deleted file mode 100644 index 3040935f4e..0000000000 --- a/app/src/main/res/drawable/item_selected_bg.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/jira_button.xml b/app/src/main/res/drawable/jira_button.xml deleted file mode 100644 index 948240cf34..0000000000 --- a/app/src/main/res/drawable/jira_button.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/material_divider.xml b/app/src/main/res/drawable/material_divider.xml deleted file mode 100644 index 61aded073c..0000000000 --- a/app/src/main/res/drawable/material_divider.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/period_button_color_selector.xml b/app/src/main/res/drawable/period_button_color_selector.xml deleted file mode 100644 index 9e154c0860..0000000000 --- a/app/src/main/res/drawable/period_button_color_selector.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_circle.xml b/app/src/main/res/drawable/progress_circle.xml deleted file mode 100644 index 01fa587eae..0000000000 --- a/app/src/main/res/drawable/progress_circle.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_circle_thick.xml b/app/src/main/res/drawable/progress_circle_thick.xml deleted file mode 100644 index 9765470bad..0000000000 --- a/app/src/main/res/drawable/progress_circle_thick.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/radiobutton_selector.xml b/app/src/main/res/drawable/radiobutton_selector.xml deleted file mode 100644 index 654b85d81f..0000000000 --- a/app/src/main/res/drawable/radiobutton_selector.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/red_circle.xml b/app/src/main/res/drawable/red_circle.xml deleted file mode 100644 index 373d32e98e..0000000000 --- a/app/src/main/res/drawable/red_circle.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/schedule_circle_green.xml b/app/src/main/res/drawable/schedule_circle_green.xml deleted file mode 100644 index 758f07b144..0000000000 --- a/app/src/main/res/drawable/schedule_circle_green.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/schedule_circle_red.xml b/app/src/main/res/drawable/schedule_circle_red.xml deleted file mode 100644 index 5dcd7876b0..0000000000 --- a/app/src/main/res/drawable/schedule_circle_red.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/schedule_path.xml b/app/src/main/res/drawable/schedule_path.xml deleted file mode 100644 index fe88656870..0000000000 --- a/app/src/main/res/drawable/schedule_path.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/seekbar_progress.xml b/app/src/main/res/drawable/seekbar_progress.xml deleted file mode 100644 index 6a7d7d8706..0000000000 --- a/app/src/main/res/drawable/seekbar_progress.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/seekbar_thumb.xml b/app/src/main/res/drawable/seekbar_thumb.xml deleted file mode 100644 index 708a6fe612..0000000000 --- a/app/src/main/res/drawable/seekbar_thumb.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_enroll_button.xml b/app/src/main/res/drawable/selector_enroll_button.xml deleted file mode 100644 index 55c5dba277..0000000000 --- a/app/src/main/res/drawable/selector_enroll_button.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_gray_button_round_7.xml b/app/src/main/res/drawable/selector_gray_button_round_7.xml deleted file mode 100644 index 08f6313eed..0000000000 --- a/app/src/main/res/drawable/selector_gray_button_round_7.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_tab.xml b/app/src/main/res/drawable/selector_tab.xml deleted file mode 100644 index 339529db85..0000000000 --- a/app/src/main/res/drawable/selector_tab.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_white_button.xml b/app/src/main/res/drawable/selector_white_button.xml deleted file mode 100644 index 15a38ee76a..0000000000 --- a/app/src/main/res/drawable/selector_white_button.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shadow_top.xml b/app/src/main/res/drawable/shadow_top.xml deleted file mode 100644 index a4b4de0dd6..0000000000 --- a/app/src/main/res/drawable/shadow_top.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle.xml b/app/src/main/res/drawable/shape_circle.xml deleted file mode 100644 index 943d86f5e3..0000000000 --- a/app/src/main/res/drawable/shape_circle.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/sms_sync_icon_bg.xml b/app/src/main/res/drawable/sms_sync_icon_bg.xml deleted file mode 100644 index 3cc7235938..0000000000 --- a/app/src/main/res/drawable/sms_sync_icon_bg.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/square_rounded_corners.xml b/app/src/main/res/drawable/square_rounded_corners.xml deleted file mode 100644 index 3d9ba60df2..0000000000 --- a/app/src/main/res/drawable/square_rounded_corners.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/tab_selected_bg.xml b/app/src/main/res/drawable/tab_selected_bg.xml deleted file mode 100644 index e8ade3f984..0000000000 --- a/app/src/main/res/drawable/tab_selected_bg.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/text_input_color_selector.xml b/app/src/main/res/drawable/text_input_color_selector.xml deleted file mode 100644 index cef734c1ec..0000000000 --- a/app/src/main/res/drawable/text_input_color_selector.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/white_button_bg.xml b/app/src/main/res/drawable/white_button_bg.xml deleted file mode 100644 index eb81274487..0000000000 --- a/app/src/main/res/drawable/white_button_bg.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/white_button_bg_pressed.xml b/app/src/main/res/drawable/white_button_bg_pressed.xml deleted file mode 100644 index 00482a8b5c..0000000000 --- a/app/src/main/res/drawable/white_button_bg_pressed.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/white_circle.xml b/app/src/main/res/drawable/white_circle.xml deleted file mode 100644 index bd2d283590..0000000000 --- a/app/src/main/res/drawable/white_circle.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/white_circle_ripple.xml b/app/src/main/res/drawable/white_circle_ripple.xml deleted file mode 100644 index 8c1752744f..0000000000 --- a/app/src/main/res/drawable/white_circle_ripple.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/yellow_circle.xml b/app/src/main/res/drawable/yellow_circle.xml deleted file mode 100644 index 8918f97314..0000000000 --- a/app/src/main/res/drawable/yellow_circle.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_dashboard_mobile.xml b/app/src/main/res/layout-land/activity_dashboard_mobile.xml index c86361c3fa..dbb5070574 100644 --- a/app/src/main/res/layout-land/activity_dashboard_mobile.xml +++ b/app/src/main/res/layout-land/activity_dashboard_mobile.xml @@ -5,10 +5,6 @@ - - @@ -19,150 +15,183 @@ android:layout_height="match_parent"> - - - - - - + android:layout_height="match_parent" + android:background="?colorPrimary"> - - - - - - - - - + + + + + + + + + + + + + + + + android:layout_height="0dp" + android:background="@drawable/ic_front_home_backdrop_bg" + android:clipToOutline="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_event_capture.xml b/app/src/main/res/layout-land/activity_event_capture.xml new file mode 100644 index 0000000000..5dcea9994d --- /dev/null +++ b/app/src/main/res/layout-land/activity_event_capture.xml @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/activity_event_initial.xml b/app/src/main/res/layout-land/activity_event_initial.xml index e1ddbd1f5e..1ab167cde3 100644 --- a/app/src/main/res/layout-land/activity_event_initial.xml +++ b/app/src/main/res/layout-land/activity_event_initial.xml @@ -73,15 +73,14 @@ app:percentageSize="13sp" app:strokeSize="3dp" /> - + - - - + app:layout_constraintStart_toEndOf="@id/backdropGuideDiv"/> - \ No newline at end of file + diff --git a/app/src/main/res/layout-land/event_details_fragment.xml b/app/src/main/res/layout-land/event_details_fragment.xml index 726c5e1a8e..c8b8cea330 100644 --- a/app/src/main/res/layout-land/event_details_fragment.xml +++ b/app/src/main/res/layout-land/event_details_fragment.xml @@ -10,8 +10,6 @@ - - diff --git a/app/src/main/res/layout-land/fragment_search.xml b/app/src/main/res/layout-land/fragment_search.xml deleted file mode 100644 index 2431fbcebd..0000000000 --- a/app/src/main/res/layout-land/fragment_search.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_tei_data.xml b/app/src/main/res/layout-land/fragment_tei_data.xml index 22d9798213..6c20084d09 100644 --- a/app/src/main/res/layout-land/fragment_tei_data.xml +++ b/app/src/main/res/layout-land/fragment_tei_data.xml @@ -18,6 +18,7 @@ @@ -85,7 +86,7 @@ android:background="@color/white" android:gravity="center" android:padding="42dp" - android:text="@string/empty_tei_add" + android:text="@string/empty_tei_event_label_add" android:textSize="@dimen/primaryTextSize" android:visibility="visible" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/activity_dashboard_mobile.xml b/app/src/main/res/layout/activity_dashboard_mobile.xml index 4d19f002f0..a787d3dbd1 100644 --- a/app/src/main/res/layout/activity_dashboard_mobile.xml +++ b/app/src/main/res/layout/activity_dashboard_mobile.xml @@ -21,118 +21,120 @@ android:layout_height="match_parent"> - - - - + android:layout_height="match_parent" + android:background="?colorPrimary"> - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - + app:layout_constraintBottom_toBottomOf="parent" /> - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_dataset_table.xml b/app/src/main/res/layout/activity_dataset_table.xml index f80d053e07..b4400f0f89 100644 --- a/app/src/main/res/layout/activity_dataset_table.xml +++ b/app/src/main/res/layout/activity_dataset_table.xml @@ -70,15 +70,14 @@ tools:ignore="ContentDescription" tools:visibility="visible"/> - + - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_event_capture.xml b/app/src/main/res/layout/activity_event_capture.xml index b6a62c437b..01e4321e76 100644 --- a/app/src/main/res/layout/activity_event_capture.xml +++ b/app/src/main/res/layout/activity_event_capture.xml @@ -16,135 +16,148 @@ android:layout_height="match_parent"> - - - - + android:layout_height="match_parent" + android:background="?colorPrimary"> + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + app:layout_constraintBottom_toBottomOf="parent" /> - + android:layout_marginTop="10dp" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/eventViewPager" /> - - - - - - - - - - - - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_event_initial.xml b/app/src/main/res/layout/activity_event_initial.xml index 38cc122418..eb5d230840 100644 --- a/app/src/main/res/layout/activity_event_initial.xml +++ b/app/src/main/res/layout/activity_event_initial.xml @@ -77,15 +77,14 @@ app:strokeSize="3dp" /> - + - - - + app:layout_constraintTop_toBottomOf="@id/passContainer" /> @@ -258,14 +239,24 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent"> + + + app:layout_constraintTop_toBottomOf="@id/biometricButton"/> + + + @@ -17,10 +20,6 @@ - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> @@ -77,110 +76,58 @@ android:textColor="@android:color/white" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/filterActionButton" + app:layout_constraintEnd_toStartOf="@id/syncActionButton" app:layout_constraintStart_toEndOf="@id/menu" app:layout_constraintTop_toTopOf="parent" /> - - - - - - - - - - - - - + + + android:id="@+id/toolbarProgress" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:indeterminate="true" + android:padding="0dp" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/toolbar" /> - + app:layout_constraintTop_toBottomOf="@id/toolbarProgress" /> + - + app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/app/src/main/res/layout/activity_nfc_write_tracker.xml b/app/src/main/res/layout/activity_nfc_write_tracker.xml deleted file mode 100644 index ab0235b964..0000000000 --- a/app/src/main/res/layout/activity_nfc_write_tracker.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_program_event_detail.xml b/app/src/main/res/layout/activity_program_event_detail.xml index 797cf7769a..385a676100 100644 --- a/app/src/main/res/layout/activity_program_event_detail.xml +++ b/app/src/main/res/layout/activity_program_event_detail.xml @@ -177,7 +177,7 @@ android:id="@+id/fragmentContainer" android:layout_width="match_parent" android:layout_height="0dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/navigationBar" app:layout_constraintTop_toBottomOf="@id/backdropGuideTop" /> - - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_program_rules_validation.xml b/app/src/main/res/layout/activity_program_rules_validation.xml deleted file mode 100644 index 89c3dc2646..0000000000 --- a/app/src/main/res/layout/activity_program_rules_validation.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_program_stage_selection.xml b/app/src/main/res/layout/activity_program_stage_selection.xml index 0a5f86914c..6cc0783f68 100644 --- a/app/src/main/res/layout/activity_program_stage_selection.xml +++ b/app/src/main/res/layout/activity_program_stage_selection.xml @@ -33,6 +33,7 @@ tools:ignore="ContentDescription" /> - - - + app:layout_constraintTop_toBottomOf="@id/mainComponent" + app:layout_constraintBottom_toBottomOf="parent" /> + - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_sms.xml b/app/src/main/res/layout/activity_sms.xml deleted file mode 100644 index 36dfd50936..0000000000 --- a/app/src/main/res/layout/activity_sms.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/cat_comb_filter.xml b/app/src/main/res/layout/cat_comb_filter.xml deleted file mode 100644 index b7a62f0ae4..0000000000 --- a/app/src/main/res/layout/cat_comb_filter.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/cat_combo_dialog.xml b/app/src/main/res/layout/cat_combo_dialog.xml deleted file mode 100644 index cabe49921f..0000000000 --- a/app/src/main/res/layout/cat_combo_dialog.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/custom_long_text_view.xml b/app/src/main/res/layout/custom_long_text_view.xml deleted file mode 100644 index 3a7023898b..0000000000 --- a/app/src/main/res/layout/custom_long_text_view.xml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/custom_long_text_view_accent.xml b/app/src/main/res/layout/custom_long_text_view_accent.xml deleted file mode 100644 index 8356c6e5f5..0000000000 --- a/app/src/main/res/layout/custom_long_text_view_accent.xml +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/custom_text_view.xml b/app/src/main/res/layout/custom_text_view.xml deleted file mode 100644 index 0ee960a6bd..0000000000 --- a/app/src/main/res/layout/custom_text_view.xml +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/custom_text_view_accent.xml b/app/src/main/res/layout/custom_text_view_accent.xml deleted file mode 100644 index 39f498008a..0000000000 --- a/app/src/main/res/layout/custom_text_view_accent.xml +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/date_time_view.xml b/app/src/main/res/layout/date_time_view.xml deleted file mode 100644 index e4360240bb..0000000000 --- a/app/src/main/res/layout/date_time_view.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/date_time_view_accent.xml b/app/src/main/res/layout/date_time_view_accent.xml deleted file mode 100644 index 8a8e8eb875..0000000000 --- a/app/src/main/res/layout/date_time_view_accent.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/development_rules.xml b/app/src/main/res/layout/development_rules.xml deleted file mode 100644 index e930a3a3e2..0000000000 --- a/app/src/main/res/layout/development_rules.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dhis_logo.xml b/app/src/main/res/layout/dhis_logo.xml index eae17af02c..50824e6fb1 100644 --- a/app/src/main/res/layout/dhis_logo.xml +++ b/app/src/main/res/layout/dhis_logo.xml @@ -6,7 +6,6 @@ android:layout_height="wrap_content"> - - - - - -