diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 4019ad3cd6c..48ab86b20b1 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -221,6 +221,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt", "src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt", "src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt", + "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragDropInteractionContentViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt", @@ -320,7 +321,6 @@ VIEW_MODELS = [ "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContentViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueNavigationButtonViewModel.kt", - "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/FeedbackViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/NextButtonViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt", diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt index 6cd22508141..d6f88ea9cec 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt @@ -1,8 +1,10 @@ package org.oppia.android.app.player.state.itemviewmodel +import androidx.annotation.StringRes import androidx.databinding.Observable import androidx.databinding.ObservableField import androidx.recyclerview.widget.RecyclerView +import org.oppia.android.R import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.ListOfSetsOfHtmlStrings @@ -13,6 +15,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 @@ -23,6 +26,18 @@ import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.translation.TranslationController import javax.inject.Inject +/** Represents the type of errors that can be thrown by drag and drop sort interaction. */ +enum class DragAndDropSortInteractionError(@StringRes private var error: Int?) { + VALID(error = null), + EMPTY_INPUT(error = R.string.drag_and_drop_interaction_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 drag drop & sort choice list. */ class DragAndDropSortInteractionViewModel private constructor( val entityId: String, @@ -55,25 +70,34 @@ class DragAndDropSortInteractionViewModel private constructor( subtitledHtml.contentId to translatedHtml } - private val _choiceItems: MutableList = + private val _originalChoiceItems: MutableList = computeChoiceItems(contentIdHtmlMap, choiceSubtitledHtmls, this, resourceHandler) + private val _choiceItems = _originalChoiceItems.toMutableList() val choiceItems: List = _choiceItems + private var pendingAnswerError: String? = null private val isAnswerAvailable = ObservableField(false) + var errorMessage = ObservableField("") init { val callback: Observable.OnPropertyChangedCallback = object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(sender: Observable, propertyId: Int) { interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck( - pendingAnswerError = null, - inputAnswerAvailable = true + pendingAnswerError, + inputAnswerAvailable = true // Allow submission without arranging or merging items. ) } } isAnswerAvailable.addOnPropertyChangedCallback(callback) - isAnswerAvailable.set(true) // For drag drop submit button will be enabled by default. + errorMessage.addOnPropertyChangedCallback(callback) + + // Initializing with default values so that submit button is enabled by default. + interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck( + pendingAnswerError = null, + inputAnswerAvailable = true + ) } override fun onItemDragged( @@ -98,6 +122,7 @@ class DragAndDropSortInteractionViewModel private constructor( if (allowMultipleItemsInSamePosition) { (adapter as BindableAdapter<*>).setDataUnchecked(_choiceItems) } + checkPendingAnswerError(AnswerErrorCategory.REAL_TIME) } fun onItemMoved( @@ -129,6 +154,20 @@ class DragAndDropSortInteractionViewModel private constructor( this@DragAndDropSortInteractionViewModel.writtenTranslationContext }.build() + /** + * It checks the pending error for the current drag and drop sort interaction, 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 convertItemsToAnswer(htmlItems: List): ListOfSetsOfHtmlStrings { return ListOfSetsOfHtmlStrings.newBuilder() @@ -190,6 +229,13 @@ class DragAndDropSortInteractionViewModel private constructor( (adapter as BindableAdapter<*>).setDataUnchecked(_choiceItems) } + private fun getSubmitTimeError(): DragAndDropSortInteractionError { + return if (_originalChoiceItems == _choiceItems) + DragAndDropSortInteractionError.EMPTY_INPUT + else + DragAndDropSortInteractionError.VALID + } + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ class FactoryImpl @Inject constructor( private val resourceHandler: AppLanguageResourceHandler, diff --git a/app/src/main/res/layout/drag_drop_interaction_item.xml b/app/src/main/res/layout/drag_drop_interaction_item.xml index 53a7074bcac..bcc6c1053ef 100644 --- a/app/src/main/res/layout/drag_drop_interaction_item.xml +++ b/app/src/main/res/layout/drag_drop_interaction_item.xml @@ -67,5 +67,12 @@ app:draggableData="@{viewModel.choiceItems}" app:onDragEnded="@{(adapter) -> viewModel.onDragEnded(adapter)}" app:onItemDrag="@{(indexFrom, indexTo, adapter) -> viewModel.onItemDragged(indexFrom, indexTo, adapter)}" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c0598ab6bf4..9a46247bf65 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -453,6 +453,7 @@ Return to lesson Explanation: If two items are equal, merge them. + Arrange the boxes to continue. Link to item %s Unlink items at %s Move item down to %s 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 d6a15aa0e00..a05e4756684 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 @@ -747,6 +747,96 @@ class StateFragmentTest { } } + @Test + fun testStateFragment_loadDragDropExp_submitWithoutArranging_showsErrorMessage() { + setUpTestWithLanguageSwitchingFeatureOff() + launchForExploration(TEST_EXPLORATION_ID_4, shouldSavePartialProgress = false).use { + startPlayingExploration() + clickSubmitAnswerButton() + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + withText( + R.string.drag_and_drop_interaction_empty_input + ) + ) + ) + } + } + + @Test + fun testStateFragment_loadDragDropExp_withGrouping_submitWithoutArranging_showsErrorMessage_dragItem_errorMessageIsReset() { // ktlint-disable max-line-length + setUpTestWithLanguageSwitchingFeatureOff() + launchForExploration(TEST_EXPLORATION_ID_4, shouldSavePartialProgress = false).use { + startPlayingExploration() + + // Drag and drop interaction with grouping. + // Submit answer without any changes. + clickSubmitAnswerButton() + // Empty input error is displayed. + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + isDisplayed() + ) + ) + // Submit button is disabled due to the error. + verifySubmitAnswerButtonIsDisabled() + // Drag and rearrange an item. + dragAndDropItem(fromPosition = 0, toPosition = 1) + // Empty input error is reset. + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + not(isDisplayed()) + ) + ) + // Submit button is enabled back. + verifySubmitAnswerButtonIsEnabled() + } + } + + @Test + fun testStateFragment_loadDragDropExp_withoutGrouping_submitWithoutArranging_showsErrorMessage_dragItem_errorMessageIsReset() { // ktlint-disable max-line-length + setUpTestWithLanguageSwitchingFeatureOff() + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { + startPlayingExploration() + playThroughPrototypeState1() + playThroughPrototypeState2() + playThroughPrototypeState3() + playThroughPrototypeState4() + playThroughPrototypeState5() + playThroughPrototypeState6() + playThroughPrototypeState7() + playThroughPrototypeState8() + + // Drag and drop interaction without grouping. + // Ninth state: Drag Drop Sort. Correct answer: Move 1st item to 4th position. + // Submit answer without any changes. + clickSubmitAnswerButton() + // Empty input error is displayed. + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + isDisplayed() + ) + ) + // Submit button is disabled due to the error. + verifySubmitAnswerButtonIsDisabled() + // Drag and rearrange an item. + dragAndDropItem(fromPosition = 0, toPosition = 1) + // Empty input error is reset. + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + not(isDisplayed()) + ) + ) + // Submit button is enabled back. + verifySubmitAnswerButtonIsEnabled() + } + } + @Test fun testStateFragment_loadDragDropExp_mergeFirstTwoItems_worksCorrectly() { setUpTestWithLanguageSwitchingFeatureOff()