diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8b199f77525..efad241eca8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -32,5 +32,6 @@
android:theme="@style/SplashScreenTheme">
+
diff --git a/app/src/main/java/org/oppia/app/testing/HtmlParserTestActivity.kt b/app/src/main/java/org/oppia/app/testing/HtmlParserTestActivity.kt
new file mode 100644
index 00000000000..efea356c7e3
--- /dev/null
+++ b/app/src/main/java/org/oppia/app/testing/HtmlParserTestActivity.kt
@@ -0,0 +1,25 @@
+package org.oppia.app.testing
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.text.Spannable
+import android.widget.TextView
+import org.oppia.app.R
+import org.oppia.util.parser.HtmlParser
+
+/** This is a dummy activity to test Html parsing. */
+class HtmlParserTestActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_test_html_parser)
+
+ val testHtmlContentTextView = findViewById(R.id.test_html_content_text_view) as TextView
+ val rawDummyString = "\u003cp\u003e\"Let's try one last question,\" said Mr. Baker. \"Here's a pineapple cake cut into pieces.\"\u003c/p\u003e\u003coppia-noninteractive-image alt-with-value=\"\u0026amp;quot;Pineapple cake with 7/9 having cherries.\u0026amp;quot;\" caption-with-value=\"\u0026amp;quot;\u0026amp;quot;\" filepath-with-value=\"\u0026amp;quot;pineapple_cake_height_479_width_480.png\u0026amp;quot;\"\u003e\u003c/oppia-noninteractive-image\u003e\u003cp\u003e\u00a0\u003c/p\u003e\u003cp\u003e\u003cstrong\u003eQuestion 6\u003c/strong\u003e: What fraction of the cake has big red cherries in the pineapple slices?\u003c/p\u003e"
+ val htmlResult: Spannable = HtmlParser(applicationContext, /* entityType= */ "", /* entityId= */ "").parseOppiaHtml(
+ rawDummyString,
+ testHtmlContentTextView
+ )
+ testHtmlContentTextView.text = htmlResult
+ }
+}
diff --git a/app/src/main/res/layout/activity_test_html_parser.xml b/app/src/main/res/layout/activity_test_html_parser.xml
new file mode 100644
index 00000000000..aeb5d7a348f
--- /dev/null
+++ b/app/src/main/res/layout/activity_test_html_parser.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/sharedTest/java/org/oppia/app/testing/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/app/testing/HtmlParserTest.kt
new file mode 100644
index 00000000000..82e95fa70bb
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/app/testing/HtmlParserTest.kt
@@ -0,0 +1,85 @@
+package org.oppia.app.testing
+
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.text.Spannable
+import android.widget.TextView
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import junit.framework.TestCase.assertNotSame
+import org.hamcrest.Matchers.not
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.app.R
+import org.oppia.util.parser.HtmlParser
+
+/** Tests for [HtmlParser]. */
+@RunWith(AndroidJUnit4::class)
+class HtmlParserTest {
+
+ private lateinit var launchedActivity: Activity
+
+ @get:Rule
+ var activityTestRule: ActivityTestRule = ActivityTestRule(
+ HtmlParserTestActivity::class.java, /* initialTouchMode= */ true, /* launchActivity= */ false
+ )
+
+ @Before
+ fun setUp() {
+ Intents.init()
+ val intent = Intent(Intent.ACTION_PICK)
+ launchedActivity = activityTestRule.launchActivity(intent)
+ }
+
+ @Test
+ fun testHtmlContent_handleCustomOppiaTags_parsedHtmldisplaysStyledText() {
+ activityTestRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ activityTestRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+
+ val textView = activityTestRule.activity.findViewById(R.id.test_html_content_text_view) as TextView
+ val htmlResult: Spannable = HtmlParser(
+ ApplicationProvider.getApplicationContext(), /* entityType= */ "", /* entityId= */ ""
+ ).parseOppiaHtml(
+ "\u003cp\u003e\"Let's try one last question,\" said Mr. Baker. \"Here's a pineapple cake cut into pieces.\"\u003c/p\u003e\u003coppia-noninteractive-image alt-with-value=\"\u0026amp;quot;Pineapple cake with 7/9 having cherries.\u0026amp;quot;\" caption-with-value=\"\u0026amp;quot;\u0026amp;quot;\" filepath-with-value=\"\u0026amp;quot;pineapple_cake_height_479_width_480.png\u0026amp;quot;\"\u003e\u003c/oppia-noninteractive-image\u003e\u003cp\u003e\u00a0\u003c/p\u003e\u003cp\u003e\u003cstrong\u003eQuestion 6\u003c/strong\u003e: What fraction of the cake has big red cherries in the pineapple slices?\u003c/p\u003e", textView
+ )
+ assertThat(textView.text.toString()).isEqualTo(htmlResult.toString())
+ onView(withId(R.id.test_html_content_text_view)).check(matches(isDisplayed()))
+ onView(withId(R.id.test_html_content_text_view)).check(matches(withText(textView.text.toString())))
+ }
+
+ @Test
+ fun testHtmlContent_nonCustomOppiaTags_notParsed() {
+ activityTestRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ activityTestRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+
+ val textView = activityTestRule.activity.findViewById(R.id.test_html_content_text_view) as TextView
+ val htmlResult: Spannable = HtmlParser(
+ ApplicationProvider.getApplicationContext(), /* entityType= */ "", /* entityId= */ ""
+ ).parseOppiaHtml(
+ "\u003cp\u003e\"Let's try one last question,\" said Mr. Baker. \"Here's a pineapple cake cut into pieces.\"\u003c/p\u003e\u003coppia--image alt-with-value=\"\u0026amp;quot;Pineapple cake with 7/9 having cherries.\u0026amp;quot;\" caption-with-value=\"\u0026amp;quot;\u0026amp;quot;\" filepath-value=\"\u0026amp;quot;pineapple_cake_height_479_width_480.png\u0026amp;quot;\"\u003e\u003c/oppia-noninteractive-image\u003e\u003cp\u003e\u00a0\u003c/p\u003e\u003cp\u003e\u003cstrongQuestion 6\u003c/strong\u003e: What fraction of the cake has big red cherries in the pineapple slices?\u003c/p\u003e",
+ textView
+ )
+ // The two strings aren't equal because this HTML contains a Non-Oppia/Non-Html tag e.g. tag and attributes "filepath-value" which isn't parsed.
+ assertThat(textView.text.toString()).isNotEqualTo(htmlResult.toString())
+ onView(withId(R.id.test_html_content_text_view)).check(matches(not(textView.text.toString())))
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ }
+}
diff --git a/utility/src/main/java/org/oppia/util/parser/HtmlParser.kt b/utility/src/main/java/org/oppia/util/parser/HtmlParser.kt
new file mode 100644
index 00000000000..cc774b19d9c
--- /dev/null
+++ b/utility/src/main/java/org/oppia/util/parser/HtmlParser.kt
@@ -0,0 +1,40 @@
+package org.oppia.util.parser
+
+import android.content.Context
+import android.text.Html
+import android.text.Spannable
+import android.widget.TextView
+
+private const val CUSTOM_IMG_TAG = "oppia-noninteractive-image"
+private const val REPLACE_IMG_TAG = "img"
+private const val CUSTOM_IMG_FILE_PATH_ATTRIBUTE = "filepath-with-value"
+private const val REPLACE_IMG_FILE_PATH_ATTRIBUTE = "src"
+
+/** Html Parser to parse custom Oppia tags with Android-compatible versions. */
+class HtmlParser(private val context: Context, private val entityType: String, private val entityId: String) {
+
+ /**
+ * This method replaces custom Oppia tags with Android-compatible versions for a given raw HTML string, and returns the HTML [Spannable].
+ * @param rawString rawString argument is the string from the string-content
+ * @param htmlContentTextView htmlContentTextView argument is the TextView, that need to be passed as argument to ImageGetter class for image parsing
+ * @return Spannable Spannable represents the styled text.
+ */
+ fun parseOppiaHtml(rawString: String, htmlContentTextView: TextView): Spannable {
+ var htmlContent = rawString
+ if (htmlContent.contains(CUSTOM_IMG_TAG)) {
+ htmlContent = htmlContent.replace(CUSTOM_IMG_TAG, REPLACE_IMG_TAG, /* ignoreCase= */false);
+ htmlContent = htmlContent.replace(
+ CUSTOM_IMG_FILE_PATH_ATTRIBUTE,
+ REPLACE_IMG_FILE_PATH_ATTRIBUTE, /* ignoreCase= */false
+ );
+ htmlContent = htmlContent.replace(""", "")
+ }
+ // TODO(#205): Integrate UrlImageParser below once it's available.
+ return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
+ Html.fromHtml(htmlContent, Html.FROM_HTML_MODE_LEGACY, /* imageGetter= */null, /* tagHandler= */null) as Spannable
+ } else {
+ Html.fromHtml(htmlContent, /* imageGetter= */null, /* tagHandler= */null) as Spannable
+ }
+
+ }
+}