pull/1352/head
parent
1a5f94e43a
commit
995c9098db
@ -1,236 +0,0 @@ |
|||||||
package io.legado.app.help.http |
|
||||||
|
|
||||||
import android.annotation.SuppressLint |
|
||||||
import android.os.Handler |
|
||||||
import android.os.Looper |
|
||||||
import android.os.Message |
|
||||||
import android.text.TextUtils |
|
||||||
import android.webkit.CookieManager |
|
||||||
import android.webkit.WebSettings |
|
||||||
import android.webkit.WebView |
|
||||||
import android.webkit.WebViewClient |
|
||||||
import io.legado.app.constant.AppConst |
|
||||||
import org.apache.commons.text.StringEscapeUtils |
|
||||||
import splitties.init.appCtx |
|
||||||
import java.lang.ref.WeakReference |
|
||||||
|
|
||||||
|
|
||||||
class AjaxWebView { |
|
||||||
var callback: Callback? = null |
|
||||||
private var mHandler: AjaxHandler = AjaxHandler(this) |
|
||||||
|
|
||||||
class AjaxHandler(private val ajaxWebView: AjaxWebView) : Handler(Looper.getMainLooper()) { |
|
||||||
|
|
||||||
private var mWebView: WebView? = null |
|
||||||
|
|
||||||
override fun handleMessage(msg: Message) { |
|
||||||
val params: AjaxParams |
|
||||||
when (msg.what) { |
|
||||||
MSG_AJAX_START -> { |
|
||||||
params = msg.obj as AjaxParams |
|
||||||
mWebView = createAjaxWebView(params, this) |
|
||||||
} |
|
||||||
MSG_SNIFF_START -> { |
|
||||||
params = msg.obj as AjaxParams |
|
||||||
mWebView = createAjaxWebView(params, this) |
|
||||||
} |
|
||||||
MSG_SUCCESS -> { |
|
||||||
ajaxWebView.callback?.onResult(msg.obj as StrResponse) |
|
||||||
destroyWebView() |
|
||||||
} |
|
||||||
MSG_ERROR -> { |
|
||||||
ajaxWebView.callback?.onError(msg.obj as Throwable) |
|
||||||
destroyWebView() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") |
|
||||||
fun createAjaxWebView(params: AjaxParams, handler: Handler): WebView { |
|
||||||
val webView = WebView(appCtx) |
|
||||||
val settings = webView.settings |
|
||||||
settings.javaScriptEnabled = true |
|
||||||
settings.domStorageEnabled = true |
|
||||||
settings.blockNetworkImage = true |
|
||||||
settings.userAgentString = params.userAgent |
|
||||||
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW |
|
||||||
if (params.isSniff) { |
|
||||||
webView.webViewClient = SnifferWebClient(params, handler) |
|
||||||
} else { |
|
||||||
webView.webViewClient = HtmlWebViewClient(params, handler) |
|
||||||
} |
|
||||||
when (params.requestMethod) { |
|
||||||
RequestMethod.POST -> params.postData?.let { |
|
||||||
webView.postUrl(params.url, it) |
|
||||||
} |
|
||||||
RequestMethod.GET -> params.headerMap?.let { |
|
||||||
webView.loadUrl(params.url, it) |
|
||||||
} |
|
||||||
} |
|
||||||
return webView |
|
||||||
} |
|
||||||
|
|
||||||
private fun destroyWebView() { |
|
||||||
mWebView?.destroy() |
|
||||||
mWebView = null |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fun load(params: AjaxParams) { |
|
||||||
if (params.sourceRegex != "") { |
|
||||||
mHandler.obtainMessage(MSG_SNIFF_START, params) |
|
||||||
.sendToTarget() |
|
||||||
} else { |
|
||||||
mHandler.obtainMessage(MSG_AJAX_START, params) |
|
||||||
.sendToTarget() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fun destroyWebView() { |
|
||||||
mHandler.obtainMessage(DESTROY_WEB_VIEW) |
|
||||||
} |
|
||||||
|
|
||||||
class AjaxParams(val url: String) { |
|
||||||
var tag: String? = null |
|
||||||
var requestMethod = RequestMethod.GET |
|
||||||
var postData: ByteArray? = null |
|
||||||
var headerMap: Map<String, String>? = null |
|
||||||
var sourceRegex: String? = null |
|
||||||
var javaScript: String? = null |
|
||||||
|
|
||||||
fun getJs(): String { |
|
||||||
javaScript?.let { |
|
||||||
if (it.isNotEmpty()) { |
|
||||||
return it |
|
||||||
} |
|
||||||
} |
|
||||||
return JS |
|
||||||
} |
|
||||||
|
|
||||||
val userAgent: String? |
|
||||||
get() = this.headerMap?.get(AppConst.UA_NAME) |
|
||||||
|
|
||||||
val isSniff: Boolean |
|
||||||
get() = !TextUtils.isEmpty(sourceRegex) |
|
||||||
|
|
||||||
fun setCookie(url: String) { |
|
||||||
tag?.let { |
|
||||||
val cookie = CookieManager.getInstance().getCookie(url) |
|
||||||
CookieStore.setCookie(it, cookie) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fun hasJavaScript(): Boolean { |
|
||||||
return !TextUtils.isEmpty(javaScript) |
|
||||||
} |
|
||||||
|
|
||||||
fun clearJavaScript() { |
|
||||||
javaScript = null |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
private class HtmlWebViewClient( |
|
||||||
private val params: AjaxParams, |
|
||||||
private val handler: Handler |
|
||||||
) : WebViewClient() { |
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView, url: String) { |
|
||||||
params.setCookie(url) |
|
||||||
val runnable = EvalJsRunnable(view, url, params.getJs(), handler) |
|
||||||
handler.postDelayed(runnable, 1000) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
private class EvalJsRunnable( |
|
||||||
webView: WebView, |
|
||||||
private val url: String, |
|
||||||
private val mJavaScript: String, |
|
||||||
private val handler: Handler |
|
||||||
) : Runnable { |
|
||||||
var retry = 0 |
|
||||||
private val mWebView: WeakReference<WebView> = WeakReference(webView) |
|
||||||
override fun run() { |
|
||||||
mWebView.get()?.evaluateJavascript(mJavaScript) { |
|
||||||
if (it.isNotEmpty() && it != "null") { |
|
||||||
val content = StringEscapeUtils.unescapeJson(it) |
|
||||||
.replace("^\"|\"$".toRegex(), "") |
|
||||||
try { |
|
||||||
val response = StrResponse(url, content) |
|
||||||
handler.obtainMessage(MSG_SUCCESS, response).sendToTarget() |
|
||||||
} catch (e: Exception) { |
|
||||||
handler.obtainMessage(MSG_ERROR, e).sendToTarget() |
|
||||||
} |
|
||||||
handler.removeCallbacks(this) |
|
||||||
return@evaluateJavascript |
|
||||||
} |
|
||||||
if (retry > 30) { |
|
||||||
handler.obtainMessage(MSG_ERROR, Exception("js执行超时")) |
|
||||||
.sendToTarget() |
|
||||||
handler.removeCallbacks(this) |
|
||||||
return@evaluateJavascript |
|
||||||
} |
|
||||||
retry++ |
|
||||||
handler.removeCallbacks(this) |
|
||||||
handler.postDelayed(this, 1000) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private class SnifferWebClient( |
|
||||||
private val params: AjaxParams, |
|
||||||
private val handler: Handler |
|
||||||
) : WebViewClient() { |
|
||||||
|
|
||||||
override fun onLoadResource(view: WebView, url: String) { |
|
||||||
params.sourceRegex?.let { |
|
||||||
if (url.matches(it.toRegex())) { |
|
||||||
try { |
|
||||||
val response = StrResponse(params.url, url) |
|
||||||
handler.obtainMessage(MSG_SUCCESS, response).sendToTarget() |
|
||||||
} catch (e: Exception) { |
|
||||||
handler.obtainMessage(MSG_ERROR, e).sendToTarget() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onPageFinished(view: WebView, url: String) { |
|
||||||
params.setCookie(url) |
|
||||||
if (params.hasJavaScript()) { |
|
||||||
evaluateJavascript(view, params.javaScript) |
|
||||||
params.clearJavaScript() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun evaluateJavascript(webView: WebView, javaScript: String?) { |
|
||||||
val runnable = LoadJsRunnable(webView, javaScript) |
|
||||||
handler.postDelayed(runnable, 1000L) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private class LoadJsRunnable( |
|
||||||
webView: WebView, |
|
||||||
private val mJavaScript: String? |
|
||||||
) : Runnable { |
|
||||||
private val mWebView: WeakReference<WebView> = WeakReference(webView) |
|
||||||
override fun run() { |
|
||||||
mWebView.get()?.loadUrl("javascript:${mJavaScript ?: ""}") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
companion object { |
|
||||||
const val MSG_AJAX_START = 0 |
|
||||||
const val MSG_SNIFF_START = 1 |
|
||||||
const val MSG_SUCCESS = 2 |
|
||||||
const val MSG_ERROR = 3 |
|
||||||
const val DESTROY_WEB_VIEW = 4 |
|
||||||
const val JS = "document.documentElement.outerHTML" |
|
||||||
} |
|
||||||
|
|
||||||
abstract class Callback { |
|
||||||
abstract fun onResult(response: StrResponse) |
|
||||||
abstract fun onError(error: Throwable) |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,208 @@ |
|||||||
|
package io.legado.app.help.http |
||||||
|
|
||||||
|
import android.annotation.SuppressLint |
||||||
|
import android.os.Handler |
||||||
|
import android.os.Looper |
||||||
|
import android.webkit.CookieManager |
||||||
|
import android.webkit.WebSettings |
||||||
|
import android.webkit.WebView |
||||||
|
import android.webkit.WebViewClient |
||||||
|
import io.legado.app.constant.AppConst |
||||||
|
import io.legado.app.model.NoStackTraceException |
||||||
|
import io.legado.app.utils.runOnUI |
||||||
|
import kotlinx.coroutines.* |
||||||
|
import org.apache.commons.text.StringEscapeUtils |
||||||
|
import splitties.init.appCtx |
||||||
|
import java.lang.ref.WeakReference |
||||||
|
import kotlin.coroutines.CoroutineContext |
||||||
|
import kotlin.coroutines.resume |
||||||
|
|
||||||
|
|
||||||
|
class BackstageWebView( |
||||||
|
private val url: String? = null, |
||||||
|
private val html: String? = null, |
||||||
|
private val encode: String? = null, |
||||||
|
private val tag: String? = null, |
||||||
|
private val headerMap: Map<String, String>? = null, |
||||||
|
private val sourceRegex: String? = null, |
||||||
|
private val javaScript: String? = null, |
||||||
|
) : CoroutineScope { |
||||||
|
private lateinit var job: Job |
||||||
|
private val mHandler = Handler(Looper.getMainLooper()) |
||||||
|
private var callback: Callback? = null |
||||||
|
private var mWebView: WebView? = null |
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext |
||||||
|
get() = Dispatchers.Main + job |
||||||
|
|
||||||
|
suspend fun getStrResponse(): StrResponse = suspendCancellableCoroutine { block -> |
||||||
|
job = Job() |
||||||
|
block.invokeOnCancellation { |
||||||
|
destroy() |
||||||
|
} |
||||||
|
callback = object : BackstageWebView.Callback() { |
||||||
|
override fun onResult(response: StrResponse) { |
||||||
|
if (!block.isCompleted) |
||||||
|
block.resume(response) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onError(error: Throwable) { |
||||||
|
if (!block.isCompleted) |
||||||
|
block.cancel(error) |
||||||
|
} |
||||||
|
} |
||||||
|
runOnUI { |
||||||
|
load() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun getEncoding(): String { |
||||||
|
return encode ?: "utf-8" |
||||||
|
} |
||||||
|
|
||||||
|
private fun load() { |
||||||
|
mWebView = createWebView().apply { |
||||||
|
when { |
||||||
|
!html.isNullOrEmpty() -> if (url.isNullOrEmpty()) { |
||||||
|
loadData(html, "text/html", getEncoding()) |
||||||
|
} else { |
||||||
|
loadDataWithBaseURL(url, html, "text/html", getEncoding(), url) |
||||||
|
} |
||||||
|
else -> if (headerMap == null) { |
||||||
|
loadUrl(url!!) |
||||||
|
} else { |
||||||
|
loadUrl(url!!, headerMap) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled", "JavascriptInterface") |
||||||
|
private fun createWebView(): WebView { |
||||||
|
val webView = WebView(appCtx) |
||||||
|
val settings = webView.settings |
||||||
|
settings.javaScriptEnabled = true |
||||||
|
settings.domStorageEnabled = true |
||||||
|
settings.blockNetworkImage = true |
||||||
|
settings.userAgentString = headerMap?.get(AppConst.UA_NAME) |
||||||
|
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW |
||||||
|
if (sourceRegex.isNullOrEmpty()) { |
||||||
|
webView.webViewClient = HtmlWebViewClient() |
||||||
|
} else { |
||||||
|
webView.webViewClient = SnifferWebClient() |
||||||
|
} |
||||||
|
return webView |
||||||
|
} |
||||||
|
|
||||||
|
private fun destroy() { |
||||||
|
launch { |
||||||
|
mWebView?.destroy() |
||||||
|
mWebView = null |
||||||
|
} |
||||||
|
job.cancel() |
||||||
|
} |
||||||
|
|
||||||
|
private fun getJs(): String { |
||||||
|
javaScript?.let { |
||||||
|
if (it.isNotEmpty()) { |
||||||
|
return it |
||||||
|
} |
||||||
|
} |
||||||
|
return JS |
||||||
|
} |
||||||
|
|
||||||
|
private fun setCookie(url: String) { |
||||||
|
tag?.let { |
||||||
|
val cookie = CookieManager.getInstance().getCookie(url) |
||||||
|
CookieStore.setCookie(it, cookie) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private inner class HtmlWebViewClient : WebViewClient() { |
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView, url: String) { |
||||||
|
setCookie(url) |
||||||
|
val runnable = EvalJsRunnable(view, url, getJs()) |
||||||
|
mHandler.postDelayed(runnable, 1000) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private inner class EvalJsRunnable( |
||||||
|
webView: WebView, |
||||||
|
private val url: String, |
||||||
|
private val mJavaScript: String |
||||||
|
) : Runnable { |
||||||
|
var retry = 0 |
||||||
|
private val mWebView: WeakReference<WebView> = WeakReference(webView) |
||||||
|
override fun run() { |
||||||
|
mWebView.get()?.evaluateJavascript(mJavaScript) { |
||||||
|
if (it.isNotEmpty() && it != "null") { |
||||||
|
val content = StringEscapeUtils.unescapeJson(it) |
||||||
|
.replace("^\"|\"$".toRegex(), "") |
||||||
|
try { |
||||||
|
val response = StrResponse(url, content) |
||||||
|
callback?.onResult(response) |
||||||
|
} catch (e: Exception) { |
||||||
|
callback?.onError(e) |
||||||
|
} |
||||||
|
mHandler.removeCallbacks(this) |
||||||
|
return@evaluateJavascript |
||||||
|
} |
||||||
|
if (retry > 30) { |
||||||
|
callback?.onError(NoStackTraceException("js执行超时")) |
||||||
|
mHandler.removeCallbacks(this) |
||||||
|
return@evaluateJavascript |
||||||
|
} |
||||||
|
retry++ |
||||||
|
mHandler.removeCallbacks(this) |
||||||
|
mHandler.postDelayed(this, 1000) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private inner class SnifferWebClient : WebViewClient() { |
||||||
|
|
||||||
|
override fun onLoadResource(view: WebView, resUrl: String) { |
||||||
|
sourceRegex?.let { |
||||||
|
if (resUrl.matches(it.toRegex())) { |
||||||
|
try { |
||||||
|
val response = StrResponse(url!!, resUrl) |
||||||
|
callback?.onResult(response) |
||||||
|
} catch (e: Exception) { |
||||||
|
callback?.onError(e) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onPageFinished(webView: WebView, url: String) { |
||||||
|
setCookie(url) |
||||||
|
val js = javaScript |
||||||
|
if (!js.isNullOrEmpty()) { |
||||||
|
val runnable = LoadJsRunnable(webView, javaScript) |
||||||
|
mHandler.postDelayed(runnable, 1000L) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private class LoadJsRunnable( |
||||||
|
webView: WebView, |
||||||
|
private val mJavaScript: String? |
||||||
|
) : Runnable { |
||||||
|
private val mWebView: WeakReference<WebView> = WeakReference(webView) |
||||||
|
override fun run() { |
||||||
|
mWebView.get()?.loadUrl("javascript:${mJavaScript ?: ""}") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
const val JS = "document.documentElement.outerHTML" |
||||||
|
} |
||||||
|
|
||||||
|
abstract class Callback { |
||||||
|
abstract fun onResult(response: StrResponse) |
||||||
|
abstract fun onError(error: Throwable) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue