Skip to content

Commit fd319be

Browse files
authored
[Jetsurvey] Updated date handling to use UTC only (#810)
This fixes #745. The answers to date questions now store the date in milliseconds and all calculations are based on UTC.
1 parent 17b6d32 commit fd319be

File tree

8 files changed

+133
-36
lines changed

8 files changed

+133
-36
lines changed

Jetsurvey/app/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,10 @@ dependencies {
114114
implementation Libs.Accompanist.permissions
115115

116116
implementation Libs.Coil.coilCompose
117+
118+
testImplementation Libs.junit
119+
testImplementation Libs.AndroidX.Test.core
120+
testImplementation Libs.AndroidX.Test.Ext.junit
121+
testImplementation Libs.AndroidX.Test.Ext.truth
122+
testImplementation Libs.Robolectric.robolectric
117123
}

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/Survey.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ data class Question(
4545
enum class SurveyActionType { PICK_DATE, TAKE_PHOTO, SELECT_CONTACT }
4646

4747
sealed class SurveyActionResult {
48-
data class Date(val date: String) : SurveyActionResult()
48+
data class Date(val dateMillis: Long) : SurveyActionResult()
4949
data class Photo(val uri: Uri) : SurveyActionResult()
5050
data class Contact(val contact: String) : SurveyActionResult()
5151
}

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,10 @@ class SurveyFragment : Fragment() {
9494
val picker = MaterialDatePicker.Builder.datePicker()
9595
.setSelection(date)
9696
.build()
97-
activity?.let {
98-
picker.show(it.supportFragmentManager, picker.toString())
99-
picker.addOnPositiveButtonClickListener {
100-
viewModel.onDatePicked(questionId, picker.selection)
97+
picker.show(requireActivity().supportFragmentManager, picker.toString())
98+
picker.addOnPositiveButtonClickListener {
99+
picker.selection?.let { selectedDate ->
100+
viewModel.onDatePicked(questionId, selectedDate)
101101
}
102102
}
103103
}

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyQuestions.kt

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
7777
import com.google.accompanist.permissions.MultiplePermissionsState
7878
import com.google.accompanist.permissions.rememberMultiplePermissionsState
7979
import java.text.SimpleDateFormat
80-
import java.util.Date
81-
import java.util.Locale
80+
import java.util.*
8281

8382
@OptIn(ExperimentalPermissionsApi::class)
8483
@Composable
@@ -642,18 +641,37 @@ private fun PhotoQuestion(
642641
}
643642
}
644643

644+
/**
645+
* Returns the start of today in milliseconds
646+
*/
647+
fun getDefaultDateInMillis(): Long {
648+
val cal = Calendar.getInstance()
649+
val year = cal.get(Calendar.YEAR)
650+
val month = cal.get(Calendar.MONTH)
651+
val date = cal.get(Calendar.DATE)
652+
cal.clear()
653+
cal.set(year, month, date)
654+
return cal.timeInMillis
655+
}
656+
645657
@Composable
646658
private fun DateQuestion(
647659
questionId: Int,
648660
answer: Answer.Action?,
649661
onAction: (Int, SurveyActionType) -> Unit,
650662
modifier: Modifier = Modifier
651663
) {
652-
val date = if (answer != null && answer.result is SurveyActionResult.Date) {
653-
answer.result.date
664+
val timestamp = if (answer != null && answer.result is SurveyActionResult.Date) {
665+
answer.result.dateMillis
654666
} else {
655-
SimpleDateFormat(simpleDateFormatPattern, Locale.getDefault()).format(Date())
667+
getDefaultDateInMillis()
656668
}
669+
670+
// All times are stored in UTC, so generate the display from UTC also
671+
val dateFormat = SimpleDateFormat(simpleDateFormatPattern, Locale.getDefault())
672+
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
673+
val dateString = dateFormat.format(timestamp)
674+
657675
Button(
658676
onClick = { onAction(questionId, SurveyActionType.PICK_DATE) },
659677
colors = ButtonDefaults.buttonColors(
@@ -669,7 +687,7 @@ private fun DateQuestion(
669687

670688
) {
671689
Text(
672-
text = date,
690+
text = dateString,
673691
modifier = Modifier
674692
.fillMaxWidth()
675693
.weight(1.8f)

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyRepository.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,22 @@ private val jetpackSurvey = Survey(
108108
questions = jetpackQuestions
109109
)
110110

111-
object SurveyRepository {
111+
object JetpackSurveyRepository : SurveyRepository {
112112

113-
suspend fun getSurvey() = jetpackSurvey
113+
override fun getSurvey() = jetpackSurvey
114114

115115
@Suppress("UNUSED_PARAMETER")
116-
fun getSurveyResult(answers: List<Answer<*>>): SurveyResult {
116+
override fun getSurveyResult(answers: List<Answer<*>>): SurveyResult {
117117
return SurveyResult(
118118
library = "Compose",
119119
result = R.string.survey_result,
120120
description = R.string.survey_result_description
121121
)
122122
}
123123
}
124+
125+
interface SurveyRepository {
126+
fun getSurvey(): Survey
127+
128+
fun getSurveyResult(answers: List<Answer<*>>): SurveyResult
129+
}

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyViewModel.kt

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ import androidx.lifecycle.MutableLiveData
2525
import androidx.lifecycle.ViewModel
2626
import androidx.lifecycle.ViewModelProvider
2727
import androidx.lifecycle.viewModelScope
28-
import java.text.SimpleDateFormat
29-
import java.util.Date
30-
import java.util.Locale
3128
import kotlinx.coroutines.launch
3229

3330
const val simpleDateFormatPattern = "EEE, MMM d"
@@ -76,13 +73,11 @@ class SurveyViewModel(
7673
_uiState.value = SurveyState.Result(surveyQuestions.surveyTitle, result)
7774
}
7875

79-
fun onDatePicked(questionId: Int, pickerSelection: Long?) {
80-
val selectedDate = Date().apply {
81-
time = pickerSelection ?: getCurrentDate(questionId)
82-
}
83-
val formattedDate =
84-
SimpleDateFormat(simpleDateFormatPattern, Locale.getDefault()).format(selectedDate)
85-
updateStateWithActionResult(questionId, SurveyActionResult.Date(formattedDate))
76+
fun onDatePicked(questionId: Int, pickerSelection: Long) {
77+
updateStateWithActionResult(
78+
questionId,
79+
SurveyActionResult.Date(pickerSelection)
80+
)
8681
}
8782

8883
fun getCurrentDate(questionId: Int): Long {
@@ -129,21 +124,17 @@ class SurveyViewModel(
129124

130125
private fun getSelectedDate(questionId: Int): Long {
131126
val latestState = _uiState.value
132-
var ret = Date().time
133127
if (latestState != null && latestState is SurveyState.Questions) {
134128
val question =
135129
latestState.questionsState.first { questionState ->
136130
questionState.question.id == questionId
137131
}
138-
val answer: Answer.Action? = question.answer as Answer.Action?
132+
val answer = question.answer as Answer.Action?
139133
if (answer != null && answer.result is SurveyActionResult.Date) {
140-
val formatter = SimpleDateFormat(simpleDateFormatPattern, Locale.ENGLISH)
141-
val formatted = formatter.parse(answer.result.date)
142-
if (formatted is Date)
143-
ret = formatted.time
134+
return answer.result.dateMillis
144135
}
145136
}
146-
return ret
137+
return getDefaultDateInMillis()
147138
}
148139
}
149140

@@ -153,7 +144,7 @@ class SurveyViewModelFactory(
153144
@Suppress("UNCHECKED_CAST")
154145
override fun <T : ViewModel> create(modelClass: Class<T>): T {
155146
if (modelClass.isAssignableFrom(SurveyViewModel::class.java)) {
156-
return SurveyViewModel(SurveyRepository, photoUriManager) as T
147+
return SurveyViewModel(JetpackSurveyRepository, photoUriManager) as T
157148
}
158149
throw IllegalArgumentException("Unknown ViewModel class")
159150
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2022 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.compose.jetsurvey.survey
18+
19+
import androidx.test.core.app.ApplicationProvider
20+
import androidx.test.ext.junit.runners.AndroidJUnit4
21+
import com.google.common.truth.Truth.assertThat
22+
import org.junit.Before
23+
import org.junit.Test
24+
import org.junit.runner.RunWith
25+
26+
@RunWith(AndroidJUnit4::class)
27+
class SurveyViewModelTest {
28+
29+
private lateinit var viewModel: SurveyViewModel
30+
31+
@Before
32+
fun setUp() {
33+
viewModel = SurveyViewModel(
34+
TestSurveyRepository(),
35+
PhotoUriManager(ApplicationProvider.getApplicationContext())
36+
)
37+
}
38+
39+
@Test
40+
fun onDatePicked_storesValueCorrectly() {
41+
// Select a date
42+
val initialDateMilliseconds = 0L
43+
viewModel.onDatePicked(dateQuestionId, initialDateMilliseconds)
44+
45+
// Get the stored date
46+
val newDateMilliseconds = viewModel.getCurrentDate(dateQuestionId)
47+
48+
// Verify they're identical
49+
assertThat(newDateMilliseconds).isEqualTo(initialDateMilliseconds)
50+
}
51+
}
52+
53+
const val dateQuestionId = 1
54+
class TestSurveyRepository : SurveyRepository {
55+
56+
private val testSurvey = Survey(
57+
title = -1,
58+
questions = listOf(
59+
Question(
60+
id = dateQuestionId,
61+
questionText = -1,
62+
answer = PossibleAnswer.Action(label = -1, SurveyActionType.PICK_DATE)
63+
)
64+
)
65+
)
66+
67+
override fun getSurvey() = testSurvey
68+
69+
override fun getSurveyResult(answers: List<Answer<*>>): SurveyResult {
70+
TODO("Not yet implemented")
71+
}
72+
}

Jetsurvey/buildSrc/src/main/java/com/example/compose/jetsurvey/buildsrc/dependencies.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ object Libs {
2424
const val androidGradlePlugin = "com.android.tools.build:gradle:7.2.0"
2525
const val jdkDesugar = "com.android.tools:desugar_jdk_libs:1.1.5"
2626

27-
const val junit = "junit:junit:4.13"
27+
const val junit = "junit:junit:4.13.2"
2828

2929
object Accompanist {
3030
const val version = "0.24.8-beta"
@@ -92,12 +92,12 @@ object Libs {
9292

9393
object Test {
9494
private const val version = "1.4.0"
95-
const val core = "androidx.test:core:$version"
95+
const val core = "androidx.test:core-ktx:$version"
9696
const val rules = "androidx.test:rules:$version"
9797

9898
object Ext {
99-
private const val version = "1.1.2"
100-
const val junit = "androidx.test.ext:junit-ktx:$version"
99+
const val junit = "androidx.test.ext:junit:1.1.3"
100+
const val truth = "androidx.test.ext:truth:1.4.0"
101101
}
102102

103103
const val espressoCore = "androidx.test.espresso:espresso-core:3.2.0"
@@ -107,4 +107,8 @@ object Libs {
107107
object Coil {
108108
const val coilCompose = "io.coil-kt:coil-compose:2.0.0"
109109
}
110+
111+
object Robolectric {
112+
const val robolectric = "org.robolectric:robolectric:4.5.1"
113+
}
110114
}

0 commit comments

Comments
 (0)