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 #161: Exploration player contentcard supports rich-text part -2 #229

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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 @@ -7,6 +7,7 @@ import org.oppia.app.home.HomeFragment
import org.oppia.app.player.exploration.ExplorationFragment
import org.oppia.app.player.state.StateFragment
import org.oppia.app.player.audio.AudioFragment
import org.oppia.app.player.content.ContentListFragment
import org.oppia.app.testing.BindableAdapterTestFragment
import org.oppia.app.topic.TopicFragment
import org.oppia.app.topic.conceptcard.ConceptCardFragment
Expand All @@ -28,6 +29,7 @@ interface FragmentComponent {

fun inject(audioFragment: AudioFragment)
fun inject(conceptCardFragment: ConceptCardFragment)
fun inject(contentListFragment: ContentListFragment)
fun inject(explorationFragment: ExplorationFragment)
fun inject(homeFragment: HomeFragment)
fun inject(questionPlayerFragment: QuestionPlayerFragment)
Expand Down
108 changes: 108 additions & 0 deletions app/src/main/java/org/oppia/app/player/content/ContentCardAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.oppia.app.player.content

import android.content.Context
import android.text.Spannable
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.databinding.library.baseAdapters.BR
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.content_card_item.view.*
import kotlinx.android.synthetic.main.interation_feedback_card_item.view.*
import org.oppia.app.R
import org.oppia.app.databinding.ContentCardItemBinding
import org.oppia.app.databinding.InterationFeedbackCardItemBinding
import org.oppia.util.data.HtmlParser

// TODO(#216): Make use of generic data-binding-enabled RecyclerView adapter
/** Adapter to bind the contents to the [RecyclerView]. It handles rich-text content. */
class ContentCardAdapter(
private val context: Context,
private val entityType: String,
private val entityId: String,
val contentList: MutableList<ContentViewModel>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

val VIEW_TYPE_CONTENT = 1
val VIEW_TYPE_INTERACTION_FEEDBACK = 2

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_CONTENT -> {
val inflater = LayoutInflater.from(parent.getContext())
val binding =
DataBindingUtil.inflate<ContentCardItemBinding>(
inflater,
R.layout.content_card_item,
parent, /* attachToParent= */
false
)
ContentViewHolder(binding, context, entityType, entityId)
}
VIEW_TYPE_INTERACTION_FEEDBACK -> {
val inflater = LayoutInflater.from(parent.getContext())
val binding =
DataBindingUtil.inflate<InterationFeedbackCardItemBinding>(
inflater,
R.layout.interation_feedback_card_item,
parent,
/* attachToParent= */false
)
InteractionFeedbackViewHolder(binding, context, entityType, entityId)
}
else -> throw IllegalArgumentException("Invalid view type")
Copy link
Member

Choose a reason for hiding this comment

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

I suggest including the viewType parameter in this exception cause since it'll help provide context if the exception is ever thrown.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't understand what viewType parameter you mean. Can you please elaborate on this.
We have only two viewTypes over here one is content and other is learner's interaction.

}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
VIEW_TYPE_CONTENT -> (holder as ContentViewHolder).bind(contentList!!.get(position).htmlContent)
VIEW_TYPE_INTERACTION_FEEDBACK -> (holder as InteractionFeedbackViewHolder).bind(contentList!!.get(position).htmlContent)
}
}

// Determines the appropriate ViewType according to the content_id.
override fun getItemViewType(position: Int): Int {
val contentId = contentList.get(position).contentId
return if (!contentId.contains("content") && !contentId.contains("Feedback")) {
VIEW_TYPE_INTERACTION_FEEDBACK
} else {
VIEW_TYPE_CONTENT
}
}

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

private class ContentViewHolder(
val binding: ViewDataBinding,
private val context: Context,
private val entityType: String,
private val entityId: String
) : RecyclerView.ViewHolder(binding.root) {
internal fun bind(rawString: String?) {
binding.setVariable(BR.htmlContent, rawString)
binding.executePendingBindings();
val html: Spannable = HtmlParser(context, entityType, entityId).parseHtml(rawString, binding.root.tv_contents)
binding.root.tv_contents.text = html
}
}

