Refactor usage WebView for parsers

This commit is contained in:
Koitharu
2025-08-24 10:39:23 +03:00
parent fd5aca7252
commit 1cc51b6a88
2 changed files with 64 additions and 46 deletions

View File

@@ -0,0 +1,58 @@
package org.koitharu.kotatsu.core.network.webview
import android.content.Context
import android.webkit.WebView
import androidx.annotation.MainThread
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import java.lang.ref.WeakReference
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Singleton
class WebViewExecutor @Inject constructor(
@ApplicationContext private val context: Context
) {
private var webViewCached: WeakReference<WebView>? = null
private val mutex = Mutex()
suspend fun evaluateJs(baseUrl: String?, script: String): String? = mutex.withLock {
withContext(Dispatchers.Main.immediate) {
val webView = obtainWebView()
if (!baseUrl.isNullOrEmpty()) {
suspendCoroutine { cont ->
webView.webViewClient = ContinuationResumeWebViewClient(cont)
webView.loadDataWithBaseURL(baseUrl, " ", "text/html", null, null)
}
}
suspendCoroutine { cont ->
webView.evaluateJavascript(script) { result ->
cont.resume(result?.takeUnless { it == "null" })
}
}
}
}
@MainThread
fun getDefaultUserAgent() = runCatching {
obtainWebView().settings.userAgentString.sanitizeHeaderValue().trim().nullIfEmpty()
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrNull()
@MainThread
private fun obtainWebView(): WebView = webViewCached?.get() ?: WebView(context).also {
it.configureForParser(null)
webViewCached = WeakReference(it)
}
}

View File

@@ -3,15 +3,10 @@ package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.Base64 import android.util.Base64
import android.webkit.WebView
import androidx.annotation.MainThread
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -22,11 +17,8 @@ import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.network.webview.ContinuationResumeWebViewClient import org.koitharu.kotatsu.core.network.webview.WebViewExecutor
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
import org.koitharu.kotatsu.core.util.ext.toList import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.core.util.ext.toMimeType import org.koitharu.kotatsu.core.util.ext.toMimeType
import org.koitharu.kotatsu.core.util.ext.use import org.koitharu.kotatsu.core.util.ext.use
@@ -37,25 +29,21 @@ import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.map import org.koitharu.kotatsu.parsers.util.map
import java.lang.ref.WeakReference
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Singleton @Singleton
class MangaLoaderContextImpl @Inject constructor( class MangaLoaderContextImpl @Inject constructor(
@MangaHttpClient override val httpClient: OkHttpClient, @MangaHttpClient override val httpClient: OkHttpClient,
override val cookieJar: MutableCookieJar, override val cookieJar: MutableCookieJar,
@ApplicationContext private val androidContext: Context, @ApplicationContext private val androidContext: Context,
private val webViewExecutor: WebViewExecutor,
) : MangaLoaderContext() { ) : MangaLoaderContext() {
private var webViewCached: WeakReference<WebView>? = null
private val webViewUserAgent by lazy { obtainWebViewUserAgent() } private val webViewUserAgent by lazy { obtainWebViewUserAgent() }
private val jsMutex = Mutex()
private val jsTimeout = TimeUnit.SECONDS.toMillis(4) private val jsTimeout = TimeUnit.SECONDS.toMillis(4)
@Deprecated("Provide a base url") @Deprecated("Provide a base url")
@@ -63,22 +51,7 @@ class MangaLoaderContextImpl @Inject constructor(
override suspend fun evaluateJs(script: String): String? = evaluateJs("", script) override suspend fun evaluateJs(script: String): String? = evaluateJs("", script)
override suspend fun evaluateJs(baseUrl: String, script: String): String? = withTimeout(jsTimeout) { override suspend fun evaluateJs(baseUrl: String, script: String): String? = withTimeout(jsTimeout) {
jsMutex.withLock { webViewExecutor.evaluateJs(baseUrl, script)
withContext(Dispatchers.Main.immediate) {
val webView = obtainWebView()
if (baseUrl.isNotEmpty()) {
suspendCoroutine { cont ->
webView.webViewClient = ContinuationResumeWebViewClient(cont)
webView.loadDataWithBaseURL(baseUrl, " ", "text/html", null, null)
}
}
suspendCoroutine { cont ->
webView.evaluateJavascript(script) { result ->
cont.resume(result?.takeUnless { it == "null" })
}
}
}
}
} }
override fun getDefaultUserAgent(): String = webViewUserAgent override fun getDefaultUserAgent(): String = webViewUserAgent
@@ -119,27 +92,14 @@ class MangaLoaderContextImpl @Inject constructor(
override fun createBitmap(width: Int, height: Int): Bitmap = BitmapWrapper.create(width, height) override fun createBitmap(width: Int, height: Int): Bitmap = BitmapWrapper.create(width, height)
@MainThread
private fun obtainWebView(): WebView = webViewCached?.get() ?: WebView(androidContext).also {
it.configureForParser(null)
webViewCached = WeakReference(it)
}
private fun obtainWebViewUserAgent(): String { private fun obtainWebViewUserAgent(): String {
val mainDispatcher = Dispatchers.Main.immediate val mainDispatcher = Dispatchers.Main.immediate
return if (!mainDispatcher.isDispatchNeeded(EmptyCoroutineContext)) { return if (!mainDispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
obtainWebViewUserAgentImpl() webViewExecutor.getDefaultUserAgent()
} else { } else {
runBlocking(mainDispatcher) { runBlocking(mainDispatcher) {
obtainWebViewUserAgentImpl() webViewExecutor.getDefaultUserAgent()
} }
} } ?: UserAgents.FIREFOX_MOBILE
} }
@MainThread
private fun obtainWebViewUserAgentImpl() = runCatching {
obtainWebView().settings.userAgentString.sanitizeHeaderValue()
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
} }