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 8 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,60 @@ 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 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.
rt4914 marked this conversation as resolved.
Show resolved Hide resolved

/** 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)

private val resultEphemeralState = ObservableField<EphemeralState>()
rt4914 marked this conversation as resolved.
Show resolved Hide resolved

private var showCellularDataDialog = true
private var useCellularData = false
Expand All @@ -47,7 +74,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 +112,201 @@ 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 controlButtonVisibility(interactionId: String, hasPreviousState: Boolean, hasNextState: Boolean) {
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
logger.d("StateFragment", "interactionId: $interactionId")
logger.d("StateFragment", "hasPreviousState: $hasPreviousState")
logger.d("StateFragment", "hasNextState: $hasNextState")
stateViewModel.hideAllButtons()
stateViewModel.setPreviousButtonVisible(hasPreviousState)
if (!hasNextState) {
when (interactionId) {
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
CONTINUE -> stateViewModel.setContinueButtonVisible(true)
END_EXPLORATION -> stateViewModel.setEndExplorationButtonVisible(true)
FRACTION_INPUT -> stateViewModel.setSubmitButtonVisible(true)
ITEM_SELECT_INPUT -> stateViewModel.setSubmitButtonVisible(true)
MULTIPLE_CHOICE_INPUT -> stateViewModel.setSubmitButtonVisible(true)
NUMERIC_INPUT -> stateViewModel.setSubmitButtonVisible(true)
NUMERIC_WITH_UNITS -> stateViewModel.setSubmitButtonVisible(true)
TEXT_INPUT -> stateViewModel.setSubmitButtonVisible(true)
}
} else {
stateViewModel.setNextButtonVisible(hasNextState)
}
}

/**
* This function subscribes to current state of the exploration.
* Whenever the current state changes it will automatically get called and therefore their is no need to call this separately.
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
* 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 ->
resultEphemeralState.set(it)
logger.d("StateFragment", "stateName: ${it.state.name}")
val interactionId = it.state.interaction.id
val hasPreviousState = it.hasPreviousState
val hasNextState = it.completedState.answerCount > 0
controlButtonVisibility(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.
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
*/
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())
}

// NB: Any interaction will lead to answer submission,
// meaning we have to submit answer before we can move to next state.
fun continueButtonClicked(v: View) {
submitContinueButtonAnswer()
}

fun submitButtonClicked(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 (resultEphemeralState.get()!!.state.interaction.id) {
Copy link
Member

Choose a reason for hiding this comment

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

Similar to above--we need to generalize this. We should have a way to retrieve the current InteractionObject from the active interaction view, then pipe that to the controller.

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
moveToNextState()
}

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

fun endExplorationButtonClicked(v: View) {
endExploration()
}

fun learnAgainButtonClicked(v: View) {
}

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 moveToNextState() {
explorationProgressController.moveToNextState()
}

private fun moveToPreviousState() {
explorationProgressController.moveToPreviousState()
}

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

import android.view.View
import androidx.databinding.ObservableField
import androidx.lifecycle.ViewModel
import org.oppia.app.fragment.FragmentScope
Expand All @@ -9,8 +10,56 @@ import javax.inject.Inject
@FragmentScope
class StateViewModel @Inject constructor() : ViewModel() {
var isAudioFragmentVisible = ObservableField<Boolean>(false)
var isContinueButtonVisible = ObservableField<Boolean>(false)
var isEndExplorationButtonVisible = ObservableField<Boolean>(false)
var isLearnAgainButtonVisible = ObservableField<Boolean>(false)
var isNextButtonVisible = ObservableField<Boolean>(false)
var isPreviousButtonVisible = ObservableField<Boolean>(false)
var isSubmitButtonVisible = ObservableField<Boolean>(false)
var isSubmitButtonActive = ObservableField<Boolean>(false)

fun hideAllButtons() {
isContinueButtonVisible.set(false)
isEndExplorationButtonVisible.set(false)
isLearnAgainButtonVisible.set(false)
isNextButtonVisible.set(false)
isPreviousButtonVisible.set(false)
isSubmitButtonVisible.set(false)
}

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

fun setContinueButtonVisible(isVisible: Boolean) {
isContinueButtonVisible.set(isVisible)
}

fun setEndExplorationButtonVisible(isVisible: Boolean) {
isEndExplorationButtonVisible.set(isVisible)
}

fun setLearnAgainButtonVisible(isVisible: Boolean) {
isLearnAgainButtonVisible.set(isVisible)
}

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

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

fun setSubmitButtonVisible(isVisible: Boolean) {
isSubmitButtonVisible.set(isVisible)
}

fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.isNotEmpty()) {
isSubmitButtonActive.set(true)
} else {
isSubmitButtonActive.set(false)
}
}
}
Loading