Skip to content

Commit 3b7ddb9

Browse files
Fix #4470, #4472, #4473 : Handle configuration change using onSavedInstance. (#5478)
<!-- READ ME FIRST: Please fill in the explanation section below and check off every point from the Essential Checklist! --> ## Explanation Fixes #4470, Fixes #4472, Fixes #4473. This PR enables the retention of input when the device configuration changes using onSavedInstance. List of interactions covered in this PR: 1. Image Region selection interaction. 2. Drag and Drop interaction | Drag and Drop | ------------------- https://github.com/user-attachments/assets/1919f99d-d56d-4bbe-b0bc-6092c488165e | Image Region | ------------------- https://github.com/user-attachments/assets/ac1ebcd8-92cd-47ca-82af-1208751a7093 Rest are covered in this PR #5458 <!-- - 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. --> ## 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)). ## For UI-specific PRs only <!-- Delete these section if this PR does not include UI-related changes. --> 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
1 parent a85cc50 commit 3b7ddb9

File tree

8 files changed

+243
-27
lines changed

8 files changed

+243
-27
lines changed

app/src/main/java/org/oppia/android/app/player/state/ImageRegionSelectionInteractionView.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.core.view.forEachIndexed
99
import androidx.fragment.app.Fragment
1010
import androidx.fragment.app.FragmentManager
1111
import org.oppia.android.app.model.ImageWithRegions
12+
import org.oppia.android.app.model.UserAnswerState
1213
import org.oppia.android.app.shim.ViewBindingShim
1314
import org.oppia.android.app.utility.ClickableAreasImage
1415
import org.oppia.android.app.utility.OnClickableAreaClickedListener
@@ -52,6 +53,8 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor(
5253
private lateinit var imageUrl: String
5354
private lateinit var clickableAreas: List<ImageWithRegions.LabeledRegion>
5455

56+
private lateinit var userAnswerState: UserAnswerState
57+
5558
/**
5659
* Sets the URL for the image & initiates loading it. This is intended to be called via
5760
* data-binding.
@@ -61,6 +64,10 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor(
6164
maybeInitializeClickableAreas()
6265
}
6366

67+
fun setUserAnswerState(userAnswerrState: UserAnswerState) {
68+
this.userAnswerState = userAnswerrState
69+
}
70+
6471
fun setEntityId(entityId: String) {
6572
this.entityId = entityId
6673
maybeInitializeClickableAreas()
@@ -121,7 +128,8 @@ class ImageRegionSelectionInteractionView @JvmOverloads constructor(
121128
onRegionClicked,
122129
bindingInterface,
123130
isAccessibilityEnabled = accessibilityService.isScreenReaderEnabled(),
124-
clickableAreas
131+
clickableAreas,
132+
userAnswerState
125133
)
126134
areasImage.addRegionViews()
127135
performAttachment(areasImage)

app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ class DragAndDropSortInteractionViewModel private constructor(
4848
val isSplitView: Boolean,
4949
private val writtenTranslationContext: WrittenTranslationContext,
5050
private val resourceHandler: AppLanguageResourceHandler,
51-
private val translationController: TranslationController
51+
private val translationController: TranslationController,
52+
userAnswerState: UserAnswerState
5253
) : StateItemViewModel(ViewType.DRAG_DROP_SORT_INTERACTION),
5354
InteractionAnswerHandler,
5455
OnItemDragListener,
@@ -71,10 +72,18 @@ class DragAndDropSortInteractionViewModel private constructor(
7172
subtitledHtml.contentId to translatedHtml
7273
}
7374

75+
private var answerErrorCetegory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR
76+
7477
private val _originalChoiceItems: MutableList<DragDropInteractionContentViewModel> =
75-
computeChoiceItems(contentIdHtmlMap, choiceSubtitledHtmls, this, resourceHandler)
78+
computeOriginalChoiceItems(contentIdHtmlMap, choiceSubtitledHtmls, this, resourceHandler)
7679

77-
private val _choiceItems = _originalChoiceItems.toMutableList()
80+
private val _choiceItems = computeSelectedChoiceItems(
81+
contentIdHtmlMap,
82+
choiceSubtitledHtmls,
83+
this,
84+
resourceHandler,
85+
userAnswerState
86+
)
7887
val choiceItems: List<DragDropInteractionContentViewModel> = _choiceItems
7988

8089
private var pendingAnswerError: String? = null
@@ -99,6 +108,7 @@ class DragAndDropSortInteractionViewModel private constructor(
99108
pendingAnswerError = null,
100109
inputAnswerAvailable = true
101110
)
111+
checkPendingAnswerError(userAnswerState.answerErrorCategory)
102112
}
103113

104114
override fun onItemDragged(
@@ -160,6 +170,7 @@ class DragAndDropSortInteractionViewModel private constructor(
160170
* updates the error string based on the specified error category.
161171
*/
162172
override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
173+
answerErrorCetegory = category
163174
pendingAnswerError = when (category) {
164175
AnswerErrorCategory.REAL_TIME -> null
165176
AnswerErrorCategory.SUBMIT_TIME ->
@@ -232,9 +243,9 @@ class DragAndDropSortInteractionViewModel private constructor(
232243
}
233244

234245
private fun getSubmitTimeError(): DragAndDropSortInteractionError {
235-
return if (_originalChoiceItems == _choiceItems)
246+
return if (_originalChoiceItems == _choiceItems) {
236247
DragAndDropSortInteractionError.EMPTY_INPUT
237-
else
248+
} else
238249
DragAndDropSortInteractionError.VALID
239250
}
240251

@@ -263,13 +274,30 @@ class DragAndDropSortInteractionViewModel private constructor(
263274
isSplitView,
264275
writtenTranslationContext,
265276
resourceHandler,
266-
translationController
277+
translationController,
278+
userAnswerState
267279
)
268280
}
269281
}
270282

