Skip to content

Commit

Permalink
Merge pull request #1 from oppia/develop
Browse files Browse the repository at this point in the history
Sync forked repository
  • Loading branch information
PrarabdhGarg authored Jan 27, 2020
2 parents f8cd71d + 167fe33 commit 710093a
Show file tree
Hide file tree
Showing 54 changed files with 560 additions and 242 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 2 additions & 2 deletions app/src/main/java/org/oppia/app/fragment/FragmentComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
71 changes: 67 additions & 4 deletions app/src/main/java/org/oppia/app/parser/StringToFractionParser.kt
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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? {
Expand All @@ -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.
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<CheckBox>(R.id.cellular_data_dialog_checkbox)

val cellularDataInterface: CellularDataInterface = parentFragment as AudioFragment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<StateItemViewModel> = mutableListOf()
Expand All @@ -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)!!
Expand Down Expand Up @@ -399,7 +401,7 @@ class StateFragmentPresenter @Inject constructor(
Handler().postDelayed({
binding.congratulationTextview.clearAnimation()
binding.congratulationTextview.visibility = View.INVISIBLE
},2000)
}, 2000)
}

/** Helper for subscribeToAnswerOutcome. */
Expand Down Expand Up @@ -440,7 +442,8 @@ class StateFragmentPresenter @Inject constructor(

fun handleKeyboardAction() {
hideKeyboard()
handleSubmitAnswer(viewModel.getPendingAnswer())
if (stateNavigationButtonViewModel.isInteractionButtonActive.get()!!)
handleSubmitAnswer(viewModel.getPendingAnswer())
}

override fun onContinueButtonClicked() {
Expand Down Expand Up @@ -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
)
}

Expand Down Expand Up @@ -559,7 +562,7 @@ class StateFragmentPresenter @Inject constructor(
hasGeneralContinueButton: Boolean,
stateIsTerminal: Boolean
) {
val stateNavigationButtonViewModel =
stateNavigationButtonViewModel =
StateNavigationButtonViewModel(context, this as StateNavigationButtonListener)
stateNavigationButtonViewModel.updatePreviousButton(isEnabled = hasPreviousState)

Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<StateItemViewModel>): InteractionAnswerHandler? {
Expand Down
Original file line number Diff line number Diff line change
@@ -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?) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

/**
Expand All @@ -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
}
Loading

0 comments on commit 710093a

Please sign in to comment.