Skip to content

Commit 59a41ab

Browse files
Fix part of #5070: Display empty answer message in text input interaction (#5311)
<!-- READ ME FIRST: Please fill in the explanation section below and check off every point from the Essential Checklist! --> ## Explanation Fixes part of #5070, In TextInteraction UI, leave submit button enabled when answer is empty. Show an error on submitting an empty answer. Created own test suite for text input interaction view. [text_input.webm](https://github.com/oppia/oppia-android/assets/76042077/a5882904-8152-4422-b9c9-c937cb056dd5) <!-- - 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. --> - [ ] 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: ...".) - [ ] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [ ] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [ ] 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)). - [ ] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [ ] 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 0911e71 commit 59a41ab

12 files changed

+517
-98
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,9 @@
187187
android:theme="@style/OppiaThemeWithoutActionBar" />
188188
<activity
189189
android:name=".app.splash.SplashActivity"
190+
android:exported="true"
190191
android:label="@string/app_name"
191192
android:screenOrientation="portrait"
192-
android:exported="true"
193193
android:theme="@style/SplashScreenTheme">
194194
<intent-filter>
195195
<action android:name="android.intent.action.MAIN" />
@@ -214,6 +214,7 @@
214214
<activity android:name=".app.testing.ExplorationInjectionActivity" />
215215
<activity android:name=".app.testing.ExplorationTestActivity" />
216216
<activity android:name=".app.testing.FractionInputInteractionViewTestActivity" />
217+
<activity android:name=".app.testing.TextInputInteractionViewTestActivity" />
217218
<activity
218219
android:name=".app.testing.TestFontScaleConfigurationUtilActivity"
219220
android:theme="@style/OppiaThemeWithoutActionBar" />

app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import org.oppia.android.app.testing.SpotlightFragmentTestActivity
8383
import org.oppia.android.app.testing.StateAssemblerMarginBindingAdaptersTestActivity
8484
import org.oppia.android.app.testing.StateAssemblerPaddingBindingAdaptersTestActivity
8585
import org.oppia.android.app.testing.TestFontScaleConfigurationUtilActivity
86+
import org.oppia.android.app.testing.TextInputInteractionViewTestActivity
8687
import org.oppia.android.app.testing.TextViewBindingAdaptersTestActivity
8788
import org.oppia.android.app.testing.TopicRevisionTestActivity
8889
import org.oppia.android.app.testing.TopicTestActivity
@@ -150,6 +151,7 @@ interface ActivityComponentImpl :
150151
fun inject(imageRegionSelectionTestActivity: ImageRegionSelectionTestActivity)
151152
fun inject(imageViewBindingAdaptersTestActivity: ImageViewBindingAdaptersTestActivity)
152153
fun inject(inputInteractionViewTestActivity: InputInteractionViewTestActivity)
154+
fun inject(textInputInteractionViewTestActivity: TextInputInteractionViewTestActivity)
153155
fun inject(ratioInputInteractionViewTestActivity: RatioInputInteractionViewTestActivity)
154156
fun inject(licenseListActivity: LicenseListActivity)
155157
fun inject(licenseTextViewerActivity: LicenseTextViewerActivity)

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package org.oppia.android.app.player.state.itemviewmodel
22