283+
override fun getUserAnswerState(): UserAnswerState {
284+
if (_choiceItems == _originalChoiceItems) {
285+
return UserAnswerState.newBuilder().apply {
286+
this.answerErrorCategory = answerErrorCetegory
287+
}.build()
288+
}
289+
return UserAnswerState.newBuilder().apply {
290+
val htmlContentIds = _choiceItems.map { it.htmlContent }
291+
listOfSetsOfTranslatableHtmlContentIds =
292+
ListOfSetsOfTranslatableHtmlContentIds.newBuilder().apply {
293+
addAllContentIdLists(htmlContentIds)
294+
}.build()
295+
answerErrorCategory = answerErrorCetegory
296+
}.build()
297+
}
298+
271299
companion object {
272-
private fun computeChoiceItems(
300+
private fun computeOriginalChoiceItems(
273301
contentIdHtmlMap: Map<String, String>,
274302
choiceStrings: List<SubtitledHtml>,
275303
dragAndDropSortInteractionViewModel: DragAndDropSortInteractionViewModel,
@@ -293,4 +321,28 @@ class DragAndDropSortInteractionViewModel private constructor(
293321
}.toMutableList()
294322
}
295323
}
324+
325+
private fun computeSelectedChoiceItems(
326+
contentIdHtmlMap: Map<String, String>,
327+
choiceStrings: List<SubtitledHtml>,
328+
dragAndDropSortInteractionViewModel: DragAndDropSortInteractionViewModel,
329+
resourceHandler: AppLanguageResourceHandler,
330+
userAnswerState: UserAnswerState
331+
): MutableList<DragDropInteractionContentViewModel> {
332+
return if (userAnswerState.listOfSetsOfTranslatableHtmlContentIds.contentIdListsCount == 0) {
333+
_originalChoiceItems.toMutableList()
334+
} else {
335+
userAnswerState.listOfSetsOfTranslatableHtmlContentIds.contentIdListsList
336+
.mapIndexed { index, contentId ->
337+
DragDropInteractionContentViewModel(
338+
contentIdHtmlMap = contentIdHtmlMap,
339+
htmlContent = contentId,
340+
itemIndex = index,
341+
listSize = choiceStrings.size,
342+
dragAndDropSortInteractionViewModel = dragAndDropSortInteractionViewModel,
343+
resourceHandler = resourceHandler
344+
)
345+
}.toMutableList()
346+
}
347+
}
296348
}

