Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -21,10 +21,12 @@ 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.Composable
import androidx.compose.runtime.getValue
import androidx.core.view.GravityCompat
import androidx.core.view.WindowInsetsCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.imageview.ShapeableImageView
Expand All @@ -37,12 +39,10 @@ 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
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
Expand All @@ -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)
Expand Down Expand Up @@ -96,9 +95,9 @@ class HomeScreenFragment :
HomeScreenFragmentDirections.actionHomeScreenFragmentToSurveySelectorFragment(false)
)
}
viewLifecycleOwner.lifecycleScope.launch { user = userRepository.getAuthenticatedUser() }

navHeader.findViewById<ShapeableImageView>(R.id.user_image).setOnClickListener {
showSignOutConfirmationDialogs()
homeScreenViewModel.showUserDetails()
}
updateNavHeader()
// Re-open data collection screen if draft submission is present.
Expand Down Expand Up @@ -131,6 +130,8 @@ class HomeScreenFragment :
val navigationView = view.findViewById<NavigationView>(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 { SetupUserConfirmationDialog() }
}

private fun updateNavHeader() =
Expand Down Expand Up @@ -192,43 +193,17 @@ class HomeScreenFragment :
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() },
)
}
}
@Composable
private fun SetupUserConfirmationDialog() {
val state by homeScreenViewModel.accountDialogState.collectAsStateWithLifecycle()
val user by homeScreenViewModel.user.collectAsStateWithLifecycle(null)

UserAccountDialogs(
state = state,
user = user,
onSignOut = { homeScreenViewModel.signOut() },
onShowSignOutConfirmation = { homeScreenViewModel.showSignOutConfirmation() },
onDismiss = { homeScreenViewModel.dismissLogoutDialog() },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,27 @@ 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
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
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
Expand All @@ -56,6 +64,15 @@ internal constructor(
private val _openDrawerRequests: MutableSharedFlow<Unit> = MutableSharedFlow()
val openDrawerRequestsFlow: SharedFlow<Unit> = _openDrawerRequests.asSharedFlow()

private val _accountDialogState = MutableStateFlow(AccountDialogState.HIDDEN)
val accountDialogState: StateFlow<AccountDialogState> = _accountDialogState.asStateFlow()

val user: Flow<User> =
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
val showOfflineAreaMenuItem: LiveData<Boolean> = MutableLiveData(true)
Expand Down Expand Up @@ -120,6 +137,29 @@ internal constructor(
suspend fun getOfflineAreas() = offlineAreaRepository.offlineAreas().first()

fun signOut() {
_accountDialogState.value = AccountDialogState.HIDDEN
viewModelScope.launch { userRepository.signOut() }
}

fun showUserDetails() {
_accountDialogState.value = AccountDialogState.USER_DETAILS
}

fun showSignOutConfirmation() {
_accountDialogState.value = AccountDialogState.SIGN_OUT_CONFIRMATION
}

fun dismissLogoutDialog() {
_accountDialogState.value = AccountDialogState.HIDDEN
}

/**
* Represents the possible visibility states of dialogs related to the user's account, such as
* profile details and sign-out confirmation.
*/
enum class AccountDialogState {
HIDDEN,
USER_DETAILS,
SIGN_OUT_CONFIRMATION,
}
}
Original file line number Diff line number Diff line change
@@ -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 -> {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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.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
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.accountDialogState.value).isEqualTo(USER_DETAILS)
}

@Test
fun testShowSignOutConfirmation() {
viewModel.showSignOutConfirmation()

assertThat(viewModel.accountDialogState.value).isEqualTo(SIGN_OUT_CONFIRMATION)
}

@Test
fun testDismissLogoutDialog() {
viewModel.showUserDetails()
viewModel.dismissLogoutDialog()

assertThat(viewModel.accountDialogState.value).isEqualTo(HIDDEN)
}

@Test
fun testSignOut() = runWithTestDispatcher {
viewModel.showSignOutConfirmation()
viewModel.signOut()

advanceUntilIdle()

assertThat(viewModel.accountDialogState.value).isEqualTo(HIDDEN)
assertThat(authenticationManager.signInState.filterIsInstance<SignInState.SignedOut>().first())
.isEqualTo(SignInState.SignedOut)
}

@Test
fun testAuthenticatedUser() = runWithTestDispatcher {
advanceUntilIdle()

val user = viewModel.user.filterNotNull().first()
assertThat(user).isEqualTo(FakeData.USER)
}
}
Loading