|
@@ -1,36 +1,54 @@
|
|
|
package io.legado.app.ui.widget.dialog
|
|
package io.legado.app.ui.widget.dialog
|
|
|
|
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.annotation.SuppressLint
|
|
|
|
|
+import android.content.Context
|
|
|
import android.content.pm.ActivityInfo
|
|
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.Build
|
|
|
import android.os.Bundle
|
|
import android.os.Bundle
|
|
|
|
|
+import android.util.Base64
|
|
|
|
|
+import android.util.TypedValue
|
|
|
|
|
+import android.view.Gravity
|
|
|
import android.view.KeyEvent
|
|
import android.view.KeyEvent
|
|
|
import android.view.View
|
|
import android.view.View
|
|
|
import android.view.ViewGroup
|
|
import android.view.ViewGroup
|
|
|
|
|
+import android.view.WindowManager
|
|
|
|
|
+import android.webkit.ConsoleMessage
|
|
|
import android.webkit.JavascriptInterface
|
|
import android.webkit.JavascriptInterface
|
|
|
import android.webkit.SslErrorHandler
|
|
import android.webkit.SslErrorHandler
|
|
|
|
|
+import android.webkit.URLUtil
|
|
|
import android.webkit.WebChromeClient
|
|
import android.webkit.WebChromeClient
|
|
|
-import android.webkit.WebChromeClient.CustomViewCallback
|
|
|
|
|
import android.webkit.WebResourceRequest
|
|
import android.webkit.WebResourceRequest
|
|
|
|
|
+import android.webkit.WebResourceResponse
|
|
|
|
|
+import android.webkit.WebSettings
|
|
|
import android.webkit.WebView
|
|
import android.webkit.WebView
|
|
|
import android.webkit.WebViewClient
|
|
import android.webkit.WebViewClient
|
|
|
|
|
+import android.widget.FrameLayout
|
|
|
|
|
+import androidx.annotation.Keep
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
|
import androidx.core.net.toUri
|
|
import androidx.core.net.toUri
|
|
|
-import androidx.core.view.isNotEmpty
|
|
|
|
|
|
|
+import androidx.core.view.size
|
|
|
import androidx.fragment.app.FragmentManager
|
|
import androidx.fragment.app.FragmentManager
|
|
|
import androidx.lifecycle.lifecycleScope
|
|
import androidx.lifecycle.lifecycleScope
|
|
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
|
|
|
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
|
import io.legado.app.R
|
|
import io.legado.app.R
|
|
|
-import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
|
|
|
import io.legado.app.constant.AppConst
|
|
import io.legado.app.constant.AppConst
|
|
|
|
|
+import io.legado.app.constant.AppConst.imagePathKey
|
|
|
import io.legado.app.constant.AppLog
|
|
import io.legado.app.constant.AppLog
|
|
|
import io.legado.app.data.appDb
|
|
import io.legado.app.data.appDb
|
|
|
import io.legado.app.data.entities.BaseSource
|
|
import io.legado.app.data.entities.BaseSource
|
|
|
import io.legado.app.databinding.DialogWebViewBinding
|
|
import io.legado.app.databinding.DialogWebViewBinding
|
|
|
|
|
+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.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
|
|
|
import io.legado.app.help.webView.WebJsExtensions.Companion.JS_INJECTION
|
|
import io.legado.app.help.webView.WebJsExtensions.Companion.JS_INJECTION
|
|
|
import io.legado.app.help.webView.WebJsExtensions.Companion.basicJs
|
|
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.nameCache
|
|
|
import io.legado.app.help.webView.WebJsExtensions.Companion.nameJava
|
|
import io.legado.app.help.webView.WebJsExtensions.Companion.nameJava
|
|
|
import io.legado.app.help.webView.WebJsExtensions.Companion.nameSource
|
|
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.model.analyzeRule.AnalyzeUrl
|
|
|
import io.legado.app.ui.association.OnLineImportActivity
|
|
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.invisible
|
|
|
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.setLayout
|
|
|
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 io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
|
import io.legado.app.utils.visible
|
|
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.launch
|
|
|
|
|
+import kotlinx.coroutines.runBlocking
|
|
|
|
|
+import java.io.ByteArrayInputStream
|
|
|
import java.lang.ref.WeakReference
|
|
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(
|
|
constructor(
|
|
|
sourceKey: String,
|
|
sourceKey: String,
|
|
@@ -74,25 +107,45 @@ class BottomWebViewDialog() :
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private val binding by viewBinding(DialogWebViewBinding::bind)
|
|
private val binding by viewBinding(DialogWebViewBinding::bind)
|
|
|
-
|
|
|
|
|
private val bottomSheet by lazy {
|
|
private val bottomSheet by lazy {
|
|
|
dialog?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
|
|
dialog?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
private val behavior by lazy {
|
|
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 lateinit var currentWebView: WebView
|
|
|
-
|
|
|
|
|
private var source: BaseSource? = null
|
|
private var source: BaseSource? = null
|
|
|
|
|
+ private var preloadJs: String? = null
|
|
|
private var isFullScreen = false
|
|
private var isFullScreen = false
|
|
|
- private var customWebViewCallback: CustomViewCallback? = null
|
|
|
|
|
|
|
+ private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
|
|
|
private var originOrientation: Int? = null
|
|
private var originOrientation: Int? = null
|
|
|
private var needClearHistory = true
|
|
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?) {
|
|
override fun show(manager: FragmentManager, tag: String?) {
|
|
|
- runCatching {
|
|
|
|
|
|
|
+ kotlin.runCatching {
|
|
|
manager.beginTransaction().remove(this).commit()
|
|
manager.beginTransaction().remove(this).commit()
|
|
|
super.show(manager, tag)
|
|
super.show(manager, tag)
|
|
|
}.onFailure {
|
|
}.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)
|
|
|
|
|
|
|
+ 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 {
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- lifecycleScope.launch {
|
|
|
|
|
- val args = arguments ?: run {
|
|
|
|
|
- dismiss(); return@launch
|
|
|
|
|
|
|
+ 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)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- val sourceKey = args.getString("sourceKey") ?: return@launch
|
|
|
|
|
- val url = args.getString("url") ?: return@launch
|
|
|
|
|
- var html = args.getString("html") ?: return@launch
|
|
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- args.getString("preloadJs")?.let { preloadJs ->
|
|
|
|
|
- html = if (html.contains("<head>")) {
|
|
|
|
|
- html.replaceFirst(
|
|
|
|
|
- "<head>",
|
|
|
|
|
- "<head><script>(() => {$JS_INJECTION\n$preloadJs\n})();</script>"
|
|
|
|
|
- )
|
|
|
|
|
- } else {
|
|
|
|
|
- "<head><script>(() => {$JS_INJECTION\n$preloadJs\n})();</script></head>$html"
|
|
|
|
|
|
|
+ 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
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- source = appDb.bookSourceDao.getBookSource(sourceKey) ?: run {
|
|
|
|
|
- activity?.toastOnUi("no find bookSource")
|
|
|
|
|
- dismiss()
|
|
|
|
|
- return@launch
|
|
|
|
|
|
|
+ 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
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- val bookType = args.getInt("bookType", 0)
|
|
|
|
|
- val analyzeUrl = AnalyzeUrl(url, source = source, coroutineContext = coroutineContext)
|
|
|
|
|
|
|
+ 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 {
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- currentWebView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
|
|
|
|
|
- behavior?.isDraggable = scrollY == 0
|
|
|
|
|
|
|
+ val scrollNoDraggable = config.scrollNoDraggable ?: if (first) true else null
|
|
|
|
|
+ scrollNoDraggable?.let {
|
|
|
|
|
+ if (it) {
|
|
|
|
|
+ currentWebView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
|
|
|
|
|
+ behavior?.isDraggable = scrollY <= 0
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ currentWebView.setOnScrollChangeListener(null)
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- currentWebView.post {
|
|
|
|
|
- initWebView(
|
|
|
|
|
- analyzeUrl.url,
|
|
|
|
|
- html,
|
|
|
|
|
- analyzeUrl.headerMap,
|
|
|
|
|
- bookType
|
|
|
|
|
- )
|
|
|
|
|
- currentWebView.clearHistory()
|
|
|
|
|
|
|
+ val longClickSaveImg = config.longClickSaveImg ?: if (first) true else null
|
|
|
|
|
+ longClickSaveImg?.let {
|
|
|
|
|
+ if (it) {
|
|
|
|
|
+ setLongClickSaveImg()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ currentWebView.setOnLongClickListener(null)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- dialog?.setOnKeyListener { _, keyCode, event ->
|
|
|
|
|
- if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
|
|
|
|
- when {
|
|
|
|
|
- binding.customWebView.isNotEmpty() -> {
|
|
|
|
|
- customWebViewCallback?.onCustomViewHidden()
|
|
|
|
|
- true
|
|
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- currentWebView.canGoBack() -> {
|
|
|
|
|
- currentWebView.goBack()
|
|
|
|
|
- true
|
|
|
|
|
|
|
+ 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)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- else -> {
|
|
|
|
|
- dismiss()
|
|
|
|
|
- true
|
|
|
|
|
|
|
+ } ?: 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
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- } else 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
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun createWebView(): WebView =
|
|
|
|
|
- WebView(requireContext()).apply {
|
|
|
|
|
- layoutParams = ViewGroup.LayoutParams(
|
|
|
|
|
- ViewGroup.LayoutParams.MATCH_PARENT,
|
|
|
|
|
- ViewGroup.LayoutParams.MATCH_PARENT
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ @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(
|
|
private fun initWebView(
|
|
|
url: String,
|
|
url: String,
|
|
@@ -191,81 +483,145 @@ class BottomWebViewDialog() :
|
|
|
headerMap: HashMap<String, String>,
|
|
headerMap: HashMap<String, String>,
|
|
|
bookType: Int
|
|
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
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- override fun onDestroyView() {
|
|
|
|
|
- customWebViewCallback?.onCustomViewHidden()
|
|
|
|
|
|
|
+ private fun buildInjectJsScript(): String {
|
|
|
|
|
+ val basicJs = basicJs
|
|
|
|
|
+ val preloadJs = this.preloadJs ?: ""
|
|
|
|
|
+ return """
|
|
|
|
|
+ <script type="text/javascript">
|
|
|
|
|
+ // 基础JS
|
|
|
|
|
+ $basicJs
|
|
|
|
|
+ // 预加载业务JS
|
|
|
|
|
+ (() => {
|
|
|
|
|
+ $JS_INJECTION
|
|
|
|
|
+ $preloadJs
|
|
|
|
|
+ })();
|
|
|
|
|
+ </script>
|
|
|
|
|
+ """.trimIndent()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- currentWebView.apply {
|
|
|
|
|
- stopLoading()
|
|
|
|
|
- loadUrl("about:blank")
|
|
|
|
|
- clearHistory()
|
|
|
|
|
- removeAllViews()
|
|
|
|
|
|
|
+ private fun saveImage(webPic: String) {
|
|
|
|
|
+ val path = ACache.get().getAsString(imagePathKey)
|
|
|
|
|
+ if (path.isNullOrEmpty()) {
|
|
|
|
|
+ selectSaveFolder(webPic)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ saveImage(webPic, path.toUri())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- webChromeClient = WebChromeClient()
|
|
|
|
|
- webViewClient = WebViewClient()
|
|
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- destroy()
|
|
|
|
|
|
|
+ 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()
|
|
|
|
|
+ binding.webViewContainer.removeView(currentWebView)
|
|
|
|
|
+ currentWebView.removeAllViews()
|
|
|
|
|
+ currentWebView.destroy()
|
|
|
originOrientation?.let {
|
|
originOrientation?.let {
|
|
|
activity?.requestedOrientation = it
|
|
activity?.requestedOrientation = it
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
super.onDestroyView()
|
|
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 class JSInterface(dialog: BottomWebViewDialog) {
|
|
|
- private val ref = WeakReference(dialog)
|
|
|
|
|
|
|
+ private val dialogRef: WeakReference<BottomWebViewDialog> = WeakReference(dialog)
|
|
|
|
|
|
|
|
@JavascriptInterface
|
|
@JavascriptInterface
|
|
|
fun lockOrientation(orientation: String) {
|
|
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) {
|
|
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
|
|
@JavascriptInterface
|
|
|
fun onCloseRequested() {
|
|
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?) {
|
|
override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
|
|
|
|
|
+ originOrientation = activity?.requestedOrientation
|
|
|
isFullScreen = true
|
|
isFullScreen = true
|
|
|
binding.webViewContainer.invisible()
|
|
binding.webViewContainer.invisible()
|
|
|
binding.customWebView.addView(view)
|
|
binding.customWebView.addView(view)
|
|
|
customWebViewCallback = callback
|
|
customWebViewCallback = callback
|
|
|
behavior?.state = BottomSheetBehavior.STATE_EXPANDED
|
|
behavior?.state = BottomSheetBehavior.STATE_EXPANDED
|
|
|
- originOrientation = activity?.requestedOrientation
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onHideCustomView() {
|
|
override fun onHideCustomView() {
|
|
|
|
|
+ originOrientation?.let {
|
|
|
|
|
+ activity?.requestedOrientation = it
|
|
|
|
|
+ originOrientation = null
|
|
|
|
|
+ }
|
|
|
isFullScreen = false
|
|
isFullScreen = false
|
|
|
binding.webViewContainer.visible()
|
|
binding.webViewContainer.visible()
|
|
|
binding.customWebView.removeAllViews()
|
|
binding.customWebView.removeAllViews()
|
|
|
customWebViewCallback = null
|
|
customWebViewCallback = null
|
|
|
- originOrientation?.let {
|
|
|
|
|
- activity?.requestedOrientation = it
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onCloseWindow(window: WebView?) {
|
|
override fun onCloseWindow(window: WebView?) {
|
|
|
dismiss()
|
|
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() {
|
|
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(
|
|
override fun shouldOverrideUrlLoading(
|
|
|
view: WebView?,
|
|
view: WebView?,
|
|
|
request: WebResourceRequest?
|
|
request: WebResourceRequest?
|
|
|
): Boolean {
|
|
): 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" -> {
|
|
"legado", "yuedu" -> {
|
|
|
- startActivity<OnLineImportActivity> { data = uri }
|
|
|
|
|
|
|
+ startActivity<OnLineImportActivity> {
|
|
|
|
|
+ data = url
|
|
|
|
|
+ }
|
|
|
true
|
|
true
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
else -> {
|
|
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
|
|
true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
@SuppressLint("WebViewClientOnReceivedSslError")
|
|
@SuppressLint("WebViewClientOnReceivedSslError")
|
|
|
override fun onReceivedSslError(
|
|
override fun onReceivedSslError(
|
|
|
- view: WebView?,
|
|
|
|
|
- handler: SslErrorHandler?,
|
|
|
|
|
- error: SslError?
|
|
|
|
|
|
|
+ view: WebView?, handler: SslErrorHandler?, error: SslError?
|
|
|
) {
|
|
) {
|
|
|
handler?.proceed()
|
|
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
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+}
|