33
import android.text.Editable
44
import android.text.TextWatcher
5+
import androidx.annotation.StringRes
56
import androidx.databinding.Observable
67
import androidx.databinding.ObservableField
78
import org.oppia.android.R
89
import org.oppia.android.app.model.Interaction
910
import org.oppia.android.app.model.InteractionObject
1011
import org.oppia.android.app.model.UserAnswer
1112
import org.oppia.android.app.model.WrittenTranslationContext
13+
import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory
1214
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
1315
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler
1416
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
@@ -28,20 +30,43 @@ class TextInputViewModel private constructor(
2830
) : StateItemViewModel(ViewType.TEXT_INPUT_INTERACTION), InteractionAnswerHandler {
2931
var answerText: CharSequence = ""
3032
val hintText: CharSequence = deriveHintText(interaction)
33+
private var pendingAnswerError: String? = null
3134

3235
var isAnswerAvailable = ObservableField<Boolean>(false)
36+
val errorMessage = ObservableField<String>("")
3337

3438
init {
3539
val callback: Observable.OnPropertyChangedCallback =
3640
object : Observable.OnPropertyChangedCallback() {
3741
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
3842
interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
39-
/* pendingAnswerError= */ null,
40-
answerText.isNotEmpty()
43+
pendingAnswerError = pendingAnswerError,
44+
inputAnswerAvailable = true // Allow submit on empty answer.
4145
)
4246
}
4347
}
4448
isAnswerAvailable.addOnPropertyChangedCallback(callback)
49+
errorMessage.addOnPropertyChangedCallback(callback)
50+
51+
// Initializing with default values so that submit button is enabled by default.
52+
interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
53+
pendingAnswerError = null,
54+
inputAnswerAvailable = true
55+
)
56+
}
57+
58+
override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
59+
return when (category) {
60+
AnswerErrorCategory.REAL_TIME -> null
61+
AnswerErrorCategory.SUBMIT_TIME -> {
62+
TextParsingUiError.createForText(
63+
answerText.toString()
64+
).createForText(resourceHandler)
65+
}
66+
}.also {
67+
pendingAnswerError = it
68+
errorMessage.set(it)
69+
}
4570
}
4671

4772
fun getAnswerTextWatcher(): TextWatcher {
@@ -55,6 +80,7 @@ class TextInputViewModel private constructor(
5580
if (isAnswerTextAvailable != isAnswerAvailable.get()) {
5681
isAnswerAvailable.set(isAnswerTextAvailable)
5782
}
83+
checkPendingAnswerError(AnswerErrorCategory.REAL_TIME)
5884
}
5985

6086
override fun afterTextChanged(s: Editable) {
@@ -121,4 +147,22 @@ class TextInputViewModel private constructor(
121147
)
122148
}
123149
}
150+
151+
private enum class TextParsingUiError(@StringRes private var error: Int?) {
152+
/** Corresponds to non empty input. */
153+
VALID(error = null),
154+
155+
/** Corresponds to empty input. */
156+
EMPTY_INPUT(error = R.string.text_error_empty_input);
157+
158+
/** Returns the string corresponding to this error's string resources, or null if there is none. */
159+
fun createForText(resourceHandler: AppLanguageResourceHandler): String? =
160+
error?.let(resourceHandler::getStringInLocale)
161+
162+
companion object {
163+
/** Returns the [TextParsingUiError] corresponding to the input. */
164+
fun createForText(text: String): TextParsingUiError =
165+
if (text.isEmpty()) EMPTY_INPUT else VALID
166+
}
167+
}
124168
}

app/src/main/java/org/oppia/android/app/testing/InputInteractionViewTestActivity.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import org.oppia.android.R
99
import org.oppia.android.app.activity.ActivityComponentImpl
1010
import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
1111
import org.oppia.android.app.customview.interaction.NumericInputInteractionView
12-
import org.oppia.android.app.customview.interaction.TextInputInteractionView
1312
import org.oppia.android.app.model.InputInteractionViewTestActivityParams
1413
import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType.ALGEBRAIC_EXPRESSION
1514
import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType.MATH_EQUATION
@@ -26,7 +25,6 @@ import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractio
2625
import org.oppia.android.app.player.state.itemviewmodel.NumericInputViewModel
2726
import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel
2827
import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.InteractionItemFactory
29-
import org.oppia.android.app.player.state.itemviewmodel.TextInputViewModel
3028
import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
3129
import org.oppia.android.databinding.ActivityInputInteractionViewTestBinding
3230
import org.oppia.android.util.extensions.getProtoExtra
@@ -36,7 +34,7 @@ import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractio
3634

3735
/**
3836
* This is a dummy activity to test input interaction views.
39-
* It contains [NumericInputInteractionView],and [TextInputInteractionView].
37+
* It contains [NumericInputInteractionView]
4038
*/
4139
class InputInteractionViewTestActivity :
4240
InjectableAutoLocalizedAppCompatActivity(),
@@ -48,16 +46,11 @@ class InputInteractionViewTestActivity :
4846
@Inject
4947
lateinit var numericInputViewModelFactory: NumericInputViewModel.FactoryImpl
5048

