From 17298ba6f86f86a1aa2b02f79babf86a7b28c83f Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 27 Feb 2026 11:01:52 +0530 Subject: [PATCH 1/8] Migrate logout dialog state management to HomeScreenViewModel --- .../android/ui/home/HomeScreenFragment.kt | 74 ++++++++----------- .../android/ui/home/HomeScreenViewModel.kt | 39 +++++++++- 2 files changed, 66 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index 4875defa89..9016a6ea80 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -21,7 +21,8 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.TextView -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.core.view.GravityCompat import androidx.core.view.WindowInsetsCompat import androidx.drawerlayout.widget.DrawerLayout @@ -37,7 +38,6 @@ import org.groundplatform.android.R import org.groundplatform.android.data.local.room.converter.SubmissionDeltasConverter import org.groundplatform.android.databinding.HomeScreenFragBinding import org.groundplatform.android.databinding.NavDrawerHeaderBinding -import org.groundplatform.android.model.User import org.groundplatform.android.repository.UserRepository import org.groundplatform.android.ui.common.AbstractFragment import org.groundplatform.android.ui.common.BackPressListener @@ -60,7 +60,6 @@ class HomeScreenFragment : @Inject lateinit var userRepository: UserRepository private lateinit var binding: HomeScreenFragBinding private lateinit var homeScreenViewModel: HomeScreenViewModel - private lateinit var user: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -96,9 +95,9 @@ class HomeScreenFragment : HomeScreenFragmentDirections.actionHomeScreenFragmentToSurveySelectorFragment(false) ) } - viewLifecycleOwner.lifecycleScope.launch { user = userRepository.getAuthenticatedUser() } + navHeader.findViewById(R.id.user_image).setOnClickListener { - showSignOutConfirmationDialogs() + homeScreenViewModel.showUserDetails() } updateNavHeader() // Re-open data collection screen if draft submission is present. @@ -131,6 +130,31 @@ class HomeScreenFragment : val navigationView = view.findViewById(R.id.nav_view) val menuItem = navigationView.menu.findItem(R.id.nav_log_version) menuItem.title = String.format(getString(R.string.build), BuildConfig.VERSION_NAME) + + binding.composeView.setComposableContent { + val state by homeScreenViewModel.showLogoutDialog.collectAsState() + val user by homeScreenViewModel.authenticatedUser.collectAsState() + + when (state) { + HomeScreenViewModel.LogoutDialogState.USER_DETAILS -> + user?.let { + UserDetailsDialog( + it, + { homeScreenViewModel.showSignOutConfirmation() }, + { homeScreenViewModel.dismissLogoutDialog() }, + ) + } + HomeScreenViewModel.LogoutDialogState.SIGN_OUT_CONFIRMATION -> + ConfirmationDialog( + title = R.string.sign_out_dialog_title, + description = R.string.sign_out_dialog_body, + confirmButtonText = R.string.sign_out, + onConfirmClicked = { homeScreenViewModel.signOut() }, + onDismiss = { homeScreenViewModel.dismissLogoutDialog() }, + ) + else -> {} + } + } } private fun updateNavHeader() = @@ -191,44 +215,4 @@ class HomeScreenFragment : closeDrawer() return true } - - private fun showSignOutConfirmationDialogs() { - val showUserDetailsDialog = mutableStateOf(false) - val showSignOutDialog = mutableStateOf(false) - - fun showUserDetailsDialog() { - showUserDetailsDialog.value = true - showSignOutDialog.value = false - } - - fun showSignOutDialog() { - showUserDetailsDialog.value = false - showSignOutDialog.value = true - } - - fun hideAllDialogs() { - showUserDetailsDialog.value = false - showSignOutDialog.value = false - } - - // Init state for composition - showUserDetailsDialog() - - // Note: Adding a compose view to the fragment's view dynamically causes the navigation click to - // stop working after 1st time. Revisit this once the navigation drawer is also generated using - // compose. - binding.composeView.setComposableContent { - if (showUserDetailsDialog.value) { - UserDetailsDialog(user, { showSignOutDialog() }, { hideAllDialogs() }) - } - if (showSignOutDialog.value) { - ConfirmationDialog( - title = R.string.sign_out_dialog_title, - description = R.string.sign_out_dialog_body, - confirmButtonText = R.string.sign_out, - onConfirmClicked = { homeScreenViewModel.signOut() }, - ) - } - } - } } diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt index 72d82335b3..3b9c0dd441 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt @@ -19,14 +19,17 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.groundplatform.android.data.sync.MediaUploadWorkManager import org.groundplatform.android.data.sync.MutationSyncWorkManager +import org.groundplatform.android.model.User import org.groundplatform.android.model.submission.DraftSubmission import org.groundplatform.android.repository.MutationRepository import org.groundplatform.android.repository.OfflineAreaRepository @@ -36,6 +39,7 @@ import org.groundplatform.android.repository.UserRepository import org.groundplatform.android.ui.common.AbstractViewModel import org.groundplatform.android.ui.common.SharedViewModel import timber.log.Timber +import javax.inject.Inject private const val AWAITING_PHOTO_CAPTURE_KEY = "awaiting_photo_capture" @@ -56,6 +60,12 @@ internal constructor( private val _openDrawerRequests: MutableSharedFlow = MutableSharedFlow() val openDrawerRequestsFlow: SharedFlow = _openDrawerRequests.asSharedFlow() + private val _showLogoutDialog = MutableStateFlow(LogoutDialogState.HIDDEN) + val showLogoutDialog: StateFlow = _showLogoutDialog.asStateFlow() + + private val _authenticatedUser = MutableStateFlow(null) + val authenticatedUser: StateFlow = _authenticatedUser.asStateFlow() + // TODO: Allow tile source configuration from a non-survey accessible source. // Issue URL: https://github.com/google/ground-android/issues/1730 val showOfflineAreaMenuItem: LiveData = MutableLiveData(true) @@ -73,7 +83,10 @@ internal constructor( } init { - viewModelScope.launch { kickLocalMutationSyncWorkers() } + viewModelScope.launch { + kickLocalMutationSyncWorkers() + _authenticatedUser.value = userRepository.getAuthenticatedUser() + } } /** @@ -120,6 +133,28 @@ internal constructor( suspend fun getOfflineAreas() = offlineAreaRepository.offlineAreas().first() fun signOut() { + _showLogoutDialog.value = LogoutDialogState.HIDDEN viewModelScope.launch { userRepository.signOut() } } + + fun showUserDetails() { + _showLogoutDialog.value = LogoutDialogState.USER_DETAILS + } + + fun showSignOutConfirmation() { + _showLogoutDialog.value = LogoutDialogState.SIGN_OUT_CONFIRMATION + } + + fun dismissLogoutDialog() { + _showLogoutDialog.value = LogoutDialogState.HIDDEN + } + + /** + * Represents the possible visibility states of the user profile and logout confirmation dialogs. + */ + enum class LogoutDialogState { + HIDDEN, + USER_DETAILS, + SIGN_OUT_CONFIRMATION, + } } From 1d48690f7f12182cb46c4d7b6b398cf1c2706d0f Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 27 Feb 2026 11:27:02 +0530 Subject: [PATCH 2/8] Add unit tests --- .../android/ui/home/HomeScreenFragment.kt | 2 +- .../android/ui/home/HomeScreenViewModel.kt | 18 ++-- .../ui/home/HomeScreenViewModelTest.kt | 93 +++++++++++++++++++ 3 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index 9016a6ea80..1095dfdd16 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -133,7 +133,7 @@ class HomeScreenFragment : binding.composeView.setComposableContent { val state by homeScreenViewModel.showLogoutDialog.collectAsState() - val user by homeScreenViewModel.authenticatedUser.collectAsState() + val user by homeScreenViewModel.user.collectAsState(null) when (state) { HomeScreenViewModel.LogoutDialogState.USER_DETAILS -> diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt index 3b9c0dd441..9052263d26 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt @@ -19,13 +19,17 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.groundplatform.android.data.sync.MediaUploadWorkManager import org.groundplatform.android.data.sync.MutationSyncWorkManager @@ -36,10 +40,10 @@ import org.groundplatform.android.repository.OfflineAreaRepository import org.groundplatform.android.repository.SubmissionRepository import org.groundplatform.android.repository.SurveyRepository import org.groundplatform.android.repository.UserRepository +import org.groundplatform.android.system.auth.SignInState import org.groundplatform.android.ui.common.AbstractViewModel import org.groundplatform.android.ui.common.SharedViewModel import timber.log.Timber -import javax.inject.Inject private const val AWAITING_PHOTO_CAPTURE_KEY = "awaiting_photo_capture" @@ -63,8 +67,11 @@ internal constructor( private val _showLogoutDialog = MutableStateFlow(LogoutDialogState.HIDDEN) val showLogoutDialog: StateFlow = _showLogoutDialog.asStateFlow() - private val _authenticatedUser = MutableStateFlow(null) - val authenticatedUser: StateFlow = _authenticatedUser.asStateFlow() + val user: Flow = + userRepository + .getSignInState() + .filter { it is SignInState.SignedIn } + .map { (it as SignInState.SignedIn).user } // TODO: Allow tile source configuration from a non-survey accessible source. // Issue URL: https://github.com/google/ground-android/issues/1730 @@ -83,10 +90,7 @@ internal constructor( } init { - viewModelScope.launch { - kickLocalMutationSyncWorkers() - _authenticatedUser.value = userRepository.getAuthenticatedUser() - } + viewModelScope.launch { kickLocalMutationSyncWorkers() } } /** diff --git a/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt new file mode 100644 index 0000000000..85491251be --- /dev/null +++ b/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.groundplatform.android.ui.home + +import com.google.common.truth.Truth.assertThat +import dagger.hilt.android.testing.HiltAndroidTest +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.advanceUntilIdle +import org.groundplatform.android.BaseHiltTest +import org.groundplatform.android.FakeData +import org.groundplatform.android.system.auth.FakeAuthenticationManager +import org.groundplatform.android.system.auth.SignInState +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +class HomeScreenViewModelTest : BaseHiltTest() { + + @Inject lateinit var authenticationManager: FakeAuthenticationManager + @Inject lateinit var viewModel: HomeScreenViewModel + + @Before + override fun setUp() { + super.setUp() + authenticationManager.setUser(FakeData.USER) + authenticationManager.signIn() + } + + @Test + fun testShowUserDetails() { + viewModel.showUserDetails() + assertThat(viewModel.showLogoutDialog.value) + .isEqualTo(HomeScreenViewModel.LogoutDialogState.USER_DETAILS) + } + + @Test + fun testShowSignOutConfirmation() { + viewModel.showSignOutConfirmation() + assertThat(viewModel.showLogoutDialog.value) + .isEqualTo(HomeScreenViewModel.LogoutDialogState.SIGN_OUT_CONFIRMATION) + } + + @Test + fun testDismissLogoutDialog() { + viewModel.showUserDetails() + viewModel.dismissLogoutDialog() + assertThat(viewModel.showLogoutDialog.value) + .isEqualTo(HomeScreenViewModel.LogoutDialogState.HIDDEN) + } + + @Test + fun testSignOut() = runWithTestDispatcher { + viewModel.showSignOutConfirmation() + viewModel.signOut() + + advanceUntilIdle() + + assertThat(viewModel.showLogoutDialog.value) + .isEqualTo(HomeScreenViewModel.LogoutDialogState.HIDDEN) + assertThat(authenticationManager.signInState.filterIsInstance().first()) + .isEqualTo(SignInState.SignedOut) + } + + @Test + fun testAuthenticatedUser() = runWithTestDispatcher { + advanceUntilIdle() + + val user = viewModel.user.filterNotNull().first() + assertThat(user).isEqualTo(FakeData.USER) + } +} From d2fdaada49602475ae89010357d536d24cfa6e0a Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 27 Feb 2026 11:29:23 +0530 Subject: [PATCH 3/8] Extract logout dialog logic to a Composable function in HomeScreenFragment --- .../android/ui/home/HomeScreenFragment.kt | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index 1095dfdd16..bd0ba9a52b 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -21,6 +21,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.core.view.GravityCompat @@ -31,7 +32,6 @@ import androidx.navigation.fragment.findNavController import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.navigation.NavigationView import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject import kotlinx.coroutines.launch import org.groundplatform.android.BuildConfig import org.groundplatform.android.R @@ -46,6 +46,7 @@ import org.groundplatform.android.ui.components.ConfirmationDialog import org.groundplatform.android.ui.main.MainViewModel import org.groundplatform.android.util.setComposableContent import org.groundplatform.android.util.systemInsets +import javax.inject.Inject /** * Fragment containing the map container and location of interest sheet fragments and NavigationView @@ -132,28 +133,7 @@ class HomeScreenFragment : menuItem.title = String.format(getString(R.string.build), BuildConfig.VERSION_NAME) binding.composeView.setComposableContent { - val state by homeScreenViewModel.showLogoutDialog.collectAsState() - val user by homeScreenViewModel.user.collectAsState(null) - - when (state) { - HomeScreenViewModel.LogoutDialogState.USER_DETAILS -> - user?.let { - UserDetailsDialog( - it, - { homeScreenViewModel.showSignOutConfirmation() }, - { homeScreenViewModel.dismissLogoutDialog() }, - ) - } - HomeScreenViewModel.LogoutDialogState.SIGN_OUT_CONFIRMATION -> - ConfirmationDialog( - title = R.string.sign_out_dialog_title, - description = R.string.sign_out_dialog_body, - confirmButtonText = R.string.sign_out, - onConfirmClicked = { homeScreenViewModel.signOut() }, - onDismiss = { homeScreenViewModel.dismissLogoutDialog() }, - ) - else -> {} - } + SetupUserConfirmationDialog() } } @@ -215,4 +195,30 @@ class HomeScreenFragment : closeDrawer() return true } + + @Composable + private fun SetupUserConfirmationDialog() { + val state by homeScreenViewModel.showLogoutDialog.collectAsState() + val user by homeScreenViewModel.user.collectAsState(null) + + when (state) { + HomeScreenViewModel.LogoutDialogState.USER_DETAILS -> + user?.let { + UserDetailsDialog( + it, + { homeScreenViewModel.showSignOutConfirmation() }, + { homeScreenViewModel.dismissLogoutDialog() }, + ) + } + HomeScreenViewModel.LogoutDialogState.SIGN_OUT_CONFIRMATION -> + ConfirmationDialog( + title = R.string.sign_out_dialog_title, + description = R.string.sign_out_dialog_body, + confirmButtonText = R.string.sign_out, + onConfirmClicked = { homeScreenViewModel.signOut() }, + onDismiss = { homeScreenViewModel.dismissLogoutDialog() }, + ) + else -> {} + } + } } From 16a295a20977ff27cab9d785a384ebb7eac12a3f Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 27 Feb 2026 11:31:33 +0530 Subject: [PATCH 4/8] Reformat code --- .../groundplatform/android/ui/home/HomeScreenFragment.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index bd0ba9a52b..c204b858a0 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -32,6 +32,7 @@ import androidx.navigation.fragment.findNavController import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.navigation.NavigationView import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject import kotlinx.coroutines.launch import org.groundplatform.android.BuildConfig import org.groundplatform.android.R @@ -46,7 +47,6 @@ import org.groundplatform.android.ui.components.ConfirmationDialog import org.groundplatform.android.ui.main.MainViewModel import org.groundplatform.android.util.setComposableContent import org.groundplatform.android.util.systemInsets -import javax.inject.Inject /** * Fragment containing the map container and location of interest sheet fragments and NavigationView @@ -132,9 +132,7 @@ class HomeScreenFragment : val menuItem = navigationView.menu.findItem(R.id.nav_log_version) menuItem.title = String.format(getString(R.string.build), BuildConfig.VERSION_NAME) - binding.composeView.setComposableContent { - SetupUserConfirmationDialog() - } + binding.composeView.setComposableContent { SetupUserConfirmationDialog() } } private fun updateNavHeader() = From 878e3455219793149d3bd1dde4883e328c681b63 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 27 Feb 2026 11:58:34 +0530 Subject: [PATCH 5/8] Extract user account dialogs to a separate Composable and rename `LogoutDialogState` to `AccountDialogState`. --- .../android/ui/home/HomeScreenFragment.kt | 27 ++--- .../android/ui/home/HomeScreenViewModel.kt | 17 +-- .../android/ui/home/UserAccountDialogs.kt | 50 +++++++++ .../ui/home/HomeScreenViewModelTest.kt | 18 ++-- .../android/ui/home/UserAccountDialogsTest.kt | 102 ++++++++++++++++++ 5 files changed, 178 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/org/groundplatform/android/ui/home/UserAccountDialogs.kt create mode 100644 app/src/test/java/org/groundplatform/android/ui/home/UserAccountDialogsTest.kt diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index c204b858a0..400df83c43 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -43,7 +43,6 @@ import org.groundplatform.android.repository.UserRepository import org.groundplatform.android.ui.common.AbstractFragment import org.groundplatform.android.ui.common.BackPressListener import org.groundplatform.android.ui.common.EphemeralPopups -import org.groundplatform.android.ui.components.ConfirmationDialog import org.groundplatform.android.ui.main.MainViewModel import org.groundplatform.android.util.setComposableContent import org.groundplatform.android.util.systemInsets @@ -199,24 +198,12 @@ class HomeScreenFragment : val state by homeScreenViewModel.showLogoutDialog.collectAsState() val user by homeScreenViewModel.user.collectAsState(null) - when (state) { - HomeScreenViewModel.LogoutDialogState.USER_DETAILS -> - user?.let { - UserDetailsDialog( - it, - { homeScreenViewModel.showSignOutConfirmation() }, - { homeScreenViewModel.dismissLogoutDialog() }, - ) - } - HomeScreenViewModel.LogoutDialogState.SIGN_OUT_CONFIRMATION -> - ConfirmationDialog( - title = R.string.sign_out_dialog_title, - description = R.string.sign_out_dialog_body, - confirmButtonText = R.string.sign_out, - onConfirmClicked = { homeScreenViewModel.signOut() }, - onDismiss = { homeScreenViewModel.dismissLogoutDialog() }, - ) - else -> {} - } + UserAccountDialogs( + state = state, + user = user, + onSignOut = { homeScreenViewModel.signOut() }, + onShowSignOutConfirmation = { homeScreenViewModel.showSignOutConfirmation() }, + onDismiss = { homeScreenViewModel.dismissLogoutDialog() }, + ) } } diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt index 9052263d26..a03bef543a 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt @@ -64,8 +64,8 @@ internal constructor( private val _openDrawerRequests: MutableSharedFlow = MutableSharedFlow() val openDrawerRequestsFlow: SharedFlow = _openDrawerRequests.asSharedFlow() - private val _showLogoutDialog = MutableStateFlow(LogoutDialogState.HIDDEN) - val showLogoutDialog: StateFlow = _showLogoutDialog.asStateFlow() + private val _showLogoutDialog = MutableStateFlow(AccountDialogState.HIDDEN) + val showLogoutDialog: StateFlow = _showLogoutDialog.asStateFlow() val user: Flow = userRepository @@ -137,26 +137,27 @@ internal constructor( suspend fun getOfflineAreas() = offlineAreaRepository.offlineAreas().first() fun signOut() { - _showLogoutDialog.value = LogoutDialogState.HIDDEN + _showLogoutDialog.value = AccountDialogState.HIDDEN viewModelScope.launch { userRepository.signOut() } } fun showUserDetails() { - _showLogoutDialog.value = LogoutDialogState.USER_DETAILS + _showLogoutDialog.value = AccountDialogState.USER_DETAILS } fun showSignOutConfirmation() { - _showLogoutDialog.value = LogoutDialogState.SIGN_OUT_CONFIRMATION + _showLogoutDialog.value = AccountDialogState.SIGN_OUT_CONFIRMATION } fun dismissLogoutDialog() { - _showLogoutDialog.value = LogoutDialogState.HIDDEN + _showLogoutDialog.value = AccountDialogState.HIDDEN } /** - * Represents the possible visibility states of the user profile and logout confirmation dialogs. + * Represents the possible visibility states of dialogs related to the user's account, such as + * profile details and sign-out confirmation. */ - enum class LogoutDialogState { + enum class AccountDialogState { HIDDEN, USER_DETAILS, SIGN_OUT_CONFIRMATION, diff --git a/app/src/main/java/org/groundplatform/android/ui/home/UserAccountDialogs.kt b/app/src/main/java/org/groundplatform/android/ui/home/UserAccountDialogs.kt new file mode 100644 index 0000000000..f466eb399e --- /dev/null +++ b/app/src/main/java/org/groundplatform/android/ui/home/UserAccountDialogs.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.android.ui.home + +import androidx.compose.runtime.Composable +import org.groundplatform.android.R +import org.groundplatform.android.model.User +import org.groundplatform.android.ui.components.ConfirmationDialog + +@Composable +fun UserAccountDialogs( + state: HomeScreenViewModel.AccountDialogState, + user: User?, + onSignOut: () -> Unit, + onShowSignOutConfirmation: () -> Unit, + onDismiss: () -> Unit, +) { + when (state) { + HomeScreenViewModel.AccountDialogState.USER_DETAILS -> + user?.let { + UserDetailsDialog( + user = it, + signOutCallback = onShowSignOutConfirmation, + dismissCallback = onDismiss, + ) + } + HomeScreenViewModel.AccountDialogState.SIGN_OUT_CONFIRMATION -> + ConfirmationDialog( + title = R.string.sign_out_dialog_title, + description = R.string.sign_out_dialog_body, + confirmButtonText = R.string.sign_out, + onConfirmClicked = onSignOut, + onDismiss = onDismiss, + ) + else -> {} + } +} diff --git a/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt index 85491251be..6064745793 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt @@ -28,6 +28,9 @@ import org.groundplatform.android.BaseHiltTest import org.groundplatform.android.FakeData import org.groundplatform.android.system.auth.FakeAuthenticationManager import org.groundplatform.android.system.auth.SignInState +import org.groundplatform.android.ui.home.HomeScreenViewModel.AccountDialogState.HIDDEN +import org.groundplatform.android.ui.home.HomeScreenViewModel.AccountDialogState.SIGN_OUT_CONFIRMATION +import org.groundplatform.android.ui.home.HomeScreenViewModel.AccountDialogState.USER_DETAILS import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -51,23 +54,23 @@ class HomeScreenViewModelTest : BaseHiltTest() { @Test fun testShowUserDetails() { viewModel.showUserDetails() - assertThat(viewModel.showLogoutDialog.value) - .isEqualTo(HomeScreenViewModel.LogoutDialogState.USER_DETAILS) + + assertThat(viewModel.showLogoutDialog.value).isEqualTo(USER_DETAILS) } @Test fun testShowSignOutConfirmation() { viewModel.showSignOutConfirmation() - assertThat(viewModel.showLogoutDialog.value) - .isEqualTo(HomeScreenViewModel.LogoutDialogState.SIGN_OUT_CONFIRMATION) + + assertThat(viewModel.showLogoutDialog.value).isEqualTo(SIGN_OUT_CONFIRMATION) } @Test fun testDismissLogoutDialog() { viewModel.showUserDetails() viewModel.dismissLogoutDialog() - assertThat(viewModel.showLogoutDialog.value) - .isEqualTo(HomeScreenViewModel.LogoutDialogState.HIDDEN) + + assertThat(viewModel.showLogoutDialog.value).isEqualTo(HIDDEN) } @Test @@ -77,8 +80,7 @@ class HomeScreenViewModelTest : BaseHiltTest() { advanceUntilIdle() - assertThat(viewModel.showLogoutDialog.value) - .isEqualTo(HomeScreenViewModel.LogoutDialogState.HIDDEN) + assertThat(viewModel.showLogoutDialog.value).isEqualTo(HIDDEN) assertThat(authenticationManager.signInState.filterIsInstance().first()) .isEqualTo(SignInState.SignedOut) } diff --git a/app/src/test/java/org/groundplatform/android/ui/home/UserAccountDialogsTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/UserAccountDialogsTest.kt new file mode 100644 index 0000000000..80d27d8d91 --- /dev/null +++ b/app/src/test/java/org/groundplatform/android/ui/home/UserAccountDialogsTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.groundplatform.android.ui.home + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.espresso.matcher.ViewMatchers.assertThat +import org.groundplatform.android.FakeData +import org.groundplatform.android.R +import org.groundplatform.android.getString +import org.groundplatform.android.model.User +import org.groundplatform.android.ui.home.HomeScreenViewModel.AccountDialogState +import org.groundplatform.android.ui.home.HomeScreenViewModel.AccountDialogState.USER_DETAILS +import org.hamcrest.Matchers.`is` +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class UserAccountDialogsTest { + + @get:Rule val composeTestRule = createComposeRule() + + private lateinit var user: User + + @Before + fun setUp() { + user = FakeData.USER + } + + private fun setupContent( + state: AccountDialogState, + onSignOut: () -> Unit = {}, + onShowSignOutConfirmation: () -> Unit = {}, + ) { + composeTestRule.setContent { + UserAccountDialogs( + state = state, + user = user, + onSignOut = onSignOut, + onShowSignOutConfirmation = onShowSignOutConfirmation, + onDismiss = {}, + ) + } + } + + @Test + fun showUserDetailsDialog() { + setupContent(state = USER_DETAILS) + + composeTestRule.onNodeWithText(user.displayName).assertExists() + composeTestRule.onNodeWithText(user.email).assertExists() + } + + @Test + fun showSignOutConfirmationDialog() { + setupContent(state = AccountDialogState.SIGN_OUT_CONFIRMATION) + + composeTestRule.onNodeWithText(getString(R.string.sign_out_dialog_title)).assertIsDisplayed() + composeTestRule.onNodeWithText(getString(R.string.sign_out_dialog_body)).assertIsDisplayed() + } + + @Test + fun clickSignOut_invokesCallback() { + var signOutClicked = false + setupContent( + state = AccountDialogState.SIGN_OUT_CONFIRMATION, + onSignOut = { signOutClicked = true }, + ) + + composeTestRule.onNodeWithText(getString(R.string.sign_out)).performClick() + + assertThat(signOutClicked, `is`(true)) + } + + @Test + fun clickSignOutInUserDetails_invokesShowSignOutConfirmation() { + var showSignOutConfirmationClicked = false + setupContent(state = USER_DETAILS, onShowSignOutConfirmation = { showSignOutConfirmationClicked = true }) + + composeTestRule.onNodeWithText(getString(R.string.sign_out)).performClick() + + assertThat(showSignOutConfirmationClicked, `is`(true)) + } +} From c756b55cfbcb0a444b621832054cb1f773852a12 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 27 Feb 2026 12:00:56 +0530 Subject: [PATCH 6/8] Apply formatting --- .../groundplatform/android/ui/home/UserAccountDialogsTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/org/groundplatform/android/ui/home/UserAccountDialogsTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/UserAccountDialogsTest.kt index 80d27d8d91..67c1b02b70 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/UserAccountDialogsTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/UserAccountDialogsTest.kt @@ -93,7 +93,10 @@ class UserAccountDialogsTest { @Test fun clickSignOutInUserDetails_invokesShowSignOutConfirmation() { var showSignOutConfirmationClicked = false - setupContent(state = USER_DETAILS, onShowSignOutConfirmation = { showSignOutConfirmationClicked = true }) + setupContent( + state = USER_DETAILS, + onShowSignOutConfirmation = { showSignOutConfirmationClicked = true }, + ) composeTestRule.onNodeWithText(getString(R.string.sign_out)).performClick() From 945c8adf2ecf05c036a5a7c033936a805a7df68a Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 27 Feb 2026 15:16:59 +0530 Subject: [PATCH 7/8] Update app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt Co-authored-by: Andreia Ferreira <51242456+andreia-ferreira@users.noreply.github.com> --- .../org/groundplatform/android/ui/home/HomeScreenFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index 400df83c43..0aa7a2c718 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -195,8 +195,8 @@ class HomeScreenFragment : @Composable private fun SetupUserConfirmationDialog() { - val state by homeScreenViewModel.showLogoutDialog.collectAsState() - val user by homeScreenViewModel.user.collectAsState(null) + val state by homeScreenViewModel.showLogoutDialog.collectAsStateWithLifecycle() + val user by homeScreenViewModel.user.collectAsStateWithLifecycle(null) UserAccountDialogs( state = state, From 19df2c49557f1c994685a43893f99f84267a3138 Mon Sep 17 00:00:00 2001 From: Shobhit Agarwal Date: Fri, 27 Feb 2026 15:19:53 +0530 Subject: [PATCH 8/8] Apply suggestions --- .../android/ui/home/HomeScreenFragment.kt | 2 +- .../android/ui/home/HomeScreenViewModel.kt | 12 ++++++------ .../android/ui/home/HomeScreenViewModelTest.kt | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt index 400df83c43..0129ce47db 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenFragment.kt @@ -195,7 +195,7 @@ class HomeScreenFragment : @Composable private fun SetupUserConfirmationDialog() { - val state by homeScreenViewModel.showLogoutDialog.collectAsState() + val state by homeScreenViewModel.accountDialogState.collectAsState() val user by homeScreenViewModel.user.collectAsState(null) UserAccountDialogs( diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt index a03bef543a..472b576cf9 100644 --- a/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeScreenViewModel.kt @@ -64,8 +64,8 @@ internal constructor( private val _openDrawerRequests: MutableSharedFlow = MutableSharedFlow() val openDrawerRequestsFlow: SharedFlow = _openDrawerRequests.asSharedFlow() - private val _showLogoutDialog = MutableStateFlow(AccountDialogState.HIDDEN) - val showLogoutDialog: StateFlow = _showLogoutDialog.asStateFlow() + private val _accountDialogState = MutableStateFlow(AccountDialogState.HIDDEN) + val accountDialogState: StateFlow = _accountDialogState.asStateFlow() val user: Flow = userRepository @@ -137,20 +137,20 @@ internal constructor( suspend fun getOfflineAreas() = offlineAreaRepository.offlineAreas().first() fun signOut() { - _showLogoutDialog.value = AccountDialogState.HIDDEN + _accountDialogState.value = AccountDialogState.HIDDEN viewModelScope.launch { userRepository.signOut() } } fun showUserDetails() { - _showLogoutDialog.value = AccountDialogState.USER_DETAILS + _accountDialogState.value = AccountDialogState.USER_DETAILS } fun showSignOutConfirmation() { - _showLogoutDialog.value = AccountDialogState.SIGN_OUT_CONFIRMATION + _accountDialogState.value = AccountDialogState.SIGN_OUT_CONFIRMATION } fun dismissLogoutDialog() { - _showLogoutDialog.value = AccountDialogState.HIDDEN + _accountDialogState.value = AccountDialogState.HIDDEN } /** diff --git a/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt index 6064745793..765d84db7c 100644 --- a/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/home/HomeScreenViewModelTest.kt @@ -55,14 +55,14 @@ class HomeScreenViewModelTest : BaseHiltTest() { fun testShowUserDetails() { viewModel.showUserDetails() - assertThat(viewModel.showLogoutDialog.value).isEqualTo(USER_DETAILS) + assertThat(viewModel.accountDialogState.value).isEqualTo(USER_DETAILS) } @Test fun testShowSignOutConfirmation() { viewModel.showSignOutConfirmation() - assertThat(viewModel.showLogoutDialog.value).isEqualTo(SIGN_OUT_CONFIRMATION) + assertThat(viewModel.accountDialogState.value).isEqualTo(SIGN_OUT_CONFIRMATION) } @Test @@ -70,7 +70,7 @@ class HomeScreenViewModelTest : BaseHiltTest() { viewModel.showUserDetails() viewModel.dismissLogoutDialog() - assertThat(viewModel.showLogoutDialog.value).isEqualTo(HIDDEN) + assertThat(viewModel.accountDialogState.value).isEqualTo(HIDDEN) } @Test @@ -80,7 +80,7 @@ class HomeScreenViewModelTest : BaseHiltTest() { advanceUntilIdle() - assertThat(viewModel.showLogoutDialog.value).isEqualTo(HIDDEN) + assertThat(viewModel.accountDialogState.value).isEqualTo(HIDDEN) assertThat(authenticationManager.signInState.filterIsInstance().first()) .isEqualTo(SignInState.SignedOut) }