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 #153/#154 : MultipleChoice/ItemSelection Interaction View #225

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e19af2b
Introduce first pass interface for ExplorationProgressController.
BenHenning Sep 25, 2019
43766b0
Fill in the stubbed logic for ExplorationProgressController. Still no
BenHenning Sep 26, 2019
c8eaa66
Fix lateinit issue in ExplorationProgressController due to wrongly or…
BenHenning Sep 27, 2019
a0eb3ce
Fix a variaty of issues in the exp progress controller, properly hook…
BenHenning Sep 29, 2019
1ea9d01
Merge branch 'develop' into introduce-exploration-progress-controller
BenHenning Sep 29, 2019
41141b6
Created a separate ExplorationRetriever, hooked up
BenHenning Oct 1, 2019
ccbac0e
Merge branch 'develop' into introduce-exploration-progress-controller
BenHenning Oct 1, 2019
5f326fc
Change the locking mechanism for ExplorationProgressController to work
BenHenning Oct 3, 2019
b5b7485
Finish tests for ExplorationProgressController and add test classific…
BenHenning Oct 4, 2019
a3c4667
Merge branch 'develop' into introduce-exploration-progress-controller
BenHenning Oct 4, 2019
4a9aad1
ObservableViewModel
Oct 7, 2019
38c0275
Merge remote-tracking branch 'upstream/introduce-exploration-progress…
Oct 7, 2019
0705586
Load exploration and open ExplorationActivity
Oct 7, 2019
d38bbe1
Test case for load exploration
Oct 7, 2019
f337b70
fetch exploration
veena14cs Oct 7, 2019
0fb1729
Connection with StateFragment
Oct 7, 2019
d04f66a
update
veena14cs Oct 7, 2019
ca0c356
update
veena14cs Oct 7, 2019
a3c7d55
Merge branch 'introduce-load-exploration-part-1' into exploration-pla…
veena14cs Oct 7, 2019
fe5ab82
working on fetching customization_args
veena14cs Oct 11, 2019
e45fb0a
Update StateFragmentPresenter.kt
veena14cs Oct 11, 2019
86dc073
resolved conflicts
veena14cs Oct 11, 2019
c6bbfdb
Separated this in its own PR
veena14cs Oct 11, 2019
0edaca1
Delete UrlImageParser.kt
veena14cs Oct 11, 2019
9f02fba
Update StateFragmentPresenter.kt
veena14cs Oct 11, 2019
af8610b
Update build.gradle
veena14cs Oct 11, 2019
e5e351c
working on fetching custom_args
veena14cs Oct 15, 2019
ad00bdd
Merge branch 'develop' into exploration-player-content-interaction
veena14cs Oct 16, 2019
0b379fc
Merge branch 'develop' into exploration-player-content-interaction
veena14cs Oct 16, 2019
5bdedfc
fixing issues
veena14cs Oct 16, 2019
c481727
Update InteractionAdapter.kt
veena14cs Oct 16, 2019
ea1dc85
fixed issues
veena14cs Oct 16, 2019
1b6a02d
Included in separate PR #226
veena14cs Oct 16, 2019
b798236
Included in separate Pr #205
veena14cs Oct 16, 2019
08cd62c
fixed spacing
veena14cs Oct 16, 2019
8797aae
Merge branch 'exploration-player-content-interaction' of https://gith…
veena14cs Oct 16, 2019
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: 3 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
128 changes: 128 additions & 0 deletions app/src/main/java/org/oppia/app/player/state/InteractionAdapter.kt
Original file line number Diff line number Diff line change
@@ -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<String>?,
val interactionInstanceId: String?
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

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<MultipleChoiceInteractionItemsBinding>(
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<ItemSelectionInteractionItemsBinding>(
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()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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<StateViewModel>,
Expand All @@ -39,6 +44,14 @@ class StateFragmentPresenter @Inject constructor(
private var useCellularData = false
private var explorationId: String? = null

private var items: Array<String>? = null
var customizationArgsMap = HashMap<String, InteractionObject>()
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<AsyncResult<CellularDataPreference>> {
Expand Down Expand Up @@ -121,7 +134,19 @@ class StateFragmentPresenter @Inject constructor(

private fun subscribeToCurrentState() {
ephemeralStateLiveData.observe(fragment, Observer<EphemeralState> { result ->
logger.d("StateFragment", "getCurrentState: ${result.state.name}")
logger.d(TAG_STATE_FRAGMENT, "getCurrentState: ${result.state.name}")
entityType = "exploration"
val customizationArgsMap: Map<String, InteractionObject> = result.state.interaction.customizationArgsMap
val allKeys: Set<String> = 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}")
}
})
}

Expand All @@ -135,8 +160,26 @@ class StateFragmentPresenter @Inject constructor(

private fun processCurrentState(ephemeralStateResult: AsyncResult<EphemeralState>): 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
}
}
}
28 changes: 28 additions & 0 deletions app/src/main/res/layout/item_selection_interaction_items.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="htmlContent"
type="String"/>
</data>
<RelativeLayout
android:id="@+id/rl_checkbox_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/cb_item_selection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
app:buttonTint="@color/oppiaDarkBlue"/>
<TextView
android:id="@+id/tv_item_selection_contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/cb_item_selection"
android:padding="4dp"
android:text="@{htmlContent}"
android:textColor="@color/oppiaDarkBlue"/>
</RelativeLayout>
</layout>
28 changes: 28 additions & 0 deletions app/src/main/res/layout/multiple_choice_interaction_items.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="htmlContent"
type="String"/>
</data>
<RelativeLayout
android:id="@+id/rl_radio_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/rb_multiple_choice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
app:buttonTint="@color/oppiaDarkBlue"/>
<TextView
android:id="@+id/tv_multiple_choice_contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/rb_multiple_choice"
android:padding="4dp"
android:text="@{htmlContent}"
android:textColor="@color/oppiaDarkBlue"/>
</RelativeLayout>
</layout>
29 changes: 26 additions & 3 deletions app/src/main/res/layout/state_fragment.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View"/>
<variable
Expand All @@ -23,6 +24,29 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<LinearLayout
android:id="@+id/interaction_container_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@color/white"
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintBottom_toTopOf="@+id/dummy_audio_button"
app:layout_constraintTop_toBottomOf="@+id/audio_fragment_placeholder"
tools:layout_editor_absoluteX="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvInteractions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:divider="@android:color/transparent"
android:dividerHeight="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</LinearLayout>
<Button
android:id="@+id/dummy_audio_button"
android:layout_width="wrap_content"
Expand All @@ -36,7 +60,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package org.oppia.app.player.state

import android.app.Application
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.onView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
Expand All @@ -16,6 +20,7 @@ import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.CoroutineDispatcher
import org.hamcrest.CoreMatchers.not
import org.junit.Test
Expand Down Expand Up @@ -158,6 +163,27 @@ class StateFragmentTest {
}
}

