diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7398e376a00..5128677c47d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,7 @@
android:supportsRtl="true"
android:theme="@style/OppiaTheme">
+
diff --git a/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt b/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt
index 94a16331fa5..d12e457a66d 100644
--- a/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt
+++ b/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt
@@ -12,6 +12,7 @@ import org.oppia.app.player.state.testing.StateFragmentTestActivity
import org.oppia.app.profile.ProfileActivity
import org.oppia.app.story.StoryActivity
import org.oppia.app.testing.BindableAdapterTestActivity
+import org.oppia.app.testing.ContentCardTestActivity
import org.oppia.app.testing.HtmlParserTestActivity
import org.oppia.app.topic.TopicActivity
import org.oppia.app.topic.questionplayer.QuestionPlayerActivity
@@ -32,6 +33,7 @@ interface ActivityComponent {
fun inject(audioFragmentTestActivity: AudioFragmentTestActivity)
fun inject(bindableAdapterTestActivity: BindableAdapterTestActivity)
fun inject(conceptCardFragmentTestActivity: ConceptCardFragmentTestActivity)
+ fun inject(contentCardTestActivity: ContentCardTestActivity)
fun inject(explorationActivity: ExplorationActivity)
fun inject(homeActivity: HomeActivity)
fun inject(htmlParserTestActivty: HtmlParserTestActivity)
diff --git a/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt
index 2c2a18900bd..0a1083c5012 100644
--- a/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt
+++ b/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt
@@ -16,6 +16,7 @@ import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
import org.oppia.util.parser.GlideImageLoaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
import org.oppia.util.threading.DispatcherModule
import javax.inject.Provider
@@ -27,7 +28,8 @@ import javax.inject.Singleton
ApplicationModule::class, DispatcherModule::class, NetworkModule::class, LoggerModule::class,
ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class,
NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, InteractionsModule::class,
- GcsResourceModule::class, ImageParsingModule::class, GlideImageLoaderModule::class
+ GcsResourceModule::class, ImageParsingModule::class, GlideImageLoaderModule::class,
+ HtmlParserEntityTypeModule::class
])
interface ApplicationComponent {
@Component.Builder
diff --git a/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt b/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt
old mode 100644
new mode 100755
index 411bbb39ab8..9c606fcde33
--- a/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt
+++ b/app/src/main/java/org/oppia/app/player/state/StateAdapter.kt
@@ -1,16 +1,21 @@
package org.oppia.app.player.state
+import android.text.Spannable
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.content_item.view.*
import kotlinx.android.synthetic.main.state_button_item.view.*
import org.oppia.app.R
+import org.oppia.app.databinding.ContentItemBinding
import org.oppia.app.player.state.itemviewmodel.StateButtonViewModel
import org.oppia.app.player.state.listener.ButtonInteractionListener
import org.oppia.app.databinding.StateButtonItemBinding
+import org.oppia.app.player.state.itemviewmodel.ContentViewModel
+import org.oppia.util.parser.HtmlParser
@Suppress("unused")
private const val VIEW_TYPE_CONTENT = 1
@@ -25,7 +30,10 @@ private const val VIEW_TYPE_STATE_BUTTON = 5
/** Adapter to inflate different items/views inside [RecyclerView]. The itemList consists of various ViewModels. */
class StateAdapter(
private val itemList: MutableList,
- private val buttonInteractionListener: ButtonInteractionListener
+ private val buttonInteractionListener: ButtonInteractionListener,
+ private val htmlParserFactory: HtmlParser.Factory,
+ private val entityType: String,
+ private val explorationId: String
) :
RecyclerView.Adapter() {
@@ -45,7 +53,18 @@ class StateAdapter(
)
StateButtonViewHolder(binding, buttonInteractionListener)
}
- else -> throw IllegalArgumentException("Invalid view type") as Throwable
+ VIEW_TYPE_CONTENT -> {
+ val inflater = LayoutInflater.from(parent.context)
+ val binding =
+ DataBindingUtil.inflate(
+ inflater,
+ R.layout.content_item,
+ parent,
+ /* attachToParent= */ false
+ )
+ ContentViewHolder(binding)
+ }
+ else -> throw IllegalArgumentException("Invalid view type")
}
}
@@ -54,11 +73,15 @@ class StateAdapter(
VIEW_TYPE_STATE_BUTTON -> {
(holder as StateButtonViewHolder).bind(itemList[position] as StateButtonViewModel)
}
+ VIEW_TYPE_CONTENT -> {
+ (holder as ContentViewHolder).bind((itemList[position] as ContentViewModel).htmlContent)
+ }
}
}
override fun getItemViewType(position: Int): Int {
return when (itemList[position]) {
+ is ContentViewModel -> VIEW_TYPE_CONTENT
is StateButtonViewModel -> {
stateButtonViewModel = itemList[position] as StateButtonViewModel
VIEW_TYPE_STATE_BUTTON
@@ -71,6 +94,18 @@ class StateAdapter(
return itemList.size
}
+ inner class ContentViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
+ internal fun bind(rawString: String) {
+ binding.setVariable(BR.htmlContent, rawString)
+ binding.executePendingBindings()
+ val htmlResult: Spannable = htmlParserFactory.create(entityType, explorationId).parseOppiaHtml(
+ rawString,
+ binding.root.content_text_view
+ )
+ binding.root.content_text_view.text = htmlResult
+ }
+ }
+
private class StateButtonViewHolder(
val binding: ViewDataBinding,
private val buttonInteractionListener: ButtonInteractionListener
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 032a6ec739f..5c38516e94e 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
@@ -17,9 +17,11 @@ 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.SubtitledHtml
import org.oppia.app.player.audio.AudioFragment
import org.oppia.app.player.audio.CellularDataDialogFragment
import org.oppia.app.player.exploration.ExplorationActivity
+import org.oppia.app.player.state.itemviewmodel.ContentViewModel
import org.oppia.app.player.state.itemviewmodel.StateButtonViewModel
import org.oppia.app.player.state.listener.ButtonInteractionListener
import org.oppia.app.viewmodel.ViewModelProvider
@@ -28,6 +30,8 @@ import org.oppia.domain.exploration.ExplorationDataController
import org.oppia.domain.exploration.ExplorationProgressController
import org.oppia.util.data.AsyncResult
import org.oppia.util.logging.Logger
+import org.oppia.util.parser.ExplorationHtmlParserEntityType
+import org.oppia.util.parser.HtmlParser
import javax.inject.Inject
const val STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY = "STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY"
@@ -52,6 +56,7 @@ private const val DEFAULT_CONTINUE_INTERACTION_TEXT_ANSWER = "Please continue."
/** The presenter for [StateFragment]. */
@FragmentScope
class StateFragmentPresenter @Inject constructor(
+ @ExplorationHtmlParserEntityType private val entityType: String,
private val activity: AppCompatActivity,
private val fragment: Fragment,
private val cellularDialogController: CellularDialogController,
@@ -59,7 +64,8 @@ class StateFragmentPresenter @Inject constructor(
private val viewModelProvider: ViewModelProvider,
private val explorationDataController: ExplorationDataController,
private val explorationProgressController: ExplorationProgressController,
- private val logger: Logger
+ private val logger: Logger,
+ private val htmlParserFactory: HtmlParser.Factory
) : ButtonInteractionListener {
private var showCellularDataDialog = true
@@ -90,9 +96,9 @@ class StateFragmentPresenter @Inject constructor(
useCellularData = prefs.useCellularData
}
})
-
- stateAdapter = StateAdapter(itemList, this as ButtonInteractionListener)
-
+ explorationId = fragment.arguments!!.getString(STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY)!!
+ stateAdapter =
+ StateAdapter(itemList, this as ButtonInteractionListener, htmlParserFactory, entityType, explorationId)
binding = StateFragmentBinding.inflate(inflater, container, /* attachToRoot= */ false)
binding.stateRecyclerView.apply {
adapter = stateAdapter
@@ -101,7 +107,6 @@ class StateFragmentPresenter @Inject constructor(
it.stateFragment = fragment as StateFragment
it.viewModel = getStateViewModel()
}
- explorationId = checkNotNull(fragment.arguments!!.getString(STATE_FRAGMENT_EXPLORATION_ID_ARGUMENT_KEY))
subscribeToCurrentState()
@@ -171,7 +176,7 @@ class StateFragmentPresenter @Inject constructor(
ephemeralStateLiveData.observe(fragment, Observer { result ->
itemList.clear()
currentEphemeralState = result
-
+ checkAndAddContentItem()
updateDummyStateName()
val interactionId = result.state.interaction.id
@@ -328,6 +333,23 @@ class StateFragmentPresenter @Inject constructor(
oldStateNameList.add(currentEphemeralState.state.name)
}
}
+
+ private fun checkAndAddContentItem() {
+ if (currentEphemeralState.state.hasContent()) {
+ addContentItem()
+ } else {
+ logger.e("StateFragment", "checkAndAddContentItem: State does not have content.")
+ }
+ }
+
+ private fun addContentItem() {
+ val contentViewModel = ContentViewModel()
+ val contentSubtitledHtml: SubtitledHtml = currentEphemeralState.state.content
+ contentViewModel.contentId = contentSubtitledHtml.contentId
+ contentViewModel.htmlContent = contentSubtitledHtml.html
+ itemList.add(contentViewModel)
+ stateAdapter.notifyDataSetChanged()
+ }
private fun updateNavigationButtonVisibility(
interactionId: String,
diff --git a/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContentViewModel.kt b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContentViewModel.kt
new file mode 100755
index 00000000000..8606d70ae38
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/player/state/itemviewmodel/ContentViewModel.kt
@@ -0,0 +1,12 @@
+package org.oppia.app.player.state.itemviewmodel
+
+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 = ""
+}
diff --git a/app/src/main/java/org/oppia/app/testing/ContentCardTestActivity.kt b/app/src/main/java/org/oppia/app/testing/ContentCardTestActivity.kt
new file mode 100644
index 00000000000..fe9d06058a1
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/testing/ContentCardTestActivity.kt
@@ -0,0 +1,17 @@
+package org.oppia.app.testing
+
+import android.os.Bundle
+import org.oppia.app.activity.InjectableAppCompatActivity
+import javax.inject.Inject
+
+/** Activity to test the functionality of content-card used in [StateFragment]. */
+class ContentCardTestActivity : InjectableAppCompatActivity() {
+ @Inject
+ lateinit var contentCardTestPresenter: ContentCardTestActivityPresenter
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ activityComponent.inject(this)
+ contentCardTestPresenter.handleOnCreate()
+ }
+}
diff --git a/app/src/main/java/org/oppia/app/testing/ContentCardTestActivityPresenter.kt b/app/src/main/java/org/oppia/app/testing/ContentCardTestActivityPresenter.kt
new file mode 100644
index 00000000000..0357856bd11
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/testing/ContentCardTestActivityPresenter.kt
@@ -0,0 +1,53 @@
+package org.oppia.app.testing
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.Observer
+import org.oppia.app.R
+import org.oppia.app.activity.ActivityScope
+import org.oppia.app.player.state.StateFragment
+import org.oppia.domain.exploration.ExplorationDataController
+import org.oppia.domain.exploration.TEST_EXPLORATION_ID_5
+import org.oppia.util.data.AsyncResult
+import org.oppia.util.logging.Logger
+import javax.inject.Inject
+
+private const val EXPLORATION_ID = TEST_EXPLORATION_ID_5
+
+/** The presenter for [ContentCardTestActivity]. */
+@ActivityScope
+class ContentCardTestActivityPresenter @Inject constructor(
+ private val activity: AppCompatActivity,
+ private val explorationDataController: ExplorationDataController,
+ private val logger: Logger
+) {
+ fun handleOnCreate() {
+ activity.setContentView(R.layout.content_card_test_activity)
+ loadDummyExplorationAtStart()
+ }
+
+ private fun getStateFragment(): StateFragment? {
+ return activity.supportFragmentManager.findFragmentById(R.id.state_fragment_placeholder) as StateFragment?
+ }
+
+ private fun loadDummyExplorationAtStart() {
+ explorationDataController.startPlayingExploration(
+ EXPLORATION_ID
+ ).observe(activity, Observer> { result ->
+ when {
+ result.isPending() -> logger.d("ContentCardTest", "Loading exploration")
+ result.isFailure() -> logger.e("ContentCardTest", "Failed to load exploration", result.getErrorOrNull()!!)
+ else -> {
+ logger.d("ContentCardTest", "Successfully loaded exploration")
+
+ if (getStateFragment() == null) {
+ val stateFragment = StateFragment.newInstance(EXPLORATION_ID)
+ activity.supportFragmentManager.beginTransaction().add(
+ R.id.state_fragment_placeholder,
+ stateFragment
+ ).commitNow()
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/app/src/main/res/layout/content_card_test_activity.xml b/app/src/main/res/layout/content_card_test_activity.xml
new file mode 100644
index 00000000000..74477fcf688
--- /dev/null
+++ b/app/src/main/res/layout/content_card_test_activity.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/app/src/main/res/layout/content_item.xml b/app/src/main/res/layout/content_item.xml
new file mode 100755
index 00000000000..5c410682691
--- /dev/null
+++ b/app/src/main/res/layout/content_item.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt
index 5dc9e2b48ed..c5ef4ce8276 100644
--- a/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt
@@ -23,6 +23,7 @@ import dagger.Component
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.CoroutineDispatcher
+import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Before
@@ -35,6 +36,7 @@ import org.oppia.app.player.exploration.ExplorationActivity
import org.oppia.app.player.state.testing.StateFragmentTestActivity
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
+import org.oppia.app.testing.ContentCardTestActivity
import org.oppia.util.threading.BackgroundDispatcher
import org.oppia.util.threading.BlockingDispatcher
import javax.inject.Singleton
@@ -364,6 +366,16 @@ class StateFragmentTest {
intended(hasComponent(HomeActivity::class.java.name))
}
+ @Test
+ fun testContentCard_forDemoExploration_withCustomOppiaTags_displaysParsedHtml() {
+ ActivityScenario.launch(ContentCardTestActivity::class.java).use {
+ val htmlResult =
+ "Hi, welcome to Oppia! is a tool that helps you create interactive learning activities that can be continually improved over time.\n\n" +
+ "Incidentally, do you know where the name 'Oppia' comes from?"
+ onView(atPositionOnView(R.id.state_recycler_view, 0,R.id.content_text_view)).check(matches(hasDescendant(withText(htmlResult))))
+ }
+ }
+
@After
fun tearDown() {
Intents.release()
diff --git a/utility/src/main/java/org/oppia/util/parser/ExplorationHtmlParserEntityType.kt b/utility/src/main/java/org/oppia/util/parser/ExplorationHtmlParserEntityType.kt
new file mode 100755
index 00000000000..779fbc5e55e
--- /dev/null
+++ b/utility/src/main/java/org/oppia/util/parser/ExplorationHtmlParserEntityType.kt
@@ -0,0 +1,7 @@
+package org.oppia.util.parser
+
+import javax.inject.Qualifier
+
+/** Qualifier for injecting the entity type for exploration. */
+@Qualifier
+annotation class ExplorationHtmlParserEntityType
diff --git a/utility/src/main/java/org/oppia/util/parser/HtmlParserEntityTypeModule.kt b/utility/src/main/java/org/oppia/util/parser/HtmlParserEntityTypeModule.kt
new file mode 100755
index 00000000000..bb33a097778
--- /dev/null
+++ b/utility/src/main/java/org/oppia/util/parser/HtmlParserEntityTypeModule.kt
@@ -0,0 +1,14 @@
+package org.oppia.util.parser
+
+import dagger.Module
+import dagger.Provides
+
+/** Provides Html parsing entity type dependencies. */
+@Module
+class HtmlParserEntityTypeModule {
+ @Provides
+ @ExplorationHtmlParserEntityType
+ fun provideExplorationHtmlParserEntityType(): String {
+ return "exploration"
+ }
+}