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 #251

Merged
merged 11 commits into from
Oct 22, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
86 changes: 86 additions & 0 deletions app/src/main/java/org/oppia/app/player/state/StateAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.oppia.app.player.state

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import androidx.databinding.library.baseAdapters.BR
import kotlinx.android.synthetic.main.state_button_item.view.*
import org.oppia.app.R
import org.oppia.app.player.state.itemviewmodel.StateButtonViewModel
import org.oppia.app.player.state.listener.ButtonInteractionListener
import org.oppia.app.databinding.StateButtonItemBinding

const val VIEW_TYPE_CONTENT = 1
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
const val VIEW_TYPE_INTERACTION_READ_ONLY = 2
const val VIEW_TYPE_NUMERIC_INPUT_INTERACTION = 3
const val VIEW_TYPE_TEXT_INPUT_INTERACTION = 4
const val VIEW_TYPE_STATE_BUTTON = 5

class StateAdapter(
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
private val itemList: MutableList<Any>,
private val buttonInteractionListener: ButtonInteractionListener
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {

lateinit var stateButtonViewModel: StateButtonViewModel

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_STATE_BUTTON -> {
val inflater = LayoutInflater.from(parent.context)
val binding =
DataBindingUtil.inflate<StateButtonItemBinding>(
inflater,
R.layout.state_button_item,
parent,
/* attachToParent= */false
)
StateButtonViewHolder(binding, buttonInteractionListener)
}
else -> throw IllegalArgumentException("Invalid view type") as Throwable
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
VIEW_TYPE_STATE_BUTTON -> {
(holder as StateButtonViewHolder).bind((itemList[position] as StateButtonViewModel))
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

override fun getItemViewType(position: Int): Int {
return when (itemList[position]) {
is StateButtonViewModel -> {
stateButtonViewModel = itemList[position] as StateButtonViewModel
VIEW_TYPE_STATE_BUTTON
}
else -> throw IllegalArgumentException("Invalid type of data $position")
}
}

override fun getItemCount(): Int {
return itemList.size
}

inner class StateButtonViewHolder(
val binding: ViewDataBinding,
private val buttonInteractionListener: ButtonInteractionListener
) : RecyclerView.ViewHolder(binding.root) {
internal fun bind(stateButtonViewModel: StateButtonViewModel) {
binding.setVariable(BR.buttonViewModel, stateButtonViewModel)
binding.root.interaction_button.setOnClickListener {
buttonInteractionListener.onInteractionButtonClicked()
}
binding.root.next_state_image_view.setOnClickListener {
buttonInteractionListener.onNextButtonClicked()
}
binding.root.previous_state_image_view.setOnClickListener {
buttonInteractionListener.onPreviousButtonClicked()
}
binding.executePendingBindings()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
package org.oppia.app.player.state

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
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.R
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.InteractionObject
import org.oppia.app.model.InteractionObjectOrBuilder
import org.oppia.app.player.audio.AudioFragment
import org.oppia.app.player.audio.CellularDataDialogFragment
import org.oppia.app.player.exploration.EXPLORATION_ACTIVITY_TOPIC_ID_ARGUMENT_KEY
import org.oppia.app.player.exploration.ExplorationActivity
import org.oppia.app.player.state.itemviewmodel.StateButtonViewModel
import org.oppia.app.player.state.listener.ButtonInteractionListener
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
Expand All @@ -25,20 +36,50 @@ import javax.inject.Inject
private const val TAG_CELLULAR_DATA_DIALOG = "CELLULAR_DATA_DIALOG"
private const val TAG_AUDIO_FRAGMENT = "AUDIO_FRAGMENT"

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

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

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

private var showCellularDataDialog = true
private var useCellularData = false
private var explorationId: String? = null

private val oldStateNameList: ArrayList<String> = ArrayList()

private val currentEphemeralState = ObservableField<EphemeralState>(EphemeralState.getDefaultInstance())
private var currentAnswerOutcome: AnswerOutcome? = null

private val itemList: MutableList<Any> = ArrayList()

private var hasGeneralContinueButton: Boolean = false

private lateinit var stateAdapter: StateAdapter

lateinit var binding: StateFragmentBinding
rt4914 marked this conversation as resolved.
Show resolved Hide resolved

fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
cellularDialogController.getCellularDataPreference()
.observe(fragment, Observer<AsyncResult<CellularDataPreference>> {
Expand All @@ -49,7 +90,12 @@ class StateFragmentPresenter @Inject constructor(
}
})

val binding = StateFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
stateAdapter = StateAdapter(itemList, this as ButtonInteractionListener)

binding = StateFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
binding.stateRecyclerView.apply {
adapter = stateAdapter
}
binding.let {
it.stateFragment = fragment as StateFragment
it.viewModel = getStateViewModel()
Expand Down Expand Up @@ -121,7 +167,38 @@ class StateFragmentPresenter @Inject constructor(

private fun subscribeToCurrentState() {
ephemeralStateLiveData.observe(fragment, Observer<EphemeralState> { result ->
logger.d("StateFragment", "getCurrentState: ${result.state.name}")
if (result.hasState()) {
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
itemList.clear()
currentEphemeralState.set(result)

updateDummyStateName()

val interactionId = result.state.interaction.id
val hasPreviousState = result.hasPreviousState
var hasNextState = false
hasGeneralContinueButton = false

if (!result.terminalState) {
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
if (result.stateTypeCase.number == EphemeralState.COMPLETED_STATE_FIELD_NUMBER
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
&& !oldStateNameList.contains(currentEphemeralState.get()!!.state.name)
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
) {
hasGeneralContinueButton = true
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
hasNextState = false
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
} else if (result.completedState.answerList.size > 0
&& oldStateNameList.contains(currentEphemeralState.get()!!.state.name)
) {
hasNextState = true
hasGeneralContinueButton = false
}
}

updateNavigationButtonVisibility(
interactionId,
hasPreviousState,
hasNextState,
hasGeneralContinueButton
)
}
})
}

Expand All @@ -139,4 +216,166 @@ class StateFragmentPresenter @Inject constructor(
}
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> {
currentAnswerOutcome = it

if (currentEphemeralState.get()!!.state.interaction.id == CONTINUE) {
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
moveToNextState()
}
})
}

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

private fun endExploration() {
explorationDataController.stopPlayingExploration()
(activity as ExplorationActivity).finish()
}

override fun onInteractionButtonClicked() {
hideKeyboard()
// 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

if (!hasGeneralContinueButton) {
val interactionObject: InteractionObject = getDummyInteractionObject()
when (currentEphemeralState.get()!!.state.interaction.id) {
END_EXPLORATION -> endExploration()
CONTINUE -> subscribeToAnswerOutcome(explorationProgressController.submitAnswer(createContinueButtonAnswer()))
MULTIPLE_CHOICE_INPUT -> subscribeToAnswerOutcome(
explorationProgressController.submitAnswer(
InteractionObject.newBuilder().setNonNegativeInt(
stateWelcomeAnswer
).build()
)
)
FRACTION_INPUT,
ITEM_SELECT_INPUT,
NUMERIC_INPUT,
NUMERIC_WITH_UNITS,
TEXT_INPUT -> subscribeToAnswerOutcome(
explorationProgressController.submitAnswer(interactionObject)
)
}
} else {
moveToNextState()
}
}

override fun onPreviousButtonClicked() {
explorationProgressController.moveToPreviousState()
}

override fun onNextButtonClicked() {
moveToNextState()
}

private fun moveToNextState() {
checkAndUpdateOldStateNameList()
itemList.clear()
currentAnswerOutcome = null
explorationProgressController.moveToNextState()
}

private fun createContinueButtonAnswer(): InteractionObject {
return InteractionObject.newBuilder().setNormalizedString(DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER).build()
}

private fun checkAndUpdateOldStateNameList() {
if (currentAnswerOutcome != null
&& !currentAnswerOutcome!!.sameState
&& !oldStateNameList.contains(currentEphemeralState.get()!!.state.name)
rt4914 marked this conversation as resolved.
Show resolved Hide resolved
) {
oldStateNameList.add(currentEphemeralState.get()!!.state.name)
}
}

private fun updateNavigationButtonVisibility(
interactionId: String,
hasPreviousState: Boolean,
hasNextState: Boolean,
hasGeneralContinueButton: Boolean
) {
getStateButtonViewModel().setPreviousButtonVisible(hasPreviousState)

when {
hasGeneralContinueButton -> {
getStateButtonViewModel().clearObservableInteractionId()
getStateButtonViewModel().setObservableInteractionId(CONTINUE)
}
hasNextState -> {
getStateButtonViewModel().clearObservableInteractionId()
getStateButtonViewModel().setNextButtonVisible(hasNextState)
}
else -> {
getStateButtonViewModel().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.
getStateButtonViewModel().optionSelected(true)
}
}
itemList.add(getStateButtonViewModel())
stateAdapter.notifyDataSetChanged()
}

private fun getStateButtonViewModel(): StateButtonViewModel {
return stateButtonViewModelProvider.getForFragment(fragment, StateButtonViewModel::class.java)
}

private fun hideKeyboard() {
val inputManager: InputMethodManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(fragment.view!!.windowToken, InputMethodManager.SHOW_FORCED)
}

// TODO(#163): Remove this function, this is just for dummy testing purposes.
private fun updateDummyStateName(){
getStateViewModel().setStateName(currentEphemeralState.get()!!.state.name)
}

// TODO(#163): Remove this function and fetch this InteractionObject from [StateAdapter].
private fun getDummyInteractionObject(): InteractionObject {
val interactionObjectBuilder: InteractionObject.Builder = InteractionObject.newBuilder()
when (currentEphemeralState.get()!!.state.name) {
"Welcome!" -> interactionObjectBuilder.nonNegativeInt = 0
"What language" -> interactionObjectBuilder.normalizedString = "finnish"
"Things you can do" -> createContinueButtonAnswer()
"Numeric input" -> interactionObjectBuilder.real = 121.0
else -> InteractionObject.getDefaultInstance()
}
return interactionObjectBuilder.build()
}
}
Loading