diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e740d722ee..2b673fefff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -137,7 +137,6 @@ to the base repository using a pull request. ## Initial build configuration -### Add Google Maps API Key(s) ### Set up Firebase @@ -157,15 +156,17 @@ to the base repository using a pull request. 4. Download the config file for the Android app to `app/src/debug/google-services.json` -5. Create a file named `secrets.properties` in the root of the project with the following contents: +### Add Google Maps API Key(s) - ``` - MAPS_API_KEY= - ``` +Create a file named `secrets.properties` in the root of the project with the following contents: + +``` +MAPS_API_KEY= +``` - You can find the Maps SDK key for your Firebase project at - http://console.cloud.google.com/google/maps-apis/credentials under - "Android key (auto created by Firebase)". +You can find the Maps SDK key for your Firebase project at +http://console.cloud.google.com/google/maps-apis/credentials under +"Android key (auto created by Firebase)". ### Troubleshooting diff --git a/app/build.gradle b/app/build.gradle index e06ae733fd..4aac105e3d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,7 +90,7 @@ android { buildConfigField "String", "SIGNUP_FORM_LINK", "\"\"" manifestPlaceholders.usesCleartextTraffic = true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "org.groundplatform.android.CustomTestRunner" } // Use flag -PtestBuildType with desired variant to change default behavior. @@ -213,6 +213,7 @@ dependencies { implementation libs.androidx.ui.tooling.preview.android stagingImplementation libs.androidx.ui.test.manifest testImplementation libs.androidx.ui.test.junit4 + androidTestImplementation libs.androidx.ui.test.junit4 implementation libs.androidx.navigation.compose implementation libs.androidx.hilt.navigation.compose @@ -383,3 +384,4 @@ secrets { // checked in version control. defaultPropertiesFileName = "local.defaults.properties" } + diff --git a/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt b/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt new file mode 100644 index 0000000000..4681c58116 --- /dev/null +++ b/app/src/androidTest/java/org/groundplatform/android/CustomTestRunner.kt @@ -0,0 +1,34 @@ +/* + * 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 + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + +/** + * A custom [AndroidJUnitRunner] used to replace the application class with [HiltTestApplication]. + * + * This is required for Hilt dependency injection to work correctly in instrumented tests. + */ +class CustomTestRunner : AndroidJUnitRunner() { + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context?, + ): Application = super.newApplication(cl, HiltTestApplication::class.java.name, context) +} diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt new file mode 100644 index 0000000000..92a87b54a4 --- /dev/null +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawer.kt @@ -0,0 +1,278 @@ +/* + * 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.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ExitToApp +import androidx.compose.material.icons.filled.Build +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.groundplatform.android.R +import org.groundplatform.android.model.Survey +import org.groundplatform.android.model.User +import org.groundplatform.android.ui.theme.AppTheme + +@Composable +fun HomeDrawer( + user: User, + survey: Survey?, + versionText: String, + onAction: (HomeDrawerAction) -> Unit, +) { + Column( + modifier = + Modifier.fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .systemBarsPadding() + .verticalScroll(rememberScrollState()) + ) { + AppInfoHeader(user = user) + SurveySelector(survey = survey, onSwitchSurvey = { onAction(HomeDrawerAction.OnSwitchSurvey) }) + HorizontalDivider() + DrawerItems(onAction, versionText) + } +} + +private val NAV_ITEMS = + listOf( + DrawerItem( + labelId = R.string.offline_map_imagery, + icon = IconSource.Drawable(R.drawable.ic_offline_pin), + action = HomeDrawerAction.OnNavigateToOfflineAreas, + ), + DrawerItem( + labelId = R.string.sync_status, + icon = IconSource.Drawable(R.drawable.ic_sync), + action = HomeDrawerAction.OnNavigateToSyncStatus, + ), + DrawerItem( + labelId = R.string.settings, + icon = IconSource.Vector(Icons.Default.Settings), + action = HomeDrawerAction.OnNavigateToSettings, + ), + DrawerItem( + labelId = R.string.about, + icon = IconSource.Drawable(R.drawable.info_outline), + action = HomeDrawerAction.OnNavigateToAbout, + ), + DrawerItem( + labelId = R.string.terms_of_service, + icon = IconSource.Drawable(R.drawable.feed), + action = HomeDrawerAction.OnNavigateToTerms, + ), + DrawerItem( + labelId = R.string.sign_out, + icon = IconSource.Vector(Icons.AutoMirrored.Filled.ExitToApp), + action = HomeDrawerAction.OnSignOut, + ), + ) + +@Composable +private fun DrawerItems(onAction: (HomeDrawerAction) -> Unit, versionText: String) { + NAV_ITEMS.forEach { item -> DrawerNavigationItem(item, onAction) } + + DrawerVersionFooter(versionText) +} + +@Composable +private fun DrawerNavigationItem(item: DrawerItem, onAction: (HomeDrawerAction) -> Unit) { + val label = stringResource(item.labelId) + NavigationDrawerItem( + label = { Text(label) }, + selected = false, + onClick = { onAction(item.action) }, + icon = { + val description = null + when (item.icon) { + is IconSource.Vector -> Icon(item.icon.imageVector, contentDescription = description) + is IconSource.Drawable -> + Icon(painterResource(item.icon.id), contentDescription = description) + } + }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding).testTag(label), + ) +} + +@Composable +private fun DrawerVersionFooter(versionText: String) { + Row( + modifier = + Modifier.fillMaxWidth() + .padding(NavigationDrawerItemDefaults.ItemPadding) + .padding(start = 16.dp, end = 24.dp, top = 12.dp, bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.Build, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.width(12.dp)) + Text( + text = versionText, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + +private data class DrawerItem( + @androidx.annotation.StringRes val labelId: Int, + val icon: IconSource, + val action: HomeDrawerAction, +) + +@Composable +private fun AppInfoHeader(user: User) { + Column( + modifier = + Modifier.fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(vertical = 24.dp, horizontal = 16.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(R.drawable.ground_logo), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + Spacer(Modifier.width(8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.app_name), + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + ) + } + if (user.photoUrl != null) { + coil.compose.AsyncImage( + model = user.photoUrl, + contentDescription = null, + modifier = Modifier.size(32.dp).clip(CircleShape), + contentScale = androidx.compose.ui.layout.ContentScale.Crop, + ) + } + } + } +} + +@Composable +private fun SurveySelector(survey: Survey?, onSwitchSurvey: () -> Unit) { + Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.ic_content_paste), + contentDescription = stringResource(R.string.current_survey), + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.width(4.dp)) + Text( + text = stringResource(R.string.current_survey), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(Modifier.height(8.dp)) + + if (survey == null) { + Text(stringResource(R.string.no_survey_selected)) + } else { + Text( + text = survey.title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + if (survey.description.isNotEmpty()) { + Text( + text = survey.description, + style = MaterialTheme.typography.bodyMedium, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 8.dp), + ) + } + } + + Spacer(Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.switch_survey), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 8.dp).clickable(onClick = onSwitchSurvey), + ) + } +} + +private sealed interface IconSource { + data class Vector(val imageVector: androidx.compose.ui.graphics.vector.ImageVector) : IconSource + + data class Drawable(@androidx.annotation.DrawableRes val id: Int) : IconSource +} + +@Preview(showBackground = true) +@Composable +fun HomeDrawerPreview() { + val mockUser = + User(id = "1", email = "test@example.com", displayName = "Jane Doe", photoUrl = null) + val mockSurvey = + Survey( + id = "1", + title = "Tree Survey", + description = "A comprehensive survey for mapping urban tree canopy and assessing health.", + jobMap = emptyMap(), + generalAccess = org.groundplatform.android.proto.Survey.GeneralAccess.PUBLIC, + ) + AppTheme { + HomeDrawer(user = mockUser, survey = mockSurvey, versionText = "1.0.0-preview", onAction = {}) + } +} diff --git a/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawerAction.kt b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawerAction.kt new file mode 100644 index 0000000000..bf97f7607c --- /dev/null +++ b/app/src/main/java/org/groundplatform/android/ui/home/HomeDrawerAction.kt @@ -0,0 +1,32 @@ +/* + * 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 + +sealed interface HomeDrawerAction { + data object OnSwitchSurvey : HomeDrawerAction + + data object OnNavigateToOfflineAreas : HomeDrawerAction + + data object OnNavigateToSyncStatus : HomeDrawerAction + + data object OnNavigateToSettings : HomeDrawerAction + + data object OnNavigateToAbout : HomeDrawerAction + + data object OnNavigateToTerms : HomeDrawerAction + + data object OnSignOut : HomeDrawerAction +} 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..50d34f38e5 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 @@ -17,35 +17,26 @@ package org.groundplatform.android.ui.home import android.os.Bundle import android.view.LayoutInflater -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.LaunchedEffect +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 +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope 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 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 /** * Fragment containing the map container and location of interest sheet fragments and NavigationView @@ -53,18 +44,14 @@ import org.groundplatform.android.util.systemInsets * fragments (e.g., view submission and edit submission) at runtime. */ @AndroidEntryPoint -class HomeScreenFragment : - AbstractFragment(), BackPressListener, NavigationView.OnNavigationItemSelectedListener { +class HomeScreenFragment : AbstractFragment(), BackPressListener { @Inject lateinit var ephemeralPopups: EphemeralPopups - @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) - getViewModel(MainViewModel::class.java).windowInsets.observe(this) { onApplyWindowInsets(it) } homeScreenViewModel = getViewModel(HomeScreenViewModel::class.java) } @@ -76,31 +63,97 @@ class HomeScreenFragment : super.onCreateView(inflater, container, savedInstanceState) binding = HomeScreenFragBinding.inflate(inflater, container, false) binding.lifecycleOwner = this - lifecycleScope.launch { homeScreenViewModel.openDrawerRequestsFlow.collect { openDrawer() } } return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val binding = binding // Ensure nav drawer cannot be swiped out, which would conflict with map pan gestures. - binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - homeScreenViewModel.showOfflineAreaMenuItem.observe(viewLifecycleOwner) { - binding.navView.menu.findItem(R.id.nav_offline_areas).isEnabled = it - } + // binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) + + setupComposeView(binding) + setupDrawerContent(binding) + restoreDraftSubmission(binding) + } + + private fun setupComposeView(binding: HomeScreenFragBinding) { + binding.composeView.setComposableContent { + val showSignOutDialog = homeScreenViewModel.showSignOutDialog.collectAsState(false) + + LaunchedEffect(Unit) { homeScreenViewModel.openDrawerRequestsFlow.collect { openDrawer() } } + + if (showSignOutDialog.value) { - binding.navView.setNavigationItemSelectedListener(this) - val navHeader = binding.navView.getHeaderView(0) - navHeader.findViewById(R.id.switch_survey_button).setOnClickListener { - findNavController() - .navigate( - HomeScreenFragmentDirections.actionHomeScreenFragmentToSurveySelectorFragment(false) + 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.dismissSignOutDialog() }, ) + } } - viewLifecycleOwner.lifecycleScope.launch { user = userRepository.getAuthenticatedUser() } - navHeader.findViewById(R.id.user_image).setOnClickListener { - showSignOutConfirmationDialogs() + } + + private fun setupDrawerContent(binding: HomeScreenFragBinding) { + binding.drawerView.setComposableContent { + val drawerState by homeScreenViewModel.drawerState.collectAsStateWithLifecycle() + + drawerState?.let { state -> + HomeDrawer( + user = state.user, + survey = state.survey, + versionText = String.format(getString(R.string.build), state.appVersion), + onAction = { action -> + when (action) { + HomeDrawerAction.OnSwitchSurvey -> { + findNavController() + .navigate( + HomeScreenFragmentDirections.actionHomeScreenFragmentToSurveySelectorFragment( + false + ) + ) + } + HomeDrawerAction.OnNavigateToOfflineAreas -> { + lifecycleScope.launch { + if (homeScreenViewModel.getOfflineAreas().isEmpty()) + findNavController() + .navigate(HomeScreenFragmentDirections.showOfflineAreaSelector()) + else findNavController().navigate(HomeScreenFragmentDirections.showOfflineAreas()) + } + closeDrawer() + } + HomeDrawerAction.OnNavigateToSyncStatus -> { + findNavController().navigate(HomeScreenFragmentDirections.showSyncStatus()) + closeDrawer() + } + HomeDrawerAction.OnNavigateToSettings -> { + findNavController() + .navigate( + HomeScreenFragmentDirections.actionHomeScreenFragmentToSettingsActivity() + ) + closeDrawer() + } + HomeDrawerAction.OnNavigateToAbout -> { + findNavController().navigate(HomeScreenFragmentDirections.showAbout()) + closeDrawer() + } + HomeDrawerAction.OnNavigateToTerms -> { + findNavController().navigate(HomeScreenFragmentDirections.showTermsOfService(true)) + closeDrawer() + } + HomeDrawerAction.OnSignOut -> { + homeScreenViewModel.showSignOutDialog() + } + } + }, + ) + } } - updateNavHeader() + } + + private fun restoreDraftSubmission(binding: HomeScreenFragBinding) { // Re-open data collection screen if draft submission is present. viewLifecycleOwner.lifecycleScope.launch { homeScreenViewModel.getDraftSubmission()?.let { draft -> @@ -127,29 +180,8 @@ 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) } - private fun updateNavHeader() = - lifecycleScope.launch { - val navHeader = binding.navView.getHeaderView(0) - val headerBinding = NavDrawerHeaderBinding.bind(navHeader) - headerBinding.user = userRepository.getAuthenticatedUser() - homeScreenViewModel.surveyRepository.activeSurveyFlow.collect { - if (it == null) { - headerBinding.surveyInfo.visibility = View.GONE - headerBinding.noSurveysInfo.visibility = View.VISIBLE - } else { - headerBinding.noSurveysInfo.visibility = View.GONE - headerBinding.surveyInfo.visibility = View.VISIBLE - headerBinding.survey = it - } - } - } - private fun openDrawer() { binding.drawerLayout.openDrawer(GravityCompat.START) } @@ -158,77 +190,5 @@ class HomeScreenFragment : binding.drawerLayout.closeDrawer(GravityCompat.START) } - private fun onApplyWindowInsets(insets: WindowInsetsCompat) { - val headerView = binding.navView.getHeaderView(0) - headerView.setPadding(0, insets.systemInsets().top, 0, 0) - } - override fun onBack(): Boolean = false - - override fun onNavigationItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.sync_status -> { - findNavController().navigate(HomeScreenFragmentDirections.showSyncStatus()) - } - R.id.nav_offline_areas -> { - lifecycleScope.launch { - if (homeScreenViewModel.getOfflineAreas().isEmpty()) - findNavController().navigate(HomeScreenFragmentDirections.showOfflineAreaSelector()) - else findNavController().navigate(HomeScreenFragmentDirections.showOfflineAreas()) - } - } - R.id.nav_settings -> { - findNavController() - .navigate(HomeScreenFragmentDirections.actionHomeScreenFragmentToSettingsActivity()) - } - R.id.about -> { - findNavController().navigate(HomeScreenFragmentDirections.showAbout()) - } - R.id.terms_of_service -> { - findNavController().navigate(HomeScreenFragmentDirections.showTermsOfService(true)) - } - } - 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..f4effb5be0 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 @@ -21,9 +21,16 @@ 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.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.groundplatform.android.data.sync.MediaUploadWorkManager import org.groundplatform.android.data.sync.MutationSyncWorkManager @@ -37,6 +44,12 @@ import org.groundplatform.android.ui.common.AbstractViewModel import org.groundplatform.android.ui.common.SharedViewModel import timber.log.Timber +data class HomeDrawerState( + val user: org.groundplatform.android.model.User, + val survey: org.groundplatform.android.model.Survey?, + val appVersion: String, +) + private const val AWAITING_PHOTO_CAPTURE_KEY = "awaiting_photo_capture" @SharedViewModel @@ -55,6 +68,8 @@ internal constructor( private val savedStateHandle: SavedStateHandle = SavedStateHandle() private val _openDrawerRequests: MutableSharedFlow = MutableSharedFlow() val openDrawerRequestsFlow: SharedFlow = _openDrawerRequests.asSharedFlow() + private val _showSignOutDialog: MutableStateFlow = MutableStateFlow(false) + val showSignOutDialog: StateFlow = _showSignOutDialog.asStateFlow() // TODO: Allow tile source configuration from a non-survey accessible source. // Issue URL: https://github.com/google/ground-android/issues/1730 @@ -76,6 +91,17 @@ internal constructor( viewModelScope.launch { kickLocalMutationSyncWorkers() } } + val drawerState: StateFlow = + flow { emit(userRepository.getAuthenticatedUser()) } + .combine(surveyRepository.activeSurveyFlow) { user, survey -> + HomeDrawerState( + user = user, + survey = survey, + appVersion = org.groundplatform.android.BuildConfig.VERSION_NAME, + ) + } + .stateIn(viewModelScope, SharingStarted.Lazily, null) + /** * Enqueue data and photo upload workers for all pending mutations when home screen is first * opened as a workaround the get stuck mutations (i.e., PENDING or FAILED mutations with no @@ -91,7 +117,7 @@ internal constructor( } } - /** Attempts to return draft submission for the currently active survey. */ + /** Attempts to return draft submission for the currently active active survey. */ suspend fun getDraftSubmission(): DraftSubmission? { val draftId = submissionRepository.getDraftSubmissionsId() val survey = surveyRepository.activeSurveyFlow.first() @@ -122,4 +148,12 @@ internal constructor( fun signOut() { viewModelScope.launch { userRepository.signOut() } } + + fun showSignOutDialog() { + _showSignOutDialog.value = true + } + + fun dismissSignOutDialog() { + _showSignOutDialog.value = false + } } diff --git a/app/src/main/res/color/nav_drawer_item.xml b/app/src/main/res/color/nav_drawer_item.xml deleted file mode 100644 index 9e0a118926..0000000000 --- a/app/src/main/res/color/nav_drawer_item.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/task_transparent_btn_selector.xml b/app/src/main/res/color/task_transparent_btn_selector.xml deleted file mode 100644 index b30f3494b1..0000000000 --- a/app/src/main/res/color/task_transparent_btn_selector.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_history.xml b/app/src/main/res/drawable-anydpi/ic_history.xml deleted file mode 100644 index 885eb0601c..0000000000 --- a/app/src/main/res/drawable-anydpi/ic_history.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/cloud_off.xml b/app/src/main/res/drawable/cloud_off.xml deleted file mode 100644 index eb8b3d88f5..0000000000 --- a/app/src/main/res/drawable/cloud_off.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml deleted file mode 100644 index ee9f81c935..0000000000 --- a/app/src/main/res/drawable/ic_settings.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/home_screen_frag.xml b/app/src/main/res/layout/home_screen_frag.xml index d6696c54d8..fb27f2b6ac 100644 --- a/app/src/main/res/layout/home_screen_frag.xml +++ b/app/src/main/res/layout/home_screen_frag.xml @@ -53,17 +53,12 @@ - + android:fitsSystemWindows="true" /> \ No newline at end of file diff --git a/app/src/main/res/layout/nav_drawer_header.xml b/app/src/main/res/layout/nav_drawer_header.xml deleted file mode 100644 index e88c07a706..0000000000 --- a/app/src/main/res/layout/nav_drawer_header.xml +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer_menu.xml b/app/src/main/res/menu/nav_drawer_menu.xml deleted file mode 100644 index 31df413277..0000000000 --- a/app/src/main/res/menu/nav_drawer_menu.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e8cfac489a..6d93e93bea 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -26,9 +26,6 @@ 48dp 4dp - - 36dp - 50dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 8ff2754e40..b71d92d354 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -16,7 +16,7 @@ ~ limitations under the License. --> - + - - - - - - +