Skip to content

Commit

Permalink
Fix #1468: Retain Drag and Drop state after an incorrect answer (#5541)
Browse files Browse the repository at this point in the history
<!-- READ ME FIRST: Please fill in the explanation section below and
check off every point from the Essential Checklist! -->
## Explanation
<!--
- Explain what your PR does. If this PR fixes an existing bug, please
include
- "Fixes #bugnum:" in the explanation so that GitHub can auto-close the
issue
  - when this PR is merged.
  -->

Fix #1468 

### This PR includes

- Utilized `explorationProgressController` to retrieve the
**'wrongAnswerList'** from ephemeralState's pendingState and use it to
populate the `DragDropInteractionContentViewModel` list.
- When `wrongAnswerList` is available, corresponding content IDs and
HTML values are applied. If not, the original `contentIdHtmlMap` and
`SubtitledHtml` values are used as a fallback.
- The `choiceItems` are set up as LiveData to ensure the UI can react to
changes.
- The `_choiceItems` are copied to `_originalChoiceItems ` for detecting
arrangement box errors during submission when the answer has not
changed.
- Retains linked-merged / unlinked states based on the `wrongAnswerList`
data

## For UI-specific PRs only

### Re-order Retain State


https://github.com/user-attachments/assets/f4c3fd72-d96d-4ee8-b82e-af134ea06a2b

### Group / Unlink Retain


https://github.com/user-attachments/assets/c90a5fc2-ccf9-4e21-a7cd-31f9ec1bd6c0

### Espresso Tests


![image](https://github.com/user-attachments/assets/2e3617eb-4d28-4bee-b8a9-f94c2a3b0231)

## Essential Checklist
<!-- Please tick the relevant boxes by putting an "x" in them. -->
- [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)).

---------

Co-authored-by: Adhiambo Peres <[email protected]>
  • Loading branch information
Rd4dev and adhiamboperes authored Dec 10, 2024
1 parent edb01a8 commit e3642c2
Show file tree
Hide file tree
Showing 2 changed files with 278 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package org.oppia.android.app.player.state.itemviewmodel
import androidx.annotation.StringRes
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.Transformations
import androidx.recyclerview.widget.RecyclerView
import org.oppia.android.R
import org.oppia.android.app.model.AnswerErrorCategory
import org.oppia.android.app.model.EphemeralState
import org.oppia.android.app.model.Interaction
import org.oppia.android.app.model.InteractionObject
import org.oppia.android.app.model.ListOfSetsOfHtmlStrings
Expand All @@ -24,7 +28,10 @@ import org.oppia.android.app.recyclerview.BindableAdapter
import org.oppia.android.app.recyclerview.OnDragEndedListener
import org.oppia.android.app.recyclerview.OnItemDragListener
import org.oppia.android.app.translation.AppLanguageResourceHandler
import org.oppia.android.domain.exploration.ExplorationProgressController
import org.oppia.android.domain.translation.TranslationController
import org.oppia.android.util.data.AsyncResult
import org.oppia.android.util.data.DataProviders.Companion.toLiveData
import javax.inject.Inject

