diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt index 796df10ec86..af2d7e0f6cc 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt @@ -1,5 +1,6 @@ package org.oppia.android.app.player.state.itemviewmodel +import androidx.annotation.StringRes import androidx.databinding.Observable import androidx.databinding.ObservableBoolean import androidx.databinding.ObservableField @@ -12,6 +13,7 @@ import org.oppia.android.app.model.SubtitledHtml import org.oppia.android.app.model.TranslatableHtmlContentId import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver @@ -26,6 +28,18 @@ enum class SelectionItemInputType { RADIO_BUTTONS } +/** Enum to the store the errors of selection input. */ +enum class SelectionInputError(@StringRes private var error: Int?) { + VALID(error = null), + EMPTY_INPUT(error = R.string.selection_error_empty_input); + + /** + * Returns the string corresponding to this error's string resources, or null if there is none. + */ + fun getErrorMessageFromStringRes(resourceHandler: AppLanguageResourceHandler): String? = + error?.let(resourceHandler::getStringInLocale) +} + /** [StateItemViewModel] for multiple or item-selection input choice list. */ class SelectionInteractionViewModel private constructor( val entityId: String, @@ -64,7 +78,9 @@ class SelectionInteractionViewModel private constructor( val choiceItems: ObservableList = computeChoiceItems(choiceSubtitledHtmls, hasConversationView, this, enabledItemsList) + private var pendingAnswerError: String? = null private val isAnswerAvailable = ObservableField(false) + val errorMessage = ObservableField("") val selectedItemText = ObservableField( resourceHandler.getStringInLocale( @@ -77,12 +93,19 @@ class SelectionInteractionViewModel private constructor( object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(sender: Observable, propertyId: Int) { interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck( - pendingAnswerError = null, - inputAnswerAvailable = selectedItems.isNotEmpty() + pendingAnswerError, + inputAnswerAvailable = true // Allow blank answer submission. ) } } + errorMessage.addOnPropertyChangedCallback(callback) isAnswerAvailable.addOnPropertyChangedCallback(callback) + + // Initializing with default values so that submit button is enabled by default. + interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck( + pendingAnswerError = null, + inputAnswerAvailable = true + ) } override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { @@ -113,6 +136,20 @@ class SelectionInteractionViewModel private constructor( writtenTranslationContext = translationContext }.build() + /** + * It checks the pending error for the current selection input, and correspondingly + * updates the error string based on the specified error category. + */ + override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { + pendingAnswerError = when (category) { + AnswerErrorCategory.REAL_TIME -> null + AnswerErrorCategory.SUBMIT_TIME -> + getSubmitTimeError().getErrorMessageFromStringRes(resourceHandler) + } + errorMessage.set(pendingAnswerError) + return pendingAnswerError + } + /** Returns an HTML list containing all of the HTML string elements as items in the list. */ private fun convertSelectedItemsToHtmlString(itemHtmls: Collection): String { return when (itemHtmls.size) { @@ -135,6 +172,7 @@ class SelectionInteractionViewModel private constructor( /** Catalogs an item being clicked by the user and returns whether the item should be considered selected. */ fun updateSelection(itemIndex: Int, isCurrentlySelected: Boolean): Boolean { + checkPendingAnswerError(AnswerErrorCategory.REAL_TIME) return when { isCurrentlySelected -> { selectedItems -= itemIndex @@ -208,6 +246,13 @@ class SelectionInteractionViewModel private constructor( } } + private fun getSubmitTimeError(): SelectionInputError { + return if (selectedItems.isEmpty()) + SelectionInputError.EMPTY_INPUT + else + SelectionInputError.VALID + } + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ class FactoryImpl @Inject constructor( private val translationController: TranslationController, diff --git a/app/src/main/res/layout/selection_interaction_item.xml b/app/src/main/res/layout/selection_interaction_item.xml index d83395e7f1b..c2e963abee5 100644 --- a/app/src/main/res/layout/selection_interaction_item.xml +++ b/app/src/main/res/layout/selection_interaction_item.xml @@ -32,7 +32,6 @@ android:paddingTop="12dp" android:paddingEnd="@dimen/selection_interaction_item_padding_end" android:paddingBottom="12dp" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -60,5 +59,15 @@ app:selectionData="@{viewModel.choiceItems}" app:writtenTranslationContext="@{viewModel.writtenTranslationContext}" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8f15a46ec2..bbcbd014a8a 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -177,6 +177,7 @@ Number of terms is not equal to the required terms. Ratios cannot have 0 as an element. Enter a ratio to continue. + Choose an answer to continue. Unknown size %s Bytes %s KB diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index f295bd9c3e5..24193e64a33 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -558,7 +558,7 @@ class StateFragmentTest { } @Test - fun testStateFragment_loadExp_thirdState_hasDisabledSubmitButton() { + fun testStateFragment_loadExp_thirdState_hasEnabledSubmitButton() { setUpTestWithLanguageSwitchingFeatureOff() launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { startPlayingExploration() @@ -569,12 +569,12 @@ class StateFragmentTest { onView(withId(R.id.submit_answer_button)).check( matches(withText(R.string.state_submit_button)) ) - onView(withId(R.id.submit_answer_button)).check(matches(not(isEnabled()))) + onView(withId(R.id.submit_answer_button)).check(matches(isEnabled())) } } @Test - fun testStateFragment_loadExp_changeConfiguration_thirdState_hasDisabledSubmitButton() { + fun testStateFragment_loadExp_changeConfiguration_thirdState_hasEnabledSubmitButton() { setUpTestWithLanguageSwitchingFeatureOff() launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { startPlayingExploration() @@ -587,7 +587,27 @@ class StateFragmentTest { onView(withId(R.id.submit_answer_button)).check( matches(withText(R.string.state_submit_button)) ) - onView(withId(R.id.submit_answer_button)).check(matches(not(isEnabled()))) + onView(withId(R.id.submit_answer_button)).check(matches(isEnabled())) + } + } + + @Test + fun testStateFragment_loadExp_thirdState_submitWithoutAnswer_showsErrorMessage() { + setUpTestWithLanguageSwitchingFeatureOff() + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { + startPlayingExploration() + playThroughPrototypeState1() + playThroughPrototypeState2() + + clickSubmitAnswerButton() + onView(withId(R.id.selection_input_error)) + .check( + matches( + withText( + R.string.selection_error_empty_input + ) + ) + ) } } @@ -660,7 +680,7 @@ class StateFragmentTest { } @Test - fun testStateFragment_loadExp_thirdState_submitInvalidAnswer_disablesSubmitButton() { + fun testStateFragment_loadExp_thirdState_submitInvalidAnswer_submitButtonIsEnabled() { setUpTestWithLanguageSwitchingFeatureOff() launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { startPlayingExploration() @@ -671,14 +691,15 @@ class StateFragmentTest { selectMultipleChoiceOption(optionPosition = 1, expectedOptionText = "Chicken") clickSubmitAnswerButton() - // The submission button should now be disabled and there should be an error. + // The submission button should now still be enabled as empty input error will be displayed + // if submit button is clicked without choosing an answer. scrollToViewType(SUBMIT_ANSWER_BUTTON) - onView(withId(R.id.submit_answer_button)).check(matches(not(isEnabled()))) + onView(withId(R.id.submit_answer_button)).check(matches(isEnabled())) } } @Test - fun testStateFragment_loadExp_land_thirdState_submitInvalidAnswer_disablesSubmitButton() { + fun testStateFragment_loadExp_land_thirdState_submitInvalidAnswer_submitButtonIsEnabled() { setUpTestWithLanguageSwitchingFeatureOff() launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { startPlayingExploration() @@ -689,9 +710,10 @@ class StateFragmentTest { selectMultipleChoiceOption(optionPosition = 1, expectedOptionText = "Chicken") clickSubmitAnswerButton() - // The submission button should now be disabled and there should be an error. + // The submission button should now still be enabled as empty input error will be displayed + // if submit button is clicked without choosing an answer. scrollToViewType(SUBMIT_ANSWER_BUTTON) - onView(withId(R.id.submit_answer_button)).check(matches(not(isEnabled()))) + onView(withId(R.id.submit_answer_button)).check(matches(isEnabled())) } }