From 7568bd3e7d2829fcfac99b8ebff5c68644b543c6 Mon Sep 17 00:00:00 2001 From: Vishwajith Shettigar <76042077+Vishwajith-Shettigar@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:10:28 +0530 Subject: [PATCH] Fix part of #4470, Fix #4471, Fix 4474: Handle configuration change using onSavedInstance. (#5458) ## Explanation Fix part of #4470. Fixes #4471. Fixes #4474 This PR enables the retention of input when the device configuration changes using onSavedInstance. List of interactions covered in this PR: 1. FractionInputInteraction 2. NumericInputInteraction 3. TextInputInteraction 4. MathExpressionInteraction 5. RatioExpressionInteraction List of interactions not covered in this PR: 1. Image Region selection interaction. 2. Drag and Drop interaction. ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). ## For UI-specific PRs only If your PR includes UI-related changes, then: - Add screenshots for portrait/landscape for both a tablet & phone of the before & after UI changes - For the screenshots above, include both English and pseudo-localized (RTL) screenshots (see [RTL guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines)) - Add a video showing the full UX flow with a screen reader enabled (see [accessibility guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide)) - For PRs introducing new UI elements or color changes, both light and dark mode screenshots must be included - Add a screenshot demonstrating that you ran affected Espresso tests locally & that they're passing --- .../android/app/player/state/StateFragment.kt | 21 +- .../player/state/StateFragmentPresenter.kt | 12 +- .../state/StatePlayerRecyclerViewAssembler.kt | 28 +- .../app/player/state/StateViewModel.kt | 10 +- .../InteractionAnswerHandler.kt | 15 +- .../ContinueInteractionViewModel.kt | 4 +- .../DragAndDropSortInteractionViewModel.kt | 7 +- .../FractionInteractionViewModel.kt | 25 +- ...mageRegionSelectionInteractionViewModel.kt | 7 +- .../MathExpressionInteractionsViewModel.kt | 26 +- .../itemviewmodel/NumericInputViewModel.kt | 25 +- ...atioExpressionInputInteractionViewModel.kt | 25 +- .../SelectionInteractionViewModel.kt | 43 ++- .../state/itemviewmodel/StateItemViewModel.kt | 4 +- .../state/itemviewmodel/TextInputViewModel.kt | 25 +- ...ractionInputInteractionViewTestActivity.kt | 2 +- ...ageRegionSelectionTestFragmentPresenter.kt | 2 +- .../InputInteractionViewTestActivity.kt | 2 +- ...hExpressionInteractionsViewTestActivity.kt | 2 +- .../RatioInputInteractionViewTestActivity.kt | 2 +- .../TextInputInteractionViewTestActivity.kt | 2 +- .../questionplayer/QuestionPlayerFragment.kt | 21 +- .../QuestionPlayerFragmentPresenter.kt | 14 +- .../questionplayer/QuestionPlayerViewModel.kt | 10 +- .../item_selection_interaction_items.xml | 5 +- .../MathExpressionInteractionsViewTest.kt | 4 +- .../app/player/state/StateFragmentTest.kt | 323 +++++++++++++++++- model/src/main/proto/exploration.proto | 35 ++ 28 files changed, 631 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragment.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragment.kt index afd00a47f9b..50f05d60082 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragment.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragment.kt @@ -10,6 +10,7 @@ import org.oppia.android.app.fragment.InjectableFragment import org.oppia.android.app.model.HelpIndex import org.oppia.android.app.model.StateFragmentArguments import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState 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 @@ -42,6 +43,9 @@ class StateFragment : /** Arguments key for StateFragment. */ const val STATE_FRAGMENT_ARGUMENTS_KEY = "StateFragment.arguments" + /** Arguments key for StateFragment saved state. */ + const val STATE_FRAGMENT_STATE_KEY = "StateFragment.state" + /** * Creates a new instance of a StateFragment. * @param internalProfileId used by StateFragment to mark progress. @@ -86,6 +90,12 @@ class StateFragment : ): View? { val args = arguments?.getProto(STATE_FRAGMENT_ARGUMENTS_KEY, StateFragmentArguments.getDefaultInstance()) + + val userAnswerState = savedInstanceState?.getProto( + STATE_FRAGMENT_STATE_KEY, + UserAnswerState.getDefaultInstance() + ) ?: UserAnswerState.getDefaultInstance() + val internalProfileId = args?.internalProfileId ?: -1 val topicId = args?.topicId!! val storyId = args.storyId!! @@ -97,7 +107,8 @@ class StateFragment : internalProfileId, topicId, storyId, - explorationId + explorationId, + userAnswerState ) } @@ -154,4 +165,12 @@ class StateFragment : fun dismissConceptCard() = stateFragmentPresenter.dismissConceptCard() fun getExplorationCheckpointState() = stateFragmentPresenter.getExplorationCheckpointState() + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putProto( + STATE_FRAGMENT_STATE_KEY, + stateFragmentPresenter.getUserAnswerState() + ) + } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index e939d9afbb4..672595d81ef 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -27,6 +27,7 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.State import org.oppia.android.app.model.SurveyQuestionName import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.player.audio.AudioButtonListener import org.oppia.android.app.player.audio.AudioFragment import org.oppia.android.app.player.audio.AudioUiManager @@ -111,7 +112,8 @@ class StateFragmentPresenter @Inject constructor( internalProfileId: Int, topicId: String, storyId: String, - explorationId: String + explorationId: String, + userAnswerState: UserAnswerState ): View? { profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() this.topicId = topicId @@ -125,7 +127,7 @@ class StateFragmentPresenter @Inject constructor( /* attachToRoot= */ false ) recyclerViewAssembler = createRecyclerViewAssembler( - assemblerBuilderFactory.create(resourceBucketName, entityType, profileId), + assemblerBuilderFactory.create(resourceBucketName, entityType, profileId, userAnswerState), binding.congratulationsTextView, binding.congratulationsTextConfettiView, binding.fullScreenConfettiView @@ -373,6 +375,7 @@ class StateFragmentPresenter @Inject constructor( private fun subscribeToAnswerOutcome( answerOutcomeResultLiveData: LiveData> ) { + recyclerViewAssembler.resetUserAnswerState() val answerOutcomeLiveData = getAnswerOutcome(answerOutcomeResultLiveData) answerOutcomeLiveData.observe( fragment, @@ -393,6 +396,11 @@ class StateFragmentPresenter @Inject constructor( ) } + /** Returns the [UserAnswerState] representing the user's current pending answer. */ + fun getUserAnswerState(): UserAnswerState { + return stateViewModel.getUserAnswerState(recyclerViewAssembler::getPendingAnswerHandler) + } + /** Helper for subscribeToAnswerOutcome. */ private fun getAnswerOutcome( answerOutcome: LiveData> diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index e0de8c91d58..8f1e627d77a 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -28,6 +28,7 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.StringList import org.oppia.android.app.model.SubtitledHtml import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.audio.AudioUiManager import org.oppia.android.app.player.state.StatePlayerRecyclerViewAssembler.Builder.Factory @@ -143,7 +144,8 @@ class StatePlayerRecyclerViewAssembler private constructor( backgroundCoroutineDispatcher: CoroutineDispatcher, private val hasConversationView: Boolean, private val resourceHandler: AppLanguageResourceHandler, - private val translationController: TranslationController + private val translationController: TranslationController, + private var userAnswerState: UserAnswerState ) : HtmlParser.CustomOppiaTagActionListener { /** * A list of view models corresponding to past view models that are hidden by default. These are @@ -323,10 +325,16 @@ class StatePlayerRecyclerViewAssembler private constructor( hasPreviousButton, isSplitView.get()!!, writtenTranslationContext, - timeToStartNoticeAnimationMs + timeToStartNoticeAnimationMs, + userAnswerState ) } + /** Reset userAnswerState once the user submits an answer. */ + fun resetUserAnswerState() { + userAnswerState = UserAnswerState.getDefaultInstance() + } + private fun addContentItem( pendingItemList: MutableList, ephemeralState: EphemeralState, @@ -904,7 +912,8 @@ class StatePlayerRecyclerViewAssembler private constructor( private val resourceHandler: AppLanguageResourceHandler, private val translationController: TranslationController, private val multiTypeBuilderFactory: BindableAdapter.MultiTypeBuilder.Factory, - private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory + private val singleTypeBuilderFactory: BindableAdapter.SingleTypeBuilder.Factory, + private val userAnswerState: UserAnswerState ) { private val adapterBuilder: BindableAdapter.MultiTypeBuilder) -> InteractionAnswerHandler? + ): UserAnswerState { + return retrieveAnswerHandler(getAnswerItemList())?.getUserAnswerState() + ?: UserAnswerState.getDefaultInstance() + } + private fun getPendingAnswerWithoutError( answerHandler: InteractionAnswerHandler? ): UserAnswer? { diff --git a/app/src/main/java/org/oppia/android/app/player/state/answerhandling/InteractionAnswerHandler.kt b/app/src/main/java/org/oppia/android/app/player/state/answerhandling/InteractionAnswerHandler.kt index 591a01d10ce..5f7458efbc8 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/answerhandling/InteractionAnswerHandler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/answerhandling/InteractionAnswerHandler.kt @@ -1,6 +1,8 @@ package org.oppia.android.app.player.state.answerhandling +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState /** * A handler for interaction answers. Handlers can either require an additional user action before the answer can be @@ -26,6 +28,11 @@ interface InteractionAnswerHandler { fun getPendingAnswer(): UserAnswer? { return null } + + /** Returns the current pending answer. */ + fun getUserAnswerState(): UserAnswerState { + return UserAnswerState.getDefaultInstance() + } } /** @@ -35,11 +42,3 @@ interface InteractionAnswerHandler { interface InteractionAnswerReceiver { fun onAnswerReadyForSubmission(answer: UserAnswer) } - -/** Categories of errors that can be inferred from a pending answer. */ -enum class AnswerErrorCategory { - /** Corresponds to errors that may be found while the user is trying to input an answer. */ - REAL_TIME, - /** Corresponds to errors that may be found only when a user tries to submit an answer. */ - SUBMIT_TIME -} diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt index 2200ad829a1..eb08cf630d0 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt @@ -4,6 +4,7 @@ import androidx.fragment.app.Fragment import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler @@ -57,7 +58,8 @@ class ContinueInteractionViewModel private constructor( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState ): StateItemViewModel { return ContinueInteractionViewModel( interactionAnswerReceiver, 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 d6f88ea9cec..aa205417099 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 @@ -5,6 +5,7 @@ import androidx.databinding.Observable import androidx.databinding.ObservableField import androidx.recyclerview.widget.RecyclerView import org.oppia.android.R +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.ListOfSetsOfHtmlStrings @@ -14,8 +15,8 @@ import org.oppia.android.app.model.StringList 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.UserAnswerState 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 @@ -163,6 +164,7 @@ class DragAndDropSortInteractionViewModel private constructor( AnswerErrorCategory.REAL_TIME -> null AnswerErrorCategory.SUBMIT_TIME -> getSubmitTimeError().getErrorMessageFromStringRes(resourceHandler) + else -> null } errorMessage.set(pendingAnswerError) return pendingAnswerError @@ -250,7 +252,8 @@ class DragAndDropSortInteractionViewModel private constructor( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState ): StateItemViewModel { return DragAndDropSortInteractionViewModel( entityId, diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt index 193248effe7..b09059e8086 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt @@ -5,12 +5,13 @@ import android.text.TextWatcher import androidx.databinding.Observable import androidx.databinding.ObservableField import org.oppia.android.R +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.parser.FractionParsingUiError -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 @@ -27,10 +28,12 @@ class FractionInteractionViewModel private constructor( private val errorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, private val writtenTranslationContext: WrittenTranslationContext, private val resourceHandler: AppLanguageResourceHandler, - private val translationController: TranslationController + private val translationController: TranslationController, + userAnswerState: UserAnswerState ) : StateItemViewModel(ViewType.FRACTION_INPUT_INTERACTION), InteractionAnswerHandler { private var pendingAnswerError: String? = null - var answerText: CharSequence = "" + var answerText: CharSequence = userAnswerState.textInputAnswer + private var answerErrorCetegory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR var isAnswerAvailable = ObservableField(false) var errorMessage = ObservableField("") @@ -54,6 +57,7 @@ class FractionInteractionViewModel private constructor( /* pendingAnswerError= */null, /* inputAnswerAvailable= */true ) + checkPendingAnswerError(userAnswerState.answerErrorCategory) } override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { @@ -69,6 +73,7 @@ class FractionInteractionViewModel private constructor( /** It checks the pending error for the current fraction input, and correspondingly updates the error string based on the specified error category. */ override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { + answerErrorCetegory = category when (category) { AnswerErrorCategory.REAL_TIME -> { if (answerText.isNotEmpty()) { @@ -86,6 +91,7 @@ class FractionInteractionViewModel private constructor( fractionParser.getSubmitTimeError(answerText.toString()) ).getErrorMessageFromStringRes(resourceHandler) } + else -> {} } errorMessage.set(pendingAnswerError) return pendingAnswerError @@ -110,6 +116,13 @@ class FractionInteractionViewModel private constructor( } } + override fun getUserAnswerState(): UserAnswerState { + return UserAnswerState.newBuilder().apply { + this.textInputAnswer = answerText.toString() + this.answerErrorCategory = answerErrorCetegory + }.build() + } + private fun deriveHintText(interaction: Interaction): CharSequence { // The subtitled unicode can apparently exist in the structure in two different formats. val placeholderUnicodeOption1 = @@ -149,7 +162,8 @@ class FractionInteractionViewModel private constructor( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState ): StateItemViewModel { return FractionInteractionViewModel( interaction, @@ -158,7 +172,8 @@ class FractionInteractionViewModel private constructor( answerErrorReceiver, writtenTranslationContext, resourceHandler, - translationController + translationController, + userAnswerState ) } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt index 4ba33e25422..aa56b9548e9 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt @@ -4,13 +4,14 @@ import androidx.annotation.StringRes import androidx.databinding.Observable import androidx.databinding.ObservableField import org.oppia.android.R +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.ClickOnImage import org.oppia.android.app.model.ImageWithRegions import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState 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 @@ -102,6 +103,7 @@ class ImageRegionSelectionInteractionViewModel private constructor( ).getErrorMessageFromStringRes(resourceHandler) } } + else -> {} } errorMessage.set(pendingAnswerError) @@ -192,7 +194,8 @@ class ImageRegionSelectionInteractionViewModel private constructor( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState ): StateItemViewModel { return ImageRegionSelectionInteractionViewModel( entityId, diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt index 06b6bb3a47c..9f6bd6682bf 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt @@ -6,14 +6,15 @@ import androidx.annotation.StringRes import androidx.databinding.Observable import androidx.databinding.ObservableField import org.oppia.android.R +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState 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 @@ -64,7 +65,8 @@ class MathExpressionInteractionsViewModel private constructor( private val resourceHandler: AppLanguageResourceHandler, private val translationController: TranslationController, private val mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil, - private val interactionType: InteractionType + private val interactionType: InteractionType, + userAnswerState: UserAnswerState ) : StateItemViewModel(interactionType.viewType), InteractionAnswerHandler { private var pendingAnswerError: String? = null @@ -72,7 +74,7 @@ class MathExpressionInteractionsViewModel private constructor( * Defines the current answer text being entered by the learner. This is expected to be directly * bound to the corresponding edit text. */ - var answerText: CharSequence = "" + var answerText: CharSequence = userAnswerState.textInputAnswer // The value of ths field is set from the Binding and from the TextWatcher. Any // programmatic modification needs to be done here, so that the Binding and the TextWatcher // do not step on each other. @@ -80,6 +82,8 @@ class MathExpressionInteractionsViewModel private constructor( field = value.toString().trim() } + private var answerErrorCetegory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR + /** * Defines whether an answer is currently available to parse. This is expected to be directly * bound to the UI. @@ -117,6 +121,14 @@ class MathExpressionInteractionsViewModel private constructor( pendingAnswerError = null, inputAnswerAvailable = true ) + checkPendingAnswerError(userAnswerState.answerErrorCategory) + } + + override fun getUserAnswerState(): UserAnswerState { + return UserAnswerState.newBuilder().apply { + this.textInputAnswer = answerText.toString() + this.answerErrorCategory = answerErrorCetegory + }.build() } override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { @@ -153,6 +165,7 @@ class MathExpressionInteractionsViewModel private constructor( }.build() override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { + answerErrorCetegory = category pendingAnswerError = when (category) { // There's no support for real-time errors. AnswerErrorCategory.REAL_TIME -> null @@ -161,6 +174,7 @@ class MathExpressionInteractionsViewModel private constructor( answerText.toString(), allowedVariables, resourceHandler ) } + else -> null } errorMessage.set(pendingAnswerError) return pendingAnswerError @@ -241,7 +255,8 @@ class MathExpressionInteractionsViewModel private constructor( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState ): StateItemViewModel { return MathExpressionInteractionsViewModel( interaction, @@ -251,7 +266,8 @@ class MathExpressionInteractionsViewModel private constructor( resourceHandler, translationController, mathExpressionAccessibilityUtil, - interactionType + interactionType, + userAnswerState ) } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt index 04174714b4f..347c2d5ccc7 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt @@ -4,12 +4,13 @@ import android.text.Editable import android.text.TextWatcher import androidx.databinding.Observable import androidx.databinding.ObservableField +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.parser.StringToNumberParser -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 @@ -22,9 +23,11 @@ class NumericInputViewModel private constructor( private val interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length val isSplitView: Boolean, private val writtenTranslationContext: WrittenTranslationContext, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + userAnswerState: UserAnswerState ) : StateItemViewModel(ViewType.NUMERIC_INPUT_INTERACTION), InteractionAnswerHandler { - var answerText: CharSequence = "" + var answerText: CharSequence = userAnswerState.textInputAnswer + private var answerErrorCetegory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR private var pendingAnswerError: String? = null val errorMessage = ObservableField("") var isAnswerAvailable = ObservableField(false) @@ -48,6 +51,7 @@ class NumericInputViewModel private constructor( pendingAnswerError = null, inputAnswerAvailable = true ) + checkPendingAnswerError(userAnswerState.answerErrorCategory) } /** @@ -55,6 +59,7 @@ class NumericInputViewModel private constructor( * error string based on the specified error category. */ override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { + answerErrorCetegory = category pendingAnswerError = when (category) { AnswerErrorCategory.REAL_TIME -> if (answerText.isNotEmpty()) @@ -64,11 +69,19 @@ class NumericInputViewModel private constructor( AnswerErrorCategory.SUBMIT_TIME -> stringToNumberParser.getSubmitTimeError(answerText.toString()) .getErrorMessageFromStringRes(resourceHandler) + else -> null } errorMessage.set(pendingAnswerError) return pendingAnswerError } + override fun getUserAnswerState(): UserAnswerState { + return UserAnswerState.newBuilder().apply { + this.textInputAnswer = answerText.toString() + this.answerErrorCategory = answerErrorCetegory + }.build() + } + fun getAnswerTextWatcher(): TextWatcher { return object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { @@ -112,14 +125,16 @@ class NumericInputViewModel private constructor( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState ): StateItemViewModel { return NumericInputViewModel( hasConversationView, answerErrorReceiver, isSplitView, writtenTranslationContext, - resourceHandler + resourceHandler, + userAnswerState ) } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt index f5c0f323bec..215dfb811ff 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt @@ -5,12 +5,13 @@ import android.text.TextWatcher import androidx.databinding.Observable import androidx.databinding.ObservableField import org.oppia.android.R +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.parser.StringToRatioParser -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 @@ -28,10 +29,12 @@ class RatioExpressionInputInteractionViewModel private constructor( private val errorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, private val writtenTranslationContext: WrittenTranslationContext, private val resourceHandler: AppLanguageResourceHandler, - private val translationController: TranslationController + private val translationController: TranslationController, + userAnswerState: UserAnswerState ) : StateItemViewModel(ViewType.RATIO_EXPRESSION_INPUT_INTERACTION), InteractionAnswerHandler { private var pendingAnswerError: String? = null - var answerText: CharSequence = "" + var answerText: CharSequence = userAnswerState.textInputAnswer + private var answerErrorCetegory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR var isAnswerAvailable = ObservableField(false) var errorMessage = ObservableField("") @@ -58,6 +61,7 @@ class RatioExpressionInputInteractionViewModel private constructor( pendingAnswerError = null, inputAnswerAvailable = true ) + checkPendingAnswerError(userAnswerState.answerErrorCategory) } override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { @@ -78,6 +82,7 @@ class RatioExpressionInputInteractionViewModel private constructor( * updates the error string based on the specified error category. */ override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { + answerErrorCetegory = category pendingAnswerError = when (category) { AnswerErrorCategory.REAL_TIME -> if (answerText.isNotEmpty()) @@ -89,11 +94,19 @@ class RatioExpressionInputInteractionViewModel private constructor( answerText.toString(), numberOfTerms = numberOfTerms ).getErrorMessageFromStringRes(resourceHandler) + else -> null } errorMessage.set(pendingAnswerError) return pendingAnswerError } + override fun getUserAnswerState(): UserAnswerState { + return UserAnswerState.newBuilder().apply { + this.textInputAnswer = answerText.toString() + this.answerErrorCategory = answerErrorCetegory + }.build() + } + fun getAnswerTextWatcher(): TextWatcher { return object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { @@ -148,7 +161,8 @@ class RatioExpressionInputInteractionViewModel private constructor( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState ): StateItemViewModel { return RatioExpressionInputInteractionViewModel( interaction, @@ -157,7 +171,8 @@ class RatioExpressionInputInteractionViewModel private constructor( answerErrorReceiver, writtenTranslationContext, resourceHandler, - translationController + translationController, + userAnswerState ) } } 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 af2d7e0f6cc..d5f4b7016c7 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 @@ -6,14 +6,16 @@ import androidx.databinding.ObservableBoolean import androidx.databinding.ObservableField import androidx.databinding.ObservableList import org.oppia.android.R +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.ItemSelectionAnswerState import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds 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.UserAnswerState 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 @@ -49,7 +51,8 @@ class SelectionInteractionViewModel private constructor( val isSplitView: Boolean, val writtenTranslationContext: WrittenTranslationContext, private val translationController: TranslationController, - private val resourceHandler: AppLanguageResourceHandler + private val resourceHandler: AppLanguageResourceHandler, + userAnswerState: UserAnswerState ) : StateItemViewModel(ViewType.SELECTION_INTERACTION), InteractionAnswerHandler { private val interactionId: String = interaction.id @@ -60,6 +63,9 @@ class SelectionInteractionViewModel private constructor( ?.map { schemaObject -> schemaObject.customSchemaValue.subtitledHtml } ?: listOf() } + + private var answerErrorCetegory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR + private val minAllowableSelectionCount: Int by lazy { interaction.customizationArgsMap["minAllowableSelectionCount"]?.signedInt ?: 1 } @@ -106,6 +112,27 @@ class SelectionInteractionViewModel private constructor( pendingAnswerError = null, inputAnswerAvailable = true ) + + if (userAnswerState.itemSelection.selectedIndexesCount != 0) { + userAnswerState.itemSelection.selectedIndexesList.forEach { selectedIndex -> + selectedItems += selectedIndex + choiceItems[selectedIndex].isAnswerSelected.set(true) + } + updateItemSelectability() + updateSelectionText() + updateIsAnswerAvailable() + } + + checkPendingAnswerError(userAnswerState.answerErrorCategory) + } + + override fun getUserAnswerState(): UserAnswerState { + return UserAnswerState.newBuilder().apply { + this.itemSelection = ItemSelectionAnswerState.newBuilder().addAllSelectedIndexes( + selectedItems + ).build() + this.answerErrorCategory = answerErrorCetegory + }.build() } override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply { @@ -141,10 +168,14 @@ class SelectionInteractionViewModel private constructor( * updates the error string based on the specified error category. */ override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { + answerErrorCetegory = category pendingAnswerError = when (category) { - AnswerErrorCategory.REAL_TIME -> null + AnswerErrorCategory.REAL_TIME -> { + null + } AnswerErrorCategory.SUBMIT_TIME -> getSubmitTimeError().getErrorMessageFromStringRes(resourceHandler) + else -> null } errorMessage.set(pendingAnswerError) return pendingAnswerError @@ -267,7 +298,8 @@ class SelectionInteractionViewModel private constructor( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState ): StateItemViewModel { return SelectionInteractionViewModel( entityId, @@ -277,7 +309,8 @@ class SelectionInteractionViewModel private constructor( isSplitView, writtenTranslationContext, translationController, - resourceHandler + resourceHandler, + userAnswerState ) } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt index 3d93249cf7c..418dec9383c 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt @@ -1,6 +1,7 @@ package org.oppia.android.app.player.state.itemviewmodel import org.oppia.android.app.model.Interaction +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver @@ -58,7 +59,8 @@ abstract class StateItemViewModel(val viewType: ViewType) : ObservableViewModel( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState = UserAnswerState.getDefaultInstance() ): StateItemViewModel } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt index 7685e3673b9..88082ac39b3 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/TextInputViewModel.kt @@ -6,11 +6,12 @@ import androidx.annotation.StringRes import androidx.databinding.Observable import androidx.databinding.ObservableField import org.oppia.android.R +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState 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,9 +27,11 @@ class TextInputViewModel private constructor( val isSplitView: Boolean, private val writtenTranslationContext: WrittenTranslationContext, private val resourceHandler: AppLanguageResourceHandler, - private val translationController: TranslationController + private val translationController: TranslationController, + userAnswerState: UserAnswerState ) : StateItemViewModel(ViewType.TEXT_INPUT_INTERACTION), InteractionAnswerHandler { - var answerText: CharSequence = "" + var answerText: CharSequence = userAnswerState.textInputAnswer + private var answerErrorCetegory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR val hintText: CharSequence = deriveHintText(interaction) private var pendingAnswerError: String? = null @@ -53,9 +56,11 @@ class TextInputViewModel private constructor( pendingAnswerError = null, inputAnswerAvailable = true ) + checkPendingAnswerError(userAnswerState.answerErrorCategory) } override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { + answerErrorCetegory = category return when (category) { AnswerErrorCategory.REAL_TIME -> null AnswerErrorCategory.SUBMIT_TIME -> { @@ -63,6 +68,7 @@ class TextInputViewModel private constructor( answerText.toString() ).createForText(resourceHandler) } + else -> null }.also { pendingAnswerError = it errorMessage.set(it) @@ -99,6 +105,13 @@ class TextInputViewModel private constructor( } }.build() + override fun getUserAnswerState(): UserAnswerState { + return UserAnswerState.newBuilder().apply { + this.textInputAnswer = answerText.toString() + this.answerErrorCategory = answerErrorCetegory + }.build() + } + private fun deriveHintText(interaction: Interaction): CharSequence { // The subtitled unicode can apparently exist in the structure in two different formats. val placeholderUnicodeOption1 = @@ -134,7 +147,8 @@ class TextInputViewModel private constructor( hasPreviousButton: Boolean, isSplitView: Boolean, writtenTranslationContext: WrittenTranslationContext, - timeToStartNoticeAnimationMs: Long? + timeToStartNoticeAnimationMs: Long?, + userAnswerState: UserAnswerState ): StateItemViewModel { return TextInputViewModel( interaction, @@ -143,7 +157,8 @@ class TextInputViewModel private constructor( isSplitView, writtenTranslationContext, resourceHandler, - translationController + translationController, + userAnswerState ) } } diff --git a/app/src/main/java/org/oppia/android/app/testing/FractionInputInteractionViewTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/FractionInputInteractionViewTestActivity.kt index 1e0a95cb7d9..86259ce5bbf 100644 --- a/app/src/main/java/org/oppia/android/app/testing/FractionInputInteractionViewTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/FractionInputInteractionViewTestActivity.kt @@ -8,11 +8,11 @@ import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.customview.interaction.FractionInputInteractionView +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.InputInteractionViewTestActivityParams import org.oppia.android.app.model.Interaction 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.InteractionAnswerReceiver import org.oppia.android.app.player.state.itemviewmodel.FractionInteractionViewModel diff --git a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt index cdd29071e40..b346a75ac56 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ImageRegionSelectionTestFragmentPresenter.kt @@ -6,13 +6,13 @@ import android.view.ViewGroup import android.widget.Button import androidx.fragment.app.Fragment import org.oppia.android.R +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.ImageWithRegions.LabeledRegion import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.Point2d import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.player.state.ImageRegionSelectionInteractionView -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.InteractionAnswerReceiver import org.oppia.android.app.player.state.itemviewmodel.ImageRegionSelectionInteractionViewModel diff --git a/app/src/main/java/org/oppia/android/app/testing/InputInteractionViewTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/InputInteractionViewTestActivity.kt index bda539ed689..506e89efd49 100644 --- a/app/src/main/java/org/oppia/android/app/testing/InputInteractionViewTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/InputInteractionViewTestActivity.kt @@ -8,11 +8,11 @@ import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.customview.interaction.NumericInputInteractionView +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.InputInteractionViewTestActivityParams import org.oppia.android.app.model.Interaction 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.InteractionAnswerReceiver import org.oppia.android.app.player.state.itemviewmodel.NumericInputViewModel diff --git a/app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity.kt index aaa119915e3..620baeae186 100644 --- a/app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity.kt @@ -7,12 +7,12 @@ import androidx.databinding.DataBindingUtil import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.MathExpressionInteractionsViewTestActivityParams import org.oppia.android.app.model.MathExpressionInteractionsViewTestActivityParams.MathInteractionType 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.InteractionAnswerReceiver import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractionsViewModel diff --git a/app/src/main/java/org/oppia/android/app/testing/RatioInputInteractionViewTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/RatioInputInteractionViewTestActivity.kt index f307c69d318..348f0c14857 100644 --- a/app/src/main/java/org/oppia/android/app/testing/RatioInputInteractionViewTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/RatioInputInteractionViewTestActivity.kt @@ -8,12 +8,12 @@ import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.customview.interaction.RatioInputInteractionView +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.InputInteractionViewTestActivityParams import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.SchemaObject 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.InteractionAnswerReceiver import org.oppia.android.app.player.state.itemviewmodel.RatioExpressionInputInteractionViewModel diff --git a/app/src/main/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivity.kt index 2a175fb9429..a997d12a554 100644 --- a/app/src/main/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/TextInputInteractionViewTestActivity.kt @@ -6,10 +6,10 @@ import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponentImpl import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity import org.oppia.android.app.customview.interaction.TextInputInteractionView +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.Interaction 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.InteractionAnswerReceiver import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragment.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragment.kt index 23cb0b8afe0..b5e67ec1318 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragment.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragment.kt @@ -12,6 +12,7 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.QuestionPlayerFragmentArguments import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.android.app.player.state.listener.ContinueNavigationButtonListener @@ -55,10 +56,17 @@ class QuestionPlayerFragment : val args = checkNotNull(arguments) { "Expected arguments to be passed to QuestionPlayerFragment" } + val userAnswerState = savedInstanceState?.getProto( + QUESTION_PLAYER_FRAGMENT_STATE_KEY, + UserAnswerState.getDefaultInstance() + ) ?: UserAnswerState.getDefaultInstance() + val arguments = args.getProto(ARGUMENTS_KEY, QuestionPlayerFragmentArguments.getDefaultInstance()) val profileId = arguments.profileId - return questionPlayerFragmentPresenter.handleCreateView(inflater, container, profileId) + return questionPlayerFragmentPresenter.handleCreateView( + inflater, container, profileId, userAnswerState + ) } override fun onAnswerReadyForSubmission(answer: UserAnswer) { @@ -105,6 +113,9 @@ class QuestionPlayerFragment : /** Arguments key for [QuestionPlayerFragment]. */ const val ARGUMENTS_KEY = "QuestionPlayerFragment.arguments" + /** Arguments key for QuestionPlayerFragment saved state. */ + const val QUESTION_PLAYER_FRAGMENT_STATE_KEY = "QuestionPlayerFragment.state" + /** * Creates a new fragment to play a question session. * @@ -124,4 +135,12 @@ class QuestionPlayerFragment : } } } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putProto( + QUESTION_PLAYER_FRAGMENT_STATE_KEY, + questionPlayerFragmentPresenter.getUserAnswerState() + ) + } } diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt index faf85e8a885..7b4861580ab 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt @@ -21,6 +21,7 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.QuestionPlayerFragmentArguments import org.oppia.android.app.model.State import org.oppia.android.app.model.UserAnswer +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.player.state.ConfettiConfig.MINI_CONFETTI_BURST import org.oppia.android.app.player.state.StatePlayerRecyclerViewAssembler import org.oppia.android.app.player.state.listener.RouteToHintsAndSolutionListener @@ -78,7 +79,8 @@ class QuestionPlayerFragmentPresenter @Inject constructor( fun handleCreateView( inflater: LayoutInflater, container: ViewGroup?, - profileId: ProfileId + profileId: ProfileId, + userAnswerState: UserAnswerState ): View? { binding = QuestionPlayerFragmentBinding.inflate( inflater, @@ -88,7 +90,7 @@ class QuestionPlayerFragmentPresenter @Inject constructor( this.profileId = profileId recyclerViewAssembler = createRecyclerViewAssembler( - assemblerBuilderFactory.create(resourceBucketName, "skill", profileId), + assemblerBuilderFactory.create(resourceBucketName, "skill", profileId, userAnswerState), binding.congratulationsTextView, binding.congratulationsTextConfettiView ) @@ -169,6 +171,11 @@ class QuestionPlayerFragmentPresenter @Inject constructor( recyclerViewAssembler.adapter.notifyDataSetChanged() } + /** Returns the [UserAnswerState] representing the user's current pending answer. */ + fun getUserAnswerState(): UserAnswerState { + return questionViewModel.getUserAnswerState(recyclerViewAssembler::getPendingAnswerHandler) + } + /** * Updates whether the submit button should be active based on whether the pending answer is in an * error state. @@ -272,6 +279,9 @@ class QuestionPlayerFragmentPresenter @Inject constructor( private fun subscribeToAnswerOutcome( answerOutcomeResultLiveData: LiveData> ) { + if (questionViewModel.getCanSubmitAnswer().get() == true) { + recyclerViewAssembler.resetUserAnswerState() + } val answerOutcomeLiveData = Transformations.map(answerOutcomeResultLiveData, ::processAnsweredQuestionOutcome) answerOutcomeLiveData.observe( diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerViewModel.kt index ee470eb7b66..4447e07bb80 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerViewModel.kt @@ -4,8 +4,9 @@ import androidx.databinding.ObservableBoolean import androidx.databinding.ObservableField import androidx.databinding.ObservableList import org.oppia.android.R +import org.oppia.android.app.model.AnswerErrorCategory import org.oppia.android.app.model.UserAnswer -import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory +import org.oppia.android.app.model.UserAnswerState import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel import org.oppia.android.app.translation.AppLanguageResourceHandler @@ -92,6 +93,13 @@ class QuestionPlayerViewModel @Inject constructor( } } + fun getUserAnswerState( + retrieveAnswerHandler: (List) -> InteractionAnswerHandler? + ): UserAnswerState { + return retrieveAnswerHandler(getAnswerItemList())?.getUserAnswerState() + ?: UserAnswerState.getDefaultInstance() + } + private fun getPendingAnswerWithoutError( answerHandler: InteractionAnswerHandler? ): UserAnswer? { diff --git a/app/src/main/res/layout/item_selection_interaction_items.xml b/app/src/main/res/layout/item_selection_interaction_items.xml index d94e5311ad1..7fe2e6a5ba5 100755 --- a/app/src/main/res/layout/item_selection_interaction_items.xml +++ b/app/src/main/res/layout/item_selection_interaction_items.xml @@ -27,13 +27,12 @@ android:id="@+id/item_selection_checkbox" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:checked="@{viewModel.answerSelected}" + app:buttonTint="@{viewModel.isEnabled ? @color/component_color_shared_item_selection_interaction_enabled_color : @color/component_color_shared_item_selection_interaction_disabled_color}" android:clickable="false" android:enabled="@{viewModel.isEnabled}" android:focusable="false" android:labelFor="@id/item_selection_contents_text_view" - app:buttonTint="@{viewModel.isEnabled ? @color/component_color_shared_item_selection_interaction_enabled_color : @color/component_color_shared_item_selection_interaction_disabled_color}" /> - + android:checked="@{viewModel.answerSelected}" />