var interactionInstanceId: String? = null

@Test
fun testMultipleChoiceInput_showsRadioButtons_onMultipleChoiceInputInteractionInstanceId_userSelectsDesiredOption() {
ActivityScenario.launch(StateFragmentTestActivity::class.java).use {
interactionInstanceId = "MultipleChoiceInput"
assertEquals(interactionInstanceId, "MultipleChoiceInput")
onView(withId(R.id.rvInteractions)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(2,click()))
Copy link
Contributor

Choose a reason for hiding this comment

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

@BenHenning Veena has added this library 'androidx.test.espresso:espresso-contrib:3.1.0', which helps in testing recyclerview items. Can I use this in my test-cases related to RecyclerView, #204

Copy link
Contributor

Choose a reason for hiding this comment

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

@BenHenning @veena14cs
We should not use this library because this library only provide click actions and there is no mechanism to check the content of an item.

Th solution for this is in my PR #204

}
}

@Test
fun tesItemSelectionInput_showsCheckbox_onItemSelectionInputInteractionInstanceId_userCanSelectMoreThanOneCorrectOption() {
ActivityScenario.launch(StateFragmentTestActivity::class.java).use {
interactionInstanceId = "ItemSelectionInput"
assertEquals(interactionInstanceId, "ItemSelectionInput")
onView(withId(R.id.rvInteractions)).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(1,click()))
}
}
@Module
class TestModule {
@Provides
Expand Down