Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Factor out job cards into Composables for new data entry points #2892

Merged
merged 32 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1663746
Add new LOI flow (unstyled)
sufyanAbbasi Nov 15, 2024
f8654dc
Merge branch 'master' into sufy/2446/new-loi-flow
sufyanAbbasi Dec 2, 2024
f5fec71
Refactor out cards
sufyanAbbasi Dec 7, 2024
5858af2
Merge branch 'master' into sufy/2446/new-loi-flow
sufyanAbbasi Dec 7, 2024
5aa9496
Refactor cards into Composables.
sufyanAbbasi Dec 19, 2024
4d0d5e6
Merge branch 'master' into sufy/2446/new-loi-flow
sufyanAbbasi Dec 19, 2024
34dc21e
Formatting.
sufyanAbbasi Dec 19, 2024
5c420e8
Address `checkCode` issues.
sufyanAbbasi Dec 19, 2024
55393d8
Merge branch 'master' into sufy/2446/new-loi-flow
gino-m Dec 19, 2024
7e66573
Remove debug logs.
sufyanAbbasi Dec 19, 2024
2581739
Merge branch 'sufy/2446/new-loi-flow' of https://github.com/google/gr…
sufyanAbbasi Dec 19, 2024
6630320
Rename MapUIData and fields.
sufyanAbbasi Dec 19, 2024
f91494f
Merge branch 'master' into sufy/2446/new-loi-flow
sufyanAbbasi Dec 19, 2024
ac03e37
Update strings.xml
jo-spek Dec 20, 2024
a6a5838
Update strings.xml
jo-spek Dec 20, 2024
b0eb253
Update strings.xml
jo-spek Dec 20, 2024
cce58a5
Update strings.xml
jo-spek Dec 20, 2024
da4a801
Update strings.xml
jo-spek Dec 20, 2024
d28e429
Update strings.xml
jo-spek Dec 20, 2024
0e30070
Update strings.xml
jo-spek Dec 20, 2024
fddb470
Merge branch 'master' into sufy/2446/new-loi-flow
sufyanAbbasi Jan 16, 2025
6a64f25
Add missing import
sufyanAbbasi Jan 24, 2025
4466139
Merge branch 'sufy/2446/new-loi-flow' of https://github.com/google/gr…
sufyanAbbasi Jan 24, 2025
2705df0
Formatting
sufyanAbbasi Jan 24, 2025
818e354
Merge branch 'master' into sufy/2446/new-loi-flow
sufyanAbbasi Jan 24, 2025
c4be97e
Add tests for new job logic.
sufyanAbbasi Jan 27, 2025
ca71435
Merge branch 'master' into sufy/2446/new-loi-flow
sufyanAbbasi Feb 1, 2025
8cf9df9
Address review comments
sufyanAbbasi Feb 5, 2025
304e428
Merge branch 'master' into sufy/2446/new-loi-flow
sufyanAbbasi Feb 5, 2025
6eaf721
Fix survey activation in test
sufyanAbbasi Feb 5, 2025
73e2cea
Refactor ComposeView --> createComposeView, remove inner classes.
sufyanAbbasi Feb 5, 2025
3fdab5b
Merge branch 'master' into sufy/2446/new-loi-flow
sufyanAbbasi Feb 5, 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 @@ -19,23 +19,16 @@
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.runtime.Composable
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SnapHelper
import com.google.android.ground.R
import com.google.android.ground.coroutines.ApplicationScope
import com.google.android.ground.coroutines.IoDispatcher
import com.google.android.ground.coroutines.MainDispatcher
import com.google.android.ground.databinding.BasemapLayoutBinding
import com.google.android.ground.databinding.LoiCardsRecyclerViewBinding
import com.google.android.ground.databinding.MenuButtonBinding
import com.google.android.ground.model.locationofinterest.LOI_NAME_PROPERTY
import com.google.android.ground.model.locationofinterest.LocationOfInterest
import com.google.android.ground.proto.Survey.DataSharingTerms
import com.google.android.ground.repository.SubmissionRepository
import com.google.android.ground.repository.UserRepository
Expand All @@ -45,8 +38,10 @@
import com.google.android.ground.ui.home.DataSharingTermsDialog
import com.google.android.ground.ui.home.HomeScreenFragmentDirections
import com.google.android.ground.ui.home.HomeScreenViewModel
import com.google.android.ground.ui.home.mapcontainer.cards.MapCardAdapter
import com.google.android.ground.ui.home.mapcontainer.cards.MapCardUiData
import com.google.android.ground.ui.home.mapcontainer.jobs.AdHocDataCollectionButtonData
import com.google.android.ground.ui.home.mapcontainer.jobs.DataCollectionEntryPointData
import com.google.android.ground.ui.home.mapcontainer.jobs.JobMapComposables
import com.google.android.ground.ui.home.mapcontainer.jobs.SelectedLoiSheetData
import com.google.android.ground.ui.map.MapFragment
import com.google.android.ground.util.renderComposableDialog
import dagger.hilt.android.AndroidEntryPoint
Expand All @@ -57,7 +52,7 @@
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.runBlocking
import timber.log.Timber

