From 26c58d9ea6924eb703ca7938d3b0b049daa68e60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 08:26:13 +0200 Subject: [PATCH 01/56] build(deps): bump urllib3 from 1.25.10 to 1.26.17 in /scripts (#3311) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.10 to 1.26.17. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.25.10...1.26.17) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index d68351bfd4..934a69820b 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -2,4 +2,4 @@ certifi==2020.6.20 chardet==3.0.4 idna==2.10 requests==2.31.0 -urllib3==1.25.10 \ No newline at end of file +urllib3==1.26.17 \ No newline at end of file From e5f65e8673eb2e6b2544c8686715296f1e930483 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 09:28:17 +0200 Subject: [PATCH 02/56] build(deps): bump urllib3 from 1.26.17 to 1.26.18 in /scripts (#3335) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.17 to 1.26.18. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.17...1.26.18) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 934a69820b..cca289fa6f 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -2,4 +2,4 @@ certifi==2020.6.20 chardet==3.0.4 idna==2.10 requests==2.31.0 -urllib3==1.26.17 \ No newline at end of file +urllib3==1.26.18 \ No newline at end of file From 18d5b8bd24b881d0746dbe63b8b82ce645105536 Mon Sep 17 00:00:00 2001 From: FerdyRod Date: Thu, 26 Oct 2023 12:10:30 +0200 Subject: [PATCH 03/56] [RELEASE/2.9] Starting Release --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f11be467b2..ebccc272bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ ndk = "21.4.7075529" sdk = "34" minSdk = "21" vCode = "128" -vName = "2.9-DEV" +vName = "2.9" kotlinCompilerExtensionVersion = "1.5.2" From 091aff6c569b5c5c3c3915cb6c4d3d5709912318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Mon, 30 Oct 2023 10:40:06 +0100 Subject: [PATCH 04/56] fix: [ANDROAPP-5646] handle online TEIs mapping (#3353) --- .../usescases/searchTrackEntity/SearchRepositoryImpl.java | 2 +- .../searchTrackEntity/adapters/SearchTeiLiveAdapter.kt | 4 ++-- .../usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt | 2 +- .../main/java/org/dhis2/commons/bindings/TEICardExtensions.kt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) 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 4ebecef374..893f72ceb1 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/SearchRepositoryImpl.java @@ -765,7 +765,7 @@ private SearchTeiModel transform(TrackedEntitySearchItem searchItem, @Nullable P } } - searchTei.setOnline(false); + searchTei.setOnline(!searchItem.isOnline()); if (offlineOnly) searchTei.setOnline(!offlineOnly); 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 e1b6e5375c..29e5b86bb3 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 @@ -85,12 +85,12 @@ class SearchTeiLiveAdapter( if (it.isOnline) { onDownloadTei.invoke( it.tei.uid(), - it.selectedEnrollment.uid(), + it.selectedEnrollment?.uid(), ) } else { onTeiClick.invoke( it.tei.uid(), - it.selectedEnrollment.uid(), + it.selectedEnrollment?.uid(), it.isOnline, ) } 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 9ba650f305..f1ae571087 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 @@ -128,7 +128,7 @@ class TEICardMapper( ) checkEnrollmentStatus( list = list, - status = searchTEIModel.selectedEnrollment.status(), + status = searchTEIModel.selectedEnrollment?.status(), ) checkOverdue( diff --git a/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt b/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt index 614900cdff..05e26bc37f 100644 --- a/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt +++ b/commons/src/main/java/org/dhis2/commons/bindings/TEICardExtensions.kt @@ -246,7 +246,7 @@ fun SearchTeiModel.setTeiImage( teiImageView.setImageDrawable(null) teiTextImageView.visibility = View.VISIBLE val valueToShow = ArrayList(textAttributeValues.values) - if (valueToShow[0] == null) { + if (valueToShow[0]?.value()?.isEmpty() != false) { teiTextImageView.text = "?" } else { teiTextImageView.text = valueToShow[0].value()?.first().toString().toUpperCase() From 90683922195edc3333ab36753a1653b2b9c2e13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Tue, 31 Oct 2023 11:55:21 +0100 Subject: [PATCH 05/56] fix: [ANDROAPP-5550] long text bottom line (#3358) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ebccc272bb..6998522a12 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' -designSystem = "1.0-20231025.123540-96" +designSystem = "1.0-20231027.121015-100" dhis2sdk = "1.9.0-20231025.143704-59" ruleEngine = "2.1.9" appcompat = "1.6.1" From 42ac7299243214d1fb6d2f510861f5697d863fe5 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:56:55 +0100 Subject: [PATCH 06/56] add empty onImageClick implementation (#3367) --- .../org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt | 1 + .../org/dhis2/form/ui/provider/inputfield/SignatureProvider.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt index 967bb9a78e..47223e6606 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt @@ -48,6 +48,7 @@ internal fun ProvideInputImage( uploadState = UploadState.UPLOADING fieldUiModel.invokeUiEvent(UiEventType.ADD_PICTURE) }, + onImageClick = {}, ) } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SignatureProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SignatureProvider.kt index 979c158509..64d8bc2ba9 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SignatureProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/SignatureProvider.kt @@ -45,5 +45,6 @@ fun ProvideInputSignature( onDownloadButtonClick = { fieldUiModel.invokeUiEvent(UiEventType.SHOW_PICTURE) }, onResetButtonClicked = { fieldUiModel.onClear() }, onAddButtonClicked = { fieldUiModel.invokeUiEvent(UiEventType.ADD_SIGNATURE) }, + onImageClick = {}, ) } From 13755b36a7f76fc0dc99921cd851405df618ba73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manu=20Mu=C3=B1oz?= Date: Thu, 2 Nov 2023 06:41:32 +0100 Subject: [PATCH 07/56] fix: [ANDROAPP-5654] foregroundService on API 34 (#3356) --- app/src/main/AndroidManifest.xml | 6 ++++++ .../main/java/org/dhis2/data/service/SyncDataWorker.java | 7 ++++++- .../java/org/dhis2/data/service/SyncMetadataWorker.java | 7 ++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8d8b9ebd04..786c96c1fd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + + + = Build.VERSION_CODES.R ? ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC : 0 + )); } diff --git a/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java b/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java index 5677d602c7..b0b066eb22 100644 --- a/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java +++ b/app/src/main/java/org/dhis2/data/service/SyncMetadataWorker.java @@ -7,6 +7,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; +import android.content.pm.ServiceInfo; import android.os.Build; import androidx.annotation.NonNull; @@ -192,7 +193,11 @@ private void triggerNotification(String title, String content, int progress) { .setProgress(100, progress, false) .setPriority(NotificationCompat.PRIORITY_DEFAULT); - setForegroundAsync(new ForegroundInfo(SyncMetadataWorker.SYNC_METADATA_ID, notificationBuilder.build())); + setForegroundAsync(new ForegroundInfo( + SyncMetadataWorker.SYNC_METADATA_ID, + notificationBuilder.build(), + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ? ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC : 0 + )); } private void cancelNotification() { From 6b676fc133ed9e5565e8fd67331db74dddd92aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Thu, 2 Nov 2023 09:37:21 +0100 Subject: [PATCH 08/56] update period date (#3368) --- .../java/org/dhis2/usescases/datasets/DataSetTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f0ed9ee295..659b9d2bd8 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetTest.kt @@ -48,7 +48,7 @@ class DataSetTest : BaseTest() { @Test fun shouldCreateNewDataSet() { - val period = "Dec 2022" + val period = "Oct 2023" val orgUnit = "Ngelehun CHC" startDataSetDetailActivity("ZOV1a5R4gqH", "DS EXTRA TEST", ruleDataSetDetail) @@ -57,7 +57,7 @@ class DataSetTest : BaseTest() { } dataSetInitialRobot { clickOnInputOrgUnit() - orgUnitSelectorRobot(composeTestRule){ + orgUnitSelectorRobot(composeTestRule) { selectTreeOrgUnit(orgUnit) } clickOnInputPeriod() From 307ef554e1d96977b5e26132c81fc53a8b799e29 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:01:45 +0100 Subject: [PATCH 09/56] feat: [ANDROAPP-5626] implement gs1 component (#3363) * Initial commit * [ANDROAPP-5626] Add DataMatrix support to new component logic * [ANDROAPP-5626] update design system --- .../java/org/dhis2/form/model/UiRenderType.kt | 1 + .../form/ui/dialog/QRDetailBottomDialog.kt | 20 ++++++++++++++----- .../ui/provider/UiEventTypesProviderImpl.kt | 2 ++ .../InputsForTextValueTypeProvider.kt | 3 ++- gradle/libs.versions.toml | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/form/src/main/java/org/dhis2/form/model/UiRenderType.kt b/form/src/main/java/org/dhis2/form/model/UiRenderType.kt index 32d91d06ab..91574aa2b8 100644 --- a/form/src/main/java/org/dhis2/form/model/UiRenderType.kt +++ b/form/src/main/java/org/dhis2/form/model/UiRenderType.kt @@ -14,6 +14,7 @@ enum class UiRenderType { SEQUENCIAL, QR_CODE, BAR_CODE, + GS1_DATAMATRIX, CANVAS, TOGGLE, ; diff --git a/form/src/main/java/org/dhis2/form/ui/dialog/QRDetailBottomDialog.kt b/form/src/main/java/org/dhis2/form/ui/dialog/QRDetailBottomDialog.kt index bf35e6884b..1bbed7d93c 100644 --- a/form/src/main/java/org/dhis2/form/ui/dialog/QRDetailBottomDialog.kt +++ b/form/src/main/java/org/dhis2/form/ui/dialog/QRDetailBottomDialog.kt @@ -42,6 +42,7 @@ import org.dhis2.form.data.FormFileProvider import org.dhis2.form.databinding.QrDetailDialogBinding import org.dhis2.form.model.UiRenderType import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper +import org.hisp.dhis.lib.expression.math.GS1Elements import org.hisp.dhis.mobile.ui.designsystem.component.BarcodeBlock import org.hisp.dhis.mobile.ui.designsystem.component.BottomSheetShell import org.hisp.dhis.mobile.ui.designsystem.component.ButtonCarousel @@ -197,7 +198,7 @@ QRDetailBottomDialog( val buttonList = getComposeButtonList() BottomSheetShell( modifier = modifier, - title = if (renderingType == UiRenderType.QR_CODE) resources.getString(R.string.qr_code) else resources.getString(R.string.bar_code), + title = if (renderingType == UiRenderType.QR_CODE || renderingType == UiRenderType.GS1_DATAMATRIX) resources.getString(R.string.qr_code) else resources.getString(R.string.bar_code), icon = { Icon( imageVector = Icons.Outlined.Info, @@ -207,10 +208,15 @@ QRDetailBottomDialog( }, content = { Row(horizontalArrangement = Arrangement.Center) { - if (renderingType == UiRenderType.QR_CODE) { - QrCodeBlock(data = value) - } else { - BarcodeBlock(data = value) + when (renderingType) { + UiRenderType.QR_CODE, UiRenderType.GS1_DATAMATRIX -> { + val isGS1Matrix = value.startsWith(GS1Elements.GS1_d2_IDENTIFIER.element) + val content = formattedContent(value) + QrCodeBlock(data = content, isDataMatrix = isGS1Matrix) + } + else -> { + BarcodeBlock(data = value) + } } } }, @@ -225,6 +231,10 @@ QRDetailBottomDialog( } } + private fun formattedContent(value: String) = + value.removePrefix(GS1Elements.GS1_d2_IDENTIFIER.element) + .removePrefix(GS1Elements.GS1_GROUP_SEPARATOR.element) + private fun getComposeButtonList(): List { val scanItem = CarouselButtonData( icon = { diff --git a/form/src/main/java/org/dhis2/form/ui/provider/UiEventTypesProviderImpl.kt b/form/src/main/java/org/dhis2/form/ui/provider/UiEventTypesProviderImpl.kt index f80ff3da64..3ae3231f96 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/UiEventTypesProviderImpl.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/UiEventTypesProviderImpl.kt @@ -28,6 +28,8 @@ class UiEventTypesProviderImpl : UiEventTypesProvider { UiRenderType.AUTOCOMPLETE ValueTypeRenderingType.QR_CODE -> UiRenderType.QR_CODE + ValueTypeRenderingType.GS1_DATAMATRIX -> + UiRenderType.GS1_DATAMATRIX ValueTypeRenderingType.BAR_CODE -> UiRenderType.BAR_CODE ValueTypeRenderingType.CANVAS -> diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt index 3bfddbcd0d..415c11ae54 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputsForTextValueTypeProvider.kt @@ -29,7 +29,7 @@ internal fun ProvideInputsForValueTypeText( focusManager: FocusManager, ) { when (fieldUiModel.renderingType) { - UiRenderType.QR_CODE -> { + UiRenderType.QR_CODE, UiRenderType.GS1_DATAMATRIX -> { ProvideQRInput( modifier = modifier, fieldUiModel = fieldUiModel, @@ -38,6 +38,7 @@ internal fun ProvideInputsForValueTypeText( focusManager = focusManager, ) } + UiRenderType.BAR_CODE -> { ProvideBarcodeInput( modifier = modifier, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6998522a12..c62a460621 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' -designSystem = "1.0-20231027.121015-100" +designSystem = "1.0-20231031.093345-104" dhis2sdk = "1.9.0-20231025.143704-59" ruleEngine = "2.1.9" appcompat = "1.6.1" From cc0ea826fbaf9df8cab0586ac3711b01e6bce139 Mon Sep 17 00:00:00 2001 From: "@dhis2-bot" Date: Fri, 3 Nov 2023 09:40:19 +0100 Subject: [PATCH 10/56] fix(translations): sync translations from transifex (develop) (#3357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: change development name (#3354) * fix(translations): sync translations from transifex (develop) --------- Co-authored-by: Ferdy Rodriguez Co-authored-by: Andrés Miguel Rubio --- form/src/main/res/values-zh/strings.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/form/src/main/res/values-zh/strings.xml b/form/src/main/res/values-zh/strings.xml index 691384b705..67b33d6384 100644 --- a/form/src/main/res/values-zh/strings.xml +++ b/form/src/main/res/values-zh/strings.xml @@ -98,4 +98,8 @@ 没有更多保留值,请联系管理员 报名日期 事件日期 - \ No newline at end of file + 扫描 + 二维码 + 条码 + 添加位置 + \ No newline at end of file From f269faa281a20ba2d94e448a2c83210a1d9f29b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Fri, 3 Nov 2023 13:56:10 +0100 Subject: [PATCH 11/56] [ANDROAPP-5652] EventInitial new inputs (#3350) * [ANDROAPP-5652] Add Date, OrgUnit and Category combo inputs in EventInitial * [ANDROAPP-5652] Add coordinates input * [ANDROAPP-5652] Add event temp component * [ANDROAPP-5652] Implement category options dropdown * [ANDROAPP-5652] Update functional tests for new components * [ANDROAPP-5652] Add Polygon component * [ANDROAPP-5652] jenkins disable concurrent builds * [ANDROAPP-5652] Sonar checks review * [ANDROAPP-5652] Switch lat long * [ANDROAPP-5652] Add labels --- Jenkinsfile | 1 + .../usescases/datasets/DataSetInitialRobot.kt | 4 - .../usescases/event/EventRegistrationRobot.kt | 45 ++- .../org/dhis2/usescases/event/EventTest.kt | 3 +- .../programevent/ProgramEventTest.kt | 4 +- .../teidashboard/TeiDashboardTest.kt | 6 +- .../teidashboard/robot/EventRobot.kt | 27 +- .../teidashboard/robot/TeiDashboardRobot.kt | 7 +- .../providers/InputFieldsProvider.kt | 366 ++++++++++++++++++ .../eventDetails/ui/EventDetailsFragment.kt | 116 ++++-- .../ui/EventDetailsViewBindings.kt | 32 -- .../eventDetails/ui/EventDetailsViewModel.kt | 13 +- .../layout-land/event_details_fragment.xml | 98 +---- .../res/layout/event_details_fragment.xml | 96 +---- app/src/main/res/values/strings.xml | 1 + 15 files changed, 526 insertions(+), 293 deletions(-) create mode 100644 app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt diff --git a/Jenkinsfile b/Jenkinsfile index 11bc1f51bf..d5c9d40f61 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -6,6 +6,7 @@ pipeline { options { buildDiscarder(logRotator(daysToKeepStr: '5')) timeout(time: 50) + disableConcurrentBuilds(abortPrevious: true) } stages { diff --git a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetInitialRobot.kt b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetInitialRobot.kt index fa4cd4e771..17bd65dd4d 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetInitialRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/datasets/DataSetInitialRobot.kt @@ -1,18 +1,14 @@ package org.dhis2.usescases.datasets -import androidx.compose.ui.test.junit4.ComposeTestRule -import androidx.compose.ui.test.onNodeWithTag import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers.hasDescendant -import androidx.test.espresso.matcher.ViewMatchers.hasSibling 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.utils.customviews.DateViewHolder -import org.hamcrest.CoreMatchers.allOf fun dataSetInitialRobot(dataSetInitialRobot: DataSetInitialRobot.() -> Unit) { 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 5df4772583..56cb29fa1d 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/event/EventRegistrationRobot.kt @@ -1,10 +1,11 @@ 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.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.isEnabled import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withSubstring import androidx.test.espresso.matcher.ViewMatchers.withText @@ -13,7 +14,6 @@ import org.dhis2.common.BaseRobot import org.dhis2.common.matchers.hasCompletedPercentage import org.dhis2.usescases.event.entity.EventDetailsUIModel import org.hamcrest.CoreMatchers.allOf -import org.hamcrest.CoreMatchers.not fun eventRegistrationRobot(eventRegistrationRobot: EventRegistrationRobot.() -> Unit) { EventRegistrationRobot().apply { @@ -48,25 +48,12 @@ class EventRegistrationRobot : BaseRobot() { onView(withId(R.id.navigation_details)).perform(click()) } - fun checkEventDetails(eventDetails: EventDetailsUIModel) { + fun checkEventDetails(eventDetails: EventDetailsUIModel, composeTestRule: ComposeTestRule) { onView(withId(R.id.detailsStageName)).check(matches(withText(eventDetails.programStage))) onView(withId(R.id.completion)).check(matches(hasCompletedPercentage(eventDetails.completedPercentage))) - onView(withId(R.id.date_layout)).check( - matches( - allOf( - isEnabled(), - hasDescendant(allOf(withId(R.id.date), withText(eventDetails.eventDate))) - ) - ) - ) - onView(withId(R.id.org_unit_layout)).check( - matches( - allOf( - not(isEnabled()), - hasDescendant(allOf(withId(R.id.org_unit), withText(eventDetails.orgUnit))) - ) - ) - ) + + composeTestRule.onNodeWithText(formatStoredDateToUI(eventDetails.eventDate)).assertIsDisplayed() + composeTestRule.onNodeWithText(eventDetails.orgUnit).assertIsDisplayed() } fun clickOnShare() { @@ -102,4 +89,22 @@ class EventRegistrationRobot : BaseRobot() { fun clickNextButton() { waitForView(withId(R.id.action_button)).perform(click()) } + + private fun formatStoredDateToUI(dateValue: String): String { + val components = dateValue.split("/") + + val year = components[2] + val month = if (components[1].length == 1) { + "0${components[1]}" + } else { + components[1] + } + val day = if (components[0].length == 1) { + "0${components[0]}" + } else { + components[0] + } + + return "$day/$month/$year" + } } \ No newline at end of file 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 67b8161f46..02c8bc8ba3 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.ProgramStageUIModel 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.form.formRobot import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity import org.dhis2.usescases.programEventDetail.eventList.EventListFragment import org.dhis2.usescases.programevent.robot.programEventsRobot @@ -74,7 +73,7 @@ class EventTest: BaseTest() { eventRegistrationRobot { checkEventFormDetails(eventDetails) clickOnDetails() - checkEventDetails(eventDetails) + checkEventDetails(eventDetails, composeTestRule) } } 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 428944489a..6ec2122d91 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/programevent/ProgramEventTest.kt @@ -11,7 +11,6 @@ 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.robot.eventRobot -import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -30,7 +29,6 @@ class ProgramEventTest : BaseTest() { return arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) } - @Ignore("Nondeterministic") @Test fun shouldCreateNewEventAndCompleteIt() { val eventOrgUnit = "Ngelehun CHC" @@ -124,7 +122,7 @@ class ProgramEventTest : BaseTest() { } eventRobot { clickOnDetails() - checkEventDetails(eventDate, eventOrgUnit) + checkEventDetails(eventDate, eventOrgUnit, composeTestRule) } } 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 a5f8a0a7cc..90370f5491 100644 --- a/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt +++ b/app/src/androidTest/java/org/dhis2/usescases/teidashboard/TeiDashboardTest.kt @@ -16,7 +16,6 @@ import org.dhis2.usescases.teidashboard.robot.eventRobot import org.dhis2.usescases.teidashboard.robot.indicatorsRobot import org.dhis2.usescases.teidashboard.robot.noteRobot import org.dhis2.usescases.teidashboard.robot.teiDashboardRobot -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -145,7 +144,10 @@ class TeiDashboardTest : BaseTest() { clickOnFab() clickOnReferral() clickOnFirstReferralEvent() - clickOnReferralOption() + clickOnReferralOption( + composeTestRule, + context.getString(R.string.one_time) + ) clickOnReferralNextButton() checkEventWasCreated(LAB_MONITORING) } 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 c6b882a480..8fe763d25e 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 @@ -1,7 +1,9 @@ package org.dhis2.usescases.teidashboard.robot +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.ViewActions.click @@ -26,6 +28,7 @@ import org.dhis2.ui.dialogs.bottomsheet.SECONDARY_BUTTON_TAG import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.DashboardProgramViewHolder import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not +import org.jetbrains.annotations.VisibleForTesting fun eventRobot(eventRobot: EventRobot.() -> Unit) { EventRobot().apply { @@ -142,9 +145,27 @@ class EventRobot : BaseRobot() { onView(withId(R.id.datePicker)).perform(PickerActions.setDate(year, monthOfYear, dayOfMonth)) } - fun checkEventDetails(eventDate: String, eventOrgUnit: String) { + fun checkEventDetails(eventDate: String, eventOrgUnit: String, composeTestRule: ComposeTestRule) { onView(withId(R.id.completion)).check(matches(hasCompletedPercentage(100))) - onView(withId(R.id.date_layout)).check(matches(allOf(isEnabled(),hasDescendant(allOf(withId(R.id.date), withText(eventDate)))))) - onView(withId(R.id.org_unit_layout)).check(matches(allOf(not(isEnabled()), hasDescendant(allOf(withId(R.id.org_unit), withText(eventOrgUnit)))))) + composeTestRule.onNodeWithText(formatStoredDateToUI(eventDate)).assertIsDisplayed() + composeTestRule.onNodeWithText(eventOrgUnit).assertIsDisplayed() + } + + private fun formatStoredDateToUI(dateValue: String): String { + val components = dateValue.split("/") + + val year = components[2] + val month = if (components[1].length == 1) { + "0${components[1]}" + } else { + components[1] + } + val day = if (components[0].length == 1) { + "0${components[0]}" + } else { + components[0] + } + + return "$day/$month/$year" } } 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 ac270d0eed..3c0f598942 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 @@ -2,10 +2,10 @@ package org.dhis2.usescases.teidashboard.robot import android.content.Context import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertIsNotDisplayed 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.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -151,14 +151,13 @@ class TeiDashboardRobot : BaseRobot() { .perform(actionOnItemAtPosition(0, click())) } - fun clickOnReferralOption() { - onView(withId(R.id.one_time)).perform(click()) + fun clickOnReferralOption(composeTestRule: ComposeTestRule, oneTime: String) { + composeTestRule.onNodeWithText(oneTime).performClick() } fun clickOnReferralNextButton() { waitForView(withId(R.id.action_button)).perform(click()) } - fun checkEventWasCreated(eventName: String) { onView(withId(R.id.tei_recycler)) 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 new file mode 100644 index 0000000000..e150a7d889 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/providers/InputFieldsProvider.kt @@ -0,0 +1,366 @@ +package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers + +import androidx.compose.foundation.background +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ExposedDropdownMenuBox +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import org.dhis2.R +import org.dhis2.commons.resources.ResourceManager +import org.dhis2.data.dhislogic.inDateRange +import org.dhis2.data.dhislogic.inOrgUnit +import org.dhis2.form.model.UiEventType +import org.dhis2.form.model.UiRenderType +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate +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.utils.category.CategoryDialog.Companion.DEFAULT_COUNT_LIMIT +import org.hisp.dhis.android.core.arch.helpers.GeometryHelper +import org.hisp.dhis.android.core.arch.helpers.Result +import org.hisp.dhis.android.core.category.CategoryOption +import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.common.Geometry +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.mobile.ui.designsystem.component.Coordinates +import org.hisp.dhis.mobile.ui.designsystem.component.DateTimeActionIconType +import org.hisp.dhis.mobile.ui.designsystem.component.InputCoordinate +import org.hisp.dhis.mobile.ui.designsystem.component.InputDateTime +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.internal.DateTransformation +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException +import java.util.Date + +@Composable +fun ProvideInputDate( + eventDate: EventDate, + detailsEnabled: Boolean, + onDateClick: () -> Unit, + onDateSet: (InputDateValues) -> Unit, +) { + var value by remember(eventDate.dateValue) { + mutableStateOf(eventDate.dateValue?.let { formatStoredDateToUI(it) }) + } + + var state by remember { + mutableStateOf(getInputState(detailsEnabled)) + } + + InputDateTime( + title = eventDate.label ?: "", + value = value, + actionIconType = DateTimeActionIconType.DATE, + onActionClicked = onDateClick, + state = state, + visualTransformation = DateTransformation(), + onValueChanged = { + value = it + if (isValid(it)) { + if (isValidDateFormat(it)) { + state = InputShellState.FOCUSED + formatUIDateToStored(it)?.let { dateValues -> + onDateSet(dateValues) + } + } else { + state = InputShellState.ERROR + } + } else { + state = InputShellState.FOCUSED + } + }, + ) +} + +fun isValidDateFormat(dateString: String): Boolean { + val year = dateString.substring(4, 8) + val month = dateString.substring(2, 4) + val day = dateString.substring(0, 2) + + val formattedDate = "$year-$month-$day" + + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + + return try { + LocalDate.parse(formattedDate, formatter) + when (ValueType.DATE.validator.validate(formattedDate)) { + is Result.Failure -> false + is Result.Success -> true + } + } catch (e: DateTimeParseException) { + false + } +} + +private fun isValid(valueString: String) = valueString.length == 8 + +private fun formatStoredDateToUI(dateValue: String): String? { + val components = dateValue.split("/") + if (components.size != 3) { + return null + } + + val year = components[2] + val month = if (components[1].length == 1) { + "0${components[1]}" + } else { + components[1] + } + val day = if (components[0].length == 1) { + "0${components[0]}" + } else { + components[0] + } + + return "$day$month$year" +} + +fun formatUIDateToStored(dateValue: String?): InputDateValues? { + if (dateValue?.length != 8) { + return null + } + + val year = dateValue.substring(4, 8).toInt() + val month = dateValue.substring(2, 4).toInt() + val day = dateValue.substring(0, 2).toInt() + + return (InputDateValues(day, month, year)) +} + +data class InputDateValues(val day: Int, val month: Int, val year: Int) + +@Composable +fun ProvideOrgUnit( + orgUnit: EventOrgUnit, + detailsEnabled: Boolean, + onOrgUnitClick: () -> Unit, + resources: ResourceManager, +) { + val state = getInputState(detailsEnabled && orgUnit.enable && orgUnit.orgUnits.size > 1) + + var inputFieldValue by remember(orgUnit.selectedOrgUnit) { + mutableStateOf(orgUnit.selectedOrgUnit?.displayName()) + } + + InputOrgUnit( + title = resources.getString(R.string.org_unit), + state = state, + inputText = inputFieldValue ?: "", + onValueChanged = { + inputFieldValue = it + }, + onOrgUnitActionCLicked = onOrgUnitClick, + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ProvideCategorySelector( + modifier: Modifier = Modifier, + category: EventCategory, + eventCatCombo: EventCatCombo, + detailsEnabled: Boolean, + currentDate: Date?, + selectedOrgUnit: String?, + onShowCategoryDialog: (EventCategory) -> Unit, + onClearCatCombo: (EventCategory) -> Unit, + onOptionSelected: (CategoryOption?) -> Unit, +) { + var selectedItem by remember(eventCatCombo) { + mutableStateOf( + eventCatCombo.selectedCategoryOptions[category.uid]?.displayName() + ?: eventCatCombo.categoryOptions?.get(category.uid)?.displayName(), + ) + } + + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = {}, + ) { + InputDropDown( + modifier = modifier, + title = category.name, + state = getInputState(detailsEnabled), + selectedItem = selectedItem, + onResetButtonClicked = { + onClearCatCombo(category) + }, + onArrowDropDownButtonClicked = { + expanded = !expanded + }, + ) + + if (expanded) { + if (category.optionsSize > DEFAULT_COUNT_LIMIT) { + onShowCategoryDialog(category) + expanded = false + } else { + DropdownMenu( + modifier = modifier.exposedDropdownSize(), + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + val selectableOptions = category.options + .filter { option -> + option.access().data().write() + }.filter { option -> + option.inDateRange(currentDate) + }.filter { option -> + option.inOrgUnit(selectedOrgUnit) + } + selectableOptions.forEach { option -> + val isSelected = option.displayName() == selectedItem + DropdownMenuItem( + modifier = Modifier.background( + when { + isSelected -> SurfaceColor.PrimaryContainer + else -> Color.Transparent + }, + ), + content = { + Text( + text = option.displayName() ?: option.code() ?: "", + color = when { + isSelected -> TextColor.OnPrimaryContainer + else -> TextColor.OnSurface + }, + ) + }, + onClick = { + expanded = false + selectedItem = option.displayName() + onOptionSelected(option) + }, + ) + } + } + } + } + } +} + +private fun getInputState(enabled: Boolean) = if (enabled) { + InputShellState.UNFOCUSED +} else { + InputShellState.DISABLED +} + +@Composable +fun ProvideCoordinates( + coordinates: EventCoordinates, + detailsEnabled: Boolean, + resources: ResourceManager, +) { + when (coordinates.model?.renderingType) { + UiRenderType.POLYGON, UiRenderType.MULTI_POLYGON -> { + InputPolygon( + title = resources.getString(R.string.polygon), + state = getInputState(detailsEnabled && coordinates.model.editable), + polygonAdded = !coordinates.model.value.isNullOrEmpty(), + onResetButtonClicked = { coordinates.model.onClear() }, + onUpdateButtonClicked = { + coordinates.model.invokeUiEvent(UiEventType.REQUEST_LOCATION_BY_MAP) + }, + ) + } + + else -> { + InputCoordinate( + title = resources.getString(R.string.coordinates), + state = getInputState(detailsEnabled && coordinates.model?.editable == true), + coordinates = mapGeometry(coordinates.model?.value, FeatureType.POINT), + latitudeText = resources.getString(R.string.latitude), + longitudeText = resources.getString(R.string.longitude), + addLocationBtnText = resources.getString(R.string.add_location), + onResetButtonClicked = { + coordinates.model?.onClear() + }, + onUpdateButtonClicked = { + coordinates.model?.invokeUiEvent(UiEventType.REQUEST_LOCATION_BY_MAP) + }, + ) + } + } +} + +fun mapGeometry(value: String?, featureType: FeatureType): Coordinates? { + return value?.let { + val geometry = Geometry.builder() + .coordinates(it) + .type(featureType) + .build() + + Coordinates( + latitude = GeometryHelper.getPoint(geometry)[1], + longitude = GeometryHelper.getPoint(geometry)[0], + ) + } +} + +@Composable +fun ProvideRadioButtons( + eventTemp: EventTemp, + detailsEnabled: Boolean, + resources: ResourceManager, + onEventTempSelected: (status: EventTempStatus?) -> Unit, +) { + 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 = "", + 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) + } + } + }, + ) +} 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 f49ccc1438..53e8e0bcca 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 @@ -8,6 +8,13 @@ import android.view.View import android.view.ViewGroup import android.widget.DatePicker import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.databinding.DataBindingUtil import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope @@ -27,16 +34,21 @@ import org.dhis2.commons.dialogs.calendarpicker.OnDatePickerListener import org.dhis2.commons.locationprovider.LocationSettingLauncher import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.orgunitselector.OrgUnitSelectorScope +import org.dhis2.commons.resources.ResourceManager import org.dhis2.databinding.EventDetailsFragmentBinding import org.dhis2.maps.views.MapSelectorActivity import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsComponentProvider import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.injection.EventDetailsModule import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideCategorySelector +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideCoordinates +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideInputDate +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideOrgUnit +import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.ProvideRadioButtons import org.dhis2.usescases.general.FragmentGlobalAbstract import org.dhis2.utils.category.CategoryDialog import org.dhis2.utils.category.CategoryDialog.Companion.TAG -import org.dhis2.utils.customviews.CatOptionPopUp import org.dhis2.utils.customviews.PeriodDialog import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.enrollment.EnrollmentStatus @@ -49,6 +61,9 @@ class EventDetailsFragment : FragmentGlobalAbstract() { @Inject lateinit var factory: EventDetailsViewModelFactory + @Inject + lateinit var resourceManager: ResourceManager + private val requestLocationPermissions = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions(), @@ -119,6 +134,82 @@ class EventDetailsFragment : FragmentGlobalAbstract() { ) binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = viewModel + binding.fieldsContainer.setContent { + val date by viewModel.eventDate.collectAsState() + val details by viewModel.eventDetails.collectAsState() + val orgUnit by viewModel.eventOrgUnit.collectAsState() + val catCombo by viewModel.eventCatCombo.collectAsState() + val coordinates by viewModel.eventCoordinates.collectAsState() + val eventTemp by viewModel.eventTemp.collectAsState() + + Column { + if (date.active) { + Spacer(modifier = Modifier.height(16.dp)) + ProvideInputDate( + eventDate = date, + detailsEnabled = details.enabled, + onDateClick = { viewModel.onDateClick() }, + onDateSet = { dateValues -> + viewModel.onDateSet(dateValues.year, dateValues.month, dateValues.day) + }, + ) + } + if (orgUnit.visible) { + Spacer(modifier = Modifier.height(16.dp)) + ProvideOrgUnit( + orgUnit = orgUnit, + detailsEnabled = details.enabled, + onOrgUnitClick = { viewModel.onOrgUnitClick() }, + resources = resourceManager, + ) + } + + if (!catCombo.isDefault) { + catCombo.categories.forEach { category -> + Spacer(modifier = Modifier.height(16.dp)) + ProvideCategorySelector( + category = category, + eventCatCombo = catCombo, + detailsEnabled = details.enabled, + currentDate = date.currentDate, + selectedOrgUnit = details.selectedOrgUnit, + onShowCategoryDialog = { + showCategoryDialog(it) + }, + onClearCatCombo = { + val selectedOption = Pair(category.uid, null) + viewModel.setUpCategoryCombo(selectedOption) + }, + onOptionSelected = { + val selectedOption = Pair(category.uid, it?.uid()) + viewModel.setUpCategoryCombo(selectedOption) + }, + ) + } + } + + if (coordinates.active) { + Spacer(modifier = Modifier.height(16.dp)) + ProvideCoordinates( + coordinates = coordinates, + detailsEnabled = details.enabled, + resources = resourceManager, + ) + } + + if (eventTemp.active) { + Spacer(modifier = Modifier.height(16.dp)) + ProvideRadioButtons( + eventTemp = eventTemp, + detailsEnabled = details.enabled, + resources = resourceManager, + onEventTempSelected = { + viewModel.setUpEventTemp(it) + }, + ) + } + } + } return binding.root } @@ -147,14 +238,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { showNoOrgUnitsDialog() } - viewModel.showCategoryDialog = { category -> - showCategoryDialog(category) - } - - viewModel.showCategoryPopUp = { category -> - showCategoryPopUp(category) - } - viewModel.requestLocationPermissions = { requestLocationPermissions.launch( arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), @@ -247,7 +330,7 @@ class EventDetailsFragment : FragmentGlobalAbstract() { OrgUnitSelectorScope.ProgramCaptureScope(viewModel.eventOrgUnit.value.programUid!!), ) .onSelection { selectedOrgUnits -> - viewModel.setUpOrgUnit(selectedOrgUnit = selectedOrgUnits.first().uid()) + viewModel.setUpOrgUnit(selectedOrgUnit = selectedOrgUnits.firstOrNull()?.uid()) } .build() .show(childFragmentManager, "ORG_UNIT_DIALOG") @@ -257,19 +340,6 @@ class EventDetailsFragment : FragmentGlobalAbstract() { showInfoDialog(getString(R.string.error), getString(R.string.no_org_units)) } - private fun showCategoryPopUp(category: EventCategory) { - CatOptionPopUp( - context = requireContext(), - anchor = binding.catComboLayout, - options = category.options, - date = viewModel.eventDate.value.currentDate, - orgUnitUid = viewModel.eventDetails.value.selectedOrgUnit, - ) { categoryOption -> - val selectedOption = Pair(category.uid, categoryOption?.uid()) - viewModel.setUpCategoryCombo(selectedOption) - }.show() - } - private fun showCategoryDialog(category: EventCategory) { CategoryDialog( CategoryDialog.Type.CATEGORY_OPTIONS, diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewBindings.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewBindings.kt index f06c04c41b..a740d4c70d 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewBindings.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewBindings.kt @@ -1,7 +1,5 @@ package org.dhis2.usescases.eventsWithoutRegistration.eventDetails.ui -import android.view.LayoutInflater -import android.widget.LinearLayout import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -9,36 +7,6 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.databinding.BindingAdapter import com.google.android.material.composethemeadapter.MdcTheme -import org.dhis2.databinding.CategorySelectorBinding -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo - -@BindingAdapter(value = ["setViewModel", "setCatCombo", "enabled"]) -fun LinearLayout.setCatCombo( - viewModel: EventDetailsViewModel, - eventCatCombo: EventCatCombo, - enabled: Boolean?, -) { - if (!eventCatCombo.isDefault) { - this@setCatCombo.removeAllViews() - eventCatCombo.categories.forEach { category -> - val catSelectorBinding: CategorySelectorBinding = - CategorySelectorBinding.inflate(LayoutInflater.from(context)) - catSelectorBinding.catCombLayout.hint = category.name - catSelectorBinding.catCombo.isEnabled = enabled ?: true - catSelectorBinding.catCombo.setOnClickListener { - viewModel.onCatComboClick(category) - } - - val selectorDisplay = - eventCatCombo.selectedCategoryOptions[category.uid]?.displayName() - ?: eventCatCombo.categoryOptions?.get(category.uid)?.displayName() - - catSelectorBinding.catCombo.setText(selectorDisplay) - - this@setCatCombo.addView(catSelectorBinding.root) - } - } -} @ExperimentalAnimationApi @BindingAdapter("setReopen") 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 290ad4e1dc..527866c203 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 @@ -18,7 +18,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.Configu import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.ConfigureOrgUnit import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.domain.CreateOrUpdateEventDetails import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCatCombo -import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCategory import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventCoordinates import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDate import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventDetails @@ -26,7 +25,6 @@ import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventOr import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTemp import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.models.EventTempStatus import org.dhis2.usescases.eventsWithoutRegistration.eventDetails.providers.EventDetailResourcesProvider -import org.dhis2.utils.category.CategoryDialog.Companion.DEFAULT_COUNT_LIMIT import org.hisp.dhis.android.core.arch.helpers.GeometryHelper import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.Geometry @@ -53,8 +51,6 @@ class EventDetailsViewModel( var showPeriods: (() -> Unit)? = null var showOrgUnits: (() -> Unit)? = null var showNoOrgUnits: (() -> Unit)? = null - var showCategoryDialog: ((category: EventCategory) -> Unit)? = null - var showCategoryPopUp: ((category: EventCategory) -> Unit)? = null var requestLocationPermissions: (() -> Unit)? = null var showEnableLocationMessage: (() -> Unit)? = null var requestLocationByMap: ((featureType: String, initCoordinate: String?) -> Unit)? = null @@ -256,14 +252,6 @@ class EventDetailsViewModel( } } - fun onCatComboClick(category: EventCategory) { - if (category.optionsSize > DEFAULT_COUNT_LIMIT) { - showCategoryDialog?.invoke(category) - } else { - showCategoryPopUp?.invoke(category) - } - } - fun requestCurrentLocation() { locationProvider.getLastKnownLocation( onNewLocation = { location -> @@ -365,5 +353,6 @@ inline fun Result.mockSafeFold( } } } + else -> onFailure(exceptionOrNull() ?: Exception()) } 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 abafb2be3d..6226903c4d 100644 --- a/app/src/main/res/layout-land/event_details_fragment.xml +++ b/app/src/main/res/layout-land/event_details_fragment.xml @@ -92,102 +92,10 @@ tools:text="Not available" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" /> Sync Error Mark for follow-up Remove + Coordinates From e237aed28a5d33b8b5c09ae0aa1e87e7dbc7f702 Mon Sep 17 00:00:00 2001 From: Xavier Molloy <44061143+xavimolloy@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:03:26 +0100 Subject: [PATCH 12/56] [ANDROAPP-5625] Don't save values for autocomplete if value is empty spaces (#3360) --- form/src/main/java/org/dhis2/form/ui/FormViewModel.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt index 9e150beeac..b716cdccdc 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormViewModel.kt @@ -279,7 +279,6 @@ class FormViewModel( private fun saveLastFocusedItem(rowAction: RowAction) = getLastFocusedTextItem()?.let { val error = checkFieldError(it.valueType, it.value, it.fieldMask) if (error != null) { - // save autocomplete form here val action = rowActionFromIntent( FormIntent.OnSave(it.uid, it.value, it.valueType, it.fieldMask), ) @@ -303,7 +302,7 @@ class FormViewModel( ) private fun checkAutoCompleteForLastFocusedItem(fieldUidModel: FieldUiModel) = getLastFocusedTextItem()?.let { - if (fieldUidModel.renderingType == UiRenderType.AUTOCOMPLETE && fieldUidModel.value != null) { + if (fieldUidModel.renderingType == UiRenderType.AUTOCOMPLETE && !fieldUidModel.value.isNullOrEmpty() && fieldUidModel.value?.trim()?.length != 0) { val autoCompleteValues = getListFromPreference(fieldUidModel.uid) if (!autoCompleteValues.contains(fieldUidModel.value)) { From 83faa8cc721358e59cde21d9fbe88da863d1d70b Mon Sep 17 00:00:00 2001 From: Ferdy Rodriguez Date: Fri, 3 Nov 2023 15:04:00 +0100 Subject: [PATCH 13/56] Added start parameter to save value coroutine (#3364) --- .../dataSetTable/dataSetSection/DataValuePresenter.kt | 6 +++++- .../android/rtsm/ui/managestock/ManageStockViewModel.kt | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt index a0ed56a271..73c2066e5a 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/dataSetTable/dataSetSection/DataValuePresenter.kt @@ -4,6 +4,7 @@ import androidx.annotation.VisibleForTesting import io.reactivex.Flowable import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -209,7 +210,10 @@ class DataValuePresenter( } fun onSaveValueChange(cell: TableCell) { - launch(dispatcherProvider.io()) { + launch( + dispatcherProvider.io(), + start = CoroutineStart.ATOMIC, + ) { saveValue(cell) view.onValueProcessed() } diff --git a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt index ff58831cf2..fb82c1158b 100644 --- a/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt +++ b/stock-usecase/src/main/java/org/dhis2/android/rtsm/ui/managestock/ManageStockViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope import com.jakewharton.rxrelay2.PublishRelay import dagger.hilt.android.lifecycle.HiltViewModel import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -324,7 +325,10 @@ class ManageStockViewModel @Inject constructor( } fun onSaveValueChange(cell: TableCell) { - viewModelScope.launch(dispatcherProvider.io()) { + viewModelScope.launch( + dispatcherProvider.io(), + start = CoroutineStart.ATOMIC, + ) { saveValue(cell) } } From f5ff82aebbfc8eccf79baf96a61e9909626b7f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Miguel=20Rubio?= Date: Mon, 6 Nov 2023 07:54:41 +0100 Subject: [PATCH 14/56] [ANDROAPP-5603] Input coordinates location updates (#3361) * [ANDROAPP-5603] Change Accept button * [ANDROAPP-5603] Update map location permission request and user location * [ANDROAPP-5652] Remove lat long for point --- dhis2_android_maps/build.gradle.kts | 5 + .../dhis2/maps/geometry/point/PointAdapter.kt | 36 ------ .../dhis2/maps/views/MapSelectorActivity.kt | 115 +++++++++++++----- .../main/res/layout/activity_map_selector.xml | 51 ++------ .../src/main/res/layout/item_point_geo.xml | 72 ----------- 5 files changed, 100 insertions(+), 179 deletions(-) delete mode 100644 dhis2_android_maps/src/main/java/org/dhis2/maps/geometry/point/PointAdapter.kt delete mode 100644 dhis2_android_maps/src/main/res/layout/item_point_geo.xml diff --git a/dhis2_android_maps/build.gradle.kts b/dhis2_android_maps/build.gradle.kts index df3e71b938..9f56ae1cc9 100644 --- a/dhis2_android_maps/build.gradle.kts +++ b/dhis2_android_maps/build.gradle.kts @@ -35,6 +35,7 @@ android { } buildFeatures { + compose = true dataBinding = true } @@ -43,6 +44,10 @@ android { kotlinOptions { jvmTarget = "17" } + + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerExtensionVersion.get() + } } dependencies { diff --git a/dhis2_android_maps/src/main/java/org/dhis2/maps/geometry/point/PointAdapter.kt b/dhis2_android_maps/src/main/java/org/dhis2/maps/geometry/point/PointAdapter.kt deleted file mode 100644 index 7adeb777b7..0000000000 --- a/dhis2_android_maps/src/main/java/org/dhis2/maps/geometry/point/PointAdapter.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.dhis2.maps.geometry.point - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.RecyclerView -import org.dhis2.maps.R -import org.dhis2.maps.databinding.ItemPointGeoBinding - -class PointAdapter( - val viewModel: PointViewModel, -) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - val binding: ItemPointGeoBinding = DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.item_point_geo, - parent, - false, - ) - return Holder(binding) - } - - override fun getItemCount(): Int { - return 1 - } - - override fun onBindViewHolder(holder: Holder, position: Int) { - holder.bind() - } - - inner class Holder(val binding: ItemPointGeoBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind() { - binding.viewModel = viewModel - } - } -} diff --git a/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorActivity.kt b/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorActivity.kt index 83b2b391db..bfeeb6bf57 100644 --- a/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorActivity.kt +++ b/dhis2_android_maps/src/main/java/org/dhis2/maps/views/MapSelectorActivity.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle +import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat @@ -14,7 +15,6 @@ import androidx.core.graphics.drawable.toBitmap import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager import com.mapbox.geojson.Feature import com.mapbox.geojson.Point import com.mapbox.geojson.Polygon @@ -35,13 +35,11 @@ import com.mapbox.mapboxsdk.style.sources.GeoJsonSource import org.dhis2.commons.extensions.truncate import org.dhis2.maps.R import org.dhis2.maps.camera.initCameraToViewAllElements -import org.dhis2.maps.camera.moveCameraToDevicePosition import org.dhis2.maps.camera.moveCameraToPosition import org.dhis2.maps.databinding.ActivityMapSelectorBinding import org.dhis2.maps.extensions.polygonToLatLngBounds import org.dhis2.maps.extensions.toLatLng import org.dhis2.maps.geometry.bound.GetBoundingBox -import org.dhis2.maps.geometry.point.PointAdapter import org.dhis2.maps.geometry.point.PointViewModel import org.dhis2.maps.geometry.polygon.PolygonAdapter import org.dhis2.maps.geometry.polygon.PolygonViewModel @@ -50,6 +48,8 @@ import org.dhis2.maps.location.MapActivityLocationCallback import org.hisp.dhis.android.core.arch.helpers.GeometryHelper import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.Geometry +import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle class MapSelectorActivity : AppCompatActivity(), @@ -60,6 +60,14 @@ class MapSelectorActivity : init = true if (initialCoordinates == null) { map.moveCameraToPosition(latLng) + getPointViewModel()?.let { + val point = + Point.fromLngLat( + latLng.longitude.truncate(), + latLng.latitude.truncate(), + ) + setPointToViewModel(point, it) + } } } } @@ -79,6 +87,10 @@ class MapSelectorActivity : BaseMapManager(this, emptyList()) } + private var onSaveButtonClick: (() -> Unit)? = null + + private var pointViewModel: PointViewModel? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = DataBindingUtil.setContentView(this, R.layout.activity_map_selector) @@ -112,19 +124,29 @@ class MapSelectorActivity : binding.mapPositionButton.setOnClickListener { centerCameraOnMyPosition() } + + binding.saveButton.setContent { + Button( + style = ButtonStyle.FILLED, + text = resources.getString(R.string.done), + onClick = { onSaveButtonClick?.invoke() }, + ) + } } private fun centerCameraOnMyPosition() { - val isLocationActivated = - map.locationComponent.isLocationComponentActivated - if (isLocationActivated) { + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION, + ) == PackageManager.PERMISSION_GRANTED + ) { + val isLocationActivated = map.locationComponent.isLocationComponentActivated val isLocationEnabled = map.locationComponent.isLocationComponentEnabled - if (isLocationEnabled) { + if (isLocationActivated && isLocationEnabled) { map.locationComponent.lastKnownLocation?.let { val latLong = LatLng(it) - map.moveCameraToDevicePosition(latLong) - if (locationType == FeatureType.POINT) { - val viewModel = ViewModelProvider(this)[PointViewModel::class.java] + map.moveCameraToPosition(latLong) + getPointViewModel()?.let { viewModel -> val point = Point.fromLngLat( latLong.longitude.truncate(), @@ -133,7 +155,19 @@ class MapSelectorActivity : setPointToViewModel(point, viewModel) } } + } else { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + CENTER_MY_POSITION, + ) } + } else { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + CENTER_MY_POSITION, + ) } } @@ -160,31 +194,38 @@ class MapSelectorActivity : } } - private fun bindPoint(initialCoordinates: String?) { - val viewModel = ViewModelProvider(this)[PointViewModel::class.java] - binding.recycler.layoutManager = LinearLayoutManager(this) - binding.recycler.adapter = PointAdapter(viewModel) - map.addOnMapClickListener { - val point = Point.fromLngLat(it.longitude.truncate(), it.latitude.truncate()) - setPointToViewModel(point, viewModel) - true - } - binding.saveButton.setOnClickListener { - val value = viewModel.getPointAsString() - value?.let { - finishResult(it) - } + private fun getPointViewModel(): PointViewModel? { + if (locationType == FeatureType.POINT && pointViewModel == null) { + pointViewModel = ViewModelProvider(this)[PointViewModel::class.java] } + return pointViewModel + } - if (initialCoordinates != null) { - val initGeometry = - Geometry.builder().coordinates(initialCoordinates).type(locationType).build() - val pointGeometry = GeometryHelper.getPoint(initGeometry) - pointGeometry.let { sdkPoint -> - val point = Point.fromLngLat(sdkPoint[0], sdkPoint[1]) + private fun bindPoint(initialCoordinates: String?) { + getPointViewModel()?.let { viewModel -> + binding.recycler.visibility = View.GONE + map.addOnMapClickListener { + val point = Point.fromLngLat(it.longitude.truncate(), it.latitude.truncate()) setPointToViewModel(point, viewModel) + true + } + onSaveButtonClick = { + val value = viewModel.getPointAsString() + value?.let { + finishResult(it) + } + } + + if (initialCoordinates != null) { + val initGeometry = + Geometry.builder().coordinates(initialCoordinates).type(locationType).build() + val pointGeometry = GeometryHelper.getPoint(initGeometry) + pointGeometry.let { sdkPoint -> + val point = Point.fromLngLat(sdkPoint[0], sdkPoint[1]) + setPointToViewModel(point, viewModel) + } + map.moveCameraToPosition(pointGeometry.toLatLng()) } - map.moveCameraToPosition(pointGeometry.toLatLng()) } } @@ -275,7 +316,7 @@ class MapSelectorActivity : viewModel.add(polygonPoint) true } - binding.saveButton.setOnClickListener { + onSaveButtonClick = { val value = viewModel.getPointAsString() value?.let { finishResult(it) @@ -415,11 +456,21 @@ class MapSelectorActivity : centerMapOnCurrentLocation() } } + + CENTER_MY_POSITION -> { + if (grantResults.isNotEmpty() && + grantResults[0] == PackageManager.PERMISSION_GRANTED + ) { + enableLocationComponent() + centerCameraOnMyPosition() + } + } } } companion object { private const val ACCESS_LOCATION_PERMISSION_REQUEST = 102 + private const val CENTER_MY_POSITION = 103 const val DATA_EXTRA = "data_extra" const val LOCATION_TYPE_EXTRA = "LOCATION_TYPE_EXTRA" const val INITIAL_GEOMETRY_COORDINATES = "INITIAL_DATA" diff --git a/dhis2_android_maps/src/main/res/layout/activity_map_selector.xml b/dhis2_android_maps/src/main/res/layout/activity_map_selector.xml index 8b414e0808..94353f4f54 100644 --- a/dhis2_android_maps/src/main/res/layout/activity_map_selector.xml +++ b/dhis2_android_maps/src/main/res/layout/activity_map_selector.xml @@ -38,24 +38,6 @@ android:text="@string/select_location" android:textSize="20sp" app:layout_constraintStart_toEndOf="@id/back" /> - - - - - - - + app:layout_constraintTop_toBottomOf="@id/mapContainer"> -