Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4bfc066
MS-939 Data layer preparation: data flow watchers
alex-vt Jul 24, 2025
bbe33d2
MS-939 Data layer preparation: EventSyncManager to allow default/cach…
alex-vt Jul 24, 2025
4689cda
MS-939 Data layer preparation: EventSyncMasterWorker to allow disabli…
alex-vt Jul 24, 2025
12c9f93
MS-939 Data layer preparation: sync progress callbacks needed for new…
alex-vt Jul 24, 2025
ced2db7
MS-939 Data layer preparation: project configuration extension functi…
alex-vt Jul 24, 2025
bef19b0
MS-939 Configurable container for XML layouts that will contain the n…
alex-vt Jul 24, 2025
3964cfd
MS-939 Module list item view update for the new UI/UX
alex-vt Jul 24, 2025
2bc38f0
MS-939 Kotlin Flow extension functions what will be used in the ViewM…
alex-vt Jul 24, 2025
6707857
MS-939 Unified model for the UI state of the upcoming new sync UI/UX
alex-vt Jul 24, 2025
094c05b
MS-939 New unified sync UI/UX in settings, dashboard and logout screen
alex-vt Jul 24, 2025
8ebb4ee
MS-939 Cleanup of now unused old sync UI
alex-vt Jul 24, 2025
3eb98d8
MS-939 Tests for ViewModel of the new unified sync UI/UX
alex-vt Jul 24, 2025
c39fe54
MS-939 Test name correction for EventSyncManager new UI/UX related ch…
alex-vt Jul 24, 2025
dbabaeb
MS-939 EventSyncMasterWorkerTest completed
alex-vt Jul 30, 2025
50e387e
MS-939 Logout/MainSyncFragmentTest completed
alex-vt Jul 30, 2025
76578d1
Merge branch 'main' into feature/sync-ui-ux-update
alex-vt Jul 30, 2025
14797da
MS-939 Criteria fix for isModuleSelectionAvailable
alex-vt Aug 11, 2025
5753b6b
MS-939 Stream observer function naming unified to observe
alex-vt Aug 11, 2025
c10299e
MS-939 countEventsToDownload cache timestamping fix
alex-vt Aug 11, 2025
acecdd3
MS-939 countEventsToDownload swapped tests fix
alex-vt Aug 11, 2025
de97425
MS-939 Layout: panel gap reduced; code cleanup
alex-vt Aug 11, 2025
35f0425
MS-939 Module selection requirement dropped for pre-logout sync info
alex-vt Aug 11, 2025
222a8d4
MS-939 Sync info fragment parts visibility control code cleanup
alex-vt Aug 11, 2025
2dd3ac8
MS-939 Module selection requirement dropped for pre-logout sync info …
alex-vt Aug 11, 2025
1132682
MS-939 Sync info placeholder code cleanup
alex-vt Aug 11, 2025
f7ce046
MS-939 SyncOrchestratorImpl associateWithIfSyncing explained in more …
alex-vt Aug 11, 2025
e2e470d
MS-939 FragmentContainerView for sync info named SyncFragmentContaine…
alex-vt Aug 11, 2025
2f5464a
MS-939 Logout even made consumable after onResume
alex-vt Aug 11, 2025
b4c4b6e
MS-939 Timer extracted for TimeHelper
alex-vt Aug 11, 2025
1d6c1a3
MS-939 Sync info observer calculation use case extracted from the vie…
alex-vt Aug 12, 2025
470f54a
MS-939 Image sync button coloring moved to XML
alex-vt Aug 12, 2025
2ed78a2
MS-939 Existing timestamp formatter reused
alex-vt Aug 12, 2025
f6f1793
MS-939 Image sync timestamp in millis instead of seconds
alex-vt Aug 12, 2025
31610f9
MS-939 Down-sync event counter caching moved to the use case
alex-vt Aug 12, 2025
8f26844
MS-939 View pulse animation helper extracted for general use
alex-vt Aug 12, 2025
95ab6b1
Merge branch 'main' into feature/sync-ui-ux-update
alex-vt Aug 12, 2025
57beaac
MS-939 Lint cleanup
alex-vt Aug 12, 2025
fbad0bd
MS-939 Test fix
alex-vt Aug 12, 2025
8a43197
Merge branch 'main' into feature/sync-ui-ux-update
alex-vt Aug 24, 2025
5ae3531
MS-939 CommCare changes integration step 1: post-merge cleanup
alex-vt Aug 25, 2025
7f8ac19
MS-939 CommCare changes integration step 2: the missing functionality…
alex-vt Aug 25, 2025
4c932fa
MS-939 CommCare changes integration step 2: CommCare permission check…
alex-vt Aug 26, 2025
bd779d6
MS-939 Image sync timestamp fix
alex-vt Aug 26, 2025
6943433
Merge branch 'main' into feature/sync-ui-ux-update
alex-vt Aug 26, 2025
6081d28
MS-939 Cleanup
alex-vt Aug 26, 2025
a76e2f3
MS-939 Sync button still enabled when CommCare permission is missing …
alex-vt Aug 26, 2025
47803ae
MS-939 EventSyncVisibleSection explained in more detail as EventSyncV…
alex-vt Aug 26, 2025
a8d254b
MS-939 Sync button visibility logic update
alex-vt Aug 27, 2025
ae570e4
MS-939 Sync button visibility logic update tests cleanup
alex-vt Aug 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,49 @@ package com.simprints.feature.dashboard.logout

