[优化] 完整支持半屏段评

This commit is contained in:
HapeLee
2026-02-02 20:56:10 +08:00
parent d95774fd40
commit d3c85eccfe
@@ -1,36 +1,54 @@
package io.legado.app.ui.widget.dialog
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.graphics.Bitmap
import android.net.Uri
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.util.Base64
import android.util.TypedValue
import android.view.Gravity
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.SslErrorHandler
import android.webkit.URLUtil
import android.webkit.WebChromeClient
import android.webkit.WebChromeClient.CustomViewCallback
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import androidx.annotation.Keep
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.view.isNotEmpty
import androidx.core.view.size
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import io.legado.app.R
import io.legado.app.base.BaseBottomSheetDialogFragment
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.imagePathKey
import io.legado.app.constant.AppLog
import io.legado.app.data.appDb
import io.legado.app.data.entities.BaseSource
import io.legado.app.databinding.DialogWebViewBinding
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.WebCacheManager
import io.legado.app.help.config.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.http.newCallResponse
import io.legado.app.help.http.newCallResponseBody
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.Companion.JS_INJECTION
import io.legado.app.help.webView.WebJsExtensions.Companion.basicJs
@@ -38,22 +56,37 @@ import io.legado.app.help.webView.WebJsExtensions.Companion.nameBasic
import io.legado.app.help.webView.WebJsExtensions.Companion.nameCache
import io.legado.app.help.webView.WebJsExtensions.Companion.nameJava
import io.legado.app.help.webView.WebJsExtensions.Companion.nameSource
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.lib.dialogs.SelectItem
import io.legado.app.lib.dialogs.selector
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.ui.association.OnLineImportActivity
import io.legado.app.utils.get
import io.legado.app.ui.file.HandleFileContract
import io.legado.app.utils.ACache
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.invisible
import io.legado.app.utils.longSnackbar
import io.legado.app.utils.openUrl
import io.legado.app.utils.setLayout
import io.legado.app.utils.startActivity
import io.legado.app.utils.toastOnUi
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
import io.legado.app.utils.writeBytes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.ByteArrayInputStream
import java.lang.ref.WeakReference
import java.util.Date
class BottomWebViewDialog() :
BaseBottomSheetDialogFragment(R.layout.dialog_web_view) {
class BottomWebViewDialog() : BottomSheetDialogFragment(R.layout.dialog_web_view),
WebJsExtensions.Callback {
companion object {
const val DEFAULT_JS_INJECT_URL = "https://legado-inject-js.internal"
}
constructor(
sourceKey: String,
@@ -74,25 +107,45 @@ class BottomWebViewDialog() :
}
private val binding by viewBinding(DialogWebViewBinding::bind)
private val bottomSheet by lazy {
dialog?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
}
private val behavior by lazy {
bottomSheet?.let { BottomSheetBehavior.from(it) }
bottomSheet?.let { sheet ->
BottomSheetBehavior.from(sheet)
}
}
private val displayMetrics by lazy { resources.displayMetrics }
private val selectImageDir = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
ACache.get().put(imagePathKey, uri.toString())
saveImage(it.value, uri)
}
}
private lateinit var currentWebView: WebView
private var source: BaseSource? = null
private var preloadJs: String? = null
private var isFullScreen = false
private var customWebViewCallback: CustomViewCallback? = null
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
private var originOrientation: Int? = null
private var needClearHistory = true
private var isBasicJsInjected = false
override fun onAttach(context: Context) {
super.onAttach(context)
currentWebView = WebView(context)
initWebViewBaseConfig()
}
override fun onStart() {
super.onStart()
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
override fun show(manager: FragmentManager, tag: String?) {
runCatching {
kotlin.runCatching {
manager.beginTransaction().remove(this).commit()
super.show(manager, tag)
}.onFailure {
@@ -100,90 +153,329 @@ class BottomWebViewDialog() :
}
}
override fun onFragmentCreated(
view: View,
savedInstanceState: Bundle?
) {
if (!AppConfig.isEInkMode) {
view.setBackgroundColor(ThemeStore.backgroundColor())
private fun setConfig(config: Config, first: Boolean = false) {
behavior?.let { behavior ->
config.state?.let { behavior.state = it }
config.peekHeight?.let { behavior.peekHeight = it }
config.isHideable?.let { behavior.isHideable = it }
config.skipCollapsed?.let { behavior.skipCollapsed = it }
config.setHalfExpandedRatio?.let { behavior.setHalfExpandedRatio(it) }
config.setExpandedOffset?.let { behavior.setExpandedOffset(it) }
config.setFitToContents?.let { behavior.setFitToContents(it) }
config.isDraggable?.let { behavior.isDraggable = it }
config.isDraggableOnNestedScroll?.let { behavior.isDraggableOnNestedScroll = it }
config.significantVelocityThreshold?.let { behavior.significantVelocityThreshold = it }
config.hideFriction?.let { behavior.hideFriction = it }
config.maxWidth?.let { behavior.maxWidth = it }
config.maxHeight?.let { behavior.maxHeight = it }
config.isGestureInsetBottomIgnored?.let { behavior.isGestureInsetBottomIgnored = it }
config.setUpdateImportantForAccessibilityOnSiblings?.let {
behavior.setUpdateImportantForAccessibilityOnSiblings(it)
}
}
currentWebView = createWebView()
binding.webViewContainer.addView(currentWebView)
lifecycleScope.launch {
val args = arguments ?: run {
dismiss(); return@launch
}
val sourceKey = args.getString("sourceKey") ?: return@launch
val url = args.getString("url") ?: return@launch
var html = args.getString("html") ?: return@launch
args.getString("preloadJs")?.let { preloadJs ->
html = if (html.contains("<head>")) {
html.replaceFirst(
"<head>",
"<head><script>(() => {$JS_INJECTION\n$preloadJs\n})();</script>"
)
config.expandedCornersRadius?.let {
val radius = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, it, displayMetrics
)
bottomSheet?.let { sheet ->
if (radius > 0) {
sheet.backgroundTintList = null
val shapeDrawable =
android.graphics.drawable.GradientDrawable().apply {
cornerRadius = 0f
cornerRadii = floatArrayOf(
radius, radius,
radius, radius,
0f, 0f,
0f, 0f
)
}
sheet.background = shapeDrawable
sheet.clipToOutline = true
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
currentWebView.outlineProvider =
object : android.view.ViewOutlineProvider() {
override fun getOutline(
view: View,
outline: android.graphics.Outline
) {
outline.setRoundRect(0, 0, view.width, view.height, radius)
}
}
currentWebView.clipToOutline = true
binding.customWebView.outlineProvider =
object : android.view.ViewOutlineProvider() {
override fun getOutline(
view: View,
outline: android.graphics.Outline
) {
outline.setRoundRect(0, 0, view.width, view.height, radius)
}
}
binding.customWebView.clipToOutline = true
}
} else {
"<head><script>(() => {$JS_INJECTION\n$preloadJs\n})();</script></head>$html"
sheet.backgroundTintList = null
sheet.background = null
sheet.clipToOutline = false
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
currentWebView.outlineProvider = null
currentWebView.clipToOutline = false
binding.customWebView.outlineProvider = null
binding.customWebView.clipToOutline = false
}
}
}
source = appDb.bookSourceDao.getBookSource(sourceKey) ?: run {
activity?.toastOnUi("no find bookSource")
dismiss()
return@launch
}
val bookType = args.getInt("bookType", 0)
val analyzeUrl = AnalyzeUrl(url, source = source, coroutineContext = coroutineContext)
currentWebView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
behavior?.isDraggable = scrollY == 0
}
currentWebView.post {
initWebView(
analyzeUrl.url,
html,
analyzeUrl.headerMap,
bookType
)
currentWebView.clearHistory()
}
}
dialog?.setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
when {
binding.customWebView.isNotEmpty() -> {
customWebViewCallback?.onCustomViewHidden()
true
}
dialog?.let { dialog ->
config.backgroundDimAmount?.let { amount ->
dialog.window?.setDimAmount(amount)
}
config.shouldDimBackground?.let { shouldDim ->
if (!shouldDim) {
dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
}
}
config.dismissOnTouchOutside?.let { touchOutside ->
isCancelable = touchOutside
}
config.hardwareAccelerated?.let { hwAccel ->
if (hwAccel) {
dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
}
}
}
currentWebView.canGoBack() -> {
currentWebView.goBack()
true
}
currentWebView.let { webView ->
config.webViewInitialScale?.let { scale ->
webView.settings.apply {
loadWithOverviewMode = true
useWideViewPort = true
textZoom = scale
}
}
config.webViewCacheMode?.let { cacheMode ->
webView.settings.cacheMode = cacheMode
}
config.isNestedScrollingEnabled?.let { enabled ->
webView.isNestedScrollingEnabled = enabled
} ?: run {
webView.isNestedScrollingEnabled = true
}
}
else -> {
dismiss()
true
bottomSheet?.let { sheet ->
val params = sheet.layoutParams
var hasChanged = false
config.widthPercentage?.let { percentage ->
if (percentage in 0.0..1.0) {
val width = (displayMetrics.widthPixels * percentage).toInt()
params.width = width
hasChanged = true
}
}
val dialogHeight = config.dialogHeight ?: if (first) -1 else null
dialogHeight?.let { height ->
params.height = height
hasChanged = true
}
config.heightPercentage?.let { percentage ->
if (percentage in 0.0..1.0) {
val height = (displayMetrics.heightPixels * percentage).toInt()
params.height = height
if (config.peekHeight == null) {
behavior?.peekHeight = height
}
if (config.maxHeight == null) {
behavior?.maxHeight = height
}
hasChanged = true
}
}
if (hasChanged) {
sheet.layoutParams = params
}
}
config.responsiveBreakpoint?.let { breakpoint ->
val screenWidth = displayMetrics.widthPixels
if (screenWidth < breakpoint) {
behavior?.peekHeight = config.peekHeight ?: 300
config.widthPercentage?.let { percentage ->
if (percentage > 0.8f) {
val maxWidth = (screenWidth * 0.9).toInt()
behavior?.maxWidth = maxWidth
}
}
} else false
} else {
behavior?.peekHeight = config.peekHeight ?: 400
config.widthPercentage?.let { percentage ->
if (percentage < 0.6f) {
bottomSheet?.layoutParams?.width =
(screenWidth * percentage).toInt()
(bottomSheet?.layoutParams as? FrameLayout.LayoutParams)?.gravity =
Gravity.CENTER_HORIZONTAL
}
}
}
}
val scrollNoDraggable = config.scrollNoDraggable ?: if (first) true else null
scrollNoDraggable?.let {
if (it) {
currentWebView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
behavior?.isDraggable = scrollY <= 0
}
} else {
currentWebView.setOnScrollChangeListener(null)
}
}
val longClickSaveImg = config.longClickSaveImg ?: if (first) true else null
longClickSaveImg?.let {
if (it) {
setLongClickSaveImg()
} else {
currentWebView.setOnLongClickListener(null)
}
}
}
private fun createWebView(): WebView =
WebView(requireContext()).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
private fun setLongClickSaveImg() {
currentWebView.setOnLongClickListener {
val hitTestResult = currentWebView.hitTestResult
if (hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE
) {
hitTestResult.extra?.let { webPic ->
requireContext().selector(
arrayListOf(
SelectItem(getString(R.string.action_save), "save"),
SelectItem(getString(R.string.select_folder), "selectFolder")
)
) { _, charSequence, _ ->
when (charSequence.value) {
"save" -> saveImage(webPic)
"selectFolder" -> selectSaveFolder(null)
}
}
return@setOnLongClickListener true
}
}
return@setOnLongClickListener false
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(0)
binding.webViewContainer.addView(currentWebView)
lifecycleScope.launch(IO) {
val args = arguments
if (args == null) {
dismiss()
return@launch
}
val sourceKey = args.getString("sourceKey") ?: return@launch
val url = args.getString("url") ?: return@launch
kotlin.runCatching {
// 初始化配置
args.getString("config")?.let { json ->
try {
GSON.fromJsonObject<Config>(json).getOrThrow().let { config ->
activity?.runOnUiThread {
setConfig(config, true)
}
}
} catch (e: Exception) {
AppLog.put("config err", e)
}
} ?: run {
activity?.runOnUiThread {
bottomSheet?.let { sheet ->
val layoutParams = sheet.layoutParams
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
sheet.layoutParams = layoutParams
}
currentWebView.overScrollMode = View.OVER_SCROLL_NEVER
currentWebView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
behavior?.isDraggable = (scrollY <= 0)
}
setLongClickSaveImg()
}
}
val analyzeUrl =
AnalyzeUrl(url, source = source, coroutineContext = coroutineContext)
val html = args.getString("html") ?: analyzeUrl.getStrResponseAwait().body
if (html.isNullOrEmpty()) {
throw NoStackTraceException("html is NullOrEmpty")
}
preloadJs = args.getString("preloadJs")
val spliceHtml = buildSpliceHtml(html)
source = appDb.bookSourceDao.getBookSource(sourceKey) ?: run {
activity?.toastOnUi("no find bookSource")
dismiss()
return@launch
}
val bookType = args.getInt("bookType", 0)
activity?.runOnUiThread {
initWebView(analyzeUrl.url, spliceHtml, analyzeUrl.headerMap, bookType)
if (needClearHistory) {
currentWebView.clearHistory()
needClearHistory = false
}
}
}.onFailure {
activity?.runOnUiThread {
currentWebView.loadDataWithBaseURL(
url,
"<html><body style='color:red;'>加载失败:${it.localizedMessage}</body></html>",
"text/html",
"utf-8",
url
)
}
AppLog.put("WebView加载失败", it)
}
}
dialog?.setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
if (binding.customWebView.size > 0) {
customWebViewCallback?.onCustomViewHidden()
return@setOnKeyListener true
}
if (currentWebView.canGoBack()) {
currentWebView.goBack()
return@setOnKeyListener true
}
dismiss()
return@setOnKeyListener true
}
false
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun initWebViewBaseConfig() {
currentWebView.settings.apply {
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
domStorageEnabled = true
useWideViewPort = true
loadWithOverviewMode = true
builtInZoomControls = true
javaScriptEnabled = true
displayZoomControls = false
}
currentWebView.isClickable = true
currentWebView.isFocusable = true
currentWebView.isFocusableInTouchMode = true
currentWebView.setOnLongClickListener(null)
}
private fun initWebView(
url: String,
@@ -191,81 +483,145 @@ class BottomWebViewDialog() :
headerMap: HashMap<String, String>,
bookType: Int
) {
currentWebView.apply {
webChromeClient = CustomWebChromeClient()
webViewClient = CustomWebViewClient()
settings.userAgentString =
headerMap.get(AppConst.UA_NAME, true)
addJavascriptInterface(JSInterface(this@BottomWebViewDialog), nameBasic)
source?.let { source ->
(activity as? AppCompatActivity)?.let { act ->
addJavascriptInterface(
WebJsExtensions(source, act, this, bookType),
nameJava
)
}
addJavascriptInterface(source, nameSource)
addJavascriptInterface(WebCacheManager, nameCache)
currentWebView.webViewClient = CustomWebViewClient()
currentWebView.webChromeClient = CustomWebChromeClient()
headerMap[AppConst.UA_NAME]?.let {
currentWebView.settings.userAgentString = it
}
currentWebView.addJavascriptInterface(JSInterface(this), nameBasic)
source?.let { source ->
(activity as? AppCompatActivity)?.let { currentActivity ->
val webJsExtensions = WebJsExtensions(
source, currentActivity, currentWebView, bookType, callback = this
)
currentWebView.addJavascriptInterface(webJsExtensions, nameJava)
}
currentWebView.addJavascriptInterface(source, nameSource)
currentWebView.addJavascriptInterface(WebCacheManager, nameCache)
}
currentWebView.loadDataWithBaseURL(url, html, "text/html", "utf-8", url)
}
loadDataWithBaseURL(url, html, "text/html", "utf-8", url)
private fun buildSpliceHtml(originalHtml: String): String {
if (preloadJs.isNullOrEmpty()) return originalHtml
val headIndex = originalHtml.indexOf("<head", ignoreCase = true)
return if (headIndex >= 0) {
val closingHeadIndex = originalHtml.indexOf('>', startIndex = headIndex)
if (closingHeadIndex >= 0) {
val insertPos = closingHeadIndex + 1
StringBuilder(originalHtml).insert(insertPos, buildInjectJsScript()).toString()
} else {
buildInjectJsScript() + originalHtml
}
} else {
buildInjectJsScript() + originalHtml
}
}
private fun buildInjectJsScript(): String {
val basicJs = basicJs
val preloadJs = this.preloadJs ?: ""
return """
<script type="text/javascript">
// 基础JS
$basicJs
// 预加载业务JS
(() => {
$JS_INJECTION
$preloadJs
})();
</script>
""".trimIndent()
}
private fun saveImage(webPic: String) {
val path = ACache.get().getAsString(imagePathKey)
if (path.isNullOrEmpty()) {
selectSaveFolder(webPic)
} else {
saveImage(webPic, path.toUri())
}
}
private fun selectSaveFolder(webPic: String?) {
val default = arrayListOf<SelectItem<Int>>()
val path = ACache.get().getAsString(imagePathKey)
if (!path.isNullOrEmpty()) {
default.add(SelectItem(path, -1))
}
selectImageDir.launch {
otherActions = default
value = webPic
}
}
private fun saveImage(webPic: String?, uri: Uri) {
webPic ?: return
Coroutine.async(lifecycleScope) {
val fileName = "${AppConst.fileNameFormat.format(Date(System.currentTimeMillis()))}.jpg"
val byteArray = webData2bitmap(webPic) ?: throw NoStackTraceException("图片数据为空")
uri.writeBytes(requireContext(), fileName, byteArray)
}.onError {
ACache.get().remove(imagePathKey)
context?.toastOnUi("保存图片失败:${it.localizedMessage}")
}.onSuccess {
context?.toastOnUi("保存成功")
}
}
private suspend fun webData2bitmap(data: String): ByteArray? {
return if (URLUtil.isValidUrl(data)) {
okHttpClient.newCallResponseBody {
url(data)
}.bytes()
} else {
runCatching {
Base64.decode(data.split(",").getOrNull(1) ?: "", Base64.DEFAULT)
}.getOrNull()
}
}
override fun onDestroyView() {
customWebViewCallback?.onCustomViewHidden()
currentWebView.apply {
stopLoading()
loadUrl("about:blank")
clearHistory()
removeAllViews()
webChromeClient = WebChromeClient()
webViewClient = WebViewClient()
destroy()
}
binding.webViewContainer.removeView(currentWebView)
currentWebView.removeAllViews()
currentWebView.destroy()
originOrientation?.let {
activity?.requestedOrientation = it
}
super.onDestroyView()
}
override fun upConfig(config: String) {
try {
lifecycleScope.launch(Dispatchers.Main) {
GSON.fromJsonObject<Config>(config).getOrThrow().let { config ->
setConfig(config)
}
}
} catch (e: Exception) {
AppLog.put("config err", e)
}
}
@Suppress("unused")
private class JSInterface(dialog: BottomWebViewDialog) {
private val ref = WeakReference(dialog)
private val dialogRef: WeakReference<BottomWebViewDialog> = WeakReference(dialog)
@JavascriptInterface
fun lockOrientation(orientation: String) {
val fra = ref.get() ?: return
val act = fra.activity ?: return
val fra = dialogRef.get() ?: return
val ctx = fra.requireActivity()
if (fra.isFullScreen && fra.dialog?.isShowing == true) {
act.runOnUiThread {
act.requestedOrientation = when (orientation) {
"portrait", "portrait-primary" ->
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
"portrait-secondary" ->
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
"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
fra.lifecycleScope.launch(Dispatchers.Main) {
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
}
}
}
@@ -273,88 +629,188 @@ class BottomWebViewDialog() :
@JavascriptInterface
fun onCloseRequested() {
ref.get()?.activity?.runOnUiThread {
ref.get()?.dismiss()
val fra = dialogRef.get() ?: return
if (fra.dialog?.isShowing == true) {
fra.lifecycleScope.launch(Dispatchers.Main) {
fra.dismiss()
}
}
}
}
inner class CustomWebChromeClient : WebChromeClient() {
@Keep
data class Config(
var state: Int? = null,
var peekHeight: Int? = null,
var isHideable: Boolean? = null,
var skipCollapsed: Boolean? = null,
var setHalfExpandedRatio: Float? = null,
var setExpandedOffset: Int? = null,
var setFitToContents: Boolean? = null,
var isDraggable: Boolean? = null,
var isDraggableOnNestedScroll: Boolean? = null,
var significantVelocityThreshold: Int? = null,
var hideFriction: Float? = null,
var maxWidth: Int? = null,
var maxHeight: Int? = null,
var isGestureInsetBottomIgnored: Boolean? = null,
var expandedCornersRadius: Float? = null,
var setUpdateImportantForAccessibilityOnSiblings: Boolean? = null,
var backgroundDimAmount: Float? = null,
var shouldDimBackground: Boolean? = null,
var webViewInitialScale: Int? = null,
var webViewCacheMode: Int? = null,
var dismissOnTouchOutside: Boolean? = null,
var hardwareAccelerated: Boolean? = null,
var isNestedScrollingEnabled: Boolean? = null,
var widthPercentage: Float? = null,
var heightPercentage: Float? = null,
var responsiveBreakpoint: Int? = null,
var dialogHeight: Int? = null,
var longClickSaveImg: Boolean? = null,
var scrollNoDraggable: Boolean? = null,
)
inner class CustomWebChromeClient : WebChromeClient() {
override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
originOrientation = activity?.requestedOrientation
isFullScreen = true
binding.webViewContainer.invisible()
binding.customWebView.addView(view)
customWebViewCallback = callback
behavior?.state = BottomSheetBehavior.STATE_EXPANDED
originOrientation = activity?.requestedOrientation
}
override fun onHideCustomView() {
originOrientation?.let {
activity?.requestedOrientation = it
originOrientation = null
}
isFullScreen = false
binding.webViewContainer.visible()
binding.customWebView.removeAllViews()
customWebViewCallback = null
originOrientation?.let {
activity?.requestedOrientation = it
}
}
override fun onCloseWindow(window: WebView?) {
dismiss()
}
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
if (!AppConfig.recordLog) return false
val source = source ?: return false
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
}
}
inner class CustomWebViewClient : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
if (needClearHistory) {
needClearHistory = false
currentWebView.clearHistory()
}
super.onPageStarted(view, url, favicon)
currentWebView.evaluateJavascript(basicJs, null)
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return request?.url?.let(::handleUri) ?: true
request?.url?.let {
return shouldOverrideUrlLoading(it)
}
return super.shouldOverrideUrlLoading(view, request)
}
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
return url?.toUri()?.let(::handleUri) ?: true
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
if (needClearHistory) {
currentWebView.clearHistory()
needClearHistory = false
}
if (!isBasicJsInjected && view != null) {
view.evaluateJavascript(basicJs, null)
isBasicJsInjected = true
}
}
private fun handleUri(uri: Uri): Boolean =
when (uri.scheme) {
"http", "https" -> false
private fun shouldOverrideUrlLoading(url: Uri): Boolean {
return when (url.scheme) {
"http", "https" -> {
false
}
"legado", "yuedu" -> {
startActivity<OnLineImportActivity> { data = uri }
startActivity<OnLineImportActivity> {
data = url
}
true
}
else -> {
binding.root.longSnackbar(
R.string.jump_to_another_app,
R.string.confirm
) {
activity?.openUrl(uri)
binding.root.longSnackbar(R.string.jump_to_another_app, R.string.confirm) {
activity?.openUrl(url)
}
true
}
}
}
@SuppressLint("WebViewClientOnReceivedSslError")
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
view: WebView?, handler: SslErrorHandler?, error: SslError?
) {
handler?.proceed()
}
private val webCookieManager by lazy { android.webkit.CookieManager.getInstance() }
override fun shouldInterceptRequest(
view: WebView, request: WebResourceRequest
): WebResourceResponse? {
val url = request.url.toString()
if (!request.isForMainFrame && url.contains(DEFAULT_JS_INJECT_URL)) {
val preloadJs = preloadJs ?: ""
return WebResourceResponse(
"text/javascript",
"utf-8",
ByteArrayInputStream("(() => {$JS_INJECTION\n$preloadJs\n})();".toByteArray())
)
}
if (request.isForMainFrame && !preloadJs.isNullOrEmpty()) {
return runBlocking(IO) {
getModifiedContentWithJs(url, request) ?: 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 { webCookieManager.setCookie(url, it) }
val body = res.body
val contentType = body.contentType()
val mimeType = contentType?.toString()?.substringBefore(";") ?: "text/html"
val charset = contentType?.charset() ?: Charsets.UTF_8
val bodyText = buildSpliceHtml(body.text(charset.toString()))
WebResourceResponse(
mimeType,
charset.name(),
ByteArrayInputStream(bodyText.toByteArray(charset))
)
} catch (e: Exception) {
AppLog.put("拦截请求处理失败", e)
null
}
}
}
}
}