diff --git a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt index 15c9d74c49..8176bc9d96 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainActivity.kt @@ -9,6 +9,8 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings +import android.transition.ChangeBounds +import android.transition.TransitionManager import android.view.View import android.webkit.MimeTypeMap import android.widget.TextView @@ -21,6 +23,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.app.NotificationCompat import androidx.databinding.DataBindingUtil import androidx.drawerlayout.widget.DrawerLayout @@ -35,6 +38,11 @@ import org.dhis2.bindings.app import org.dhis2.bindings.hasPermissions import org.dhis2.commons.animations.hide import org.dhis2.commons.animations.show +import org.dhis2.commons.date.DateUtils +import org.dhis2.commons.filters.FilterItem +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.filters.FiltersAdapter +import org.dhis2.commons.orgunitselector.OUTreeFragment import org.dhis2.commons.sync.OnDismissListener import org.dhis2.commons.sync.SyncContext import org.dhis2.databinding.ActivityMainBinding @@ -68,11 +76,15 @@ class MainActivity : DrawerLayout.DrawerListener { private lateinit var binding: ActivityMainBinding + lateinit var mainComponent: MainComponent @Inject lateinit var presenter: MainPresenter + @Inject + lateinit var newAdapter: FiltersAdapter + @Inject lateinit var pageConfigurator: NavigationPageConfigurator @@ -83,6 +95,8 @@ class MainActivity : // no-op } + private var backDropActive = false + private val requestWritePermissions = registerForActivityResult( ActivityResultContracts.RequestPermission(), @@ -136,9 +150,14 @@ class MainActivity : mainNavigator = MainNavigator( dispatcherProvider = presenter.dispatcherProvider, supportFragmentManager, - { /*no-op*/ }, - ) { titleRes, _, showBottomNavigation -> + { + if (backDropActive) { + showHideFilter() + } + }, + ) { titleRes, showFilterButton, showBottomNavigation -> setTitle(getString(titleRes)) + setFilterButtonVisibility(showFilterButton) setBottomNavigationVisibility(showBottomNavigation) } super.onCreate(savedInstanceState) @@ -157,6 +176,8 @@ class MainActivity : binding.mainDrawerLayout.addDrawerListener(this) + binding.filterRecycler.adapter = newAdapter + setUpNavigationBar() setUpDevelopmentMode() @@ -218,6 +239,8 @@ class MainActivity : super.onResume() if (sessionManagerServiceImpl.isUserLoggedIn()) { presenter.init() + presenter.initFilters() + binding.totalFilters = FilterManager.getInstance().totalFilters } } @@ -289,10 +312,12 @@ class MainActivity : when (it.running) { true -> { binding.syncActionButton.visibility = View.GONE + setFilterButtonVisibility(false) setBottomNavigationVisibility(false) } false -> { + setFilterButtonVisibility(true) binding.syncActionButton.visibility = View.VISIBLE setBottomNavigationVisibility(true) presenter.onDataSuccess() @@ -329,6 +354,35 @@ class MainActivity : } } + override fun showHideFilter() { + val transition = ChangeBounds() + transition.duration = 200 + TransitionManager.beginDelayedTransition(binding.backdropLayout, transition) + backDropActive = !backDropActive + val initSet = ConstraintSet() + initSet.clone(binding.backdropLayout) + if (backDropActive) { + initSet.connect( + R.id.fragment_container, + ConstraintSet.TOP, + R.id.filterRecycler, + ConstraintSet.BOTTOM, + 50, + ) + binding.navigationBar.hide() + } else { + initSet.connect( + R.id.fragment_container, + ConstraintSet.TOP, + R.id.toolbar, + ConstraintSet.BOTTOM, + 0, + ) + binding.navigationBar.show() + } + initSet.applyTo(binding.backdropLayout) + } + override fun showGranularSync() { SyncStatusDialog.Builder() .withContext(this) @@ -353,6 +407,37 @@ class MainActivity : .show("ALL_SYNC") } + override fun updateFilters(totalFilters: Int) { + binding.totalFilters = totalFilters + } + + override fun showPeriodRequest(periodRequest: FilterManager.PeriodRequest) { + if (periodRequest == FilterManager.PeriodRequest.FROM_TO) { + DateUtils.getInstance() + .fromCalendarSelector(this) { FilterManager.getInstance().addPeriod(it) } + } else { + DateUtils.getInstance() + .showPeriodDialog( + this, + { datePeriods -> FilterManager.getInstance().addPeriod(datePeriods) }, + true, + { FilterManager.getInstance().addPeriod(null) }, + ) + } + } + + override fun openOrgUnitTreeSelector() { + OUTreeFragment.Builder() + .withPreselectedOrgUnits( + FilterManager.getInstance().orgUnitFilters.map { it.uid() }.toMutableList(), + ) + .onSelection { selectedOrgUnits -> + presenter.setOrgUnitFilters(selectedOrgUnits) + } + .build() + .show(supportFragmentManager, "OUTreeFragment") + } + override fun goToLogin(accountsCount: Int, isDeletion: Boolean) { startActivity( LoginActivity::class.java, @@ -373,6 +458,19 @@ class MainActivity : binding.executePendingBindings() } + private fun setFilterButtonVisibility(showFilterButton: Boolean) { + binding.filterActionButton.visibility = if (showFilterButton && presenter.hasFilters()) { + View.VISIBLE + } else { + View.GONE + } + binding.syncActionButton.visibility = if (showFilterButton) { + View.VISIBLE + } else { + View.GONE + } + } + override fun openDrawer(gravity: Int) { if (!binding.mainDrawerLayout.isDrawerOpen(gravity)) { binding.mainDrawerLayout.openDrawer(gravity) @@ -381,6 +479,14 @@ class MainActivity : } } + override fun setFilters(filters: List) { + newAdapter.submitList(filters) + } + + override fun hideFilters() { + binding.filterActionButton.visibility = View.GONE + } + override fun onLockClick() { if (!presenter.isPinStored()) { binding.mainDrawerLayout.closeDrawers() @@ -425,9 +531,11 @@ class MainActivity : } override fun onDrawerStateChanged(newState: Int) { + // no op } override fun onDrawerSlide(drawerView: View, slideOffset: Float) { + // no op } override fun onDrawerClosed(drawerView: View) { @@ -435,6 +543,7 @@ class MainActivity : } override fun onDrawerOpened(drawerView: View) { + // no op } private fun initCurrentScreen() { diff --git a/app/src/main/java/org/dhis2/usescases/main/MainModule.kt b/app/src/main/java/org/dhis2/usescases/main/MainModule.kt index 62014e272b..e8bf1c7a24 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainModule.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainModule.kt @@ -6,6 +6,8 @@ import dhis2.org.analytics.charts.Charts import org.dhis2.commons.di.dagger.PerActivity import org.dhis2.commons.featureconfig.data.FeatureConfigRepository import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.filters.FiltersAdapter +import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.prefs.PreferenceProvider import org.dhis2.commons.resources.ColorUtils @@ -32,6 +34,7 @@ class MainModule(val view: MainView, private val forceToNotSynced: Boolean) { preferences: PreferenceProvider, workManagerController: WorkManagerController, filterManager: FilterManager, + filterRepository: FilterRepository, matomoAnalyticsController: MatomoAnalyticsController, userManager: UserManager, deleteUserData: DeleteUserData, @@ -47,6 +50,7 @@ class MainModule(val view: MainView, private val forceToNotSynced: Boolean) { preferences, workManagerController, filterManager, + filterRepository, matomoAnalyticsController, userManager, deleteUserData, @@ -74,6 +78,12 @@ class MainModule(val view: MainView, private val forceToNotSynced: Boolean) { return HomeRepositoryImpl(d2, charts, featureConfigRepositoryImpl) } + @Provides + @PerActivity + fun provideNewFiltersAdapter(): FiltersAdapter { + return FiltersAdapter() + } + @Provides @PerActivity fun providePageConfigurator( diff --git a/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt b/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt index bc88aa6a72..a4147ee408 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainPresenter.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import androidx.work.ExistingWorkPolicy import io.reactivex.Completable +import io.reactivex.Flowable import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -15,6 +16,7 @@ import kotlinx.coroutines.launch import org.dhis2.BuildConfig import org.dhis2.commons.Constants import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.filters.data.FilterRepository import org.dhis2.commons.matomo.Actions.Companion.BLOCK_SESSION_PIN import org.dhis2.commons.matomo.Actions.Companion.OPEN_ANALYTICS import org.dhis2.commons.matomo.Actions.Companion.QR_SCANNER @@ -40,6 +42,7 @@ import org.dhis2.usescases.login.SyncIsPerformedInteractor import org.dhis2.usescases.settings.DeleteUserData import org.dhis2.usescases.sync.WAS_INITIAL_SYNC_DONE import org.dhis2.utils.TRUE +import org.hisp.dhis.android.core.organisationunit.OrganisationUnit import org.hisp.dhis.android.core.systeminfo.SystemInfo import org.hisp.dhis.android.core.user.User import timber.log.Timber @@ -57,6 +60,7 @@ class MainPresenter( private val preferences: PreferenceProvider, private val workManagerController: WorkManagerController, private val filterManager: FilterManager, + private val filterRepository: FilterRepository, private val matomoAnalyticsController: MatomoAnalyticsController, private val userManager: UserManager, private val deleteUserData: DeleteUserData, @@ -77,7 +81,6 @@ class MainPresenter( val downloadingVersion = MutableLiveData(false) fun init() { - filterManager.clearAllFilters() preferences.removeValue(Preference.CURRENT_ORG_UNIT) disposable.add( repository.user() @@ -117,6 +120,54 @@ class MainPresenter( trackDhis2Server() } + fun initFilters() { + disposable.add( + Flowable.just(filterRepository.homeFilters()) + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { filters -> + if (filters.isEmpty()) { + view.hideFilters() + } else { + view.setFilters(filters) + } + }, + { Timber.e(it) }, + ), + ) + + disposable.add( + filterManager.asFlowable() + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { filterManager -> view.updateFilters(filterManager.totalFilters) }, + { Timber.e(it) }, + ), + ) + + disposable.add( + filterManager.periodRequest + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { periodRequest -> view.showPeriodRequest(periodRequest.first) }, + { Timber.e(it) }, + ), + ) + + disposable.add( + filterManager.ouTreeFlowable() + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { view.openOrgUnitTreeSelector() }, + { Timber.e(it) }, + ), + ) + } + fun trackDhis2Server() { disposable.add( repository.getServerVersion() @@ -145,6 +196,10 @@ class MainPresenter( } } + fun setOrgUnitFilters(selectedOrgUnits: List) { + filterManager.addOrgUnits(selectedOrgUnits) + } + private fun getUserUid(): String { return try { userManager.d2.userModule().user().blockingGet()?.uid() ?: "" @@ -201,6 +256,10 @@ class MainPresenter( view.back() } + fun showFilter() { + view.showHideFilter() + } + fun onDetach() { disposable.clear() } @@ -219,6 +278,7 @@ class MainPresenter( fun onNavigateBackToHome() { view.goToHome() + initFilters() } fun onClickSyncManager() { @@ -307,4 +367,8 @@ class MainPresenter( fun getSingleItemData(): HomeItemData? { return repository.singleHomeItemData() } + + fun hasFilters(): Boolean { + return filterRepository.homeFilters().isNotEmpty() + } } diff --git a/app/src/main/java/org/dhis2/usescases/main/MainView.kt b/app/src/main/java/org/dhis2/usescases/main/MainView.kt index 1d590f3c84..7f518d138a 100644 --- a/app/src/main/java/org/dhis2/usescases/main/MainView.kt +++ b/app/src/main/java/org/dhis2/usescases/main/MainView.kt @@ -26,6 +26,8 @@ package org.dhis2.usescases.main import androidx.annotation.UiThread +import org.dhis2.commons.filters.FilterItem +import org.dhis2.commons.filters.FilterManager import org.dhis2.usescases.general.AbstractActivityContracts import java.io.File @@ -36,12 +38,24 @@ interface MainView : AbstractActivityContracts.View { fun openDrawer(gravity: Int) + fun showHideFilter() + fun onLockClick() fun changeFragment(id: Int) + fun updateFilters(totalFilters: Int) + + fun showPeriodRequest(periodRequest: FilterManager.PeriodRequest) + + fun openOrgUnitTreeSelector() + fun goToHome() + fun setFilters(filters: List) + + fun hideFilters() + fun showGranularSync() fun goToLogin(accountsCount: Int, isDeletion: Boolean) diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt index 8f22319228..e4bb21670b 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramModule.kt @@ -4,6 +4,7 @@ import dagger.Module import dagger.Provides import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.featureconfig.data.FeatureConfigRepository +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.filters.data.FilterPresenter import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.resources.ColorUtils @@ -12,6 +13,7 @@ import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.dhislogic.DhisProgramUtils +import org.dhis2.data.dhislogic.DhisTrackedEntityInstanceUtils import org.dhis2.data.service.SyncStatusController import org.hisp.dhis.android.core.D2 @@ -25,7 +27,9 @@ class ProgramModule(private val view: ProgramView) { dispatcherProvider: DispatcherProvider, featureConfigRepository: FeatureConfigRepository, matomoAnalyticsController: MatomoAnalyticsController, + filterManager: FilterManager, syncStatusController: SyncStatusController, + schedulerProvider: SchedulerProvider, ): ProgramViewModelFactory { return ProgramViewModelFactory( view, @@ -33,7 +37,9 @@ class ProgramModule(private val view: ProgramView) { featureConfigRepository, dispatcherProvider, matomoAnalyticsController, + filterManager, syncStatusController, + schedulerProvider, ) } @@ -43,6 +49,7 @@ class ProgramModule(private val view: ProgramView) { d2: D2, filterPresenter: FilterPresenter, dhisProgramUtils: DhisProgramUtils, + dhisTrackedEntityInstanceUtils: DhisTrackedEntityInstanceUtils, schedulerProvider: SchedulerProvider, colorUtils: ColorUtils, metadataIconProvider: MetadataIconProvider, @@ -51,6 +58,7 @@ class ProgramModule(private val view: ProgramView) { d2, filterPresenter, dhisProgramUtils, + dhisTrackedEntityInstanceUtils, ResourceManager(view.context, colorUtils), metadataIconProvider, schedulerProvider, diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt index e19c0ca348..a9fdacf122 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramRepositoryImpl.kt @@ -9,6 +9,7 @@ import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.data.dhislogic.DhisProgramUtils +import org.dhis2.data.dhislogic.DhisTrackedEntityInstanceUtils import org.dhis2.data.service.SyncStatusData import org.hisp.dhis.android.core.D2 import org.hisp.dhis.android.core.common.State @@ -21,6 +22,7 @@ internal class ProgramRepositoryImpl( private val d2: D2, private val filterPresenter: FilterPresenter, private val dhisProgramUtils: DhisProgramUtils, + private val dhisTeiUtils: DhisTrackedEntityInstanceUtils, private val resourceManager: ResourceManager, private val metadataIconProvider: MetadataIconProvider, private val schedulerProvider: SchedulerProvider, @@ -67,8 +69,13 @@ internal class ProgramRepositoryImpl( programViewModelMapper.map( dataSet, it, - it.dataSetInstanceCount(), + if (filterPresenter.isAssignedToMeApplied()) { + 0 + } else { + it.dataSetInstanceCount() + }, resourceManager.defaultDataSetLabel(), + filterPresenter.areFiltersActive(), metadataIconProvider(dataSet.style(), SurfaceColor.Primary), ) } @@ -107,6 +114,8 @@ internal class ProgramRepositoryImpl( 0, recordLabel, state, + hasOverdue = false, + filtersAreActive = false, metadataIconData = metadataIconProvider(program.style(), SurfaceColor.Primary), ).copy( stockConfig = if (d2.isStockProgram(program.uid())) { @@ -121,15 +130,19 @@ internal class ProgramRepositoryImpl( private fun List.applyFilters(): List { return map { programModel -> val program = d2.programModule().programs().uid(programModel.uid).blockingGet() - val count = + val (count, hasOverdue) = if (program?.programType() == WITHOUT_REGISTRATION) { getSingleEventCount(program) } else if (program?.programType() == WITH_REGISTRATION) { getTrackerTeiCount(program) } else { - 0 + Pair(0, false) } - programModel.copy(count = count) + programModel.copy( + count = count, + hasOverdueEvent = hasOverdue, + filtersAreActive = filterPresenter.areFiltersActive(), + ) } } @@ -156,13 +169,20 @@ internal class ProgramRepositoryImpl( } } - private fun getSingleEventCount(program: Program): Int { - return filterPresenter.filteredEventProgram(program) - .blockingGet().filter { event -> event.syncState() != State.RELATIONSHIP }.size + private fun getSingleEventCount(program: Program): Pair { + return Pair( + filterPresenter.filteredEventProgram(program) + .blockingGet().filter { event -> event.syncState() != State.RELATIONSHIP }.size, + false, + ) } - private fun getTrackerTeiCount(program: Program): Int { - return filterPresenter.filteredTrackerProgram(program) - .offlineFirst().blockingCount() + private fun getTrackerTeiCount(program: Program): Pair { + val teiIds = filterPresenter.filteredTrackerProgram(program) + .offlineFirst().blockingGetUids() + val mCount = teiIds.size + val mOverdue = dhisTeiUtils.hasOverdueInProgram(teiIds, program) + + return Pair(mCount, mOverdue) } } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt index bb2ac6334d..0ce9a243c6 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUi.kt @@ -682,6 +682,8 @@ private fun testingProgramModel() = ProgramUiModel( onlyEnrollOnce = false, accessDataWrite = true, state = State.SYNCED, + hasOverdueEvent = false, + filtersAreActive = false, downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = Date(), diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt index c1440bcf30..ff19b3a8f1 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramUiModel.kt @@ -17,6 +17,8 @@ data class ProgramUiModel( val onlyEnrollOnce: Boolean, val accessDataWrite: Boolean, val state: State, + val hasOverdueEvent: Boolean, + val filtersAreActive: Boolean, val downloadState: ProgramDownloadState, val downloadActive: Boolean = false, val stockConfig: AppConfig?, diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt index 26f010bb11..df757fc112 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModel.kt @@ -5,18 +5,22 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.reactivex.disposables.CompositeDisposable +import io.reactivex.processors.PublishProcessor import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.dhis2.commons.featureconfig.data.FeatureConfigRepository import org.dhis2.commons.featureconfig.model.Feature import org.dhis2.commons.featureconfig.model.FeatureOptions +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.matomo.Actions.Companion.SYNC_BTN import org.dhis2.commons.matomo.Categories.Companion.HOME import org.dhis2.commons.matomo.Labels.Companion.CLICK_ON import org.dhis2.commons.matomo.MatomoAnalyticsController +import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.service.SyncStatusController import timber.log.Timber +import java.util.concurrent.TimeUnit class ProgramViewModel internal constructor( private val view: ProgramView, @@ -24,17 +28,60 @@ class ProgramViewModel internal constructor( private val featureConfigRepository: FeatureConfigRepository, private val dispatchers: DispatcherProvider, private val matomoAnalyticsController: MatomoAnalyticsController, + private val filterManager: FilterManager, private val syncStatusController: SyncStatusController, + private val schedulerProvider: SchedulerProvider, ) : ViewModel() { private val _programs = MutableLiveData>() val programs: LiveData> = _programs - + private val refreshData = PublishProcessor.create() var disposable: CompositeDisposable = CompositeDisposable() fun init() { programRepository.clearCache() fetchPrograms() + initFilters() + } + + private fun initFilters() { + val applyFilter = PublishProcessor.create() + disposable.add( + applyFilter + .switchMap { + refreshData.debounce( + 500, + TimeUnit.MILLISECONDS, + schedulerProvider.io(), + ).startWith(Unit).switchMap { + programRepository.homeItems( + syncStatusController.observeDownloadProcess().value, + ) + } + } + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { programs -> + _programs.postValue(programs) + }, + { throwable -> Timber.d(throwable) }, + { Timber.tag("INIT DATA").d("LOADING ENDED") }, + ), + ) + + disposable.add( + filterManager.asFlowable() + .startWith(filterManager) + .subscribeOn(schedulerProvider.io()) + .observeOn(schedulerProvider.ui()) + .subscribe( + { + applyFilter.onNext(filterManager) + }, + { Timber.e(it) }, + ), + ) } private fun fetchPrograms() { diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt index f302ba11bf..8b074ce1f9 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelFactory.kt @@ -3,7 +3,9 @@ package org.dhis2.usescases.main.program import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import org.dhis2.commons.featureconfig.data.FeatureConfigRepository +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.matomo.MatomoAnalyticsController +import org.dhis2.commons.schedulers.SchedulerProvider import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.data.service.SyncStatusController @@ -14,7 +16,9 @@ class ProgramViewModelFactory( private val featureConfigRepository: FeatureConfigRepository, private val dispatchers: DispatcherProvider, private val matomoAnalyticsController: MatomoAnalyticsController, + private val filterManager: FilterManager, private val syncStatusController: SyncStatusController, + private val schedulerProvider: SchedulerProvider, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return ProgramViewModel( @@ -23,7 +27,9 @@ class ProgramViewModelFactory( featureConfigRepository, dispatchers, matomoAnalyticsController, + filterManager, syncStatusController, + schedulerProvider, ) as T } } diff --git a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt index a632bfdf50..dce4e8f6a5 100644 --- a/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/main/program/ProgramViewModelMapper.kt @@ -13,6 +13,8 @@ class ProgramViewModelMapper() { recordCount: Int, recordLabel: String, state: State, + hasOverdue: Boolean, + filtersAreActive: Boolean, metadataIconData: MetadataIconData, ): ProgramUiModel { return ProgramUiModel( @@ -31,6 +33,8 @@ class ProgramViewModelMapper() { onlyEnrollOnce = program.onlyEnrollOnce() == true, accessDataWrite = program.access().data().write(), state = State.valueOf(state.name), + hasOverdueEvent = hasOverdue, + filtersAreActive = filtersAreActive, downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = program.lastUpdated() ?: Date(), @@ -42,6 +46,7 @@ class ProgramViewModelMapper() { dataSetInstanceSummary: DataSetInstanceSummary, recordCount: Int, dataSetLabel: String, + filtersAreActive: Boolean, metadataIconData: MetadataIconData, ): ProgramUiModel { return ProgramUiModel( @@ -56,6 +61,8 @@ class ProgramViewModelMapper() { onlyEnrollOnce = false, accessDataWrite = dataSet.access().data().write(), state = dataSetInstanceSummary.state(), + hasOverdueEvent = false, + filtersAreActive = filtersAreActive, downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = dataSet.lastUpdated() ?: Date(), diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java index cca9c72c9b..3cc3cd948c 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListRepositoryImpl.java @@ -107,6 +107,8 @@ public Flowable> allPrograms(String trackedEntityId) { 0, "", State.SYNCED, + false, + false, metadataIconProvider.invoke(program.style()) ) ) diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt index a0411cc345..5b3d506d51 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/teiProgramList/ui/EnrollToProgram.kt @@ -113,6 +113,8 @@ private fun testingProgramModel(downloadState: ProgramDownloadState) = ProgramUi downloadState = downloadState, stockConfig = null, lastUpdated = Date(), + hasOverdueEvent = false, + filtersAreActive = false, ) const val PROGRAM_TO_ENROLL = "PROGRAM_TO_ENROLL_%s" diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index e245110545..c8565416d3 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -20,6 +20,10 @@ + + + + + + + + + + + = PublishProcessor.create() + val filterManagerFlowable = Flowable.just(filterManager).startWith(filterProcessor) + val periodRequest: FlowableProcessor> = + BehaviorProcessor.create() + whenever(filterManager.asFlowable()) doReturn filterManagerFlowable + whenever(filterManager.periodRequest) doReturn periodRequest + whenever(filterManager.ouTreeFlowable()) doReturn Flowable.just(true) + presenter.onNavigateBackToHome() verify(view).goToHome() diff --git a/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt b/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt index e04f232364..4a94d1a716 100644 --- a/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt +++ b/app/src/test/java/org/dhis2/usescases/main/program/ProgramRepositoryImplTest.kt @@ -10,6 +10,7 @@ import org.dhis2.commons.filters.data.FilterPresenter import org.dhis2.commons.resources.MetadataIconProvider import org.dhis2.commons.resources.ResourceManager import org.dhis2.data.dhislogic.DhisProgramUtils +import org.dhis2.data.dhislogic.DhisTrackedEntityInstanceUtils import org.dhis2.data.schedulers.TrampolineSchedulerProvider import org.dhis2.data.service.SyncStatusData import org.dhis2.ui.MetadataIconData @@ -46,6 +47,7 @@ class ProgramRepositoryImplTest { private val filterPresenter: FilterPresenter = Mockito.mock(FilterPresenter::class.java, Mockito.RETURNS_DEEP_STUBS) private val dhisProgramUtils: DhisProgramUtils = mock() + private val dhis2TeiUtils: DhisTrackedEntityInstanceUtils = mock() private val scheduler = TrampolineSchedulerProvider() private val resourceManager: ResourceManager = mock() private val metadataIconProvider: MetadataIconProvider = mock { @@ -60,6 +62,7 @@ class ProgramRepositoryImplTest { d2, filterPresenter, dhisProgramUtils, + dhis2TeiUtils, resourceManager, metadataIconProvider, scheduler, @@ -129,9 +132,9 @@ class ProgramRepositoryImplTest { fun `Should return list of program ProgramViewModels`() { val syncStatusData = SyncStatusData(true) initWheneverForPrograms() - val testOvserver = programRepository.programModels(syncStatusData).test() + val testObserver = programRepository.programModels(syncStatusData).test() - testOvserver + testObserver .assertNoErrors() .assertValue { it.size == mockedPrograms().size && @@ -175,6 +178,8 @@ class ProgramRepositoryImplTest { Event.builder().uid("9").syncState(State.SYNCED).build(), Event.builder().uid("10").syncState(State.RELATIONSHIP).build(), ) + whenever(dhis2TeiUtils.hasOverdueInProgram(any(), any())) doReturn false + whenever(filterPresenter.areFiltersActive()) doReturn false whenever( filterPresenter.filteredTrackerProgram(any()), ) doReturn mock() @@ -182,8 +187,8 @@ class ProgramRepositoryImplTest { filterPresenter.filteredTrackerProgram(any()).offlineFirst(), ) doReturn mock() whenever( - filterPresenter.filteredTrackerProgram(any()).offlineFirst().blockingCount(), - ) doReturn 2 + filterPresenter.filteredTrackerProgram(any()).offlineFirst().blockingGetUids(), + ) doReturn listOf("0", "1") } private fun mockedDataSetInstanceSummaries(): List { diff --git a/app/src/test/java/org/dhis2/usescases/main/program/ProgramViewModelTest.kt b/app/src/test/java/org/dhis2/usescases/main/program/ProgramViewModelTest.kt index fe4e7767af..78c88b39ba 100644 --- a/app/src/test/java/org/dhis2/usescases/main/program/ProgramViewModelTest.kt +++ b/app/src/test/java/org/dhis2/usescases/main/program/ProgramViewModelTest.kt @@ -2,6 +2,9 @@ package org.dhis2.usescases.main.program import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.reactivex.Flowable +import io.reactivex.processors.FlowableProcessor +import io.reactivex.processors.PublishProcessor +import io.reactivex.schedulers.TestScheduler import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -9,8 +12,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.setMain import org.dhis2.commons.featureconfig.data.FeatureConfigRepository +import org.dhis2.commons.filters.FilterManager import org.dhis2.commons.matomo.MatomoAnalyticsController import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.data.schedulers.TestSchedulerProvider import org.dhis2.data.service.SyncStatusController import org.dhis2.data.service.SyncStatusData import org.dhis2.ui.MetadataIconData @@ -43,6 +48,8 @@ class ProgramViewModelTest { private val view: ProgramView = mock() private val programRepository: ProgramRepository = mock() private val matomoAnalyticsController: MatomoAnalyticsController = mock() + private val filterManager: FilterManager = mock() + private val schedulerProvider: TestSchedulerProvider = TestSchedulerProvider(TestScheduler()) private val syncStatusController: SyncStatusController = mock() private val testingDispatcher = UnconfinedTestDispatcher() private val featureConfigRepository: FeatureConfigRepository = mock { @@ -70,8 +77,11 @@ class ProgramViewModelTest { programRepository, featureConfigRepository, dispatcherProvider, + matomoAnalyticsController, + filterManager, syncStatusController, + schedulerProvider, ) } @@ -79,7 +89,12 @@ class ProgramViewModelTest { fun `Should initialize program list`() { val programs = listOf(programViewModel()) val programsFlowable = Flowable.just(programs) + val filterProcessor: FlowableProcessor = PublishProcessor.create() + val syncStatusData = SyncStatusData(true) + val filterManagerFlowable = Flowable.just(filterManager).startWith(filterProcessor) + + whenever(filterManager.asFlowable()) doReturn filterManagerFlowable whenever( syncStatusController.observeDownloadProcess(), @@ -149,6 +164,8 @@ class ProgramViewModelTest { downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = Date(), + hasOverdueEvent = false, + filtersAreActive = false, ) } @@ -176,6 +193,8 @@ class ProgramViewModelTest { downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = Date(), + hasOverdueEvent = false, + filtersAreActive = false, ) } } diff --git a/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenterTest.kt b/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenterTest.kt index 872946e1cb..9b6e696f9d 100644 --- a/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenterTest.kt +++ b/app/src/test/java/org/dhis2/usescases/teiDashboard/teiProgramList/TeiProgramListPresenterTest.kt @@ -175,6 +175,8 @@ class TeiProgramListPresenterTest { downloadState = ProgramDownloadState.NONE, stockConfig = null, lastUpdated = Date(), + filtersAreActive = false, + hasOverdueEvent = false, ) }