Skip to content

Commit

Permalink
Fix part of #5070: Display empty answer message in text input interac…
Browse files Browse the repository at this point in the history
…tion (#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
  • Loading branch information
Vishwajith-Shettigar committed Mar 4, 2024
1 parent 0911e71 commit 59a41ab
Show file tree
Hide file tree
Showing 12 changed files with 517 additions and 98 deletions.
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,9 @@
android:theme="@style/OppiaThemeWithoutActionBar" />
<activity
android:name=".app.splash.SplashActivity"
android:exported="true"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:exported="true"
android:theme="@style/SplashScreenTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand All @@ -214,6 +214,7 @@
<activity android:name=".app.testing.ExplorationInjectionActivity" />
<activity android:name=".app.testing.ExplorationTestActivity" />
<activity android:name=".app.testing.FractionInputInteractionViewTestActivity" />
<activity android:name=".app.testing.TextInputInteractionViewTestActivity" />
<activity
android:name=".app.testing.TestFontScaleConfigurationUtilActivity"
android:theme="@style/OppiaThemeWithoutActionBar" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import org.oppia.android.app.testing.SpotlightFragmentTestActivity
import org.oppia.android.app.testing.StateAssemblerMarginBindingAdaptersTestActivity
import org.oppia.android.app.testing.StateAssemblerPaddingBindingAdaptersTestActivity
import org.oppia.android.app.testing.TestFontScaleConfigurationUtilActivity
import org.oppia.android.app.testing.TextInputInteractionViewTestActivity
import org.oppia.android.app.testing.TextViewBindingAdaptersTestActivity
import org.oppia.android.app.testing.TopicRevisionTestActivity
import org.oppia.android.app.testing.TopicTestActivity
Expand Down Expand Up @@ -150,6 +151,7 @@ interface ActivityComponentImpl :
fun inject(imageRegionSelectionTestActivity: ImageRegionSelectionTestActivity)
fun inject(imageViewBindingAdaptersTestActivity: ImageViewBindingAdaptersTestActivity)
fun inject(inputInteractionViewTestActivity: InputInteractionViewTestActivity)
fun inject(textInputInteractionViewTestActivity: TextInputInteractionViewTestActivity)
fun inject(ratioInputInteractionViewTestActivity: RatioInputInteractionViewTestActivity)
fun inject(licenseListActivity: LicenseListActivity)
fun inject(licenseTextViewerActivity: LicenseTextViewerActivity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package org.oppia.android.app.player.state.itemviewmodel

import android.text.Editable
import android.text.TextWatcher
import androidx.annotation.StringRes
import androidx.databinding.Observable
import androidx.databinding.ObservableField
import org.oppia.android.R
import org.oppia.android.app.model.Interaction
import org.oppia.android.app.model.InteractionObject
import org.oppia.android.app.model.UserAnswer
import org.oppia.android.app.model.WrittenTranslationContext
import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
Expand All @@ -28,20 +30,43 @@ class TextInputViewModel private constructor(
) : StateItemViewModel(ViewType.TEXT_INPUT_INTERACTION), InteractionAnswerHandler {
var answerText: CharSequence = ""
val hintText: CharSequence = deriveHintText(interaction)
private var pendingAnswerError: String? = null

var isAnswerAvailable = ObservableField<Boolean>(false)
val errorMessage = ObservableField<String>("")

init {
val callback: Observable.OnPropertyChangedCallback =
object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
/* pendingAnswerError= */ null,
answerText.isNotEmpty()
pendingAnswerError = pendingAnswerError,
inputAnswerAvailable = true // Allow submit on empty answer.
)
}
}
isAnswerAvailable.addOnPropertyChangedCallback(callback)
errorMessage.addOnPropertyChangedCallback(callback)

// Initializing with default values so that submit button is enabled by default.
interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
pendingAnswerError = null,
inputAnswerAvailable = true
)
}

override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
return when (category) {
AnswerErrorCategory.REAL_TIME -> null
AnswerErrorCategory.SUBMIT_TIME -> {
TextParsingUiError.createForText(
answerText.toString()
).createForText(resourceHandler)
}
}.also {
pendingAnswerError = it
errorMessage.set(it)
}
}