/** Main app view, displaying the map and related controls (center cross-hairs, add button, etc). */
Expand All @@ -74,55 +69,57 @@
private lateinit var mapContainerViewModel: HomeScreenMapContainerViewModel
private lateinit var homeScreenViewModel: HomeScreenViewModel
private lateinit var binding: BasemapLayoutBinding
private lateinit var adapter: MapCardAdapter
private lateinit var jobMapComposables: JobMapComposables
private lateinit var infoPopup: EphemeralPopups.InfoPopup

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mapContainerViewModel = getViewModel(HomeScreenMapContainerViewModel::class.java)
homeScreenViewModel = getViewModel(HomeScreenViewModel::class.java)
adapter = MapCardAdapter { loi, view -> updateSubmissionCount(loi, view) }
jobMapComposables = JobMapComposables { loi ->
submissionRepository.getTotalSubmissionCount(loi)

Check warning on line 80 in app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt#L80

Added line #L80 was not covered by tests
}

launchWhenStarted {
val canUserSubmitData = userRepository.canUserSubmitData()

// Handle collect button clicks
adapter.setCollectDataListener { mapCardUiData ->
jobMapComposables.setCollectDataListener { mapUiData ->
val job =
lifecycleScope.launch {
mapContainerViewModel.activeSurveyDataSharingTermsFlow.cancellable().collectLatest {
hasDataSharingTerms ->
onCollectData(
canUserSubmitData,
hasValidTasks(mapCardUiData),
hasValidTasks(mapUiData),

Check warning on line 94 in app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt#L94

Added line #L94 was not covered by tests
hasDataSharingTerms,
mapCardUiData,
mapUiData,

Check warning on line 96 in app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt#L96

Added line #L96 was not covered by tests
)
}
}
job.cancel()
}

// Bind data for cards
mapContainerViewModel.getMapCardUiData().launchWhenStartedAndCollect { (mapCards, loiCount) ->
adapter.updateData(canUserSubmitData, mapCards, loiCount - 1)
mapContainerViewModel.processDataCollectionEntryPoints().launchWhenStartedAndCollect {
(loiCard, jobCards) ->

Check warning on line 105 in app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt#L105

Added line #L105 was not covered by tests
runBlocking { jobMapComposables.updateData(canUserSubmitData, loiCard, jobCards) }
}
}

map.featureClicks.launchWhenStartedAndCollect { mapContainerViewModel.onFeatureClicked(it) }
}

private fun hasValidTasks(cardUiData: MapCardUiData) =
private fun hasValidTasks(cardUiData: DataCollectionEntryPointData) =
when (cardUiData) {
// LOI tasks are filtered out of the tasks list for pre-defined tasks.
is MapCardUiData.LoiCardUiData ->
cardUiData.loi.job.tasks.values.count { !it.isAddLoiTask } > 0
is MapCardUiData.AddLoiCardUiData -> cardUiData.job.tasks.values.isNotEmpty()
is SelectedLoiSheetData -> cardUiData.loi.job.tasks.values.count { !it.isAddLoiTask } > 0
is AdHocDataCollectionButtonData -> cardUiData.job.tasks.values.isNotEmpty()
}

@Composable
private fun ShowDataSharingTermsDialog(
cardUiData: MapCardUiData,
cardUiData: DataCollectionEntryPointData,
dataSharingTerms: DataSharingTerms,
) {
DataSharingTermsDialog(dataSharingTerms) {
Expand All @@ -137,7 +134,7 @@
canUserSubmitData: Boolean,
hasTasks: Boolean,
hasDataSharingTerms: DataSharingTerms?,
cardUiData: MapCardUiData,
cardUiData: DataCollectionEntryPointData,
) {
if (!canUserSubmitData) {
// Skip data collection screen if the user can't submit any data
Expand Down Expand Up @@ -165,17 +162,6 @@
navigateToDataCollectionFragment(cardUiData)
}

/** Updates the given [TextView] with the submission count for the given [LocationOfInterest]. */
private fun updateSubmissionCount(loi: LocationOfInterest, view: TextView) {
externalScope.launch {
val count = submissionRepository.getTotalSubmissionCount(loi)
val submissionText =
if (count == 0) resources.getString(R.string.no_submissions)
else resources.getQuantityString(R.plurals.submission_count, count, count)
withContext(mainDispatcher) { view.text = submissionText }
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand All @@ -191,8 +177,19 @@

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupMenuFab()
setupBottomLoiCards()
val menuBinding = setupMenuFab()
val onOpen = {
binding.mapTypeBtn.hide()
binding.locationLockBtn.hide()
menuBinding.hamburgerBtn.hide()
}

Check warning on line 185 in app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerFragment.kt#L184-L185

Added lines #L184 - L185 were not covered by tests
val onDismiss = {
binding.mapTypeBtn.show()
binding.locationLockBtn.show()
menuBinding.hamburgerBtn.show()
}
jobMapComposables.render(binding.bottomContainer, onOpen, onDismiss)
binding.bottomContainer.bringToFront()
showDataCollectionHint()
}

Expand Down Expand Up @@ -237,54 +234,17 @@
}
}

private fun setupMenuFab() {
private fun setupMenuFab(): MenuButtonBinding {
val mapOverlay = binding.overlay
val menuBinding = MenuButtonBinding.inflate(layoutInflater, mapOverlay, true)
menuBinding.homeScreenViewModel = homeScreenViewModel
menuBinding.lifecycleOwner = this
return menuBinding
}

private fun setupBottomLoiCards() {
val container = binding.bottomContainer
val recyclerViewBinding = LoiCardsRecyclerViewBinding.inflate(layoutInflater, container, true)
val recyclerView = recyclerViewBinding.recyclerView
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
val lastVisiblePosition = layoutManager.findLastVisibleItemPosition()
val firstCompletelyVisiblePosition =
layoutManager.findFirstCompletelyVisibleItemPosition()
var midPosition = (firstVisiblePosition + lastVisiblePosition) / 2

// Focus the last card
if (firstCompletelyVisiblePosition > midPosition) {
midPosition = firstCompletelyVisiblePosition
}

adapter.focusItemAtIndex(midPosition)
}
}
)

val helper: SnapHelper = PagerSnapHelper()
helper.attachToRecyclerView(recyclerView)

mapContainerViewModel.loiClicks.launchWhenStartedAndCollect {
val index = it?.let { adapter.getIndex(it) } ?: -1
if (index != -1) {
recyclerView.scrollToPosition(index)
adapter.focusItemAtIndex(index)
}
}
}

