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 #379: Collapse past wrong answers #412

Merged
merged 15 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from 9 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
4 changes: 2 additions & 2 deletions app/src/main/java/org/oppia/app/player/state/StateFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.oppia.app.fragment.InjectableFragment
import org.oppia.app.model.InteractionObject
import org.oppia.app.model.UserAnswer
import org.oppia.app.player.audio.CellularDataInterface
import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver
import javax.inject.Inject
Expand Down Expand Up @@ -48,7 +48,7 @@ class StateFragment : InjectableFragment(), CellularDataInterface, InteractionAn
stateFragmentPresenter.handleDisableAudio(saveUserChoice)
}

override fun onAnswerReadyForSubmission(answer: InteractionObject) {
override fun onAnswerReadyForSubmission(answer: UserAnswer) {
stateFragmentPresenter.handleAnswerReadyForSubmission(answer)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.databinding.ObservableBoolean
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
Expand All @@ -18,18 +19,20 @@ import org.oppia.app.databinding.ContinueInteractionItemBinding
import org.oppia.app.databinding.FeedbackItemBinding
import org.oppia.app.databinding.FractionInteractionItemBinding
import org.oppia.app.databinding.NumericInputInteractionItemBinding
import org.oppia.app.databinding.PreviousResponsesHeaderItemBinding
import org.oppia.app.databinding.SelectionInteractionItemBinding
import org.oppia.app.databinding.StateButtonItemBinding
import org.oppia.app.databinding.StateFragmentBinding
import org.oppia.app.databinding.SubmittedAnswerItemBinding
import org.oppia.app.databinding.TextInputInteractionItemBinding
import org.oppia.app.fragment.FragmentScope
import org.oppia.app.model.AnswerAndResponse
import org.oppia.app.model.AnswerOutcome
import org.oppia.app.model.CellularDataPreference
import org.oppia.app.model.EphemeralState
import org.oppia.app.model.Interaction
import org.oppia.app.model.InteractionObject
import org.oppia.app.model.SubtitledHtml
import org.oppia.app.model.UserAnswer
import org.oppia.app.player.audio.AudioFragment
import org.oppia.app.player.audio.CellularDataDialogFragment
import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver
Expand All @@ -39,11 +42,14 @@ import org.oppia.app.player.state.itemviewmodel.FeedbackViewModel
import org.oppia.app.player.state.itemviewmodel.FractionInteractionViewModel
import org.oppia.app.player.state.itemviewmodel.InteractionViewModelFactory
import org.oppia.app.player.state.itemviewmodel.NumericInputViewModel
import org.oppia.app.player.state.itemviewmodel.PreviousResponsesHeaderViewModel
import org.oppia.app.player.state.itemviewmodel.SelectionInteractionViewModel
import org.oppia.app.player.state.itemviewmodel.StateItemViewModel
import org.oppia.app.player.state.itemviewmodel.StateNavigationButtonViewModel
import org.oppia.app.player.state.itemviewmodel.StateNavigationButtonViewModel.ContinuationNavigationButtonType
import org.oppia.app.player.state.itemviewmodel.SubmittedAnswerViewModel
import org.oppia.app.player.state.itemviewmodel.TextInputViewModel
import org.oppia.app.player.state.listener.PreviousResponsesHeaderClickListener
import org.oppia.app.player.state.listener.StateNavigationButtonListener
import org.oppia.app.recyclerview.BindableAdapter
import org.oppia.app.viewmodel.ViewModelProvider
Expand Down Expand Up @@ -74,7 +80,7 @@ class StateFragmentPresenter @Inject constructor(
private val htmlParserFactory: HtmlParser.Factory,
private val context: Context,
private val interactionViewModelFactoryMap: Map<String, @JvmSuppressWildcards InteractionViewModelFactory>
) : StateNavigationButtonListener {
) : StateNavigationButtonListener, PreviousResponsesHeaderClickListener {

private var showCellularDataDialog = true
private var useCellularData = false
Expand All @@ -86,6 +92,18 @@ class StateFragmentPresenter @Inject constructor(
private val ephemeralStateLiveData: LiveData<AsyncResult<EphemeralState>> by lazy {
explorationProgressController.getCurrentState()
}
/**
* 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
* (when present).
*/
private val previousAnswerViewModels: MutableList<StateItemViewModel> = mutableListOf()
/**
* Whether the previously submitted wrong answers should be expanded. This value is intentionally not retained upon
* configuration changes since the user can just re-expand the list.
*/
private var hasPreviousResponsesExpanded: Boolean = false

fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
cellularDialogController.getCellularDataPreference()
Expand Down Expand Up @@ -178,6 +196,30 @@ class StateFragmentPresenter @Inject constructor(
setViewModel = TextInputInteractionItemBinding::setViewModel,
transformViewModel = { it as TextInputViewModel }
)
.registerViewBinder(
viewType = StateItemViewModel.ViewType.SUBMITTED_ANSWER,
inflateView = { parent ->
SubmittedAnswerItemBinding.inflate(
LayoutInflater.from(parent.context), parent, /* attachToParent= */ false
).root
},
bindView = { view, viewModel ->
val binding = DataBindingUtil.findBinding<SubmittedAnswerItemBinding>(view)!!
val userAnswer = (viewModel as SubmittedAnswerViewModel).submittedUserAnswer
when (userAnswer.textualAnswerCase) {
UserAnswer.TextualAnswerCase.HTML_ANSWER -> binding.submittedAnswer = htmlParserFactory.create(
entityType, explorationId).parseOppiaHtml(userAnswer.htmlAnswer, binding.submittedAnswerTextView
)
else -> binding.submittedAnswer = userAnswer.plainAnswer
}
}
)
.registerViewDataBinder(
viewType = StateItemViewModel.ViewType.PREVIOUS_RESPONSES_HEADER,
inflateDataBinding = PreviousResponsesHeaderItemBinding::inflate,
setViewModel = PreviousResponsesHeaderItemBinding::setViewModel,
transformViewModel = { it as PreviousResponsesHeaderViewModel }
)
.build()
}

Expand Down Expand Up @@ -207,7 +249,7 @@ class StateFragmentPresenter @Inject constructor(
}
}

fun handleAnswerReadyForSubmission(answer: InteractionObject) {
fun handleAnswerReadyForSubmission(answer: UserAnswer) {
// An interaction has indicated that an answer is ready for submission.
handleSubmitAnswer(answer)
}
Expand Down Expand Up @@ -271,14 +313,15 @@ class StateFragmentPresenter @Inject constructor(
val scrollToTop = ::currentStateName.isInitialized && currentStateName != ephemeralState.state.name

currentStateName = ephemeralState.state.name
previousAnswerViewModels.clear() // But retain whether the list is currently open.
val pendingItemList = mutableListOf<StateItemViewModel>()
addContentItem(pendingItemList, ephemeralState)
val interaction = ephemeralState.state.interaction
if (ephemeralState.stateTypeCase == EphemeralState.StateTypeCase.PENDING_STATE) {
addPreviousAnswers(pendingItemList, interaction, ephemeralState.pendingState.wrongAnswerList)
addPreviousAnswers(pendingItemList, ephemeralState.pendingState.wrongAnswerList)
addInteractionForPendingState(pendingItemList, interaction)
} else if (ephemeralState.stateTypeCase == EphemeralState.StateTypeCase.COMPLETED_STATE) {
addPreviousAnswers(pendingItemList, interaction, ephemeralState.completedState.answerList)
addPreviousAnswers(pendingItemList, ephemeralState.completedState.answerList)
}

val hasPreviousState = ephemeralState.hasPreviousState
Expand Down Expand Up @@ -355,7 +398,7 @@ class StateFragmentPresenter @Inject constructor(
moveToNextState()
}

private fun handleSubmitAnswer(answer: InteractionObject) {
private fun handleSubmitAnswer(answer: UserAnswer) {
subscribeToAnswerOutcome(explorationProgressController.submitAnswer(answer))
}

Expand All @@ -365,25 +408,22 @@ class StateFragmentPresenter @Inject constructor(

override fun onNextButtonClicked() = moveToNextState()

override fun onResponsesHeaderClicked() {
togglePreviousAnswers()
}

private fun moveToNextState() {
explorationProgressController.moveToNextState()
explorationProgressController.moveToNextState().observe(fragment, Observer<AsyncResult<Any?>> {
hasPreviousResponsesExpanded = false
})
}

private fun addInteractionForPendingState(
pendingItemList: MutableList<StateItemViewModel>, interaction: Interaction
) = addInteraction(pendingItemList, interaction)

private fun addInteractionForCompletedState(
pendingItemList: MutableList<StateItemViewModel>, interaction: Interaction, existingAnswer: InteractionObject
) = addInteraction(pendingItemList, interaction, existingAnswer = existingAnswer, isReadOnly = true)

private fun addInteraction(
pendingItemList: MutableList<StateItemViewModel>, interaction: Interaction, existingAnswer:
InteractionObject? = null, isReadOnly: Boolean = false
) {
val interactionViewModelFactory = interactionViewModelFactoryMap.getValue(interaction.id)
pendingItemList += interactionViewModelFactory(
explorationId, interaction, fragment as InteractionAnswerReceiver, existingAnswer, isReadOnly
explorationId, interaction, fragment as InteractionAnswerReceiver
)
}

Expand All @@ -393,21 +433,69 @@ class StateFragmentPresenter @Inject constructor(
}

private fun addPreviousAnswers(
pendingItemList: MutableList<StateItemViewModel>, interaction: Interaction,
answersAndResponses: List<AnswerAndResponse>
pendingItemList: MutableList<StateItemViewModel>, answersAndResponses: List<AnswerAndResponse>
) {
// TODO: add support for displaying the previous answer, too.
for (answerAndResponse in answersAndResponses) {
addInteractionForCompletedState(pendingItemList, interaction, answerAndResponse.userAnswer)
addFeedbackItem(pendingItemList, answerAndResponse.feedback)
if (answersAndResponses.size > 1) {
PreviousResponsesHeaderViewModel(
answersAndResponses.size - 1, ObservableBoolean(hasPreviousResponsesExpanded), this
).let { viewModel ->
pendingItemList += viewModel
previousAnswerViewModels += viewModel
}
// Only add previous answers if current responses are expanded.
for (answerAndResponse in answersAndResponses.take(answersAndResponses.size - 1)) {
createSubmittedAnswer(answerAndResponse.userAnswer).let { viewModel ->
if (hasPreviousResponsesExpanded) {
pendingItemList += viewModel
}
previousAnswerViewModels += viewModel
}
createFeedbackItem(answerAndResponse.feedback)?.let { viewModel ->
if (hasPreviousResponsesExpanded) {
pendingItemList += viewModel
}
previousAnswerViewModels += viewModel
}
}
}
answersAndResponses.lastOrNull()?.let { answerAndResponse ->
pendingItemList += createSubmittedAnswer(answerAndResponse.userAnswer)
createFeedbackItem(answerAndResponse.feedback)?.let(pendingItemList::add)
}
}

/**
* Toggles whether the previous answers should be shown based on the current state stored in
* [PreviousResponsesHeaderViewModel].
*/
private fun togglePreviousAnswers() {
val headerModel = previousAnswerViewModels.first() as PreviousResponsesHeaderViewModel
val expandPreviousAnswers = !headerModel.isExpanded.get()
val headerIndex = viewModel.itemList.indexOf(headerModel)
val previousAnswersAndFeedbacks = previousAnswerViewModels.takeLast(previousAnswerViewModels.size - 1)
if (expandPreviousAnswers) {
// Add the pending view models to the recycler view to expand them.
viewModel.itemList.addAll(headerIndex + 1, previousAnswersAndFeedbacks)
} else {
// Remove the pending view models to collapse the list.
viewModel.itemList.removeAll(previousAnswersAndFeedbacks)
}
// Ensure the header matches the updated state.
headerModel.isExpanded.set(expandPreviousAnswers)
hasPreviousResponsesExpanded = expandPreviousAnswers
recyclerViewAdapter.notifyDataSetChanged()
}

private fun createSubmittedAnswer(userAnswer: UserAnswer): SubmittedAnswerViewModel {
return SubmittedAnswerViewModel(userAnswer)
}

private fun addFeedbackItem(pendingItemList: MutableList<StateItemViewModel>, feedback: SubtitledHtml) {
private fun createFeedbackItem(feedback: SubtitledHtml): FeedbackViewModel? {
// Only show feedback if there's some to show.
if (feedback.html.isNotEmpty()) {
pendingItemList += FeedbackViewModel(feedback.html)
return FeedbackViewModel(feedback.html)
}
return null
}

private fun updateNavigationButtonVisibility(
Expand Down
10 changes: 5 additions & 5 deletions app/src/main/java/org/oppia/app/player/state/StateViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package org.oppia.app.player.state

import androidx.databinding.ObservableArrayList
import androidx.databinding.ObservableField
import androidx.databinding.ObservableList
import androidx.lifecycle.ViewModel
import org.oppia.app.fragment.FragmentScope
import org.oppia.app.model.InteractionObject
import org.oppia.app.model.UserAnswer
import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler
import org.oppia.app.player.state.itemviewmodel.StateItemViewModel
import org.oppia.app.viewmodel.ObservableArrayList
import org.oppia.app.viewmodel.ObservableViewModel
import javax.inject.Inject

/** [ViewModel] for state-fragment. */
@FragmentScope
class StateViewModel @Inject constructor() : ObservableViewModel() {
val itemList: ObservableList<StateItemViewModel> = ObservableArrayList<StateItemViewModel>()
val itemList: ObservableList<StateItemViewModel> = ObservableArrayList()

val isAudioBarVisible = ObservableField<Boolean>(false)

Expand All @@ -31,8 +31,8 @@ class StateViewModel @Inject constructor() : ObservableViewModel() {
}

// TODO(#164): Add a hasPendingAnswer() that binds to the enabled state of the Submit button.
fun getPendingAnswer(): InteractionObject {
return getPendingAnswerHandler(itemList)?.getPendingAnswer() ?: InteractionObject.getDefaultInstance()
fun getPendingAnswer(): UserAnswer {
return getPendingAnswerHandler(itemList)?.getPendingAnswer() ?: UserAnswer.getDefaultInstance()
}

private fun getPendingAnswerHandler(itemList: List<StateItemViewModel>): InteractionAnswerHandler? {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.oppia.app.player.state.answerhandling

import org.oppia.app.model.InteractionObject
import org.oppia.app.model.UserAnswer

/**
* A handler for interaction answers. Handlers can either require an additional user action before the answer can be
Expand All @@ -15,13 +15,13 @@ interface InteractionAnswerHandler {
fun isExplicitAnswerSubmissionRequired(): Boolean = true

/** Return the current answer that is ready for handling. */
fun getPendingAnswer(): InteractionObject
fun getPendingAnswer(): UserAnswer
}

/**
* A callback that will be called by [InteractionAnswerHandler]s when a user submits an answer. To be implemented by
* the parent fragment of the handler.
*/
interface InteractionAnswerReceiver {
fun onAnswerReadyForSubmission(answer: InteractionObject)
fun onAnswerReadyForSubmission(answer: UserAnswer)
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
package org.oppia.app.player.state.itemviewmodel

import org.oppia.app.model.InteractionObject
import org.oppia.app.model.UserAnswer
import org.oppia.app.player.state.answerhandling.InteractionAnswerHandler
import org.oppia.app.player.state.answerhandling.InteractionAnswerReceiver
import org.oppia.domain.util.toAnswerString

// 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."

/** [ViewModel] for the 'Continue' button. */
class ContinueInteractionViewModel(
private val interactionAnswerReceiver: InteractionAnswerReceiver, existingAnswer: InteractionObject?,
val isReadOnly: Boolean
private val interactionAnswerReceiver: InteractionAnswerReceiver
): StateItemViewModel(ViewType.CONTINUE_INTERACTION), InteractionAnswerHandler {
val answerText: CharSequence = existingAnswer?.toAnswerString() ?: ""

override fun isExplicitAnswerSubmissionRequired(): Boolean = false

override fun getPendingAnswer(): InteractionObject {
return InteractionObject.newBuilder().setNormalizedString(DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER).build()
override fun getPendingAnswer(): UserAnswer {
return UserAnswer.newBuilder()
.setAnswer(InteractionObject.newBuilder().setNormalizedString(DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER))
.setPlainAnswer(DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER)
.build()
}

fun handleButtonClicked() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
package org.oppia.app.player.state.itemviewmodel

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.InteractionAnswerHandler
import org.oppia.domain.util.toAnswerString

class FractionInteractionViewModel(
existingAnswer: InteractionObject?, val isReadOnly: Boolean
): StateItemViewModel(ViewType.FRACTION_INPUT_INTERACTION), InteractionAnswerHandler {
var answerText: CharSequence = existingAnswer?.toAnswerString() ?: ""
/** [ViewModel] for the fraction input interaction. */
class FractionInteractionViewModel: StateItemViewModel(ViewType.FRACTION_INPUT_INTERACTION), InteractionAnswerHandler {
var answerText: CharSequence = ""

override fun getPendingAnswer(): InteractionObject {
val interactionObjectBuilder = InteractionObject.newBuilder()
override fun getPendingAnswer(): UserAnswer {
val userAnswerBuilder = UserAnswer.newBuilder()
if (answerText.isNotEmpty()) {
interactionObjectBuilder.fraction = StringToFractionParser().getFractionFromString(answerText.toString())
val answerTextString = answerText.toString()
userAnswerBuilder.answer = InteractionObject.newBuilder()
.setFraction(StringToFractionParser().getFractionFromString(answerTextString))
.build()
userAnswerBuilder.plainAnswer = answerTextString
}
return interactionObjectBuilder.build()
return userAnswerBuilder.build()
}
}
Loading