51-
@Inject
52-
lateinit var textInputViewModelFactory: TextInputViewModel.FactoryImpl
53-
5449
@Inject
5550
lateinit var mathExpViewModelFactoryFactory: MathExpViewModelFactoryFactoryImpl
5651

5752
val numericInputViewModel by lazy { numericInputViewModelFactory.create<NumericInputViewModel>() }
5853

59-
val textInputViewModel by lazy { textInputViewModelFactory.create<TextInputViewModel>() }
60-
6154
lateinit var mathExpressionViewModel: MathExpressionInteractionsViewModel
6255
lateinit var writtenTranslationContext: WrittenTranslationContext
6356

@@ -103,7 +96,6 @@ class InputInteractionViewTestActivity :
10396
}
10497

10598
binding.numericInputViewModel = numericInputViewModel
106-
binding.textInputViewModel = textInputViewModel
10799
binding.mathExpressionInteractionsViewModel = mathExpressionViewModel
108100
}
109101

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package org.oppia.android.app.testing
2+
3+
import android.os.Bundle
4+
import android.view.View
5+
import androidx.databinding.DataBindingUtil
6+
import org.oppia.android.R
7+
import org.oppia.android.app.activity.ActivityComponentImpl
8+
import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
9+
import org.oppia.android.app.customview.interaction.TextInputInteractionView
10+
import org.oppia.android.app.model.Interaction
11+
import org.oppia.android.app.model.UserAnswer
12+
import org.oppia.android.app.model.WrittenTranslationContext
13+
import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory
14+
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
15+
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
16+
import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel
17+
import org.oppia.android.app.player.state.itemviewmodel.TextInputViewModel
18+
import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
19+
import org.oppia.android.databinding.ActivityTextInputInteractionViewTestBinding
20+
import javax.inject.Inject
21+
22+
/**
23+
* This is a dummy activity to test input interaction views.
24+
* It contains [TextInputInteractionView]
25+
*/
26+
class TextInputInteractionViewTestActivity :
27+
InjectableAutoLocalizedAppCompatActivity(),
28+
StateKeyboardButtonListener,
29+
InteractionAnswerErrorOrAvailabilityCheckReceiver,
30+
InteractionAnswerReceiver {
31+
32+
private lateinit var binding: ActivityTextInputInteractionViewTestBinding
33+
34+
@Inject
35+
lateinit var textinputViewModelFactory: TextInputViewModel.FactoryImpl
36+
37+
/** Gives access to the [TextInputViewModel]. */
38+
val textInputViewModel by lazy {
39+
textinputViewModelFactory.create<TextInputViewModel>()
40+
}
41+
42+
/** Gives access to the translation context. */
43+
lateinit var writtenTranslationContext: WrittenTranslationContext
44+
45+
override fun onCreate(savedInstanceState: Bundle?) {
46+
super.onCreate(savedInstanceState)
47+
(activityComponent as ActivityComponentImpl).inject(this)
48+
binding = DataBindingUtil.setContentView(
49+
this, R.layout.activity_text_input_interaction_view_test
50+
)
51+
52+
writtenTranslationContext = WrittenTranslationContext.getDefaultInstance()
53+
binding.textInputViewModel = textInputViewModel
54+
}
55+
56+
/** Checks submit-time errors. */
57+
fun getPendingAnswerErrorOnSubmitClick(v: View) {
58+
textInputViewModel.checkPendingAnswerError(AnswerErrorCategory.SUBMIT_TIME)
59+
}
60+
61+
override fun onPendingAnswerErrorOrAvailabilityCheck(
62+
pendingAnswerError: String?,
63+
inputAnswerAvailable: Boolean
64+
) {
65+
}
66+
67+
override fun onAnswerReadyForSubmission(answer: UserAnswer) {
68+
}
69+
70+
override fun onEditorAction(actionCode: Int) {
71+
}
72+
73+
private inline fun <reified T : StateItemViewModel>
74+
StateItemViewModel.InteractionItemFactory.create(
75+
interaction: Interaction = Interaction.getDefaultInstance()
76+
): T {
77+
return create(
78+
entityId = "fake_entity_id",
79+
hasConversationView = false,
80+
interaction = interaction,
81+
interactionAnswerReceiver = this@TextInputInteractionViewTestActivity,
82+
answerErrorReceiver = this@TextInputInteractionViewTestActivity,
83+
hasPreviousButton = false,
84+
isSplitView = false,
85+
writtenTranslationContext,
86+
timeToStartNoticeAnimationMs = null
87+
) as T
88+
}
89+
}

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

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111
name="numericInputViewModel"
1212
type="org.oppia.android.app.player.state.itemviewmodel.NumericInputViewModel" />
1313