private class InteractionFeedbackViewHolder(
val binding: ViewDataBinding,
private val context: Context,
private val entityType: String,
private val entityId: String
) :
RecyclerView.ViewHolder(binding.root) {
internal fun bind(rawString: String?) {
binding.setVariable(BR.htmlContent, rawString)
binding.executePendingBindings();
val html: Spannable =
HtmlParser(context, entityType, entityId).parseHtml(rawString, binding.root.tv_interaction_feedback)
binding.root.tv_interaction_feedback.text = html
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.oppia.app.player.content

import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button

import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.oppia.app.R
import org.oppia.app.fragment.InjectableFragment
import org.oppia.data.backends.gae.NetworkModule
import org.oppia.data.backends.gae.model.GaeExplorationContainer
import org.oppia.data.backends.gae.model.GaeState
import org.oppia.data.backends.gae.model.GaeSubtitledHtml
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.lang.Exception

import java.util.ArrayList
import javax.inject.Inject

/** Fragment that displays contents that supports rich-text. */
class ContentListFragment : InjectableFragment() {

@Inject
lateinit var contentListFragmentPresenter: ContentListFragmentPresenter

override fun onAttach(context: Context?) {
super.onAttach(context)
fragmentComponent.inject(this)
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return contentListFragmentPresenter.handleCreateView(inflater, container)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.oppia.app.player.content

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.ContentListFragmentBinding
import org.oppia.app.model.EphemeralState
import org.oppia.app.player.state.StateViewModel
import org.oppia.app.viewmodel.ViewModelProvider
import org.oppia.domain.exploration.ExplorationProgressController
import org.oppia.util.data.AsyncResult
import org.oppia.util.logging.Logger
import javax.inject.Inject

/** Presenter for [ContentListFragment]. */
class ContentListFragmentPresenter @Inject constructor(
private val fragment: Fragment,
private val viewModelProvider: ViewModelProvider<ContentViewModel>,
private val explorationProgressController: ExplorationProgressController,
private val logger: Logger
) {

private var entityType: String = ""
private var entityId: String = ""

lateinit var contentCardAdapter: ContentCardAdapter

var contentList: MutableList<ContentViewModel> = ArrayList()

fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
val binding = ContentListFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)

entityType = fragment.arguments!!.getString("entityType")
entityId = fragment.arguments!!.getString("exploration_id")

binding.recyclerview.apply {
contentCardAdapter =
ContentCardAdapter(context, entityType, entityId, contentList);
binding.recyclerview.adapter = contentCardAdapter
}
subscribeToCurrentState()

return binding.root
}

private fun subscribeToCurrentState() {
ephemeralStateLiveData.observe(fragment, Observer<EphemeralState> { result ->
logger.d("StateFragment", "getCurrentState: ${result.state.content.html}")
if (!result.state.content.contentId.equals("")) {
getContentViewModel().contentId = result.state.content.contentId
}else{
getContentViewModel().contentId = "content"
}
getContentViewModel().htmlContent = result.state.content.html
bindContentList()
})
}

private fun bindContentList() {
contentList.add(getContentViewModel())
contentCardAdapter.notifyDataSetChanged()
}

private fun getContentViewModel(): ContentViewModel {
return viewModelProvider.getForFragment(fragment, ContentViewModel::class.java)
}

private val ephemeralStateLiveData: LiveData<EphemeralState> by lazy {
getEphemeralState()
}

private fun getEphemeralState(): LiveData<EphemeralState> {
return Transformations.map(explorationProgressController.getCurrentState(), ::processCurrentState)
}

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())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.oppia.app.player.content

import androidx.databinding.ObservableField
import androidx.lifecycle.ViewModel
import org.oppia.app.fragment.FragmentScope
import javax.inject.Inject

/** [ViewModel] for content-card state. */
@FragmentScope
class ContentViewModel @Inject constructor() : ViewModel() {

var contentId = ""
var htmlContent = ""
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package org.oppia.app.player.state

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.CellularDataPreference
import org.oppia.app.model.EphemeralState
import org.oppia.app.player.audio.CellularDataDialogFragment
import org.oppia.app.player.content.ContentListFragment
import org.oppia.app.viewmodel.ViewModelProvider
import org.oppia.domain.audio.CellularDialogController
import org.oppia.domain.exploration.ExplorationProgressController
Expand All @@ -33,6 +36,8 @@ class StateFragmentPresenter @Inject constructor(

private var showCellularDataDialog = true
private var useCellularData = false
private val dummyExploration_id: String = "umPkwp0L1M0-"
private val entityType: String = "exploration"

fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
cellularDialogController.getCellularDataPreference()
Expand Down Expand Up @@ -94,7 +99,16 @@ class StateFragmentPresenter @Inject constructor(

private fun subscribeToCurrentState() {
ephemeralStateLiveData.observe(fragment, Observer<EphemeralState> { result ->
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
logger.d("StateFragment", "getCurrentState: ${result.state.name}")
logger.d("StateFragment", "getCurrentState: ${result.state}")
val contentListFragment = ContentListFragment()
val args = Bundle()
args.putString("entityType", entityType)
args.putString("exploration_id", dummyExploration_id)
contentListFragment.arguments = args
fragment.childFragmentManager.beginTransaction().add(
R.id.content_list_fragment_placeholder,
contentListFragment
).commitNow()
})
}

Expand Down
22 changes: 22 additions & 0 deletions app/src/main/res/layout/content_card_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="htmlContent"
type="String"/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:background="@drawable/bg_blue_card"
android:orientation="vertical">
<TextView
android:id="@+id/tv_contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:gravity="left"
android:padding="8dp"
android:text="@{htmlContent}"/>
</FrameLayout>
</layout>
23 changes: 23 additions & 0 deletions app/src/main/res/layout/content_list_fragment.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".player.content.ContentListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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"
veena14cs marked this conversation as resolved.
Show resolved Hide resolved
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
25 changes: 25 additions & 0 deletions app/src/main/res/layout/interation_feedback_card_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android">
Copy link
Member

Choose a reason for hiding this comment

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

For file name, should this be interaction_feedback_card_item?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How shall I name it then, it relates to the learners interaction that should be shown on the right side.

<data>
<variable
name="htmlContent"
type="String"/>
</data>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_interaction_feedback"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:background="@drawable/bg_white_card"
android:gravity="right|center"
android:padding="8dp"
android:text="@{htmlContent}"/>
</FrameLayout>
</layout>
Loading