Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #156 & #157: Continue/End Exploration player buttons- low-fi #222

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,63 @@ package org.oppia.app.player.state
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.ObservableField
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.Transformations
import org.oppia.app.databinding.StateFragmentBinding
import org.oppia.app.fragment.FragmentScope
import org.oppia.app.model.AnswerOutcome
import org.oppia.app.model.CellularDataPreference
import org.oppia.app.model.EphemeralState
import org.oppia.app.model.Fraction
import org.oppia.app.model.InteractionObject
import org.oppia.app.model.NumberWithUnits
import org.oppia.app.player.audio.CellularDataDialogFragment
import org.oppia.app.player.exploration.ExplorationActivity
import org.oppia.app.viewmodel.ViewModelProvider
import org.oppia.domain.audio.CellularDialogController
import org.oppia.domain.exploration.ExplorationDataController
import org.oppia.domain.exploration.ExplorationProgressController
import org.oppia.util.data.AsyncResult
import org.oppia.util.logging.Logger
import javax.inject.Inject

private const val CONTINUE = "Continue"
private const val END_EXPLORATION = "EndExploration"
private const val LEARN_AGAIN = "LearnAgain"
private const val MULTIPLE_CHOICE_INPUT = "MultipleChoiceInput"
private const val ITEM_SELECT_INPUT = "ItemSelectionInput"
private const val TEXT_INPUT = "TextInput"
private const val FRACTION_INPUT = "FractionInput"
private const val NUMERIC_INPUT = "NumericInput"
private const val NUMERIC_WITH_UNITS = "NumberWithUnits"
private const val TAG_CELLULAR_DATA_DIALOG = "CELLULAR_DATA_DIALOG"

// For context:
// https://github.com/oppia/oppia/blob/37285a/extensions/interactions/Continue/directives/oppia-interactive-continue.directive.ts.
private const val DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER = "Please continue."

// TODO(#163): Remove all loggers.

