From d19d51e9a235f5e7b984661712a0021ed76ace08 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 16 Feb 2024 08:41:48 +0100 Subject: [PATCH 01/19] export database option in settings Signed-off-by: Pablo --- .../settings/SyncManagerFragment.java | 3 ++ .../usescases/settings/ui/ExportOption.kt | 42 +++++++++++++++++++ .../main/res/drawable/ic_settings_export.xml | 35 ++++++++++++++++ app/src/main/res/layout/fragment_settings.xml | 37 ++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 5 files changed, 119 insertions(+) create mode 100644 app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt create mode 100644 app/src/main/res/drawable/ic_settings_export.xml diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java index 68d62c6cea..2905c75803 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java @@ -66,6 +66,7 @@ import org.dhis2.usescases.settings.models.ReservedValueSettingsViewModel; import org.dhis2.usescases.settings.models.SMSSettingsViewModel; import org.dhis2.usescases.settings.models.SyncParametersViewModel; +import org.dhis2.usescases.settings.ui.ExportOptionKt; import org.dhis2.usescases.settingsprogram.SettingsProgramActivity; import org.dhis2.utils.HelpManager; import org.hisp.dhis.android.core.settings.LimitScope; @@ -132,6 +133,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, binding.setPresenter(presenter); binding.smsSettings.setVisibility(ContextExtensionsKt.showSMS(context) ? View.VISIBLE : View.GONE); binding.setVersionName(BuildConfig.VERSION_NAME); + + ExportOptionKt.setExportOption(binding.exportShare, () -> null); return binding.getRoot(); } diff --git a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt new file mode 100644 index 0000000000..8daf1bd5b7 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt @@ -0,0 +1,42 @@ +package org.dhis2.usescases.settings.ui + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.dhis2.R +import org.dhis2.ui.IconTextButton + +@Composable +fun ExportOption( + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(72.dp) + ) { + IconTextButton( + onClick = onClick, + painter = rememberVectorPainter(image = Icons.Filled.Share), + text = stringResource( + id = R.string.share + ) + ) + } +} + +fun ComposeView.setExportOption( + onClick: () -> Unit +) { + setContent { + ExportOption(onClick) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings_export.xml b/app/src/main/res/drawable/ic_settings_export.xml new file mode 100644 index 0000000000..c1babf8eb9 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_export.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 27be308456..c717fadcbe 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -550,6 +550,43 @@ app:layout_constraintTop_toBottomOf="@id/errorLogLayout" /> + + + + + + + + + + + + Check and edit parameters related to the sms gateway Software update Software + Export database + Export yout database and share it with your administrator Enrollment date From dbf2669ef14c6b7d5474bad0c375eb59f2604c13 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 16 Feb 2024 12:01:48 +0100 Subject: [PATCH 02/19] export database Signed-off-by: Pablo --- .../dhis2/usescases/settings/SettingItem.kt | 1 + .../settings/SyncManagerFragment.java | 207 ++++++++++-------- .../settings/SyncManagerPresenter.kt | 42 ++-- .../settings/models/ExportDbModel.kt | 9 + .../usescases/settings/ui/ExportOption.kt | 38 +++- app/src/main/res/layout/fragment_settings.xml | 48 ++-- .../settings/SyncManagerPresenterTest.kt | 26 +++ 7 files changed, 228 insertions(+), 143 deletions(-) create mode 100644 app/src/main/java/org/dhis2/usescases/settings/models/ExportDbModel.kt diff --git a/app/src/main/java/org/dhis2/usescases/settings/SettingItem.kt b/app/src/main/java/org/dhis2/usescases/settings/SettingItem.kt index b1615bf051..9bef5a9a7c 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SettingItem.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/SettingItem.kt @@ -5,6 +5,7 @@ enum class SettingItem { META_SYNC, SYNC_PARAMETERS, RESERVED_VALUES, + EXPORT_DB, DELETE_LOCAL_DATA, SMS, VERSION_UPDATE, diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java index 2905c75803..598aaa886b 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.Rect; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.text.Editable; @@ -37,6 +38,7 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.databinding.DataBindingUtil; import androidx.work.WorkInfo; @@ -50,6 +52,7 @@ import org.dhis2.bindings.ViewExtensionsKt; import org.dhis2.commons.Constants; import org.dhis2.commons.animations.ViewAnimationsKt; +import org.dhis2.commons.data.FormFileProvider; import org.dhis2.commons.network.NetworkUtils; import org.dhis2.commons.resources.ColorType; import org.dhis2.commons.resources.ColorUtils; @@ -77,7 +80,7 @@ import javax.inject.Inject; import kotlin.Unit; -import kotlin.jvm.functions.Function0; +import timber.log.Timber; public class SyncManagerFragment extends FragmentGlobalAbstract implements SyncManagerContracts.View { @@ -133,8 +136,28 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, binding.setPresenter(presenter); binding.smsSettings.setVisibility(ContextExtensionsKt.showSMS(context) ? View.VISIBLE : View.GONE); binding.setVersionName(BuildConfig.VERSION_NAME); + FormFileProvider.INSTANCE.init(requireContext()); + presenter.getExportedDb().observe(getViewLifecycleOwner(), fileData -> { + Uri contentUri = FileProvider.getUriForFile(requireContext(), + FormFileProvider.INSTANCE.fileProviderAuthority, + fileData.getFile()); + Intent intentShare = new Intent(Intent.ACTION_SEND) + .setDataAndType(contentUri, requireContext().getContentResolver().getType(contentUri)) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .putExtra(Intent.EXTRA_STREAM, contentUri); + + Intent chooser = Intent.createChooser(intentShare, getString(R.string.open_with)); + try { + startActivity(chooser); + } catch (Exception e) { + Timber.e(e); + } + }); - ExportOptionKt.setExportOption(binding.exportShare, () -> null); + ExportOptionKt.setExportOption(binding.exportShare, () -> { + presenter.onExportAndShareDB(); + return null; + }); return binding.getRoot(); } @@ -282,73 +305,65 @@ public void openItem(SettingItem settingsItem) { binding.metaDivider.setVisibility(View.VISIBLE); binding.parameterDivider.setVisibility(View.VISIBLE); binding.reservedValueDivider.setVisibility(View.VISIBLE); + binding.exportDivider.setVisibility(View.VISIBLE); switch (settingsItem) { - case DATA_SYNC: - ViewAnimationsKt.expand(binding.syncDataActions, true, () -> { - binding.syncDataActions.setVisibility(View.VISIBLE); - binding.dataDivider.setVisibility(View.GONE); - binding.dataSyncBottomShadow.setVisibility(View.VISIBLE); - binding.dataSyncTopShadow.setVisibility(View.VISIBLE); - scrollToChild(binding.settingsItemData); - return Unit.INSTANCE; - }); - break; - case META_SYNC: - ViewAnimationsKt.expand(binding.syncMetadataActions, true, () -> { - binding.syncMetadataActions.setVisibility(View.VISIBLE); - binding.metaDivider.setVisibility(View.GONE); - binding.metaDataTopShadow.setVisibility(View.VISIBLE); - binding.metaDataBottomShadow.setVisibility(View.VISIBLE); - scrollToChild(binding.settingsItemMeta); - return Unit.INSTANCE; - }); - break; - case SYNC_PARAMETERS: - ViewAnimationsKt.expand(binding.parameterData, true, () -> { - binding.parameterData.setVisibility(View.VISIBLE); - binding.parameterDivider.setVisibility(View.GONE); - binding.itemParamsSyncTopShadow.setVisibility(View.VISIBLE); - binding.itemParamsSyncBottomShadow.setVisibility(View.VISIBLE); - scrollToChild(binding.settingsItemParams); - return Unit.INSTANCE; - }); - break; - case RESERVED_VALUES: - ViewAnimationsKt.expand(binding.reservedValuesActions, true, () -> { - binding.reservedValuesActions.setVisibility(View.VISIBLE); - binding.reservedValueDivider.setVisibility(View.GONE); - binding.reservedValueTopShadow.setVisibility(View.VISIBLE); - binding.reservedValueBottomShadow.setVisibility(View.VISIBLE); - scrollToChild(binding.settingsItemValues); - return Unit.INSTANCE; - }); - break; - case DELETE_LOCAL_DATA: - ViewAnimationsKt.expand(binding.deleteDataButton, true, () -> { - binding.deleteDataButton.setVisibility(View.VISIBLE); - scrollToChild(binding.settingsItemDeleteData); - return Unit.INSTANCE; - }); - break; - case SMS: - ViewAnimationsKt.expand(binding.smsContent, true, () -> { - binding.smsContent.setVisibility(View.VISIBLE); - binding.smsTopShadow.setVisibility(View.VISIBLE); - binding.smsBottomShadow.setVisibility(View.VISIBLE); - scrollToChild(binding.smsSettings); - return Unit.INSTANCE; - }); - break; - case VERSION_UPDATE: - ViewAnimationsKt.expand(binding.versionButton, true, () -> { - binding.versionButton.setVisibility(View.VISIBLE); - scrollToChild(binding.settingsItemVersion); - return Unit.INSTANCE; - }); - break; - default: - break; + case DATA_SYNC -> ViewAnimationsKt.expand(binding.syncDataActions, true, () -> { + binding.syncDataActions.setVisibility(View.VISIBLE); + binding.dataDivider.setVisibility(View.GONE); + binding.dataSyncBottomShadow.setVisibility(View.VISIBLE); + binding.dataSyncTopShadow.setVisibility(View.VISIBLE); + scrollToChild(binding.settingsItemData); + return Unit.INSTANCE; + }); + case META_SYNC -> ViewAnimationsKt.expand(binding.syncMetadataActions, true, () -> { + binding.syncMetadataActions.setVisibility(View.VISIBLE); + binding.metaDivider.setVisibility(View.GONE); + binding.metaDataTopShadow.setVisibility(View.VISIBLE); + binding.metaDataBottomShadow.setVisibility(View.VISIBLE); + scrollToChild(binding.settingsItemMeta); + return Unit.INSTANCE; + }); + case SYNC_PARAMETERS -> ViewAnimationsKt.expand(binding.parameterData, true, () -> { + binding.parameterData.setVisibility(View.VISIBLE); + binding.parameterDivider.setVisibility(View.GONE); + binding.itemParamsSyncTopShadow.setVisibility(View.VISIBLE); + binding.itemParamsSyncBottomShadow.setVisibility(View.VISIBLE); + scrollToChild(binding.settingsItemParams); + return Unit.INSTANCE; + }); + case RESERVED_VALUES -> + ViewAnimationsKt.expand(binding.reservedValuesActions, true, () -> { + binding.reservedValuesActions.setVisibility(View.VISIBLE); + binding.reservedValueDivider.setVisibility(View.GONE); + binding.reservedValueTopShadow.setVisibility(View.VISIBLE); + binding.reservedValueBottomShadow.setVisibility(View.VISIBLE); + scrollToChild(binding.settingsItemValues); + return Unit.INSTANCE; + }); + case EXPORT_DB -> ViewAnimationsKt.expand(binding.exportOptions, true, () -> { + binding.exportShare.setVisibility(View.VISIBLE); + scrollToChild(binding.settingsItemExport); + return Unit.INSTANCE; + }); + case DELETE_LOCAL_DATA -> + ViewAnimationsKt.expand(binding.deleteDataButton, true, () -> { + binding.deleteDataButton.setVisibility(View.VISIBLE); + scrollToChild(binding.settingsItemDeleteData); + return Unit.INSTANCE; + }); + case SMS -> ViewAnimationsKt.expand(binding.smsContent, true, () -> { + binding.smsContent.setVisibility(View.VISIBLE); + binding.smsTopShadow.setVisibility(View.VISIBLE); + binding.smsBottomShadow.setVisibility(View.VISIBLE); + scrollToChild(binding.smsSettings); + return Unit.INSTANCE; + }); + case VERSION_UPDATE -> ViewAnimationsKt.expand(binding.versionButton, true, () -> { + binding.versionButton.setVisibility(View.VISIBLE); + scrollToChild(binding.settingsItemVersion); + return Unit.INSTANCE; + }); } } else { closedSettingItem(settingOpened); @@ -360,13 +375,13 @@ private void scrollToChild(View child) { int[] l = new int[2]; child.getLocationOnScreen(l); Rect rect = new Rect(l[0], l[1], l[0] + child.getWidth(), l[1] + child.getHeight()); - binding.scrollView.requestChildRectangleOnScreen(child,rect, false); + binding.scrollView.requestChildRectangleOnScreen(child, rect, false); } private void closedSettingItem(SettingItem settingItemToClose) { if (settingItemToClose != null) { switch (settingItemToClose) { - case DATA_SYNC: + case DATA_SYNC -> { ViewAnimationsKt.collapse(binding.syncDataActions, () -> { binding.syncDataActions.setVisibility(View.GONE); binding.dataSyncTopShadow.setVisibility(View.GONE); @@ -374,8 +389,8 @@ private void closedSettingItem(SettingItem settingItemToClose) { return Unit.INSTANCE; }); binding.dataDivider.setVisibility(View.VISIBLE); - break; - case META_SYNC: + } + case META_SYNC -> { ViewAnimationsKt.collapse(binding.syncMetadataActions, () -> { binding.syncMetadataActions.setVisibility(View.GONE); binding.metaDataTopShadow.setVisibility(View.GONE); @@ -383,8 +398,8 @@ private void closedSettingItem(SettingItem settingItemToClose) { return Unit.INSTANCE; }); binding.metaDivider.setVisibility(View.VISIBLE); - break; - case SYNC_PARAMETERS: + } + case SYNC_PARAMETERS -> { ViewAnimationsKt.collapse(binding.parameterData, () -> { binding.parameterData.setVisibility(View.GONE); binding.itemParamsSyncTopShadow.setVisibility(View.GONE); @@ -392,8 +407,8 @@ private void closedSettingItem(SettingItem settingItemToClose) { return Unit.INSTANCE; }); binding.parameterDivider.setVisibility(View.VISIBLE); - break; - case RESERVED_VALUES: + } + case RESERVED_VALUES -> { ViewAnimationsKt.collapse(binding.reservedValuesActions, () -> { binding.reservedValuesActions.setVisibility(View.GONE); binding.reservedValueTopShadow.setVisibility(View.GONE); @@ -401,29 +416,29 @@ private void closedSettingItem(SettingItem settingItemToClose) { return Unit.INSTANCE; }); binding.reservedValueDivider.setVisibility(View.VISIBLE); - break; - case DELETE_LOCAL_DATA: - ViewAnimationsKt.collapse(binding.deleteDataButton, () -> { - binding.deleteDataButton.setVisibility(View.GONE); - return Unit.INSTANCE; - }); - break; - case SMS: - ViewAnimationsKt.collapse(binding.smsContent, () -> { - binding.smsContent.setVisibility(View.GONE); - binding.smsTopShadow.setVisibility(View.GONE); - binding.smsBottomShadow.setVisibility(View.GONE); - return Unit.INSTANCE; - }); - break; - case VERSION_UPDATE: - ViewAnimationsKt.collapse(binding.versionButton, () -> { - binding.versionButton.setVisibility(View.GONE); + } + case EXPORT_DB -> { + ViewAnimationsKt.collapse(binding.exportOptions, () -> { + binding.exportShare.setVisibility(View.GONE); return Unit.INSTANCE; }); - break; - default: - break; + binding.exportDivider.setVisibility(View.VISIBLE); + } + case DELETE_LOCAL_DATA -> + ViewAnimationsKt.collapse(binding.deleteDataButton, () -> { + binding.deleteDataButton.setVisibility(View.GONE); + return Unit.INSTANCE; + }); + case SMS -> ViewAnimationsKt.collapse(binding.smsContent, () -> { + binding.smsContent.setVisibility(View.GONE); + binding.smsTopShadow.setVisibility(View.GONE); + binding.smsBottomShadow.setVisibility(View.GONE); + return Unit.INSTANCE; + }); + case VERSION_UPDATE -> ViewAnimationsKt.collapse(binding.versionButton, () -> { + binding.versionButton.setVisibility(View.GONE); + return Unit.INSTANCE; + }); } } } diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt index 10f1249f03..55ead0823a 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt @@ -34,6 +34,7 @@ import org.dhis2.usescases.settings.GatewayValidator.Companion.max_size import org.dhis2.usescases.settings.models.DataSettingsViewModel import org.dhis2.usescases.settings.models.ErrorModelMapper import org.dhis2.usescases.settings.models.ErrorViewModel +import org.dhis2.usescases.settings.models.ExportDbModel import org.dhis2.usescases.settings.models.MetadataSettingsViewModel import org.dhis2.usescases.settings.models.ReservedValueSettingsViewModel import org.dhis2.usescases.settings.models.SMSSettingsViewModel @@ -48,7 +49,6 @@ import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.settings.LimitScope import timber.log.Timber import java.util.Locale -import kotlin.collections.ArrayList import kotlin.coroutines.CoroutineContext class SyncManagerPresenter internal constructor( @@ -87,6 +87,9 @@ class SyncManagerPresenter internal constructor( val versionToUpdate: LiveData = versionRepository.newAppVersion.asLiveData(coroutineContext) + private val _exportedDb = MutableLiveData() + val exportedDb: LiveData = _exportedDb + init { checkData = PublishProcessor.create() compositeDisposable = CompositeDisposable() @@ -106,11 +109,12 @@ class SyncManagerPresenter internal constructor( settingsRepository.syncParameters(), settingsRepository.reservedValues(), settingsRepository.sms(), - ) { metadataSettingsViewModel: MetadataSettingsViewModel?, - dataSettingsViewModel: DataSettingsViewModel?, - syncParametersViewModel: SyncParametersViewModel?, - reservedValueSettingsViewModel: ReservedValueSettingsViewModel?, - smsSettingsViewModel: SMSSettingsViewModel?, + ) { + metadataSettingsViewModel: MetadataSettingsViewModel?, + dataSettingsViewModel: DataSettingsViewModel?, + syncParametersViewModel: SyncParametersViewModel?, + reservedValueSettingsViewModel: ReservedValueSettingsViewModel?, + smsSettingsViewModel: SMSSettingsViewModel?, -> SettingsViewModel( metadataSettingsViewModel!!, @@ -124,13 +128,14 @@ class SyncManagerPresenter internal constructor( .subscribeOn(schedulerProvider.io()) .observeOn(schedulerProvider.ui()) .subscribe( - { ( - metadataSettingsViewModel, - dataSettingsViewModel, - syncParametersViewModel, - reservedValueSettingsViewModel, - smsSettingsViewModel1, - ): SettingsViewModel, + { + ( + metadataSettingsViewModel, + dataSettingsViewModel, + syncParametersViewModel, + reservedValueSettingsViewModel, + smsSettingsViewModel1, + ): SettingsViewModel, -> view.setMetadataSettings( metadataSettingsViewModel, @@ -282,6 +287,7 @@ class SyncManagerPresenter internal constructor( Constants.META_NOW -> view.onMetadataSyncInProgress() Constants.DATA_NOW -> view.onDataSyncInProgress() } + else -> when (workerTag) { Constants.META_NOW -> view.onMetadataFinished() Constants.DATA_NOW -> view.onDataFinished() @@ -465,4 +471,14 @@ class SyncManagerPresenter internal constructor( }, ) } + + fun onExportAndShareDB() { + try { + val db = d2.maintenanceModule().databaseImportExport() + .exportLoggedUserDatabase() + _exportedDb.value = ExportDbModel(file = db) + } catch (e: Exception) { + view.displayMessage(resourceManager.parseD2Error(e)) + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/settings/models/ExportDbModel.kt b/app/src/main/java/org/dhis2/usescases/settings/models/ExportDbModel.kt new file mode 100644 index 0000000000..dfd92512f3 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/settings/models/ExportDbModel.kt @@ -0,0 +1,9 @@ +package org.dhis2.usescases.settings.models + +import java.io.File +import java.util.UUID + +data class ExportDbModel( + val id: UUID = UUID.randomUUID(), + val file: File, +) diff --git a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt index 8daf1bd5b7..1cab8e48f8 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt @@ -1,42 +1,56 @@ package org.dhis2.usescases.settings.ui +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Share import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.google.accompanist.themeadapter.material3.Mdc3Theme import org.dhis2.R -import org.dhis2.ui.IconTextButton +import org.hisp.dhis.mobile.ui.designsystem.component.Button +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle @Composable fun ExportOption( - onClick: () -> Unit + onClick: () -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() - .height(72.dp) + .height(72.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, ) { - IconTextButton( + Button( onClick = onClick, - painter = rememberVectorPainter(image = Icons.Filled.Share), - text = stringResource( - id = R.string.share - ) + style = ButtonStyle.TEXT, + text = stringResource(id = R.string.share), + icon = { + Icon( + imageVector = Icons.Filled.Share, + contentDescription = "Share", + tint = MaterialTheme.colors.primary, + ) + }, ) } } fun ComposeView.setExportOption( - onClick: () -> Unit + onClick: () -> Unit, ) { setContent { - ExportOption(onClick) + Mdc3Theme { + ExportOption(onClick) + } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index c717fadcbe..ba2876c5a9 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -106,9 +106,9 @@ android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginEnd="16dp" + app:addTextButton="@{presenter.syncDataButton}" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/dataOptions" - app:addTextButton="@{presenter.syncDataButton}"/> + app:layout_constraintTop_toBottomOf="@id/dataOptions" /> @@ -211,9 +211,9 @@ android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginEnd="16dp" + app:addTextButton="@{presenter.syncMetaDataButton}" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/metaOptions" - app:addTextButton="@{presenter.syncMetaDataButton}"/> + app:layout_constraintTop_toBottomOf="@id/metaOptions" /> + tools:visibility="visible" /> + app:layout_constraintBottom_toTopOf="@id/specificSettingsText" + app:layout_constraintTop_toBottomOf="@id/teiInputLayout" /> @@ -394,7 +394,7 @@ android:textColor="?colorPrimary" android:textSize="12sp" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/parameterOptions"/> + app:layout_constraintTop_toBottomOf="@id/parameterOptions" /> @@ -554,20 +554,23 @@ android:id="@+id/settingsItemExport" android:layout_width="match_parent" android:layout_height="wrap_content" - android:onClick="@{()->presenter.onItemClick(SettingItem.DELETE_LOCAL_DATA)}"> + android:onClick="@{()->presenter.onItemClick(SettingItem.EXPORT_DB)}"> + android:layout_height="72dp" + android:background="@color/form_field_background" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/exportLayout" + tools:visibility="visible"> @@ -710,33 +714,33 @@ android:id="@+id/versionButton" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" android:background="@color/form_field_background" android:padding="16dp" - android:layout_marginHorizontal="16dp" android:visibility="gone" app:layout_constraintTop_toBottomOf="@id/syncVersionLayout"> + app:layout_constraintTop_toTopOf="parent" /> + app:progressIndicator="@{@string/checking_for_updates}" /> diff --git a/app/src/test/java/org/dhis2/usescases/settings/SyncManagerPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/settings/SyncManagerPresenterTest.kt index c29ed900c3..0098b66f6e 100644 --- a/app/src/test/java/org/dhis2/usescases/settings/SyncManagerPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/settings/SyncManagerPresenterTest.kt @@ -1,5 +1,6 @@ package org.dhis2.usescases.settings +import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.WorkInfo import io.reactivex.Single @@ -29,20 +30,27 @@ import org.dhis2.usescases.settings.models.SyncParametersViewModel import org.dhis2.utils.analytics.AnalyticsHelper import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.settings.LimitScope +import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever +import java.io.File @OptIn(ExperimentalCoroutinesApi::class) class SyncManagerPresenterTest { + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + private lateinit var presenter: SyncManagerPresenter private val d2: D2 = Mockito.mock(D2::class.java, Mockito.RETURNS_DEEP_STUBS) private val schedulers = TrampolineSchedulerProvider() @@ -377,4 +385,22 @@ class SyncManagerPresenterTest { verify(view).isResultTimeoutValid verifyNoMoreInteractions(view) } + + @Test + fun `Should export database`() { + val mockedFile: File = mock() + whenever(d2.maintenanceModule().databaseImportExport())doReturn mock() + whenever(d2.maintenanceModule().databaseImportExport().exportLoggedUserDatabase())doReturn mockedFile + presenter.onExportAndShareDB() + assertTrue(presenter.exportedDb.value?.file == mockedFile) + } + + @Test + fun `Should display export database error`() { + whenever(d2.maintenanceModule().databaseImportExport())doReturn mock() + whenever(d2.maintenanceModule().databaseImportExport().exportLoggedUserDatabase())doThrow RuntimeException("Testing exception") + whenever(resourcesManager.parseD2Error(any()))doReturn "Testing exception" + presenter.onExportAndShareDB() + verify(view).displayMessage("Testing exception") + } } From 385c8c0d5e72b6c4192df7fa382532e2c24fd4e0 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 19 Feb 2024 12:14:22 +0100 Subject: [PATCH 03/19] import database Signed-off-by: Pablo --- .../dhis2/usescases/login/LoginActivity.kt | 68 ++++++-- .../dhis2/usescases/login/LoginContracts.kt | 1 + .../org/dhis2/usescases/login/LoginModule.kt | 3 + .../dhis2/usescases/login/LoginViewModel.kt | 55 +++++-- .../usescases/login/LoginViewModelFactory.kt | 3 + .../login/SyncIsPerformedInteractor.kt | 24 ++- .../dhis2/usescases/login/ui/LoginScreen.kt | 154 ++++++++++++++++++ .../settings/SyncManagerFragment.java | 33 ++-- app/src/main/res/drawable/ic_import_db.xml | 9 + app/src/main/res/layout/activity_login.xml | 23 ++- .../dhis2/commons/resources/D2ErrorUtils.kt | 4 +- 11 files changed, 319 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt create mode 100644 app/src/main/res/drawable/ic_import_db.xml 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 1cd250ba96..a7b3198a51 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt @@ -10,13 +10,22 @@ import android.text.TextWatcher import android.util.Patterns import android.view.View import android.view.WindowManager +import android.webkit.MimeTypeMap import android.webkit.URLUtil import android.widget.ArrayAdapter import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.databinding.DataBindingUtil +import com.google.android.material.composethemeadapter.MdcTheme import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import java.io.BufferedReader +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStreamReader +import java.io.StringWriter +import javax.inject.Inject import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.dhis2.App import org.dhis2.R @@ -42,6 +51,7 @@ import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.usescases.login.accounts.AccountsActivity import org.dhis2.usescases.login.auth.AuthServiceModel import org.dhis2.usescases.login.auth.OpenIdProviders +import org.dhis2.usescases.login.ui.LoginTopBar import org.dhis2.usescases.main.MainActivity import org.dhis2.usescases.qrScanner.ScanActivity import org.dhis2.usescases.sync.SyncActivity @@ -55,10 +65,6 @@ import org.dhis2.utils.session.PIN_DIALOG_TAG import org.dhis2.utils.session.PinDialog import org.hisp.dhis.android.core.user.openid.IntentWithRequestCode import timber.log.Timber -import java.io.BufferedReader -import java.io.InputStreamReader -import java.io.StringWriter -import javax.inject.Inject const val EXTRA_SKIP_SYNC = "SKIP_SYNC" const val EXTRA_SESSION_EXPIRED = "EXTRA_SESSION_EXPIRED" @@ -89,6 +95,34 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { private var skipSync = false private var openIDRequestCode = -1 + private val filePickerLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + result.data?.data?.let { uri -> + val fileType = with(contentResolver) { + MimeTypeMap.getSingleton().getExtensionFromMimeType(getType(uri)) + } + val file = File.createTempFile("importedDb", fileType) + val inputStream = contentResolver.openInputStream(uri)!! + try { + FileOutputStream(file, false).use { outputStream -> + var read: Int + val bytes = ByteArray(DEFAULT_BUFFER_SIZE) + while (inputStream.read(bytes).also { read = it } != -1) { + outputStream.write(bytes, 0, read) + } + } + } catch (e: IOException) { + Timber.e("Failed to load file: ", e.message.toString()) + } + if (file.exists()) + presenter.onImportDataBase(file) + } + } + + override fun onDbImportFinished() { + showLoginProgress(false) + } + companion object { fun bundle( skipSync: Boolean = false, @@ -106,6 +140,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { EXTRA_ACCOUNT_DISABLED, true, ) + null -> { // Nothing to do in this case } @@ -149,6 +184,18 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { binding = DataBindingUtil.setContentView(this, R.layout.activity_login) + binding.topbar.setContent { + MdcTheme { + LoginTopBar(version = buildInfo(), onImportDatabase = { + showLoginProgress(false, "Importing database") + val intent = Intent() + intent.type = "*/*" + intent.action = Intent.ACTION_GET_CONTENT + filePickerLauncher.launch(intent) + }) + } + } + binding.presenter = presenter setLoginVisibility(false) @@ -194,12 +241,11 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { binding.clearUrl.setOnClickListener { binding.serverUrlEdit.text = null } presenter.loginProgressVisible.observe(this) { show -> - showLoginProgress(show) + showLoginProgress(show, getString(R.string.authenticating)) } setTestingCredentials() setAutocompleteAdapters() - setUpLoginInfo() checkMessage() presenter.apply { checkServerInfoAndShowBiometricButton() @@ -212,7 +258,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { private fun checkUrl(urlString: String): Boolean { return URLUtil.isValidUrl(urlString) && - Patterns.WEB_URL.matcher(urlString).matches() && urlString.toHttpUrlOrNull() != null + Patterns.WEB_URL.matcher(urlString).matches() && urlString.toHttpUrlOrNull() != null } override fun setTestingCredentials() { @@ -294,7 +340,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { binding.login.isEnabled = isVisible } - private fun showLoginProgress(showLogin: Boolean) { + private fun showLoginProgress(showLogin: Boolean, message: String? = null) { if (showLogin) { window.setFlags( WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, @@ -304,6 +350,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { binding.progressLayout.visibility = View.VISIBLE } else { window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + binding.progressMessage.text = message binding.credentialLayout.visibility = View.VISIBLE binding.progressLayout.visibility = View.GONE } @@ -388,6 +435,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { handleFingerPrint() goToNextScreen() } + else -> goToNextScreen() } } @@ -544,10 +592,6 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { requestQRScanner.launch(Intent(context, ScanActivity::class.java)) } - private fun setUpLoginInfo() { - binding.appBuildInfo.text = buildInfo() - } - override fun getDefaultServerProtocol(): String = getString(R.string.login_https) private fun showLoginOptions(authServiceModel: AuthServiceModel?) { 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 a8270573c1..05a46cfe35 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginContracts.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginContracts.kt @@ -55,5 +55,6 @@ class LoginContracts { fun openAccountsActivity() fun showNoConnectionDialog() fun initLogin(): UserManager? + fun onDbImportFinished() } } 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 130622fcbb..39571c7b39 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginModule.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginModule.kt @@ -9,6 +9,7 @@ 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.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.data.fingerprint.FingerPrintController import org.dhis2.data.server.UserManager @@ -26,6 +27,7 @@ class LoginModule( @PerActivity fun providePresenter( preferenceProvider: PreferenceProvider, + resourceManager: ResourceManager, schedulerProvider: SchedulerProvider, fingerPrintController: FingerPrintController, analyticsHelper: AnalyticsHelper, @@ -37,6 +39,7 @@ class LoginModule( LoginViewModelFactory( view, preferenceProvider, + resourceManager, schedulerProvider, fingerPrintController, analyticsHelper, 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 5f6cf9e966..991e1c8b71 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt @@ -6,8 +6,12 @@ import androidx.annotation.VisibleForTesting import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable +import java.io.File +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import org.dhis2.commons.Constants.PREFS_URLS import org.dhis2.commons.Constants.PREFS_USERS import org.dhis2.commons.Constants.USER_TEST_ANDROID @@ -22,6 +26,7 @@ import org.dhis2.commons.prefs.SECURE_PASS import org.dhis2.commons.prefs.SECURE_SERVER_URL import org.dhis2.commons.prefs.SECURE_USER_NAME import org.dhis2.commons.reporting.CrashReportController +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.data.fingerprint.FingerPrintController import org.dhis2.data.fingerprint.Type @@ -35,6 +40,7 @@ import org.dhis2.utils.analytics.DATA_STORE_ANALYTICS_PERMISSION_KEY import org.dhis2.utils.analytics.LOGIN import org.dhis2.utils.analytics.SERVER_QR_SCANNER import org.dhis2.utils.analytics.USER_PROPERTY_SERVER +import org.hisp.dhis.android.core.D2Manager import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.hisp.dhis.android.core.systeminfo.SystemInfo @@ -47,6 +53,7 @@ const val VERSION = "version" class LoginViewModel( private val view: LoginContracts.View, private val preferenceProvider: PreferenceProvider, + private val resourceManager: ResourceManager, private val schedulers: SchedulerProvider, private val fingerPrintController: FingerPrintController, private val analyticsHelper: AnalyticsHelper, @@ -69,6 +76,9 @@ class LoginViewModel( private val _loginProgressVisible = MutableLiveData(false) val loginProgressVisible: LiveData = _loginProgressVisible + private val _hasAccounts = MutableLiveData() + val hasAccounts : LiveData = _hasAccounts + init { this.userManager?.let { disposable.add( @@ -103,6 +113,7 @@ class LoginViewModel( ), ) } ?: view.setUrl(view.getDefaultServerProtocol()) + displayManageAccount() } private fun trackServerVersion() { @@ -356,13 +367,13 @@ class LoginViewModel( fun areSameCredentials(): Boolean { return ( - preferenceProvider.areCredentialsSet() && - preferenceProvider.areSameCredentials( - serverUrl.value!!, - userName.value!!, - password.value!!, - ) - ).also { areSameCredentials -> if (!areSameCredentials) saveUserCredentials() } + preferenceProvider.areCredentialsSet() && + preferenceProvider.areSameCredentials( + serverUrl.value!!, + userName.value!!, + password.value!!, + ) + ).also { areSameCredentials -> if (!areSameCredentials) saveUserCredentials() } } private fun saveUserCredentials() { @@ -462,9 +473,9 @@ class LoginViewModel( return Pair(urls, users) } - fun displayManageAccount(): Boolean { + fun displayManageAccount() { val users = userManager?.d2?.userModule()?.accountManager()?.getAccounts()?.count() ?: 0 - return users >= 1 + _hasAccounts.value = (users >= 1) } fun onManageAccountClicked() { @@ -519,8 +530,8 @@ class LoginViewModel( private fun checkData() { val newValue = !serverUrl.value.isNullOrEmpty() && - !userName.value.isNullOrEmpty() && - !password.value.isNullOrEmpty() + !userName.value.isNullOrEmpty() && + !password.value.isNullOrEmpty() if (isDataComplete.value == null || isDataComplete.value != newValue) { isDataComplete.value = newValue } @@ -548,7 +559,25 @@ class LoginViewModel( this.userName.value = userName } - fun testCoverage() { - view.setUser("Coverage test") + fun onImportDataBase(file: File) { + viewModelScope.launch { + val importResult = async { + D2Manager.getD2().maintenanceModule().databaseImportExport().importDatabase(file) + } + val importedMetadata = try { + importResult.await() + }catch (e:Exception){ + view.displayMessage(resourceManager.parseD2Error(e)) + Timber.e(e) + null + } + importedMetadata?.let { + setAccountInfo(it.serverUrl, it.username) + view.setUrl(it.serverUrl) + view.setUser(it.username) + displayManageAccount() + } + view.onDbImportFinished() + } } } 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 6c1e3a0436..f59d167cc6 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.network.NetworkUtils import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.reporting.CrashReportController +import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.data.fingerprint.FingerPrintController import org.dhis2.data.server.UserManager @@ -13,6 +14,7 @@ import org.dhis2.utils.analytics.AnalyticsHelper class LoginViewModelFactory( private val view: LoginContracts.View, private val preferenceProvider: PreferenceProvider, + private val resources:ResourceManager, private val schedulerProvider: SchedulerProvider, private val fingerPrintController: FingerPrintController, private val analyticsHelper: AnalyticsHelper, @@ -24,6 +26,7 @@ class LoginViewModelFactory( return LoginViewModel( view, preferenceProvider, + resources, schedulerProvider, fingerPrintController, analyticsHelper, diff --git a/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt b/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt index 72092b10b7..862e96d92a 100644 --- a/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt +++ b/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt @@ -4,20 +4,26 @@ import org.dhis2.data.server.UserManager import org.dhis2.usescases.sync.WAS_INITIAL_SYNC_DONE class SyncIsPerformedInteractor(private val userManager: UserManager?) { - fun execute(): Boolean { + fun execute(serverUrl: String, username: String): Boolean { if (userManager == null) return false val entryExists = userManager.d2.dataStoreModule().localDataStore().value( WAS_INITIAL_SYNC_DONE, ).blockingExists() - val isInitialSyncDone = if (entryExists) { - val entry = userManager.d2.dataStoreModule().localDataStore().value( - WAS_INITIAL_SYNC_DONE, - ).blockingGet() - !entry?.value().isNullOrEmpty() && entry?.value() == "True" - } else { - false + val dataBaseIsImport = userManager.d2.userModule().accountManager() + .getAccounts().firstOrNull { it.serverUrl() == serverUrl && it.username() == username } + ?.importDB() != null + + return when { + dataBaseIsImport -> true + entryExists -> { + val entry = userManager.d2.dataStoreModule().localDataStore().value( + WAS_INITIAL_SYNC_DONE, + ).blockingGet() + !entry?.value().isNullOrEmpty() && entry?.value() == "True" + } + + else -> false } - return isInitialSyncDone } } diff --git a/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt b/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt new file mode 100644 index 0000000000..ac2e8586bc --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt @@ -0,0 +1,154 @@ +package org.dhis2.usescases.login.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.size +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import org.dhis2.R +import org.hisp.dhis.mobile.ui.designsystem.resource.provideFontResource +import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +@Composable +fun LoginTopBar( + version: String, + onImportDatabase: () -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .height(80.dp) + .background(MaterialTheme.colors.primary) + ) { + val (logoLayout, versionLabel) = createRefs() + + Row( + Modifier + .constrainAs(logoLayout) { + start.linkTo(parent.start) + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + .padding(horizontal = 4.dp), + horizontalArrangement = spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer( + modifier = Modifier + .size(48.dp) + ) + Image( + modifier = Modifier + .weight(1f) + .height(48.dp), + painter = painterResource(id = R.drawable.ic_dhis_white), + contentDescription = "dhis2 logo" + ) + + Box { + IconButton( + modifier = Modifier + .size(48.dp), + onClick = { expanded = true }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "More options", + tint = MaterialTheme.colors.onPrimary + ) + } + + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + DropdownMenuItem(onClick = { + expanded = false + onImportDatabase() + }) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(16.dp) + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_import_db), + contentDescription = "Import database", + tint = MaterialTheme.colors.primary + ) + + Text( + text = "Import database", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = provideFontResource("rubik_medium"), + fontWeight = FontWeight.Medium, + color = Color.Black, + letterSpacing = 0.5.sp, + ) + ) + } + } + } + } + } + + Text( + modifier = Modifier.constrainAs(versionLabel) { + end.linkTo(parent.end, margin = 16.dp) + bottom.linkTo(parent.bottom, margin = 8.dp) + }, + text = version, + style = TextStyle( + fontSize = 12.sp, + lineHeight = 16.sp, + fontFamily = provideFontResource("rubik_medium"), + fontWeight = FontWeight.Medium, + color = TextColor.OnPrimary, + letterSpacing = 0.5.sp, + ) + ) + } +} + +@Preview +@Composable +private fun PreviewLoginTopBar() { + DHIS2Theme { + LoginTopBar(version = "v2.9") {} + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java index 598aaa886b..131ba3fe59 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java @@ -52,6 +52,7 @@ import org.dhis2.bindings.ViewExtensionsKt; import org.dhis2.commons.Constants; import org.dhis2.commons.animations.ViewAnimationsKt; +import org.dhis2.commons.data.FileHandler; import org.dhis2.commons.data.FormFileProvider; import org.dhis2.commons.network.NetworkUtils; import org.dhis2.commons.resources.ColorType; @@ -138,20 +139,24 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, binding.setVersionName(BuildConfig.VERSION_NAME); FormFileProvider.INSTANCE.init(requireContext()); presenter.getExportedDb().observe(getViewLifecycleOwner(), fileData -> { - Uri contentUri = FileProvider.getUriForFile(requireContext(), - FormFileProvider.INSTANCE.fileProviderAuthority, - fileData.getFile()); - Intent intentShare = new Intent(Intent.ACTION_SEND) - .setDataAndType(contentUri, requireContext().getContentResolver().getType(contentUri)) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .putExtra(Intent.EXTRA_STREAM, contentUri); - - Intent chooser = Intent.createChooser(intentShare, getString(R.string.open_with)); - try { - startActivity(chooser); - } catch (Exception e) { - Timber.e(e); - } + new FileHandler().copyAndOpen(fileData.getFile(), fileLiveData -> { + fileLiveData.observe(getViewLifecycleOwner(), file -> { + Uri contentUri = FileProvider.getUriForFile(requireContext(), + FormFileProvider.fileProviderAuthority, + fileData.getFile()); + Intent intentShare = new Intent(Intent.ACTION_SEND) + .setDataAndType(contentUri, requireContext().getContentResolver().getType(contentUri)) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .putExtra(Intent.EXTRA_STREAM, contentUri); + Intent chooser = Intent.createChooser(intentShare, getString(R.string.open_with)); + try { + startActivity(chooser); + } catch (Exception e) { + Timber.e(e); + } + }); + return null; + }); }); ExportOptionKt.setExportOption(binding.exportShare, () -> { diff --git a/app/src/main/res/drawable/ic_import_db.xml b/app/src/main/res/drawable/ic_import_db.xml new file mode 100644 index 0000000000..403fe2b3e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_import_db.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 9f6e87458c..18d2547402 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -17,7 +17,7 @@ android:layout_height="match_parent" android:background="?colorPrimary"> - + - + --> + + + app:layout_constraintTop_toBottomOf="@id/topbar"> @@ -363,9 +369,10 @@ android:paddingEnd="40dp" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toBottomOf="@id/logo"> + app:layout_constraintTop_toBottomOf="@id/topbar"> defaultError() - D2ErrorCode.DATABASE_IMPORT_INVALID_FILE -> defaultError() + D2ErrorCode.DATABASE_IMPORT_FAILED -> "Database import failed" + D2ErrorCode.DATABASE_IMPORT_INVALID_FILE -> "Invalid file" } } From c7d73e1e47b8103bb81edd08f8f26db6c18fd3c8 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 19 Feb 2024 12:23:51 +0100 Subject: [PATCH 04/19] ktlint Signed-off-by: Pablo --- .../dhis2/usescases/login/LoginActivity.kt | 19 +++++++------- .../dhis2/usescases/login/LoginViewModel.kt | 25 ++++++++++--------- .../usescases/login/LoginViewModelFactory.kt | 2 +- .../dhis2/usescases/login/ui/LoginScreen.kt | 25 ++++++++++--------- 4 files changed, 37 insertions(+), 34 deletions(-) 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 a7b3198a51..a119649cfc 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt @@ -19,13 +19,6 @@ import androidx.databinding.DataBindingUtil import com.google.android.material.composethemeadapter.MdcTheme import com.google.gson.Gson import com.google.gson.reflect.TypeToken -import java.io.BufferedReader -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStreamReader -import java.io.StringWriter -import javax.inject.Inject import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.dhis2.App import org.dhis2.R @@ -65,6 +58,13 @@ import org.dhis2.utils.session.PIN_DIALOG_TAG import org.dhis2.utils.session.PinDialog import org.hisp.dhis.android.core.user.openid.IntentWithRequestCode import timber.log.Timber +import java.io.BufferedReader +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStreamReader +import java.io.StringWriter +import javax.inject.Inject const val EXTRA_SKIP_SYNC = "SKIP_SYNC" const val EXTRA_SESSION_EXPIRED = "EXTRA_SESSION_EXPIRED" @@ -114,8 +114,9 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { } catch (e: IOException) { Timber.e("Failed to load file: ", e.message.toString()) } - if (file.exists()) + if (file.exists()) { presenter.onImportDataBase(file) + } } } @@ -258,7 +259,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { private fun checkUrl(urlString: String): Boolean { return URLUtil.isValidUrl(urlString) && - Patterns.WEB_URL.matcher(urlString).matches() && urlString.toHttpUrlOrNull() != null + Patterns.WEB_URL.matcher(urlString).matches() && urlString.toHttpUrlOrNull() != null } override fun setTestingCredentials() { 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 991e1c8b71..1edd1c77ec 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable -import java.io.File import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.dhis2.commons.Constants.PREFS_URLS @@ -47,6 +46,7 @@ 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 const val VERSION = "version" @@ -77,7 +77,7 @@ class LoginViewModel( val loginProgressVisible: LiveData = _loginProgressVisible private val _hasAccounts = MutableLiveData() - val hasAccounts : LiveData = _hasAccounts + val hasAccounts: LiveData = _hasAccounts init { this.userManager?.let { @@ -367,13 +367,13 @@ class LoginViewModel( fun areSameCredentials(): Boolean { return ( - preferenceProvider.areCredentialsSet() && - preferenceProvider.areSameCredentials( - serverUrl.value!!, - userName.value!!, - password.value!!, - ) - ).also { areSameCredentials -> if (!areSameCredentials) saveUserCredentials() } + preferenceProvider.areCredentialsSet() && + preferenceProvider.areSameCredentials( + serverUrl.value!!, + userName.value!!, + password.value!!, + ) + ).also { areSameCredentials -> if (!areSameCredentials) saveUserCredentials() } } private fun saveUserCredentials() { @@ -530,8 +530,8 @@ class LoginViewModel( private fun checkData() { val newValue = !serverUrl.value.isNullOrEmpty() && - !userName.value.isNullOrEmpty() && - !password.value.isNullOrEmpty() + !userName.value.isNullOrEmpty() && + !password.value.isNullOrEmpty() if (isDataComplete.value == null || isDataComplete.value != newValue) { isDataComplete.value = newValue } @@ -566,7 +566,8 @@ class LoginViewModel( } val importedMetadata = try { importResult.await() - }catch (e:Exception){ + } + catch (e: Exception) { view.displayMessage(resourceManager.parseD2Error(e)) Timber.e(e) null 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 f59d167cc6..0f23f21294 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt @@ -14,7 +14,7 @@ import org.dhis2.utils.analytics.AnalyticsHelper class LoginViewModelFactory( private val view: LoginContracts.View, private val preferenceProvider: PreferenceProvider, - private val resources:ResourceManager, + private val resources: ResourceManager, private val schedulerProvider: SchedulerProvider, private val fingerPrintController: FingerPrintController, private val analyticsHelper: AnalyticsHelper, diff --git a/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt b/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt index ac2e8586bc..9197af6307 100644 --- a/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt +++ b/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt @@ -43,7 +43,7 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor @Composable fun LoginTopBar( version: String, - onImportDatabase: () -> Unit + onImportDatabase: () -> Unit, ) { var expanded by remember { mutableStateOf(false) } @@ -51,7 +51,7 @@ fun LoginTopBar( modifier = Modifier .fillMaxWidth() .height(80.dp) - .background(MaterialTheme.colors.primary) + .background(MaterialTheme.colors.primary), ) { val (logoLayout, versionLabel) = createRefs() @@ -65,29 +65,30 @@ fun LoginTopBar( } .padding(horizontal = 4.dp), horizontalArrangement = spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Spacer( modifier = Modifier - .size(48.dp) + .size(48.dp), ) Image( modifier = Modifier .weight(1f) .height(48.dp), painter = painterResource(id = R.drawable.ic_dhis_white), - contentDescription = "dhis2 logo" + contentDescription = "dhis2 logo", ) Box { IconButton( modifier = Modifier .size(48.dp), - onClick = { expanded = true }) { + onClick = { expanded = true }, + ) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "More options", - tint = MaterialTheme.colors.onPrimary + tint = MaterialTheme.colors.onPrimary, ) } @@ -102,12 +103,12 @@ fun LoginTopBar( .height(48.dp) .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = spacedBy(16.dp) + horizontalArrangement = spacedBy(16.dp), ) { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_import_db), contentDescription = "Import database", - tint = MaterialTheme.colors.primary + tint = MaterialTheme.colors.primary, ) Text( @@ -119,7 +120,7 @@ fun LoginTopBar( fontWeight = FontWeight.Medium, color = Color.Black, letterSpacing = 0.5.sp, - ) + ), ) } } @@ -140,7 +141,7 @@ fun LoginTopBar( fontWeight = FontWeight.Medium, color = TextColor.OnPrimary, letterSpacing = 0.5.sp, - ) + ), ) } } @@ -151,4 +152,4 @@ private fun PreviewLoginTopBar() { DHIS2Theme { LoginTopBar(version = "v2.9") {} } -} \ No newline at end of file +} From 8f5b3b8dee8f4c79dd35a38a14190a419e33cf51 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 19 Feb 2024 16:14:29 +0100 Subject: [PATCH 05/19] ktlint Signed-off-by: Pablo --- .../main/java/org/dhis2/usescases/login/LoginViewModel.kt | 3 +-- .../org/dhis2/usescases/login/SyncIsPerformedInteractor.kt | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) 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 1edd1c77ec..cba9074734 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt @@ -566,8 +566,7 @@ class LoginViewModel( } val importedMetadata = try { importResult.await() - } - catch (e: Exception) { + } catch (e: Exception) { view.displayMessage(resourceManager.parseD2Error(e)) Timber.e(e) null diff --git a/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt b/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt index 862e96d92a..0105c0e4b5 100644 --- a/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt +++ b/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt @@ -4,12 +4,16 @@ import org.dhis2.data.server.UserManager import org.dhis2.usescases.sync.WAS_INITIAL_SYNC_DONE class SyncIsPerformedInteractor(private val userManager: UserManager?) { - fun execute(serverUrl: String, username: String): Boolean { + fun execute(): Boolean { if (userManager == null) return false val entryExists = userManager.d2.dataStoreModule().localDataStore().value( WAS_INITIAL_SYNC_DONE, ).blockingExists() + + val serverUrl = userManager.d2.systemInfoModule().systemInfo().blockingGet()?.contextPath() + val username = userManager.d2.userModule().user().blockingGet()?.username() + val dataBaseIsImport = userManager.d2.userModule().accountManager() .getAccounts().firstOrNull { it.serverUrl() == serverUrl && it.username() == username } ?.importDB() != null From 3578242481f47fc5cbcf2c70c4b87b2c4de60660 Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 20 Feb 2024 08:58:28 +0100 Subject: [PATCH 06/19] block login info after importing Signed-off-by: Pablo --- .../dhis2/usescases/login/LoginActivity.kt | 1 + .../org/dhis2/usescases/login/LoginModule.kt | 3 + .../dhis2/usescases/login/LoginViewModel.kt | 39 ++++++----- .../usescases/login/LoginViewModelFactory.kt | 3 + .../usescases/login/LoginViewModelTest.kt | 66 ++++++++++++++++++- 5 files changed, 91 insertions(+), 21 deletions(-) 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 a119649cfc..2caaa00e61 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt @@ -122,6 +122,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { override fun onDbImportFinished() { showLoginProgress(false) + blockLoginInfo() } companion object { 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 39571c7b39..e5b14d772d 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginModule.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginModule.kt @@ -11,6 +11,7 @@ import org.dhis2.commons.prefs.PreferenceProvider 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.server.UserManager import org.dhis2.usescases.login.auth.OpenIdProviders @@ -29,6 +30,7 @@ class LoginModule( preferenceProvider: PreferenceProvider, resourceManager: ResourceManager, schedulerProvider: SchedulerProvider, + dispatcherProvider: DispatcherProvider, fingerPrintController: FingerPrintController, analyticsHelper: AnalyticsHelper, crashReportController: CrashReportController, @@ -41,6 +43,7 @@ class LoginModule( preferenceProvider, resourceManager, schedulerProvider, + dispatcherProvider, fingerPrintController, analyticsHelper, crashReportController, 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 cba9074734..c138246a17 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt @@ -27,6 +27,7 @@ import org.dhis2.commons.prefs.SECURE_USER_NAME 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.server.UserManager @@ -39,7 +40,6 @@ import org.dhis2.utils.analytics.DATA_STORE_ANALYTICS_PERMISSION_KEY import org.dhis2.utils.analytics.LOGIN import org.dhis2.utils.analytics.SERVER_QR_SCANNER import org.dhis2.utils.analytics.USER_PROPERTY_SERVER -import org.hisp.dhis.android.core.D2Manager import org.hisp.dhis.android.core.maintenance.D2Error import org.hisp.dhis.android.core.maintenance.D2ErrorCode import org.hisp.dhis.android.core.systeminfo.SystemInfo @@ -55,6 +55,7 @@ class LoginViewModel( private val preferenceProvider: PreferenceProvider, private val resourceManager: ResourceManager, private val schedulers: SchedulerProvider, + private val dispatchers: DispatcherProvider, private val fingerPrintController: FingerPrintController, private val analyticsHelper: AnalyticsHelper, private val crashReportController: CrashReportController, @@ -560,24 +561,26 @@ class LoginViewModel( } fun onImportDataBase(file: File) { - viewModelScope.launch { - val importResult = async { - D2Manager.getD2().maintenanceModule().databaseImportExport().importDatabase(file) - } - val importedMetadata = try { - importResult.await() - } catch (e: Exception) { - view.displayMessage(resourceManager.parseD2Error(e)) - Timber.e(e) - null - } - importedMetadata?.let { - setAccountInfo(it.serverUrl, it.username) - view.setUrl(it.serverUrl) - view.setUser(it.username) - displayManageAccount() + userManager?.let { + viewModelScope.launch { + val importResult = async(dispatchers.io()) { + it.d2.maintenanceModule().databaseImportExport().importDatabase(file) + } + val importedMetadata = try { + importResult.await() + } catch (e: Exception) { + view.displayMessage(resourceManager.parseD2Error(e)) + Timber.e(e) + null + } + importedMetadata?.let { + setAccountInfo(it.serverUrl, it.username) + view.setUrl(it.serverUrl) + view.setUser(it.username) + displayManageAccount() + } + view.onDbImportFinished() } - view.onDbImportFinished() } } } 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 0f23f21294..cf1b998e88 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModelFactory.kt @@ -7,6 +7,7 @@ import org.dhis2.commons.prefs.PreferenceProvider 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.server.UserManager import org.dhis2.utils.analytics.AnalyticsHelper @@ -16,6 +17,7 @@ class LoginViewModelFactory( private val preferenceProvider: PreferenceProvider, private val resources: ResourceManager, private val schedulerProvider: SchedulerProvider, + private val dispatcherProvider: DispatcherProvider, private val fingerPrintController: FingerPrintController, private val analyticsHelper: AnalyticsHelper, private val crashReportController: CrashReportController, @@ -28,6 +30,7 @@ class LoginViewModelFactory( preferenceProvider, resources, schedulerProvider, + dispatcherProvider, fingerPrintController, analyticsHelper, crashReportController, diff --git a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt index c71f384853..be94b64ffe 100644 --- a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt @@ -4,7 +4,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.setMain import org.dhis2.commons.Constants.PREFS_URLS import org.dhis2.commons.Constants.PREFS_USERS import org.dhis2.commons.Constants.USER_ASKED_CRASHLYTICS @@ -15,7 +19,9 @@ import org.dhis2.commons.prefs.SECURE_PASS import org.dhis2.commons.prefs.SECURE_SERVER_URL import org.dhis2.commons.prefs.SECURE_USER_NAME 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.FingerPrintResult import org.dhis2.data.fingerprint.Type @@ -28,11 +34,13 @@ import org.dhis2.utils.analytics.AnalyticsHelper import org.dhis2.utils.analytics.CLICK import org.dhis2.utils.analytics.LOGIN import org.dhis2.utils.analytics.SERVER_QR_SCANNER +import org.hisp.dhis.android.core.arch.db.access.DatabaseExportMetadata import org.hisp.dhis.android.core.systeminfo.SystemInfo import org.hisp.dhis.android.core.user.User import org.hisp.dhis.android.core.user.openid.IntentWithRequestCode import org.hisp.dhis.android.core.user.openid.OpenIDConnectConfig import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.Mockito @@ -45,6 +53,8 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import retrofit2.Response +import java.io.File + class LoginViewModelTest { @get:Rule @@ -66,12 +76,29 @@ class LoginViewModelTest { private val network: NetworkUtils = mock() private lateinit var loginViewModel: LoginViewModel private val openidconfig: OpenIDConnectConfig = mock() + private val resourceManager: ResourceManager = mock() + private val testingDispatcher = StandardTestDispatcher() + private val dispatcherProvider = object : DispatcherProvider { + override fun io(): CoroutineDispatcher { + return testingDispatcher + } + + override fun computation(): CoroutineDispatcher { + return testingDispatcher + } + + override fun ui(): CoroutineDispatcher { + return testingDispatcher + } + } private fun instantiateLoginViewModel() { loginViewModel = LoginViewModel( view, preferenceProvider, + resourceManager, schedulers, + dispatcherProvider, goldfinger, analyticsHelper, crashReportController, @@ -79,11 +106,14 @@ class LoginViewModelTest { userManager, ) } + private fun instantiateLoginViewModelWithNullUserManager() { loginViewModel = LoginViewModel( view, preferenceProvider, + resourceManager, schedulers, + dispatcherProvider, goldfinger, analyticsHelper, crashReportController, @@ -92,6 +122,12 @@ class LoginViewModelTest { ) } + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + Dispatchers.setMain(testingDispatcher) + } + @Test fun `Should go to MainActivity if user is already logged in`() { whenever(userManager.isUserLoggedIn) doReturn Observable.just(true) @@ -353,8 +389,8 @@ class LoginViewModelTest { fun `Should set server and username if user is logged`() { instantiateLoginViewModel() mockSystemInfo() - whenever(userManager.userName())doReturn Single.just("Username") - whenever(goldfinger.hasFingerPrint())doReturn true + whenever(userManager.userName()) doReturn Single.just("Username") + whenever(goldfinger.hasFingerPrint()) doReturn true whenever(preferenceProvider.contains(SECURE_SERVER_URL)) doReturn true loginViewModel.checkServerInfoAndShowBiometricButton() verify(view).setUrl("contextPath") @@ -389,12 +425,36 @@ class LoginViewModelTest { instantiateLoginViewModelWithNullUserManager() val openidconfig: OpenIDConnectConfig = mock() val it: IntentWithRequestCode = mock() - whenever(view.initLogin())doReturn userManager + whenever(view.initLogin()) doReturn userManager whenever(userManager.logIn(openidconfig)) doReturn Observable.just(it) instantiateLoginViewModelWithNullUserManager() loginViewModel.openIdLogin(openidconfig) verify(view).openOpenIDActivity(it) } + + @Test + fun `Should import database`() { + val mockedDatabase: File = mock() + + instantiateLoginViewModel() + whenever( + userManager.d2.maintenanceModule().databaseImportExport() + .importDatabase(mockedDatabase), + ) doReturn DatabaseExportMetadata( + 0, + "2024-01-01", + "serverUrl", + "userName", + false, + ) + + loginViewModel.onImportDataBase(mockedDatabase) + testingDispatcher.scheduler.advanceUntilIdle() + verify(view).setUrl("serverUrl") + verify(view).setUser("userName") + verify(view).onDbImportFinished() + } + private fun mockSystemInfo(isUserLoggedIn: Boolean = true) { whenever(userManager.isUserLoggedIn) doReturn Observable.just(isUserLoggedIn) if (isUserLoggedIn) { From 6184293dab1b5ccdecce2259e59abb42261b34f0 Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 20 Feb 2024 14:44:47 +0100 Subject: [PATCH 07/19] fix tests Signed-off-by: Pablo --- .../usescases/login/LoginViewModelTest.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt index be94b64ffe..8c8ac05914 100644 --- a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt @@ -381,6 +381,24 @@ class LoginViewModelTest { userManager.d2.dataStoreModule().localDataStore().value("WasInitialSyncDone") .blockingExists(), ) doReturn false + + whenever( + userManager.d2.userModule() + )doReturn mock() + whenever( + userManager.d2.userModule().user() + )doReturn mock() + whenever( + userManager.d2.userModule().user().blockingGet() + )doReturn null + whenever( + userManager.d2.userModule().accountManager() + )doReturn mock() + + whenever( + userManager.d2.userModule().accountManager().getAccounts() + )doReturn listOf() + loginViewModel.handleResponse(response) verify(view).saveUsersData(true, false) } From 954266d4f3976ed5c79a224c139ac19664a21e4b Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 20 Feb 2024 15:03:58 +0100 Subject: [PATCH 08/19] ktlint Signed-off-by: Pablo --- .../org/dhis2/usescases/login/LoginViewModelTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt index 8c8ac05914..05d0d830ef 100644 --- a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt @@ -383,20 +383,20 @@ class LoginViewModelTest { ) doReturn false whenever( - userManager.d2.userModule() + userManager.d2.userModule(), )doReturn mock() whenever( - userManager.d2.userModule().user() + userManager.d2.userModule().user(), )doReturn mock() whenever( - userManager.d2.userModule().user().blockingGet() + userManager.d2.userModule().user().blockingGet(), )doReturn null whenever( - userManager.d2.userModule().accountManager() + userManager.d2.userModule().accountManager(), )doReturn mock() whenever( - userManager.d2.userModule().accountManager().getAccounts() + userManager.d2.userModule().accountManager().getAccounts(), )doReturn listOf() loginViewModel.handleResponse(response) From 40e6caaacf4e25f92f0d59493f083a40b956bfe7 Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 21 Feb 2024 12:16:17 +0100 Subject: [PATCH 09/19] add progress Signed-off-by: Pablo --- .../settings/SyncManagerFragment.java | 72 ++++++++--- .../settings/SyncManagerPresenter.kt | 19 ++- .../settings/models/ExportDbModel.kt | 2 + .../usescases/settings/ui/ExportOption.kt | 114 ++++++++++++++---- 4 files changed, 165 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java index 131ba3fe59..9070907938 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java @@ -35,6 +35,7 @@ import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; @@ -66,6 +67,7 @@ import org.dhis2.usescases.general.FragmentGlobalAbstract; import org.dhis2.usescases.settings.models.DataSettingsViewModel; import org.dhis2.usescases.settings.models.ErrorViewModel; +import org.dhis2.usescases.settings.models.ExportDbModel; import org.dhis2.usescases.settings.models.MetadataSettingsViewModel; import org.dhis2.usescases.settings.models.ReservedValueSettingsViewModel; import org.dhis2.usescases.settings.models.SMSSettingsViewModel; @@ -138,32 +140,62 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, binding.smsSettings.setVisibility(ContextExtensionsKt.showSMS(context) ? View.VISIBLE : View.GONE); binding.setVersionName(BuildConfig.VERSION_NAME); FormFileProvider.INSTANCE.init(requireContext()); + presenter.getExportedDb().observe(getViewLifecycleOwner(), fileData -> { - new FileHandler().copyAndOpen(fileData.getFile(), fileLiveData -> { - fileLiveData.observe(getViewLifecycleOwner(), file -> { - Uri contentUri = FileProvider.getUriForFile(requireContext(), - FormFileProvider.fileProviderAuthority, - fileData.getFile()); - Intent intentShare = new Intent(Intent.ACTION_SEND) - .setDataAndType(contentUri, requireContext().getContentResolver().getType(contentUri)) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .putExtra(Intent.EXTRA_STREAM, contentUri); - Intent chooser = Intent.createChooser(intentShare, getString(R.string.open_with)); - try { - startActivity(chooser); - } catch (Exception e) { - Timber.e(e); - } - }); - return null; + if (fileData.getShare()) { + shareDB(fileData); + } else { + downloadDB(fileData); + } + + }); + + ExportOptionKt.setExportOption( + binding.exportShare, + () -> { + presenter.onExportAndDownloadDB(); + return null; + }, + () -> { + presenter.onExportAndShareDB(); + return null; + }, + ()-> presenter.getExporting() + ); + return binding.getRoot(); + } + + private void shareDB(ExportDbModel fileData) { + new FileHandler().copyAndOpen(fileData.getFile(), fileLiveData -> { + fileLiveData.observe(getViewLifecycleOwner(), file -> { + Uri contentUri = FileProvider.getUriForFile(requireContext(), + FormFileProvider.fileProviderAuthority, + fileData.getFile()); + Intent intentShare = new Intent(Intent.ACTION_SEND) + .setDataAndType(contentUri, requireContext().getContentResolver().getType(contentUri)) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .putExtra(Intent.EXTRA_STREAM, contentUri); + Intent chooser = Intent.createChooser(intentShare, getString(R.string.open_with)); + try { + startActivity(chooser); + } catch (Exception e) { + Timber.e(e); + }finally { + presenter.onExportEnd(); + } }); + return null; }); + } - ExportOptionKt.setExportOption(binding.exportShare, () -> { - presenter.onExportAndShareDB(); + private void downloadDB(ExportDbModel fileData) { + new FileHandler().copyAndOpen(fileData.getFile(), fileLiveData -> { + fileLiveData.observe(getViewLifecycleOwner(), file -> { + Toast.makeText(requireContext(), R.string.downloaded_confirm, Toast.LENGTH_SHORT).show(); + presenter.onExportEnd(); + }); return null; }); - return binding.getRoot(); } @Override diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt index 55ead0823a..74e6f5876e 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt @@ -90,6 +90,9 @@ class SyncManagerPresenter internal constructor( private val _exportedDb = MutableLiveData() val exportedDb: LiveData = _exportedDb + private val _exporting = MutableLiveData(false) + val exporting: LiveData = _exporting + init { checkData = PublishProcessor.create() compositeDisposable = CompositeDisposable() @@ -473,12 +476,26 @@ class SyncManagerPresenter internal constructor( } fun onExportAndShareDB() { + exportDB(download = true, share = false) + } + + fun onExportAndDownloadDB() { + exportDB(download = false, share = true) + } + + private fun exportDB(download: Boolean, share: Boolean) { + _exporting.value = true try { val db = d2.maintenanceModule().databaseImportExport() .exportLoggedUserDatabase() - _exportedDb.value = ExportDbModel(file = db) + _exportedDb.value = ExportDbModel(file = db, share = share, download = download) } catch (e: Exception) { view.displayMessage(resourceManager.parseD2Error(e)) + onExportEnd() } } + + fun onExportEnd() { + _exporting.value = false + } } diff --git a/app/src/main/java/org/dhis2/usescases/settings/models/ExportDbModel.kt b/app/src/main/java/org/dhis2/usescases/settings/models/ExportDbModel.kt index dfd92512f3..dfd049e0f4 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/models/ExportDbModel.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/models/ExportDbModel.kt @@ -6,4 +6,6 @@ import java.util.UUID data class ExportDbModel( val id: UUID = UUID.randomUUID(), val file: File, + val share: Boolean, + val download: Boolean, ) diff --git a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt index 1cab8e48f8..82e97826f6 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt @@ -1,56 +1,128 @@ package org.dhis2.usescases.settings.ui +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Share import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.LiveData import com.google.accompanist.themeadapter.material3.Mdc3Theme 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.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType @Composable fun ExportOption( - onClick: () -> Unit, + onDownload: () -> Unit, + onShare: () -> Unit, + displayProgress: Boolean, ) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(72.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - Button( - onClick = onClick, - style = ButtonStyle.TEXT, - text = stringResource(id = R.string.share), - icon = { - Icon( - imageVector = Icons.Filled.Share, - contentDescription = "Share", - tint = MaterialTheme.colors.primary, + AnimatedContent( + targetState = displayProgress, + transitionSpec = { + fadeIn( + animationSpec = tween(3000), + ) togetherWith fadeOut(animationSpec = tween(3000)) + }, + label = "import content", + ) { targetState -> + + Row( + modifier = Modifier + .fillMaxWidth() + .height(72.dp) + .padding( + start = if (displayProgress) 16.dp else 72.dp, + top = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = if (displayProgress) Arrangement.Center else spacedBy(16.dp), + ) { + if (targetState.not()) { + Button( + modifier = Modifier.weight(1f), + onClick = onDownload, + style = ButtonStyle.TEXT, + text = stringResource(id = R.string.download), + icon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_file_download), + contentDescription = "Download", + tint = MaterialTheme.colors.primary, + ) + }, + ) + + Button( + modifier = Modifier.weight(1f), + onClick = onShare, + style = ButtonStyle.TEXT, + text = stringResource(id = R.string.share), + icon = { + Icon( + imageVector = Icons.Filled.Share, + contentDescription = "Share", + tint = MaterialTheme.colors.primary, + ) + }, ) - }, - ) + } else { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) + } + } + } +} + +@Preview +@Composable +fun PreviewExportOption() { + Mdc3Theme { + ExportOption(onDownload = { }, onShare = { }, false) + } +} + +@Preview +@Composable +fun PreviewExportOptionProgress() { + Mdc3Theme { + ExportOption(onDownload = { }, onShare = { }, true) } } fun ComposeView.setExportOption( - onClick: () -> Unit, + onDownload: () -> Unit, + onShare: () -> Unit, + displayProgressProvider: () -> LiveData, ) { setContent { + val displayProgress by displayProgressProvider().observeAsState(false) Mdc3Theme { - ExportOption(onClick) + ExportOption(onShare, onDownload, displayProgress) } } } From 7d29b0ab90495cbe3f7a56da90bd68f5e71cd8ae Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 21 Feb 2024 15:13:49 +0100 Subject: [PATCH 10/19] fix code smells Signed-off-by: Pablo --- .../login/SyncIsPerformedInteractor.kt | 6 ++-- app/src/main/res/layout/activity_login.xml | 35 ------------------- 2 files changed, 4 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt b/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt index 0105c0e4b5..0b6cf42286 100644 --- a/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt +++ b/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt @@ -15,8 +15,10 @@ class SyncIsPerformedInteractor(private val userManager: UserManager?) { val username = userManager.d2.userModule().user().blockingGet()?.username() val dataBaseIsImport = userManager.d2.userModule().accountManager() - .getAccounts().firstOrNull { it.serverUrl() == serverUrl && it.username() == username } - ?.importDB() != null + .getAccounts() + .any { + it.serverUrl() == serverUrl && it.username() == username && it.importDB() != null + } return when { dataBaseIsImport -> true diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 18d2547402..9d25b41ef1 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -17,41 +17,6 @@ android:layout_height="match_parent" android:background="?colorPrimary"> - - Date: Tue, 27 Feb 2024 10:08:09 +0100 Subject: [PATCH 11/19] fix export db Signed-off-by: Pablo --- .../settings/SyncManagerFragment.java | 3 +++ .../settings/SyncManagerPresenter.kt | 21 ++++++++++++------- .../usescases/settings/ui/ExportOption.kt | 7 ++++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java index 9070907938..428b9b53da 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java @@ -38,6 +38,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.databinding.DataBindingUtil; @@ -83,6 +84,8 @@ import javax.inject.Inject; import kotlin.Unit; +import kotlin.coroutines.Continuation; +import kotlinx.coroutines.flow.FlowCollector; import timber.log.Timber; public class SyncManagerFragment extends FragmentGlobalAbstract implements SyncManagerContracts.View { diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt index 74e6f5876e..6d802859ee 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerPresenter.kt @@ -13,6 +13,7 @@ import io.reactivex.processors.FlowableProcessor import io.reactivex.processors.PublishProcessor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.dhis2.R import org.dhis2.commons.Constants @@ -485,17 +486,21 @@ class SyncManagerPresenter internal constructor( private fun exportDB(download: Boolean, share: Boolean) { _exporting.value = true - try { - val db = d2.maintenanceModule().databaseImportExport() - .exportLoggedUserDatabase() - _exportedDb.value = ExportDbModel(file = db, share = share, download = download) - } catch (e: Exception) { - view.displayMessage(resourceManager.parseD2Error(e)) - onExportEnd() + launch(context = dispatcherProvider.ui()) { + try { + val db = async(dispatcherProvider.io()) { + d2.maintenanceModule().databaseImportExport() + .exportLoggedUserDatabase() + }.await() + _exportedDb.postValue(ExportDbModel(file = db, share = share, download = download)) + } catch (e: Exception) { + view.displayMessage(resourceManager.parseD2Error(e)) + onExportEnd() + } } } fun onExportEnd() { - _exporting.value = false + _exporting.postValue(false) } } diff --git a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt index 82e97826f6..f105da265d 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.LiveData import com.google.accompanist.themeadapter.material3.Mdc3Theme +import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.R import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle @@ -44,8 +45,8 @@ fun ExportOption( targetState = displayProgress, transitionSpec = { fadeIn( - animationSpec = tween(3000), - ) togetherWith fadeOut(animationSpec = tween(3000)) + animationSpec = tween(700), + ) togetherWith fadeOut(animationSpec = tween(700)) }, label = "import content", ) { targetState -> @@ -121,7 +122,7 @@ fun ComposeView.setExportOption( ) { setContent { val displayProgress by displayProgressProvider().observeAsState(false) - Mdc3Theme { + MdcTheme { ExportOption(onShare, onDownload, displayProgress) } } From ac0f0f4a3920b2f365f6a48c222599adf32e73b5 Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 27 Feb 2024 14:32:46 +0100 Subject: [PATCH 12/19] fix download for android < 10 Signed-off-by: Pablo --- .../settings/SyncManagerFragment.java | 2 +- .../usescases/settings/ui/ExportOption.kt | 39 ++++++++++++------- app/src/main/res/values/strings.xml | 1 + .../org/dhis2/commons/data/FileHandler.kt | 23 ++++++++--- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java index 428b9b53da..ab27a4d59b 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java +++ b/app/src/main/java/org/dhis2/usescases/settings/SyncManagerFragment.java @@ -194,7 +194,7 @@ private void shareDB(ExportDbModel fileData) { private void downloadDB(ExportDbModel fileData) { new FileHandler().copyAndOpen(fileData.getFile(), fileLiveData -> { fileLiveData.observe(getViewLifecycleOwner(), file -> { - Toast.makeText(requireContext(), R.string.downloaded_confirm, Toast.LENGTH_SHORT).show(); + Toast.makeText(requireContext(), R.string.database_export_downloaded, Toast.LENGTH_SHORT).show(); presenter.onExportEnd(); }); return null; diff --git a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt index f105da265d..2c065ac55c 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt @@ -51,20 +51,20 @@ fun ExportOption( label = "import content", ) { targetState -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(72.dp) - .padding( - start = if (displayProgress) 16.dp else 72.dp, - top = 16.dp, - end = 16.dp, - bottom = 16.dp, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = if (displayProgress) Arrangement.Center else spacedBy(16.dp), - ) { - if (targetState.not()) { + if (targetState.not()) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(72.dp) + .padding( + start = 72.dp, + top = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = if (displayProgress) Arrangement.Center else spacedBy(16.dp), + ) { Button( modifier = Modifier.weight(1f), onClick = onDownload, @@ -92,7 +92,16 @@ fun ExportOption( ) }, ) - } else { + } + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .height(72.dp) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = if (displayProgress) Arrangement.Center else spacedBy(16.dp), + ) { ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2cac87e68..965af9aa4c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -963,4 +963,5 @@ Timeline Clear search Optional + Database downloaded diff --git a/commons/src/main/java/org/dhis2/commons/data/FileHandler.kt b/commons/src/main/java/org/dhis2/commons/data/FileHandler.kt index 4be787d393..ee57921290 100644 --- a/commons/src/main/java/org/dhis2/commons/data/FileHandler.kt +++ b/commons/src/main/java/org/dhis2/commons/data/FileHandler.kt @@ -1,6 +1,7 @@ package org.dhis2.commons.data import android.graphics.Bitmap +import android.os.Build import android.os.Environment import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -43,10 +44,20 @@ class FileHandler { return sourceFile.copyTo(destinationDirectory, true) } - private fun getDownloadDirectory(outputFileName: String) = File( - Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_DOWNLOADS, - ), - "dhis2" + File.separator + outputFileName, - ) + private fun getDownloadDirectory(outputFileName: String) = if (Build.VERSION.SDK_INT >= 29) { + File( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS, + ), + "dhis2" + File.separator + outputFileName, + ) + } else { + File.createTempFile( + "copied_", + outputFileName, + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS, + ), + ) + } } From aecd7a54cb28e12e8cc1ff153987ffa22c741a62 Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 27 Feb 2024 15:48:22 +0100 Subject: [PATCH 13/19] add permission checks Signed-off-by: Pablo --- .../usescases/settings/ui/ExportOption.kt | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt index 2c065ac55c..3bec711847 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt @@ -1,5 +1,8 @@ package org.dhis2.usescases.settings.ui +import android.content.pm.PackageManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -22,10 +25,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import com.google.accompanist.themeadapter.material3.Mdc3Theme import com.google.android.material.composethemeadapter.MdcTheme @@ -41,6 +46,19 @@ fun ExportOption( onShare: () -> Unit, displayProgress: Boolean, ) { + var onPermissionGrantedCallback: () -> Unit = {} + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { isGranted -> + if (isGranted) { + onPermissionGrantedCallback() + } + onPermissionGrantedCallback = {} + } + + val context = LocalContext.current + AnimatedContent( targetState = displayProgress, transitionSpec = { @@ -67,7 +85,18 @@ fun ExportOption( ) { Button( modifier = Modifier.weight(1f), - onClick = onDownload, + onClick = { + if (ContextCompat.checkSelfPermission( + context, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + ) == PackageManager.PERMISSION_GRANTED + ) { + onDownload() + } else { + onPermissionGrantedCallback = onDownload + launcher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + }, style = ButtonStyle.TEXT, text = stringResource(id = R.string.download), icon = { @@ -81,7 +110,18 @@ fun ExportOption( Button( modifier = Modifier.weight(1f), - onClick = onShare, + onClick = { + if (ContextCompat.checkSelfPermission( + context, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + ) == PackageManager.PERMISSION_GRANTED + ) { + onShare() + } else { + onPermissionGrantedCallback = onShare + launcher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + }, style = ButtonStyle.TEXT, text = stringResource(id = R.string.share), icon = { From 18b2f4aa85680145e2d6ca4a01c5e39e4f9f5c6e Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 28 Feb 2024 09:46:55 +0100 Subject: [PATCH 14/19] fix crash while importing db Signed-off-by: Pablo --- .../dhis2/usescases/login/LoginViewModel.kt | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) 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 c138246a17..ab9c35b2ff 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt @@ -563,22 +563,28 @@ class LoginViewModel( fun onImportDataBase(file: File) { userManager?.let { viewModelScope.launch { - val importResult = async(dispatchers.io()) { - it.d2.maintenanceModule().databaseImportExport().importDatabase(file) - } - val importedMetadata = try { - importResult.await() - } catch (e: Exception) { - view.displayMessage(resourceManager.parseD2Error(e)) - Timber.e(e) - null - } - importedMetadata?.let { - setAccountInfo(it.serverUrl, it.username) - view.setUrl(it.serverUrl) - view.setUser(it.username) - displayManageAccount() + val result = async { + try { + val importedMetadata = + it.d2.maintenanceModule().databaseImportExport().importDatabase(file) + Result.success(importedMetadata) + } catch (e: Exception) { + Result.failure(e) + } } + + result.await().fold( + onSuccess = { + setAccountInfo(it.serverUrl, it.username) + view.setUrl(it.serverUrl) + view.setUser(it.username) + displayManageAccount() + }, + onFailure = { + view.displayMessage(resourceManager.parseD2Error(it)) + }, + ) + view.onDbImportFinished() } } From 3594fe93e2e23a0fcdb9042080e6bb25e0c8742a Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 28 Feb 2024 09:58:08 +0100 Subject: [PATCH 15/19] extract string resource Signed-off-by: Pablo --- app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 2caaa00e61..931f80704b 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt @@ -189,7 +189,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { binding.topbar.setContent { MdcTheme { LoginTopBar(version = buildInfo(), onImportDatabase = { - showLoginProgress(false, "Importing database") + showLoginProgress(false, getString(R.string.importing_database)) val intent = Intent() intent.type = "*/*" intent.action = Intent.ACTION_GET_CONTENT diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 965af9aa4c..0d42e01e85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -964,4 +964,5 @@ Clear search Optional Database downloaded + Importing database From 22cf6d2977769e27851a9c54205aa5bcc349d77c Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 4 Mar 2024 12:28:17 +0100 Subject: [PATCH 16/19] fix design comments Signed-off-by: Pablo --- .../org/dhis2/bindings/ContextExtensions.kt | 2 +- .../org/dhis2/bindings/ContextExtensions.kt | 2 +- .../org/dhis2/bindings/ContextExtensions.kt | 2 +- app/src/main/java/org/dhis2/App.java | 9 +- .../dhis2/usescases/login/LoginActivity.kt | 29 ++++-- .../dhis2/usescases/login/LoginContracts.kt | 2 +- .../dhis2/usescases/login/LoginViewModel.kt | 16 +++- .../login/accounts/AccountRepository.kt | 12 +++ .../login/accounts/AccountsActivity.kt | 59 ++++++++++-- .../login/accounts/AccountsViewModel.kt | 27 ++++++ .../login/accounts/ui/AccountsScreen.kt | 16 +++- .../dhis2/usescases/login/ui/LoginScreen.kt | 95 ++++++++++--------- .../usescases/settings/ui/ExportOption.kt | 54 ++++++++++- .../analytics/matomo/TrackerController.kt | 5 +- .../usescases/login/LoginViewModelTest.kt | 2 +- .../org/dhis2/commons/data/FileHandler.kt | 2 + 16 files changed, 255 insertions(+), 79 deletions(-) diff --git a/app/src/dhis/java/org/dhis2/bindings/ContextExtensions.kt b/app/src/dhis/java/org/dhis2/bindings/ContextExtensions.kt index 177f8e5df1..b9d36fd878 100644 --- a/app/src/dhis/java/org/dhis2/bindings/ContextExtensions.kt +++ b/app/src/dhis/java/org/dhis2/bindings/ContextExtensions.kt @@ -11,7 +11,7 @@ fun Context.buildInfo(): String { return if (BuildConfig.BUILD_TYPE == "release") { "v${BuildConfig.VERSION_NAME}" } else { - "v${BuildConfig.VERSION_NAME} : ${BuildConfig.BUILD_DATE} : ${BuildConfig.GIT_SHA} " + "v${BuildConfig.VERSION_NAME} : ${BuildConfig.GIT_SHA} " } } diff --git a/app/src/dhisPlayServices/java/org/dhis2/bindings/ContextExtensions.kt b/app/src/dhisPlayServices/java/org/dhis2/bindings/ContextExtensions.kt index 1699dcb3e7..caa840d62e 100644 --- a/app/src/dhisPlayServices/java/org/dhis2/bindings/ContextExtensions.kt +++ b/app/src/dhisPlayServices/java/org/dhis2/bindings/ContextExtensions.kt @@ -10,7 +10,7 @@ fun Context.buildInfo(): String { return if (BuildConfig.BUILD_TYPE == "release") { "v${BuildConfig.VERSION_NAME}" } else { - "v${BuildConfig.VERSION_NAME} : ${BuildConfig.BUILD_DATE} : ${BuildConfig.GIT_SHA} " + "v${BuildConfig.VERSION_NAME} : ${BuildConfig.GIT_SHA} " } } diff --git a/app/src/dhisUITesting/java/org/dhis2/bindings/ContextExtensions.kt b/app/src/dhisUITesting/java/org/dhis2/bindings/ContextExtensions.kt index 9c26109728..982b706a05 100644 --- a/app/src/dhisUITesting/java/org/dhis2/bindings/ContextExtensions.kt +++ b/app/src/dhisUITesting/java/org/dhis2/bindings/ContextExtensions.kt @@ -11,7 +11,7 @@ fun Context.buildInfo(): String { return if (BuildConfig.BUILD_TYPE == "release") { "v${BuildConfig.VERSION_NAME}" } else { - "v${BuildConfig.VERSION_NAME} : ${BuildConfig.BUILD_DATE} : ${BuildConfig.GIT_SHA} " + "v${BuildConfig.VERSION_NAME} : ${BuildConfig.GIT_SHA} " } } diff --git a/app/src/main/java/org/dhis2/App.java b/app/src/main/java/org/dhis2/App.java index 3b4c5e2f4a..e1a443f550 100644 --- a/app/src/main/java/org/dhis2/App.java +++ b/app/src/main/java/org/dhis2/App.java @@ -4,6 +4,7 @@ import android.content.Context; import android.os.Looper; +import android.os.StrictMode; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -108,6 +109,12 @@ public class App extends MultiDexApplication implements Components, LifecycleObs public void onCreate() { super.onCreate(); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .penaltyDeath() + .build() + ); ProcessLifecycleOwner.get().getLifecycle().addObserver(this); @@ -116,7 +123,7 @@ public void onCreate() { MapController.Companion.init(this); setUpAppComponent(); - if(BuildConfig.DEBUG){ + if (BuildConfig.DEBUG) { Timber.plant(new DebugTree()); } 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 931f80704b..0983a16df2 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginActivity.kt @@ -15,6 +15,8 @@ import android.webkit.URLUtil import android.widget.ArrayAdapter import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.databinding.DataBindingUtil import com.google.android.material.composethemeadapter.MdcTheme import com.google.gson.Gson @@ -120,9 +122,11 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { } } - override fun onDbImportFinished() { + override fun onDbImportFinished(isSuccess: Boolean) { showLoginProgress(false) - blockLoginInfo() + if (isSuccess) { + blockLoginInfo() + } } companion object { @@ -187,14 +191,19 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { binding = DataBindingUtil.setContentView(this, R.layout.activity_login) binding.topbar.setContent { + val displayMoreActions by presenter.displayMoreActions().observeAsState(true) MdcTheme { - LoginTopBar(version = buildInfo(), onImportDatabase = { - showLoginProgress(false, getString(R.string.importing_database)) - val intent = Intent() - intent.type = "*/*" - intent.action = Intent.ACTION_GET_CONTENT - filePickerLauncher.launch(intent) - }) + LoginTopBar( + version = buildInfo(), + displayMoreActions = displayMoreActions, + onImportDatabase = { + showLoginProgress(false, getString(R.string.importing_database)) + val intent = Intent() + intent.type = "*/*" + intent.action = Intent.ACTION_GET_CONTENT + filePickerLauncher.launch(intent) + }, + ) } } @@ -540,6 +549,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { binding.userNameEdit.isEnabled = true binding.clearUrl.visibility = View.VISIBLE binding.clearUserNameButton.visibility = View.VISIBLE + presenter.setDisplayMoreActions(true) } private fun blockLoginInfo() { @@ -549,6 +559,7 @@ class LoginActivity : ActivityGlobalAbstract(), LoginContracts.View { binding.userNameEdit.isEnabled = false binding.clearUrl.visibility = View.GONE binding.clearUserNameButton.visibility = View.GONE + presenter.setDisplayMoreActions(false) } /* 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 05a46cfe35..cdb1882b88 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginContracts.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginContracts.kt @@ -55,6 +55,6 @@ class LoginContracts { fun openAccountsActivity() fun showNoConnectionDialog() fun initLogin(): UserManager? - fun onDbImportFinished() + fun onDbImportFinished(isSuccess: Boolean) } } 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 ab9c35b2ff..1e4f1710a9 100644 --- a/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/LoginViewModel.kt @@ -80,6 +80,9 @@ class LoginViewModel( private val _hasAccounts = MutableLiveData() val hasAccounts: LiveData = _hasAccounts + private val _displayMoreActions = MutableLiveData(true) + val displayMoreActions: LiveData = _displayMoreActions + init { this.userManager?.let { disposable.add( @@ -563,7 +566,7 @@ class LoginViewModel( fun onImportDataBase(file: File) { userManager?.let { viewModelScope.launch { - val result = async { + val resultJob = async { try { val importedMetadata = it.d2.maintenanceModule().databaseImportExport().importDatabase(file) @@ -573,7 +576,9 @@ class LoginViewModel( } } - result.await().fold( + val result = resultJob.await() + + result.fold( onSuccess = { setAccountInfo(it.serverUrl, it.username) view.setUrl(it.serverUrl) @@ -585,8 +590,13 @@ class LoginViewModel( }, ) - view.onDbImportFinished() + view.onDbImportFinished(result.isSuccess) } } } + + fun displayMoreActions() = displayMoreActions + fun setDisplayMoreActions(shouldDisplayMoreActions: Boolean) { + _displayMoreActions.postValue(shouldDisplayMoreActions) + } } diff --git a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountRepository.kt b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountRepository.kt index cbc58d709d..fa950ca3d3 100644 --- a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountRepository.kt @@ -1,6 +1,8 @@ package org.dhis2.usescases.login.accounts import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.arch.db.access.DatabaseExportMetadata +import java.io.File class AccountRepository(val d2: D2) { @@ -9,4 +11,14 @@ class AccountRepository(val d2: D2) { AccountModel(it.username(), it.serverUrl()) } } + + fun importDatabase(file: File): Result { + return try { + val importedMetadata = + d2.maintenanceModule().databaseImportExport().importDatabase(file) + Result.success(importedMetadata) + } catch (e: Exception) { + Result.failure(e) + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsActivity.kt b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsActivity.kt index 01b80005a1..cca1514537 100644 --- a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsActivity.kt @@ -1,14 +1,24 @@ package org.dhis2.usescases.login.accounts +import android.content.Intent import android.os.Bundle +import android.webkit.MimeTypeMap import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.livedata.observeAsState +import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.bindings.app +import org.dhis2.commons.resources.ColorUtils +import org.dhis2.commons.resources.ResourceManager import org.dhis2.usescases.general.ActivityGlobalAbstract import org.dhis2.usescases.login.LoginActivity import org.dhis2.usescases.login.accounts.ui.AccountsScreen +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.io.IOException import javax.inject.Inject class AccountsActivity : ActivityGlobalAbstract() { @@ -17,6 +27,35 @@ class AccountsActivity : ActivityGlobalAbstract() { lateinit var viewModelFactory: AccountsViewModelFactory private val viewModel: AccountsViewModel by viewModels { viewModelFactory } + private val filePickerLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + result.data?.data?.let { uri -> + val fileType = with(contentResolver) { + MimeTypeMap.getSingleton().getExtensionFromMimeType(getType(uri)) + } + val file = File.createTempFile("importedDb", fileType) + val inputStream = contentResolver.openInputStream(uri)!! + try { + FileOutputStream(file, false).use { outputStream -> + var read: Int + val bytes = ByteArray(DEFAULT_BUFFER_SIZE) + while (inputStream.read(bytes).also { read = it } != -1) { + outputStream.write(bytes, 0, read) + } + } + } catch (e: IOException) { + Timber.e("Failed to load file: ", e.message.toString()) + } + if (file.exists()) { + viewModel.onImportDataBase( + file, + { navigateToLogin(it) }, + { displayMessage(ResourceManager(this, ColorUtils()).parseD2Error(it)) }, + ) + } + } + } + @ExperimentalMaterialApi override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -24,12 +63,20 @@ class AccountsActivity : ActivityGlobalAbstract() { app().serverComponent()?.plus(AccountsModule())?.inject(this) setContent { - val accounts = viewModel.accounts.observeAsState(listOf()) - AccountsScreen( - accounts = accounts.value, - onAccountClicked = { navigateToLogin(it) }, - onAddAccountClicked = { navigateToLogin() }, - ) + MdcTheme { + val accounts = viewModel.accounts.observeAsState(listOf()) + AccountsScreen( + accounts = accounts.value, + onAccountClicked = { navigateToLogin(it) }, + onAddAccountClicked = { navigateToLogin() }, + onImportDatabase = { + val intent = Intent() + intent.type = "*/*" + intent.action = Intent.ACTION_GET_CONTENT + filePickerLauncher.launch(intent) + }, + ) + } } viewModel.getAccounts() } diff --git a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsViewModel.kt b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsViewModel.kt index bd3911b48f..6acdc459d2 100644 --- a/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/login/accounts/AccountsViewModel.kt @@ -3,6 +3,10 @@ package org.dhis2.usescases.login.accounts import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import java.io.File class AccountsViewModel( val repository: AccountRepository, @@ -15,4 +19,27 @@ class AccountsViewModel( fun getAccounts() { _accounts.value = repository.getLoggedInAccounts() } + + fun onImportDataBase( + file: File, + onSuccess: (AccountModel) -> Unit, + onFailure: (Throwable) -> Unit, + ) { + viewModelScope.launch { + val resultJob = async { repository.importDatabase(file) } + + val result = resultJob.await() + + result.fold( + onSuccess = { + onSuccess( + AccountModel(it.username, it.serverUrl), + ) + }, + onFailure = { + onFailure(it) + }, + ) + } + } } diff --git a/app/src/main/java/org/dhis2/usescases/login/accounts/ui/AccountsScreen.kt b/app/src/main/java/org/dhis2/usescases/login/accounts/ui/AccountsScreen.kt index 009bf57d6a..fabcf741ae 100644 --- a/app/src/main/java/org/dhis2/usescases/login/accounts/ui/AccountsScreen.kt +++ b/app/src/main/java/org/dhis2/usescases/login/accounts/ui/AccountsScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.viewinterop.AndroidView import org.dhis2.R import org.dhis2.bindings.buildInfo import org.dhis2.usescases.login.accounts.AccountModel +import org.dhis2.usescases.login.ui.LoginTopBar @ExperimentalMaterialApi @Composable @@ -42,6 +43,7 @@ fun AccountsScreen( accounts: List, onAccountClicked: (AccountModel) -> Unit, onAddAccountClicked: () -> Unit, + onImportDatabase: () -> Unit, ) { MaterialTheme { Column( @@ -49,7 +51,11 @@ fun AccountsScreen( .fillMaxWidth() .background(colorResource(id = R.color.colorPrimary)), ) { - LoginHeader() + LoginTopBar( + version = LocalContext.current.buildInfo(), + onImportDatabase = onImportDatabase, + ) + Column( verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier @@ -57,7 +63,11 @@ fun AccountsScreen( .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) .background(Color.White), ) { - LazyColumn(Modifier.weight(1f).padding(top = 16.dp)) { + LazyColumn( + Modifier + .weight(1f) + .padding(top = 16.dp), + ) { items(accounts) { AccountItem( Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -134,6 +144,7 @@ fun AccountsPreview() { accounts, {}, {}, + {}, ) } @@ -150,5 +161,6 @@ fun FewAccountsPreview() { accounts, {}, {}, + {}, ) } diff --git a/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt b/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt index 9197af6307..0e0bf01898 100644 --- a/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt +++ b/app/src/main/java/org/dhis2/usescases/login/ui/LoginScreen.kt @@ -38,11 +38,12 @@ import androidx.constraintlayout.compose.ConstraintLayout import org.dhis2.R import org.hisp.dhis.mobile.ui.designsystem.resource.provideFontResource import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme -import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @Composable fun LoginTopBar( version: String, + displayMoreActions: Boolean = true, onImportDatabase: () -> Unit, ) { var expanded by remember { mutableStateOf(false) } @@ -79,49 +80,51 @@ fun LoginTopBar( contentDescription = "dhis2 logo", ) - Box { - IconButton( - modifier = Modifier - .size(48.dp), - onClick = { expanded = true }, - ) { - Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = "More options", - tint = MaterialTheme.colors.onPrimary, - ) - } + if (displayMoreActions) { + Box { + IconButton( + modifier = Modifier + .size(48.dp), + onClick = { expanded = true }, + ) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "More options", + tint = MaterialTheme.colors.onPrimary, + ) + } - DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { - DropdownMenuItem(onClick = { - expanded = false - onImportDatabase() - }) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = spacedBy(16.dp), - ) { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_import_db), - contentDescription = "Import database", - tint = MaterialTheme.colors.primary, - ) + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + DropdownMenuItem(onClick = { + expanded = false + onImportDatabase() + }) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(16.dp), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_import_db), + contentDescription = "Import database", + tint = MaterialTheme.colors.primary, + ) - Text( - text = "Import database", - style = TextStyle( - fontSize = 16.sp, - lineHeight = 24.sp, - fontFamily = provideFontResource("rubik_medium"), - fontWeight = FontWeight.Medium, - color = Color.Black, - letterSpacing = 0.5.sp, - ), - ) + Text( + text = "Import database", + style = TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontFamily = provideFontResource("rubik_regular"), + fontWeight = FontWeight.Normal, + color = Color.Black, + letterSpacing = 0.5.sp, + ), + ) + } } } } @@ -137,10 +140,10 @@ fun LoginTopBar( style = TextStyle( fontSize = 12.sp, lineHeight = 16.sp, - fontFamily = provideFontResource("rubik_medium"), - fontWeight = FontWeight.Medium, - color = TextColor.OnPrimary, - letterSpacing = 0.5.sp, + fontFamily = provideFontResource("rubik_regular"), + fontWeight = FontWeight.Normal, + color = SurfaceColor.ContainerHighest, + letterSpacing = 0.4.sp, ), ) } diff --git a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt index 3bec711847..5215b119dd 100644 --- a/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt +++ b/app/src/main/java/org/dhis2/usescases/settings/ui/ExportOption.kt @@ -1,6 +1,10 @@ package org.dhis2.usescases.settings.ui +import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent @@ -21,6 +25,9 @@ import androidx.compose.material.icons.filled.Share import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -35,6 +42,8 @@ import androidx.lifecycle.LiveData import com.google.accompanist.themeadapter.material3.Mdc3Theme import com.google.android.material.composethemeadapter.MdcTheme import org.dhis2.R +import org.dhis2.ui.dialogs.alert.Dhis2AlertDialogUi +import org.dhis2.ui.model.ButtonUiModel 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.ProgressIndicator @@ -46,18 +55,31 @@ fun ExportOption( onShare: () -> Unit, displayProgress: Boolean, ) { - var onPermissionGrantedCallback: () -> Unit = {} + val context = LocalContext.current + var onPermissionGrantedCallback: () -> Unit = {} + var showPermissionDialog by remember { mutableStateOf(false) } val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), ) { isGranted -> if (isGranted) { onPermissionGrantedCallback() + } else { + showPermissionDialog = true } - onPermissionGrantedCallback = {} } - val context = LocalContext.current + val permissionSettingLauncher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { + if (ContextCompat.checkSelfPermission( + context, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + ) == PackageManager.PERMISSION_GRANTED + ) { + onPermissionGrantedCallback() + } + onPermissionGrantedCallback = {} + } AnimatedContent( targetState = displayProgress, @@ -86,7 +108,8 @@ fun ExportOption( Button( modifier = Modifier.weight(1f), onClick = { - if (ContextCompat.checkSelfPermission( + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU || + ContextCompat.checkSelfPermission( context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE, ) == PackageManager.PERMISSION_GRANTED @@ -111,7 +134,8 @@ fun ExportOption( Button( modifier = Modifier.weight(1f), onClick = { - if (ContextCompat.checkSelfPermission( + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU || + ContextCompat.checkSelfPermission( context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE, ) == PackageManager.PERMISSION_GRANTED @@ -146,6 +170,26 @@ fun ExportOption( } } } + + if (showPermissionDialog) { + Dhis2AlertDialogUi( + labelText = stringResource(id = R.string.permission_denied), + descriptionText = "You need to provide the permission to carry out this action", + iconResource = R.drawable.ic_info, + dismissButton = ButtonUiModel("Cancel") { + showPermissionDialog = false + onPermissionGrantedCallback = {} + }, + confirmButton = ButtonUiModel("Change permission") { + permissionSettingLauncher.launch( + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null), + ), + ) + }, + ) + } } @Preview diff --git a/app/src/main/java/org/dhis2/utils/analytics/matomo/TrackerController.kt b/app/src/main/java/org/dhis2/utils/analytics/matomo/TrackerController.kt index a279316cd8..063725cc3c 100644 --- a/app/src/main/java/org/dhis2/utils/analytics/matomo/TrackerController.kt +++ b/app/src/main/java/org/dhis2/utils/analytics/matomo/TrackerController.kt @@ -10,8 +10,9 @@ const val DEFAULT_EXTERNAL_TRACKER_NAME = "secondaryTracker" class TrackerController { companion object { fun dhis2InternalTracker(matomo: Matomo): Tracker? { - return TrackerBuilder.createDefault(BuildConfig.MATOMO_URL, BuildConfig.MATOMO_ID) - .build(matomo) + return null + /*return TrackerBuilder.createDefault(BuildConfig.MATOMO_URL, BuildConfig.MATOMO_ID) + .build(matomo)*/ } fun dhis2ExternalTracker( diff --git a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt index 05d0d830ef..8e2b7019bb 100644 --- a/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/login/LoginViewModelTest.kt @@ -470,7 +470,7 @@ class LoginViewModelTest { testingDispatcher.scheduler.advanceUntilIdle() verify(view).setUrl("serverUrl") verify(view).setUser("userName") - verify(view).onDbImportFinished() + verify(view).onDbImportFinished(true) } private fun mockSystemInfo(isUserLoggedIn: Boolean = true) { diff --git a/commons/src/main/java/org/dhis2/commons/data/FileHandler.kt b/commons/src/main/java/org/dhis2/commons/data/FileHandler.kt index ee57921290..c52eda9047 100644 --- a/commons/src/main/java/org/dhis2/commons/data/FileHandler.kt +++ b/commons/src/main/java/org/dhis2/commons/data/FileHandler.kt @@ -59,5 +59,7 @@ class FileHandler { Environment.DIRECTORY_DOWNLOADS, ), ) + }.also { + if (it.exists()) it.delete() } } From 6b6d127c3097c2957c5769be347657d3cce56d69 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 4 Mar 2024 13:50:28 +0100 Subject: [PATCH 17/19] remove strict policy Signed-off-by: Pablo --- app/src/main/java/org/dhis2/App.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/src/main/java/org/dhis2/App.java b/app/src/main/java/org/dhis2/App.java index e1a443f550..a1efb3fc2f 100644 --- a/app/src/main/java/org/dhis2/App.java +++ b/app/src/main/java/org/dhis2/App.java @@ -109,13 +109,6 @@ public class App extends MultiDexApplication implements Components, LifecycleObs public void onCreate() { super.onCreate(); - StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() - .detectAll() - .penaltyLog() - .penaltyDeath() - .build() - ); - ProcessLifecycleOwner.get().getLifecycle().addObserver(this); appInspector = new AppInspector(this).init(); From 04a8dadc29d6d49d7d4c41d06ad20000bdffef5d Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 4 Mar 2024 15:32:58 +0100 Subject: [PATCH 18/19] remove comments Signed-off-by: Pablo --- .../org/dhis2/utils/analytics/matomo/TrackerController.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/dhis2/utils/analytics/matomo/TrackerController.kt b/app/src/main/java/org/dhis2/utils/analytics/matomo/TrackerController.kt index 063725cc3c..a279316cd8 100644 --- a/app/src/main/java/org/dhis2/utils/analytics/matomo/TrackerController.kt +++ b/app/src/main/java/org/dhis2/utils/analytics/matomo/TrackerController.kt @@ -10,9 +10,8 @@ const val DEFAULT_EXTERNAL_TRACKER_NAME = "secondaryTracker" class TrackerController { companion object { fun dhis2InternalTracker(matomo: Matomo): Tracker? { - return null - /*return TrackerBuilder.createDefault(BuildConfig.MATOMO_URL, BuildConfig.MATOMO_ID) - .build(matomo)*/ + return TrackerBuilder.createDefault(BuildConfig.MATOMO_URL, BuildConfig.MATOMO_ID) + .build(matomo) } fun dhis2ExternalTracker( From 28e4283ba158e08062ebbe9d3948ddf0f3877f86 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 7 Mar 2024 16:59:32 +0100 Subject: [PATCH 19/19] update sdk Signed-off-by: Pablo --- .../login/SyncIsPerformedInteractor.kt | 7 ++- .../login/SyncIsPerformedInteractorTest.kt | 51 +++++++++++++++++++ gradle/libs.versions.toml | 2 +- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt b/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt index 0b6cf42286..0bacf2074c 100644 --- a/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt +++ b/app/src/main/java/org/dhis2/usescases/login/SyncIsPerformedInteractor.kt @@ -14,11 +14,10 @@ class SyncIsPerformedInteractor(private val userManager: UserManager?) { val serverUrl = userManager.d2.systemInfoModule().systemInfo().blockingGet()?.contextPath() val username = userManager.d2.userModule().user().blockingGet()?.username() - val dataBaseIsImport = userManager.d2.userModule().accountManager() - .getAccounts() - .any { + val dataBaseIsImport = userManager.d2.userModule().accountManager().getCurrentAccount() + ?.let { it.serverUrl() == serverUrl && it.username() == username && it.importDB() != null - } + } ?: false return when { dataBaseIsImport -> true diff --git a/app/src/test/java/org/dhis2/usescases/login/SyncIsPerformedInteractorTest.kt b/app/src/test/java/org/dhis2/usescases/login/SyncIsPerformedInteractorTest.kt index edd531b1d2..7d19119ceb 100644 --- a/app/src/test/java/org/dhis2/usescases/login/SyncIsPerformedInteractorTest.kt +++ b/app/src/test/java/org/dhis2/usescases/login/SyncIsPerformedInteractorTest.kt @@ -2,6 +2,9 @@ package org.dhis2.usescases.login import org.dhis2.data.server.UserManager import org.dhis2.usescases.sync.WAS_INITIAL_SYNC_DONE +import org.hisp.dhis.android.core.configuration.internal.DatabaseAccount +import org.hisp.dhis.android.core.systeminfo.SystemInfo +import org.hisp.dhis.android.core.user.User import org.junit.Before import org.junit.Test import org.mockito.Mockito @@ -30,9 +33,57 @@ class SyncIsPerformedInteractorTest { userManager.d2.dataStoreModule().localDataStore().value(WAS_INITIAL_SYNC_DONE) .blockingExists(), ) doReturn false + whenever( + userManager.d2.userModule().accountManager().getCurrentAccount(), + )doReturn null val result = interactor.execute() assert(!result) } + + @Test + fun `Should check if import exists on datastore module`() { + val serverUrl = "https://play.dhis2.org/40" + val userName = "pepe" + + whenever(userManager.d2.dataStoreModule().localDataStore()) doReturn mock() + whenever( + userManager.d2.dataStoreModule().localDataStore().value(WAS_INITIAL_SYNC_DONE), + ) doReturn mock() + whenever( + userManager.d2.dataStoreModule().localDataStore().value(WAS_INITIAL_SYNC_DONE) + .blockingExists(), + ) doReturn false + + val mockedSystemInfo: SystemInfo = mock { + on { contextPath() } doReturn serverUrl + } + + val mockedUser: User = mock { + on { username() } doReturn userName + } + + whenever( + userManager.d2.userModule().user().blockingGet(), + )doReturn mockedUser + + whenever( + userManager.d2.systemInfoModule().systemInfo().blockingGet(), + )doReturn mockedSystemInfo + + val mockedAccount: DatabaseAccount = mock() { + on { serverUrl() } doReturn serverUrl + on { username() } doReturn userName + on { importDB() } doReturn mock() + } + + whenever( + userManager.d2.userModule().accountManager().getCurrentAccount(), + )doReturn mockedAccount + + val result = interactor.execute() + + assert(result) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7545cd2913..66b3be4dfb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' designSystem = "0.2-20240222.160714-31" -dhis2sdk = "1.10.0-20240219.122222-17" +dhis2sdk = "1.10.0-20240307.130705-33" ruleEngine = "3.0.0-20240119.134348-12" expressionParser = "1.1.0-20240219.115041-14" appcompat = "1.6.1"