14-
<variable
15-
name="textInputViewModel"
16-
type="org.oppia.android.app.player.state.itemviewmodel.TextInputViewModel" />
17-
1814
<variable
1915
name="mathExpressionInteractionsViewModel"
2016
type="org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractionsViewModel" />
@@ -23,6 +19,7 @@
2319
<ScrollView
2420
android:layout_width="match_parent"
2521
android:layout_height="match_parent">
22+
2623
<LinearLayout
2724
android:layout_width="match_parent"
2825
android:layout_height="wrap_content"
@@ -36,18 +33,18 @@
3633
android:id="@+id/test_number_input_interaction_view"
3734
android:layout_width="match_parent"
3835
android:layout_height="wrap_content"
39-
android:minHeight="48dp"
4036
android:layout_margin="8dp"
4137
android:background="@drawable/edit_text_background"
4238
android:focusable="true"
4339
android:hint="@string/test_number_input_interaction_hint"
44-
android:textColor="@color/component_color_shared_primary_text_color"
45-
android:textColorHint="@color/component_color_shared_edit_text_hint_color"
4640
android:longClickable="false"
4741
android:maxLength="200"
42+
android:minHeight="48dp"
4843
android:padding="8dp"
4944
android:singleLine="true"
5045
android:text="@={numericInputViewModel.answerText}"
46+
android:textColor="@color/component_color_shared_primary_text_color"
47+
android:textColorHint="@color/component_color_shared_edit_text_hint_color"
5148
app:textChangedListener="@{numericInputViewModel.answerTextWatcher}" />
5249

5350
<TextView
@@ -64,33 +61,17 @@
6461
android:textSize="12sp"
6562
android:visibility="@{numericInputViewModel.errorMessage.length() > 0 ? View.VISIBLE : View.INVISIBLE}" />
6663

67-
<org.oppia.android.app.customview.interaction.TextInputInteractionView
68-
android:id="@+id/test_text_input_interaction_view"
69-
android:layout_width="match_parent"
70-
android:layout_height="wrap_content"
71-
android:minHeight="48dp"
72-
android:layout_margin="8dp"
73-
android:background="@drawable/edit_text_background"
74-
android:focusable="true"
75-
android:hint="@string/test_text_input_interaction_hint"
76-
android:inputType="text"
77-
android:longClickable="false"
78-
android:maxLength="200"
79-
android:padding="8dp"
80-
android:singleLine="true"
81-
android:text="@={textInputViewModel.answerText}" />
82-
8364
<org.oppia.android.app.customview.interaction.MathExpressionInteractionsView
8465
android:id="@+id/test_math_expression_input_interaction_view"
8566
style="@style/InputInteractionEditText"
86-
android:minHeight="48dp"
87-
app:placeholder="@{mathExpressionInteractionsViewModel.hintText}"
8867
android:inputType="text"
68+
android:minHeight="48dp"
8969
android:text="@={mathExpressionInteractionsViewModel.answerText}"
90-
app:textChangedListener="@{mathExpressionInteractionsViewModel.answerTextWatcher}"
9170
app:layout_constraintEnd_toEndOf="parent"
9271
app:layout_constraintStart_toStartOf="parent"
93-
app:layout_constraintTop_toTopOf="parent"/>
72+
app:layout_constraintTop_toTopOf="parent"
73+
app:placeholder="@{mathExpressionInteractionsViewModel.hintText}"
74+
app:textChangedListener="@{mathExpressionInteractionsViewModel.answerTextWatcher}" />
9475

9576
<Button
9677
android:id="@+id/submit_button"

0 commit comments

Comments
 (0)