diff --git a/app/build.gradle b/app/build.gradle index 4083f59c3f9..2126e0e0d6f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,7 +77,8 @@ dependencies { 'androidx.test.ext:junit:1.1.1', 'com.google.truth:truth:0.43', 'org.robolectric:robolectric:4.3', - 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2' + 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2', + 'androidx.test.espresso:espresso-contrib:3.1.0', ) androidTestImplementation( 'androidx.test:core:1.2.0', @@ -86,6 +87,7 @@ dependencies { 'androidx.test.ext:junit:1.1.1', 'androidx.test:runner:1.2.0', 'com.google.truth:truth:0.43', + 'androidx.test.espresso:espresso-contrib:3.1.0', ) androidTestUtil( 'androidx.test:orchestrator:1.2.0', diff --git a/app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt b/app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt new file mode 100755 index 00000000000..91b0f92a5c4 --- /dev/null +++ b/app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt @@ -0,0 +1,128 @@ +package org.oppia.app.player.state; + +import android.content.Context +import android.text.Html +import android.text.Spannable +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.databinding.library.baseAdapters.BR +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.item_selection_interaction_items.view.* +import kotlinx.android.synthetic.main.multiple_choice_interaction_items.view.* +import org.oppia.app.R +import org.oppia.app.databinding.ItemSelectionInteractionItemsBinding +import org.oppia.app.databinding.MultipleChoiceInteractionItemsBinding +import org.oppia.util.data.HtmlParser + +const val VIEW_TYPE_MULTIPLE_CHOICE = 1 +const val VIEW_TYPE_ITEM_SELECTION = 2 + +/** Adapter to bind the interactions to the [RecyclerView]. It handles MultipleChoiceInput and ItemSelectionInput interaction views. */ +class InteractionAdapter( + private val context: Context, + private val entityType: String, + private val entityId: String, + val itemList: Array?, + val interactionInstanceId: String? +) : RecyclerView.Adapter() { + + private var mSelectedItem = -1 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_MULTIPLE_CHOICE -> { + val inflater = LayoutInflater.from(parent.getContext()) + val binding = + DataBindingUtil.inflate( + inflater, + R.layout.multiple_choice_interaction_items, + parent, + false + ) + MultipleChoiceViewHolder(binding) + } + VIEW_TYPE_ITEM_SELECTION -> { + val inflater = LayoutInflater.from(parent.getContext()) + val binding = + DataBindingUtil.inflate( + inflater, + R.layout.item_selection_interaction_items, + parent, + false + ) + ItemSelectionViewHolder(binding) + } + else -> throw IllegalArgumentException("Invalid view type") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder.itemViewType) { + VIEW_TYPE_MULTIPLE_CHOICE -> (holder as MultipleChoiceViewHolder).bind( + itemList!!.get(position), + position, + mSelectedItem + ) + VIEW_TYPE_ITEM_SELECTION -> (holder as ItemSelectionViewHolder).bind( + itemList!!.get(position), + position, + mSelectedItem + ) + } + } + + // Determines the appropriate ViewType according to the interaction type. + override fun getItemViewType(position: Int): Int { + return if (interactionInstanceId.equals("ItemSelectionInput")) { + VIEW_TYPE_ITEM_SELECTION + } else { + VIEW_TYPE_MULTIPLE_CHOICE + } + } + + override fun getItemCount(): Int { + return itemList!!.size + } + + private inner class ItemSelectionViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { + internal fun bind(rawString: String?, position: Int, selectedPosition: Int) { + binding.setVariable(BR.htmlContent, rawString) + binding.executePendingBindings(); + val html: Spannable = HtmlParser(context,entityType,entityId).parseHtml(rawString, binding.root.tv_item_selection_contents) + binding.root.tv_item_selection_contents.text = html + + binding.root.rl_checkbox_container.setOnClickListener { + if (binding.root.cb_item_selection.isChecked) + binding.root.cb_item_selection.setChecked(false) + else + binding.root.cb_item_selection.setChecked(true) + Toast.makeText(context, "" + binding.root.tv_item_selection_contents.text, Toast.LENGTH_LONG).show() + notifyDataSetChanged() + } + } + } + + private inner class MultipleChoiceViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { + internal fun bind(rawString: String?, position: Int, selectedPosition: Int) { + binding.setVariable(BR.htmlContent, rawString) + binding.executePendingBindings(); + val html: Spannable = HtmlParser(context,entityType,entityId).parseHtml(rawString, binding.root.tv_multiple_choice_contents) + binding.root.tv_multiple_choice_contents.text = html + + if (selectedPosition == position) + binding.root.rb_multiple_choice.setChecked(true) + else + binding.root.rb_multiple_choice.setChecked(false) + + binding.root.rl_radio_container.setOnClickListener { + Toast.makeText(context, "" + binding.root.tv_multiple_choice_contents.text, Toast.LENGTH_LONG).show() + mSelectedItem = getAdapterPosition() + notifyDataSetChanged() + } + } + } +} diff --git a/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt index a1c0ea3501e..258980e8f7a 100755 --- a/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/app/player/state/StateFragmentPresenter.kt @@ -1,5 +1,6 @@ package org.oppia.app.player.state +import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -12,6 +13,8 @@ import org.oppia.app.databinding.StateFragmentBinding import org.oppia.app.fragment.FragmentScope import org.oppia.app.model.CellularDataPreference import org.oppia.app.model.EphemeralState +import org.oppia.app.model.InteractionObject +import org.oppia.app.model.StringList 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 @@ -24,10 +27,12 @@ import javax.inject.Inject private const val TAG_CELLULAR_DATA_DIALOG = "CELLULAR_DATA_DIALOG" private const val TAG_AUDIO_FRAGMENT = "AUDIO_FRAGMENT" +private const val TAG_STATE_FRAGMENT = "STATE_FRAGMENT" /** The presenter for [StateFragment]. */ @FragmentScope class StateFragmentPresenter @Inject constructor( + private val context: Context, private val fragment: Fragment, private val cellularDialogController: CellularDialogController, private val viewModelProvider: ViewModelProvider, @@ -39,6 +44,14 @@ class StateFragmentPresenter @Inject constructor( private var useCellularData = false private var explorationId: String? = null + private var items: Array? = null + var customizationArgsMap = HashMap() + var interactionInstanceId: String? = null + private var entityType: String = "" + private var entityId: String = "" + + var interactionAdapter: InteractionAdapter? = null + fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? { cellularDialogController.getCellularDataPreference() .observe(fragment, Observer> { @@ -121,7 +134,19 @@ class StateFragmentPresenter @Inject constructor( private fun subscribeToCurrentState() { ephemeralStateLiveData.observe(fragment, Observer { result -> - logger.d("StateFragment", "getCurrentState: ${result.state.name}") + logger.d(TAG_STATE_FRAGMENT, "getCurrentState: ${result.state.name}") + entityType = "exploration" + val customizationArgsMap: Map = result.state.interaction.customizationArgsMap + val allKeys: Set = customizationArgsMap.keys + + for (key in allKeys) { + logger.d(TAG_STATE_FRAGMENT, key) + } + if (customizationArgsMap.contains("choices")) { + val customizationArgs: InteractionObject? = customizationArgsMap.get("choices") + val stringList: StringList = customizationArgs!!.setOfHtmlString + logger.d(TAG_STATE_FRAGMENT, "value: ${stringList.htmlCount}") + } }) } @@ -135,8 +160,26 @@ class StateFragmentPresenter @Inject constructor( private fun processCurrentState(ephemeralStateResult: AsyncResult): EphemeralState { if (ephemeralStateResult.isFailure()) { - logger.e("StateFragment", "Failed to retrieve ephemeral state", ephemeralStateResult.getErrorOrNull()!!) + logger.e(TAG_STATE_FRAGMENT, "Failed to retrieve ephemeral state", ephemeralStateResult.getErrorOrNull()!!) } return ephemeralStateResult.getOrDefault(EphemeralState.getDefaultInstance()) } + + private fun showInputInteractions(binding: StateFragmentBinding) { + val gaeCustomizationArgs: Any? = customizationArgsMap.values + if (interactionInstanceId.equals("MultipleChoiceInput")) { + val gaeCustomArgsInString: String = gaeCustomizationArgs.toString().replace("[", "").replace("]", "") + items = gaeCustomArgsInString.split(",").toTypedArray() + interactionAdapter = InteractionAdapter(context, entityType, entityId, items, interactionInstanceId); + binding.rvInteractions.adapter = interactionAdapter + + } else if (interactionInstanceId.equals("ItemSelectionInput") || interactionInstanceId.equals("SingleChoiceInput")) { + val gaeCustomArgsInString: String = gaeCustomizationArgs.toString().replace("[", "").replace("]", "") + items = gaeCustomArgsInString.split(",").toTypedArray() + interactionAdapter = InteractionAdapter(context, entityType, entityId, items, interactionInstanceId); + binding.rvInteractions.adapter = interactionAdapter + } else { + //Do no show any view + } + } } diff --git a/app/src/main/res/layout/item_selection_interaction_items.xml b/app/src/main/res/layout/item_selection_interaction_items.xml new file mode 100755 index 00000000000..ab26c54878a --- /dev/null +++ b/app/src/main/res/layout/item_selection_interaction_items.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/multiple_choice_interaction_items.xml b/app/src/main/res/layout/multiple_choice_interaction_items.xml new file mode 100755 index 00000000000..3e1ebe50a29 --- /dev/null +++ b/app/src/main/res/layout/multiple_choice_interaction_items.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/state_fragment.xml b/app/src/main/res/layout/state_fragment.xml index 4c30178d521..c301ec9b3c4 100755 --- a/app/src/main/res/layout/state_fragment.xml +++ b/app/src/main/res/layout/state_fragment.xml @@ -1,7 +1,8 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + +