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 #2134: Fix concept card naming/linking #2980

Merged
merged 64 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
5e33b02
Add support for loading proto versions of lessons.
BenHenning Mar 23, 2021
a3d5f4a
Upgrade Espresso versions to work around build issues.
BenHenning Mar 23, 2021
a017e09
Merge branch 'fix-protobuf-duplication-issue' into add-support-for-pr…
BenHenning Mar 23, 2021
1815e06
Lint fix post-copying from #2927.
BenHenning Mar 23, 2021
97b28ef
Fix Glide in Bazel.
BenHenning Mar 23, 2021
6aadb95
Fix concept card name escaping.
BenHenning Mar 23, 2021
156b01e
Copy over test fix from #2927.
BenHenning Mar 23, 2021
37547c9
Remove image loading annotation.
BenHenning Mar 23, 2021
b33ff7a
Add caching module to needed test suites.
BenHenning Mar 23, 2021
a6a2121
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Mar 23, 2021
e8b8d1f
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Mar 23, 2021
441e2ae
Add support for importing text protos as assets.
BenHenning Mar 24, 2021
927508b
Add gitignore for Android Studio Bazel plugin output.
BenHenning Mar 24, 2021
d39e926
Import textproto versions of existing dev assets.
BenHenning Mar 24, 2021
9079ab6
Undo Espresso version change since it breaks Bazel tests.
BenHenning Mar 24, 2021
be5b785
Fix tests.
BenHenning Mar 25, 2021
34333eb
Use correct Espresso core.
BenHenning Mar 25, 2021
a691bcf
Move coroutine dispatchers.
BenHenning Apr 3, 2021
8ab7437
Refactor testing utilities build graph.
BenHenning Apr 3, 2021
a78b59d
Introduce dedicated test for TestCoroutineDispatcher.
BenHenning Apr 6, 2021
1e138b9
Add tests for TestCoroutineDispatcher.
BenHenning Apr 8, 2021
797ff85
Merge branch 'develop' into add-coroutine-dispatcher-tests
BenHenning Apr 8, 2021
b45b212
Workaround ktlint semicolon issue.
BenHenning Apr 8, 2021
5db9e82
Lint fixes.
BenHenning Apr 8, 2021
236a4f0
Fix test post-merge.
BenHenning Apr 9, 2021
54dc9bc
Merge branch 'develop' into add-coroutine-dispatcher-tests
BenHenning Apr 9, 2021
dc8be26
Move CoroutineDispatcher to be part of threading.
BenHenning Apr 9, 2021
f2ef2b2
Lint fixes.
BenHenning Apr 9, 2021
12d42dc
Merge branch 'develop' into fix-protobuf-duplication-issue
BenHenning Apr 12, 2021
b0eb428
Undo version upgrade.
BenHenning Apr 12, 2021
138b83a
Merge branch 'fix-protobuf-duplication-issue' into add-support-for-pr…
BenHenning Apr 12, 2021
8601fde
Fix broken tests.
BenHenning Apr 12, 2021
7f6ba6d
Lint fixes.
BenHenning Apr 12, 2021
6aa8dbf
Add a bit more proto loading test coverage.
BenHenning Apr 12, 2021
b2a116b
Comment fix.
BenHenning Apr 12, 2021
c6fa23c
Lint fixes.
BenHenning Apr 12, 2021
3fae8d3
Address earlier TODO.
BenHenning Apr 13, 2021
caed7a7
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 13, 2021
467e90b
Post-merge restructure.
BenHenning Apr 13, 2021
522c9b6
Merge branch 'develop' into add-support-for-proto-lesson-loading-from…
BenHenning Apr 13, 2021
6d16f2a
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 13, 2021
adab013
Post-merge fixes.
BenHenning Apr 13, 2021
38d797e
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 13, 2021
537ec36
Merge branch 'develop' into add-coroutine-dispatcher-tests
BenHenning Apr 13, 2021
5b48ada
Add tests for new tag handlers.
BenHenning Apr 14, 2021
4ab42a2
Fix broken test build.
BenHenning Apr 14, 2021
661da02
Merge branch 'add-coroutine-dispatcher-tests' into fix-glide-image-lo…
BenHenning Apr 14, 2021
4d8d5be
Post-merge lint fixes.
BenHenning Apr 14, 2021
5ae75e4
Merge branch 'add-coroutine-dispatcher-tests' into add-support-for-pr…
BenHenning Apr 14, 2021
76270d3
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 14, 2021
a5d8d17
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 14, 2021
5d40f2b
Lint fixes.
BenHenning Apr 14, 2021
c5ee89d
Address reviewer comment.
BenHenning Apr 14, 2021
39ab486
Post-merge fix.
BenHenning Apr 14, 2021
770252a
Fix typo.
BenHenning Apr 14, 2021
9e99d27
Merge branch 'develop' into add-coroutine-dispatcher-tests
BenHenning Apr 15, 2021
31f4f9b
Merge branch 'add-coroutine-dispatcher-tests' into add-support-for-pr…
BenHenning Apr 15, 2021
fe0cd33
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 15, 2021
bf1b6a6
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 15, 2021
3fddc33
Merge branch 'develop' into add-support-for-proto-lesson-loading-from…
BenHenning Apr 15, 2021
e6b41a9
Merge branch 'add-support-for-proto-lesson-loading-from-local-assets'…
BenHenning Apr 15, 2021
e5369b5
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 15, 2021
92b4cc3
Merge branch 'develop' into fix-glide-image-loading
BenHenning Apr 23, 2021
19ea66c
Merge branch 'fix-glide-image-loading' into fix-concept-card-name-ren…
BenHenning Apr 23, 2021
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
2 changes: 1 addition & 1 deletion app/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,6 @@ kt_android_library(
"src/sharedTest/java/org/oppia/android/app/utility/DrawableMatcher.kt",
"src/sharedTest/java/org/oppia/android/app/utility/EspressoTestsMatchers.kt",
"src/sharedTest/java/org/oppia/android/app/utility/FontSizeMatcher.kt",
"src/sharedTest/java/org/oppia/android/app/utility/MockitoKotlinHelper.kt",
"src/sharedTest/java/org/oppia/android/app/utility/OrientationChangeAction.kt",
"src/sharedTest/java/org/oppia/android/app/utility/ProgressMatcher.kt",
"src/sharedTest/java/org/oppia/android/app/utility/TabMatcher.kt",
Expand All @@ -684,6 +683,7 @@ TEST_DEPS = [
":test_deps",
"//domain",
"//testing",
"//testing/src/main/java/org/oppia/android/testing/mockito",
"//testing/src/main/java/org/oppia/android/testing/robolectric:is_on_robolectric",
"//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
"//testing/src/main/java/org/oppia/android/testing/threading:coroutine_executor_service",
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositi
import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.hasItemCount
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape
import org.oppia.android.app.utility.anyOrNull
import org.oppia.android.app.utility.capture
import org.oppia.android.domain.classify.InteractionsModule
import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule
import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
Expand All @@ -90,6 +88,8 @@ import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.android.domain.topic.TEST_STORY_ID_0
import org.oppia.android.domain.topic.TEST_TOPIC_ID_0
import org.oppia.android.testing.TestLogReportingModule
import org.oppia.android.testing.mockito.anyOrNull
import org.oppia.android.testing.mockito.capture
import org.oppia.android.testing.profile.ProfileTestHelper
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.story.StoryProgressTestHelper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import org.oppia.android.app.utility.DefaultRegionClickedEvent
import org.oppia.android.app.utility.NamedRegionClickedEvent
import org.oppia.android.app.utility.OnClickableAreaClickedListener
import org.oppia.android.app.utility.RegionClickedEvent
import org.oppia.android.app.utility.capture
import org.oppia.android.app.utility.clickPoint
import org.oppia.android.domain.classify.InteractionsModule
import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule
Expand All @@ -62,6 +61,7 @@ import org.oppia.android.domain.oppialogger.loguploader.WorkManagerConfiguration
import org.oppia.android.domain.question.QuestionModule
import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.android.testing.TestLogReportingModule
import org.oppia.android.testing.mockito.capture
import org.oppia.android.testing.robolectric.RobolectricModule
import org.oppia.android.testing.threading.TestDispatcherModule
import org.oppia.android.testing.time.FakeOppiaClockModule
Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions testing/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ load("//testing:testing_test.bzl", "testing_test")
# globs here to ensure new files added to migrated packages don't accidentally get included in the
# top-level module library.
MIGRATED_PROD_FILES = glob([
"src/main/java/org/oppia/android/testing/mockito/*.kt",
"src/main/java/org/oppia/android/testing/robolectric/*.kt",
"src/main/java/org/oppia/android/testing/threading/*.kt",
"src/main/java/org/oppia/android/testing/time/*.kt",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# TODO(#1532): Rename file to 'BUILD' post-Gradle.
"""
Package for Mockito-specific test utilities and helpers.
"""

load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library")

# Library for general-purpose testing fakes.
kt_android_library(
name = "mockito",
testonly = True,
srcs = [
"MockitoKotlinHelper.kt",
],
visibility = ["//:oppia_testing_visibility"],
deps = [
"//third_party:org_mockito_mockito-core",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.oppia.android.testing.mockito

import org.mockito.ArgumentCaptor
import org.mockito.Mockito.any

/**
* Returns ArgumentCaptor.capture() as a non-nullable type to avoid either an IllegalStateException
* or a NullPointerException (essentially making Mockito's argument captor Kotlin-compatible).
*
* This hacky solution is necessary since Kotlin doesn't allow null values to be passed in for
* non-nullable parameters, yet this is fundamentally how Mockito works internally when tracking
* captors.
*/
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()

/** Matches anything, including nulls. */
fun <T> anyOrNull(): T = any()
1 change: 1 addition & 0 deletions utility/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ TEST_DEPS = [
"//model",
"//model:test_models",
"//testing",
"//testing/src/main/java/org/oppia/android/testing/mockito",
"//testing/src/main/java/org/oppia/android/testing/robolectric:test_module",
"//testing/src/main/java/org/oppia/android/testing/threading:test_module",
"//testing/src/main/java/org/oppia/android/testing/time:test_module",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.oppia.android.util.parser

import android.text.Editable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ClickableSpan
import android.view.View
import org.oppia.android.util.logging.ConsoleLogger
import org.xml.sax.Attributes

/** The custom tag corresponding to [ConceptCardTagHandler]. */
const val CUSTOM_CONCEPT_CARD_TAG = "oppia-noninteractive-skillreview"

// https://mohammedlakkadshaw.com/blog/handling-custom-tags-in-android-using-html-taghandler.html/
class ConceptCardTagHandler(
private val listener: ConceptCardLinkClickListener,
private val consoleLogger: ConsoleLogger
) : CustomHtmlContentHandler.CustomTagHandler {
override fun handleTag(
attributes: Attributes,
openIndex: Int,
closeIndex: Int,
output: Editable,
imageRetriever: CustomHtmlContentHandler.ImageRetriever
) {
// Replace the custom tag with a clickable piece of text based on the tag's customizations.
val skillId = attributes.getJsonStringValue("skill_id-with-value")
val text = attributes.getJsonStringValue("text-with-value")
if (skillId != null && text != null) {
val spannableBuilder = SpannableStringBuilder(text)
spannableBuilder.setSpan(
object : ClickableSpan() {
override fun onClick(view: View) {
listener.onConceptCardLinkClicked(view, skillId)
}
},
/* start= */ 0, /* end= */ text.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
output.replace(openIndex, closeIndex, spannableBuilder)
} else consoleLogger.e("ConceptCardTagHandler", "Failed to parse concept card tag")
}

/** Listener called when concept card links are clicked. */
interface ConceptCardLinkClickListener {
/**
* Called when a concept card link is called in the specified view corresponding to the
* specified skill ID.
*/
fun onConceptCardLinkClicked(view: View, skillId: String)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.oppia.android.util.parser

import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.Html
import android.text.Spannable
Expand All @@ -17,7 +18,8 @@ import java.util.ArrayDeque
* This is based on the implementation provided in https://stackoverflow.com/a/36528149.
*/
class CustomHtmlContentHandler private constructor(
private val customTagHandlers: Map<String, CustomTagHandler>
private val customTagHandlers: Map<String, CustomTagHandler>,
private val imageRetriever: ImageRetriever
) : ContentHandler, Html.TagHandler {
private var originalContentHandler: ContentHandler? = null
private var currentTrackedTag: TrackedTag? = null
Expand Down Expand Up @@ -107,7 +109,8 @@ class CustomHtmlContentHandler private constructor(
}
val (_, attributes, openTagIndex) = currentTrackedCustomTag
customTagHandlers.getValue(tag).handleClosingTag(output)
customTagHandlers.getValue(tag).handleTag(attributes, openTagIndex, output.length, output)
customTagHandlers.getValue(tag)
.handleTag(attributes, openTagIndex, output.length, output, imageRetriever)
}
}
}
Expand All @@ -128,8 +131,16 @@ class CustomHtmlContentHandler private constructor(
* @param openIndex the index in the output [Editable] at which this tag begins
* @param closeIndex the index in the output [Editable] at which this tag ends
* @param output the destination [Editable] to which spans can be added
* @param imageRetriever a utility to load image drawables if needed by the handler
*/
fun handleTag(attributes: Attributes, openIndex: Int, closeIndex: Int, output: Editable) {}
fun handleTag(
attributes: Attributes,
openIndex: Int,
closeIndex: Int,
output: Editable,
imageRetriever: ImageRetriever
) {
}

/**
* Called when the opening of a custom tag is encountered. This does not support processing
Expand All @@ -152,25 +163,54 @@ class CustomHtmlContentHandler private constructor(
fun handleClosingTag(output: Editable) {}
}

/**
* Retriever of images for custom tag handlers. The built-in Android analog for this class is
* Html's ImageGetter.
*/
interface ImageRetriever {
/** Returns a new [Drawable] corresponding to the specified URL and [Type]. */
fun loadDrawable(sourceUrl: String, type: Type): Drawable

/** Corresponds to the types of images that can be retrieved. */
enum class Type {
/**
* Corresponds to a block image that should be positioned in a way that may break text, and
* potentially centered depending on the configuration of the implementation.
*/
BLOCK_IMAGE
}
}

companion object {
/**
* Returns a new [Spannable] with HTML parsed from [html] using the specified [imageGetter] for
* handling image retrieval, and map of tags to [CustomTagHandler]s for handling custom tags.
* All possible custom tags must be registered in the [customTagHandlers] map.
* Returns a new [Spannable] with HTML parsed from [html] using the specified [imageRetriever]
* for handling image retrieval, and map of tags to [CustomTagHandler]s for handling custom
* tags. All possible custom tags must be registered in the [customTagHandlers] map.
*/
fun fromHtml(
fun <T> fromHtml(
html: String,
imageGetter: Html.ImageGetter,
imageRetriever: T,
customTagHandlers: Map<String, CustomTagHandler>
): Spannable {
): Spannable where T : Html.ImageGetter, T : ImageRetriever {
// Adjust the HTML to allow the custom content handler to properly initialize custom tag
// tracking.
return HtmlCompat.fromHtml(
"<init-custom-handler/>$html",
HtmlCompat.FROM_HTML_MODE_LEGACY,
imageGetter,
CustomHtmlContentHandler(customTagHandlers)
imageRetriever,
CustomHtmlContentHandler(customTagHandlers, imageRetriever),
) as Spannable
}
}
}

/**
* Returns a string value from this [Attributes] object, but interpreted as a JSON string, or null
* if the corresponding value doesn't exist in this attributes map.
*/
fun Attributes.getJsonStringValue(name: String): String? {
// Note that the attribute is actually an encoded JSON string (so it has escaped quotes around
// it). Since it's only a source string, the quotes can simply be removed in order to extract
// the string value.
return getValue(name)?.replace("&quot;", "")
}
66 changes: 20 additions & 46 deletions utility/src/main/java/org/oppia/android/util/parser/HtmlParser.kt
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
package org.oppia.android.util.parser

import android.text.Editable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.widget.TextView
import org.xml.sax.Attributes
import org.oppia.android.util.logging.ConsoleLogger
import javax.inject.Inject

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"

private const val CUSTOM_CONCEPT_CARD_TAG = "oppia-noninteractive-skillreview"

/** Html Parser to parse custom Oppia tags with Android-compatible versions. */
class HtmlParser private constructor(
private val urlImageParserFactory: UrlImageParser.Factory,
private val gcsResourceName: String,
private val entityType: String,
private val entityId: String,
private val imageCenterAlign: Boolean,
private val consoleLogger: ConsoleLogger,
customOppiaTagActionListener: CustomOppiaTagActionListener?
) {
private val conceptCardTagHandler = ConceptCardTagHandler(customOppiaTagActionListener)
private val bulletTagHandler = BulletTagHandler()
private val conceptCardTagHandler by lazy {
ConceptCardTagHandler(
object : ConceptCardTagHandler.ConceptCardLinkClickListener {
override fun onConceptCardLinkClicked(view: View, skillId: String) {
customOppiaTagActionListener?.onConceptCardLinkClicked(view, skillId)
}
},
consoleLogger
)
}
private val bulletTagHandler by lazy { BulletTagHandler() }
private val imageTagHandler by lazy { ImageTagHandler(consoleLogger) }

/**
* Parses a raw HTML string with support for custom Oppia tags.
Expand Down Expand Up @@ -57,13 +59,6 @@ class HtmlParser private constructor(
.replace("</li>", "</$CUSTOM_BULLET_LIST_TAG>")
}

if (CUSTOM_IMG_TAG in htmlContent) {
htmlContent = htmlContent.replace(CUSTOM_IMG_TAG, REPLACE_IMG_TAG)
htmlContent =
htmlContent.replace(CUSTOM_IMG_FILE_PATH_ATTRIBUTE, REPLACE_IMG_FILE_PATH_ATTRIBUTE)
htmlContent = htmlContent.replace("&amp;quot;", "")
}

// https://stackoverflow.com/a/8662457
if (supportsLinks) {
htmlContentTextView.movementMethod = LinkMovementMethod.getInstance()
Expand All @@ -88,38 +83,13 @@ class HtmlParser private constructor(
): Map<String, CustomHtmlContentHandler.CustomTagHandler> {
val handlersMap = mutableMapOf<String, CustomHtmlContentHandler.CustomTagHandler>()
handlersMap[CUSTOM_BULLET_LIST_TAG] = bulletTagHandler
handlersMap[CUSTOM_IMG_TAG] = imageTagHandler
if (supportsConceptCards) {
handlersMap[CUSTOM_CONCEPT_CARD_TAG] = conceptCardTagHandler
}
return handlersMap
}

// https://mohammedlakkadshaw.com/blog/handling-custom-tags-in-android-using-html-taghandler.html/
private class ConceptCardTagHandler(
private val customOppiaTagActionListener: CustomOppiaTagActionListener?
) : CustomHtmlContentHandler.CustomTagHandler {
override fun handleTag(
attributes: Attributes,
openIndex: Int,
closeIndex: Int,
output: Editable
) {
// Replace the custom tag with a clickable piece of text based on the tag's customizations.
val skillId = attributes.getValue("skill_id-with-value")
val text = attributes.getValue("text-with-value")
val spannableBuilder = SpannableStringBuilder(text)
spannableBuilder.setSpan(
object : ClickableSpan() {
override fun onClick(view: View) {
customOppiaTagActionListener?.onConceptCardLinkClicked(view, skillId)
}
},
0, text.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
)
output.replace(openIndex, closeIndex, spannableBuilder)
}
}

private fun trimSpannable(spannable: SpannableStringBuilder): SpannableStringBuilder {
val trimmedText = spannable.toString()
val trimStart = if (trimmedText.startsWith("\n")) 1 else 0
Expand Down Expand Up @@ -150,7 +120,10 @@ class HtmlParser private constructor(
}

/** Factory for creating new [HtmlParser]s. */
class Factory @Inject constructor(private val urlImageParserFactory: UrlImageParser.Factory) {
class Factory @Inject constructor(
private val urlImageParserFactory: UrlImageParser.Factory,
private val consoleLogger: ConsoleLogger
) {
/**
* Returns a new [HtmlParser] with the specified entity type and ID for loading images, and an
* optionally specified [CustomOppiaTagActionListener] for handling custom Oppia tag events.
Expand All @@ -168,6 +141,7 @@ class HtmlParser private constructor(
entityType,
entityId,
imageCenterAlign,
consoleLogger,
customOppiaTagActionListener
)
}
Expand Down
Loading