diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e435dfc707..e7ef3e04575 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,4 +35,4 @@ android:theme="@style/SplashScreenTheme" /> - \ No newline at end of file + 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 6a041f9e040..8aaa1da7745 100644 --- a/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt +++ b/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt @@ -4,6 +4,7 @@ import android.app.Application import dagger.BindsInstance import dagger.Component import org.oppia.app.activity.ActivityComponent +import org.oppia.util.parser.HtmlParserEntityTypeModule import org.oppia.data.backends.gae.NetworkModule import org.oppia.domain.classify.InteractionsModule import org.oppia.domain.classify.rules.continueinteraction.ContinueModule @@ -28,7 +29,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, HtmlParsingModule::class, ImageLoaderModule::class + GcsResourceModule::class, ImageParsingModule::class, HtmlParsingModule::class, ImageLoaderModule::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 index 00356009f9d..9c606fcde33 100644 --- 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,5 +1,6 @@ package org.oppia.app.player.state +import android.text.Spannable import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil @@ -14,6 +15,7 @@ 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 @@ -28,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() { @@ -47,18 +52,19 @@ class StateAdapter( /* attachToParent= */false ) StateButtonViewHolder(binding, buttonInteractionListener) - } VIEW_TYPE_CONTENT -> { + } + VIEW_TYPE_CONTENT -> { val inflater = LayoutInflater.from(parent.context) val binding = DataBindingUtil.inflate( inflater, R.layout.content_item, parent, - /* attachToParent= */false + /* attachToParent= */ false ) ContentViewHolder(binding) } - else -> throw IllegalArgumentException("Invalid view type") as Throwable + else -> throw IllegalArgumentException("Invalid view type") } } @@ -68,8 +74,8 @@ class StateAdapter( (holder as StateButtonViewHolder).bind(itemList[position] as StateButtonViewModel) } VIEW_TYPE_CONTENT -> { - (holder as ContentViewHolder).bind((itemList[position] as ContentViewModel).htmlContent) - } + (holder as ContentViewHolder).bind((itemList[position] as ContentViewModel).htmlContent) + } } } @@ -87,13 +93,19 @@ class StateAdapter( override fun getItemCount(): Int { return itemList.size } + inner class ContentViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) { - internal fun bind(rawString: String?) { + internal fun bind(rawString: String) { binding.setVariable(BR.htmlContent, rawString) binding.executePendingBindings() - binding.root.content_text_view.text = rawString + 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 c12eb395c17..a43b1f3c8b1 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 @@ -30,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" @@ -54,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, @@ -61,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 @@ -92,9 +96,8 @@ 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 @@ -103,7 +106,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() @@ -331,7 +333,7 @@ class StateFragmentPresenter @Inject constructor( } } private fun checkAndAddContentItem() { - if (currentEphemeralState!!.state.hasContent()) { + if (currentEphemeralState.state.hasContent()) { addContentItem() } else { logger.e("StateFragment", "checkAndAddContentItem: State does not have content.") @@ -340,7 +342,7 @@ class StateFragmentPresenter @Inject constructor( private fun addContentItem() { val contentViewModel = ContentViewModel() - val contentSubtitledHtml: SubtitledHtml = currentEphemeralState!!.state.content + val contentSubtitledHtml: SubtitledHtml = currentEphemeralState.state.content if (contentSubtitledHtml.contentId != "") { contentViewModel.contentId = contentSubtitledHtml.contentId } else { diff --git a/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentContentCardTest.kt b/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentContentCardTest.kt new file mode 100755 index 00000000000..88cc5d778f8 --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentContentCardTest.kt @@ -0,0 +1,31 @@ +package org.oppia.app.player.state + +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.app.R +import org.oppia.app.home.HomeActivity +import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition + +// TODO(#205): Add test case for image parsing once PR #205 is merged. +/** Tests for [VIEW_TYPE_CONTENT]. */ +@RunWith(AndroidJUnit4::class) +class StateFragmentContentCardTest { + + @Test + fun testContentCard_forDemoExploration_withCustomOppiaTags_displaysParsedHtml() { + ActivityScenario.launch(HomeActivity::class.java).use { + onView(withId(R.id.play_exploration_button)).perform(click()) + 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?\n\n" + onView(atPosition(R.id.state_recycler_view, 0)).check(matches(hasDescendant(withText(htmlResult)))) + } + } +} 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 100644 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 100644 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" + } +}