Skip to content
Open
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 @@ -20,66 +20,50 @@ import android.content.DialogInterface
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.View
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dagger.hilt.android.AndroidEntryPoint
import java.text.SimpleDateFormat
import java.util.Calendar
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import java.util.Date
import org.groundplatform.android.R
import org.groundplatform.android.databinding.DateTaskFragBinding
import org.groundplatform.android.model.submission.DateTimeTaskData
import org.groundplatform.android.ui.datacollection.components.TaskView
import org.groundplatform.android.ui.datacollection.components.TaskViewFactory
import org.groundplatform.android.ui.datacollection.tasks.AbstractTaskFragment
import org.groundplatform.android.util.createComposeView
import org.jetbrains.annotations.TestOnly

@AndroidEntryPoint
class DateTaskFragment : AbstractTaskFragment<DateTaskViewModel>() {

private var datePickerDialog: DatePickerDialog? = null

lateinit var dateText: LiveData<String>
lateinit var dateTextHint: LiveData<String>
override fun onCreateTaskView(inflater: LayoutInflater): TaskView =
TaskViewFactory.createWithHeader(inflater)

override fun onTaskViewAttached() {
super.onTaskViewAttached()
dateText =
viewModel.taskTaskData
.filterIsInstance<DateTimeTaskData?>()
.map { taskData ->
if (taskData != null) {
val calendar = Calendar.getInstance()
calendar.timeInMillis = taskData.timeInMillis
DateFormat.getDateFormat(requireContext()).format(calendar.time)
} else {
""
}
}
.asLiveData()
override fun onCreateTaskBody(inflater: LayoutInflater): View = createComposeView {
val taskData by viewModel.taskTaskData.collectAsStateWithLifecycle()
val context = LocalContext.current

dateTextHint =
MutableLiveData<String>().apply {
val dateFormat = DateFormat.getDateFormat(requireContext()) as SimpleDateFormat
val pattern = dateFormat.toPattern()
val hint = pattern.uppercase()
value = hint
val dateText =
remember(taskData) {
(taskData as? DateTimeTaskData)?.let {
DateFormat.getDateFormat(context).format(Date(it.timeInMillis))
} ?: ""
}
}

override fun onCreateTaskView(inflater: LayoutInflater): TaskView =
TaskViewFactory.createWithHeader(inflater)
val hintText = remember {
(DateFormat.getDateFormat(context) as SimpleDateFormat).toPattern().uppercase()
}

override fun onCreateTaskBody(inflater: LayoutInflater): View {
val taskBinding = DateTaskFragBinding.inflate(inflater)
taskBinding.lifecycleOwner = this
taskBinding.fragment = this
return taskBinding.root
DateTaskScreen(dateText = dateText, hintText = hintText, onDateClick = { showDateDialog() })
}

fun showDateDialog() {
// TODO: Replace with bottom modal date picker.
private fun showDateDialog() {
val calendar = Calendar.getInstance()
val year = calendar[Calendar.YEAR]
val month = calendar[Calendar.MONTH]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.datacollection.tasks.date

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport
import org.groundplatform.android.ui.theme.AppTheme

// TODO: Add trailing icon (close logo) for clearing selected date.

@Composable
fun DateTaskScreen(
dateText: String,
hintText: String,
onDateClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val interactionSource = remember { MutableInteractionSource() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
if (interaction is PressInteraction.Release) {
onDateClick()
}
}
}

Column(modifier = modifier) {
// TODO: Replace with simple text field.
OutlinedTextField(
value = dateText,
onValueChange = {},
readOnly = true,
placeholder = { Text(hintText) },
modifier = Modifier.width(200.dp).testTag("dateInputText"),
interactionSource = interactionSource,
)
}
Comment on lines +48 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation for handling clicks on the OutlinedTextField using MutableInteractionSource and LaunchedEffect is more complex than necessary for this use case. A more idiomatic and simpler approach in Jetpack Compose is to use the Modifier.clickable directly on the OutlinedTextField. This simplifies the code, improves readability, and removes unnecessary boilerplate.

  Column(modifier = modifier) {
    // TODO: Replace with simple text field.
    OutlinedTextField(
      value = dateText,
      onValueChange = {},
      readOnly = true,
      placeholder = { Text(hintText) },
      modifier = Modifier
        .width(200.dp)
        .testTag("dateInputText")
        .clickable { onDateClick() },
    )
  }

}

@Preview(showBackground = true)
@Composable
@ExcludeFromJacocoGeneratedReport
fun DateTaskScreenPreview() {
AppTheme {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
DateTaskScreen(dateText = "", hintText = "DD/MM/YYYY", onDateClick = {})
Spacer(modifier = Modifier.height(10.dp))
DateTaskScreen(dateText = "14/02/2026", hintText = "DD/MM/YYYY", onDateClick = {})
}
}
}
48 changes: 0 additions & 48 deletions app/src/main/res/layout/date_task_frag.xml

This file was deleted.

1 change: 0 additions & 1 deletion app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
<string name="tos_title">Términos de servicio</string>
<string name="agree_terms">Aceptar</string>
<string name="agree_checkbox">Estoy de acuerdo</string>
<string name="date">Fecha</string>
<string name="time">Hora</string>
<!-- Other strings (to be organized) -->
<string name="sync_status">Estado de sincronización de datos</string>
Expand Down
1 change: 0 additions & 1 deletion app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
<string name="tos_title">Conditions générales</string>
<string name="agree_terms">Accepter</string>
<string name="agree_checkbox">J&#8217;accepte</string>
<string name="date">Date</string>
<string name="time">l&#8217;heure</string>
<!-- Other strings (to be organized) -->
<string name="sync_status">État de synchronisation</string>
Expand Down
1 change: 0 additions & 1 deletion app/src/main/res/values-pt/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
<string name="tos_title">Termos de serviço</string>
<string name="agree_terms">Concordar</string>
<string name="agree_checkbox">Concordo</string>
<string name="date">Data</string>
<string name="time">Hora</string>
<!-- Other strings (to be organized) -->
<string name="sync_status">Estado da sincronização de dados</string>
Expand Down
1 change: 0 additions & 1 deletion app/src/main/res/values-vi/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
<string name="tos_title">Điều khoản dịch vụ</string>
<string name="agree_terms">Đồng ý</string>
<string name="agree_checkbox">Tôi đồng ý</string>
<string name="date">Ngày</string>
<string name="time">Thời gian</string>
<!-- Other strings (to be organized) -->
<string name="sync_status">Trạng thái đồng bộ dữ liệu</string>
Expand Down
1 change: 0 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
<string name="tos_title">Terms of service</string>
<string name="agree_terms">Agree</string>
<string name="agree_checkbox">I agree</string>
<string name="date">Date</string>
<string name="time">Time</string>
<!-- Other strings (to be organized) -->
<string name="sync_status">Data sync status</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,13 @@ import android.app.DatePickerDialog
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.isNotDisplayed
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.compose.ui.test.performClick
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidTest
Expand All @@ -46,9 +43,6 @@ import org.junit.runner.RunWith
import org.mockito.Mock
import org.robolectric.RobolectricTestRunner

// TODO: Add a test for selecting a date and verifying response.
// Issue URL: https://github.com/google/ground-android/issues/2134

@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
class DateTaskFragmentTest : BaseTaskFragmentTest<DateTaskFragment, DateTaskViewModel>() {
Expand Down Expand Up @@ -94,10 +88,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest<DateTaskFragment, DateTaskView
fun `default response is empty`() {
setupTaskFragment<DateTaskFragment>(job, task)

onView(withId(R.id.user_date_response_text))
.check(matches(withText("")))
.check(matches(isDisplayed()))
.check(matches(isEnabled()))
composeTestRule.onNodeWithTag("dateInputText").assertIsDisplayed().assertTextContains("M/D/YY")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hardcoding the expected hint text "M/D/YY" makes this test brittle as it depends on the default locale of the test environment. It's better to dynamically generate the expected hint string in the same way it's generated in the DateTaskFragment, using DateFormat.getDateFormat(context). This will make the test more robust and less likely to fail if the locale changes.

You will need to add the following imports:

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import java.text.SimpleDateFormat
import android.text.format.DateFormat
Suggested change
composeTestRule.onNodeWithTag("dateInputText").assertIsDisplayed().assertTextContains("M/D/YY")
val context = ApplicationProvider.getApplicationContext<Context>()
val expectedHint = (DateFormat.getDateFormat(context) as SimpleDateFormat).toPattern().uppercase()
composeTestRule.onNodeWithTag("dateInputText").assertIsDisplayed().assertTextContains(expectedHint)


runner().assertButtonIsDisabled("Next")
}
Expand All @@ -113,7 +104,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest<DateTaskFragment, DateTaskView
view?.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)

assertThat(fragment.getDatePickerDialog()).isNull()
onView(withId(R.id.user_date_response_text)).perform(click())
composeTestRule.onNodeWithTag("dateInputText").performClick()
assertThat(fragment.getDatePickerDialog()).isNotNull()
assertThat(fragment.getDatePickerDialog()?.isShowing).isTrue()
}
Expand All @@ -124,7 +115,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest<DateTaskFragment, DateTaskView

val view: View? = fragment.view?.findViewById(R.id.task_container)
view?.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)
onView(withId(R.id.user_date_response_text)).perform(click())
composeTestRule.onNodeWithTag("dateInputText").performClick()
assertThat(fragment.getDatePickerDialog()?.isShowing).isTrue()

val hardcodedYear = 2024
Expand All @@ -146,7 +137,7 @@ class DateTaskFragmentTest : BaseTaskFragmentTest<DateTaskFragment, DateTaskView

val view: View? = fragment.view?.findViewById(R.id.task_container)
view?.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1)
onView(withId(R.id.user_date_response_text)).perform(click())
composeTestRule.onNodeWithTag("dateInputText").performClick()
assertThat(fragment.getDatePickerDialog()?.isShowing).isTrue()

val hardcodedYear = 2024
Expand Down
Loading