|
@@ -5,11 +5,14 @@ package io.legado.app.ui.rss.read
|
|
|
import android.annotation.SuppressLint
|
|
import android.annotation.SuppressLint
|
|
|
import android.app.Activity
|
|
import android.app.Activity
|
|
|
import android.content.Context
|
|
import android.content.Context
|
|
|
|
|
+import android.content.pm.ActivityInfo
|
|
|
import android.graphics.Bitmap
|
|
import android.graphics.Bitmap
|
|
|
import android.net.Uri
|
|
import android.net.Uri
|
|
|
import android.net.http.SslError
|
|
import android.net.http.SslError
|
|
|
import android.os.SystemClock
|
|
import android.os.SystemClock
|
|
|
import android.view.View
|
|
import android.view.View
|
|
|
|
|
+import android.webkit.ConsoleMessage
|
|
|
|
|
+import android.webkit.CookieManager
|
|
|
import android.webkit.JavascriptInterface
|
|
import android.webkit.JavascriptInterface
|
|
|
import android.webkit.SslErrorHandler
|
|
import android.webkit.SslErrorHandler
|
|
|
import android.webkit.URLUtil
|
|
import android.webkit.URLUtil
|
|
@@ -19,7 +22,6 @@ import android.webkit.WebResourceResponse
|
|
|
import android.webkit.WebSettings
|
|
import android.webkit.WebSettings
|
|
|
import android.webkit.WebView
|
|
import android.webkit.WebView
|
|
|
import android.webkit.WebViewClient
|
|
import android.webkit.WebViewClient
|
|
|
-import androidx.appcompat.app.AlertDialog
|
|
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
|
import androidx.core.graphics.createBitmap
|
|
import androidx.core.graphics.createBitmap
|
|
|
import androidx.core.net.toUri
|
|
import androidx.core.net.toUri
|
|
@@ -28,8 +30,13 @@ import com.script.rhino.runScriptWithContext
|
|
|
import io.legado.app.R
|
|
import io.legado.app.R
|
|
|
import io.legado.app.constant.AppConst
|
|
import io.legado.app.constant.AppConst
|
|
|
import io.legado.app.constant.AppLog
|
|
import io.legado.app.constant.AppLog
|
|
|
|
|
+import io.legado.app.exception.NoStackTraceException
|
|
|
import io.legado.app.help.WebCacheManager
|
|
import io.legado.app.help.WebCacheManager
|
|
|
import io.legado.app.help.config.AppConfig
|
|
import io.legado.app.help.config.AppConfig
|
|
|
|
|
+import io.legado.app.help.http.CookieStore
|
|
|
|
|
+import io.legado.app.help.http.newCallResponse
|
|
|
|
|
+import io.legado.app.help.http.okHttpClient
|
|
|
|
|
+import io.legado.app.help.http.text
|
|
|
import io.legado.app.help.webView.WebJsExtensions
|
|
import io.legado.app.help.webView.WebJsExtensions
|
|
|
import io.legado.app.lib.dialogs.SelectItem
|
|
import io.legado.app.lib.dialogs.SelectItem
|
|
|
import io.legado.app.lib.dialogs.selector
|
|
import io.legado.app.lib.dialogs.selector
|
|
@@ -40,25 +47,35 @@ import io.legado.app.utils.isTrue
|
|
|
import io.legado.app.utils.longSnackbar
|
|
import io.legado.app.utils.longSnackbar
|
|
|
import io.legado.app.utils.openUrl
|
|
import io.legado.app.utils.openUrl
|
|
|
import io.legado.app.utils.setDarkeningAllowed
|
|
import io.legado.app.utils.setDarkeningAllowed
|
|
|
|
|
+import io.legado.app.utils.splitNotBlank
|
|
|
import io.legado.app.utils.startActivity
|
|
import io.legado.app.utils.startActivity
|
|
|
import io.legado.app.utils.toastOnUi
|
|
import io.legado.app.utils.toastOnUi
|
|
|
|
|
+import kotlinx.coroutines.Dispatchers.IO
|
|
|
|
|
+import kotlinx.coroutines.runBlocking
|
|
|
|
|
+import okhttp3.MediaType
|
|
|
import java.io.ByteArrayInputStream
|
|
import java.io.ByteArrayInputStream
|
|
|
import java.net.URLDecoder
|
|
import java.net.URLDecoder
|
|
|
|
|
+import java.util.regex.PatternSyntaxException
|
|
|
|
|
|
|
|
internal data class RssReadWebControllerCallbacks(
|
|
internal data class RssReadWebControllerCallbacks(
|
|
|
val onProgressChanged: (Int) -> Unit,
|
|
val onProgressChanged: (Int) -> Unit,
|
|
|
val onPageTitleResolved: (String) -> Unit,
|
|
val onPageTitleResolved: (String) -> Unit,
|
|
|
val onShowCustomView: (View?, WebChromeClient.CustomViewCallback?) -> Unit,
|
|
val onShowCustomView: (View?, WebChromeClient.CustomViewCallback?) -> Unit,
|
|
|
val onHideCustomView: () -> Unit,
|
|
val onHideCustomView: () -> Unit,
|
|
|
- val navigateToArticles: (String?) -> Unit
|
|
|
|
|
|
|
+ val navigateToArticles: (sortUrl: String?, origin: String?) -> Unit,
|
|
|
|
|
+ val onAskRedirect: (String?, String, (Boolean) -> Unit) -> Unit,
|
|
|
|
|
+ val onCloseRequested: () -> Unit,
|
|
|
|
|
+ val isFullscreenProvider: () -> Boolean,
|
|
|
) : WebJsExtensions.Callback {
|
|
) : WebJsExtensions.Callback {
|
|
|
override fun upConfig(config: String) = Unit
|
|
override fun upConfig(config: String) = Unit
|
|
|
|
|
|
|
|
- override fun onNavigateToArticles(sortUrl: String?) {
|
|
|
|
|
- navigateToArticles(sortUrl)
|
|
|
|
|
|
|
+ override fun onNavigateToArticles(sortUrl: String?, origin: String?) {
|
|
|
|
|
+ navigateToArticles(sortUrl, origin)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+private val webCookieManager: CookieManager by lazy { CookieManager.getInstance() }
|
|
|
|
|
+
|
|
|
@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
|
|
@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
|
|
|
internal fun configureRssReadWebView(
|
|
internal fun configureRssReadWebView(
|
|
|
webView: VisibleWebView,
|
|
webView: VisibleWebView,
|
|
@@ -67,7 +84,6 @@ internal fun configureRssReadWebView(
|
|
|
appCompatActivity: AppCompatActivity?,
|
|
appCompatActivity: AppCompatActivity?,
|
|
|
viewModel: ReadRssViewModel,
|
|
viewModel: ReadRssViewModel,
|
|
|
initialTitle: String?,
|
|
initialTitle: String?,
|
|
|
- isStartPage: Boolean,
|
|
|
|
|
redirectPolicyProvider: () -> RedirectPolicy,
|
|
redirectPolicyProvider: () -> RedirectPolicy,
|
|
|
callbacks: RssReadWebControllerCallbacks
|
|
callbacks: RssReadWebControllerCallbacks
|
|
|
) {
|
|
) {
|
|
@@ -87,6 +103,26 @@ internal fun configureRssReadWebView(
|
|
|
override fun onHideCustomView() {
|
|
override fun onHideCustomView() {
|
|
|
callbacks.onHideCustomView()
|
|
callbacks.onHideCustomView()
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
|
|
|
|
+ val source = viewModel.rssSource
|
|
|
|
|
+ if (source?.showWebLog == true) {
|
|
|
|
|
+ val messageLevel = consoleMessage.messageLevel().name
|
|
|
|
|
+ val message = consoleMessage.message()
|
|
|
|
|
+ AppLog.put(
|
|
|
|
|
+ "${source.getTag()}${messageLevel}: $message",
|
|
|
|
|
+ NoStackTraceException(
|
|
|
|
|
+ "\n${message}\n- Line ${consoleMessage.lineNumber()} of ${consoleMessage.sourceId()}"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onCloseWindow(window: WebView?) {
|
|
|
|
|
+ callbacks.onCloseRequested()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
webView.webViewClient = object : WebViewClient() {
|
|
webView.webViewClient = object : WebViewClient() {
|
|
@@ -123,31 +159,121 @@ internal fun configureRssReadWebView(
|
|
|
view: WebView?,
|
|
view: WebView?,
|
|
|
request: WebResourceRequest
|
|
request: WebResourceRequest
|
|
|
): WebResourceResponse? {
|
|
): WebResourceResponse? {
|
|
|
- val source = viewModel.rssSource
|
|
|
|
|
- val preloadJs = source?.preloadJs
|
|
|
|
|
- if (
|
|
|
|
|
- isStartPage &&
|
|
|
|
|
- !preloadJsInjected &&
|
|
|
|
|
- !preloadJs.isNullOrBlank() &&
|
|
|
|
|
- request.url.toString() == WebJsExtensions.nameUrl
|
|
|
|
|
- ) {
|
|
|
|
|
- preloadJsInjected = true
|
|
|
|
|
- val jsContent = buildString {
|
|
|
|
|
- append("(() => {")
|
|
|
|
|
- append(WebJsExtensions.JS_INJECTION)
|
|
|
|
|
- append("\n")
|
|
|
|
|
- append(preloadJs)
|
|
|
|
|
- append("\n})();")
|
|
|
|
|
|
|
+ val source = viewModel.rssSource ?: return super.shouldInterceptRequest(view, request)
|
|
|
|
|
+ val url = request.url.toString()
|
|
|
|
|
+ if (request.isForMainFrame) {
|
|
|
|
|
+ if (viewModel.hasPreloadJs) {
|
|
|
|
|
+ preloadJsInjected = false
|
|
|
|
|
+ if (url.startsWith("data:text/html;") || request.method == "POST") {
|
|
|
|
|
+ return super.shouldInterceptRequest(view, request)
|
|
|
|
|
+ }
|
|
|
|
|
+ return runBlocking(IO) {
|
|
|
|
|
+ getModifiedContentWithJs(url, request) ?: super.shouldInterceptRequest(
|
|
|
|
|
+ view,
|
|
|
|
|
+ request
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ } else if (!preloadJsInjected && url == WebJsExtensions.nameUrl) {
|
|
|
|
|
+ preloadJsInjected = true
|
|
|
|
|
+ val preloadJs = source.preloadJs ?: ""
|
|
|
return WebResourceResponse(
|
|
return WebResourceResponse(
|
|
|
"text/javascript",
|
|
"text/javascript",
|
|
|
"utf-8",
|
|
"utf-8",
|
|
|
- ByteArrayInputStream(jsContent.toByteArray())
|
|
|
|
|
|
|
+ ByteArrayInputStream("(() => {${WebJsExtensions.JS_INJECTION}\n$preloadJs\n})();".toByteArray())
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
+ val blacklist = source.contentBlacklist?.splitNotBlank(",")
|
|
|
|
|
+ if (!blacklist.isNullOrEmpty()) {
|
|
|
|
|
+ blacklist.forEach {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (url.startsWith(it) || url.matches(it.toRegex())) {
|
|
|
|
|
+ return createEmptyResource()
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e: PatternSyntaxException) {
|
|
|
|
|
+ AppLog.put("黑名单规则正则语法错误 源名称:${source.sourceName} 正则:$it", e)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ val whitelist = source.contentWhitelist?.splitNotBlank(",")
|
|
|
|
|
+ if (!whitelist.isNullOrEmpty()) {
|
|
|
|
|
+ whitelist.forEach {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (url.startsWith(it) || url.matches(it.toRegex())) {
|
|
|
|
|
+ return super.shouldInterceptRequest(view, request)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e: PatternSyntaxException) {
|
|
|
|
|
+ AppLog.put(
|
|
|
|
|
+ "白名单规则正则语法错误 源名称:${source.sourceName} 正则:$it",
|
|
|
|
|
+ e
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return createEmptyResource()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
return super.shouldInterceptRequest(view, request)
|
|
return super.shouldInterceptRequest(view, request)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private suspend fun getModifiedContentWithJs(
|
|
|
|
|
+ url: String,
|
|
|
|
|
+ request: WebResourceRequest
|
|
|
|
|
+ ): WebResourceResponse? {
|
|
|
|
|
+ return try {
|
|
|
|
|
+ val cookie = webCookieManager.getCookie(url)
|
|
|
|
|
+ val res = okHttpClient.newCallResponse {
|
|
|
|
|
+ url(url)
|
|
|
|
|
+ method(request.method, null)
|
|
|
|
|
+ if (!cookie.isNullOrEmpty()) {
|
|
|
|
|
+ addHeader("Cookie", cookie)
|
|
|
|
|
+ }
|
|
|
|
|
+ request.requestHeaders?.forEach { (key, value) ->
|
|
|
|
|
+ addHeader(key, value)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ res.headers("Set-Cookie").forEach { setCookie ->
|
|
|
|
|
+ webCookieManager.setCookie(url, setCookie)
|
|
|
|
|
+ }
|
|
|
|
|
+ val body = res.body
|
|
|
|
|
+ val contentType = body.contentType()
|
|
|
|
|
+ if (!shouldInjectPreloadJs(contentType, res.header("Content-Disposition"))) {
|
|
|
|
|
+ res.close()
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+ val mimeType = contentType?.toString()?.substringBefore(";") ?: "text/html"
|
|
|
|
|
+ val charset = contentType?.charset() ?: Charsets.UTF_8
|
|
|
|
|
+ val charsetName = charset.name()
|
|
|
|
|
+ val bodyText = body.text().let { originalText ->
|
|
|
|
|
+ val headIndex = originalText.indexOf("<head", ignoreCase = true)
|
|
|
|
|
+ if (headIndex >= 0) {
|
|
|
|
|
+ val closingHeadIndex = originalText.indexOf('>', startIndex = headIndex)
|
|
|
|
|
+ if (closingHeadIndex >= 0) {
|
|
|
|
|
+ val insertPos = closingHeadIndex + 1
|
|
|
|
|
+ StringBuilder(originalText).insert(insertPos, WebJsExtensions.JS_URL)
|
|
|
|
|
+ .toString()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ originalText
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ originalText
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ WebResourceResponse(
|
|
|
|
|
+ mimeType,
|
|
|
|
|
+ charsetName,
|
|
|
|
|
+ ByteArrayInputStream(bodyText.toByteArray(charset))
|
|
|
|
|
+ )
|
|
|
|
|
+ } catch (_: Exception) {
|
|
|
|
|
+ null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun createEmptyResource(): WebResourceResponse {
|
|
|
|
|
+ return WebResourceResponse(
|
|
|
|
|
+ "text/plain", "utf-8", ByteArrayInputStream("".toByteArray())
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private fun handleRedirect(view: WebView, fromUrl: String?, toUrl: String): Boolean {
|
|
private fun handleRedirect(view: WebView, fromUrl: String?, toUrl: String): Boolean {
|
|
|
val fromHost = fromUrl?.toUri()?.host
|
|
val fromHost = fromUrl?.toUri()?.host
|
|
|
val toHost = toUrl.toUri().host
|
|
val toHost = toUrl.toUri().host
|
|
@@ -192,13 +318,7 @@ internal fun configureRssReadWebView(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun askUser(fromUrl: String?, toUrl: String, onResult: (Boolean) -> Unit) {
|
|
private fun askUser(fromUrl: String?, toUrl: String, onResult: (Boolean) -> Unit) {
|
|
|
- AlertDialog.Builder(context)
|
|
|
|
|
- .setTitle("重定向请求")
|
|
|
|
|
- .setMessage("是否允许页面跳转?\n\n来源:${fromUrl ?: "未知"}\n目标:$toUrl")
|
|
|
|
|
- .setPositiveButton("允许") { _, _ -> onResult(true) }
|
|
|
|
|
- .setNegativeButton("拒绝") { _, _ -> onResult(false) }
|
|
|
|
|
- .setCancelable(true)
|
|
|
|
|
- .show()
|
|
|
|
|
|
|
+ callbacks.onAskRedirect(fromUrl, toUrl, onResult)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun handleCustomScheme(url: Uri): Boolean {
|
|
private fun handleCustomScheme(url: Uri): Boolean {
|
|
@@ -245,8 +365,18 @@ internal fun configureRssReadWebView(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
|
|
|
|
+ view?.evaluateJavascript(WebJsExtensions.basicJs, null)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
override fun onPageFinished(view: WebView, url: String?) {
|
|
override fun onPageFinished(view: WebView, url: String?) {
|
|
|
super.onPageFinished(view, url)
|
|
super.onPageFinished(view, url)
|
|
|
|
|
+ val finishedUrl = url ?: return
|
|
|
|
|
+ if (URLUtil.isNetworkUrl(finishedUrl)) {
|
|
|
|
|
+ webCookieManager.getCookie(finishedUrl)?.takeIf { it.isNotBlank() }?.let { cookie ->
|
|
|
|
|
+ CookieStore.setCookie(finishedUrl, cookie)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
view.applyDayNight(AppConfig.isNightTheme)
|
|
view.applyDayNight(AppConfig.isNightTheme)
|
|
|
view.title?.let { webTitle ->
|
|
view.title?.let { webTitle ->
|
|
|
if (
|
|
if (
|
|
@@ -296,28 +426,41 @@ internal fun configureRssReadWebView(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- val source = viewModel.rssSource
|
|
|
|
|
- if (
|
|
|
|
|
- isStartPage &&
|
|
|
|
|
- !source?.preloadJs.isNullOrBlank() &&
|
|
|
|
|
- appCompatActivity != null
|
|
|
|
|
- ) {
|
|
|
|
|
- val webJsExtensions = WebJsExtensions(
|
|
|
|
|
- source,
|
|
|
|
|
- appCompatActivity,
|
|
|
|
|
- webView,
|
|
|
|
|
- callback = callbacks
|
|
|
|
|
- )
|
|
|
|
|
- webView.addJavascriptInterface(webJsExtensions, WebJsExtensions.nameJava)
|
|
|
|
|
- webView.addJavascriptInterface(source, WebJsExtensions.nameSource)
|
|
|
|
|
- webView.addJavascriptInterface(WebCacheManager, WebJsExtensions.nameCache)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
webView.addJavascriptInterface(object {
|
|
webView.addJavascriptInterface(object {
|
|
|
@JavascriptInterface
|
|
@JavascriptInterface
|
|
|
fun isNightTheme(): Boolean = AppConfig.isNightTheme
|
|
fun isNightTheme(): Boolean = AppConfig.isNightTheme
|
|
|
}, "thisActivity")
|
|
}, "thisActivity")
|
|
|
|
|
|
|
|
|
|
+ webView.addJavascriptInterface(object {
|
|
|
|
|
+ @JavascriptInterface
|
|
|
|
|
+ fun lockOrientation(orientation: String) {
|
|
|
|
|
+ val ctx = activity
|
|
|
|
|
+ if (ctx != null && callbacks.isFullscreenProvider() && !ctx.isFinishing && !ctx.isDestroyed) {
|
|
|
|
|
+ ctx.runOnUiThread {
|
|
|
|
|
+ ctx.requestedOrientation = when (orientation) {
|
|
|
|
|
+ "portrait", "portrait-primary" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
|
|
|
+ "portrait-secondary" -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
|
|
|
|
+ "landscape" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
|
|
|
|
+ "landscape-primary" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
|
|
|
+ "landscape-secondary" -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
|
|
|
|
+ "any", "unspecified" -> ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
|
|
|
|
+ else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @JavascriptInterface
|
|
|
|
|
+ fun onCloseRequested() {
|
|
|
|
|
+ val ctx = activity
|
|
|
|
|
+ if (ctx != null && !ctx.isFinishing && !ctx.isDestroyed) {
|
|
|
|
|
+ ctx.runOnUiThread {
|
|
|
|
|
+ callbacks.onCloseRequested()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, WebJsExtensions.nameBasic)
|
|
|
|
|
+
|
|
|
webView.setOnLongClickListener {
|
|
webView.setOnLongClickListener {
|
|
|
val hitTestResult = webView.hitTestResult
|
|
val hitTestResult = webView.hitTestResult
|
|
|
if (
|
|
if (
|
|
@@ -351,3 +494,47 @@ internal fun configureRssReadWebView(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+private fun shouldInjectPreloadJs(
|
|
|
|
|
+ contentType: MediaType?,
|
|
|
|
|
+ contentDisposition: String?
|
|
|
|
|
+): Boolean {
|
|
|
|
|
+ if (contentDisposition
|
|
|
|
|
+ ?.substringBefore(';')
|
|
|
|
|
+ ?.trim()
|
|
|
|
|
+ ?.equals("attachment", ignoreCase = true) == true
|
|
|
|
|
+ ) return false
|
|
|
|
|
+ return contentType == null ||
|
|
|
|
|
+ contentType.type == "text" && contentType.subtype == "html" ||
|
|
|
|
|
+ contentType.type == "application" && contentType.subtype == "xhtml+xml"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 在页面内容加载前注入 preloadJs 所需的 JS 接口(java/source/cache)。
|
|
|
|
|
+ * 必须在 [ReadRssViewModel.hasPreloadJs] 确定后调用,否则接口会因时序缺失而
|
|
|
|
|
+ * 导致 startJs 首行(依赖 window.cache 等)抛错,进而在 let 声明前中断脚本。
|
|
|
|
|
+ */
|
|
|
|
|
+@SuppressLint("JavascriptInterface")
|
|
|
|
|
+internal fun injectRssReadJsInterfaces(
|
|
|
|
|
+ webView: WebView,
|
|
|
|
|
+ viewModel: ReadRssViewModel,
|
|
|
|
|
+ appCompatActivity: AppCompatActivity?,
|
|
|
|
|
+ callbacks: RssReadWebControllerCallbacks,
|
|
|
|
|
+ injectedSourceUrl: String? = null,
|
|
|
|
|
+ onInjected: (String) -> Unit = {}
|
|
|
|
|
+) {
|
|
|
|
|
+ val source = viewModel.rssSource ?: return
|
|
|
|
|
+ if (injectedSourceUrl == source.sourceUrl) return
|
|
|
|
|
+ if (!viewModel.hasPreloadJs || source.preloadJs.isNullOrBlank()) return
|
|
|
|
|
+ if (appCompatActivity == null) return
|
|
|
|
|
+ val webJsExtensions = WebJsExtensions(
|
|
|
|
|
+ source,
|
|
|
|
|
+ appCompatActivity,
|
|
|
|
|
+ webView,
|
|
|
|
|
+ callback = callbacks
|
|
|
|
|
+ )
|
|
|
|
|
+ webView.addJavascriptInterface(webJsExtensions, WebJsExtensions.nameJava)
|
|
|
|
|
+ webView.addJavascriptInterface(source, WebJsExtensions.nameSource)
|
|
|
|
|
+ webView.addJavascriptInterface(WebCacheManager, WebJsExtensions.nameCache)
|
|
|
|
|
+ onInjected(source.sourceUrl)
|
|
|
|
|
+}
|