private fun navigateToDataCollectionFragment(cardUiData: MapCardUiData) {
private fun navigateToDataCollectionFragment(cardUiData: DataCollectionEntryPointData) {
when (cardUiData) {
is MapCardUiData.LoiCardUiData ->
is SelectedLoiSheetData ->
findNavController()
.navigate(
HomeScreenFragmentDirections.actionHomeScreenFragmentToDataCollectionFragment(
Expand All @@ -296,7 +256,7 @@
"",
)
)
is MapCardUiData.AddLoiCardUiData ->
is AdHocDataCollectionButtonData ->
findNavController()
.navigate(
HomeScreenFragmentDirections.actionHomeScreenFragmentToDataCollectionFragment(
Expand All @@ -314,13 +274,7 @@
override fun onMapReady(map: MapFragment) {
mapContainerViewModel.mapLoiFeatures.launchWhenStartedAndCollect { map.setFeatures(it) }

adapter.setLoiCardFocusedListener {
when (it) {
is MapCardUiData.LoiCardUiData -> mapContainerViewModel.selectLocationOfInterest(it.loi.id)
is MapCardUiData.AddLoiCardUiData,
null -> mapContainerViewModel.selectLocationOfInterest(null)
}
}
jobMapComposables.setSelectedFeature { mapContainerViewModel.selectLocationOfInterest(it) }
}

override fun getMapViewModel(): BaseMapViewModel = mapContainerViewModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import com.google.android.ground.system.SettingsManager
import com.google.android.ground.ui.common.BaseMapViewModel
import com.google.android.ground.ui.common.SharedViewModel
import com.google.android.ground.ui.home.mapcontainer.cards.MapCardUiData
import com.google.android.ground.ui.home.mapcontainer.jobs.AdHocDataCollectionButtonData
import com.google.android.ground.ui.home.mapcontainer.jobs.DataCollectionEntryPointData
import com.google.android.ground.ui.home.mapcontainer.jobs.SelectedLoiSheetData
import com.google.android.ground.ui.map.Feature
import com.google.android.ground.ui.map.FeatureType
import com.google.android.ground.ui.map.isLocationOfInterest
Expand Down Expand Up @@ -110,8 +112,8 @@
*/
private val loisInViewport: StateFlow<List<LocationOfInterest>>

/** [LocationOfInterest] clicked by the user. */
val loiClicks: MutableStateFlow<LocationOfInterest?> = MutableStateFlow(null)
/** [Feature] clicked by the user. */
val featureClicked: MutableStateFlow<Feature?> = MutableStateFlow(null)

/**
* List of [Job]s which allow LOIs to be added during field collection, populated only when zoomed
Expand Down Expand Up @@ -178,15 +180,23 @@
}

/**
* Returns a flow of [MapCardUiData] associated with the active survey's LOIs and adhoc jobs for
* displaying the cards.
* Returns a flow of [DataCollectionEntryPointData] associated with the active survey's LOIs and
* adhoc jobs for displaying the cards.
*/
fun getMapCardUiData(): Flow<Pair<List<MapCardUiData>, Int>> =
loisInViewport.combine(adHocLoiJobs) { lois, jobs ->
val loiCards = lois.map { MapCardUiData.LoiCardUiData(it) }
val jobCards = jobs.map { MapCardUiData.AddLoiCardUiData(it) }

Pair(loiCards + jobCards, lois.size)
fun processDataCollectionEntryPoints():
Flow<Pair<SelectedLoiSheetData?, List<AdHocDataCollectionButtonData>>> =
combine(loisInViewport, featureClicked, adHocLoiJobs) { loisInView, feature, jobs ->
val loiCard =
loisInView
.filter { it.geometry == feature?.geometry }
.firstOrNull()
?.let { SelectedLoiSheetData(it) }
if (loiCard == null && feature != null) {
// The feature is not in view anymore.
featureClicked.value = null

Check warning on line 196 in app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt#L196

Added line #L196 was not covered by tests
}
val jobCard = jobs.map { AdHocDataCollectionButtonData(it) }
Pair(loiCard, jobCard)
}

private fun updatedLoiSelectedStates(
Expand All @@ -204,12 +214,7 @@
* list of provided features is empty.
*/
fun onFeatureClicked(features: Set<Feature>) {
val geometry = features.map { it.geometry }.minByOrNull { it.area } ?: return
for (loi in loisInViewport.value) {
if (loi.geometry == geometry) {
loiClicks.value = loi
}
}
featureClicked.value = features.minByOrNull { it.geometry.area }
}

suspend fun updateDataSharingConsent(dataSharingTerms: Boolean) {
Expand Down Expand Up @@ -242,5 +247,8 @@

fun selectLocationOfInterest(id: String?) {
selectedLoiIdFlow.value = id
if (id == null) {
featureClicked.value = null

Check warning on line 251 in app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/google/android/ground/ui/home/mapcontainer/HomeScreenMapContainerViewModel.kt#L251

Added line #L251 was not covered by tests
}
}
}
Loading