diff --git a/app/build.gradle b/app/build.gradle index 2d123c82098..ddb27864f4e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -75,7 +75,7 @@ dependencies { 'androidx.recyclerview:recyclerview:1.0.0', 'com.chaos.view:pinview:1.4.3', 'com.github.bumptech.glide:glide:4.9.0', - 'com.google.android.material:material:1.0.0-alpha1', + 'com.google.android.material:material:1.2.0-alpha02', 'com.google.dagger:dagger:2.24', 'com.google.guava:guava:28.1-android', 'de.hdodenhof:circleimageview:3.0.1', diff --git a/app/src/main/java/org/oppia/app/customview/interaction/FractionInputInteractionView.kt b/app/src/main/java/org/oppia/app/customview/interaction/FractionInputInteractionView.kt index b1e6c2ca63b..45368654b00 100644 --- a/app/src/main/java/org/oppia/app/customview/interaction/FractionInputInteractionView.kt +++ b/app/src/main/java/org/oppia/app/customview/interaction/FractionInputInteractionView.kt @@ -26,12 +26,12 @@ class FractionInputInteractionView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyle: Int = android.R.attr.editTextStyle ) : EditText(context, attrs, defStyle), View.OnFocusChangeListener { - private val hintText: String + private val hintText: CharSequence private val stateKeyboardButtonListener: StateKeyboardButtonListener init { onFocusChangeListener = this - hintText = (hint ?: "").toString() + hintText = (hint ?: "") stateKeyboardButtonListener = context as StateKeyboardButtonListener } diff --git a/app/src/main/java/org/oppia/app/customview/interaction/NumericInputInteractionView.kt b/app/src/main/java/org/oppia/app/customview/interaction/NumericInputInteractionView.kt index 6e0b256942b..b04c4f7b4be 100644 --- a/app/src/main/java/org/oppia/app/customview/interaction/NumericInputInteractionView.kt +++ b/app/src/main/java/org/oppia/app/customview/interaction/NumericInputInteractionView.kt @@ -27,16 +27,20 @@ class NumericInputInteractionView @JvmOverloads constructor( defStyle: Int = android.R.attr.editTextStyle ) : EditText(context, attrs, defStyle), View.OnFocusChangeListener { private val stateKeyboardButtonListener: StateKeyboardButtonListener + private val hintText: CharSequence init { onFocusChangeListener = this + hintText = (hint ?: "") stateKeyboardButtonListener = context as StateKeyboardButtonListener } override fun onFocusChange(v: View, hasFocus: Boolean) = if (hasFocus) { + hint = "" typeface = Typeface.DEFAULT showSoftKeyboard(v, context) } else { + hint = hintText if (text.isEmpty()) setTypeface(typeface, Typeface.ITALIC) hideSoftKeyboard(v, context) } diff --git a/app/src/main/java/org/oppia/app/customview/interaction/TextInputInteractionView.kt b/app/src/main/java/org/oppia/app/customview/interaction/TextInputInteractionView.kt index fb263d1c3dc..092529b3a7e 100644 --- a/app/src/main/java/org/oppia/app/customview/interaction/TextInputInteractionView.kt +++ b/app/src/main/java/org/oppia/app/customview/interaction/TextInputInteractionView.kt @@ -23,12 +23,12 @@ class TextInputInteractionView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyle: Int = android.R.attr.editTextStyle ) : EditText(context, attrs, defStyle), View.OnFocusChangeListener { - private val hintText: String + private val hintText: CharSequence private val stateKeyboardButtonListener: StateKeyboardButtonListener init { onFocusChangeListener = this - hintText = (hint ?: "").toString() + hintText = (hint ?: "") stateKeyboardButtonListener = context as StateKeyboardButtonListener } diff --git a/app/src/main/java/org/oppia/app/databinding/EditTextBindingAdapters.kt b/app/src/main/java/org/oppia/app/databinding/EditTextBindingAdapters.kt new file mode 100644 index 00000000000..bf66211b71e --- /dev/null +++ b/app/src/main/java/org/oppia/app/databinding/EditTextBindingAdapters.kt @@ -0,0 +1,11 @@ +package org.oppia.app.databinding + +import android.text.TextWatcher +import android.widget.EditText +import androidx.databinding.BindingAdapter + +/** Binding adapter for setting a [TextWatcher] as a change listener for an [EditText]. */ +@BindingAdapter("app:textChangedListener") +fun bindTextWatcher(editText: EditText, textWatcher: TextWatcher) { + editText.addTextChangedListener(textWatcher) +} diff --git a/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt b/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt index 136012b4ce3..2dcb69bd80b 100644 --- a/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt +++ b/app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt @@ -25,7 +25,7 @@ import org.oppia.app.topic.info.TopicInfoFragment import org.oppia.app.topic.play.TopicPlayFragment import org.oppia.app.topic.questionplayer.QuestionPlayerFragment import org.oppia.app.topic.review.TopicReviewFragment -import org.oppia.app.topic.train.TopicTrainFragment +import org.oppia.app.topic.practice.TopicPracticeFragment import org.oppia.app.view.ViewComponent import javax.inject.Provider @@ -63,6 +63,6 @@ interface FragmentComponent { fun inject(topicInfoFragment: TopicInfoFragment) fun inject(topicPlayFragment: TopicPlayFragment) fun inject(topicReviewFragment: TopicReviewFragment) - fun inject(topicTrainFragment: TopicTrainFragment) + fun inject(topicPracticeFragment: TopicPracticeFragment) fun inject(updatesTabFragment: UpdatesTabFragment) } diff --git a/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt b/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt index dfa93f157a1..069400687c7 100644 --- a/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt +++ b/app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt @@ -1,5 +1,9 @@ package org.oppia.app.parser +import android.content.Context +import androidx.annotation.StringRes +import org.oppia.app.R +import org.oppia.app.customview.interaction.FractionInputInteractionView import org.oppia.app.model.Fraction import org.oppia.domain.util.normalizeWhitespace @@ -8,14 +12,59 @@ class StringToFractionParser { private val wholeNumberOnlyRegex = """^-? ?(\d+)$""".toRegex() private val fractionOnlyRegex = """^-? ?(\d+) ?/ ?(\d+)$""".toRegex() private val mixedNumberRegex = """^-? ?(\d+) (\d+) ?/ ?(\d+)$""".toRegex() + private val invalidCharsRegex = """^[\d\s/-]+$""".toRegex() + private val invalidCharsLengthRegex = "\\d{8,}".toRegex() - fun getFractionFromString(text: String): Fraction { + /** + * Returns a [FractionParsingError] for the specified text input if it's an invalid fraction, or + * [FractionParsingError.VALID] if no issues are found. Note that a valid fraction returned by this method is guaranteed + * to be parsed correctly by [parseRegularFraction]. + * + * This method should only be used when a user tries submitting an answer. Real-time error detection should be done + * using [getRealTimeAnswerError], instead. + */ + fun getSubmitTimeError(text: String): FractionParsingError { + if (invalidCharsLengthRegex.find(text) != null) + return FractionParsingError.NUMBER_TOO_LONG + val fraction = parseFraction(text) + return when { + fraction == null -> FractionParsingError.INVALID_FORMAT + fraction.denominator == 0 -> FractionParsingError.DIVISION_BY_ZERO + else -> FractionParsingError.VALID + } + } + + /** + * Returns a [FractionParsingError] for obvious incorrect fraction formatting issues for the specified raw text, or + * [FractionParsingError.VALID] if not such issues are found. + * + * Note that this method returning a valid result does not guarantee the text is a valid fraction-- + * [getSubmitTimeError] should be used for that, instead. This method is meant to be used as a quick sanity check for + * general validity, not for definite correctness. + */ + fun getRealTimeAnswerError(text: String): FractionParsingError { + val normalized = text.normalizeWhitespace() + return when { + !normalized.matches(invalidCharsRegex) -> FractionParsingError.INVALID_CHARS + normalized.startsWith("/") -> FractionParsingError.INVALID_FORMAT + normalized.count { it == '/' } > 1 -> FractionParsingError.INVALID_FORMAT + normalized.lastIndexOf('-') > 0 -> FractionParsingError.INVALID_FORMAT + else -> FractionParsingError.VALID + } + } + + /** Returns a [Fraction] parse from the specified raw text string. */ + fun parseFraction(text: String): Fraction? { // Normalize whitespace to ensure that answer follows a simpler subset of possible patterns. val inputText: String = text.normalizeWhitespace() return parseMixedNumber(inputText) - ?: parseFraction(inputText) + ?: parseRegularFraction(inputText) ?: parseWholeNumber(inputText) - ?: throw IllegalArgumentException("Incorrectly formatted fraction: $text") + } + + /** Returns a [Fraction] parse from the specified raw text string. */ + fun parseFractionFromString(text: String): Fraction { + return parseFraction(text) ?: throw IllegalArgumentException("Incorrectly formatted fraction: $text") } private fun parseMixedNumber(inputText: String): Fraction? { @@ -29,7 +78,7 @@ class StringToFractionParser { .build() } - private fun parseFraction(inputText: String): Fraction? { + private fun parseRegularFraction(inputText: String): Fraction? { val fractionOnlyMatch = fractionOnlyRegex.matchEntire(inputText) ?: return null val (_, numeratorText, denominatorText) = fractionOnlyMatch.groupValues // Fraction-only numbers imply no whole number. @@ -53,4 +102,18 @@ class StringToFractionParser { } private fun isInputNegative(inputText: String): Boolean = inputText.startsWith("-") + + /** Enum to store the errors of [FractionInputInteractionView]. */ + enum class FractionParsingError(@StringRes private var error: Int?) { + VALID(error = null), + INVALID_CHARS(error = R.string.fraction_error_invalid_chars), + INVALID_FORMAT(error = R.string.fraction_error_invalid_format), + DIVISION_BY_ZERO(error = R.string.fraction_error_divide_by_zero), + NUMBER_TOO_LONG(error = R.string.fraction_error_larger_than_seven_digits); + + /** Returns the string corresponding to this error's string resources, or null if there is none. */ + fun getErrorMessageFromStringRes(context: Context): String? { + return error?.let(context::getString) + } + } } diff --git a/app/src/main/java/org/oppia/app/player/audio/CellularAudioDialogFragment.kt b/app/src/main/java/org/oppia/app/player/audio/CellularAudioDialogFragment.kt index 7f50962ff21..d0aca39f75c 100755 --- a/app/src/main/java/org/oppia/app/player/audio/CellularAudioDialogFragment.kt +++ b/app/src/main/java/org/oppia/app/player/audio/CellularAudioDialogFragment.kt @@ -3,6 +3,7 @@ package org.oppia.app.player.audio import android.app.Dialog import android.content.Context import android.os.Bundle +import android.view.View import android.widget.CheckBox import androidx.appcompat.app.AlertDialog import androidx.appcompat.view.ContextThemeWrapper @@ -25,7 +26,7 @@ class CellularAudioDialogFragment : DialogFragment() { } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val view = activity!!.layoutInflater.inflate(R.layout.cellular_data_dialog, null) + val view = View.inflate(context, R.layout.cellular_data_dialog, /* root= */ null) val checkBox = view.findViewById(R.id.cellular_data_dialog_checkbox) val cellularDataInterface: CellularDataInterface = parentFragment as AudioFragment diff --git a/app/src/main/java/org/oppia/app/player/state/StateFragment.kt b/app/src/main/java/org/oppia/app/player/state/StateFragment.kt index 210a8cd9bed..8475e743436 100755 --- a/app/src/main/java/org/oppia/app/player/state/StateFragment.kt +++ b/app/src/main/java/org/oppia/app/player/state/StateFragment.kt @@ -7,11 +7,14 @@ import android.view.View import android.view.ViewGroup import org.oppia.app.fragment.InjectableFragment import org.oppia.app.model.UserAnswer +import org.oppia.app.player.state.answerhandling.InteractionAnswerErrorReceiver +import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver import javax.inject.Inject /** Fragment that represents the current state of an exploration. */ -class StateFragment : InjectableFragment(), InteractionAnswerReceiver { +class StateFragment : InjectableFragment(), InteractionAnswerReceiver, InteractionAnswerHandler, + InteractionAnswerErrorReceiver { companion object { /** * Creates a new instance of a StateFragment. @@ -46,6 +49,10 @@ class StateFragment : InjectableFragment(), InteractionAnswerReceiver { fun handleKeyboardAction() = stateFragmentPresenter.handleKeyboardAction() + override fun onPendingAnswerError(pendingAnswerError: String?) { + stateFragmentPresenter.updateSubmitButton(pendingAnswerError) + } + fun setAudioBarVisibility(visibility: Boolean) = stateFragmentPresenter.setAudioBarVisibility(visibility) fun scrollToTop() = stateFragmentPresenter.scrollToTop() diff --git a/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt index 20aa877f59f..5af7807c6b0 100755 --- a/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt @@ -38,10 +38,11 @@ import org.oppia.app.model.EphemeralState import org.oppia.app.model.Interaction import org.oppia.app.model.State import org.oppia.app.model.SubtitledHtml -import org.oppia.app.player.audio.AudioButtonListener import org.oppia.app.model.UserAnswer +import org.oppia.app.player.audio.AudioButtonListener import org.oppia.app.player.audio.AudioFragment import org.oppia.app.player.audio.AudioUiManager +import org.oppia.app.player.state.answerhandling.InteractionAnswerErrorReceiver import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.app.player.state.itemviewmodel.ContentViewModel import org.oppia.app.player.state.itemviewmodel.ContinueInteractionViewModel @@ -100,7 +101,7 @@ class StateFragmentPresenter @Inject constructor( /** * A list of view models corresponding to past view models that are hidden by default. These are intentionally not * retained upon configuration changes since the user can just re-expand the list. Note that the first element of this - * list (when initialized), will always be the previous answers header to help locate the items in the recycler view + * list (when initialized), will always be the previous answer's header to help locate the items in the recycler view * (when present). */ private val previousAnswerViewModels: MutableList = mutableListOf() @@ -109,6 +110,7 @@ class StateFragmentPresenter @Inject constructor( * configuration changes since the user can just re-expand the list. */ private var hasPreviousResponsesExpanded: Boolean = false + private lateinit var stateNavigationButtonViewModel: StateNavigationButtonViewModel fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { explorationId = fragment.arguments!!.getString(STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY)!! @@ -399,7 +401,7 @@ class StateFragmentPresenter @Inject constructor( Handler().postDelayed({ binding.congratulationTextview.clearAnimation() binding.congratulationTextview.visibility = View.INVISIBLE - },2000) + }, 2000) } /** Helper for subscribeToAnswerOutcome. */ @@ -440,7 +442,8 @@ class StateFragmentPresenter @Inject constructor( fun handleKeyboardAction() { hideKeyboard() - handleSubmitAnswer(viewModel.getPendingAnswer()) + if (stateNavigationButtonViewModel.isInteractionButtonActive.get()!!) + handleSubmitAnswer(viewModel.getPendingAnswer()) } override fun onContinueButtonClicked() { @@ -473,7 +476,7 @@ class StateFragmentPresenter @Inject constructor( ) { val interactionViewModelFactory = interactionViewModelFactoryMap.getValue(interaction.id) pendingItemList += interactionViewModelFactory( - explorationId, interaction, fragment as InteractionAnswerReceiver + explorationId, interaction, fragment as InteractionAnswerReceiver, fragment as InteractionAnswerErrorReceiver ) } @@ -559,7 +562,7 @@ class StateFragmentPresenter @Inject constructor( hasGeneralContinueButton: Boolean, stateIsTerminal: Boolean ) { - val stateNavigationButtonViewModel = + stateNavigationButtonViewModel = StateNavigationButtonViewModel(context, this as StateNavigationButtonListener) stateNavigationButtonViewModel.updatePreviousButton(isEnabled = hasPreviousState) @@ -611,4 +614,13 @@ class StateFragmentPresenter @Inject constructor( } private fun isAudioShowing(): Boolean = viewModel.isAudioBarVisible.get()!! + + /** Updates submit button UI as active if pendingAnswerError null else inactive. */ + fun updateSubmitButton(pendingAnswerError: String?) { + if (pendingAnswerError != null) { + stateNavigationButtonViewModel.isInteractionButtonActive.set(false) + } else { + stateNavigationButtonViewModel.isInteractionButtonActive.set(true) + } + } } diff --git a/app/src/main/java/org/oppia/app/player/state/StateViewModel.kt b/app/src/main/java/org/oppia/app/player/state/StateViewModel.kt index 64de089ee7a..c4b56a8a5aa 100644 --- a/app/src/main/java/org/oppia/app/player/state/StateViewModel.kt +++ b/app/src/main/java/org/oppia/app/player/state/StateViewModel.kt @@ -5,6 +5,7 @@ import androidx.databinding.ObservableList import androidx.lifecycle.ViewModel import org.oppia.app.fragment.FragmentScope import org.oppia.app.model.UserAnswer +import org.oppia.app.player.state.answerhandling.AnswerErrorCategory import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.app.player.state.itemviewmodel.StateItemViewModel import org.oppia.app.viewmodel.ObservableArrayList @@ -34,7 +35,10 @@ class StateViewModel @Inject constructor() : ObservableViewModel() { // TODO(#164): Add a hasPendingAnswer() that binds to the enabled state of the Submit button. fun getPendingAnswer(): UserAnswer { - return getPendingAnswerHandler(itemList)?.getPendingAnswer() ?: UserAnswer.getDefaultInstance() + return if (getPendingAnswerHandler(itemList)?.checkPendingAnswerError(AnswerErrorCategory.SUBMIT_TIME) != null) { + UserAnswer.getDefaultInstance() + } else + getPendingAnswerHandler(itemList)?.getPendingAnswer() ?: UserAnswer.getDefaultInstance() } private fun getPendingAnswerHandler(itemList: List): InteractionAnswerHandler? { diff --git a/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerErrorReceiver.kt b/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerErrorReceiver.kt new file mode 100644 index 00000000000..b7fa87be9b8 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerErrorReceiver.kt @@ -0,0 +1,15 @@ +package org.oppia.app.player.state.answerhandling + +/** + * A handler for interaction answer's error receiving to update submit button. + * Handlers can either require an additional user action before the submit button UI can be updated. + */ +interface InteractionAnswerErrorReceiver { + + /** + * Called when an error was detected upon answer submission. Implementations are recommended to prevent further answer + * submission until the pending answer itself changes. The interaction is responsible for displaying the error provided + * here, not the implementation. + */ + fun onPendingAnswerError(pendingAnswerError: String?) {} +} diff --git a/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerHandler.kt b/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerHandler.kt index 04272dbe190..484ded82e1b 100644 --- a/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerHandler.kt +++ b/app/src/main/java/org/oppia/app/player/state/answerhandling/InteractionAnswerHandler.kt @@ -14,8 +14,15 @@ interface InteractionAnswerHandler { */ fun isExplicitAnswerSubmissionRequired(): Boolean = true + /** Return the current answer's error messages if not valid else return null. */ + fun checkPendingAnswerError(category: AnswerErrorCategory): String? { + return null + } + /** Return the current answer that is ready for handling. */ - fun getPendingAnswer(): UserAnswer + fun getPendingAnswer(): UserAnswer? { + return null + } } /** @@ -25,3 +32,11 @@ 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/app/player/state/itemviewmodel/FractionInteractionViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/FractionInteractionViewModel.kt index bcc78f07786..febc7f0ee7d 100644 --- a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/FractionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/FractionInteractionViewModel.kt @@ -1,32 +1,88 @@ package org.oppia.app.player.state.itemviewmodel import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import androidx.databinding.Bindable +import androidx.databinding.Observable +import androidx.databinding.ObservableField import org.oppia.app.R import org.oppia.app.model.Interaction import org.oppia.app.model.InteractionObject import org.oppia.app.model.UserAnswer import org.oppia.app.parser.StringToFractionParser +import org.oppia.app.player.state.answerhandling.AnswerErrorCategory +import org.oppia.app.player.state.answerhandling.InteractionAnswerErrorReceiver import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler /** [ViewModel] for the fraction input interaction. */ class FractionInteractionViewModel( - interaction: Interaction, private val context: Context + interaction: Interaction, + private val context: Context, + private val interactionAnswerErrorReceiver: InteractionAnswerErrorReceiver ) : StateItemViewModel(ViewType.FRACTION_INPUT_INTERACTION), InteractionAnswerHandler { + private var pendingAnswerError: String? = null var answerText: CharSequence = "" + var errorMessage = ObservableField("") + val hintText: CharSequence = deriveHintText(interaction) + private val stringToFractionParser: StringToFractionParser = StringToFractionParser() + + init { + val callback: Observable.OnPropertyChangedCallback = object : Observable.OnPropertyChangedCallback() { + override fun onPropertyChanged(sender: Observable, propertyId: Int) { + interactionAnswerErrorReceiver.onPendingAnswerError(pendingAnswerError) + } + } + errorMessage.addOnPropertyChangedCallback(callback) + } override fun getPendingAnswer(): UserAnswer { val userAnswerBuilder = UserAnswer.newBuilder() if (answerText.isNotEmpty()) { val answerTextString = answerText.toString() userAnswerBuilder.answer = InteractionObject.newBuilder() - .setFraction(StringToFractionParser().getFractionFromString(answerTextString)) + .setFraction(stringToFractionParser.parseFractionFromString(answerTextString)) .build() userAnswerBuilder.plainAnswer = answerTextString } return userAnswerBuilder.build() } + /** 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? { + if (answerText.isNotEmpty()) { + when (category) { + AnswerErrorCategory.REAL_TIME -> pendingAnswerError = + stringToFractionParser.getRealTimeAnswerError(answerText.toString()).getErrorMessageFromStringRes( + context + ) + AnswerErrorCategory.SUBMIT_TIME -> pendingAnswerError = + stringToFractionParser.getSubmitTimeError(answerText.toString()).getErrorMessageFromStringRes( + context + ) + } + errorMessage.set(pendingAnswerError) + } + return pendingAnswerError + } + + @Bindable + fun getAnswerTextWatcher(): TextWatcher { + return object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(answer: CharSequence, start: Int, before: Int, count: Int) { + answerText = answer.toString().trim() + checkPendingAnswerError(AnswerErrorCategory.REAL_TIME) + } + + override fun afterTextChanged(s: Editable) { + } + } + } + private fun deriveHintText(interaction: Interaction): CharSequence { val customPlaceholder = interaction.customizationArgsMap["customPlaceholder"]?.normalizedString ?: "" val allowNonzeroIntegerPart = interaction.customizationArgsMap["allowNonzeroIntegerPart"]?.boolValue ?: true diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelFactory.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelFactory.kt index 9aad4791032..14a9c8f4f0d 100644 --- a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelFactory.kt +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelFactory.kt @@ -1,7 +1,7 @@ package org.oppia.app.player.state.itemviewmodel import org.oppia.app.model.Interaction -import org.oppia.app.model.InteractionObject +import org.oppia.app.player.state.answerhandling.InteractionAnswerErrorReceiver import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver /** @@ -9,5 +9,5 @@ import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver * pushes answers, the [Interaction] object corresponding to the interaction view, and the exploration ID. */ typealias InteractionViewModelFactory = ( - explorationId: String, interaction: Interaction, interactionAnswerReceiver: InteractionAnswerReceiver + explorationId: String, interaction: Interaction, interactionAnswerReceiver: InteractionAnswerReceiver, interactionAnswerHandler: InteractionAnswerErrorReceiver ) -> StateItemViewModel diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelModule.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelModule.kt index 45a261641fd..49881b93a86 100644 --- a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelModule.kt +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/InteractionViewModelModule.kt @@ -18,7 +18,7 @@ class InteractionViewModelModule { @IntoMap @StringKey("Continue") fun provideContinueInteractionViewModelFactory(): InteractionViewModelFactory { - return { _, _, interactionAnswerReceiver -> + return { _, _, interactionAnswerReceiver, _ -> ContinueInteractionViewModel(interactionAnswerReceiver) } } @@ -26,7 +26,7 @@ class InteractionViewModelModule { @Provides @IntoMap @StringKey("MultipleChoiceInput") - fun provideMultipleChoiceInputViewModelFactory(): InteractionViewModelFactory { + fun provideMultipleChoiceInputViewModelFactory(): InteractionViewModelFactory{ return ::SelectionInteractionViewModel } @@ -41,20 +41,20 @@ class InteractionViewModelModule { @IntoMap @StringKey("FractionInput") fun provideFractionInputViewModelFactory(context: Context): InteractionViewModelFactory { - return { _, interaction, _ -> FractionInteractionViewModel(interaction, context) } + return { _, interaction, _, interactionAnswerHandler -> FractionInteractionViewModel(interaction, context, interactionAnswerHandler) } } @Provides @IntoMap @StringKey("NumericInput") fun provideNumericInputViewModelFactory(): InteractionViewModelFactory { - return { _, _, _ -> NumericInputViewModel() } + return { _, _, _, _ -> NumericInputViewModel() } } @Provides @IntoMap @StringKey("TextInput") fun provideTextInputViewModelFactory(): InteractionViewModelFactory { - return { _, interaction, _ -> TextInputViewModel(interaction) } + return { _, interaction, _, _ -> TextInputViewModel(interaction) } } } diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt index d7e68285bfa..15d5748a547 100644 --- a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt @@ -6,15 +6,20 @@ import org.oppia.app.model.InteractionObject import org.oppia.app.model.StringList import org.oppia.app.model.UserAnswer import org.oppia.app.player.state.SelectionItemInputType +import org.oppia.app.player.state.answerhandling.InteractionAnswerErrorReceiver import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver import org.oppia.app.viewmodel.ObservableArrayList /** ViewModel for multiple or item-selection input choice list. */ class SelectionInteractionViewModel( - val explorationId: String, interaction: Interaction, private val interactionAnswerReceiver: InteractionAnswerReceiver -): StateItemViewModel(ViewType.SELECTION_INTERACTION), InteractionAnswerHandler { + val explorationId: String, + interaction: Interaction, + private val interactionAnswerReceiver: InteractionAnswerReceiver, + interactionAnswerErrorReceiver: InteractionAnswerErrorReceiver +) : StateItemViewModel(ViewType.SELECTION_INTERACTION), InteractionAnswerHandler { private val interactionId: String = interaction.id + private val choiceStrings: List by lazy { interaction.customizationArgsMap["choices"]?.setOfHtmlString?.htmlList ?: listOf() } diff --git a/app/src/main/java/org/oppia/app/testing/InputInteractionViewTestActivity.kt b/app/src/main/java/org/oppia/app/testing/InputInteractionViewTestActivity.kt index 3310db63d7c..ac367daba86 100644 --- a/app/src/main/java/org/oppia/app/testing/InputInteractionViewTestActivity.kt +++ b/app/src/main/java/org/oppia/app/testing/InputInteractionViewTestActivity.kt @@ -1,39 +1,63 @@ package org.oppia.app.testing import android.os.Bundle +import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil import org.oppia.app.R import org.oppia.app.customview.interaction.FractionInputInteractionView import org.oppia.app.customview.interaction.NumericInputInteractionView import org.oppia.app.customview.interaction.TextInputInteractionView -import org.oppia.app.databinding.ActivityNumericInputInteractionViewTestBinding +import org.oppia.app.databinding.ActivityInputInteractionViewTestBinding import org.oppia.app.model.Interaction +import org.oppia.app.player.state.answerhandling.AnswerErrorCategory +import org.oppia.app.player.state.answerhandling.InteractionAnswerErrorReceiver import org.oppia.app.player.state.itemviewmodel.FractionInteractionViewModel import org.oppia.app.player.state.itemviewmodel.NumericInputViewModel import org.oppia.app.player.state.itemviewmodel.TextInputViewModel +import org.oppia.app.player.state.listener.StateKeyboardButtonListener /** * This is a dummy activity to test input interaction views. - * It contains [NumericInputInteractionView], [TextInputInteractionView], [FractionInputInteractionView] and [NumberWithUnitsInputInteractionView]. + * It contains [NumericInputInteractionView], [TextInputInteractionView],and [FractionInputInteractionView]. */ -class InputInteractionViewTestActivity : AppCompatActivity() { +class InputInteractionViewTestActivity : AppCompatActivity(), StateKeyboardButtonListener, + InteractionAnswerErrorReceiver { + override fun onEditorAction(actionCode: Int) { + } + + private lateinit var binding: ActivityInputInteractionViewTestBinding val numericInputViewModel = NumericInputViewModel() val textInputViewModel = TextInputViewModel( interaction = Interaction.getDefaultInstance() ) - val fractionInteractionViewModel = FractionInteractionViewModel( - interaction = Interaction.getDefaultInstance(), - context = this@InputInteractionViewTestActivity.applicationContext - ) + lateinit var fractionInteractionViewModel: FractionInteractionViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = DataBindingUtil.setContentView( - this, R.layout.activity_numeric_input_interaction_view_test + binding = DataBindingUtil.setContentView( + this, R.layout.activity_input_interaction_view_test + ) + fractionInteractionViewModel = FractionInteractionViewModel( + interaction = Interaction.getDefaultInstance(), + context = this, + interactionAnswerErrorReceiver = this ) binding.numericInputViewModel = numericInputViewModel binding.textInputViewModel = textInputViewModel binding.fractionInteractionViewModel = fractionInteractionViewModel } + + fun getPendingAnswerErrorOnSubmitClick(v: View) { + fractionInteractionViewModel.checkPendingAnswerError(AnswerErrorCategory.SUBMIT_TIME) + } + + override fun onPendingAnswerError( + pendingAnswerError: String? + ) { + if (pendingAnswerError != null) + binding.submitButton.isEnabled = false + else + binding.submitButton.isEnabled = true + } } diff --git a/app/src/main/java/org/oppia/app/topic/TopicFragmentPresenter.kt b/app/src/main/java/org/oppia/app/topic/TopicFragmentPresenter.kt index e3c3ba1c5f1..8ac1c2d986d 100644 --- a/app/src/main/java/org/oppia/app/topic/TopicFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/topic/TopicFragmentPresenter.kt @@ -35,7 +35,7 @@ class TopicFragmentPresenter @Inject constructor( intArrayOf( R.drawable.ic_info_icon_24dp, R.drawable.ic_play_icon_24dp, - R.drawable.ic_train_icon_24dp, + R.drawable.ic_practice_icon_24dp, R.drawable.ic_review_icon_24dp ) @@ -66,7 +66,7 @@ class TopicFragmentPresenter @Inject constructor( tabLayout.setupWithViewPager(viewPager) tabLayout.getTabAt(0)!!.setText(fragment.getString(R.string.info)).setIcon(tabIcons[0]) tabLayout.getTabAt(1)!!.setText(fragment.getString(R.string.play)).setIcon(tabIcons[1]) - tabLayout.getTabAt(2)!!.setText(fragment.getString(R.string.train)).setIcon(tabIcons[2]) + tabLayout.getTabAt(2)!!.setText(fragment.getString(R.string.practice)).setIcon(tabIcons[2]) tabLayout.getTabAt(3)!!.setText(fragment.getString(R.string.review)).setIcon(tabIcons[3]) if (topicId.isNotEmpty() && storyId.isNotEmpty()) setCurrentTab(TopicTab.PLAY) diff --git a/app/src/main/java/org/oppia/app/topic/TopicTab.kt b/app/src/main/java/org/oppia/app/topic/TopicTab.kt index 0829ec085f1..d8db6ad392e 100644 --- a/app/src/main/java/org/oppia/app/topic/TopicTab.kt +++ b/app/src/main/java/org/oppia/app/topic/TopicTab.kt @@ -4,7 +4,7 @@ package org.oppia.app.topic enum class TopicTab(private var position: Int) { INFO(position = 0), PLAY(position = 1), - TRAIN(position = 2), + PRACTICE(position = 2), REVIEW(position = 3); companion object { diff --git a/app/src/main/java/org/oppia/app/topic/ViewPagerAdapter.kt b/app/src/main/java/org/oppia/app/topic/ViewPagerAdapter.kt index 21b7def5922..89a6119c3ae 100644 --- a/app/src/main/java/org/oppia/app/topic/ViewPagerAdapter.kt +++ b/app/src/main/java/org/oppia/app/topic/ViewPagerAdapter.kt @@ -7,7 +7,7 @@ import androidx.fragment.app.FragmentStatePagerAdapter import org.oppia.app.topic.info.TopicInfoFragment import org.oppia.app.topic.play.TopicPlayFragment import org.oppia.app.topic.review.TopicReviewFragment -import org.oppia.app.topic.train.TopicTrainFragment +import org.oppia.app.topic.practice.TopicPracticeFragment /** Adapter to bind fragments to [FragmentStatePagerAdapter] inside [TopicFragment]. */ class ViewPagerAdapter( @@ -33,10 +33,10 @@ class ViewPagerAdapter( topicPlayTab.arguments = args return topicPlayTab } - TopicTab.TRAIN -> { - val topicTrainTab = TopicTrainFragment() - topicTrainTab.arguments = args - return topicTrainTab + TopicTab.PRACTICE -> { + val topicPracticeTab = TopicPracticeFragment() + topicPracticeTab.arguments = args + return topicPracticeTab } TopicTab.REVIEW -> { val topicReviewTab = TopicReviewFragment() diff --git a/app/src/main/java/org/oppia/app/topic/train/SkillSelectionAdapter.kt b/app/src/main/java/org/oppia/app/topic/practice/SkillSelectionAdapter.kt similarity index 77% rename from app/src/main/java/org/oppia/app/topic/train/SkillSelectionAdapter.kt rename to app/src/main/java/org/oppia/app/topic/practice/SkillSelectionAdapter.kt index 9a53afa5c9b..d507da03aa0 100644 --- a/app/src/main/java/org/oppia/app/topic/train/SkillSelectionAdapter.kt +++ b/app/src/main/java/org/oppia/app/topic/practice/SkillSelectionAdapter.kt @@ -1,17 +1,17 @@ -package org.oppia.app.topic.train +package org.oppia.app.topic.practice import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.databinding.library.baseAdapters.BR import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.topic_train_skill_view.view.* +import kotlinx.android.synthetic.main.topic_practice_skill_view.view.* import org.oppia.app.R -import org.oppia.app.databinding.TopicTrainSkillViewBinding +import org.oppia.app.databinding.TopicPracticeSkillViewBinding import org.oppia.app.model.SkillSummary // TODO(#216): Make use of generic data-binding-enabled RecyclerView adapter. -/** Adapter to bind skills to [RecyclerView] inside [TopicTrainFragment]. */ +/** Adapter to bind skills to [RecyclerView] inside [TopicPracticeFragment]. */ class SkillSelectionAdapter(private val skillSelector: SkillSelector) : RecyclerView.Adapter() { @@ -19,9 +19,9 @@ class SkillSelectionAdapter(private val skillSelector: SkillSelector) : private var selectedSkillIdList: ArrayList = ArrayList() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SkillViewHolder { - val skillListItemBinding = DataBindingUtil.inflate( + val skillListItemBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.context), - R.layout.topic_train_skill_view, parent, + R.layout.topic_practice_skill_view, parent, /* attachToRoot= */ false ) return SkillViewHolder(skillListItemBinding) @@ -44,7 +44,7 @@ class SkillSelectionAdapter(private val skillSelector: SkillSelector) : selectedSkillIdList = skillIdList } - inner class SkillViewHolder(val binding: TopicTrainSkillViewBinding) : RecyclerView.ViewHolder(binding.root) { + inner class SkillViewHolder(val binding: TopicPracticeSkillViewBinding) : RecyclerView.ViewHolder(binding.root) { internal fun bind(skill: SkillSummary, position: Int) { binding.setVariable(BR.isChecked, selectedSkillIdList.contains(skill.skillId)) binding.setVariable(BR.skill, skill) diff --git a/app/src/main/java/org/oppia/app/topic/practice/SkillSelector.kt b/app/src/main/java/org/oppia/app/topic/practice/SkillSelector.kt new file mode 100644 index 00000000000..c38de9eacae --- /dev/null +++ b/app/src/main/java/org/oppia/app/topic/practice/SkillSelector.kt @@ -0,0 +1,10 @@ +package org.oppia.app.topic.practice + +/** Interface to update the selectedSkillList in [TopicPracticeFragmentPresenter]. */ +interface SkillSelector { + /** This skill will get added to selectedSkillList in [TopicPracticeFragmentPresenter]. */ + fun skillSelected(skillId: String) + + /** This skill will get removed from selectedSkillList in [TopicPracticeFragmentPresenter]. */ + fun skillUnselected(skillId: String) +} diff --git a/app/src/main/java/org/oppia/app/topic/train/TopicTrainFragment.kt b/app/src/main/java/org/oppia/app/topic/practice/TopicPracticeFragment.kt similarity index 64% rename from app/src/main/java/org/oppia/app/topic/train/TopicTrainFragment.kt rename to app/src/main/java/org/oppia/app/topic/practice/TopicPracticeFragment.kt index a3d196888c6..5542144ed28 100644 --- a/app/src/main/java/org/oppia/app/topic/train/TopicTrainFragment.kt +++ b/app/src/main/java/org/oppia/app/topic/practice/TopicPracticeFragment.kt @@ -1,4 +1,4 @@ -package org.oppia.app.topic.train +package org.oppia.app.topic.practice import android.content.Context import android.os.Bundle @@ -10,9 +10,9 @@ import javax.inject.Inject private const val KEY_SKILL_ID_LIST = "SKILL_ID_LIST" -/** Fragment that displays skills for topic train mode. */ -class TopicTrainFragment : InjectableFragment() { - @Inject lateinit var topicTrainFragmentPresenter: TopicTrainFragmentPresenter +/** Fragment that displays skills for topic practice mode. */ +class TopicPracticeFragment : InjectableFragment() { + @Inject lateinit var topicPracticeFragmentPresenter: TopicPracticeFragmentPresenter override fun onAttach(context: Context) { super.onAttach(context) @@ -24,11 +24,11 @@ class TopicTrainFragment : InjectableFragment() { if (savedInstanceState != null) { selectedIdList = savedInstanceState.getStringArrayList(KEY_SKILL_ID_LIST) } - return topicTrainFragmentPresenter.handleCreateView(inflater, container, selectedIdList) + return topicPracticeFragmentPresenter.handleCreateView(inflater, container, selectedIdList) } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putStringArrayList(KEY_SKILL_ID_LIST, topicTrainFragmentPresenter.selectedSkillIdList) + outState.putStringArrayList(KEY_SKILL_ID_LIST, topicPracticeFragmentPresenter.selectedSkillIdList) } } diff --git a/app/src/main/java/org/oppia/app/topic/train/TopicTrainFragmentPresenter.kt b/app/src/main/java/org/oppia/app/topic/practice/TopicPracticeFragmentPresenter.kt similarity index 75% rename from app/src/main/java/org/oppia/app/topic/train/TopicTrainFragmentPresenter.kt rename to app/src/main/java/org/oppia/app/topic/practice/TopicPracticeFragmentPresenter.kt index a3fb42f477d..9ffad34adeb 100644 --- a/app/src/main/java/org/oppia/app/topic/train/TopicTrainFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/topic/practice/TopicPracticeFragmentPresenter.kt @@ -1,4 +1,4 @@ -package org.oppia.app.topic.train +package org.oppia.app.topic.practice import android.view.LayoutInflater import android.view.View @@ -8,7 +8,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.lifecycle.Transformations -import org.oppia.app.databinding.TopicTrainFragmentBinding +import org.oppia.app.databinding.TopicPracticeFragmentBinding import org.oppia.app.fragment.FragmentScope import org.oppia.app.model.Topic import org.oppia.app.topic.RouteToQuestionPlayerListener @@ -19,14 +19,14 @@ import org.oppia.util.data.AsyncResult import org.oppia.util.logging.Logger import javax.inject.Inject -/** The presenter for [TopicTrainFragment]. */ +/** The presenter for [TopicPracticeFragment]. */ @FragmentScope -class TopicTrainFragmentPresenter @Inject constructor( +class TopicPracticeFragmentPresenter @Inject constructor( activity: AppCompatActivity, private val fragment: Fragment, private val logger: Logger, private val topicController: TopicController, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider ) : SkillSelector { lateinit var selectedSkillIdList: ArrayList private lateinit var topicId: String @@ -35,10 +35,10 @@ class TopicTrainFragmentPresenter @Inject constructor( fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?, skillList: ArrayList): View? { topicId = checkNotNull(fragment.arguments?.getString(TOPIC_ID_ARGUMENT_KEY)) { - "Expected topic ID to be included in arguments for TopicTrainFragment." + "Expected topic ID to be included in arguments for TopicPracticeFragment." } selectedSkillIdList = skillList - val binding = TopicTrainFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) + val binding = TopicPracticeFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false) skillSelectionAdapter = SkillSelectionAdapter(this) binding.skillRecyclerView.isNestedScrollingEnabled = false @@ -46,7 +46,7 @@ class TopicTrainFragmentPresenter @Inject constructor( adapter = skillSelectionAdapter } binding.let { - it.viewModel = getTopicTrainViewModel() + it.viewModel = getTopicPracticeViewModel() it.lifecycleOwner = fragment } subscribeToTopicLiveData() @@ -72,27 +72,27 @@ class TopicTrainFragmentPresenter @Inject constructor( private fun processTopicResult(topic: AsyncResult): Topic { if (topic.isFailure()) { - logger.e("TopicTrainFragment", "Failed to retrieve topic", topic.getErrorOrNull()!!) + logger.e("TopicPracticeFragment", "Failed to retrieve topic", topic.getErrorOrNull()!!) } return topic.getOrDefault(Topic.getDefaultInstance()) } - private fun getTopicTrainViewModel(): TopicTrainViewModel { - return viewModelProvider.getForFragment(fragment, TopicTrainViewModel::class.java) + private fun getTopicPracticeViewModel(): TopicPracticeViewModel { + return viewModelProvider.getForFragment(fragment, TopicPracticeViewModel::class.java) } override fun skillSelected(skillId: String) { if (!selectedSkillIdList.contains(skillId)) { selectedSkillIdList.add(skillId) } - getTopicTrainViewModel().notifySelectedSkillList(selectedSkillIdList) + getTopicPracticeViewModel().notifySelectedSkillList(selectedSkillIdList) } override fun skillUnselected(skillId: String) { if (selectedSkillIdList.contains(skillId)) { selectedSkillIdList.remove(skillId) } - getTopicTrainViewModel().notifySelectedSkillList(selectedSkillIdList) + getTopicPracticeViewModel().notifySelectedSkillList(selectedSkillIdList) } internal fun onStartButtonClicked() { diff --git a/app/src/main/java/org/oppia/app/topic/train/TopicTrainViewModel.kt b/app/src/main/java/org/oppia/app/topic/practice/TopicPracticeViewModel.kt similarity index 61% rename from app/src/main/java/org/oppia/app/topic/train/TopicTrainViewModel.kt rename to app/src/main/java/org/oppia/app/topic/practice/TopicPracticeViewModel.kt index 814dfe6bed2..09835763e00 100644 --- a/app/src/main/java/org/oppia/app/topic/train/TopicTrainViewModel.kt +++ b/app/src/main/java/org/oppia/app/topic/practice/TopicPracticeViewModel.kt @@ -1,4 +1,4 @@ -package org.oppia.app.topic.train +package org.oppia.app.topic.practice import android.view.View import androidx.databinding.ObservableField @@ -6,10 +6,10 @@ import androidx.lifecycle.ViewModel import org.oppia.app.fragment.FragmentScope import javax.inject.Inject -/** [ViewModel] for showing skills in train fragment. */ +/** [ViewModel] for showing skills in practice fragment. */ @FragmentScope -class TopicTrainViewModel @Inject constructor( - private val topicTrainFragmentPresenter: TopicTrainFragmentPresenter +class TopicPracticeViewModel @Inject constructor( + private val topicPracticeFragmentPresenter: TopicPracticeFragmentPresenter ) : ViewModel() { var isSubmitButtonActive = ObservableField(false) @@ -19,6 +19,6 @@ class TopicTrainViewModel @Inject constructor( } fun startButtonClicked(v: View) { - topicTrainFragmentPresenter.onStartButtonClicked() + topicPracticeFragmentPresenter.onStartButtonClicked() } } diff --git a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivity.kt b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivity.kt index 0bcaaf38a0c..c23322a4cb3 100644 --- a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivity.kt +++ b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivity.kt @@ -8,7 +8,7 @@ import javax.inject.Inject const val QUESTION_PLAYER_ACTIVITY_SKILL_ID_LIST_ARGUMENT_KEY = "QuestionPlayerActivity.skill_id_list" -/** Activity for QuestionPlayer in train mode. */ +/** Activity for QuestionPlayer in practice mode. */ class QuestionPlayerActivity : InjectableAppCompatActivity() { @Inject lateinit var questionPlayerActivityPresenter: QuestionPlayerActivityPresenter @@ -19,7 +19,7 @@ class QuestionPlayerActivity : InjectableAppCompatActivity() { } companion object { - // TODO(#159): Use this skillList from TopicTrainFragment to fetch questions and start train mode. + // TODO(#159): Use this skillList from TopicPracticeFragment to fetch questions and start practice mode. /** Returns a new [Intent] to route to [QuestionPlayerActivity] for a specified skill ID list. */ fun createQuestionPlayerActivityIntent(context: Context, skillIdList: ArrayList): Intent { val intent = Intent(context, QuestionPlayerActivity::class.java) diff --git a/app/src/main/java/org/oppia/app/topic/train/SkillSelector.kt b/app/src/main/java/org/oppia/app/topic/train/SkillSelector.kt deleted file mode 100644 index 2de2a44f2ca..00000000000 --- a/app/src/main/java/org/oppia/app/topic/train/SkillSelector.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.oppia.app.topic.train - -/** Interface to update the selectedSkillList in [TopicTrainFragmentPresenter]. */ -interface SkillSelector { - /** This skill will get added to selectedSkillList in [TopicTrainFragmentPresenter]. */ - fun skillSelected(skillId: String) - - /** This skill will get removed from selectedSkillList in [TopicTrainFragmentPresenter]. */ - fun skillUnselected(skillId: String) -} diff --git a/app/src/main/res/drawable/circular_progress.xml b/app/src/main/res/drawable/circular_progress.xml deleted file mode 100755 index def50eeaa3e..00000000000 --- a/app/src/main/res/drawable/circular_progress.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_check_primary.xml b/app/src/main/res/drawable/ic_check_primary.xml deleted file mode 100644 index 8db2f4079bf..00000000000 --- a/app/src/main/res/drawable/ic_check_primary.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_train_icon_24dp.xml b/app/src/main/res/drawable/ic_practice_icon_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_train_icon_24dp.xml rename to app/src/main/res/drawable/ic_practice_icon_24dp.xml diff --git a/app/src/main/res/drawable/state_button_inactive_background.xml b/app/src/main/res/drawable/state_button_inactive_background.xml new file mode 100644 index 00000000000..9089afd6a24 --- /dev/null +++ b/app/src/main/res/drawable/state_button_inactive_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/state_button_transparent_background.xml b/app/src/main/res/drawable/state_button_transparent_background.xml index 96aa2dbe473..5ea1eb8baaa 100644 --- a/app/src/main/res/drawable/state_button_transparent_background.xml +++ b/app/src/main/res/drawable/state_button_transparent_background.xml @@ -1,7 +1,5 @@ - - + diff --git a/app/src/main/res/layout/activity_numeric_input_interaction_view_test.xml b/app/src/main/res/layout/activity_input_interaction_view_test.xml similarity index 60% rename from app/src/main/res/layout/activity_numeric_input_interaction_view_test.xml rename to app/src/main/res/layout/activity_input_interaction_view_test.xml index 6e11a8fa8e4..74f0cd43b03 100644 --- a/app/src/main/res/layout/activity_numeric_input_interaction_view_test.xml +++ b/app/src/main/res/layout/activity_input_interaction_view_test.xml @@ -1,15 +1,20 @@ + + + + @@ -18,9 +23,9 @@ @@ -49,11 +54,11 @@ android:focusable="true" android:hint="Write here." android:inputType="text" - android:text="@={textInputViewModel.answerText}" android:longClickable="false" android:maxLength="200" android:padding="8dp" - android:singleLine="true" /> + android:singleLine="true" + android:text="@={textInputViewModel.answerText}" /> + android:minHeight="48dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:paddingBottom="8dp" + android:singleLine="true" + android:text="@={fractionInteractionViewModel.answerText}" + android:textColor="@color/oppiaPrimaryText" + android:textColorHint="@color/editTextHint" + android:textSize="14sp" + android:textStyle="italic" + app:textChangedListener="@{fractionInteractionViewModel.answerTextWatcher}" /> + + + +