fun getAnswerTextWatcher(): TextWatcher {
Expand All @@ -55,6 +80,7 @@ class TextInputViewModel private constructor(
if (isAnswerTextAvailable != isAnswerAvailable.get()) {
isAnswerAvailable.set(isAnswerTextAvailable)
}
checkPendingAnswerError(AnswerErrorCategory.REAL_TIME)
}

override fun afterTextChanged(s: Editable) {
Expand Down Expand Up @@ -121,4 +147,22 @@ class TextInputViewModel private constructor(
)
}
}

private enum class TextParsingUiError(@StringRes private var error: Int?) {
/** Corresponds to non empty input. */
VALID(error = null),

/** Corresponds to empty input. */
EMPTY_INPUT(error = R.string.text_error_empty_input);

/** Returns the string corresponding to this error's string resources, or null if there is none. */
fun createForText(resourceHandler: AppLanguageResourceHandler): String? =
error?.let(resourceHandler::getStringInLocale)

companion object {
/** Returns the [TextParsingUiError] corresponding to the input. */
fun createForText(text: String): TextParsingUiError =
if (text.isEmpty()) EMPTY_INPUT else VALID
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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.customview.interaction.TextInputInteractionView
import org.oppia.android.app.model.InputInteractionViewTestActivityParams
import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType.ALGEBRAIC_EXPRESSION
import org.oppia.android.app.model.InputInteractionViewTestActivityParams.MathInteractionType.MATH_EQUATION
Expand All @@ -26,7 +25,6 @@ import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractio
import org.oppia.android.app.player.state.itemviewmodel.NumericInputViewModel
import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel
import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.InteractionItemFactory
import org.oppia.android.app.player.state.itemviewmodel.TextInputViewModel
import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
import org.oppia.android.databinding.ActivityInputInteractionViewTestBinding
import org.oppia.android.util.extensions.getProtoExtra
Expand All @@ -36,7 +34,7 @@ import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractio

/**
* This is a dummy activity to test input interaction views.
* It contains [NumericInputInteractionView],and [TextInputInteractionView].
* It contains [NumericInputInteractionView]
*/
class InputInteractionViewTestActivity :
InjectableAutoLocalizedAppCompatActivity(),
Expand All @@ -48,16 +46,11 @@ class InputInteractionViewTestActivity :
@Inject
lateinit var numericInputViewModelFactory: NumericInputViewModel.FactoryImpl

@Inject
lateinit var textInputViewModelFactory: TextInputViewModel.FactoryImpl

@Inject
lateinit var mathExpViewModelFactoryFactory: MathExpViewModelFactoryFactoryImpl

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

val textInputViewModel by lazy { textInputViewModelFactory.create<TextInputViewModel>() }

lateinit var mathExpressionViewModel: MathExpressionInteractionsViewModel
lateinit var writtenTranslationContext: WrittenTranslationContext

Expand Down Expand Up @@ -103,7 +96,6 @@ class InputInteractionViewTestActivity :
}

binding.numericInputViewModel = numericInputViewModel
binding.textInputViewModel = textInputViewModel
binding.mathExpressionInteractionsViewModel = mathExpressionViewModel
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package org.oppia.android.app.testing

import android.os.Bundle
import android.view.View
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.customview.interaction.TextInputInteractionView
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
import org.oppia.android.app.player.state.itemviewmodel.TextInputViewModel
import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
import org.oppia.android.databinding.ActivityTextInputInteractionViewTestBinding
import javax.inject.Inject

/**
* This is a dummy activity to test input interaction views.
* It contains [TextInputInteractionView]
*/
class TextInputInteractionViewTestActivity :
InjectableAutoLocalizedAppCompatActivity(),
StateKeyboardButtonListener,
InteractionAnswerErrorOrAvailabilityCheckReceiver,
InteractionAnswerReceiver {

private lateinit var binding: ActivityTextInputInteractionViewTestBinding

@Inject
lateinit var textinputViewModelFactory: TextInputViewModel.FactoryImpl

/** Gives access to the [TextInputViewModel]. */
val textInputViewModel by lazy {
textinputViewModelFactory.create<TextInputViewModel>()
}

/** Gives access to the translation context. */
lateinit var writtenTranslationContext: WrittenTranslationContext

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(activityComponent as ActivityComponentImpl).inject(this)
binding = DataBindingUtil.setContentView(
this, R.layout.activity_text_input_interaction_view_test
)

writtenTranslationContext = WrittenTranslationContext.getDefaultInstance()
binding.textInputViewModel = textInputViewModel
}

/** Checks submit-time errors. */
fun getPendingAnswerErrorOnSubmitClick(v: View) {
textInputViewModel.checkPendingAnswerError(AnswerErrorCategory.SUBMIT_TIME)
}

override fun onPendingAnswerErrorOrAvailabilityCheck(
pendingAnswerError: String?,
inputAnswerAvailable: Boolean
) {
}

override fun onAnswerReadyForSubmission(answer: UserAnswer) {
}

override fun onEditorAction(actionCode: Int) {
}

private inline fun <reified T : StateItemViewModel>
StateItemViewModel.InteractionItemFactory.create(
interaction: Interaction = Interaction.getDefaultInstance()
): T {
return create(
entityId = "fake_entity_id",
hasConversationView = false,
interaction = interaction,
interactionAnswerReceiver = this@TextInputInteractionViewTestActivity,
answerErrorReceiver = this@TextInputInteractionViewTestActivity,
hasPreviousButton = false,
isSplitView = false,
writtenTranslationContext,
timeToStartNoticeAnimationMs = null
) as T
}
}
35 changes: 8 additions & 27 deletions app/src/main/res/layout/activity_input_interaction_view_test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
name="numericInputViewModel"
type="org.oppia.android.app.player.state.itemviewmodel.NumericInputViewModel" />