/** Represents the type of errors that can be thrown by drag and drop sort interaction. */
Expand All @@ -49,11 +56,13 @@ class DragAndDropSortInteractionViewModel private constructor(
private val writtenTranslationContext: WrittenTranslationContext,
private val resourceHandler: AppLanguageResourceHandler,
private val translationController: TranslationController,
userAnswerState: UserAnswerState
userAnswerState: UserAnswerState,
private val explorationProgressController: ExplorationProgressController
) : StateItemViewModel(ViewType.DRAG_DROP_SORT_INTERACTION),
InteractionAnswerHandler,
OnItemDragListener,
OnDragEndedListener {

private val allowMultipleItemsInSamePosition: Boolean by lazy {
interaction.customizationArgsMap["allowMultipleItemsInSamePosition"]?.boolValue ?: false
}
Expand All @@ -72,19 +81,18 @@ class DragAndDropSortInteractionViewModel private constructor(
subtitledHtml.contentId to translatedHtml
}

private var answerErrorCetegory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR
private var answerErrorCategory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR

private val _originalChoiceItems: MutableList<DragDropInteractionContentViewModel> =
private var _originalChoiceItems: MutableList<DragDropInteractionContentViewModel> =
computeOriginalChoiceItems(contentIdHtmlMap, choiceSubtitledHtmls, this, resourceHandler)

private val _choiceItems = computeSelectedChoiceItems(
contentIdHtmlMap,
choiceSubtitledHtmls,
this,
resourceHandler,
userAnswerState
)
val choiceItems: List<DragDropInteractionContentViewModel> = _choiceItems
lateinit var choiceItems: LiveData<List<DragDropInteractionContentViewModel>>
private var _choiceItems: MutableList<DragDropInteractionContentViewModel> =
computeSelectedChoiceItems(
contentIdHtmlMap,
this,
resourceHandler
)

private var pendingAnswerError: String? = null
private val isAnswerAvailable = ObservableField(false)
Expand Down Expand Up @@ -170,7 +178,7 @@ class DragAndDropSortInteractionViewModel private constructor(
* updates the error string based on the specified error category.
*/
override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
answerErrorCetegory = category
answerErrorCategory = category
pendingAnswerError = when (category) {
AnswerErrorCategory.REAL_TIME -> null
AnswerErrorCategory.SUBMIT_TIME ->
Expand Down Expand Up @@ -252,7 +260,8 @@ class DragAndDropSortInteractionViewModel private constructor(
/** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */
class FactoryImpl @Inject constructor(
private val resourceHandler: AppLanguageResourceHandler,
private val translationController: TranslationController
private val translationController: TranslationController,
private val explorationProgressController: ExplorationProgressController
) : InteractionItemFactory {
override fun create(
entityId: String,
Expand All @@ -275,15 +284,16 @@ class DragAndDropSortInteractionViewModel private constructor(
writtenTranslationContext,
resourceHandler,
translationController,
userAnswerState
userAnswerState,
explorationProgressController
)
}
}

override fun getUserAnswerState(): UserAnswerState {
if (_choiceItems == _originalChoiceItems) {
return UserAnswerState.newBuilder().apply {
this.answerErrorCategory = answerErrorCetegory
this.answerErrorCategory = answerErrorCategory
}.build()
}
return UserAnswerState.newBuilder().apply {
Expand All @@ -292,7 +302,7 @@ class DragAndDropSortInteractionViewModel private constructor(
ListOfSetsOfTranslatableHtmlContentIds.newBuilder().apply {
addAllContentIdLists(htmlContentIds)
}.build()
answerErrorCategory = answerErrorCetegory
answerErrorCategory = answerErrorCategory
}.build()
}

Expand Down Expand Up @@ -324,25 +334,69 @@ class DragAndDropSortInteractionViewModel private constructor(

private fun computeSelectedChoiceItems(
contentIdHtmlMap: Map<String, String>,
choiceStrings: List<SubtitledHtml>,
dragAndDropSortInteractionViewModel: DragAndDropSortInteractionViewModel,
resourceHandler: AppLanguageResourceHandler,
userAnswerState: UserAnswerState
resourceHandler: AppLanguageResourceHandler
): MutableList<DragDropInteractionContentViewModel> {
return if (userAnswerState.listOfSetsOfTranslatableHtmlContentIds.contentIdListsCount == 0) {
_originalChoiceItems.toMutableList()
} else {
userAnswerState.listOfSetsOfTranslatableHtmlContentIds.contentIdListsList
.mapIndexed { index, contentId ->
DragDropInteractionContentViewModel(
contentIdHtmlMap = contentIdHtmlMap,
htmlContent = contentId,
itemIndex = index,
listSize = choiceStrings.size,
dragAndDropSortInteractionViewModel = dragAndDropSortInteractionViewModel,
resourceHandler = resourceHandler
val explorationEphemeralStateLiveData = MediatorLiveData<AsyncResult<EphemeralState>>().apply {
value = AsyncResult.Pending()
addSource(explorationProgressController.getCurrentState().toLiveData()) { result ->
value = result
}
}

choiceItems = Transformations.map(explorationEphemeralStateLiveData) { result ->
when (result) {
is AsyncResult.Failure, is AsyncResult.Pending -> {
_originalChoiceItems
}
is AsyncResult.Success -> {
_choiceItems = processEphemeralStateResult(
result.value,
contentIdHtmlMap,
dragAndDropSortInteractionViewModel,
resourceHandler
)
}.toMutableList()
_originalChoiceItems = _choiceItems.toMutableList()
_choiceItems
}
else -> _originalChoiceItems
}
}

return _choiceItems.takeIf { !it.isNullOrEmpty() }
?: _originalChoiceItems.toMutableList()
}

private fun processEphemeralStateResult(
state: EphemeralState,
contentIdHtmlMap: Map<String, String>,
dragAndDropSortInteractionViewModel: DragAndDropSortInteractionViewModel,
resourceHandler: AppLanguageResourceHandler
): MutableList<DragDropInteractionContentViewModel> {
val wrongAnswerList = state.pendingState.wrongAnswerList
return if (wrongAnswerList.isNotEmpty()) {
val latestWrongAnswerContentIdList = wrongAnswerList.last()
.userAnswer.answer.listOfSetsOfTranslatableHtmlContentIds.contentIdListsList
latestWrongAnswerContentIdList.mapIndexed { index, setOfTranslatableHtmlContentIds ->
DragDropInteractionContentViewModel(
contentIdHtmlMap = contentIdHtmlMap,
htmlContent = SetOfTranslatableHtmlContentIds.newBuilder().apply {
for (contentIds in setOfTranslatableHtmlContentIds.contentIdsList) {
addContentIds(
TranslatableHtmlContentId.newBuilder().apply {
contentId = contentIds.contentId
}
)
}
}.build(),
itemIndex = index,
listSize = latestWrongAnswerContentIdList.size,
dragAndDropSortInteractionViewModel = dragAndDropSortInteractionViewModel,
resourceHandler = resourceHandler
)
}.toMutableList()
} else {
_originalChoiceItems.toMutableList()
}
}
}
Loading

0 comments on commit e3642c2

Please sign in to comment.