From a9d95db49da6e99c3dbe06f3c81846b4db019059 Mon Sep 17 00:00:00 2001 From: santosh-pingle <86107848+santosh-pingle@users.noreply.github.com> Date: Wed, 22 Feb 2023 22:35:26 +0530 Subject: [PATCH 1/6] DateTime Picker : Do not enable time input if date textfield is empty, do not show error message if date input is valid. (#1871) * Do not enable time input if date textfield is empty, do not show error message id date input is valid. * Address review comments. * Address inline comments. * Avoid duplication of invalid date error text. * Address inline comment. * Address review comments. * address my own comments * BREAKS: rebind if draft answer changes * Fix build failure. * Rename test case and add one more assertion. * breaking: cleaned up date time picker * clear text on imporper answer * disable time at the start * use text in datebox to set datetime * set edti text in date * breaking: fix time disappering * cleaner code * init textwatcher once * dont clear answer in change text * simplify even more * remove unneessary method * fix tests * fix tests * final refactor * final final comment fixes * enable time picker on cal choosing date * add a UI test to check if the answer gets saved * Address comments * address comments and cleanup tests --------- Co-authored-by: Santosh Pingle Co-authored-by: omarismail --- .../component_date_time_picker.json | 30 ++ .../QuestionnaireUiEspressoTest.kt | 99 ++++++- ...TimePickerViewHolderFactoryEspressoTest.kt | 92 +----- ...ionnaireItemDatePickerViewHolderFactory.kt | 36 +-- ...aireItemDateTimePickerViewHolderFactory.kt | 280 ++++++------------ .../views/QuestionnaireItemViewItem.kt | 11 +- ...ItemDateTimePickerViewHolderFactoryTest.kt | 78 +++-- 7 files changed, 282 insertions(+), 344 deletions(-) create mode 100644 datacapture/sampledata/component_date_time_picker.json diff --git a/datacapture/sampledata/component_date_time_picker.json b/datacapture/sampledata/component_date_time_picker.json new file mode 100644 index 0000000000..1084fe3225 --- /dev/null +++ b/datacapture/sampledata/component_date_time_picker.json @@ -0,0 +1,30 @@ +{ + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "1", + "text": "Schedule an appointment", + "type": "dateTime", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-display-category", + "code": "instructions" + } + ] + } + } + ], + "linkId": "1-most-recent", + "text": "Select a date 4 weeks from now", + "type": "display" + } + ] + } + ] +} diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/QuestionnaireUiEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/QuestionnaireUiEspressoTest.kt index 7da5b72339..b5d551ac28 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/QuestionnaireUiEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/QuestionnaireUiEspressoTest.kt @@ -21,6 +21,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.add import androidx.fragment.app.commitNow import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers @@ -29,9 +30,13 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.android.fhir.datacapture.TestQuestionnaireFragment.Companion.QUESTIONNAIRE_FILE_PATH_KEY import com.google.android.fhir.datacapture.test.R +import com.google.android.fhir.datacapture.utilities.clickIcon import com.google.android.fhir.datacapture.utilities.clickOnText +import com.google.android.fhir.datacapture.views.localDateTime import com.google.android.material.textfield.TextInputLayout import com.google.common.truth.Truth.assertThat +import java.time.LocalDateTime +import org.hl7.fhir.r4.model.QuestionnaireResponse import org.junit.Before import org.junit.Rule import org.junit.Test @@ -54,16 +59,9 @@ class QuestionnaireUiEspressoTest { @Test fun shouldDisplayReviewButtonWhenNoMorePagesToDisplay() { - val bundle = - bundleOf(QUESTIONNAIRE_FILE_PATH_KEY to "/paginated_questionnaire_with_dependent_answer.json") - activityScenarioRule.scenario.onActivity { activity -> - activity.supportFragmentManager.commitNow { - setReorderingAllowed(true) - add(R.id.container_holder, args = bundle) - } - } + buildFragmentFromQuestionnaire("/paginated_questionnaire_with_dependent_answer.json") - onView(ViewMatchers.withId(R.id.review_mode_button)) + onView(withId(R.id.review_mode_button)) .check( ViewAssertions.matches( ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) @@ -71,13 +69,13 @@ class QuestionnaireUiEspressoTest { ) clickOnText("Yes") - onView(ViewMatchers.withId(R.id.review_mode_button)) + onView(withId(R.id.review_mode_button)) .check( ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.GONE)) ) clickOnText("No") - onView(ViewMatchers.withId(R.id.review_mode_button)) + onView(withId(R.id.review_mode_button)) .check( ViewAssertions.matches( ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) @@ -87,17 +85,86 @@ class QuestionnaireUiEspressoTest { @Test fun integerTextEdit_inputOutOfRange_shouldShowError() { - val bundle = bundleOf(QUESTIONNAIRE_FILE_PATH_KEY to "/text_questionnaire_integer.json") + buildFragmentFromQuestionnaire("/text_questionnaire_integer.json") + + onView(withId(R.id.text_input_edit_text)).perform(typeText("12345678901")) + onView(withId(R.id.text_input_layout)).check { view, _ -> + val actualError = (view as TextInputLayout).error + assertThat(actualError).isEqualTo("Number must be between -2,147,483,648 and 2,147,483,647") + } + } + + @Test + fun dateTimePicker_shouldShowErrorForWrongDate() { + buildFragmentFromQuestionnaire("/component_date_time_picker.json") + + // Add month and day. No need to add slashes as they are added automatically + onView(withId(R.id.date_input_edit_text)) + .perform(ViewActions.click()) + .perform(ViewActions.typeTextIntoFocusedView("0105")) + + onView(withId(R.id.date_input_layout)).check { view, _ -> + val actualError = (view as TextInputLayout).error + assertThat(actualError).isEqualTo("Date format needs to be MM/dd/yyyy (e.g. 01/31/2023)") + } + onView(withId(R.id.time_input_layout)).check { view, _ -> assertThat(view.isEnabled).isFalse() } + } + + @Test + fun dateTimePicker_shouldEnableTimePickerWithCorrectDate_butNotSaveInQuestionnaireResponse() { + buildFragmentFromQuestionnaire("/component_date_time_picker.json") + + onView(withId(R.id.date_input_edit_text)) + .perform(ViewActions.click()) + .perform(ViewActions.typeTextIntoFocusedView("01052005")) + + onView(withId(R.id.date_input_layout)).check { view, _ -> + val actualError = (view as TextInputLayout).error + assertThat(actualError).isEqualTo(null) + } + + onView(withId(R.id.time_input_layout)).check { view, _ -> assertThat(view.isEnabled).isTrue() } + + assertThat(getQuestionnaireResponse().item.size).isEqualTo(1) + assertThat(getQuestionnaireResponse().item.first().answer.size).isEqualTo(0) + } + + @Test + fun dateTimePicker_shouldSetAnswerWhenDateAndTimeAreFilled() { + buildFragmentFromQuestionnaire("/component_date_time_picker.json") + + onView(withId(R.id.date_input_edit_text)) + .perform(ViewActions.click()) + .perform(ViewActions.typeTextIntoFocusedView("01052005")) + + onView(withId(R.id.time_input_layout)).perform(clickIcon(true)) + clickOnText("AM") + clickOnText("6") + clickOnText("10") + clickOnText("OK") + + val answer = getQuestionnaireResponse().item.first().answer.first().valueDateTimeType + + assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 6, 10)) + } + + private fun buildFragmentFromQuestionnaire(fileName: String) { + val bundle = bundleOf(QUESTIONNAIRE_FILE_PATH_KEY to fileName) activityScenarioRule.scenario.onActivity { activity -> activity.supportFragmentManager.commitNow { setReorderingAllowed(true) add(R.id.container_holder, args = bundle) } } - onView(withId(R.id.text_input_edit_text)).perform(typeText("12345678901")) - onView(withId(R.id.text_input_layout)).check { view, _ -> - val actualError = (view as TextInputLayout).error - assertThat(actualError).isEqualTo("Number must be between -2,147,483,648 and 2,147,483,647") + } + private fun getQuestionnaireResponse(): QuestionnaireResponse { + var testQuestionnaireFragment: QuestionnaireFragment? = null + activityScenarioRule.scenario.onActivity { activity -> + testQuestionnaireFragment = + activity.supportFragmentManager + .findFragmentById(R.id.container_holder) + ?.childFragmentManager?.findFragmentById(R.id.container) as QuestionnaireFragment } + return testQuestionnaireFragment!!.getQuestionnaireResponse() } } diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest.kt index 7c0b5c879c..433e7ed78f 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest.kt @@ -18,7 +18,6 @@ package com.google.android.fhir.datacapture.views import android.view.View import android.widget.FrameLayout -import android.widget.TextView import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches @@ -33,7 +32,6 @@ import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.TestActivity import com.google.android.fhir.datacapture.utilities.clickIcon import com.google.android.fhir.datacapture.validation.NotValidated -import com.google.common.truth.Truth.assertThat import org.hamcrest.CoreMatchers.allOf import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -54,95 +52,11 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest { private lateinit var viewHolder: QuestionnaireItemViewHolder @Before fun setup() { - activityScenarioRule.getScenario().onActivity { activity -> parent = FrameLayout(activity) } + activityScenarioRule.scenario.onActivity { activity -> parent = FrameLayout(activity) } viewHolder = QuestionnaireItemDateTimePickerViewHolderFactory.create(parent) setTestLayout(viewHolder.itemView) } - @Test - fun shouldSetFirstDateInputThenTimeInput() { - val questionnaireItemView = - QuestionnaireItemViewItem( - Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, - QuestionnaireResponse.QuestionnaireResponseItemComponent(), - validationResult = NotValidated, - answersChangedCallback = { _, _, _, _ -> }, - ) - - runOnUI { viewHolder.bind(questionnaireItemView) } - - assertThat( - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text.toString() - ) - .isEmpty() - assertThat( - viewHolder.itemView.findViewById(R.id.time_input_edit_text).text.toString() - ) - .isEmpty() - - onView(withId(R.id.date_input_layout)).perform(clickIcon(true)) - onView(allOf(withText("OK"))) - .inRoot(isDialog()) - .check(matches(isDisplayed())) - .perform(ViewActions.click()) - onView(withId(R.id.time_input_layout)).perform(clickIcon(true)) - onView(allOf(withText("OK"))) - .inRoot(isDialog()) - .check(matches(isDisplayed())) - .perform(ViewActions.click()) - - assertThat( - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text.toString() - ) - .isNotEmpty() - assertThat( - viewHolder.itemView.findViewById(R.id.time_input_edit_text).text.toString() - ) - .isNotEmpty() - } - - @Test - fun shouldSetFirstTimeInputThenDateInput() { - val questionnaireItemView = - QuestionnaireItemViewItem( - Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, - QuestionnaireResponse.QuestionnaireResponseItemComponent(), - validationResult = NotValidated, - answersChangedCallback = { _, _, _, _ -> }, - ) - - runOnUI { viewHolder.bind(questionnaireItemView) } - - assertThat( - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text.toString() - ) - .isEmpty() - assertThat( - viewHolder.itemView.findViewById(R.id.time_input_edit_text).text.toString() - ) - .isEmpty() - - onView(withId(R.id.time_input_layout)).perform(clickIcon(true)) - onView(allOf(withText("OK"))) - .inRoot(isDialog()) - .check(matches(isDisplayed())) - .perform(ViewActions.click()) - onView(withId(R.id.date_input_layout)).perform(clickIcon(true)) - onView(allOf(withText("OK"))) - .inRoot(isDialog()) - .check(matches(isDisplayed())) - .perform(ViewActions.click()) - - assertThat( - viewHolder.itemView.findViewById(R.id.date_input_edit_text).text.toString() - ) - .isNotEmpty() - assertThat( - viewHolder.itemView.findViewById(R.id.time_input_edit_text).text.toString() - ) - .isNotEmpty() - } - @Test fun showsTimePickerInInputMode() { val questionnaireItemView = @@ -189,12 +103,12 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest { /** Method to run code snippet on UI/main thread */ private fun runOnUI(action: () -> Unit) { - activityScenarioRule.getScenario().onActivity { activity -> action() } + activityScenarioRule.scenario.onActivity { activity -> action() } } /** Method to set content view for test activity */ private fun setTestLayout(view: View) { - activityScenarioRule.getScenario().onActivity { activity -> activity.setContentView(view) } + activityScenarioRule.scenario.onActivity { activity -> activity.setContentView(view) } InstrumentationRegistry.getInstrumentation().waitForIdleSync() } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt index 757c58c3ce..74912c6ac2 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactory.kt @@ -17,6 +17,7 @@ package com.google.android.fhir.datacapture.views import android.annotation.SuppressLint +import android.content.Context import android.text.Editable import android.text.TextWatcher import android.view.View @@ -183,16 +184,7 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : } catch (e: ParseException) { displayValidationResult( Invalid( - listOf( - textInputEditText.context.getString( - R.string.date_format_validation_error_msg, - canonicalizedDatePattern, - canonicalizedDatePattern - .replace("dd", "31") - .replace("MM", "01") - .replace("yyyy", "2023") - ) - ) + listOf(invalidDateErrorText(textInputEditText.context, canonicalizedDatePattern)) ) ) if (questionnaireItemViewItem.answers.isNotEmpty()) { @@ -213,7 +205,7 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : val textToDisplayInTheTextField = answer?.format(canonicalizedDatePattern) ?: draftAnswerToDisplay - // Since pull request #1822 has been merged, the same date format style is now used for both + // The same date format style is now used for both // accepting user date input and displaying the answer in the text field. For instance, the // "MM/dd/yyyy" format is employed to accept and display the date value. As a result, it is // possible to simply compare @@ -228,16 +220,7 @@ internal object QuestionnaireItemDatePickerViewHolderFactory : if (!draftAnswerToDisplay.isNullOrBlank()) { displayValidationResult( Invalid( - listOf( - textInputEditText.context.getString( - R.string.date_format_validation_error_msg, - canonicalizedDatePattern, - canonicalizedDatePattern - .replace("dd", "31") - .replace("MM", "01") - .replace("yyyy", "2023") - ) - ) + listOf(invalidDateErrorText(textInputEditText.context, canonicalizedDatePattern)) ) ) } else { @@ -351,3 +334,14 @@ internal fun Int.length() = 0 -> 1 else -> log10(abs(toDouble())).toInt() + 1 } + +/** + * Replaces 'dd' with '31', 'MM' with '01' and 'yyyy' with '2023' and returns new string. For + * example, given a `formatPattern` of dd/MM/yyyy, returns 31/01/2023 + */ +internal fun invalidDateErrorText(context: Context, formatPattern: String) = + context.getString( + R.string.date_format_validation_error_msg, + formatPattern, + formatPattern.replace("dd", "31").replace("MM", "01").replace("yyyy", "2023") + ) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactory.kt index 9ca6abb52c..374f59429e 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactory.kt @@ -30,7 +30,6 @@ import com.google.android.fhir.datacapture.utilities.format import com.google.android.fhir.datacapture.utilities.getDateSeparator import com.google.android.fhir.datacapture.utilities.parseDate import com.google.android.fhir.datacapture.utilities.toLocalizedString -import com.google.android.fhir.datacapture.utilities.toLocalizedTimeString import com.google.android.fhir.datacapture.utilities.tryUnwrapContext import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.NotValidated @@ -62,8 +61,6 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : private lateinit var timeInputLayout: TextInputLayout private lateinit var timeInputEditText: TextInputEditText override lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem - private var localDate: LocalDate? = null - private var localTime: LocalTime? = null private lateinit var canonicalizedDatePattern: String private lateinit var textWatcher: DatePatternTextWatcher @@ -84,19 +81,13 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : // from the view's context. val context = itemView.context.tryUnwrapContext()!! val localDateInput = - localDate - ?: questionnaireItemViewItem.answers.singleOrNull()?.valueDateTimeType?.localDate - createMaterialDatePicker(localDateInput) + questionnaireItemViewItem.answers.singleOrNull()?.valueDateTimeType?.localDate + buildMaterialDatePicker(localDateInput) .apply { addOnPositiveButtonClickListener { epochMilli -> with(Instant.ofEpochMilli(epochMilli).atZone(ZONE_ID_UTC).toLocalDate()) { - localDate = this - dateInputEditText.setText(localDate?.format(canonicalizedDatePattern)) - enableOrDisableTimePicker(enableIt = true) - generateLocalDateTime(this, localTime)?.let { - updateDateTimeInput(it, canonicalizedDatePattern) - updateDateTimeAnswer(it) - } + dateInputEditText.setText(this?.format(canonicalizedDatePattern)) + timeInputLayout.isEnabled = true } // Clear focus so that the user can refocus to open the dialog dateInputEditText.clearFocus() @@ -108,56 +99,57 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : timeInputLayout = itemView.findViewById(R.id.time_input_layout) timeInputEditText = itemView.findViewById(R.id.time_input_edit_text) timeInputEditText.inputType = InputType.TYPE_NULL + timeInputLayout.isEnabled = false timeInputLayout.setEndIconOnClickListener { // The application is wrapped in a ContextThemeWrapper in QuestionnaireFragment // and again in TextInputEditText during layout inflation. As a result, it is // necessary to access the base context twice to retrieve the application object // from the view's context. val context = itemView.context.tryUnwrapContext()!! - showMaterialTimePicker(context, INPUT_MODE_CLOCK) + buildMaterialTimePicker(context, INPUT_MODE_CLOCK) } timeInputEditText.setOnClickListener { - showMaterialTimePicker(itemView.context, INPUT_MODE_KEYBOARD) + buildMaterialTimePicker(itemView.context, INPUT_MODE_KEYBOARD) } + val localeDatePattern = getLocalizedDateTimePattern() + // Special character used in date pattern + val datePatternSeparator = getDateSeparator(localeDatePattern) + textWatcher = DatePatternTextWatcher(datePatternSeparator) + canonicalizedDatePattern = canonicalizeDatePattern(localeDatePattern) } @SuppressLint("NewApi") // java.time APIs can be used due to desugaring override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) { clearPreviousState() header.bind(questionnaireItemViewItem.questionnaireItem) - val localeDatePattern = getLocalizedDateTimePattern() - // Special character used in date pattern - val datePatternSeparator = getDateSeparator(localeDatePattern) - textWatcher = DatePatternTextWatcher(datePatternSeparator) - canonicalizedDatePattern = canonicalizeDatePattern(localeDatePattern) dateInputLayout.hint = canonicalizedDatePattern dateInputEditText.removeTextChangedListener(textWatcher) - val dateTime = questionnaireItemViewItem.answers.singleOrNull()?.valueDateTimeType - updateDateTimeInput( - dateTime?.let { - it.localDateTime.also { - localDate = it.toLocalDate() - localTime = it.toLocalTime() - } - }, - canonicalizedDatePattern - ) - dateInputEditText.addTextChangedListener(textWatcher) - displayValidationResult(questionnaireItemViewItem.validationResult) - } + val questionnaireItemViewItemDateTimeAnswer = + questionnaireItemViewItem.answers.singleOrNull()?.valueDateTimeType?.localDateTime + + val dateStringToDisplay = + questionnaireItemViewItemDateTimeAnswer?.toLocalDate()?.format(canonicalizedDatePattern) + ?: questionnaireItemViewItem.draftAnswer as? String + + // Determine whether the text field text should be overridden or not. + if (dateInputEditText.text.toString() != dateStringToDisplay) { + dateInputEditText.setText(dateStringToDisplay) + } + + enableOrDisableTimePicker(questionnaireItemViewItem, dateStringToDisplay) - private fun displayValidationResult(validationResult: ValidationResult) { - displayDateValidationError(validationResult) - displayTimeValidationError(validationResult) + // If there is no set answer in the QuestionnaireItemViewItem, make the time field empty. + timeInputEditText.setText( + questionnaireItemViewItemDateTimeAnswer + ?.toLocalTime() + ?.toLocalizedString(timeInputEditText.context) + ?: "" + ) + dateInputEditText.addTextChangedListener(textWatcher) } private fun displayDateValidationError(validationResult: ValidationResult) { - // Since the draft answer is still displayed in the text field, do not erase the error - // text if the answer is cleared and the validation result is valid. - if (questionnaireItemViewItem.answers.isEmpty() && validationResult == Valid) { - return - } dateInputLayout.error = when (validationResult) { is NotValidated, @@ -166,118 +158,18 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : } } - fun displayTimeValidationError(validationResult: ValidationResult) { - timeInputLayout.error = - when (validationResult) { - is NotValidated, - Valid -> null - is Invalid -> - if (timeInputLayout.isEnabled) { - validationResult.getSingleStringValidationMessage() - } else { - null - } - } - } - override fun setReadOnly(isReadOnly: Boolean) { // The system outside this delegate should only be able to mark it read only. Otherwise, it // will change the state set by this delegate in bindView(). if (isReadOnly) { - setReadOnlyInternal(isReadOnly = true) - } - } - - private fun setReadOnlyInternal(isReadOnly: Boolean) { - timeInputEditText.isEnabled = !isReadOnly - dateInputEditText.isEnabled = !isReadOnly - timeInputLayout.isEnabled = !isReadOnly - dateInputLayout.isEnabled = !isReadOnly - } - - /** Update the date and time input fields in the UI. */ - private fun updateDateTimeInput( - localDateTime: LocalDateTime?, - canonicalizedDatePattern: String - ) { - enableOrDisableTimePicker(enableIt = localDateTime != null) - val draftAnswerToDisplay = questionnaireItemViewItem.draftAnswer as? String - val textToDisplayInTheTextField = - localDateTime?.toLocalDate()?.format(canonicalizedDatePattern) ?: draftAnswerToDisplay - - // Since pull request #1822 has been merged, the same date format style is now used for both - // accepting user date input and displaying the answer in the text field. For instance, the - // "MM/dd/yyyy" format is employed to accept and display the date value. As a result, it is - // possible to simply compare the - // text field text to the partial or valid answer to determine whether the text field text - // should be overridden or not. - if (dateInputEditText.text.toString() != textToDisplayInTheTextField) { - dateInputEditText.setText(textToDisplayInTheTextField) - displayDateValidationError(Valid) - enableOrDisableTimePicker(enableIt = true) - } - // Show an error text - if (!draftAnswerToDisplay.isNullOrBlank()) { - displayValidationResult( - Invalid( - listOf( - dateInputEditText.context.getString( - R.string.date_format_validation_error_msg, - canonicalizedDatePattern, - canonicalizedDatePattern - .replace("dd", "01") - .replace("MM", "01") - .replace("yyyy", "2023") - ) - ) - ) - ) - } else { - displayValidationResult(NotValidated) - } - - timeInputEditText.setText( - localDateTime?.toLocalizedTimeString(timeInputEditText.context) ?: "" - ) - } - - /** Updates the recorded answer. */ - private fun updateDateTimeAnswer(localDateTime: LocalDateTime) { - questionnaireItemViewItem.setAnswer( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() - .setValue( - DateTimeType( - Date( - localDateTime.year - 1900, - localDateTime.monthValue - 1, - localDateTime.dayOfMonth, - localDateTime.hour, - localDateTime.minute, - localDateTime.second - ) - ) - ) - ) - } - - private fun generateLocalDateTime( - localDate: LocalDate?, - localTime: LocalTime? - ): LocalDateTime? { - return when { - localDate != null && localTime != null -> { - LocalDateTime.of(localDate, localTime) - } - localDate != null -> { - questionnaireItemViewItem.answers.singleOrNull()?.valueDateTimeType?.let { - LocalDateTime.of(localDate, it.localTime) - } - } - else -> null + dateInputEditText.isEnabled = false + dateInputLayout.isEnabled = false + timeInputEditText.isEnabled = false + timeInputLayout.isEnabled = false } } - private fun createMaterialDatePicker(localDate: LocalDate?): MaterialDatePicker { + private fun buildMaterialDatePicker(localDate: LocalDate?): MaterialDatePicker { val selectedDateMillis = localDate?.atStartOfDay(ZONE_ID_UTC)?.toInstant()?.toEpochMilli() ?: MaterialDatePicker.todayInUtcMilliseconds() @@ -288,17 +180,7 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : .build() } - private fun clearPreviousState() { - localDate = null - localTime = null - setReadOnlyInternal(isReadOnly = false) - } - - private fun enableOrDisableTimePicker(enableIt: Boolean) { - timeInputLayout.isEnabled = enableIt - } - - private fun showMaterialTimePicker(context: Context, inputMode: Int) { + private fun buildMaterialTimePicker(context: Context, inputMode: Int) { val selectedTime = questionnaireItemViewItem.answers.singleOrNull()?.valueDateTimeType?.localTime ?: LocalTime.now() @@ -318,12 +200,13 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : .apply { addOnPositiveButtonClickListener { with(LocalTime.of(this.hour, this.minute, 0)) { - localTime = this timeInputEditText.setText(this.toLocalizedString(context)) - generateLocalDateTime(localDate, this)?.let { - updateDateTimeInput(it, canonicalizedDatePattern) - updateDateTimeAnswer(it) - } + setQuestionnaireItemViewItemAnswer( + LocalDateTime.of( + parseDate(dateInputEditText.text.toString(), canonicalizedDatePattern), + this + ) + ) timeInputEditText.clearFocus() } } @@ -331,47 +214,51 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : .show(context.tryUnwrapContext()!!.supportFragmentManager, TAG_TIME_PICKER) } - private fun updateAnswerAfterTextChanged(text: String?) { - if (text.isNullOrEmpty()) { - questionnaireItemViewItem.clearAnswer() - questionnaireItemViewItem.setDraftAnswer(text.toString()) - return - } + /** Set the answer in the [QuestionnaireResponse]. */ + private fun setQuestionnaireItemViewItemAnswer(localDateTime: LocalDateTime) { + questionnaireItemViewItem.setAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue( + DateTimeType( + Date( + localDateTime.year - 1900, + localDateTime.monthValue - 1, + localDateTime.dayOfMonth, + localDateTime.hour, + localDateTime.minute, + localDateTime.second + ) + ) + ) + ) + } + + private fun clearPreviousState() { + dateInputEditText.isEnabled = true + dateInputLayout.isEnabled = true + } + + /* If the passed in date can be parsed, then enable the time picker, otherwise, keep the time + picker disabled and display an error + */ + private fun enableOrDisableTimePicker( + questionnaireItemViewItem: QuestionnaireItemViewItem, + dateToDisplay: String? + ) = try { - localDate = parseDate(text, canonicalizedDatePattern) - questionnaireItemViewItem.setDraftAnswer(text.toString()) - displayDateValidationError(Valid) - enableOrDisableTimePicker(enableIt = true) - generateLocalDateTime(localDate, localTime)?.run { - updateDateTimeInput(this, canonicalizedDatePattern) - updateDateTimeAnswer(this) + if (dateToDisplay != null) { + parseDate(dateToDisplay, canonicalizedDatePattern) + timeInputLayout.isEnabled = true } + displayDateValidationError(questionnaireItemViewItem.validationResult) } catch (e: ParseException) { + timeInputLayout.isEnabled = false displayDateValidationError( Invalid( - listOf( - dateInputEditText.context.getString( - R.string.date_format_validation_error_msg, - canonicalizedDatePattern, - canonicalizedDatePattern - .replace("dd", "31") - .replace("MM", "01") - .replace("yyyy", "2023") - ) - ) + listOf(invalidDateErrorText(dateInputEditText.context, canonicalizedDatePattern)) ) ) - if (!timeInputLayout.isEnabled) { - displayTimeValidationError(Valid) - } - if (questionnaireItemViewItem.answers.isNotEmpty()) { - questionnaireItemViewItem.clearAnswer() - } - questionnaireItemViewItem.setDraftAnswer(text.toString()) - localDate = null - enableOrDisableTimePicker(enableIt = false) } - } /** Automatically appends date separator (e.g. "/") during date input. */ inner class DatePatternTextWatcher(private val datePatternSeparator: Char) : TextWatcher { @@ -400,7 +287,8 @@ internal object QuestionnaireItemDateTimePickerViewHolderFactory : datePatternSeparator, isDeleting ) - updateAnswerAfterTextChanged(editable.toString()) + // Always set the draft answer because time is not input yet + questionnaireItemViewItem.setDraftAnswer(editable.toString()) } } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewItem.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewItem.kt index 0dbadd7fb6..986a46f144 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewItem.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemViewItem.kt @@ -96,7 +96,7 @@ data class QuestionnaireItemViewItem( val answers: List = questionnaireResponseItem.answer.map { it.copy() } - /** Updates the answers. This will override any existing answers. */ + /** Updates the answers. This will override any existing answers and removes the draft answer. */ fun setAnswer( vararg questionnaireResponseItemAnswerComponent: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent @@ -112,12 +112,12 @@ data class QuestionnaireItemViewItem( ) } - /** Clears existing answers. */ + /** Clears existing answers and any draft answer. */ fun clearAnswer() { answersChangedCallback(questionnaireItem, questionnaireResponseItem, listOf(), null) } - /** Adds an answer to the existing answers. */ + /** Adds an answer to the existing answers and removes the draft answer. */ internal fun addAnswer( questionnaireResponseItemAnswerComponent: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent @@ -133,7 +133,7 @@ data class QuestionnaireItemViewItem( ) } - /** Removes an answer from the existing answers. */ + /** Removes an answer from the existing answers, as well as any draft answer. */ internal fun removeAnswer( questionnaireResponseItemAnswerComponent: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent @@ -224,7 +224,8 @@ data class QuestionnaireItemViewItem( otherAnswer.value != null && answer.value.equalsShallow(otherAnswer.value) } - .all { it } + .all { it } && + draftAnswer == other.draftAnswer /** * Returns whether this [QuestionnaireItemViewItem] and the `other` [QuestionnaireItemViewItem] diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryTest.kt index 08960e808c..fc0de94c58 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryTest.kt @@ -100,7 +100,7 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { @Test fun `parse date text input in US locale`() { - var answers: List? = null + var draftAnswer: Any? = null val itemViewItem = QuestionnaireItemViewItem( Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, @@ -110,22 +110,18 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { .setValue(DateTimeType(Date(2020 - 1900, 1, 5, 1, 30, 0))) ), validationResult = NotValidated, - answersChangedCallback = { _, _, result, _ -> answers = result }, + answersChangedCallback = { _, _, _, result -> draftAnswer = result }, ) viewHolder.bind(itemViewItem) viewHolder.dateInputView.text = "11/19/2020" - - val answer = answers!!.single().value as DateTimeType - assertThat(answer.day).isEqualTo(19) - assertThat(answer.month).isEqualTo(10) - assertThat(answer.year).isEqualTo(2020) + assertThat(draftAnswer as String).isEqualTo("11/19/2020") } @Test fun `parse date text input in Japan locale`() { Locale.setDefault(Locale.JAPAN) - var answers: List? = null + var draftAnswer: Any? = null val itemViewItem = QuestionnaireItemViewItem( Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, @@ -135,16 +131,13 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { .setValue(DateTimeType(Date(2020 - 1900, 1, 5, 1, 30, 0))) ), validationResult = NotValidated, - answersChangedCallback = { _, _, result, _ -> answers = result }, + answersChangedCallback = { _, _, _, result -> draftAnswer = result }, ) viewHolder.bind(itemViewItem) viewHolder.dateInputView.text = "2020/11/19" - val answer = answers!!.single().value as DateTimeType - assertThat(answer.day).isEqualTo(19) - assertThat(answer.month).isEqualTo(10) - assertThat(answer.year).isEqualTo(2020) + assertThat(draftAnswer as String).isEqualTo("2020/11/19") } @Test @@ -293,34 +286,68 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { } @Test - fun `if date input is invalid then do not enable time text input layout`() { + fun `if draft answer input is invalid then do not enable time text input layout`() { val itemViewItem = QuestionnaireItemViewItem( Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, + draftAnswer = "11/19/" ) viewHolder.bind(itemViewItem) - viewHolder.dateInputView.text = "11/19/" assertThat(viewHolder.itemView.findViewById(R.id.time_input_layout).isEnabled) .isFalse() } @Test - fun `if date input is valid then enable time text input layout`() { + fun `if the draft answer input is empty, do not enable the time text input layout`() { val itemViewItem = QuestionnaireItemViewItem( Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, + draftAnswer = "" + ) + + viewHolder.bind(itemViewItem) + + assertThat(viewHolder.itemView.findViewById(R.id.time_input_layout).isEnabled) + .isFalse() + } + + @Test + fun `if there is no answer or draft answer, do not enable the time text input layout`() { + val itemViewItem = + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + draftAnswer = null + ) + + viewHolder.bind(itemViewItem) + + assertThat(viewHolder.itemView.findViewById(R.id.time_input_layout).isEnabled) + .isFalse() + } + + @Test + fun `if date draft answer is valid then enable time text input layout`() { + val itemViewItem = + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + draftAnswer = "11/19/2020" ) viewHolder.bind(itemViewItem) - viewHolder.dateInputView.text = "11/19/2020" assertThat(viewHolder.itemView.findViewById(R.id.time_input_layout).isEnabled) .isTrue() @@ -372,6 +399,23 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryTest { .isNull() } + @Test + fun `if the draft answer is invalid, display the error message`() { + val itemViewItem = + QuestionnaireItemViewItem( + Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + draftAnswer = "11/19/202" + ) + + viewHolder.bind(itemViewItem) + + assertThat(viewHolder.itemView.findViewById(R.id.date_input_layout).error) + .isEqualTo("Date format needs to be MM/dd/yyyy (e.g. 01/31/2023)") + } + @Test fun `hides error textview in the header`() { viewHolder.bind( From b80747ac5b9295ff710ad017999c2f895a72b2a8 Mon Sep 17 00:00:00 2001 From: Jing Tang Date: Wed, 22 Feb 2023 20:02:16 +0000 Subject: [PATCH 2/6] Create nested items correctly (#1881) * Create nested items correctly * Revert incorrect questionnaire change * Update MoreQuestionnaireItemComponents.kt * Re-enable test case * Update nit * Update doc for zipByLinkId * Update datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt Co-authored-by: Omar Ismail <44980219+omarismail94@users.noreply.github.com> * Update doc in QuestionnaireViewModel * Add another level to the test for nested items --------- Co-authored-by: Omar Ismail <44980219+omarismail94@users.noreply.github.com> --- .../MoreQuestionnaireItemComponents.kt | 63 +++--- ...oreQuestionnairesResponseItemComponents.kt | 14 ++ .../datacapture/QuestionnaireViewModel.kt | 132 +++++++----- .../ValueConstraintExtensionValidator.kt | 2 +- ...QuestionnaireItemGroupViewHolderFactory.kt | 2 + .../datacapture/QuestionnaireViewModelTest.kt | 190 +++++------------- 6 files changed, 178 insertions(+), 225 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt index 14a10aef0c..199129a514 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnaireItemComponents.kt @@ -284,14 +284,6 @@ internal val Questionnaire.QuestionnaireItemComponent.isHelpCode: Boolean } } } - -/** - * Whether the corresponding [QuestionnaireResponse.QuestionnaireResponseItemComponent] should have - * nested items within [QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent](s). - */ -internal val Questionnaire.QuestionnaireItemComponent.hasNestedItemsWithinAnswers: Boolean - get() = item.isNotEmpty() && type != Questionnaire.QuestionnaireItemType.GROUP - /** Converts Text with HTML Tag to formated text. */ private fun String.toSpanned(): Spanned { return HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_COMPACT) @@ -424,6 +416,33 @@ internal val Questionnaire.QuestionnaireItemComponent.sliderStepValue: Int? return null } +/** + * Whether the corresponding [QuestionnaireResponse.QuestionnaireResponseItemComponent] should have + * [QuestionnaireResponse.QuestionnaireResponseItemComponent]s nested under + * [QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent]s. + * + * This is true for the following two cases: + * 1. Questions with nested items + * 2. Repeated groups with nested items (Note that this is how repeated groups are organized in the + * [QuestionnaireViewModel], and that they will be flattened in the final [QuestionnaireResponse].) + * + * Non-repeated groups should have child items nested directly under the group itself. + * + * For background, see https://build.fhir.org/questionnaireresponse.html#link. + */ +internal val Questionnaire.QuestionnaireItemComponent.shouldHaveNestedItemsUnderAnswers: Boolean + get() = item.isNotEmpty() && (type != Questionnaire.QuestionnaireItemType.GROUP || !repeats) + +/** + * Creates a list of [QuestionnaireResponse.QuestionnaireResponseItemComponent]s from the nested + * items in the [Questionnaire.QuestionnaireItemComponent]. + * + * The hierarchy and order of child items will be retained as specified in the standard. See + * https://www.hl7.org/fhir/questionnaireresponse.html#notes for more details. + */ +fun Questionnaire.QuestionnaireItemComponent.getNestedQuestionnaireResponseItems() = + item.map { it.createQuestionnaireResponseItem() } + /** * Creates a [QuestionnaireResponse.QuestionnaireResponseItemComponent] from the provided * [Questionnaire.QuestionnaireItemComponent]. @@ -436,10 +455,10 @@ fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponseItem(): return QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = this@createQuestionnaireResponseItem.linkId answer = createQuestionnaireResponseItemAnswers() - if (hasNestedItemsWithinAnswers && answer.isNotEmpty()) { + if (shouldHaveNestedItemsUnderAnswers && answer.isNotEmpty()) { this.addNestedItemsToAnswer(this@createQuestionnaireResponseItem) } else if (this@createQuestionnaireResponseItem.type == - Questionnaire.QuestionnaireItemType.GROUP + Questionnaire.QuestionnaireItemType.GROUP && !repeats ) { this@createQuestionnaireResponseItem.item.forEach { this.addItem(it.createQuestionnaireResponseItem()) @@ -492,20 +511,6 @@ private fun Questionnaire.QuestionnaireItemComponent.createQuestionnaireResponse ) } -/** - * Add items within [QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent] from the - * provided parent [Questionnaire.QuestionnaireItemComponent] with nested items. The hierarchy and - * order of child items will be retained as specified in the standard. See - * https://www.hl7.org/fhir/questionnaireresponse.html#notes for more details. - */ -fun QuestionnaireResponse.QuestionnaireResponseItemComponent.addNestedItemsToAnswer( - questionnaireItemComponent: Questionnaire.QuestionnaireItemComponent -) { - if (answer.isNotEmpty()) { - answer.first().item = questionnaireItemComponent.getNestedQuestionnaireResponseItems() - } -} - internal val Questionnaire.QuestionnaireItemComponent.answerExpression: Expression? get() = ToolingExtensions.getExtension(this, EXTENSION_ANSWER_EXPRESSION_URL)?.value?.let { @@ -599,16 +604,6 @@ fun List.flattened(): return this + this.flatMap { it.item.flattened() } } -/** - * Creates a list of [QuestionnaireResponse.QuestionnaireResponseItemComponent]s from the nested - * items in the [Questionnaire.QuestionnaireItemComponent]. - * - * The hierarchy and order of child items will be retained as specified in the standard. See - * https://www.hl7.org/fhir/questionnaireresponse.html#notes for more details. - */ -fun Questionnaire.QuestionnaireItemComponent.getNestedQuestionnaireResponseItems() = - item.map { it.createQuestionnaireResponseItem() } - val Resource.logicalId: String get() { return this.idElement?.idPart.orEmpty() diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnairesResponseItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnairesResponseItemComponents.kt index 7c512e8a82..0d51a9933b 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnairesResponseItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/MoreQuestionnairesResponseItemComponents.kt @@ -16,6 +16,7 @@ package com.google.android.fhir.datacapture +import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse /** @@ -28,3 +29,16 @@ val QuestionnaireResponse.QuestionnaireResponseItemComponent.descendant: this.item.flatMap { it.descendant } + this.answer.flatMap { answer -> answer.item.flatMap { it.descendant } } } + +/** + * Add nested items under the provided `questionnaireItem` to each answer in the questionnaire + * response item. The hierarchy and order of nested items will be retained as specified in the + * standard. + * + * See https://www.hl7.org/fhir/questionnaireresponse.html#notes for more details. + */ +fun QuestionnaireResponse.QuestionnaireResponseItemComponent.addNestedItemsToAnswer( + questionnaireItem: Questionnaire.QuestionnaireItemComponent +) { + answer.forEach { it.item = questionnaireItem.getNestedQuestionnaireResponseItems() } +} diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index cbbe2ae61a..c59c7ebe43 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -46,6 +46,7 @@ import kotlinx.coroutines.flow.update import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent @@ -89,9 +90,6 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } - @VisibleForTesting - val entryMode: EntryMode by lazy { questionnaire.entryMode ?: EntryMode.RANDOM } - /** The current questionnaire response as questions are being answered. */ private val questionnaireResponse: QuestionnaireResponse @@ -133,12 +131,12 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat /** The map from each item in the [Questionnaire] to its parent. */ private var questionnaireItemParentMap: - Map + Map init { /** Adds each child-parent pair in the [Questionnaire] to the parent map. */ fun buildParentList( - item: Questionnaire.QuestionnaireItemComponent, + item: QuestionnaireItemComponent, questionnaireItemToParentMap: ItemToParentMap, ) { for (child in item.item) { @@ -154,6 +152,9 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } + @VisibleForTesting + val entryMode: EntryMode by lazy { questionnaire.entryMode ?: EntryMode.RANDOM } + /** Flag to determine if the questionnaire should be read-only. */ private val isReadOnly = state[QuestionnaireFragment.EXTRA_READ_ONLY] ?: false @@ -244,7 +245,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat */ private val answersChangedCallback: ( - Questionnaire.QuestionnaireItemComponent, + QuestionnaireItemComponent, QuestionnaireResponseItemComponent, List, Any? @@ -264,7 +265,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } } - if (questionnaireItem.hasNestedItemsWithinAnswers) { + if (questionnaireItem.shouldHaveNestedItemsUnderAnswers) { questionnaireResponseItem.addNestedItemsToAnswer(questionnaireItem) } modifiedQuestionnaireResponseItemSet.add(questionnaireResponseItem) @@ -390,7 +391,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat ) private fun updateDependentQuestionnaireResponseItems( - updatedQuestionnaireItem: Questionnaire.QuestionnaireItemComponent, + updatedQuestionnaireItem: QuestionnaireItemComponent, ) { evaluateCalculatedExpressions( updatedQuestionnaireItem, @@ -464,7 +465,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat // https://build.fhir.org/ig/HL7/sdc/expressions.html#x-fhir-query-enhancements @PublishedApi internal suspend fun resolveAnswerExpression( - item: Questionnaire.QuestionnaireItemComponent, + item: QuestionnaireItemComponent, ): List { // Check cache first for database queries val answerExpression = item.answerExpression ?: return emptyList() @@ -481,7 +482,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } private suspend fun loadAnswerExpressionOptions( - item: Questionnaire.QuestionnaireItemComponent, + item: QuestionnaireItemComponent, expression: Expression, ): List { val data = @@ -573,26 +574,14 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * questionnaire response items. */ private fun getQuestionnaireAdapterItems( - questionnaireItemList: List, + questionnaireItemList: List, questionnaireResponseItemList: List, ): List { - var responseIndex = 0 return questionnaireItemList - .asSequence() - .flatMap { questionnaireItem -> - var questionnaireResponseItem = questionnaireItem.createQuestionnaireResponseItem() - // If there is an enabled questionnaire response available then we use that. Or else we - // just use an empty questionnaireResponse Item - if (responseIndex < questionnaireResponseItemList.size && - questionnaireItem.linkId == questionnaireResponseItemList[responseIndex].linkId - ) { - questionnaireResponseItem = questionnaireResponseItemList[responseIndex] - responseIndex += 1 - } - + .zipByLinkId(questionnaireResponseItemList) { questionnaireItem, questionnaireResponseItem -> getQuestionnaireAdapterItems(questionnaireItem, questionnaireResponseItem) } - .toList() + .flatten() } /** @@ -600,7 +589,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * questionnaire response item. */ private fun getQuestionnaireAdapterItems( - questionnaireItem: Questionnaire.QuestionnaireItemComponent, + questionnaireItem: QuestionnaireItemComponent, questionnaireResponseItem: QuestionnaireResponseItemComponent, ): List { // Hidden questions should not get QuestionnaireItemViewItem instances @@ -630,31 +619,42 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } else { NotValidated } - val items = - buildList { - // Add an item for the question itself - add( - QuestionnaireAdapterItem.Question( - QuestionnaireItemViewItem( - questionnaireItem, - questionnaireResponseItem, - validationResult = validationResult, - answersChangedCallback = answersChangedCallback, - resolveAnswerValueSet = { resolveAnswerValueSet(it) }, - resolveAnswerExpression = { resolveAnswerExpression(it) }, - draftAnswer = draftAnswerMap[questionnaireResponseItem] - ) + val items = buildList { + // Add an item for the question itself + add( + QuestionnaireAdapterItem.Question( + QuestionnaireItemViewItem( + questionnaireItem, + questionnaireResponseItem, + validationResult = validationResult, + answersChangedCallback = answersChangedCallback, + resolveAnswerValueSet = { resolveAnswerValueSet(it) }, + resolveAnswerExpression = { resolveAnswerExpression(it) }, + draftAnswer = draftAnswerMap[questionnaireResponseItem] ) ) - val nestedResponses: List> = - when { - // Repeated questions have one answer item per response instance, which we must display - // after the question. - questionnaireItem.repeats -> questionnaireResponseItem.answer.map { it.item } - // Non-repeated questions may have nested items, which we should display - else -> listOf(questionnaireResponseItem.item) - } - nestedResponses.forEach { nestedResponse -> + ) + + // Add nested questions after the parent item. We need to get the questionnaire items and + // (possibly multiple sets of) matching questionnaire response items and generate the adapter + // items. There are three different cases: + // 1. Questions nested under a non-repeated group: Simply take the nested question items and + // the nested question response items and "zip" them. + // 2. Questions nested under a question: In this case, the nested questions are repeated for + // each answer to the parent question. Therefore, we need to take the questions and lists of + // questionnaire response items nested under each answer and generate multiple sets of adapter + // items. + // 3. Questions nested under a repeated group: In the in-memory questionnaire response in the + // view model, we create dummy answers for each repeated group. As a result the processing of + // this case is similar to the case of questions nested under a question. + // For background, see https://build.fhir.org/questionnaireresponse.html#link. + buildList { + // Case 1 + add(questionnaireResponseItem.item) + // Case 2 and 3 + addAll(questionnaireResponseItem.answer.map { it.item }) + } + .forEach { nestedResponseItemList -> addAll( getQuestionnaireAdapterItems( // If nested display item is identified as instructions or flyover, then do not create @@ -664,11 +664,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat it.type == Questionnaire.QuestionnaireItemType.DISPLAY && (it.isInstructionsCode || it.isFlyoverCode || it.isHelpCode) }, - questionnaireResponseItemList = nestedResponse, + questionnaireResponseItemList = nestedResponseItemList, ) ) } - } + } currentPageItems = items return items } @@ -701,7 +701,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } private fun getEnabledResponseItems( - questionnaireItemList: List, + questionnaireItemList: List, questionnaireResponseItemList: List, ): List { val enablementEvaluator = EnablementEvaluator(questionnaireResponse) @@ -749,7 +749,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat * individual questions within this particular group instance). */ private fun createRepeatedGroupResponse( - questionnaireItem: Questionnaire.QuestionnaireItemComponent, + questionnaireItem: QuestionnaireItemComponent, questionnaireResponseItem: QuestionnaireResponseItemComponent, ): List { val individualQuestions = questionnaireItem.item @@ -795,8 +795,7 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat } } -typealias ItemToParentMap = - MutableMap +typealias ItemToParentMap = MutableMap /** Questionnaire state for the Fragment to consume. */ internal data class QuestionnaireState( @@ -833,3 +832,26 @@ internal val QuestionnairePagination.hasPreviousPage: Boolean internal val QuestionnairePagination.hasNextPage: Boolean get() = pages.any { it.index > currentPageIndex && it.enabled } + +/** + * Returns a list of values built from the elements of `this` and the + * `questionnaireResponseItemList` with the same linkId using the provided `transform` function + * applied to each pair of questionnaire item and questionnaire response item. + * + * It is assumed that the linkIds are unique in `this` and in `questionnaireResponseItemList`. + * + * Although linkIds may appear more than once in questionnaire response, they would not appear more + * than once within a list of questionnaire response items sharing the same parent. + */ +private inline fun List.zipByLinkId( + questionnaireResponseItemList: List, + transform: (QuestionnaireItemComponent, QuestionnaireResponseItemComponent) -> T +): List { + val linkIdToQuestionnaireResponseItemMap = questionnaireResponseItemList.associateBy { it.linkId } + return mapNotNull { questionnaireItem -> + linkIdToQuestionnaireResponseItemMap[questionnaireItem.linkId]?.let { questionnaireResponseItem + -> + transform(questionnaireItem, questionnaireResponseItem) + } + } +} diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ValueConstraintExtensionValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ValueConstraintExtensionValidator.kt index 3d09052268..459e41c49d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ValueConstraintExtensionValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/ValueConstraintExtensionValidator.kt @@ -33,7 +33,7 @@ internal open class ValueConstraintExtensionValidator( answers: List, context: Context ): ConstraintValidator.ConstraintValidationResult { - if (questionnaireItem.hasExtension(url) && !answers.isEmpty()) { + if (questionnaireItem.hasExtension(url) && answers.isNotEmpty()) { val extension = questionnaireItem.getExtensionByUrl(url) // TODO(https://github.com/google/android-fhir/issues/487): Validates all answers. val answer = answers[0] diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemGroupViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemGroupViewHolderFactory.kt index b73919ba39..f11353ba50 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemGroupViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireItemGroupViewHolderFactory.kt @@ -49,6 +49,8 @@ internal object QuestionnaireItemGroupViewHolderFactory : addItemButton.setOnClickListener { questionnaireItemViewItem.addAnswer( QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + // TODO(jingtang10): This can be removed since we already do this in the + // answerChangedCallback in the QuestionnaireViewModel. item = questionnaireItemViewItem.questionnaireItem.getNestedQuestionnaireResponseItems() } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index 0fd526d370..cfa732c60b 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -70,7 +70,6 @@ import org.hl7.fhir.r4.model.ValueSet import org.hl7.fhir.r4.utils.ToolingExtensions import org.junit.Assert.assertThrows import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TestWatcher @@ -1341,122 +1340,6 @@ class QuestionnaireViewModelTest { // Test cases for state flow - @Test - fun stateHasQuestionnaireResponse_lessItemsInQuestionnaireResponse_shouldAddTheMissingItem() = - runTest { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "a-link-id" - text = "Basic question" - type = Questionnaire.QuestionnaireItemType.BOOLEAN - initial = listOf(Questionnaire.QuestionnaireItemInitialComponent(BooleanType(true))) - } - ) - } - val questionnaireResponse = QuestionnaireResponse().apply { id = "a-questionnaire-response" } - val questionnaireResponseWithMissingItem = - QuestionnaireResponse().apply { - id = "a-questionnaire-response" - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "a-link-id" - answer = - listOf( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = BooleanType(true) - } - ) - } - ) - } - - val questionnaireViewModel = - createQuestionnaireViewModel(questionnaire, questionnaireResponse) - - val questionnaireItemViewItem = questionnaireViewModel.questionnaireStateFlow.first() - assertThat(questionnaireItemViewItem.items.first().asQuestion().questionnaireItem.linkId) - .isEqualTo(questionnaireResponseWithMissingItem.item.first().linkId) - assertThat( - questionnaireItemViewItem.items - .single() - .asQuestion() - .answers - .single() - .valueBooleanType.booleanValue() - ) - .isTrue() - } - - @Test - fun stateHasQuestionnaireResponse_lessItemsInQuestionnaireResponse_shouldCopyAnswer() = runTest { - val questionnaire = - Questionnaire().apply { - id = "a-questionnaire" - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "q1" - text = "Basic question" - type = Questionnaire.QuestionnaireItemType.BOOLEAN - initial = listOf(Questionnaire.QuestionnaireItemInitialComponent(BooleanType(false))) - } - ) - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "q2" - text = "Another basic question" - type = Questionnaire.QuestionnaireItemType.BOOLEAN - initial = listOf(Questionnaire.QuestionnaireItemInitialComponent(BooleanType(false))) - } - ) - addItem( - Questionnaire.QuestionnaireItemComponent().apply { - linkId = "q3" - text = "Another basic question" - type = Questionnaire.QuestionnaireItemType.BOOLEAN - initial = listOf(Questionnaire.QuestionnaireItemInitialComponent(BooleanType(false))) - } - ) - } - val questionnaireResponse = - QuestionnaireResponse().apply { - id = "a-questionnaire-response" - addItem( - QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { - linkId = "q2" - answer = - listOf( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = BooleanType(true) - } - ) - } - ) - } - - val questionnaireViewModel = createQuestionnaireViewModel(questionnaire, questionnaireResponse) - val questionnaireItemViewItemList = questionnaireViewModel.questionnaireStateFlow.first().items - - // Answer to first question should be created from questionnaire - val questionnaireItemViewItem1 = questionnaireItemViewItemList[0].asQuestion() - assertThat(questionnaireItemViewItem1.questionnaireItem.linkId).isEqualTo("q1") - assertThat(questionnaireItemViewItem1.answers.single().valueBooleanType.booleanValue()) - .isFalse() - - // Answer to second question should be copied from questionnaire response - val questionnaireItemViewItem2 = questionnaireItemViewItemList[1].asQuestion() - assertThat(questionnaireItemViewItem2.questionnaireItem.linkId).isEqualTo("q2") - assertThat(questionnaireItemViewItem2.answers.single().valueBooleanType.booleanValue()).isTrue() - - // Answer to third question should be created from questionnaire - val questionnaireItemViewItem3 = questionnaireItemViewItemList[2].asQuestion() - assertThat(questionnaireItemViewItem3.questionnaireItem.linkId).isEqualTo("q3") - assertThat(questionnaireItemViewItem3.answers.single().valueBooleanType.booleanValue()) - .isFalse() - } - @Test fun `should emit questionnaire state flow`() = runTest { val questionnaire = @@ -1931,7 +1814,6 @@ class QuestionnaireViewModelTest { } @Test - @Ignore("https://github.com/google/android-fhir/issues/487") fun questionnaireHasNestedItem_notOfTypeGroup_shouldNestItemWithinAnswerItem() = runTest { val questionnaire = Questionnaire().apply { @@ -1946,6 +1828,13 @@ class QuestionnaireViewModelTest { linkId = "a-nested-boolean-item" text = "Nested question" type = Questionnaire.QuestionnaireItemType.BOOLEAN + addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "a-nested-nested-boolean-item" + text = "Nested nested question" + type = Questionnaire.QuestionnaireItemType.BOOLEAN + } + ) } ) } @@ -1957,15 +1846,27 @@ class QuestionnaireViewModelTest { addItem( QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "a-boolean-item" + text = "Parent question" addAnswer( QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { this.value = valueBooleanType.setValue(false) addItem( QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = "a-nested-boolean-item" + text = "Nested question" addAnswer( QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { this.value = valueBooleanType.setValue(false) + addItem( + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "a-nested-nested-boolean-item" + text = "Nested nested question" + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .apply { this.value = valueBooleanType.setValue(false) } + ) + } + ) } ) } @@ -1977,25 +1878,44 @@ class QuestionnaireViewModelTest { } val viewModel = createQuestionnaireViewModel(questionnaire) + viewModel.runViewModelBlocking { + var items = viewModel.getQuestionnaireItemViewItemList().map { it.asQuestion() } + assertThat(items.map { it.questionnaireItem.linkId }).containsExactly("a-boolean-item") - viewModel - .getQuestionnaireItemViewItemList()[0] - .asQuestion() - .setAnswer( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - this.value = valueBooleanType.setValue(false) - } - ) - viewModel - .getQuestionnaireItemViewItemList()[1] - .asQuestion() - .setAnswer( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - this.value = valueBooleanType.setValue(false) - } - ) + items + .first() + .setAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = valueBooleanType.setValue(false) + } + ) - assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse) + items = viewModel.getQuestionnaireItemViewItemList().map { it.asQuestion() } + assertThat(items.map { it.questionnaireItem.linkId }) + .containsExactly("a-boolean-item", "a-nested-boolean-item") + + items + .last() + .setAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = valueBooleanType.setValue(false) + } + ) + + items = viewModel.getQuestionnaireItemViewItemList().map { it.asQuestion() } + assertThat(items.map { it.questionnaireItem.linkId }) + .containsExactly("a-boolean-item", "a-nested-boolean-item", "a-nested-nested-boolean-item") + + items + .last() + .setAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + this.value = valueBooleanType.setValue(false) + } + ) + + assertResourceEquals(viewModel.getQuestionnaireResponse(), questionnaireResponse) + } } // Test cases for pagination and navigation From 5e57859836e9343384d6eedb9952ea994caaadf9 Mon Sep 17 00:00:00 2001 From: Jing Tang Date: Wed, 22 Feb 2023 20:34:58 +0000 Subject: [PATCH 3/6] Bump up SDC lib version number to 1.0.0 (#1879) * Bump up SDC lib version number to 1.0.0 * Run spotless apply --------- Co-authored-by: Omar Ismail <44980219+omarismail94@users.noreply.github.com> --- buildSrc/src/main/kotlin/Releases.kt | 4 ++-- implementationguide/build.gradle.kts | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/kotlin/Releases.kt b/buildSrc/src/main/kotlin/Releases.kt index c6da3648f2..20077e2977 100644 --- a/buildSrc/src/main/kotlin/Releases.kt +++ b/buildSrc/src/main/kotlin/Releases.kt @@ -54,7 +54,7 @@ object Releases { object DataCapture : LibraryArtifact { override val artifactId = "data-capture" - override val version = "0.1.0-beta06" + override val version = "1.0.0" override val name = "Android FHIR Structured Data Capture Library" } @@ -72,7 +72,7 @@ object Releases { } } - object ImplmentationGuide : LibraryArtifact { + object ImplementationGuide : LibraryArtifact { override val artifactId = "implementationguide" override val version = "0.1.0-alpha001" override val name = "Android FHIR Implementation Guide Library" diff --git a/implementationguide/build.gradle.kts b/implementationguide/build.gradle.kts index 67682641a1..011426d455 100644 --- a/implementationguide/build.gradle.kts +++ b/implementationguide/build.gradle.kts @@ -9,7 +9,7 @@ plugins { id(Plugins.BuildPlugins.dokka).version(Plugins.Versions.dokka) } -publishArtifact(Releases.ImplmentationGuide) +publishArtifact(Releases.ImplementationGuide) createJacocoTestReportTask() @@ -110,13 +110,15 @@ dependencies { tasks.dokkaHtml.configure { outputDirectory.set( - file("../docs/${Releases.ImplmentationGuide.artifactId}/${Releases.ImplmentationGuide.version}") + file( + "../docs/${Releases.ImplementationGuide.artifactId}/${Releases.ImplementationGuide.version}" + ) ) suppressInheritedMembers.set(true) dokkaSourceSets { named("main") { - moduleName.set(Releases.ImplmentationGuide.artifactId) - moduleVersion.set(Releases.ImplmentationGuide.version) + moduleName.set(Releases.ImplementationGuide.artifactId) + moduleVersion.set(Releases.ImplementationGuide.version) noAndroidSdkLink.set(false) externalDocumentationLink { url.set(URL("https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-structures-r4/")) From aafd4512d24ae2332b16a08d64d3621b9813bfb7 Mon Sep 17 00:00:00 2001 From: maimoonak <4829880+maimoonak@users.noreply.github.com> Date: Thu, 23 Feb 2023 11:05:05 -0500 Subject: [PATCH 4/6] Sync state progress update (#1572) * Sync state progress update * Update tests | Fix missing states * Fix feedback | Add tests * Fix flow with upload * Fix failing tests * Fix for flow collect extension * Use download and uploader statuses instead of progress * add internal to percentOf function * add todo to generalize downloader apis --------- Co-authored-by: Jing Tang Co-authored-by: fikrimilano --- .../fhir/demo/MainActivityViewModel.kt | 7 +- .../android/fhir/demo/PatientListFragment.kt | 34 ++- .../fhir/demo/data/DownloadWorkManagerImpl.kt | 10 + .../main/res/layout/sync_status_layout.xml | 51 +++- demo/src/main/res/values/strings.xml | 1 + .../main/java/com/google/android/fhir/Util.kt | 3 + .../com/google/android/fhir/sync/Config.kt | 2 + .../android/fhir/sync/DownloadWorkManager.kt | 9 +- .../google/android/fhir/sync/Downloader.kt | 10 +- .../android/fhir/sync/FhirSynchronizer.kt | 23 +- .../google/android/fhir/sync/SyncJobStatus.kt | 7 +- .../com/google/android/fhir/sync/Uploader.kt | 15 +- .../fhir/sync/download/DownloaderImpl.kt | 34 ++- .../ResourceParamsBasedDownloadWorkManager.kt | 59 ++++- .../fhir/sync/remote/RemoteFhirService.kt | 3 +- .../fhir/sync/upload/BundleUploader.kt | 27 +- .../android/fhir/resource/TestingUtils.kt | 34 ++- .../java/com/google/android/fhir/UtilTest.kt | 10 + .../fhir/sync/download/DownloaderImplTest.kt | 241 ++++++++++++------ ...ourceParamsBasedDownloadWorkManagerTest.kt | 150 ++++++----- .../fhir/sync/upload/BundleUploaderTest.kt | 31 ++- 21 files changed, 543 insertions(+), 218 deletions(-) diff --git a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt index 1ebf6cdc9b..d5c27b91e6 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/MainActivityViewModel.kt @@ -33,6 +33,8 @@ import java.util.concurrent.TimeUnit import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch /** View model for [MainActivity]. */ @@ -55,13 +57,16 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica repeat = RepeatInterval(interval = 15, timeUnit = TimeUnit.MINUTES) ) ) + .shareIn(this, SharingStarted.Eagerly, 10) .collect { _pollState.emit(it) } } } fun triggerOneTimeSync() { viewModelScope.launch { - Sync.oneTimeSync(getApplication()).collect { _pollState.emit(it) } + Sync.oneTimeSync(getApplication()) + .shareIn(this, SharingStarted.Eagerly, 10) + .collect { _pollState.emit(it) } } } diff --git a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt index 3550a2aa74..b629323b6c 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/PatientListFragment.kt @@ -29,6 +29,7 @@ import android.view.ViewGroup import android.view.animation.AnimationUtils import android.view.inputmethod.InputMethodManager import android.widget.LinearLayout +import android.widget.ProgressBar import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity @@ -44,6 +45,7 @@ import com.google.android.fhir.FhirEngine import com.google.android.fhir.demo.PatientListViewModel.PatientListViewModelFactory import com.google.android.fhir.demo.databinding.FragmentPatientListBinding import com.google.android.fhir.sync.SyncJobStatus +import kotlin.math.roundToInt import kotlinx.coroutines.launch import timber.log.Timber @@ -53,6 +55,8 @@ class PatientListFragment : Fragment() { private lateinit var searchView: SearchView private lateinit var topBanner: LinearLayout private lateinit var syncStatus: TextView + private lateinit var syncPercent: TextView + private lateinit var syncProgress: ProgressBar private var _binding: FragmentPatientListBinding? = null private val binding get() = _binding!! @@ -101,6 +105,8 @@ class PatientListFragment : Fragment() { searchView = binding.search topBanner = binding.syncStatusContainer.linearLayoutSyncStatus syncStatus = binding.syncStatusContainer.tvSyncingStatus + syncPercent = binding.syncStatusContainer.tvSyncingPercent + syncProgress = binding.syncStatusContainer.progressSyncing searchView.setOnQueryTextListener( object : SearchView.OnQueryTextListener { override fun onQueryTextChange(newText: String): Boolean { @@ -149,11 +155,11 @@ class PatientListFragment : Fragment() { when (it) { is SyncJobStatus.Started -> { Timber.i("Sync: ${it::class.java.simpleName}") - fadeInTopBanner() + fadeInTopBanner(it) } is SyncJobStatus.InProgress -> { - Timber.i("Sync: ${it::class.java.simpleName} with ${it.resourceType?.name}") - fadeInTopBanner() + Timber.i("Sync: ${it::class.java.simpleName} with data $it") + fadeInTopBanner(it) } is SyncJobStatus.Finished -> { Timber.i("Sync: ${it::class.java.simpleName} at ${it.timestamp}") @@ -205,18 +211,36 @@ class PatientListFragment : Fragment() { .navigate(PatientListFragmentDirections.actionPatientListToAddPatientFragment()) } - private fun fadeInTopBanner() { + private fun fadeInTopBanner(state: SyncJobStatus) { if (topBanner.visibility != View.VISIBLE) { syncStatus.text = resources.getString(R.string.syncing).uppercase() + syncPercent.text = "" + syncProgress.progress = 0 + syncProgress.visibility = View.VISIBLE topBanner.visibility = View.VISIBLE val animation = AnimationUtils.loadAnimation(topBanner.context, R.anim.fade_in) topBanner.startAnimation(animation) + } else if (state is SyncJobStatus.InProgress) { + val progress = + state + .let { it.completed.toDouble().div(it.total) } + .let { if (it.isNaN()) 0.0 else it } + .times(100) + .roundToInt() + "$progress% ${state.syncOperation.name.lowercase()}ed".also { syncPercent.text = it } + syncProgress.progress = progress } } private fun fadeOutTopBanner(state: SyncJobStatus) { + if (state is SyncJobStatus.Finished) syncPercent.text = "" + syncProgress.visibility = View.GONE + if (topBanner.visibility == View.VISIBLE) { - syncStatus.text = state::class.java.simpleName.uppercase() + "${resources.getString(R.string.sync).uppercase()} ${state::class.java.simpleName.uppercase()}".also { + syncStatus.text = it + } + val animation = AnimationUtils.loadAnimation(topBanner.context, R.anim.fade_out) topBanner.startAnimation(animation) Handler(Looper.getMainLooper()).postDelayed({ topBanner.visibility = View.GONE }, 2000) diff --git a/demo/src/main/java/com/google/android/fhir/demo/data/DownloadWorkManagerImpl.kt b/demo/src/main/java/com/google/android/fhir/demo/data/DownloadWorkManagerImpl.kt index c9165b5584..0490557c7e 100644 --- a/demo/src/main/java/com/google/android/fhir/demo/data/DownloadWorkManagerImpl.kt +++ b/demo/src/main/java/com/google/android/fhir/demo/data/DownloadWorkManagerImpl.kt @@ -18,6 +18,7 @@ package com.google.android.fhir.demo.data import com.google.android.fhir.SyncDownloadContext import com.google.android.fhir.sync.DownloadWorkManager +import com.google.android.fhir.sync.SyncDataParams import java.util.LinkedList import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.model.Bundle @@ -43,6 +44,15 @@ class DownloadWorkManagerImpl : DownloadWorkManager { return url } + override suspend fun getSummaryRequestUrls( + context: SyncDownloadContext + ): Map { + return urls.associate { + ResourceType.fromCode(it.substringBefore("?")) to + it.plus("&${SyncDataParams.SUMMARY_KEY}=${SyncDataParams.SUMMARY_COUNT_VALUE}") + } + } + override suspend fun processResponse(response: Resource): Collection { // As per FHIR documentation : // If the search fails (cannot be executed, not that there are no matches), the diff --git a/demo/src/main/res/layout/sync_status_layout.xml b/demo/src/main/res/layout/sync_status_layout.xml index 9dec03fd8e..c9c40823f4 100644 --- a/demo/src/main/res/layout/sync_status_layout.xml +++ b/demo/src/main/res/layout/sync_status_layout.xml @@ -1,32 +1,57 @@ - + + + + + diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index 46fd993148..3a439cf024 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -67,4 +67,5 @@ Sync Last sync Syncing + Sync diff --git a/engine/src/main/java/com/google/android/fhir/Util.kt b/engine/src/main/java/com/google/android/fhir/Util.kt index 007e74cc38..5c9dc0f69f 100644 --- a/engine/src/main/java/com/google/android/fhir/Util.kt +++ b/engine/src/main/java/com/google/android/fhir/Util.kt @@ -79,3 +79,6 @@ class OffsetDateTimeTypeAdapter : TypeAdapter() { /** Url for the UCUM system of measures. */ const val ucumUrl = "http://unitsofmeasure.org" + +internal fun percentOf(value: Number, total: Number) = + if (total == 0) 0.0 else value.toDouble() / total.toDouble() diff --git a/engine/src/main/java/com/google/android/fhir/sync/Config.kt b/engine/src/main/java/com/google/android/fhir/sync/Config.kt index 1cbb2ee201..fbc3282b14 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/Config.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/Config.kt @@ -44,6 +44,8 @@ object SyncDataParams { const val SORT_KEY = "_sort" const val LAST_UPDATED_KEY = "_lastUpdated" const val ADDRESS_COUNTRY_KEY = "address-country" + const val SUMMARY_KEY = "_summary" + const val SUMMARY_COUNT_VALUE = "count" } /** Configuration for period synchronisation */ diff --git a/engine/src/main/java/com/google/android/fhir/sync/DownloadWorkManager.kt b/engine/src/main/java/com/google/android/fhir/sync/DownloadWorkManager.kt index 994f99fc9d..202193d055 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/DownloadWorkManager.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/DownloadWorkManager.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package com.google.android.fhir.sync import com.google.android.fhir.SyncDownloadContext import org.hl7.fhir.r4.model.Resource +import org.hl7.fhir.r4.model.ResourceType /** * Manager that generates the FHIR requests and handles the FHIR responses of a download job. @@ -32,6 +33,12 @@ interface DownloadWorkManager { */ suspend fun getNextRequestUrl(context: SyncDownloadContext): String? + /* TODO: Generalize the DownloadWorkManager API to not sequentially download resource by type (https://github.com/google/android-fhir/issues/1884) */ + /** + * Returns the map of resourceType and URL for summary of total count for each download request + */ + suspend fun getSummaryRequestUrls(context: SyncDownloadContext): Map + /** * Processes the download response and returns the resources to be saved to the local database. */ diff --git a/engine/src/main/java/com/google/android/fhir/sync/Downloader.kt b/engine/src/main/java/com/google/android/fhir/sync/Downloader.kt index b0cde096c8..6aaa172066 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/Downloader.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/Downloader.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,16 +25,18 @@ import org.hl7.fhir.r4.model.ResourceType internal interface Downloader { /** * @return Flow of the [DownloadState] which keeps emitting [Resource]s or Error based on the - * response of each page download request. + * response of each page download request. It also updates progress if [ProgressCallback] exists */ suspend fun download(context: SyncDownloadContext): Flow } +/* TODO: Generalize the Downloader API to not sequentially download resource by type (https://github.com/google/android-fhir/issues/1884) */ internal sealed class DownloadState { - data class Started(val type: ResourceType) : DownloadState() + data class Started(val type: ResourceType, val total: Int) : DownloadState() - data class Success(val resources: List) : DownloadState() + data class Success(val resources: List, val total: Int, val completed: Int) : + DownloadState() data class Failure(val syncError: ResourceSyncException) : DownloadState() } diff --git a/engine/src/main/java/com/google/android/fhir/sync/FhirSynchronizer.kt b/engine/src/main/java/com/google/android/fhir/sync/FhirSynchronizer.kt index 17e5092a36..094b698ef3 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/FhirSynchronizer.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/FhirSynchronizer.kt @@ -25,6 +25,11 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flow import org.hl7.fhir.r4.model.ResourceType +enum class SyncOperation { + DOWNLOAD, + UPLOAD +} + private sealed class SyncResult { val timestamp: OffsetDateTime = OffsetDateTime.now() @@ -97,9 +102,10 @@ internal class FhirSynchronizer( downloader.download(it).collect { when (it) { is DownloadState.Started -> { - setSyncState(SyncJobStatus.InProgress(it.type)) + setSyncState(SyncJobStatus.InProgress(SyncOperation.DOWNLOAD, it.total)) } is DownloadState.Success -> { + setSyncState(SyncJobStatus.InProgress(SyncOperation.DOWNLOAD, it.total, it.completed)) emit(it.resources) } is DownloadState.Failure -> { @@ -121,10 +127,17 @@ internal class FhirSynchronizer( val exceptions = mutableListOf() fhirEngine.syncUpload { list -> flow { - uploader.upload(list).collect { - when (it) { - is UploadResult.Success -> emit(it.localChangeToken to it.resource) - is UploadResult.Failure -> exceptions.add(it.syncError) + uploader.upload(list).collect { result -> + when (result) { + is UploadResult.Started -> + setSyncState(SyncJobStatus.InProgress(SyncOperation.UPLOAD, result.total)) + is UploadResult.Success -> + emit(result.localChangeToken to result.resource).also { + setSyncState( + SyncJobStatus.InProgress(SyncOperation.UPLOAD, result.total, result.completed) + ) + } + is UploadResult.Failure -> exceptions.add(result.syncError) } } } diff --git a/engine/src/main/java/com/google/android/fhir/sync/SyncJobStatus.kt b/engine/src/main/java/com/google/android/fhir/sync/SyncJobStatus.kt index 30b68fadd1..647dae7ec0 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/SyncJobStatus.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/SyncJobStatus.kt @@ -17,7 +17,6 @@ package com.google.android.fhir.sync import java.time.OffsetDateTime -import org.hl7.fhir.r4.model.ResourceType sealed class SyncJobStatus { val timestamp: OffsetDateTime = OffsetDateTime.now() @@ -26,7 +25,11 @@ sealed class SyncJobStatus { class Started : SyncJobStatus() /** Syncing in progress with the server. */ - data class InProgress(val resourceType: ResourceType?) : SyncJobStatus() + data class InProgress( + val syncOperation: SyncOperation, + val total: Int = 0, + val completed: Int = 0 + ) : SyncJobStatus() /** Glitched but sync job is being retried. */ data class Glitch(val exceptions: List) : SyncJobStatus() diff --git a/engine/src/main/java/com/google/android/fhir/sync/Uploader.kt b/engine/src/main/java/com/google/android/fhir/sync/Uploader.kt index efd90cdabd..f819ac0f91 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/Uploader.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/Uploader.kt @@ -26,14 +26,19 @@ internal interface Uploader { /** * Uploads the local changes to the [DataSource]. Particular implementations should take care of - * transforming the [SquashedLocalChange]s to particular network operations. + * transforming the [SquashedLocalChange]s to particular network operations. If [ProgressCallback] + * is provided it also reports the intermediate progress */ - suspend fun upload( - localChanges: List, - ): Flow + suspend fun upload(localChanges: List): Flow } internal sealed class UploadResult { - data class Success(val localChangeToken: LocalChangeToken, val resource: Bundle) : UploadResult() + data class Started(val total: Int) : UploadResult() + data class Success( + val localChangeToken: LocalChangeToken, + val resource: Bundle, + val total: Int, + val completed: Int + ) : UploadResult() data class Failure(val syncError: ResourceSyncException) : UploadResult() } diff --git a/engine/src/main/java/com/google/android/fhir/sync/download/DownloaderImpl.kt b/engine/src/main/java/com/google/android/fhir/sync/download/DownloaderImpl.kt index d66b0dc73b..a6b8ea9358 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/download/DownloaderImpl.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/download/DownloaderImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,9 @@ import com.google.android.fhir.sync.Downloader import com.google.android.fhir.sync.ResourceSyncException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.ResourceType +import timber.log.Timber /** * Implementation of the [Downloader]. It orchestrates the pre & post processing of resources via @@ -40,7 +42,27 @@ internal class DownloaderImpl( override suspend fun download(context: SyncDownloadContext): Flow = flow { var resourceTypeToDownload: ResourceType = ResourceType.Bundle - emit(DownloadState.Started(resourceTypeToDownload)) + + // download count summary of all resources for progress i.e. + val progressSummary = + downloadWorkManager + .getSummaryRequestUrls(context) + .map { summary -> + summary.key to + runCatching { dataSource.download(summary.value) } + .onFailure { Timber.e(it) } + .getOrNull() + .takeIf { it is Bundle } + ?.let { (it as Bundle).total } + } + .also { Timber.i("Download summary " + it.joinToString()) } + .toMap() + + val total = progressSummary.values.sumOf { it ?: 0 } + var completed = 0 + + emit(DownloadState.Started(resourceTypeToDownload, total)) + var url = downloadWorkManager.getNextRequestUrl(context) while (url != null) { try { @@ -48,11 +70,13 @@ internal class DownloaderImpl( ResourceType.fromCode(url.findAnyOf(resourceTypeList, ignoreCase = true)!!.second) emit( - DownloadState.Success( - downloadWorkManager.processResponse(dataSource.download(url!!)).toList() - ) + downloadWorkManager.processResponse(dataSource.download(url!!)).toList().let { + completed += it.size + DownloadState.Success(it, total, completed) + } ) } catch (exception: Exception) { + Timber.e(exception) emit(DownloadState.Failure(ResourceSyncException(resourceTypeToDownload, exception))) } diff --git a/engine/src/main/java/com/google/android/fhir/sync/download/ResourceParamsBasedDownloadWorkManager.kt b/engine/src/main/java/com/google/android/fhir/sync/download/ResourceParamsBasedDownloadWorkManager.kt index 0ae5b55bdc..e0841b45dd 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/download/ResourceParamsBasedDownloadWorkManager.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/download/ResourceParamsBasedDownloadWorkManager.kt @@ -46,19 +46,54 @@ class ResourceParamsBasedDownloadWorkManager(syncParams: ResourceSearchParams) : return urlOfTheNextPagesToDownloadForAResource.poll() return resourcesToDownloadWithSearchParams.poll()?.let { (resourceType, params) -> - val newParams = params.toMutableMap() - if (!params.containsKey(SyncDataParams.SORT_KEY)) { - newParams[SyncDataParams.SORT_KEY] = SyncDataParams.LAST_UPDATED_KEY - } - if (!params.containsKey(SyncDataParams.LAST_UPDATED_KEY)) { - val lastUpdate = context.getLatestTimestampFor(resourceType) - if (!lastUpdate.isNullOrEmpty()) { - newParams[SyncDataParams.LAST_UPDATED_KEY] = "$GREATER_THAN_PREFIX$lastUpdate" + val newParams = + params.toMutableMap().apply { putAll(getLastUpdatedParam(resourceType, params, context)) } + + "${resourceType.name}?${newParams.concatParams()}" + } + } + + /** + * Returns the map of resourceType and URL for summary of total count for each download request + */ + override suspend fun getSummaryRequestUrls( + context: SyncDownloadContext + ): Map { + return resourcesToDownloadWithSearchParams.associate { (resourceType, params) -> + val newParams = + params.toMutableMap().apply { + putAll(getLastUpdatedParam(resourceType, params, context)) + putAll(getSummaryParam(params)) } + + resourceType to "${resourceType.name}?${newParams.concatParams()}" + } + } + + private suspend fun getLastUpdatedParam( + resourceType: ResourceType, + params: ParamMap, + context: SyncDownloadContext + ): MutableMap { + val newParams = mutableMapOf() + if (!params.containsKey(SyncDataParams.SORT_KEY)) { + newParams[SyncDataParams.SORT_KEY] = SyncDataParams.LAST_UPDATED_KEY + } + if (!params.containsKey(SyncDataParams.LAST_UPDATED_KEY)) { + val lastUpdate = context.getLatestTimestampFor(resourceType) + if (!lastUpdate.isNullOrEmpty()) { + newParams[SyncDataParams.LAST_UPDATED_KEY] = "$GREATER_THAN_PREFIX$lastUpdate" } + } + return newParams + } - "${resourceType.name}?${newParams.concatParams()}" + private fun getSummaryParam(params: ParamMap): MutableMap { + val newParams = mutableMapOf() + if (!params.containsKey(SyncDataParams.SUMMARY_KEY)) { + newParams[SyncDataParams.SUMMARY_KEY] = SyncDataParams.SUMMARY_COUNT_VALUE } + return newParams } override suspend fun processResponse(response: Resource): Collection { @@ -67,9 +102,9 @@ class ResourceParamsBasedDownloadWorkManager(syncParams: ResourceSearchParams) : } return if (response is Bundle && response.type == Bundle.BundleType.SEARCHSET) { - response.link.firstOrNull { component -> component.relation == "next" }?.url?.let { next -> - urlOfTheNextPagesToDownloadForAResource.add(next) - } + response.link + .firstOrNull { component -> component.relation == "next" } + ?.url?.let { next -> urlOfTheNextPagesToDownloadForAResource.add(next) } response.entry.map { it.resource } } else { diff --git a/engine/src/main/java/com/google/android/fhir/sync/remote/RemoteFhirService.kt b/engine/src/main/java/com/google/android/fhir/sync/remote/RemoteFhirService.kt index 3d2e5087d1..5a554a3d3c 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/remote/RemoteFhirService.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/remote/RemoteFhirService.kt @@ -27,7 +27,6 @@ import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Resource import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Url @@ -37,7 +36,7 @@ internal interface RemoteFhirService : DataSource { @GET override suspend fun download(@Url path: String): Resource - @POST(".") override suspend fun upload(@Body bundle: Bundle): Resource + @POST(".") override suspend fun upload(bundle: Bundle): Resource class Builder( private val baseUrl: String, diff --git a/engine/src/main/java/com/google/android/fhir/sync/upload/BundleUploader.kt b/engine/src/main/java/com/google/android/fhir/sync/upload/BundleUploader.kt index b61f656993..8b5af9a22d 100644 --- a/engine/src/main/java/com/google/android/fhir/sync/upload/BundleUploader.kt +++ b/engine/src/main/java/com/google/android/fhir/sync/upload/BundleUploader.kt @@ -37,24 +37,39 @@ internal class BundleUploader( private val localChangesPaginator: LocalChangesPaginator ) : Uploader { - override suspend fun upload( - localChanges: List, - ): Flow = flow { + override suspend fun upload(localChanges: List): Flow = flow { + val total = localChanges.size + var completed = 0 + + emit(UploadResult.Started(total)) + bundleGenerator.generate(localChangesPaginator.page(localChanges)).forEach { (bundle, localChangeTokens) -> try { val response = dataSource.upload(bundle) - emit(getUploadResult(response, localChangeTokens)) + + completed += bundle.entry.size + emit(getUploadResult(response, localChangeTokens, total, completed)) } catch (e: Exception) { emit(UploadResult.Failure(ResourceSyncException(ResourceType.Bundle, e))) } } } - private fun getUploadResult(response: Resource, localChangeTokens: List) = + private fun getUploadResult( + response: Resource, + localChangeTokens: List, + total: Int, + completed: Int + ) = when { response is Bundle && response.type == Bundle.BundleType.TRANSACTIONRESPONSE -> { - UploadResult.Success(LocalChangeToken(localChangeTokens.flatMap { it.ids }), response) + UploadResult.Success( + LocalChangeToken(localChangeTokens.flatMap { it.ids }), + response, + total, + completed + ) } response is OperationOutcome && response.issue.isNotEmpty() -> { UploadResult.Failure( diff --git a/engine/src/test-common/java/com/google/android/fhir/resource/TestingUtils.kt b/engine/src/test-common/java/com/google/android/fhir/resource/TestingUtils.kt index 87c2880c92..d9b8e2d340 100644 --- a/engine/src/test-common/java/com/google/android/fhir/resource/TestingUtils.kt +++ b/engine/src/test-common/java/com/google/android/fhir/resource/TestingUtils.kt @@ -30,6 +30,7 @@ import com.google.common.truth.Truth.assertThat import java.time.OffsetDateTime import java.util.Date import java.util.LinkedList +import kotlin.streams.toList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect import org.hl7.fhir.r4.model.Bundle @@ -100,11 +101,20 @@ class TestingUtils constructor(private val iParser: IParser) { } open class TestDownloadManagerImpl( - queries: List = listOf("Patient?address-city=NAIROBI") + val queries: List = listOf("Patient?address-city=NAIROBI") ) : DownloadWorkManager { private val urls = LinkedList(queries) override suspend fun getNextRequestUrl(context: SyncDownloadContext): String? = urls.poll() + override suspend fun getSummaryRequestUrls( + context: SyncDownloadContext + ): Map { + return queries + .stream() + .map { ResourceType.fromCode(it.substringBefore("?")) to it.plus("?_summary=count") } + .toList() + .toMap() + } override suspend fun processResponse(response: Resource): Collection { val patient = Patient().setMeta(Meta().setLastUpdated(Date())) @@ -134,7 +144,7 @@ class TestingUtils constructor(private val iParser: IParser) { override suspend fun syncUpload( upload: suspend (List) -> Flow> ) { - upload(listOf()) + upload(listOf(getLocalChange(ResourceType.Patient, "123"))) } override suspend fun syncDownload( @@ -142,12 +152,12 @@ class TestingUtils constructor(private val iParser: IParser) { download: suspend (SyncDownloadContext) -> Flow> ) { download( - object : SyncDownloadContext { - override suspend fun getLatestTimestampFor(type: ResourceType): String { - return "123456788" + object : SyncDownloadContext { + override suspend fun getLatestTimestampFor(type: ResourceType): String { + return "123456788" + } } - } - ) + ) .collect {} } override suspend fun count(search: Search): Long { @@ -160,8 +170,14 @@ class TestingUtils constructor(private val iParser: IParser) { override suspend fun clearDatabase() {} - override suspend fun getLocalChange(type: ResourceType, id: String): LocalChange? { - TODO("Not yet implemented") + override suspend fun getLocalChange(type: ResourceType, id: String): LocalChange { + return LocalChange( + resourceType = type.name, + resourceId = id, + payload = "{}", + token = LocalChangeToken(listOf()), + type = LocalChange.Type.INSERT + ) } override suspend fun purge(type: ResourceType, id: String, forcePurge: Boolean) {} diff --git a/engine/src/test/java/com/google/android/fhir/UtilTest.kt b/engine/src/test/java/com/google/android/fhir/UtilTest.kt index 7ab3955a3b..5bea3eb192 100644 --- a/engine/src/test/java/com/google/android/fhir/UtilTest.kt +++ b/engine/src/test/java/com/google/android/fhir/UtilTest.kt @@ -101,6 +101,16 @@ class UtilTest : TestCase() { assertThat(isValidDateOnly("33-33-33")).isFalse() } + @Test + fun `percentOf() should return 0 when total is zero`() { + assertThat(percentOf(0, 0)).isEqualTo(0) + } + + @Test + fun `percentOf() should return percentage`() { + assertThat(percentOf(25, 50)).isEqualTo(0.5) + } + companion object { val TEST_OPERATION_OUTCOME_ERROR = OperationOutcome() val TEST_OPERATION_OUTCOME_INFO = OperationOutcome() diff --git a/engine/src/test/java/com/google/android/fhir/sync/download/DownloaderImplTest.kt b/engine/src/test/java/com/google/android/fhir/sync/download/DownloaderImplTest.kt index 3f333bdb81..2a1eecfdc2 100644 --- a/engine/src/test/java/com/google/android/fhir/sync/download/DownloaderImplTest.kt +++ b/engine/src/test/java/com/google/android/fhir/sync/download/DownloaderImplTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Google LLC + * Copyright 2022 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import com.google.android.fhir.sync.DataSource import com.google.android.fhir.sync.DownloadState import com.google.common.truth.Truth.assertThat import java.net.UnknownHostException -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.collectIndexed import kotlinx.coroutines.runBlocking import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Observation @@ -83,6 +83,7 @@ class DownloaderImplTest { object : DataSource { override suspend fun download(path: String): Resource { return when { + path.contains("summary") -> Bundle().apply { total = 1 } path.contains("patient-page1") -> searchPageParamToSearchResponseBundleMap["patient-page1"]!! path.contains("patient-page2") -> @@ -108,7 +109,8 @@ class DownloaderImplTest { ) val result = mutableListOf() - downloader.download( + downloader + .download( object : SyncDownloadContext { override suspend fun getLatestTimestampFor(type: ResourceType): String? = null } @@ -117,7 +119,7 @@ class DownloaderImplTest { assertThat(result.filterIsInstance()) .containsExactly( - DownloadState.Started(ResourceType.Bundle), + DownloadState.Started(ResourceType.Bundle, 2), // 1 patient and 1 observation ) assertThat( @@ -129,7 +131,139 @@ class DownloaderImplTest { @Test fun `downloader with patient and observations should return failure in case of server or network error`() = - runBlocking { + runBlocking { + val downloader = + DownloaderImpl( + object : DataSource { + override suspend fun download(path: String): Resource { + return when { + path.contains("summary") -> Bundle().apply { total = 1 } + path.contains("patient-page1") -> + searchPageParamToSearchResponseBundleMap["patient-page1"]!! + path.contains("patient-page2") -> + OperationOutcome().apply { + addIssue( + OperationOutcome.OperationOutcomeIssueComponent().apply { + diagnostics = "Server couldn't fulfil the request." + } + ) + } + path.contains("observation-page1") -> + searchPageParamToSearchResponseBundleMap["observation-page1"]!! + path.contains("observation-page2") -> + throw UnknownHostException( + "Url host can't be found. Check if device is connected to internet." + ) + else -> OperationOutcome() + } + } + + override suspend fun upload(bundle: Bundle): Resource { + TODO("Upload not tested in this path") + } + }, + ResourceParamsBasedDownloadWorkManager( + mapOf( + ResourceType.Patient to mapOf("param" to "patient-page1"), + ResourceType.Observation to mapOf("param" to "observation-page1") + ) + ) + ) + + val result = mutableListOf() + downloader + .download( + object : SyncDownloadContext { + override suspend fun getLatestTimestampFor(type: ResourceType) = null + } + ) + .collect { result.add(it) } + + assertThat(result.filterIsInstance()) + .containsExactly( + DownloadState.Started(ResourceType.Bundle, 2), // 1 patient and 1 observation + ) + + assertThat(result.filterIsInstance()).hasSize(2) + + assertThat(result.filterIsInstance().map { it.syncError.resourceType }) + .containsExactly(ResourceType.Patient, ResourceType.Observation) + .inOrder() + assertThat( + result.filterIsInstance().map { it.syncError.exception.message } + ) + .containsExactly( + "Server couldn't fulfil the request.", + "Url host can't be found. Check if device is connected to internet." + ) + .inOrder() + } + + @Test + fun `downloader with patient and observations should continue to download observations if patient download fail`() = + runBlocking { + val downloader = + DownloaderImpl( + object : DataSource { + override suspend fun download(path: String): Resource { + return when { + path.contains("summary") -> Bundle().apply { total = 1 } + path.contains("patient-page1") || path.contains("patient-page2") -> + OperationOutcome().apply { + addIssue( + OperationOutcome.OperationOutcomeIssueComponent().apply { + diagnostics = "Server couldn't fulfil the request." + } + ) + } + path.contains("observation-page1") -> + searchPageParamToSearchResponseBundleMap["observation-page1"]!! + path.contains("observation-page2") -> + searchPageParamToSearchResponseBundleMap["observation-page2"]!! + else -> OperationOutcome() + } + } + + override suspend fun upload(bundle: Bundle): Resource { + TODO("Not yet implemented") + } + }, + ResourceParamsBasedDownloadWorkManager( + mapOf( + ResourceType.Patient to mapOf("param" to "patient-page1"), + ResourceType.Observation to mapOf("param" to "observation-page1") + ) + ) + ) + + val result = mutableListOf() + downloader + .download( + object : SyncDownloadContext { + override suspend fun getLatestTimestampFor(type: ResourceType) = null + } + ) + .collect { result.add(it) } + + assertThat(result.filterIsInstance()) + .containsExactly( + DownloadState.Started(ResourceType.Bundle, 2), // 1 patient and 1 observation + ) + + assertThat(result.filterIsInstance().map { it.syncError.resourceType }) + .containsExactly(ResourceType.Patient) + + assertThat( + result + .filterIsInstance() + .flatMap { it.resources } + .filterIsInstance() + ) + .hasSize(2) + } + + @Test + fun `downloader should emit Started state`() = runBlocking { val downloader = DownloaderImpl( object : DataSource { @@ -137,122 +271,67 @@ class DownloaderImplTest { return when { path.contains("patient-page1") -> searchPageParamToSearchResponseBundleMap["patient-page1"]!! - path.contains("patient-page2") -> - OperationOutcome().apply { - addIssue( - OperationOutcome.OperationOutcomeIssueComponent().apply { - diagnostics = "Server couldn't fulfil the request." - } - ) - } - path.contains("observation-page1") -> - searchPageParamToSearchResponseBundleMap["observation-page1"]!! - path.contains("observation-page2") -> - throw UnknownHostException( - "Url host can't be found. Check if device is connected to internet." - ) else -> OperationOutcome() } } override suspend fun upload(bundle: Bundle): Resource { - TODO("Upload not tested in this path") + throw UnsupportedOperationException() } }, ResourceParamsBasedDownloadWorkManager( - mapOf( - ResourceType.Patient to mapOf("param" to "patient-page1"), - ResourceType.Observation to mapOf("param" to "observation-page1") - ) + mapOf(ResourceType.Patient to mapOf("param" to "patient-page1")) ) ) val result = mutableListOf() - downloader.download( + downloader + .download( object : SyncDownloadContext { - override suspend fun getLatestTimestampFor(type: ResourceType) = null + override suspend fun getLatestTimestampFor(type: ResourceType): String? = null } ) - .collect { result.add(it) } - - assertThat(result.filterIsInstance()) - .containsExactly( - DownloadState.Started(ResourceType.Bundle), - ) + .collectIndexed { index, value -> result.add(value) } - assertThat(result.filterIsInstance()).hasSize(2) - - assertThat(result.filterIsInstance().map { it.syncError.resourceType }) - .containsExactly(ResourceType.Patient, ResourceType.Observation) - .inOrder() - assertThat( - result.filterIsInstance().map { it.syncError.exception.message } - ) - .containsExactly( - "Server couldn't fulfil the request.", - "Url host can't be found. Check if device is connected to internet." - ) - .inOrder() + assertThat(result.first()).isInstanceOf(DownloadState.Started::class.java) } @Test - fun `downloader with patient and observations should continue to download observations if patient download fail`() = - runBlocking { + fun `downloader should emit Success state`() = runBlocking { val downloader = DownloaderImpl( object : DataSource { override suspend fun download(path: String): Resource { return when { - path.contains("patient-page1") || path.contains("patient-page2") -> - OperationOutcome().apply { - addIssue( - OperationOutcome.OperationOutcomeIssueComponent().apply { - diagnostics = "Server couldn't fulfil the request." - } - ) - } - path.contains("observation-page1") -> - searchPageParamToSearchResponseBundleMap["observation-page1"]!! - path.contains("observation-page2") -> - searchPageParamToSearchResponseBundleMap["observation-page2"]!! + path.contains("patient-page1") -> + searchPageParamToSearchResponseBundleMap["patient-page1"]!!.apply { total = 1 } else -> OperationOutcome() } } override suspend fun upload(bundle: Bundle): Resource { - TODO("Not yet implemented") + throw UnsupportedOperationException() } }, ResourceParamsBasedDownloadWorkManager( - mapOf( - ResourceType.Patient to mapOf("param" to "patient-page1"), - ResourceType.Observation to mapOf("param" to "observation-page1") - ) + mapOf(ResourceType.Patient to mapOf("param" to "patient-page1")) ) ) val result = mutableListOf() - downloader.download( + downloader + .download( object : SyncDownloadContext { - override suspend fun getLatestTimestampFor(type: ResourceType) = null + override suspend fun getLatestTimestampFor(type: ResourceType): String? = null } ) - .collect { result.add(it) } + .collectIndexed { index, value -> result.add(value) } - assertThat(result.filterIsInstance()) - .containsExactly( - DownloadState.Started(ResourceType.Bundle), - ) - - assertThat(result.filterIsInstance().map { it.syncError.resourceType }) - .containsExactly(ResourceType.Patient) + assertThat(result.first()).isInstanceOf(DownloadState.Started::class.java) + assertThat(result.elementAt(1)).isInstanceOf(DownloadState.Success::class.java) - assertThat( - result - .filterIsInstance() - .flatMap { it.resources } - .filterIsInstance() - ) - .hasSize(2) + val success = result.elementAt(1) as DownloadState.Success + assertThat(success.total).isEqualTo(1) + assertThat(success.completed).isEqualTo(1) } } diff --git a/engine/src/test/java/com/google/android/fhir/sync/download/ResourceParamsBasedDownloadWorkManagerTest.kt b/engine/src/test/java/com/google/android/fhir/sync/download/ResourceParamsBasedDownloadWorkManagerTest.kt index ff6acdc43d..0d2351d0e5 100644 --- a/engine/src/test/java/com/google/android/fhir/sync/download/ResourceParamsBasedDownloadWorkManagerTest.kt +++ b/engine/src/test/java/com/google/android/fhir/sync/download/ResourceParamsBasedDownloadWorkManagerTest.kt @@ -115,86 +115,114 @@ class ResourceParamsBasedDownloadWorkManagerTest { @Test fun getNextRequestUrl_withLastUpdatedTimeProvidedInContext_ShouldAppendGtPrefixToLastUpdatedSearchParam() = - runBlockingTest { - val downloadManager = - ResourceParamsBasedDownloadWorkManager(mapOf(ResourceType.Patient to emptyMap())) - val url = - downloadManager.getNextRequestUrl( - object : SyncDownloadContext { - override suspend fun getLatestTimestampFor(type: ResourceType) = "2022-06-28" - } - ) - assertThat(url).isEqualTo("Patient?_sort=_lastUpdated&_lastUpdated=gt2022-06-28") - } + runBlockingTest { + val downloadManager = + ResourceParamsBasedDownloadWorkManager(mapOf(ResourceType.Patient to emptyMap())) + val url = + downloadManager.getNextRequestUrl( + object : SyncDownloadContext { + override suspend fun getLatestTimestampFor(type: ResourceType) = "2022-06-28" + } + ) + assertThat(url).isEqualTo("Patient?_sort=_lastUpdated&_lastUpdated=gt2022-06-28") + } @Test fun getNextRequestUrl_withLastUpdatedSyncParamProvided_shouldReturnUrlWithExactProvidedLastUpdatedSyncParam() = - runBlockingTest { - val downloadManager = - ResourceParamsBasedDownloadWorkManager( - mapOf( - ResourceType.Patient to - mapOf( - SyncDataParams.LAST_UPDATED_KEY to "2022-06-28", - SyncDataParams.SORT_KEY to "status" - ) + runBlockingTest { + val downloadManager = + ResourceParamsBasedDownloadWorkManager( + mapOf( + ResourceType.Patient to + mapOf( + SyncDataParams.LAST_UPDATED_KEY to "2022-06-28", + SyncDataParams.SORT_KEY to "status" + ) + ) ) - ) - val url = - downloadManager.getNextRequestUrl( - object : SyncDownloadContext { - override suspend fun getLatestTimestampFor(type: ResourceType) = "2022-07-07" - } - ) - assertThat(url).isEqualTo("Patient?_lastUpdated=2022-06-28&_sort=status") - } + val url = + downloadManager.getNextRequestUrl( + object : SyncDownloadContext { + override suspend fun getLatestTimestampFor(type: ResourceType) = "2022-07-07" + } + ) + assertThat(url).isEqualTo("Patient?_lastUpdated=2022-06-28&_sort=status") + } @Test fun getNextRequestUrl_withLastUpdatedSyncParamHavingGtPrefix_shouldReturnUrlWithExactProvidedLastUpdatedSyncParam() = - runBlockingTest { - val downloadManager = - ResourceParamsBasedDownloadWorkManager( - mapOf(ResourceType.Patient to mapOf(SyncDataParams.LAST_UPDATED_KEY to "gt2022-06-28")) - ) - val url = - downloadManager.getNextRequestUrl( - object : SyncDownloadContext { - override suspend fun getLatestTimestampFor(type: ResourceType) = "2022-07-07" - } - ) - assertThat(url).isEqualTo("Patient?_lastUpdated=gt2022-06-28&_sort=_lastUpdated") - } + runBlockingTest { + val downloadManager = + ResourceParamsBasedDownloadWorkManager( + mapOf(ResourceType.Patient to mapOf(SyncDataParams.LAST_UPDATED_KEY to "gt2022-06-28")) + ) + val url = + downloadManager.getNextRequestUrl( + object : SyncDownloadContext { + override suspend fun getLatestTimestampFor(type: ResourceType) = "2022-07-07" + } + ) + assertThat(url).isEqualTo("Patient?_lastUpdated=gt2022-06-28&_sort=_lastUpdated") + } @Test fun getNextRequestUrl_withNullUpdatedTimeStamp_shouldReturnUrlWithoutLastUpdatedQueryParam() = - runBlockingTest { - val downloadManager = - ResourceParamsBasedDownloadWorkManager( - mapOf(ResourceType.Patient to mapOf(Patient.ADDRESS_CITY.paramName to "NAIROBI")) - ) - val actual = - downloadManager.getNextRequestUrl( - object : SyncDownloadContext { - override suspend fun getLatestTimestampFor(type: ResourceType) = null - } - ) - assertThat(actual).isEqualTo("Patient?address-city=NAIROBI&_sort=_lastUpdated") - } + runBlockingTest { + val downloadManager = + ResourceParamsBasedDownloadWorkManager( + mapOf(ResourceType.Patient to mapOf(Patient.ADDRESS_CITY.paramName to "NAIROBI")) + ) + val actual = + downloadManager.getNextRequestUrl( + object : SyncDownloadContext { + override suspend fun getLatestTimestampFor(type: ResourceType) = null + } + ) + assertThat(actual).isEqualTo("Patient?address-city=NAIROBI&_sort=_lastUpdated") + } @Test fun getNextRequestUrl_withEmptyUpdatedTimeStamp_shouldReturnUrlWithoutLastUpdatedQueryParam() = - runBlockingTest { + runBlockingTest { + val downloadManager = + ResourceParamsBasedDownloadWorkManager( + mapOf(ResourceType.Patient to mapOf(Patient.ADDRESS_CITY.paramName to "NAIROBI")) + ) + val actual = + downloadManager.getNextRequestUrl( + object : SyncDownloadContext { + override suspend fun getLatestTimestampFor(type: ResourceType) = "" + } + ) + assertThat(actual).isEqualTo("Patient?address-city=NAIROBI&_sort=_lastUpdated") + } + + @Test + fun `getSummaryRequestUrls should return resource summary urls`() = runBlockingTest { val downloadManager = ResourceParamsBasedDownloadWorkManager( - mapOf(ResourceType.Patient to mapOf(Patient.ADDRESS_CITY.paramName to "NAIROBI")) + mapOf( + ResourceType.Patient to mapOf(Patient.ADDRESS_CITY.paramName to "NAIROBI"), + ResourceType.Immunization to emptyMap(), + ResourceType.Observation to emptyMap(), + ) ) - val actual = - downloadManager.getNextRequestUrl( + + val urls = + downloadManager.getSummaryRequestUrls( object : SyncDownloadContext { - override suspend fun getLatestTimestampFor(type: ResourceType) = "" + override suspend fun getLatestTimestampFor(type: ResourceType) = "2022-03-20" } ) - assertThat(actual).isEqualTo("Patient?address-city=NAIROBI&_sort=_lastUpdated") + + assertThat(urls.map { it.key }) + .containsExactly(ResourceType.Patient, ResourceType.Immunization, ResourceType.Observation) + assertThat(urls.map { it.value }) + .containsExactly( + "Patient?address-city=NAIROBI&_sort=_lastUpdated&_lastUpdated=gt2022-03-20&_summary=count", + "Immunization?_sort=_lastUpdated&_lastUpdated=gt2022-03-20&_summary=count", + "Observation?_sort=_lastUpdated&_lastUpdated=gt2022-03-20&_summary=count" + ) } @Test diff --git a/engine/src/test/java/com/google/android/fhir/sync/upload/BundleUploaderTest.kt b/engine/src/test/java/com/google/android/fhir/sync/upload/BundleUploaderTest.kt index 12482afe8c..7899f639b8 100644 --- a/engine/src/test/java/com/google/android/fhir/sync/upload/BundleUploaderTest.kt +++ b/engine/src/test/java/com/google/android/fhir/sync/upload/BundleUploaderTest.kt @@ -52,8 +52,27 @@ class BundleUploaderTest { .upload(localChanges) .toList() - assertThat(result).hasSize(1) - assertThat(result.first()).isInstanceOf(UploadResult.Success::class.java) + assertThat(result).hasSize(2) + assertThat(result.first()).isInstanceOf(UploadResult.Started::class.java) + assertThat(result.last()).isInstanceOf(UploadResult.Success::class.java) + + val success = result.last() as UploadResult.Success + assertThat(success.total).isEqualTo(1) + assertThat(success.completed).isEqualTo(1) + } + + @Test + fun `upload Bundle transaction should emit Started state`() = runBlocking { + val result = + BundleUploader( + TestingUtils.BundleDataSource { Bundle() }, + TransactionBundleGenerator.getDefault(), + LocalChangesPaginator.DEFAULT + ) + .upload(localChanges) + .toList() + + assertThat(result.first()).isInstanceOf(UploadResult.Started::class.java) } @Test @@ -76,8 +95,8 @@ class BundleUploaderTest { .upload(localChanges) .toList() - assertThat(result).hasSize(1) - assertThat(result.first()).isInstanceOf(UploadResult.Failure::class.java) + assertThat(result).hasSize(2) + assertThat(result.last()).isInstanceOf(UploadResult.Failure::class.java) } @Test @@ -91,8 +110,8 @@ class BundleUploaderTest { .upload(localChanges) .toList() - assertThat(result).hasSize(1) - assertThat(result.first()).isInstanceOf(UploadResult.Failure::class.java) + assertThat(result).hasSize(2) + assertThat(result.last()).isInstanceOf(UploadResult.Failure::class.java) } companion object { From 2363c11b8b361facb94d5009e27db6ab8dd41d84 Mon Sep 17 00:00:00 2001 From: Jing Tang Date: Thu, 23 Feb 2023 16:55:12 +0000 Subject: [PATCH 5/6] Rename view holder factories and reorganize files (#1883) * Renaming * Rename test files * Run spotless apply * Fix imports for attachment * Correct package declarations * Fix unit and instrumentation tests * Update MoreEnumerationsTest.kt * Spotless apply * Move view holder factory tests in the right package * Rename views * Fix build * Update QuestionnaireEditAdapterTest.kt * Create extensions package * Extract capitalize extension function --- .../catalog/ComponentsRecyclerViewadapter.kt | 4 +- .../fhir/catalog/CustomNumberPickerFactory.kt | 10 +- .../catalog/CustomQuestionnaireFragment.kt | 16 +- ...eaderViewHolderFactoryInstrumentedTest.kt} | 16 +- ...y.kt => BarCodeReaderViewHolderFactory.kt} | 32 +-- .../QuestionnaireUiEspressoTest.kt | 2 +- ...umberViewHolderFactoryInstrumentedTest.kt} | 54 ++-- ...ttachmentViewHolderFactoryEspressoTest.kt} | 30 ++- ...atePickerViewHolderFactoryEspressoTest.kt} | 24 +- ...imePickerViewHolderFactoryEspressoTest.kt} | 10 +- ... DropDownViewHolderFactoryEspressoTest.kt} | 58 +++-- ...edTest.kt => MediaViewInstrumentedTest.kt} | 6 +- ...ultiSelectViewHolderFactoryEspressoTest.kt | 82 +++--- ...xtQuantityViewHolderFactoryEspressoTest.kt | 16 +- ...ultiSelectHolderFactoryInstrumentedTest.kt | 12 +- .../datacapture/QuestionnaireAdapterItem.kt | 6 +- ...Adapter.kt => QuestionnaireEditAdapter.kt} | 183 +++++++------ .../fhir/datacapture/QuestionnaireFragment.kt | 16 +- ...apter.kt => QuestionnaireReviewAdapter.kt} | 8 +- ...Type.kt => QuestionnaireViewHolderType.kt} | 4 +- .../datacapture/QuestionnaireViewModel.kt | 15 +- ...ory.kt => PhoneNumberViewHolderFactory.kt} | 25 +- .../enablement/EnablementEvaluator.kt | 2 +- .../{utilities => extensions}/MoreContexts.kt | 2 +- .../MoreEnumerations.kt} | 19 +- .../{ => extensions}/MoreExpressions.kt | 0 .../MoreLocalDateTimes.kt | 2 +- .../MoreLocalDates.kt | 7 +- .../MoreLocalTimes.kt | 4 +- ...QuestionnaireItemAnswerOptionComponents.kt | 0 .../MoreQuestionnaireItemComponents.kt | 18 +- ...estionnaireResponseItemAnswerComponents.kt | 0 .../{ => extensions}/MoreQuestionnaires.kt | 5 +- ...oreQuestionnairesResponseItemComponents.kt | 0 .../MoreQuestionnairesResponses.kt | 0 .../datatype => extensions}/MoreTypes.kt | 30 ++- .../fhirpath/ExpressionEvaluator.kt | 1 + .../{utilities => fhirpath}/FhirPathUtil.kt | 2 +- .../datacapture/mapping/ResourceMapper.kt | 25 +- .../utilities/MoreReflectionUtil.kt | 32 --- .../utilities/TypeConversionUtil.kt | 43 ---- ...upTypeHeaderView.kt => GroupHeaderView.kt} | 4 +- ...onnaireItemHeaderView.kt => HeaderView.kt} | 5 +- ...tionnaireItemMediaView.kt => MediaView.kt} | 7 +- .../views/OptionSelectDialogFragment.kt | 15 +- ...emViewItem.kt => QuestionnaireViewItem.kt} | 28 +- .../attachment/CameraLauncherFragment.kt | 5 +- .../OpenDocumentLauncherFragment.kt | 5 +- .../AttachmentViewHolderFactory.kt} | 42 ++- .../AutoCompleteViewHolderFactory.kt} | 43 ++-- .../BooleanChoiceViewHolderFactory.kt} | 35 +-- .../CheckBoxGroupViewHolderFactory.kt} | 41 +-- .../DatePickerViewHolderFactory.kt} | 54 ++-- .../DateTimePickerViewHolderFactory.kt} | 46 ++-- .../DialogSelectViewHolderFactory.kt} | 30 ++- .../DisplayViewHolderFactory.kt} | 16 +- .../DropDownViewHolderFactory.kt} | 36 ++- .../EditTextDecimalViewHolderFactory.kt} | 23 +- .../EditTextIntegerViewHolderFactory.kt} | 26 +- .../EditTextMultiLineViewHolderFactory.kt} | 11 +- .../EditTextQuantityViewHolderFactory.kt} | 24 +- .../EditTextSingleLineViewHolderFactory.kt} | 11 +- .../EditTextStringViewHolderDelegate.kt} | 21 +- .../EditTextViewHolderFactory.kt} | 31 +-- .../GroupViewHolderFactory.kt} | 25 +- .../QuestionnaireItemViewHolderFactory.kt | 26 +- .../RadioGroupViewHolderFactory.kt} | 29 ++- .../ReviewViewHolderFactory.kt} | 23 +- .../SliderViewHolderFactory.kt} | 30 +-- ...ttachment_view.xml => attachment_view.xml} | 4 +- ...icker_view.xml => boolean_choice_view.xml} | 4 +- ..._check_box_view.xml => check_box_view.xml} | 0 ...group_view.xml => checkbox_group_view.xml} | 4 +- ...e_picker_view.xml => date_picker_view.xml} | 4 +- ...ker_view.xml => date_time_picker_view.xml} | 4 +- ...item_display_view.xml => display_view.xml} | 4 +- ..._list_item.xml => drop_down_list_item.xml} | 0 ..._drop_down_view.xml => drop_down_view.xml} | 4 +- ...w.xml => edit_text_auto_complete_view.xml} | 4 +- ...view.xml => edit_text_multi_line_view.xml} | 4 +- ...iew.xml => edit_text_single_line_view.xml} | 4 +- ..._header_view.xml => group_header_view.xml} | 4 +- ..._header.xml => group_type_header_view.xml} | 0 ...nnaire_item_header.xml => header_view.xml} | 0 ...ionnaire_item_media.xml => media_view.xml} | 0 ...ect_dialog.xml => multi_select_dialog.xml} | 2 +- ...n_item_multi.xml => option_item_multi.xml} | 0 ....xml => option_item_other_add_another.xml} | 0 ...er_text.xml => option_item_other_text.xml} | 0 ...item_single.xml => option_item_single.xml} | 0 ...select_view.xml => option_select_view.xml} | 4 +- ...item_radio_button.xml => radio_button.xml} | 0 ...io_group_view.xml => radio_group_view.xml} | 4 +- ...estion_answer_view.xml => review_view.xml} | 4 +- ...nnaire_item_slider.xml => slider_view.xml} | 4 +- ...est.kt => QuestionnaireEditAdapterTest.kt} | 240 +++++++++--------- .../datacapture/QuestionnaireFragmentTest.kt | 4 +- ...t.kt => QuestionnaireReviewAdapterTest.kt} | 30 +-- ....kt => QuestionnaireViewHolderTypeTest.kt} | 8 +- .../datacapture/QuestionnaireViewModelTest.kt | 11 +- .../MoreContextsTest.kt | 3 +- .../MoreEnumerationsTest.kt} | 24 +- .../{ => extensions}/MoreExpressionsTest.kt | 4 +- .../MoreLocalDateTimesTest.kt | 4 +- .../MoreLocalDatesTest.kt | 8 +- .../MoreLocalTimesTest.kt | 3 +- ...ionnaireItemAnswerOptionComponentsTest.kt} | 6 +- .../MoreQuestionnaireItemComponentsTest.kt | 57 ++++- ...nnaireResponseItemAnswerComponentsTest.kt} | 10 +- ...QuestionnaireResponseItemComponentsTest.kt | 3 +- .../MoreQuestionnaireResponsesTest.kt | 3 +- .../MoreQuestionnairesTest.kt | 5 +- .../datatype => extensions}/MoreTypesTest.kt | 33 ++- .../fhirpath/ExpressionEvaluatorTest.kt | 1 + .../FhirPathUtilTest.kt | 2 +- .../datacapture/mapping/ResourceMapperTest.kt | 2 +- .../utilities/MoreEnumerationTest.kt | 42 --- .../utilities/TypeConversionUtilTest.kt | 55 ---- ...aderViewTest.kt => GroupHeaderViewTest.kt} | 4 +- ...temHeaderViewTest.kt => HeaderViewTest.kt} | 4 +- ...emTest.kt => QuestionnaireViewItemTest.kt} | 144 +++++------ .../AutoCompleteViewHolderFactoryTest.kt} | 23 +- .../BooleanChoiceViewHolderFactoryTest.kt} | 81 +++--- .../CheckBoxGroupViewHolderFactoryTest.kt} | 49 ++-- .../DatePickerViewHolderFactoryTest.kt} | 53 ++-- .../DateTimePickerViewHolderFactoryTest.kt} | 57 +++-- .../DisplayViewHolderFactoryTest.kt} | 11 +- .../DropDownViewHolderFactoryTest.kt} | 29 ++- .../EditTextDecimalViewHolderFactoryTest.kt} | 39 +-- .../EditTextIntegerViewHolderFactoryTest.kt} | 39 +-- ...EditTextMultiLineViewHolderFactoryTest.kt} | 39 +-- .../EditTextQuantityViewHolderFactoryTest.kt} | 47 ++-- ...ditTextSingleLineViewHolderFactoryTest.kt} | 39 +-- .../GroupViewHolderFactoryTest.kt} | 36 ++- .../RadioGroupViewHolderFactoryTest.kt} | 53 ++-- .../ReviewViewHolderFactoryTest.kt} | 32 +-- .../SliderViewHolderFactoryTest.kt} | 48 ++-- 137 files changed, 1474 insertions(+), 1486 deletions(-) rename contrib/barcode/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/barcode/{QuestionnaireItemBarCodeReaderViewHolderFactoryInstrumentedTest.kt => BarCodeReaderViewHolderFactoryInstrumentedTest.kt} (90%) rename contrib/barcode/src/main/java/com/google/android/fhir/datacapture/contrib/views/barcode/{QuestionnaireItemBarCodeReaderViewHolderFactory.kt => BarCodeReaderViewHolderFactory.kt} (75%) rename datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/{QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest.kt => PhoneNumberViewHolderFactoryInstrumentedTest.kt} (83%) rename datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemAttachmentViewHolderFactoryEspressoTest.kt => AttachmentViewHolderFactoryEspressoTest.kt} (96%) rename datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDatePickerViewHolderFactoryEspressoTest.kt => DatePickerViewHolderFactoryEspressoTest.kt} (95%) rename datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest.kt => DateTimePickerViewHolderFactoryEspressoTest.kt} (92%) rename datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDropDownViewHolderFactoryEspressoTest.kt => DropDownViewHolderFactoryEspressoTest.kt} (96%) rename datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemMediaViewInstrumentedTest.kt => MediaViewInstrumentedTest.kt} (97%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{QuestionnaireItemEditAdapter.kt => QuestionnaireEditAdapter.kt} (51%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{QuestionnaireItemReviewAdapter.kt => QuestionnaireReviewAdapter.kt} (78%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{QuestionnaireItemViewHolderType.kt => QuestionnaireViewHolderType.kt} (89%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/contrib/views/{QuestionnaireItemPhoneNumberViewHolderFactory.kt => PhoneNumberViewHolderFactory.kt} (68%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{utilities => extensions}/MoreContexts.kt (96%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{utilities/MoreEnumeration.kt => extensions/MoreEnumerations.kt} (80%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{ => extensions}/MoreExpressions.kt (100%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{utilities => extensions}/MoreLocalDateTimes.kt (95%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{utilities => extensions}/MoreLocalDates.kt (94%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{utilities => extensions}/MoreLocalTimes.kt (93%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnaireItemAnswerOptionComponents.kt (100%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnaireItemComponents.kt (97%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnaireResponseItemAnswerComponents.kt (100%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnaires.kt (92%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnairesResponseItemComponents.kt (100%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnairesResponses.kt (100%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{common/datatype => extensions}/MoreTypes.kt (81%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/{utilities => fhirpath}/FhirPathUtil.kt (95%) delete mode 100644 datacapture/src/main/java/com/google/android/fhir/datacapture/utilities/MoreReflectionUtil.kt delete mode 100644 datacapture/src/main/java/com/google/android/fhir/datacapture/utilities/TypeConversionUtil.kt rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireGroupTypeHeaderView.kt => GroupHeaderView.kt} (90%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemHeaderView.kt => HeaderView.kt} (95%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemMediaView.kt => MediaView.kt} (93%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemViewItem.kt => QuestionnaireViewItem.kt} (89%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemAttachmentViewHolderFactory.kt => factories/AttachmentViewHolderFactory.kt} (92%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemAutoCompleteViewHolderFactory.kt => factories/AutoCompleteViewHolderFactory.kt} (83%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemBooleanTypePickerViewHolderFactory.kt => factories/BooleanChoiceViewHolderFactory.kt} (80%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemCheckBoxGroupViewHolderFactory.kt => factories/CheckBoxGroupViewHolderFactory.kt} (80%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDatePickerViewHolderFactory.kt => factories/DatePickerViewHolderFactory.kt} (87%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDateTimePickerViewHolderFactory.kt => factories/DateTimePickerViewHolderFactory.kt} (87%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDialogSelectViewHolderFactory.kt => factories/DialogSelectViewHolderFactory.kt} (87%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDisplayViewHolderFactory.kt => factories/DisplayViewHolderFactory.kt} (64%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDropDownViewHolderFactory.kt => factories/DropDownViewHolderFactory.kt} (83%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextDecimalViewHolderFactory.kt => factories/EditTextDecimalViewHolderFactory.kt} (78%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextIntegerViewHolderFactory.kt => factories/EditTextIntegerViewHolderFactory.kt} (78%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextMultiLineViewHolderFactory.kt => factories/EditTextMultiLineViewHolderFactory.kt} (65%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextQuantityViewHolderFactory.kt => factories/EditTextQuantityViewHolderFactory.kt} (83%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextSingleLineViewHolderFactory.kt => factories/EditTextSingleLineViewHolderFactory.kt} (65%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextStringViewHolderDelegate.kt => factories/EditTextStringViewHolderDelegate.kt} (74%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextViewHolderFactory.kt => factories/EditTextViewHolderFactory.kt} (79%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemGroupViewHolderFactory.kt => factories/GroupViewHolderFactory.kt} (73%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{ => factories}/QuestionnaireItemViewHolderFactory.kt (75%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemRadioGroupViewHolderFactory.kt => factories/RadioGroupViewHolderFactory.kt} (85%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemSimpleQuestionAnswerDisplayViewHolderFactory.kt => factories/ReviewViewHolderFactory.kt} (75%) rename datacapture/src/main/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemSliderViewHolderFactory.kt => factories/SliderViewHolderFactory.kt} (78%) rename datacapture/src/main/res/layout/{questionnaire_item_attachment_view.xml => attachment_view.xml} (97%) rename datacapture/src/main/res/layout/{questionnaire_item_boolean_type_picker_view.xml => boolean_choice_view.xml} (95%) rename datacapture/src/main/res/layout/{questionnaire_item_check_box_view.xml => check_box_view.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_checkbox_group_view.xml => checkbox_group_view.xml} (93%) rename datacapture/src/main/res/layout/{questionnaire_item_date_picker_view.xml => date_picker_view.xml} (93%) rename datacapture/src/main/res/layout/{questionnaire_item_date_time_picker_view.xml => date_time_picker_view.xml} (96%) rename datacapture/src/main/res/layout/{questionnaire_item_display_view.xml => display_view.xml} (89%) rename datacapture/src/main/res/layout/{questionnaire_item_drop_down_list_item.xml => drop_down_list_item.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_drop_down_view.xml => drop_down_view.xml} (92%) rename datacapture/src/main/res/layout/{questionnaire_item_edit_text_auto_complete_view.xml => edit_text_auto_complete_view.xml} (95%) rename datacapture/src/main/res/layout/{questionnaire_item_edit_text_multi_line_view.xml => edit_text_multi_line_view.xml} (92%) rename datacapture/src/main/res/layout/{questionnaire_item_edit_text_single_line_view.xml => edit_text_single_line_view.xml} (92%) rename datacapture/src/main/res/layout/{questionnaire_item_group_header_view.xml => group_header_view.xml} (93%) rename datacapture/src/main/res/layout/{questionnaire_group_type_header.xml => group_type_header_view.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_header.xml => header_view.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_media.xml => media_view.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_multi_select_dialog.xml => multi_select_dialog.xml} (97%) rename datacapture/src/main/res/layout/{questionnaire_item_option_item_multi.xml => option_item_multi.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_option_item_other_add_another.xml => option_item_other_add_another.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_option_item_other_text.xml => option_item_other_text.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_option_item_single.xml => option_item_single.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_option_select_view.xml => option_select_view.xml} (92%) rename datacapture/src/main/res/layout/{questionnaire_item_radio_button.xml => radio_button.xml} (100%) rename datacapture/src/main/res/layout/{questionnaire_item_radio_group_view.xml => radio_group_view.xml} (93%) rename datacapture/src/main/res/layout/{questionnaire_item_simple_question_answer_view.xml => review_view.xml} (92%) rename datacapture/src/main/res/layout/{questionnaire_item_slider.xml => slider_view.xml} (91%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{QuestionnaireItemEditAdapterTest.kt => QuestionnaireEditAdapterTest.kt} (77%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{QuestionnaireItemReviewAdapterTest.kt => QuestionnaireReviewAdapterTest.kt} (73%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{QuestionnaireItemViewHolderTypeTest.kt => QuestionnaireViewHolderTypeTest.kt} (80%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{utilities => extensions}/MoreContextsTest.kt (92%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{utilities/MoreReflectionUtilTest.kt => extensions/MoreEnumerationsTest.kt} (65%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{ => extensions}/MoreExpressionsTest.kt (91%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{utilities => extensions}/MoreLocalDateTimesTest.kt (94%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{utilities => extensions}/MoreLocalDatesTest.kt (91%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{utilities => extensions}/MoreLocalTimesTest.kt (95%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{MoreQuestionnaireItemAnswerOptionComponent.kt => extensions/MoreQuestionnaireItemAnswerOptionComponentsTest.kt} (96%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnaireItemComponentsTest.kt (94%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{MoreQuestionnaireResponseItemAnswerComponentTest.kt => extensions/MoreQuestionnaireResponseItemAnswerComponentsTest.kt} (97%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnaireResponseItemComponentsTest.kt (95%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnaireResponsesTest.kt (95%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{ => extensions}/MoreQuestionnairesTest.kt (93%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{common/datatype => extensions}/MoreTypesTest.kt (83%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/{utilities => fhirpath}/FhirPathUtilTest.kt (96%) delete mode 100644 datacapture/src/test/java/com/google/android/fhir/datacapture/utilities/MoreEnumerationTest.kt delete mode 100644 datacapture/src/test/java/com/google/android/fhir/datacapture/utilities/TypeConversionUtilTest.kt rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireGroupTypeHeaderViewTest.kt => GroupHeaderViewTest.kt} (98%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemHeaderViewTest.kt => HeaderViewTest.kt} (98%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemViewItemTest.kt => QuestionnaireViewItemTest.kt} (89%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemAutoCompleteViewHolderFactoryTest.kt => factories/AutoCompleteViewHolderFactoryTest.kt} (95%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemBooleanTypePickerViewHolderFactoryTest.kt => factories/BooleanChoiceViewHolderFactoryTest.kt} (86%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemCheckBoxGroupViewHolderFactoryTest.kt => factories/CheckBoxGroupViewHolderFactoryTest.kt} (97%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDatePickerViewHolderFactoryTest.kt => factories/DatePickerViewHolderFactoryTest.kt} (94%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDateTimePickerViewHolderFactoryTest.kt => factories/DateTimePickerViewHolderFactoryTest.kt} (94%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDisplayViewHolderFactoryTest.kt => factories/DisplayViewHolderFactoryTest.kt} (87%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemDropDownViewHolderFactoryTest.kt => factories/DropDownViewHolderFactoryTest.kt} (94%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextDecimalViewHolderFactoryTest.kt => factories/EditTextDecimalViewHolderFactoryTest.kt} (89%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextIntegerViewHolderFactoryTest.kt => factories/EditTextIntegerViewHolderFactoryTest.kt} (90%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextMultiLineViewHolderFactoryTest.kt => factories/EditTextMultiLineViewHolderFactoryTest.kt} (89%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextQuantityViewHolderFactoryTest.kt => factories/EditTextQuantityViewHolderFactoryTest.kt} (88%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemEditTextSingleLineViewHolderFactoryTest.kt => factories/EditTextSingleLineViewHolderFactoryTest.kt} (89%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemGroupViewHolderFactoryTest.kt => factories/GroupViewHolderFactoryTest.kt} (87%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemRadioGroupViewHolderFactoryTest.kt => factories/RadioGroupViewHolderFactoryTest.kt} (97%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemSimpleQuestionAnswerDisplayViewHolderFactoryTest.kt => factories/ReviewViewHolderFactoryTest.kt} (94%) rename datacapture/src/test/java/com/google/android/fhir/datacapture/views/{QuestionnaireItemSliderViewHolderFactoryTest.kt => factories/SliderViewHolderFactoryTest.kt} (93%) diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentsRecyclerViewadapter.kt b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentsRecyclerViewadapter.kt index fe5881e647..b503e7d8c3 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/ComponentsRecyclerViewadapter.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/ComponentsRecyclerViewadapter.kt @@ -81,8 +81,8 @@ class ComponentListViewHolder( class ComponentHeaderViewHolder(private val binding: ComponentHeaderLayoutBinding) : RecyclerView.ViewHolder(binding.root), ComponentViewHolder { - override fun bind(viewItem: ComponentListViewModel.ViewItem) { - val headerItem = viewItem as ComponentListViewModel.ViewItem.HeaderItem + override fun bind(component: ComponentListViewModel.ViewItem) { + val headerItem = component as ComponentListViewModel.ViewItem.HeaderItem binding.tvComponentHeader.text = binding.tvComponentHeader.context.getString(headerItem.header.textId) } diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/CustomNumberPickerFactory.kt b/catalog/src/main/java/com/google/android/fhir/catalog/CustomNumberPickerFactory.kt index 83e6d58889..1aa2c21000 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/CustomNumberPickerFactory.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/CustomNumberPickerFactory.kt @@ -18,16 +18,16 @@ package com.google.android.fhir.catalog import android.view.View import android.widget.NumberPicker -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolderDelegate -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem +import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderDelegate +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory object CustomNumberPickerFactory : QuestionnaireItemViewHolderFactory(R.layout.custom_number_picker_layout) { override fun getQuestionnaireItemViewHolderDelegate(): QuestionnaireItemViewHolderDelegate = object : QuestionnaireItemViewHolderDelegate { private lateinit var numberPicker: NumberPicker - override lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem + override lateinit var questionnaireViewItem: QuestionnaireViewItem override fun init(itemView: View) { /** @@ -37,7 +37,7 @@ object CustomNumberPickerFactory : numberPicker = itemView.findViewById(R.id.number_picker) } - override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) { + override fun bind(questionnaireViewItem: QuestionnaireViewItem) { numberPicker.minValue = 1 numberPicker.maxValue = 100 } diff --git a/catalog/src/main/java/com/google/android/fhir/catalog/CustomQuestionnaireFragment.kt b/catalog/src/main/java/com/google/android/fhir/catalog/CustomQuestionnaireFragment.kt index 92aa9181f3..9d78a1be48 100644 --- a/catalog/src/main/java/com/google/android/fhir/catalog/CustomQuestionnaireFragment.kt +++ b/catalog/src/main/java/com/google/android/fhir/catalog/CustomQuestionnaireFragment.kt @@ -17,7 +17,7 @@ package com.google.android.fhir.catalog import com.google.android.fhir.datacapture.QuestionnaireFragment.QuestionnaireItemViewHolderFactoryMatcher -import com.google.android.fhir.datacapture.contrib.views.barcode.QuestionnaireItemBarCodeReaderViewHolderFactory +import com.google.android.fhir.datacapture.contrib.views.barcode.BarCodeReaderViewHolderFactory // TODO Remove this file and move this code to maybe a custom view in catalog app? class CustomQuestionnaireFragment /*: QuestionnaireFragment()*/ { @@ -29,14 +29,12 @@ class CustomQuestionnaireFragment /*: QuestionnaireFragment()*/ { if (it == null) false else it.value.toString() == CustomNumberPickerFactory.WIDGET_TYPE } }, - QuestionnaireItemViewHolderFactoryMatcher(QuestionnaireItemBarCodeReaderViewHolderFactory) { - questionnaireItem -> - questionnaireItem - .getExtensionByUrl(QuestionnaireItemBarCodeReaderViewHolderFactory.WIDGET_EXTENSION) - .let { - if (it == null) false - else it.value.toString() == QuestionnaireItemBarCodeReaderViewHolderFactory.WIDGET_TYPE - } + QuestionnaireItemViewHolderFactoryMatcher(BarCodeReaderViewHolderFactory) { questionnaireItem + -> + questionnaireItem.getExtensionByUrl(BarCodeReaderViewHolderFactory.WIDGET_EXTENSION).let { + if (it == null) false + else it.value.toString() == BarCodeReaderViewHolderFactory.WIDGET_TYPE + } } ) } diff --git a/contrib/barcode/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/barcode/QuestionnaireItemBarCodeReaderViewHolderFactoryInstrumentedTest.kt b/contrib/barcode/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/barcode/BarCodeReaderViewHolderFactoryInstrumentedTest.kt similarity index 90% rename from contrib/barcode/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/barcode/QuestionnaireItemBarCodeReaderViewHolderFactoryInstrumentedTest.kt rename to contrib/barcode/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/barcode/BarCodeReaderViewHolderFactoryInstrumentedTest.kt index 552a88495c..8b701bdcac 100644 --- a/contrib/barcode/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/barcode/QuestionnaireItemBarCodeReaderViewHolderFactoryInstrumentedTest.kt +++ b/contrib/barcode/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/barcode/BarCodeReaderViewHolderFactoryInstrumentedTest.kt @@ -24,8 +24,8 @@ import androidx.test.annotation.UiThreadTest import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.google.android.fhir.datacapture.validation.NotValidated -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolder -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem +import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import com.google.common.truth.Truth.assertThat import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -35,7 +35,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class QuestionnaireItemBarCodeReaderViewHolderFactoryInstrumentedTest { +class BarCodeReaderViewHolderFactoryInstrumentedTest { private lateinit var context: ContextThemeWrapper private lateinit var parent: FrameLayout @@ -49,13 +49,13 @@ class QuestionnaireItemBarCodeReaderViewHolderFactoryInstrumentedTest { R.style.Theme_MaterialComponents ) parent = FrameLayout(context) - viewHolder = QuestionnaireItemBarCodeReaderViewHolderFactory.create(parent) + viewHolder = BarCodeReaderViewHolderFactory.create(parent) } @Test fun shouldShowPrefixText() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { prefix = "Prefix?" }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -72,7 +72,7 @@ class QuestionnaireItemBarCodeReaderViewHolderFactoryInstrumentedTest { @Test fun shouldHidePrefixText() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { prefix = "" }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -87,7 +87,7 @@ class QuestionnaireItemBarCodeReaderViewHolderFactoryInstrumentedTest { @Test fun shouldSetTextViewText() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -104,7 +104,7 @@ class QuestionnaireItemBarCodeReaderViewHolderFactoryInstrumentedTest { @UiThreadTest fun shouldSetInputText() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, QuestionnaireResponse.QuestionnaireResponseItemComponent() .addAnswer( diff --git a/contrib/barcode/src/main/java/com/google/android/fhir/datacapture/contrib/views/barcode/QuestionnaireItemBarCodeReaderViewHolderFactory.kt b/contrib/barcode/src/main/java/com/google/android/fhir/datacapture/contrib/views/barcode/BarCodeReaderViewHolderFactory.kt similarity index 75% rename from contrib/barcode/src/main/java/com/google/android/fhir/datacapture/contrib/views/barcode/QuestionnaireItemBarCodeReaderViewHolderFactory.kt rename to contrib/barcode/src/main/java/com/google/android/fhir/datacapture/contrib/views/barcode/BarCodeReaderViewHolderFactory.kt index 4f0c6b7367..154a5336be 100644 --- a/contrib/barcode/src/main/java/com/google/android/fhir/datacapture/contrib/views/barcode/QuestionnaireItemBarCodeReaderViewHolderFactory.kt +++ b/contrib/barcode/src/main/java/com/google/android/fhir/datacapture/contrib/views/barcode/BarCodeReaderViewHolderFactory.kt @@ -22,14 +22,14 @@ import android.widget.TextView import com.google.android.fhir.datacapture.contrib.views.barcode.mlkit.md.LiveBarcodeScanningFragment import com.google.android.fhir.datacapture.localizedPrefixSpanned import com.google.android.fhir.datacapture.localizedTextSpanned -import com.google.android.fhir.datacapture.utilities.tryUnwrapContext -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolderDelegate -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem +import com.google.android.fhir.datacapture.tryUnwrapContext +import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderDelegate +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.StringType -object QuestionnaireItemBarCodeReaderViewHolderFactory : +object BarCodeReaderViewHolderFactory : QuestionnaireItemViewHolderFactory(R.layout.questionnaire_item_bar_code_reader_view) { override fun getQuestionnaireItemViewHolderDelegate() = object : QuestionnaireItemViewHolderDelegate { @@ -37,7 +37,7 @@ object QuestionnaireItemBarCodeReaderViewHolderFactory : private lateinit var textQuestion: TextView private lateinit var barcodeTextView: TextView private lateinit var reScanView: TextView - override lateinit var questionnaireItemViewItem: QuestionnaireItemViewItem + override lateinit var questionnaireViewItem: QuestionnaireViewItem override fun init(itemView: View) { prefixTextView = itemView.findViewById(R.id.prefix) @@ -69,31 +69,31 @@ object QuestionnaireItemBarCodeReaderViewHolderFactory : } if (answer == null) { - questionnaireItemViewItem.clearAnswer() + questionnaireViewItem.clearAnswer() } else { - questionnaireItemViewItem.setAnswer(answer) + questionnaireViewItem.setAnswer(answer) } - setInitial(questionnaireItemViewItem.answers.singleOrNull(), reScanView) + setInitial(questionnaireViewItem.answers.singleOrNull(), reScanView) } LiveBarcodeScanningFragment() .show( context.supportFragmentManager, - QuestionnaireItemBarCodeReaderViewHolderFactory.javaClass.simpleName + BarCodeReaderViewHolderFactory.javaClass.simpleName ) } } - override fun bind(questionnaireItemViewItem: QuestionnaireItemViewItem) { - this.questionnaireItemViewItem = questionnaireItemViewItem - if (!questionnaireItemViewItem.questionnaireItem.prefix.isNullOrEmpty()) { + override fun bind(questionnaireViewItem: QuestionnaireViewItem) { + this.questionnaireViewItem = questionnaireViewItem + if (!questionnaireViewItem.questionnaireItem.prefix.isNullOrEmpty()) { prefixTextView.visibility = View.VISIBLE - prefixTextView.text = questionnaireItemViewItem.questionnaireItem.localizedPrefixSpanned + prefixTextView.text = questionnaireViewItem.questionnaireItem.localizedPrefixSpanned } else { prefixTextView.visibility = View.GONE } - textQuestion.text = questionnaireItemViewItem.questionnaireItem.localizedTextSpanned - setInitial(questionnaireItemViewItem.answers.singleOrNull(), reScanView) + textQuestion.text = questionnaireViewItem.questionnaireItem.localizedTextSpanned + setInitial(questionnaireViewItem.answers.singleOrNull(), reScanView) } private fun setInitial( diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/QuestionnaireUiEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/QuestionnaireUiEspressoTest.kt index b5d551ac28..af65495c5b 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/QuestionnaireUiEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/QuestionnaireUiEspressoTest.kt @@ -32,7 +32,7 @@ import com.google.android.fhir.datacapture.TestQuestionnaireFragment.Companion.Q import com.google.android.fhir.datacapture.test.R import com.google.android.fhir.datacapture.utilities.clickIcon import com.google.android.fhir.datacapture.utilities.clickOnText -import com.google.android.fhir.datacapture.views.localDateTime +import com.google.android.fhir.datacapture.views.factories.localDateTime import com.google.android.material.textfield.TextInputLayout import com.google.common.truth.Truth.assertThat import java.time.LocalDateTime diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactoryInstrumentedTest.kt similarity index 83% rename from datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest.kt rename to datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactoryInstrumentedTest.kt index 5c04b3a627..518a9154b0 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactoryInstrumentedTest.kt @@ -22,13 +22,13 @@ import androidx.appcompat.view.ContextThemeWrapper import androidx.test.annotation.UiThreadTest import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.google.android.fhir.datacapture.QuestionnaireItemEditAdapter -import com.google.android.fhir.datacapture.QuestionnaireItemViewHolderType +import com.google.android.fhir.datacapture.QuestionnaireEditAdapter +import com.google.android.fhir.datacapture.QuestionnaireViewHolderType import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.NotValidated -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolder -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem +import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.google.common.truth.Truth.assertThat @@ -42,11 +42,11 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest { +class PhoneNumberViewHolderFactoryInstrumentedTest { private lateinit var context: ContextThemeWrapper private lateinit var parent: FrameLayout private lateinit var viewHolder: QuestionnaireItemViewHolder - private lateinit var questionnaireItemEditAdapter: QuestionnaireItemEditAdapter + private lateinit var questionnaireEditAdapter: QuestionnaireEditAdapter @Before fun setUp() { @@ -56,18 +56,18 @@ class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest { R.style.Theme_Material3_DayNight ) parent = FrameLayout(context) - viewHolder = QuestionnaireItemPhoneNumberViewHolderFactory.create(parent) - questionnaireItemEditAdapter = QuestionnaireItemEditAdapter() + viewHolder = PhoneNumberViewHolderFactory.create(parent) + questionnaireEditAdapter = QuestionnaireEditAdapter() } @Test fun createViewHolder_shouldReturn_phoneNumberViewHolder() { val viewHolderFromAdapter = - questionnaireItemEditAdapter.createViewHolder( + questionnaireEditAdapter.createViewHolder( parent, - QuestionnaireItemEditAdapter.ViewType.from( - type = QuestionnaireItemEditAdapter.ViewType.Type.QUESTION, - subtype = QuestionnaireItemViewHolderType.PHONE_NUMBER.value, + QuestionnaireEditAdapter.ViewType.from( + type = QuestionnaireEditAdapter.ViewType.Type.QUESTION, + subtype = QuestionnaireViewHolderType.PHONE_NUMBER.value, ) .viewType, ) @@ -77,7 +77,7 @@ class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest { @Test fun shouldSetTextViewText() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -92,7 +92,7 @@ class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest { @UiThreadTest fun shouldSetInputText() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { text = "Question?" }, QuestionnaireResponse.QuestionnaireResponseItemComponent() .addAnswer( @@ -117,7 +117,7 @@ class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest { @UiThreadTest fun shouldSetInputTextToEmpty() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent() .addAnswer( @@ -130,7 +130,7 @@ class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest { ) ) viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -150,42 +150,42 @@ class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest { @UiThreadTest @Ignore("https://github.com/google/android-fhir/issues/1494") fun shouldSetQuestionnaireResponseItemAnswer() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - viewHolder.bind(questionnaireItemViewItem) + viewHolder.bind(questionnaireViewItem) viewHolder.itemView .findViewById(R.id.text_input_edit_text) .setText("+12345678910") - assertThat(questionnaireItemViewItem.answers.single().valueStringType.value) + assertThat(questionnaireViewItem.answers.single().valueStringType.value) .isEqualTo("+12345678910") } @Test @UiThreadTest fun shouldSetQuestionnaireResponseItemAnswerToEmpty() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - viewHolder.bind(questionnaireItemViewItem) + viewHolder.bind(questionnaireViewItem) viewHolder.itemView.findViewById(R.id.text_input_edit_text).setText("") - assertThat(questionnaireItemViewItem.answers).isEmpty() + assertThat(questionnaireViewItem.answers).isEmpty() } @Test @UiThreadTest fun displayValidationResult_noError_shouldShowNoErrorMessage() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/minLength" @@ -212,7 +212,7 @@ class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest { @UiThreadTest fun displayValidationResult_error_shouldShowErrorMessage() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { maxLength = 10 }, QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { addAnswer( @@ -236,7 +236,7 @@ class QuestionnaireItemPhoneNumberViewHolderFactoryInstrumentedTest { @UiThreadTest fun bind_readOnly_shouldDisableView() { viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { readOnly = true }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemAttachmentViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/AttachmentViewHolderFactoryEspressoTest.kt similarity index 96% rename from datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemAttachmentViewHolderFactoryEspressoTest.kt rename to datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/AttachmentViewHolderFactoryEspressoTest.kt index 7fe80d8cf8..47e5f03b1b 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemAttachmentViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/AttachmentViewHolderFactoryEspressoTest.kt @@ -28,6 +28,8 @@ import androidx.test.platform.app.InstrumentationRegistry import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.TestActivity import com.google.android.fhir.datacapture.validation.NotValidated +import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import com.google.common.truth.Truth.assertThat import org.hl7.fhir.r4.model.Attachment import org.hl7.fhir.r4.model.CodeType @@ -39,7 +41,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { +class AttachmentViewHolderFactoryEspressoTest { @Rule @JvmField @@ -52,14 +54,14 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Before fun setup() { activityScenarioRule.getScenario().onActivity { activity -> parent = FrameLayout(activity) } - viewHolder = QuestionnaireItemAttachmentViewHolderFactory.create(parent) + viewHolder = AttachmentViewHolderFactory.create(parent) setTestLayout(viewHolder.itemView) } @Test fun shouldDisplayTakePhotoAndUploadPhotoButton() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -86,7 +88,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun shouldDisplayUploadAudioButton() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -110,7 +112,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun shouldDisplayUploadVideoButton() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -134,7 +136,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun shouldDisplayUploadDocumentButton() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -158,7 +160,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun shouldDisplayTakePhotoAndUploadFileButton() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -189,7 +191,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun shouldDisplayImagePreviewFromAnswer() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -227,7 +229,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun shouldDisplayAudioFilePreviewFromAnswer() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -266,7 +268,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun shouldDisplayVideoFilePreviewFromAnswer() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -305,7 +307,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun shouldDisplayDocumentFilePreviewFromAnswer() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -344,7 +346,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun doNotShowPreviewIfAnswerDoesNotHaveAttachment() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -372,7 +374,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { @Test fun doNotShowPreviewOfPreviousAnswerAttachmentForCurrentAnswerItem() { val questionnaireItem = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" @@ -409,7 +411,7 @@ class QuestionnaireItemAttachmentViewHolderFactoryEspressoTest { .isEqualTo(View.VISIBLE) val questionnaireItemWithNullAnswer = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/mimeType" diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/DatePickerViewHolderFactoryEspressoTest.kt similarity index 95% rename from datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryEspressoTest.kt rename to datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/DatePickerViewHolderFactoryEspressoTest.kt index 114d4dcc66..814f24199e 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDatePickerViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/DatePickerViewHolderFactoryEspressoTest.kt @@ -36,6 +36,8 @@ import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.NotValidated import com.google.android.fhir.datacapture.validation.QuestionnaireResponseItemValidator import com.google.android.fhir.datacapture.validation.Valid +import com.google.android.fhir.datacapture.views.factories.DatePickerViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import com.google.android.material.textfield.TextInputLayout import com.google.common.truth.Truth.assertThat import java.util.Calendar @@ -53,7 +55,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { +class DatePickerViewHolderFactoryEspressoTest { @Rule @JvmField @@ -66,7 +68,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { fun setup() { Locale.setDefault(Locale.US) activityScenarioRule.getScenario().onActivity { activity -> parent = FrameLayout(activity) } - viewHolder = QuestionnaireItemDatePickerViewHolderFactory.create(parent) + viewHolder = DatePickerViewHolderFactory.create(parent) setTestLayout(viewHolder.itemView) } @@ -74,7 +76,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { fun shouldSetDateInput() { var answerHolder: List? = null val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -98,7 +100,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { fun shouldSetDateInput_withinRange() { var answerHolder: List? = null val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/minValue" @@ -139,7 +141,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { @Test fun shouldSetDateInput_invalid_date_entry_invalid_month_day_year() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -158,7 +160,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { @Test fun shouldSetDateInput_invalid_date_entry_invalid_day() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -177,7 +179,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { @Test fun shouldSetDateInput_invalid_date_entry_invalid_month() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -196,7 +198,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { @Test fun shouldSetDateInput_invalid_date_entry_invalid_year() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -216,7 +218,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { var answerHolder: List? = null val maxDate = DateType(Date()).apply { add(Calendar.YEAR, -2) } val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/minValue" @@ -258,7 +260,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { var answerHolder: List? = null val minDate = DateType(Date()).apply { add(Calendar.YEAR, 1) } val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/minValue" @@ -298,7 +300,7 @@ class QuestionnaireItemDatePickerViewHolderFactoryEspressoTest { @Test fun shouldThrowException_whenMinValueRangeIsGreaterThanMaxValueRange() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { addExtension().apply { url = "http://hl7.org/fhir/StructureDefinition/minValue" diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/DateTimePickerViewHolderFactoryEspressoTest.kt similarity index 92% rename from datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest.kt rename to datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/DateTimePickerViewHolderFactoryEspressoTest.kt index 433e7ed78f..570308552b 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/DateTimePickerViewHolderFactoryEspressoTest.kt @@ -32,6 +32,8 @@ import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.TestActivity import com.google.android.fhir.datacapture.utilities.clickIcon import com.google.android.fhir.datacapture.validation.NotValidated +import com.google.android.fhir.datacapture.views.factories.DateTimePickerViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import org.hamcrest.CoreMatchers.allOf import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -41,7 +43,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest { +class DateTimePickerViewHolderFactoryEspressoTest { @Rule @JvmField @@ -53,14 +55,14 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest { @Before fun setup() { activityScenarioRule.scenario.onActivity { activity -> parent = FrameLayout(activity) } - viewHolder = QuestionnaireItemDateTimePickerViewHolderFactory.create(parent) + viewHolder = DateTimePickerViewHolderFactory.create(parent) setTestLayout(viewHolder.itemView) } @Test fun showsTimePickerInInputMode() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, @@ -83,7 +85,7 @@ class QuestionnaireItemDateTimePickerViewHolderFactoryEspressoTest { @Test fun showsTimePickerInClockMode() { val questionnaireItemView = - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent(), QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDropDownViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/DropDownViewHolderFactoryEspressoTest.kt similarity index 96% rename from datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDropDownViewHolderFactoryEspressoTest.kt rename to datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/DropDownViewHolderFactoryEspressoTest.kt index 6c250f26a9..5b5e4bc4be 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDropDownViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/DropDownViewHolderFactoryEspressoTest.kt @@ -34,6 +34,8 @@ import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.TestActivity import com.google.android.fhir.datacapture.utilities.showDropDown import com.google.android.fhir.datacapture.validation.NotValidated +import com.google.android.fhir.datacapture.views.factories.DropDownViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.common.truth.Truth.assertThat import org.hl7.fhir.r4.model.Attachment @@ -46,7 +48,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test -class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { +class DropDownViewHolderFactoryEspressoTest { @Rule @JvmField var activityScenarioRule: ActivityScenarioRule = @@ -69,39 +71,39 @@ class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { @Before fun setup() { activityScenarioRule.scenario.onActivity { activity -> parent = FrameLayout(activity) } - viewHolder = QuestionnaireItemDropDownViewHolderFactory.create(parent) + viewHolder = DropDownViewHolderFactory.create(parent) setTestLayout(viewHolder.itemView) } @Test fun shouldClearAutoCompleteTextView() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions("Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.auto_complete)).perform(showDropDown()) onView(withText("-")).inRoot(isPlatformPopup()).check(matches(isDisplayed())).perform(click()) assertThat(viewHolder.itemView.findViewById(R.id.auto_complete).text.toString()) .isEqualTo("-") - assertThat(questionnaireItemViewItem.answers).isEmpty() + assertThat(questionnaireViewItem.answers).isEmpty() } @Test fun shouldSetDropDownValueToAutoCompleteTextView() { var answerHolder: List? = null - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions("Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.auto_complete)).perform(showDropDown()) onView(withText("Coding 3")) @@ -116,14 +118,14 @@ class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { @Test fun shouldClearAutoCompleteTextViewOnRebindingView() { var answerHolder: List? = null - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions("Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.auto_complete)).perform(showDropDown()) onView(withText("Coding 3")) @@ -133,7 +135,7 @@ class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { assertThat(viewHolder.itemView.findViewById(R.id.auto_complete).text.toString()) .isEqualTo("Coding 3") - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } assertThat(viewHolder.itemView.findViewById(R.id.auto_complete).text.toString()) .isEqualTo("") val autoCompleteTextView = @@ -147,8 +149,8 @@ class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { @Test fun shouldSetImageToAutoCompleteTextViewOnItemSelection() { var answerHolder: List? = null - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( createAnswerOptions( "Coding 1", "Coding 2", @@ -161,7 +163,7 @@ class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { validationResult = NotValidated, answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.auto_complete)).perform(showDropDown()) onView(withText("Coding 3")) @@ -181,14 +183,14 @@ class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { @Test fun shouldSetDropDownValueStringToAutoCompleteTextView() { var answerHolder: List? = null - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( createAnswerOptions("Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseValueStringOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.auto_complete)).perform(showDropDown()) onView(withText("Coding 1")) @@ -202,14 +204,14 @@ class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { @Test fun shouldReturnNonFilteredDropDownMenuItems() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( createAnswerOptions("Coding 1", "Coding 2", "Coding 3", "Add", "Subtract"), responseValueStringOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.auto_complete)).perform(showDropDown()) assertThat( @@ -221,14 +223,14 @@ class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { } @Test fun shouldReturnFilteredDropDownMenuItems() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( createAnswerOptions("Coding 1", "Coding 2", "Coding 3", "Add", "Subtract"), responseValueStringOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.auto_complete)).perform(showDropDown()) onView(withId(R.id.auto_complete)).perform(typeText("Coding")) @@ -242,14 +244,14 @@ class QuestionnaireItemDropDownViewHolderFactoryEspressoTest { @Test fun shouldReturnFilteredWithNoResultsDropDownMenuItems() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( createAnswerOptions("Coding 1", "Coding 2", "Coding 3", "Add", "Subtract"), responseValueStringOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.auto_complete)).perform(showDropDown()) onView(withId(R.id.auto_complete)).perform(typeText("Division")) diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemMediaViewInstrumentedTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/MediaViewInstrumentedTest.kt similarity index 97% rename from datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemMediaViewInstrumentedTest.kt rename to datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/MediaViewInstrumentedTest.kt index 1d121f1fdd..a4e3eb96fa 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemMediaViewInstrumentedTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/MediaViewInstrumentedTest.kt @@ -37,7 +37,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class QuestionnaireItemMediaViewInstrumentedTest { +class MediaViewInstrumentedTest { @Rule @JvmField @@ -45,12 +45,12 @@ class QuestionnaireItemMediaViewInstrumentedTest { ActivityScenarioRule(TestActivity::class.java) private lateinit var parent: FrameLayout - private lateinit var view: QuestionnaireItemMediaView + private lateinit var view: MediaView @Before fun setUp() { activityScenarioRule.scenario.onActivity { activity -> parent = FrameLayout(activity) } - view = QuestionnaireItemMediaView(parent.context, null) + view = MediaView(parent.context, null) setTestLayout(view) } diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt index 41e321b220..23f1726d91 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt @@ -42,6 +42,8 @@ import com.google.android.fhir.datacapture.utilities.clickOnText import com.google.android.fhir.datacapture.utilities.clickOnTextInDialog import com.google.android.fhir.datacapture.utilities.endIconClickInTextInputLayout import com.google.android.fhir.datacapture.validation.NotValidated +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemDialogSelectViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import com.google.android.material.textfield.TextInputLayout import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertThat @@ -74,15 +76,15 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { @Test fun multipleChoice_selectMultiple_clickSave_shouldSaveMultipleOptions() { var answerHolder: List? = null - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions(true, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) clickOnTextInDialog("Coding 1") @@ -96,34 +98,34 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { @Test fun multipleChoice_SelectNothing_clickSave_shouldSaveNothing() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions(true, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) clickOnTextInDialog("Save") assertDisplayedText().isEmpty() - assertThat(questionnaireItemViewItem.answers).isEmpty() + assertThat(questionnaireViewItem.answers).isEmpty() } @Test fun multipleChoice_selectMultiple_clickCancel_shouldSaveNothing() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions(true, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) clickOnTextInDialog("Coding 3") @@ -131,21 +133,21 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { clickOnText("Cancel") assertDisplayedText().isEmpty() - assertThat(questionnaireItemViewItem.answers).isEmpty() + assertThat(questionnaireViewItem.answers).isEmpty() } @Test fun shouldSelectSingleOptionOnChangeInOptionFromDropDown() { var answerHolder: List? = null - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions(false, "Coding 1", "Coding 2", "Coding 3"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) clickOnTextInDialog("Coding 2") @@ -159,15 +161,15 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { @Test fun singleOption_select_clickSave_shouldSaveSingleOption() { var answerHolder: List? = null - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions(false, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) clickOnTextInDialog("Coding 2") @@ -179,27 +181,27 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { @Test fun singleOption_selectNothing_clickSave_shouldSaveNothing() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions(false, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) clickOnTextInDialog("Save") assertDisplayedText().isEmpty() - assertThat(questionnaireItemViewItem.answers).isEmpty() + assertThat(questionnaireViewItem.answers).isEmpty() } @Test fun bindView_setHintText() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions(false, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5") .addItem( Questionnaire.QuestionnaireItemComponent().apply { @@ -224,7 +226,7 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } assertThat( viewHolder.itemView @@ -236,21 +238,21 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { @Test fun singleOption_select_clickCancel_shouldSaveNothing() { - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( answerOptions(false, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"), responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) clickOnTextInDialog("Coding 2") clickOnText("Cancel") assertDisplayedText().isEmpty() - assertThat(questionnaireItemViewItem.answers).isEmpty() + assertThat(questionnaireViewItem.answers).isEmpty() } @Test @@ -268,15 +270,15 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { "Coding 8" ) questionnaireItem.addExtension(openChoiceType) - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( questionnaireItem, responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) onView(withId(R.id.recycler_view)) @@ -300,15 +302,15 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { "Coding 8" ) questionnaireItem.addExtension(openChoiceType) - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( questionnaireItem, responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) onView(withId(R.id.recycler_view)) @@ -333,15 +335,15 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { "Coding 8" ) questionnaireItem.addExtension(openChoiceType) - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( questionnaireItem, responseOptions(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } endIconClickInTextInputLayout(R.id.multi_select_summary_holder) onView(withId(R.id.recycler_view)) @@ -355,15 +357,15 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { fun `shouldHideErrorTextviewInHeader`() { val questionnaireItem = answerOptions(true, "Coding 1") questionnaireItem.addExtension(openChoiceType) - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( questionnaireItem, QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, answersChangedCallback = { _, _, _, _ -> }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.error_text_at_header)).check(matches(not(isDisplayed()))) } diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt index 0e7590a007..da7a5ecd4f 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest.kt @@ -28,6 +28,8 @@ import androidx.test.platform.app.InstrumentationRegistry import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.TestActivity import com.google.android.fhir.datacapture.validation.NotValidated +import com.google.android.fhir.datacapture.views.factories.EditTextQuantityViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import com.google.common.truth.Truth.assertThat import java.math.BigDecimal import org.hl7.fhir.r4.model.Quantity @@ -49,15 +51,15 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { @Before fun setup() { activityScenarioRule.scenario.onActivity { activity -> parent = FrameLayout(activity) } - viewHolder = QuestionnaireItemEditTextQuantityViewHolderFactory.create(parent) + viewHolder = EditTextQuantityViewHolderFactory.create(parent) setTestLayout(viewHolder.itemView) } @Test fun getValue_WithInitial_shouldReturn_Quantity_With_UnitAndSystem() { var answerHolder: List? = null - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { required = true addInitial( @@ -73,7 +75,7 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { validationResult = NotValidated, answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.text_input_edit_text)).perform(click()) onView(withId(R.id.text_input_edit_text)).perform(typeText("22")) @@ -91,14 +93,14 @@ class QuestionnaireItemEditTextQuantityViewHolderFactoryEspressoTest { @Test fun getValue_WithoutInitial_shouldReturn_Quantity_Without_UnitAndSystem() { var answerHolder: List? = null - val questionnaireItemViewItem = - QuestionnaireItemViewItem( + val questionnaireViewItem = + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { required = true }, QuestionnaireResponse.QuestionnaireResponseItemComponent(), validationResult = NotValidated, answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, ) - runOnUI { viewHolder.bind(questionnaireItemViewItem) } + runOnUI { viewHolder.bind(questionnaireViewItem) } onView(withId(R.id.text_input_edit_text)).perform(click()) onView(withId(R.id.text_input_edit_text)).perform(typeText("22")) diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemMultiSelectHolderFactoryInstrumentedTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemMultiSelectHolderFactoryInstrumentedTest.kt index 3ec074637f..8a3a2490c7 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemMultiSelectHolderFactoryInstrumentedTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/QuestionnaireItemMultiSelectHolderFactoryInstrumentedTest.kt @@ -25,6 +25,8 @@ import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.TestActivity import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.NotValidated +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemDialogSelectViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import com.google.android.material.textfield.TextInputLayout import com.google.common.truth.Truth.assertThat import org.hl7.fhir.r4.model.Coding @@ -41,7 +43,7 @@ class QuestionnaireItemMultiSelectHolderFactoryInstrumentedTest { @Test fun emptyResponseOptions_showNoneSelected() = withViewHolder { holder -> holder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( answerOptions("Coding 1", "Coding 2"), responseOptions(), validationResult = NotValidated, @@ -55,7 +57,7 @@ class QuestionnaireItemMultiSelectHolderFactoryInstrumentedTest { @Test fun selectedResponseOptions_showSelectedOptions() = withViewHolder { holder -> holder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( answerOptions("Coding 1", "Coding 2", "Coding 3"), responseOptions("Coding 1", "Coding 3"), validationResult = NotValidated, @@ -70,7 +72,7 @@ class QuestionnaireItemMultiSelectHolderFactoryInstrumentedTest { @UiThreadTest fun displayValidationResult_error_shouldShowErrorMessage() = withViewHolder { viewHolder -> viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "1" required = true @@ -91,7 +93,7 @@ class QuestionnaireItemMultiSelectHolderFactoryInstrumentedTest { @UiThreadTest fun displayValidationResult_noError_shouldShowNoErrorMessage() = withViewHolder { viewHolder -> viewHolder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "1" required = true @@ -122,7 +124,7 @@ class QuestionnaireItemMultiSelectHolderFactoryInstrumentedTest { @Test fun bind_readOnly_shouldDisableView() = withViewHolder { holder -> holder.bind( - QuestionnaireItemViewItem( + QuestionnaireViewItem( Questionnaire.QuestionnaireItemComponent().apply { linkId = "1" readOnly = true diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt index 6309e18dea..66d0a77da7 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt @@ -16,10 +16,10 @@ package com.google.android.fhir.datacapture -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem +import com.google.android.fhir.datacapture.views.QuestionnaireViewItem /** Various types of rows that can be used in a Questionnaire RecyclerView. */ internal sealed interface QuestionnaireAdapterItem { - /** A row for a quesion in a Questionnaire RecyclerView. */ - data class Question(val item: QuestionnaireItemViewItem) : QuestionnaireAdapterItem + /** A row for a question in a Questionnaire RecyclerView. */ + data class Question(val item: QuestionnaireViewItem) : QuestionnaireAdapterItem } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireItemEditAdapter.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt similarity index 51% rename from datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireItemEditAdapter.kt rename to datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt index 9732bdef06..077ddb6f43 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireItemEditAdapter.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt @@ -19,36 +19,36 @@ package com.google.android.fhir.datacapture import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.google.android.fhir.datacapture.contrib.views.QuestionnaireItemPhoneNumberViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemAttachmentViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemAutoCompleteViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemBooleanTypePickerViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemCheckBoxGroupViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemDatePickerViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemDateTimePickerViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemDialogSelectViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemDisplayViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemDropDownViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextDecimalViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextIntegerViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextMultiLineViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextQuantityViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemEditTextSingleLineViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemGroupViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemRadioGroupViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemSliderViewHolderFactory -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolder -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewItem +import com.google.android.fhir.datacapture.contrib.views.PhoneNumberViewHolderFactory +import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.AutoCompleteViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.BooleanChoiceViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.CheckBoxGroupViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.DatePickerViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.DateTimePickerViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.DisplayViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.DropDownViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.EditTextDecimalViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.EditTextIntegerViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.EditTextMultiLineViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.EditTextQuantityViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.EditTextSingleLineViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.GroupViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemDialogSelectViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder +import com.google.android.fhir.datacapture.views.factories.RadioGroupViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.SliderViewHolderFactory import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemType -internal class QuestionnaireItemEditAdapter( +internal class QuestionnaireEditAdapter( private val questionnaireItemViewHolderMatchers: List = emptyList(), ) : ListAdapter(DiffCallbacks.ITEMS) { /** - * @param viewType the integer value of the [QuestionnaireItemViewHolderType] used to render the - * [QuestionnaireItemViewItem]. + * @param viewType the integer value of the [QuestionnaireViewHolderType] used to render the + * [QuestionnaireViewItem]. */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionnaireItemViewHolder { val typedViewType = ViewType.parse(viewType) @@ -62,7 +62,7 @@ internal class QuestionnaireItemEditAdapter( parent: ViewGroup, subtype: Int, ): QuestionnaireItemViewHolder { - val numOfCanonicalWidgets = QuestionnaireItemViewHolderType.values().size + val numOfCanonicalWidgets = QuestionnaireViewHolderType.values().size check(subtype < numOfCanonicalWidgets + questionnaireItemViewHolderMatchers.size) { "Invalid widget type specified. Widget Int type cannot exceed the total number of supported custom and canonical widgets" } @@ -73,36 +73,25 @@ internal class QuestionnaireItemEditAdapter( .factory.create(parent) val viewHolderFactory = - when (QuestionnaireItemViewHolderType.fromInt(subtype)) { - QuestionnaireItemViewHolderType.GROUP -> QuestionnaireItemGroupViewHolderFactory - QuestionnaireItemViewHolderType.BOOLEAN_TYPE_PICKER -> - QuestionnaireItemBooleanTypePickerViewHolderFactory - QuestionnaireItemViewHolderType.DATE_PICKER -> QuestionnaireItemDatePickerViewHolderFactory - QuestionnaireItemViewHolderType.DATE_TIME_PICKER -> - QuestionnaireItemDateTimePickerViewHolderFactory - QuestionnaireItemViewHolderType.EDIT_TEXT_SINGLE_LINE -> - QuestionnaireItemEditTextSingleLineViewHolderFactory - QuestionnaireItemViewHolderType.EDIT_TEXT_MULTI_LINE -> - QuestionnaireItemEditTextMultiLineViewHolderFactory - QuestionnaireItemViewHolderType.EDIT_TEXT_INTEGER -> - QuestionnaireItemEditTextIntegerViewHolderFactory - QuestionnaireItemViewHolderType.EDIT_TEXT_DECIMAL -> - QuestionnaireItemEditTextDecimalViewHolderFactory - QuestionnaireItemViewHolderType.RADIO_GROUP -> QuestionnaireItemRadioGroupViewHolderFactory - QuestionnaireItemViewHolderType.DROP_DOWN -> QuestionnaireItemDropDownViewHolderFactory - QuestionnaireItemViewHolderType.DISPLAY -> QuestionnaireItemDisplayViewHolderFactory - QuestionnaireItemViewHolderType.QUANTITY -> - QuestionnaireItemEditTextQuantityViewHolderFactory - QuestionnaireItemViewHolderType.CHECK_BOX_GROUP -> - QuestionnaireItemCheckBoxGroupViewHolderFactory - QuestionnaireItemViewHolderType.AUTO_COMPLETE -> - QuestionnaireItemAutoCompleteViewHolderFactory - QuestionnaireItemViewHolderType.DIALOG_SELECT -> - QuestionnaireItemDialogSelectViewHolderFactory - QuestionnaireItemViewHolderType.SLIDER -> QuestionnaireItemSliderViewHolderFactory - QuestionnaireItemViewHolderType.PHONE_NUMBER -> - QuestionnaireItemPhoneNumberViewHolderFactory - QuestionnaireItemViewHolderType.ATTACHMENT -> QuestionnaireItemAttachmentViewHolderFactory + when (QuestionnaireViewHolderType.fromInt(subtype)) { + QuestionnaireViewHolderType.GROUP -> GroupViewHolderFactory + QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER -> BooleanChoiceViewHolderFactory + QuestionnaireViewHolderType.DATE_PICKER -> DatePickerViewHolderFactory + QuestionnaireViewHolderType.DATE_TIME_PICKER -> DateTimePickerViewHolderFactory + QuestionnaireViewHolderType.EDIT_TEXT_SINGLE_LINE -> EditTextSingleLineViewHolderFactory + QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE -> EditTextMultiLineViewHolderFactory + QuestionnaireViewHolderType.EDIT_TEXT_INTEGER -> EditTextIntegerViewHolderFactory + QuestionnaireViewHolderType.EDIT_TEXT_DECIMAL -> EditTextDecimalViewHolderFactory + QuestionnaireViewHolderType.RADIO_GROUP -> RadioGroupViewHolderFactory + QuestionnaireViewHolderType.DROP_DOWN -> DropDownViewHolderFactory + QuestionnaireViewHolderType.DISPLAY -> DisplayViewHolderFactory + QuestionnaireViewHolderType.QUANTITY -> EditTextQuantityViewHolderFactory + QuestionnaireViewHolderType.CHECK_BOX_GROUP -> CheckBoxGroupViewHolderFactory + QuestionnaireViewHolderType.AUTO_COMPLETE -> AutoCompleteViewHolderFactory + QuestionnaireViewHolderType.DIALOG_SELECT -> QuestionnaireItemDialogSelectViewHolderFactory + QuestionnaireViewHolderType.SLIDER -> SliderViewHolderFactory + QuestionnaireViewHolderType.PHONE_NUMBER -> PhoneNumberViewHolderFactory + QuestionnaireViewHolderType.ATTACHMENT -> AttachmentViewHolderFactory } return viewHolderFactory.create(parent) } @@ -161,90 +150,90 @@ internal class QuestionnaireItemEditAdapter( } /** - * Returns the integer value of the [QuestionnaireItemViewHolderType] that will be used to render - * the [QuestionnaireItemViewItem]. This is determined by a combination of the data type of the - * question and any additional Questionnaire Item UI Control Codes + * Returns the integer value of the [QuestionnaireViewHolderType] that will be used to render the + * [QuestionnaireViewItem]. This is determined by a combination of the data type of the question + * and any additional Questionnaire Item UI Control Codes * (http://hl7.org/fhir/R4/valueset-questionnaire-item-control.html) used in the itemControl * extension (http://hl7.org/fhir/R4/extension-questionnaire-itemcontrol.html). */ internal fun getItemViewTypeForQuestion( - questionnaireItemViewItem: QuestionnaireItemViewItem, + questionnaireViewItem: QuestionnaireViewItem, ): Int { - val questionnaireItem = questionnaireItemViewItem.questionnaireItem + val questionnaireItem = questionnaireViewItem.questionnaireItem // For custom widgets, generate an int value that's greater than any int assigned to the // canonical FHIR widgets questionnaireItemViewHolderMatchers.forEachIndexed { index, matcher -> if (matcher.matches(questionnaireItem)) { - return index + QuestionnaireItemViewHolderType.values().size + return index + QuestionnaireViewHolderType.values().size } } - if (questionnaireItemViewItem.answerOption.isNotEmpty()) { - return getChoiceViewHolderType(questionnaireItemViewItem).value + if (questionnaireViewItem.answerOption.isNotEmpty()) { + return getChoiceViewHolderType(questionnaireViewItem).value } return when (val type = questionnaireItem.type) { - QuestionnaireItemType.GROUP -> QuestionnaireItemViewHolderType.GROUP - QuestionnaireItemType.BOOLEAN -> QuestionnaireItemViewHolderType.BOOLEAN_TYPE_PICKER - QuestionnaireItemType.DATE -> QuestionnaireItemViewHolderType.DATE_PICKER - QuestionnaireItemType.DATETIME -> QuestionnaireItemViewHolderType.DATE_TIME_PICKER - QuestionnaireItemType.STRING -> getStringViewHolderType(questionnaireItemViewItem) - QuestionnaireItemType.TEXT -> QuestionnaireItemViewHolderType.EDIT_TEXT_MULTI_LINE - QuestionnaireItemType.INTEGER -> getIntegerViewHolderType(questionnaireItemViewItem) - QuestionnaireItemType.DECIMAL -> QuestionnaireItemViewHolderType.EDIT_TEXT_DECIMAL - QuestionnaireItemType.CHOICE -> getChoiceViewHolderType(questionnaireItemViewItem) - QuestionnaireItemType.DISPLAY -> QuestionnaireItemViewHolderType.DISPLAY - QuestionnaireItemType.QUANTITY -> QuestionnaireItemViewHolderType.QUANTITY - QuestionnaireItemType.REFERENCE -> getChoiceViewHolderType(questionnaireItemViewItem) - QuestionnaireItemType.ATTACHMENT -> QuestionnaireItemViewHolderType.ATTACHMENT + QuestionnaireItemType.GROUP -> QuestionnaireViewHolderType.GROUP + QuestionnaireItemType.BOOLEAN -> QuestionnaireViewHolderType.BOOLEAN_TYPE_PICKER + QuestionnaireItemType.DATE -> QuestionnaireViewHolderType.DATE_PICKER + QuestionnaireItemType.DATETIME -> QuestionnaireViewHolderType.DATE_TIME_PICKER + QuestionnaireItemType.STRING -> getStringViewHolderType(questionnaireViewItem) + QuestionnaireItemType.TEXT -> QuestionnaireViewHolderType.EDIT_TEXT_MULTI_LINE + QuestionnaireItemType.INTEGER -> getIntegerViewHolderType(questionnaireViewItem) + QuestionnaireItemType.DECIMAL -> QuestionnaireViewHolderType.EDIT_TEXT_DECIMAL + QuestionnaireItemType.CHOICE -> getChoiceViewHolderType(questionnaireViewItem) + QuestionnaireItemType.DISPLAY -> QuestionnaireViewHolderType.DISPLAY + QuestionnaireItemType.QUANTITY -> QuestionnaireViewHolderType.QUANTITY + QuestionnaireItemType.REFERENCE -> getChoiceViewHolderType(questionnaireViewItem) + QuestionnaireItemType.ATTACHMENT -> QuestionnaireViewHolderType.ATTACHMENT else -> throw NotImplementedError("Question type $type not supported.") }.value } private fun getChoiceViewHolderType( - questionnaireItemViewItem: QuestionnaireItemViewItem, - ): QuestionnaireItemViewHolderType { - val questionnaireItem = questionnaireItemViewItem.questionnaireItem + questionnaireViewItem: QuestionnaireViewItem, + ): QuestionnaireViewHolderType { + val questionnaireItem = questionnaireViewItem.questionnaireItem // Use the view type that the client wants if they specified an itemControl return questionnaireItem.itemControl?.viewHolderType // Otherwise, choose a sensible UI element automatically ?: run { - val numOptions = questionnaireItemViewItem.answerOption.size + val numOptions = questionnaireViewItem.answerOption.size when { // Always use a dialog for questions with a large number of options numOptions >= MINIMUM_NUMBER_OF_ANSWER_OPTIONS_FOR_DIALOG -> - QuestionnaireItemViewHolderType.DIALOG_SELECT + QuestionnaireViewHolderType.DIALOG_SELECT // Use a check box group if repeated answers are permitted - questionnaireItem.repeats -> QuestionnaireItemViewHolderType.CHECK_BOX_GROUP + questionnaireItem.repeats -> QuestionnaireViewHolderType.CHECK_BOX_GROUP // Use a dropdown if there are a medium number of options numOptions >= MINIMUM_NUMBER_OF_ANSWER_OPTIONS_FOR_DROP_DOWN -> - QuestionnaireItemViewHolderType.DROP_DOWN + QuestionnaireViewHolderType.DROP_DOWN // Use a radio group only if there are a small number of options - else -> QuestionnaireItemViewHolderType.RADIO_GROUP + else -> QuestionnaireViewHolderType.RADIO_GROUP } } } private fun getIntegerViewHolderType( - questionnaireItemViewItem: QuestionnaireItemViewItem, - ): QuestionnaireItemViewHolderType { - val questionnaireItem = questionnaireItemViewItem.questionnaireItem + questionnaireViewItem: QuestionnaireViewItem, + ): QuestionnaireViewHolderType { + val questionnaireItem = questionnaireViewItem.questionnaireItem // Use the view type that the client wants if they specified an itemControl return questionnaireItem.itemControl?.viewHolderType - ?: QuestionnaireItemViewHolderType.EDIT_TEXT_INTEGER + ?: QuestionnaireViewHolderType.EDIT_TEXT_INTEGER } private fun getStringViewHolderType( - questionnaireItemViewItem: QuestionnaireItemViewItem, - ): QuestionnaireItemViewHolderType { - val questionnaireItem = questionnaireItemViewItem.questionnaireItem + questionnaireViewItem: QuestionnaireViewItem, + ): QuestionnaireViewHolderType { + val questionnaireItem = questionnaireViewItem.questionnaireItem // Use the view type that the client wants if they specified an itemControl return questionnaireItem.itemControl?.viewHolderType - ?: QuestionnaireItemViewHolderType.EDIT_TEXT_SINGLE_LINE + ?: QuestionnaireViewHolderType.EDIT_TEXT_SINGLE_LINE } internal companion object { @@ -285,12 +274,12 @@ internal object DiffCallbacks { val QUESTIONS = object : DiffUtil.ItemCallback() { /** - * [QuestionnaireItemViewItem] is a transient object for the UI only. Whenever the user makes - * any change via the UI, a new list of [QuestionnaireItemViewItem]s will be created, each - * holding references to the underlying [QuestionnaireItem] and [QuestionnaireResponseItem], - * both of which should be read-only, and the current answers. To help recycler view handle - * update and/or animations, we consider two [QuestionnaireItemViewItem]s to be the same if - * they have the same underlying [QuestionnaireItem] and [QuestionnaireResponseItem]. + * [QuestionnaireViewItem] is a transient object for the UI only. Whenever the user makes any + * change via the UI, a new list of [QuestionnaireViewItem]s will be created, each holding + * references to the underlying [QuestionnaireItem] and [QuestionnaireResponseItem], both of + * which should be read-only, and the current answers. To help recycler view handle update + * and/or animations, we consider two [QuestionnaireViewItem]s to be the same if they have the + * same underlying [QuestionnaireItem] and [QuestionnaireResponseItem]. */ override fun areItemsTheSame( oldItem: QuestionnaireAdapterItem.Question, diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index cff7b49b33..1695c44de4 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -34,7 +34,7 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.fhir.datacapture.validation.Invalid -import com.google.android.fhir.datacapture.views.QuestionnaireItemViewHolderFactory +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory import com.google.android.material.progressindicator.LinearProgressIndicator import org.hl7.fhir.r4.model.Questionnaire import timber.log.Timber @@ -113,9 +113,9 @@ class QuestionnaireFragment : Fragment() { } val questionnaireProgressIndicator: LinearProgressIndicator = view.findViewById(R.id.questionnaire_progress_indicator) - val questionnaireItemEditAdapter = - QuestionnaireItemEditAdapter(questionnaireItemViewHolderFactoryMatchersProvider.get()) - val questionnaireItemReviewAdapter = QuestionnaireItemReviewAdapter() + val questionnaireEditAdapter = + QuestionnaireEditAdapter(questionnaireItemViewHolderFactoryMatchersProvider.get()) + val questionnaireReviewAdapter = QuestionnaireReviewAdapter() val submitButton = requireView().findViewById