<variable
name="textInputViewModel"
type="org.oppia.android.app.player.state.itemviewmodel.TextInputViewModel" />

<variable
name="mathExpressionInteractionsViewModel"
type="org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractionsViewModel" />
Expand All @@ -23,6 +19,7 @@
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
Expand All @@ -36,18 +33,18 @@
android:id="@+id/test_number_input_interaction_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_margin="8dp"
android:background="@drawable/edit_text_background"
android:focusable="true"
android:hint="@string/test_number_input_interaction_hint"
android:textColor="@color/component_color_shared_primary_text_color"
android:textColorHint="@color/component_color_shared_edit_text_hint_color"
android:longClickable="false"
android:maxLength="200"
android:minHeight="48dp"
android:padding="8dp"
android:singleLine="true"
android:text="@={numericInputViewModel.answerText}"
android:textColor="@color/component_color_shared_primary_text_color"
android:textColorHint="@color/component_color_shared_edit_text_hint_color"
app:textChangedListener="@{numericInputViewModel.answerTextWatcher}" />

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

<org.oppia.android.app.customview.interaction.TextInputInteractionView
android:id="@+id/test_text_input_interaction_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:layout_margin="8dp"
android:background="@drawable/edit_text_background"
android:focusable="true"
android:hint="@string/test_text_input_interaction_hint"
android:inputType="text"
android:longClickable="false"
android:maxLength="200"
android:padding="8dp"
android:singleLine="true"
android:text="@={textInputViewModel.answerText}" />

<org.oppia.android.app.customview.interaction.MathExpressionInteractionsView
android:id="@+id/test_math_expression_input_interaction_view"
style="@style/InputInteractionEditText"
android:minHeight="48dp"
app:placeholder="@{mathExpressionInteractionsViewModel.hintText}"
android:inputType="text"
android:minHeight="48dp"
android:text="@={mathExpressionInteractionsViewModel.answerText}"
app:textChangedListener="@{mathExpressionInteractionsViewModel.answerTextWatcher}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
app:layout_constraintTop_toTopOf="parent"
app:placeholder="@{mathExpressionInteractionsViewModel.hintText}"
app:textChangedListener="@{mathExpressionInteractionsViewModel.answerTextWatcher}" />

<Button
android:id="@+id/submit_button"
Expand Down
Loading

0 comments on commit 59a41ab

Please sign in to comment.