Skip to content

Commit b1d059b

Browse files
authored
Merge branch 'master' into anandwana001/2699/undo-redo-feature-draw
2 parents 45a764e + 3a809d1 commit b1d059b

20 files changed

Lines changed: 265 additions & 23 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@
7272

7373
<category android:name="android.intent.category.LAUNCHER" />
7474
</intent-filter>
75+
76+
<intent-filter android:autoVerify="true">
77+
<action android:name="android.intent.action.VIEW"/>
78+
<category android:name="android.intent.category.DEFAULT"/>
79+
<category android:name="android.intent.category.BROWSABLE"/>
80+
<data
81+
android:scheme="https"
82+
android:host="groundplatform.org"
83+
android:pathPrefix="/survey/" />
84+
</intent-filter>
7585
</activity>
7686
<activity
7787
android:name="org.groundplatform.android.SettingsActivity"

app/src/main/java/org/groundplatform/android/Config.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ object Config {
4444
*/
4545
const val CLUSTERING_ZOOM_THRESHOLD = 14f
4646

47+
/**
48+
* The path segment used in deep‑link URIs to identify the survey screen.
49+
*
50+
* When handling incoming deep links, compare the first segment of the URI’s path to this constant
51+
* to determine whether to navigate to the survey flow.
52+
*/
53+
const val SURVEY_PATH_SEGMENT = "survey"
54+
55+
/** Limit on the permitted character length for free text question responses. */
56+
const val TEXT_DATA_CHAR_LIMIT = 255
57+
4758
// TODO: Make sub-paths configurable and
4859
// stop hardcoding here.
4960
// Issue URL: https://github.com/google/ground-android/issues/1730

app/src/main/java/org/groundplatform/android/MainActivity.kt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package org.groundplatform.android
1717

1818
import android.app.AlertDialog
1919
import android.content.Intent
20+
import android.net.Uri
2021
import android.os.Bundle
2122
import androidx.activity.OnBackPressedCallback
2223
import androidx.activity.enableEdgeToEdge
@@ -27,12 +28,15 @@ import androidx.compose.runtime.remember
2728
import androidx.compose.runtime.setValue
2829
import androidx.core.view.WindowInsetsCompat
2930
import androidx.lifecycle.lifecycleScope
31+
import androidx.navigation.NavController
3032
import androidx.navigation.NavDirections
3133
import androidx.navigation.fragment.NavHostFragment
3234
import dagger.hilt.android.AndroidEntryPoint
3335
import javax.inject.Inject
3436
import kotlinx.coroutines.flow.filterNotNull
37+
import kotlinx.coroutines.flow.first
3538
import kotlinx.coroutines.launch
39+
import org.groundplatform.android.Config.SURVEY_PATH_SEGMENT
3640
import org.groundplatform.android.databinding.MainActBinding
3741
import org.groundplatform.android.repository.UserRepository
3842
import org.groundplatform.android.system.ActivityCallback
@@ -60,6 +64,8 @@ class MainActivity : AbstractActivity() {
6064

6165
private var signInProgressDialog: AlertDialog? = null
6266

67+
private var pendingDeepLink: Uri? = null
68+
6369
override fun onCreate(savedInstanceState: Bundle?) {
6470
// Make sure this is before calling super.onCreate()
6571
setTheme(R.style.AppTheme)
@@ -83,7 +89,19 @@ class MainActivity : AbstractActivity() {
8389

8490
viewModel = viewModelFactory[this, MainViewModel::class.java]
8591

86-
lifecycleScope.launch { viewModel.navigationRequests.filterNotNull().collect { updateUi(it) } }
92+
lifecycleScope.launch {
93+
viewModel.navigationRequests.filterNotNull().first()
94+
95+
intent.data?.let {
96+
if (navHostFragment.navController.currentDestination?.id != R.id.sign_in_fragment) {
97+
navHostFragment.navController.handleDeepLinkIfNeeded(it)
98+
} else {
99+
pendingDeepLink = it
100+
}
101+
}
102+
103+
viewModel.navigationRequests.filterNotNull().collect { updateUi(it) }
104+
}
87105

88106
onBackPressedDispatcher.addCallback(
89107
this,
@@ -234,4 +252,13 @@ class MainActivity : AbstractActivity() {
234252
navHostFragment.navController.navigate(directions)
235253
}
236254
}
255+
256+
private fun NavController.handleDeepLinkIfNeeded(uri: Uri) {
257+
if (uri.pathSegments.firstOrNull() == SURVEY_PATH_SEGMENT) {
258+
val surveyId = uri.lastPathSegment
259+
val action = SurveySelectorFragmentDirections.showSurveySelectorScreen(false)
260+
action.surveyId = surveyId
261+
navigate(action)
262+
}
263+
}
237264
}

app/src/main/java/org/groundplatform/android/ui/datacollection/components/TextTaskInput.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,23 @@
1515
*/
1616
package org.groundplatform.android.ui.datacollection.components
1717

18+
import androidx.compose.foundation.layout.Row
19+
import androidx.compose.foundation.layout.Spacer
1820
import androidx.compose.foundation.layout.fillMaxWidth
1921
import androidx.compose.foundation.layout.padding
2022
import androidx.compose.foundation.layout.wrapContentHeight
2123
import androidx.compose.foundation.text.KeyboardOptions
2224
import androidx.compose.material3.MaterialTheme
23-
import androidx.compose.material3.TextField
25+
import androidx.compose.material3.OutlinedTextField
26+
import androidx.compose.material3.Text
2427
import androidx.compose.runtime.Composable
2528
import androidx.compose.ui.Alignment
2629
import androidx.compose.ui.Modifier
2730
import androidx.compose.ui.text.input.KeyboardType
31+
import androidx.compose.ui.text.style.TextAlign
2832
import androidx.compose.ui.tooling.preview.Preview
2933
import androidx.compose.ui.unit.dp
34+
import org.groundplatform.android.Config
3035
import org.groundplatform.android.ExcludeFromJacocoGeneratedReport
3136
import org.groundplatform.android.ui.theme.AppTheme
3237

@@ -37,7 +42,14 @@ fun TextTaskInput(
3742
keyboardType: KeyboardType = KeyboardType.Text,
3843
valueChanged: (text: String) -> Unit = {},
3944
) {
40-
TextField(
45+
OutlinedTextField(
46+
supportingText = {
47+
Row(modifier = modifier.fillMaxWidth()) {
48+
Spacer(modifier = modifier.weight(1f))
49+
Text("${value.length} / ${Config.TEXT_DATA_CHAR_LIMIT}", textAlign = TextAlign.End)
50+
}
51+
},
52+
isError = value.length > Config.TEXT_DATA_CHAR_LIMIT,
4153
value = value,
4254
onValueChange = { valueChanged(it) },
4355
modifier =

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/AbstractTaskViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ open class AbstractTaskViewModel internal constructor() : AbstractViewModel() {
4444
/** Checks if the current value is valid and updates error value. */
4545
fun validate(): Int? = validate(task, taskTaskData.value)
4646

47+
/**
48+
* Performs input validation on the given [Task] and associated [TaskData].
49+
*
50+
* Returns an [Int] identifier for an error string if validation fails, returns null otherwise.
51+
* Subclasses may override this method to validate input data and display an error message to the
52+
* user.
53+
*/
4754
open fun validate(task: Task, taskData: TaskData?): Int? {
4855
// Empty response for a required task.
4956
if (task.isRequired && (taskData == null || taskData.isEmpty())) {

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceItemView.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ package org.groundplatform.android.ui.datacollection.tasks.multiplechoice
1717

1818
import androidx.compose.foundation.layout.Column
1919
import androidx.compose.foundation.layout.Row
20+
import androidx.compose.foundation.layout.Spacer
2021
import androidx.compose.foundation.layout.fillMaxWidth
2122
import androidx.compose.foundation.layout.padding
2223
import androidx.compose.foundation.text.ClickableText
2324
import androidx.compose.material3.Checkbox
2425
import androidx.compose.material3.HorizontalDivider
2526
import androidx.compose.material3.MaterialTheme
27+
import androidx.compose.material3.OutlinedTextField
2628
import androidx.compose.material3.RadioButton
27-
import androidx.compose.material3.TextField
29+
import androidx.compose.material3.Text
2830
import androidx.compose.runtime.Composable
2931
import androidx.compose.runtime.LaunchedEffect
3032
import androidx.compose.ui.Alignment
@@ -34,8 +36,10 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
3436
import androidx.compose.ui.platform.testTag
3537
import androidx.compose.ui.res.stringResource
3638
import androidx.compose.ui.text.AnnotatedString
39+
import androidx.compose.ui.text.style.TextAlign
3740
import androidx.compose.ui.tooling.preview.Preview
3841
import androidx.compose.ui.unit.dp
42+
import org.groundplatform.android.Config
3943
import org.groundplatform.android.ExcludeFromJacocoGeneratedReport
4044
import org.groundplatform.android.R
4145
import org.groundplatform.android.model.task.MultipleChoice
@@ -97,7 +101,17 @@ fun MultipleChoiceItemView(
97101

98102
if (item.isOtherOption) {
99103
Row(modifier = modifier.padding(horizontal = 48.dp)) {
100-
TextField(
104+
OutlinedTextField(
105+
supportingText = {
106+
Row(modifier = modifier.fillMaxWidth()) {
107+
Spacer(modifier = modifier.weight(1f))
108+
Text(
109+
"${item.otherText.length} / ${Config.TEXT_DATA_CHAR_LIMIT}",
110+
textAlign = TextAlign.End,
111+
)
112+
}
113+
},
114+
isError = item.otherText.length > Config.TEXT_DATA_CHAR_LIMIT,
101115
value = item.otherText,
102116
textStyle = MaterialTheme.typography.bodyLarge,
103117
onValueChange = { otherValueChanged(it) },

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskViewModel.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
2121
import kotlinx.coroutines.flow.asStateFlow
2222
import kotlinx.coroutines.flow.filterNotNull
2323
import kotlinx.coroutines.flow.update
24+
import org.groundplatform.android.Config
25+
import org.groundplatform.android.R
2426
import org.groundplatform.android.model.job.Job
2527
import org.groundplatform.android.model.submission.MultipleChoiceTaskData
2628
import org.groundplatform.android.model.submission.MultipleChoiceTaskData.Companion.fromList
@@ -57,6 +59,16 @@ class MultipleChoiceTaskViewModel @Inject constructor() : AbstractTaskViewModel(
5759
updateMultipleChoiceItems()
5860
}
5961

62+
override fun validate(task: Task, taskData: TaskData?): Int? {
63+
if (task.type != Task.Type.MULTIPLE_CHOICE || !selectedIds.contains(OTHER_ID))
64+
return super.validate(task, taskData)
65+
66+
if (otherText.length > Config.TEXT_DATA_CHAR_LIMIT)
67+
return R.string.text_task_data_character_limit
68+
69+
return super.validate(task, taskData)
70+
}
71+
6072
/**
6173
* Invoked when "other" text input field is modified. It updates the internal state with the new
6274
* text, if valid.

app/src/main/java/org/groundplatform/android/ui/datacollection/tasks/text/TextTaskViewModel.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,25 @@ import androidx.lifecycle.asLiveData
2020
import javax.inject.Inject
2121
import kotlinx.coroutines.flow.filterIsInstance
2222
import kotlinx.coroutines.flow.map
23+
import org.groundplatform.android.Config
24+
import org.groundplatform.android.R
2325
import org.groundplatform.android.model.submission.TaskData
2426
import org.groundplatform.android.model.submission.TextTaskData
27+
import org.groundplatform.android.model.task.Task
2528
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskViewModel
2629

2730
class TextTaskViewModel @Inject constructor() : AbstractTaskViewModel() {
2831

2932
/** Transcoded text to be displayed for the current [TaskData]. */
3033
val responseText: LiveData<String> =
3134
taskTaskData.filterIsInstance<TextTaskData?>().map { it?.text ?: "" }.asLiveData()
35+
36+
override fun validate(task: Task, taskData: TaskData?): Int? {
37+
if (task.type != Task.Type.TEXT) return super.validate(task, taskData)
38+
39+
if ((taskData as TextTaskData).text.length > Config.TEXT_DATA_CHAR_LIMIT)
40+
return R.string.text_task_data_character_limit
41+
42+
return super.validate(task, taskData)
43+
}
3244
}

app/src/main/java/org/groundplatform/android/ui/surveyselector/SurveySelectorFragment.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import android.view.View
2323
import android.view.ViewGroup
2424
import android.widget.PopupMenu
2525
import androidx.navigation.fragment.findNavController
26+
import androidx.navigation.fragment.navArgs
2627
import dagger.hilt.android.AndroidEntryPoint
2728
import javax.inject.Inject
2829
import org.groundplatform.android.R
@@ -45,11 +46,16 @@ class SurveySelectorFragment : AbstractFragment(), BackPressListener {
4546
private lateinit var binding: SurveySelectorFragBinding
4647
private lateinit var adapter: SurveyListAdapter
4748

49+
private val args: SurveySelectorFragmentArgs by navArgs()
50+
4851
override fun onCreate(savedInstanceState: Bundle?) {
4952
super.onCreate(savedInstanceState)
5053
viewModel = getViewModel(SurveySelectorViewModel::class.java)
5154
adapter = SurveyListAdapter(viewModel, this)
5255
viewModel.uiState.launchWhenStartedAndCollect { updateUi(it) }
56+
if (!args.surveyId.isNullOrBlank()) {
57+
viewModel.activateSurvey(args.surveyId!!)
58+
}
5359
}
5460

5561
private fun updateUi(uiState: UiState) {

app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceFragment.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import androidx.lifecycle.lifecycleScope
4949
import androidx.navigation.fragment.findNavController
5050
import dagger.hilt.android.AndroidEntryPoint
5151
import kotlinx.coroutines.launch
52+
import org.groundplatform.android.Config.SURVEY_PATH_SEGMENT
5253
import org.groundplatform.android.R
5354
import org.groundplatform.android.ui.common.AbstractFragment
5455
import org.groundplatform.android.ui.compose.Toolbar
@@ -139,9 +140,19 @@ class TermsOfServiceFragment : AbstractFragment() {
139140
super.onViewCreated(view, savedInstanceState)
140141
lifecycleScope.launch {
141142
viewModel.navigateToSurveySelector.collect {
142-
findNavController()
143-
.navigate(SurveySelectorFragmentDirections.showSurveySelectorScreen(true))
143+
activity?.intent?.data?.let { uri ->
144+
if (uri.pathSegments.firstOrNull() == SURVEY_PATH_SEGMENT) {
145+
val surveyId = uri.lastPathSegment
146+
openSurveySelector(surveyId)
147+
}
148+
} ?: run { openSurveySelector() }
144149
}
145150
}
146151
}
152+
153+
private fun openSurveySelector(surveyId: String? = null) {
154+
val action = SurveySelectorFragmentDirections.showSurveySelectorScreen(true)
155+
action.surveyId = surveyId
156+
findNavController().navigate(action)
157+
}
147158
}

0 commit comments

Comments
 (0)