/** The presenter for [StateFragment]. */
@FragmentScope
class StateFragmentPresenter @Inject constructor(
private val fragment: Fragment,
private val activity: AppCompatActivity,
private val cellularDialogController: CellularDialogController,
private val viewModelProvider: ViewModelProvider<StateViewModel>,
private val explorationDataController: ExplorationDataController,
private val explorationProgressController: ExplorationProgressController,
private val logger: Logger
private val fragment: Fragment,
private val logger: Logger,
private val viewModelProvider: ViewModelProvider<StateViewModel>
) {
private val stateViewModel = viewModelProvider.getForFragment(fragment, StateViewModel::class.java)

val currentEphemeralState = ObservableField<EphemeralState>()

private val stateName = ObservableField<String>("")

private var showCellularDataDialog = true
private var useCellularData = false
Expand All @@ -47,7 +77,8 @@ class StateFragmentPresenter @Inject constructor(
val binding = StateFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
binding.let {
it.stateFragment = fragment as StateFragment
it.viewModel = getStateViewModel()
it.presenter = this
it.viewModel = stateViewModel
}

subscribeToCurrentState()
Expand Down Expand Up @@ -84,32 +115,182 @@ class StateFragmentPresenter @Inject constructor(
dialogFragment.showNow(fragment.childFragmentManager, TAG_CELLULAR_DATA_DIALOG)
}

private fun getStateViewModel(): StateViewModel {
return viewModelProvider.getForFragment(fragment, StateViewModel::class.java)
fun setAudioFragmentVisible(isVisible: Boolean) {
stateViewModel.setAudioFragmentVisible(isVisible)
}

fun setAudioFragmentVisible(isVisible: Boolean) {
getStateViewModel().setAudioFragmentVisible(isVisible)
private fun updateNavigationButtonVisibility(
interactionId: String,
hasPreviousState: Boolean,
hasNextState: Boolean
) {
logger.d("StateFragment", "interactionId: $interactionId")
logger.d("StateFragment", "hasPreviousState: $hasPreviousState")
logger.d("StateFragment", "hasNextState: $hasNextState")
stateViewModel.setPreviousButtonVisible(hasPreviousState)
if (!hasNextState) {
stateViewModel.setObservableInteractionId(interactionId)
// TODO(#163): This function controls whether the "Submit" button should be displayed or not.
// Remove this function in final implementation and control this whenever user selects some option in
// MultipleChoiceInput or InputSelectionInput. For now this is `true` because we do not have a mechanism to work
// with MultipleChoiceInput or InputSelectionInput, which will eventually be responsible for controlling this.
stateViewModel.optionSelected(true)
} else {
stateViewModel.clearObservableInteractionId()
stateViewModel.setNextButtonVisible(hasNextState)
}
}

/**
* This function subscribes to current state of the exploration.
* Whenever the current state changes it will automatically get called and therefore there is no need to call this separately.
* this function currently provides important information to decide which button we should display.
*/
private fun subscribeToCurrentState() {
ephemeralStateLiveData.observe(fragment, Observer<EphemeralState> { result ->
logger.d("StateFragment", "getCurrentState: ${result.state.name}")
ephemeralStateLiveData.observe(fragment, Observer<EphemeralState> { it ->
currentEphemeralState.set(it)
val interactionId = it.state.interaction.id
val hasPreviousState = it.hasPreviousState
val hasNextState = it.completedState.answerCount > 0
stateName.set(it.state.name)
updateNavigationButtonVisibility(interactionId, hasPreviousState, hasNextState)
})
}

/** Helper for subscribeToCurrentState. */
private val ephemeralStateLiveData: LiveData<EphemeralState> by lazy {
getEphemeralState()
}

/** Helper for subscribeToCurrentState. */
private fun getEphemeralState(): LiveData<EphemeralState> {
return Transformations.map(explorationProgressController.getCurrentState(), ::processCurrentState)
}

/** Helper for subscribeToCurrentState. */
private fun processCurrentState(ephemeralStateResult: AsyncResult<EphemeralState>): EphemeralState {
if (ephemeralStateResult.isFailure()) {
logger.e("StateFragment", "Failed to retrieve ephemeral state", ephemeralStateResult.getErrorOrNull()!!)
}
return ephemeralStateResult.getOrDefault(EphemeralState.getDefaultInstance())
}

/**
* This function listens to the result of submitAnswer.
* Whenever an answer is submitted using ExplorationProgressController.submitAnswer function,
* this function will wait for the response from that function and based on which we can move to next state.
*/
private fun subscribeToAnswerOutcome(answerOutcomeResultLiveData: LiveData<AsyncResult<AnswerOutcome>>) {
val answerOutcomeLiveData = getAnswerOutcome(answerOutcomeResultLiveData)
answerOutcomeLiveData.observe(fragment, Observer<AnswerOutcome> {
explorationProgressController.moveToNextState()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right? From the mocks it seems like some interactions submit upon selecting an answer (e.g. multiple choice), and others the 'submit' button changes to 'continue' once the answer is submitted. Either way, submitting the answer and progressing to the next card always seem to be distinct user actions (except for continue since there is no explicit answer submission).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check revised implementation TODO comment.

I have created one method onOptionSelected(isOptionSelected: Boolean), which will control the visibility of the submit-button.

But for now I have kept this true because, currently we don't have functionality of MultipleChoiceInput and ItemSelectionInput.

})
}

/** Helper for subscribeToAnswerOutcome. */
private fun getAnswerOutcome(answerOutcome: LiveData<AsyncResult<AnswerOutcome>>): LiveData<AnswerOutcome> {
return Transformations.map(answerOutcome, ::processAnswerOutcome)
}

/** Helper for subscribeToAnswerOutcome. */
private fun processAnswerOutcome(ephemeralStateResult: AsyncResult<AnswerOutcome>): AnswerOutcome {
if (ephemeralStateResult.isFailure()) {
logger.e("StateFragment", "Failed to retrieve answer outcome", ephemeralStateResult.getErrorOrNull()!!)
}
return ephemeralStateResult.getOrDefault(AnswerOutcome.getDefaultInstance())
}

fun interactionButtonClicked(v: View) {
// TODO(#163): Remove these dummy answers and fetch answers from different interaction views.
// NB: This sample data will work only with TEST_EXPLORATION_ID_5
// 0 -> What Language
// 2 -> Welcome!
// XX -> What Language
val stateWelcomeAnswer = 0
// finnish -> Numeric input
// suomi -> Numeric input
// XX -> What Language
val stateWhatLanguageAnswer: String = "finnish"
// 121 -> Things You can do
// < 121 -> Estimate 100
// > 121 -> Numeric Input
// XX -> Numeric Input
val stateNumericInputAnswer = 121

when (currentEphemeralState.get()!!.state.interaction.id) {
CONTINUE -> submitContinueButtonAnswer()
END_EXPLORATION -> endExploration()
FRACTION_INPUT -> submitFractionInputAnswer(Fraction.getDefaultInstance())
ITEM_SELECT_INPUT -> submitMultipleChoiceAnswer(0)
MULTIPLE_CHOICE_INPUT -> submitMultipleChoiceAnswer(stateWelcomeAnswer)
NUMERIC_INPUT -> submitNumericInputAnswer(stateNumericInputAnswer.toDouble())
NUMERIC_WITH_UNITS -> submitNumericWithUnitsInputAnswer(NumberWithUnits.getDefaultInstance())
TEXT_INPUT -> submitTextInputAnswer(stateWhatLanguageAnswer)
}
}

fun nextButtonClicked(v: View) {
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
explorationProgressController.moveToNextState()
}

fun previousButtonClicked(v: View) {
explorationProgressController.moveToPreviousState()
}

private fun submitContinueButtonAnswer() {
subscribeToAnswerOutcome(explorationProgressController.submitAnswer(createContinueButtonAnswer()))
}

private fun submitFractionInputAnswer(fractionAnswer: Fraction) {
subscribeToAnswerOutcome(explorationProgressController.submitAnswer(createFractionInputAnswer(fractionAnswer)))
}

private fun submitMultipleChoiceAnswer(choiceIndex: Int) {
subscribeToAnswerOutcome(explorationProgressController.submitAnswer(createMultipleChoiceAnswer(choiceIndex)))
}

private fun submitNumericInputAnswer(numericAnswer: Double) {
subscribeToAnswerOutcome(explorationProgressController.submitAnswer(createNumericInputAnswer(numericAnswer)))
}

private fun submitNumericWithUnitsInputAnswer(numberWithUnits: NumberWithUnits) {
subscribeToAnswerOutcome(
explorationProgressController.submitAnswer(
createNumericWithUnitsInputAnswer(
numberWithUnits
)
)
)
}

private fun submitTextInputAnswer(textAnswer: String) {
subscribeToAnswerOutcome(explorationProgressController.submitAnswer(createTextInputAnswer(textAnswer)))
}

private fun createContinueButtonAnswer() = createTextInputAnswer(DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER)

private fun createFractionInputAnswer(fractionAnswer: Fraction): InteractionObject {
return InteractionObject.newBuilder().setFraction(fractionAnswer).build()
}

private fun createMultipleChoiceAnswer(choiceIndex: Int): InteractionObject {
return InteractionObject.newBuilder().setNonNegativeInt(choiceIndex).build()
}

private fun createNumericInputAnswer(numericAnswer: Double): InteractionObject {
return InteractionObject.newBuilder().setReal(numericAnswer).build()
}

private fun createNumericWithUnitsInputAnswer(numberWithUnits: NumberWithUnits): InteractionObject {
return InteractionObject.newBuilder().setNumberWithUnits(numberWithUnits).build()
}

private fun createTextInputAnswer(textAnswer: String): InteractionObject {
return InteractionObject.newBuilder().setNormalizedString(textAnswer).build()
}

private fun endExploration() {
explorationDataController.stopPlayingExploration()
(activity as ExplorationActivity).finish()
}
}
100 changes: 99 additions & 1 deletion app/src/main/java/org/oppia/app/player/state/StateViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,114 @@
package org.oppia.app.player.state

import android.content.Context
import android.widget.Button
import androidx.databinding.BindingAdapter
import androidx.databinding.ObservableField
import androidx.lifecycle.ViewModel
import org.oppia.app.fragment.FragmentScope
import javax.inject.Inject
import org.oppia.app.R
import org.oppia.app.viewmodel.ObservableViewModel

private const val CONTINUE = "Continue"
private const val END_EXPLORATION = "EndExploration"
private const val LEARN_AGAIN = "LearnAgain"
private const val MULTIPLE_CHOICE_INPUT = "MultipleChoiceInput"
private const val ITEM_SELECT_INPUT = "ItemSelectionInput"
private const val TEXT_INPUT = "TextInput"
private const val FRACTION_INPUT = "FractionInput"
private const val NUMERIC_INPUT = "NumericInput"
private const val NUMERIC_WITH_UNITS = "NumberWithUnits"

/** [ViewModel] for state-fragment. */
@FragmentScope
class StateViewModel @Inject constructor() : ViewModel() {
class StateViewModel @Inject constructor(val context: Context) : ObservableViewModel() {
companion object {
@JvmStatic
@BindingAdapter("buttonDrawable")
fun setBackgroundResource(button: Button, resource: Int) {
button.setBackgroundResource(resource)
}
}

var isAudioFragmentVisible = ObservableField<Boolean>(false)

var isNextButtonVisible = ObservableField<Boolean>(false)
var isPreviousButtonVisible = ObservableField<Boolean>(false)

var observableInteractionId = ObservableField<String>()
var isInteractionButtonActive = ObservableField<Boolean>(false)
var isInteractionButtonVisible = ObservableField<Boolean>(false)
var drawableResourceValue = ObservableField<Int>(R.drawable.state_button_primary_background)

var name = ObservableField<String>()

fun setObservableInteractionId(interactionId: String) {
setNextButtonVisible(false)
observableInteractionId.set(interactionId)
when (interactionId) {
CONTINUE -> {
isInteractionButtonActive.set(true)
isInteractionButtonVisible.set(true)
name.set(context.getString(R.string.state_continue_button))
drawableResourceValue.set(R.drawable.state_button_primary_background)
}
END_EXPLORATION -> {
isInteractionButtonActive.set(true)
isInteractionButtonVisible.set(true)
name.set(context.getString(R.string.state_end_exploration_button))
drawableResourceValue.set(R.drawable.state_button_primary_background)
}
LEARN_AGAIN -> {
isInteractionButtonActive.set(true)
isInteractionButtonVisible.set(true)
name.set(context.getString(R.string.state_learn_again_button))
drawableResourceValue.set(R.drawable.state_button_blue_background)
}
ITEM_SELECT_INPUT, MULTIPLE_CHOICE_INPUT -> {
isInteractionButtonActive.set(true)
isInteractionButtonVisible.set(false)
name.set(context.getString(R.string.state_submit_button))
drawableResourceValue.set(R.drawable.state_button_primary_background)
}
FRACTION_INPUT, NUMERIC_INPUT, NUMERIC_WITH_UNITS, TEXT_INPUT -> {
isInteractionButtonActive.set(false)
isInteractionButtonVisible.set(true)
name.set(context.getString(R.string.state_submit_button))
drawableResourceValue.set(R.drawable.state_button_transparent_background)
}
}
}

fun clearObservableInteractionId() {
observableInteractionId.set("")
isInteractionButtonVisible.set(false)
isInteractionButtonActive.set(false)
}

fun setAudioFragmentVisible(isVisible: Boolean) {
isAudioFragmentVisible.set(isVisible)
}

fun setNextButtonVisible(isVisible: Boolean) {
isNextButtonVisible.set(isVisible)
}

fun setPreviousButtonVisible(isVisible: Boolean) {
isPreviousButtonVisible.set(isVisible)
}

fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.isNotEmpty()) {
isInteractionButtonActive.set(true)
drawableResourceValue.set(R.drawable.state_button_primary_background)
} else {
isInteractionButtonActive.set(false)
drawableResourceValue.set(R.drawable.state_button_transparent_background)
}
}

fun optionSelected(isOptionSelected: Boolean) {
isInteractionButtonVisible.set(isOptionSelected)
}
}
Loading