app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ class ImageRegionSelectionInteractionViewModel private constructor(
3030
private val errorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver,
3131
val isSplitView: Boolean,
3232
private val writtenTranslationContext: WrittenTranslationContext,
33-
private val resourceHandler: AppLanguageResourceHandler
33+
private val resourceHandler: AppLanguageResourceHandler,
34+
userAnswerState: UserAnswerState
3435
) : StateItemViewModel(ViewType.IMAGE_REGION_SELECTION_INTERACTION),
3536
InteractionAnswerHandler,
3637
OnClickableAreaClickedListener {
@@ -43,6 +44,12 @@ class ImageRegionSelectionInteractionViewModel private constructor(
4344
schemaObject?.customSchemaValue?.imageWithRegions?.labelRegionsList ?: listOf()
4445
}
4546

47+
val observableUserAnswrerState by lazy {
48+
ObservableField(userAnswerState)
49+
}
50+
51+
private var answerErrorCetegory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR
52+
4653
val imagePath: String by lazy {
4754
val schemaObject = interaction.customizationArgsMap["imageAndRegions"]
4855
schemaObject?.customSchemaValue?.imageWithRegions?.imagePath ?: ""
@@ -68,10 +75,10 @@ class ImageRegionSelectionInteractionViewModel private constructor(
6875
pendingAnswerError = null,
6976
inputAnswerAvailable = true
7077
)
78+
checkPendingAnswerError(userAnswerState.answerErrorCategory)
7179
}
7280

7381
override fun onClickableAreaTouched(region: RegionClickedEvent) {
74-
7582
when (region) {
7683
is DefaultRegionClickedEvent -> {
7784
answerText = ""
@@ -88,6 +95,7 @@ class ImageRegionSelectionInteractionViewModel private constructor(
8895

8996
/** It checks the pending error for the current image region input, and correspondingly updates the error string based on the specified error category. */
9097
override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
98+
answerErrorCetegory = category
9199
when (category) {
92100
AnswerErrorCategory.REAL_TIME -> {
93101
pendingAnswerError = null
@@ -110,18 +118,35 @@ class ImageRegionSelectionInteractionViewModel private constructor(
110118
return pendingAnswerError
111119
}
112120

113-
override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply {
114-
val answerTextString = answerText.toString()
115-
answer = InteractionObject.newBuilder().apply {
116-
clickOnImage = parseClickOnImage(answerTextString)
121+
override fun getUserAnswerState(): UserAnswerState {
122+
return UserAnswerState.newBuilder().apply {
123+
if (answerText.isNotEmpty()) {
124+
this.imageLabel = answerText.toString()
125+
}
126+
this.answerErrorCategory = answerErrorCetegory
117127
}.build()
118-
plainAnswer = resourceHandler.getStringInLocaleWithWrapping(
119-
R.string.image_interaction_answer_text,
120-
answerTextString
121-
)
122-
this.writtenTranslationContext =
123-
this@ImageRegionSelectionInteractionViewModel.writtenTranslationContext
124-
}.build()
128+
}
129+
130+
override fun getPendingAnswer(): UserAnswer {
131+
// Resetting Observable UserAnswerState to its default instance to ensure that
132+
// the ImageRegionSelectionInteractionView reflects no image region selection.
133+
// This is necessary because ImageRegionSelectionInteractionView is not recreated every time
134+
// the user submits an answer, causing it to retain the old UserAnswerState.
135+
observableUserAnswrerState.set(UserAnswerState.getDefaultInstance())
136+
137+
return UserAnswer.newBuilder().apply {
138+
val answerTextString = answerText.toString()
139+
answer = InteractionObject.newBuilder().apply {
140+
clickOnImage = parseClickOnImage(answerTextString)
141+
}.build()
142+
plainAnswer = resourceHandler.getStringInLocaleWithWrapping(
143+
R.string.image_interaction_answer_text,
144+
answerTextString
145+
)
146+
this.writtenTranslationContext =
147+
this@ImageRegionSelectionInteractionViewModel.writtenTranslationContext
148+
}.build()
149+
}
125150

126151
private fun parseClickOnImage(answerTextString: String): ClickOnImage {
127152
val region = selectableRegions.find { it.label == answerTextString }
@@ -204,7 +229,8 @@ class ImageRegionSelectionInteractionViewModel private constructor(
204229
answerErrorReceiver,
205230
isSplitView,
206231
writtenTranslationContext,
207-
resourceHandler
232+
resourceHandler,
233+
userAnswerState
208234
)
209235
}
210236
}

app/src/main/java/org/oppia/android/app/utility/ClickableAreasImage.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.core.view.forEachIndexed
1010
import androidx.core.view.isVisible
1111
import org.oppia.android.R
1212
import org.oppia.android.app.model.ImageWithRegions.LabeledRegion
13+
import org.oppia.android.app.model.UserAnswerState
1314
import org.oppia.android.app.player.state.ImageRegionSelectionInteractionView
1415
import org.oppia.android.app.shim.ViewBindingShim
1516
import kotlin.math.roundToInt
@@ -21,11 +22,19 @@ class ClickableAreasImage(
2122
private val listener: OnClickableAreaClickedListener,
2223
bindingInterface: ViewBindingShim,
2324
private val isAccessibilityEnabled: Boolean,
24-
private val clickableAreas: List<LabeledRegion>
25+
private val clickableAreas: List<LabeledRegion>,
26+
userAnswerState: UserAnswerState
2527
) {
28+
private var imageLabel: String? = null
2629
private val defaultRegionView by lazy { bindingInterface.getDefaultRegion(parentView) }
2730

28-
init { imageView.initializeShowRegionTouchListener() }
31+
init {
32+
imageView.initializeShowRegionTouchListener()
33+
34+
if (userAnswerState.imageLabel.isNotBlank()) {
35+
imageLabel = userAnswerState.imageLabel
36+
}
37+
}
2938

3039
/**
3140
* Called when an image is clicked.
@@ -41,7 +50,7 @@ class ClickableAreasImage(
4150
defaultRegionView.setBackgroundResource(R.drawable.selected_region_background)
4251
defaultRegionView.x = x
4352
defaultRegionView.y = y
44-
listener.onClickableAreaTouched(DefaultRegionClickedEvent())
53+
listener.onClickableAreaTouched(DefaultRegionClickedEvent(x, y))
4554
}
4655
}
4756

@@ -104,6 +113,9 @@ class ClickableAreasImage(
104113
newView.isFocusableInTouchMode = true
105114
newView.tag = clickableArea.label
106115
newView.initializeToggleRegionTouchListener(clickableArea)
116+
if (clickableArea.label.equals(imageLabel)) {
117+
showOrHideRegion(newView = newView, clickableArea = clickableArea)
118+
}
107119
if (isAccessibilityEnabled) {
108120
// Make default region visibility gone when talkback enabled to avoid any accidental touch.
109121
defaultRegionView.isVisible = false

app/src/main/java/org/oppia/android/app/utility/RegionClickEvent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ data class NamedRegionClickedEvent(val regionLabel: String, val contentDescripti
1717
* Class to be used in case when [OnClickableAreaClickedListener] is called with an unspecified
1818
* region that is when any other is tapped on which wasn't defined by creator.
1919
*/
20-
class DefaultRegionClickedEvent : RegionClickedEvent()
20+
class DefaultRegionClickedEvent(val x: Float, val y: Float) : RegionClickedEvent()

app/src/main/res/layout/image_region_selection_interaction_item.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
app:clickableAreas="@{viewModel.selectableRegions}"
5959
app:entityId="@{viewModel.entityId}"
6060
app:imageUrl="@{viewModel.imagePath}"
61+
app:userAnswerState="@{viewModel.observableUserAnswrerState}"
6162
app:onRegionClicked="@{(region) -> viewModel.onClickableAreaTouched(region)}"
6263
app:overlayView="@{interactionContainerFrameLayout}" />
6364

0 commit comments

Comments
 (0)