import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import com.simprints.core.livedata.LiveDataEventWithContent
import com.simprints.feature.dashboard.logout.usecase.LogoutUseCase
import com.simprints.infra.authstore.AuthStore
import com.simprints.infra.config.store.models.SettingsPasswordConfig
import com.simprints.infra.config.sync.ConfigManager
import com.simprints.infra.eventsync.EventSyncManager
import com.simprints.infra.sync.SyncOrchestrator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import javax.inject.Inject

@HiltViewModel
internal class LogoutSyncViewModel @Inject constructor(
private val configManager: ConfigManager,
eventSyncManager: EventSyncManager,
syncOrchestrator: SyncOrchestrator,
authStore: AuthStore,
private val logoutUseCase: LogoutUseCase,
) : ViewModel() {
val logoutEventLiveData: LiveData<Unit> =
authStore
.observeSignedInProjectId()
.filter { projectId ->
projectId.isEmpty()
}.distinctUntilChanged()
.map { /* Unit on every "true" */ }
.asLiveData()

val isLogoutWithoutSyncVisibleLiveData: LiveData<Boolean> = combine(
eventSyncManager.getLastSyncState(useDefaultValue = true).asFlow(),
syncOrchestrator.observeImageSyncStatus(),
) { eventSyncState, imageSyncStatus ->
!eventSyncState.isSyncCompleted() || imageSyncStatus.isSyncing
}.debounce(timeoutMillis = ANTI_JITTER_DELAY_MILLIS).asLiveData()

val settingsLocked: LiveData<LiveDataEventWithContent<SettingsPasswordConfig>>
get() = liveData(context = viewModelScope.coroutineContext) {
emit(LiveDataEventWithContent(configManager.getProjectConfiguration().general.settingsPassword))
Expand All @@ -24,4 +53,8 @@ internal class LogoutSyncViewModel @Inject constructor(
fun logout() {
logoutUseCase()
}

private companion object {
private const val ANTI_JITTER_DELAY_MILLIS = 1000L
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
package com.simprints.feature.dashboard.logout.sync

import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.view.View
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import com.simprints.core.livedata.LiveDataEventWithContentObserver
import com.simprints.feature.dashboard.R
import com.simprints.feature.dashboard.databinding.FragmentLogoutSyncBinding
import com.simprints.feature.dashboard.logout.LogoutSyncViewModel
import com.simprints.feature.dashboard.main.sync.SyncViewModel
import com.simprints.feature.dashboard.views.SyncCardState
import com.simprints.feature.login.LoginContract
import com.simprints.feature.login.LoginResult
import com.simprints.infra.uibase.navigation.handleResult
import com.simprints.infra.uibase.navigation.navigateSafely
import com.simprints.infra.uibase.viewbinding.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

@AndroidEntryPoint
class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) {
private val logoutSyncViewModel by viewModels<LogoutSyncViewModel>()
private val syncViewModel by viewModels<SyncViewModel>()
private val viewModel by viewModels<LogoutSyncViewModel>()
private val binding by viewBinding(FragmentLogoutSyncBinding::bind)

override fun onViewCreated(
Expand All @@ -35,25 +28,9 @@ class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) {
super.onViewCreated(view, savedInstanceState)
initViews()
observeLiveData()

findNavController().handleResult<LoginResult>(
viewLifecycleOwner,
R.id.logOutSyncFragment,
LoginContract.DESTINATION,
) { result -> syncViewModel.handleLoginResult(result) }
}

private fun initViews() = with(binding) {
logoutSyncCard.onSyncButtonClick = { syncViewModel.sync() }
logoutSyncCard.onOfflineButtonClick =
{ startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS)) }
logoutSyncCard.onSelectNoModulesButtonClick = {
findNavController().navigateSafely(
this@LogoutSyncFragment,
LogoutSyncFragmentDirections.actionLogoutSyncFragmentToModuleSelectionFragment(),
)
}
logoutSyncCard.onLoginButtonClick = { syncViewModel.login() }
logoutSyncToolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
Expand All @@ -63,38 +40,22 @@ class LogoutSyncFragment : Fragment(R.layout.fragment_logout_sync) {
LogoutSyncFragmentDirections.actionLogoutSyncFragmentToLogoutSyncDeclineFragment(),
)
}
logoutButton.setOnClickListener {
logoutSyncViewModel.logout()
findNavController().navigateSafely(
this@LogoutSyncFragment,
LogoutSyncFragmentDirections.actionLogoutSyncFragmentToRequestLoginFragment(),
)
}
}

private fun observeLiveData() = with(binding) {
syncViewModel.syncCardLiveData.observe(viewLifecycleOwner) { state ->
val isLogoutButtonVisible = isLogoutButtonVisible(state)
logoutSyncCard.render(state)
logoutButton.isVisible = isLogoutButtonVisible
logoutWithoutSyncButton.isVisible = isLogoutButtonVisible.not()
logoutSyncInfo.isInvisible = isLogoutButtonVisible
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.isLogoutWithoutSyncVisibleLiveData.observe(viewLifecycleOwner) { isLogoutWithoutSyncVisible ->
logoutSyncInfo.visibility = if (isLogoutWithoutSyncVisible) View.VISIBLE else View.INVISIBLE
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I would recommend using view.isVisible = true/false and view.isInvisible = true/false extensions to keep the code a bit cleaner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The views have 3 states of visibility, and isVisible and isInvisible toggle pairs of those is a less readable way than specifying the needed visibility directly. Wherever certain views need to be hidden as INVISIBLE and the other ones as GONE by design, I think it looks more clear than a mix of look-alike (!)is(In)Visibles.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume there is a reason to use "invisible" over the default "gone", but IMO the second option is still much more readable than the inline if with constants, especially when it is repeated many times over and over.

logoutSyncInfo.visibility = if (isLogoutWithoutSyncVisible) View.VISIBLE else View.INVISIBLE

vs 

logoutSyncInfo.isInvisible = !isLogoutWithoutSyncVisible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but most views here are GONE rather than INVISIBLE. Luckily isGone is available too, i'll use that then.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the regular case of visible/gone isVisible function is prefereble, IMO. Since pretty much all flags are usually "somethingSomethingVisible". "invisible" is an edge-case that warrants the explicit usage.

logoutWithoutSyncButton.visibility = if (isLogoutWithoutSyncVisible) View.VISIBLE else View.INVISIBLE
}
}
}
viewModel.logoutEventLiveData.observe(viewLifecycleOwner) {
findNavController().navigateSafely(
this@LogoutSyncFragment,
R.id.action_logoutSyncFragment_to_requestLoginFragment,
)
}
syncViewModel.loginRequestedEventLiveData.observe(
viewLifecycleOwner,
LiveDataEventWithContentObserver { loginArgs ->
findNavController().navigateSafely(
this@LogoutSyncFragment,
R.id.action_logOutSyncFragment_to_login,
loginArgs,
)
},
)
}

/**
* Helper function that calculates whether the 'proceed to log out' button should be visible.
* The button should be visible only when synchronization is complete
*/
private fun isLogoutButtonVisible(state: SyncCardState) = state is SyncCardState.SyncComplete
}

This file was deleted.

Loading