From b93dce4b913c9e0a28b6ea950b4f87e5ec6031b1 Mon Sep 17 00:00:00 2001
From: Brayan Oliveira <69634269+brayandso@users.noreply.github.com>
Date: Fri, 22 Dec 2023 18:48:44 -0300
Subject: [PATCH] perf/refactor: move reviewer's asset loading from server to
WebViewAssetLoader
---
AnkiDroid/src/main/assets/card_template.html | 14 ++--
.../com/ichi2/anki/AbstractFlashcardViewer.kt | 69 ++++++++++---------
.../java/com/ichi2/anki/CollectionHelper.kt | 4 ++
.../java/com/ichi2/anki/ReviewerServer.kt | 54 +--------------
4 files changed, 50 insertions(+), 91 deletions(-)
diff --git a/AnkiDroid/src/main/assets/card_template.html b/AnkiDroid/src/main/assets/card_template.html
index ea9fe4613cae..d413a032a6f1 100644
--- a/AnkiDroid/src/main/assets/card_template.html
+++ b/AnkiDroid/src/main/assets/card_template.html
@@ -3,17 +3,17 @@
AnkiDroid Flashcard
-
-
+
+
-
+
::script::
-
-
-
-
+
+
+
+
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
index 1f086c6138b2..0d11d5e059d2 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
@@ -69,6 +69,7 @@ import com.ichi2.anki.dialogs.tags.TagsDialog
import com.ichi2.anki.dialogs.tags.TagsDialogFactory
import com.ichi2.anki.dialogs.tags.TagsDialogListener
import com.ichi2.anki.model.CardStateFilter
+import com.ichi2.anki.pages.AnkiServer.Companion.LOCALHOST
import com.ichi2.anki.pages.CongratsPage
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.anki.receiver.SdCardReceiver
@@ -129,10 +130,7 @@ abstract class AbstractFlashcardViewer :
private var mTtsInitialized = false
private var mReplayOnTtsInit = false
private var mAnkiDroidJsAPI: AnkiDroidJsAPI? = null
- var server: ReviewerServer? = null
-
- /** Can be used to wait until async calls in onCreate() have finished. */
- var asyncCreateJob: Job? = null
+ lateinit var server: ReviewerServer
/**
* Broadcast that informs us when the sd card is about to be unmounted
@@ -236,7 +234,6 @@ abstract class AbstractFlashcardViewer :
private set
private var mBaseUrl: String? = null
private var mViewerUrl: String? = null
- private var mAssetLoader: WebViewAssetLoader? = null
private val mFadeDuration = 300
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
@@ -545,8 +542,7 @@ abstract class AbstractFlashcardViewer :
setContentView(getContentViewAttr(fullscreenMode))
- val mediaDir = CollectionHelper.getMediaDirectory(this).path
- server = ReviewerServer(this@AbstractFlashcardViewer, mediaDir).apply { start() }
+ server = ReviewerServer(this@AbstractFlashcardViewer).apply { start() }
// Make ACTION_PROCESS_TEXT for in-app searching possible on > Android 4.0
delegate.isHandleNativeActionModesEnabled = true
@@ -581,23 +577,7 @@ abstract class AbstractFlashcardViewer :
mSoundPlayer = Sound(baseUrl)
mViewerUrl = baseUrl + "__viewer__.html"
}
- mAssetLoader = WebViewAssetLoader.Builder()
- .addPathHandler("/") { path: String ->
- try {
- val file = File(mediaDir, path)
- val inputStream = FileInputStream(file)
- val mimeType = guessMimeType(path)
- val headers = HashMap()
- headers["Access-Control-Allow-Origin"] = "*"
- val response = WebResourceResponse(mimeType, null, inputStream)
- response.responseHeaders = headers
- return@addPathHandler response
- } catch (e: Exception) {
- Timber.w(e, "Error trying to open path in asset loader")
- }
- null
- }
- .build()
+
registerExternalStorageListener()
restoreCollectionPreferences(col)
initLayout()
@@ -672,7 +652,9 @@ abstract class AbstractFlashcardViewer :
override fun onDestroy() {
super.onDestroy()
- server?.closeAllConnections()
+ if (this::server.isInitialized) {
+ server.closeAllConnections()
+ }
mTTS.releaseTts(this)
if (mUnmountReceiver != null) {
unregisterReceiver(mUnmountReceiver)
@@ -1076,8 +1058,28 @@ abstract class AbstractFlashcardViewer :
mTouchLayer!!.layoutParams = touchLayerContainerParams
}
- @SuppressLint("SetJavaScriptEnabled") // they request we review carefully because of XSS security, we have
protected open fun createWebView(): WebView {
+ val mediaDir = CollectionHelper.getMediaDirectory(this).path
+ val domain = "$LOCALHOST:${server.listeningPort}"
+ val assetLoader = WebViewAssetLoader.Builder()
+ .setHttpAllowed(true)
+ .setDomain(domain)
+ .addPathHandler("/web/") { path: String ->
+ val inputStream = this.javaClass.classLoader!!.getResourceAsStream("web/$path")
+ WebResourceResponse(guessMimeType(path), null, inputStream)
+ }
+ .addPathHandler("/") { path: String ->
+ try {
+ val file = File(mediaDir, path)
+ val inputStream = FileInputStream(file)
+ WebResourceResponse(guessMimeType(path), null, inputStream)
+ } catch (e: Exception) {
+ Timber.w(e, "Error trying to open path in asset loader: %s", path)
+ null
+ }
+ }
+ .build()
+
val webView: WebView = MyWebView(this).apply {
scrollBarStyle = View.SCROLLBARS_OUTSIDE_OVERLAY
with(settings) {
@@ -1093,7 +1095,7 @@ abstract class AbstractFlashcardViewer :
isFocusableInTouchMode = typeAnswer!!.autoFocus
isScrollbarFadingEnabled = true
setBackgroundColor(Color.argb(1, 0, 0, 0))
- webViewClient = CardViewerWebClient(mAssetLoader, this@AbstractFlashcardViewer)
+ webViewClient = CardViewerWebClient(assetLoader, this@AbstractFlashcardViewer)
}
Timber.d(
"Focusable = %s, Focusable in touch mode = %s",
@@ -1638,8 +1640,7 @@ abstract class AbstractFlashcardViewer :
Timber.w("fillFlashCard() called with no card content")
return
}
- val cardContent = cardContent!!
- processCardAction { cardWebView: WebView? -> loadContentIntoCard(cardWebView, cardContent) }
+ processCardAction { cardWebView: WebView? -> loadContentIntoCard(cardWebView, cardContent!!) }
mGestureDetectorImpl.onFillFlashcard()
if (!displayAnswer) {
updateForNewCard()
@@ -1648,12 +1649,16 @@ abstract class AbstractFlashcardViewer :
private fun loadContentIntoCard(card: WebView?, content: String) {
launchCatchingTask {
- asyncCreateJob?.join()
- server?.reviewerHtml = content
if (card != null) {
card.settings.mediaPlaybackRequiresUserGesture = !mCardSoundConfig!!.autoplay
Timber.v("*** set server %s content to %s", server, content)
- card.loadUrl(server?.baseUrl() + "reviewer.html")
+ card.loadDataWithBaseURL(
+ server.baseUrl(),
+ content,
+ "text/html",
+ null,
+ null
+ )
}
}
}
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt
index 1e1228c23f3f..97f3e102e862 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/CollectionHelper.kt
@@ -481,6 +481,10 @@ open class CollectionHelper {
return getCurrentAnkiDroidDirectoryOptionalContext(context.sharedPrefs()) { context }
}
+ fun getMediaDirectory(context: Context): File {
+ return File(getCurrentAnkiDroidDirectory(context), "collection.media")
+ }
+
/**
* An accessor which makes [Context] optional in the case that [PREF_COLLECTION_PATH] is set
*
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt
index d23ef852c7e7..99796dc937cf 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/ReviewerServer.kt
@@ -18,15 +18,9 @@ package com.ichi2.anki
import anki.frontend.SetSchedulingStatesRequest
import com.ichi2.anki.pages.AnkiServer
-import com.ichi2.anki.pages.RangeHeader
-import com.ichi2.anki.pages.toInputStream
-import com.ichi2.utils.AssetHelper
import timber.log.Timber
-import java.io.File
-import java.io.FileInputStream
-class ReviewerServer(activity: AbstractFlashcardViewer, private val mediaDir: String) : AnkiServer(activity) {
- var reviewerHtml: String = ""
+class ReviewerServer(activity: AbstractFlashcardViewer) : AnkiServer(activity) {
private val jsApi = activity.javaScriptFunction()
override fun start() {
@@ -37,51 +31,7 @@ class ReviewerServer(activity: AbstractFlashcardViewer, private val mediaDir: St
override fun serve(session: IHTTPSession): Response {
val uri = session.uri
Timber.d("${session.method} $uri")
- if (session.method == Method.GET) {
- if (uri == "/reviewer.html") {
- return newFixedLengthResponse(reviewerHtml)
- }
- if (uri.startsWith("/assets/")) {
- val mime = getMimeFromUri(uri)
- val stream = when (uri) {
- "/assets/reviewer_extras_bundle.js" ->
- this.javaClass.classLoader!!.getResourceAsStream("web/reviewer_extras_bundle.js")
- "/assets/reviewer_extras.css" ->
- this.javaClass.classLoader!!.getResourceAsStream("web/reviewer_extras.css")
- else ->
- this.javaClass.classLoader!!.getResourceAsStream(uri.substring(1))
- }
- if (stream != null) {
- Timber.v("OK: $uri")
- return newChunkedResponse(Response.Status.OK, mime, stream)
- }
- }
-
- // fall back to looking in media folder
- val file = File(mediaDir, uri.substring(1))
- if (file.exists()) {
- val rangeHeader = RangeHeader.from(session, defaultEnd = file.length() - 1)
- val mimeType = AssetHelper.guessMimeType(uri)
- return if (rangeHeader != null) {
- val (start, end) = rangeHeader
- newFixedLengthResponse(
- Response.Status.PARTIAL_CONTENT,
- mimeType,
- file.toInputStream(rangeHeader),
- rangeHeader.contentLength
- ).apply {
- addHeader("Content-Range", "bytes $start-$end/${file.length()}")
- addHeader("Accept-Ranges", "bytes")
- }
- } else {
- val inputStream = FileInputStream(file)
- Timber.v("OK: $uri")
- newChunkedResponse(Response.Status.OK, mimeType, inputStream)
- }
- // probably don't need this anymore
- // resp.addHeader("Access-Control-Allow-Origin", "*")
- }
- } else if (session.method == Method.POST) {
+ if (session.method == Method.POST) {
val inputBytes = getSessionBytes(session)
if (uri.startsWith(ANKI_PREFIX)) {
return buildResponse {