mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-01 09:54:34 -05:00
* WebView: Add initial controller Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com> * WebView: Prepare page * WebView: Basic HTML setup * WebView: Improve navigation * WebView: Refactor message class deserialization * WebView: Refactor event message serialization * WebView: Handle click events * WebView: Fix events after refactor * WebView: Fix normalizing of URLs * WebView: HTML remove navigation buttons * WebView: Handle more events * WebView: Handle document change in events * WebView: Refactor to send mutation events * WebView: More mouse events * WebView: Include bubbles, cancelable in event Those seem to be important * WebView: Attempt to support nested iframe * WebView: Handle long titles * WebView: Avoid setting invalid url * WebView: Send mousemove * WebView: Start switch to canvas-based render * WebView: Send on every render * WebView: Dynamic size * WebView: Keyboard events * WebView: Handle mouse events in CEF This is important because JS can't click into iFrames, meaning the previous solution doesn't work for captchas * WebView: Cleanup * WebView: Cleanup 2 * WebView: Document title * WebView: Also send title on address change * WebView: Load and flush cookies from store * WebView: remove outdated TODOs * Offline WebView: Load cookies from store * Cleanup * Add KcefCookieManager, need to figure out how to inject it * ktLintFormat * Fix a few cookie bugs * Fix Webview on Windows * Minor cleanup * WebView: Remove /tmp image write, lint * Remove custom cookie manager * Multiple cookie fixes * Minor fix * Minor cleanup and add support for MacOS meta key * Get enter working * WebView HTML: Make responsive for mobile pages * WebView: Translate touch events to mouse scroll * WebView: Overlay an actual input to allow typing on mobile Browsers will only show the keyboard if an input is focused. This also removes the `tabstop` hack. * WebView: Protect against occasional NullPointerException * WebView: Use float for clientX/Y * WebView: Fix ChromeAndroid being a pain * Simplify enter fix * NetworkHelper: Fix cache * Improve CookieStore url matching, fix another cookie conversion issue * Move distinctBy * WebView: Mouse direction toggle * Remove accidentally copied comment --------- Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
1112 lines
38 KiB
Kotlin
1112 lines
38 KiB
Kotlin
package xyz.nulldev.androidcompat.webkit
|
|
|
|
import android.content.Intent
|
|
import android.content.res.Configuration
|
|
import android.graphics.Bitmap
|
|
import android.graphics.Canvas
|
|
import android.graphics.Paint
|
|
import android.graphics.Picture
|
|
import android.graphics.Rect
|
|
import android.graphics.drawable.Drawable
|
|
import android.net.Uri
|
|
import android.net.http.SslCertificate
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.os.Message
|
|
import android.print.PrintDocumentAdapter
|
|
import android.util.Log
|
|
import android.util.SparseArray
|
|
import android.view.DragEvent
|
|
import android.view.KeyEvent
|
|
import android.view.MotionEvent
|
|
import android.view.PointerIcon
|
|
import android.view.View
|
|
import android.view.ViewGroup.LayoutParams
|
|
import android.view.WindowInsets
|
|
import android.view.accessibility.AccessibilityEvent
|
|
import android.view.accessibility.AccessibilityNodeInfo
|
|
import android.view.accessibility.AccessibilityNodeProvider
|
|
import android.view.autofill.AutofillValue
|
|
import android.view.inputmethod.EditorInfo
|
|
import android.view.inputmethod.InputConnection
|
|
import android.view.textclassifier.TextClassifier
|
|
import android.webkit.DownloadListener
|
|
import android.webkit.RenderProcessGoneDetail
|
|
import android.webkit.ValueCallback
|
|
import android.webkit.WebBackForwardList
|
|
import android.webkit.WebChromeClient
|
|
import android.webkit.WebMessage
|
|
import android.webkit.WebMessagePort
|
|
import android.webkit.WebResourceRequest
|
|
import android.webkit.WebResourceResponse
|
|
import android.webkit.WebSettings
|
|
import android.webkit.WebView
|
|
import android.webkit.WebView.HitTestResult
|
|
import android.webkit.WebView.PictureListener
|
|
import android.webkit.WebView.VisualStateCallback
|
|
import android.webkit.WebViewClient
|
|
import android.webkit.WebViewProvider
|
|
import android.webkit.WebViewProvider.ScrollDelegate
|
|
import android.webkit.WebViewProvider.ViewDelegate
|
|
import android.webkit.WebViewRenderProcess
|
|
import android.webkit.WebViewRenderProcessClient
|
|
import dev.datlag.kcef.KCEF
|
|
import dev.datlag.kcef.KCEFBrowser
|
|
import dev.datlag.kcef.KCEFClient
|
|
import dev.datlag.kcef.KCEFResourceRequestHandler
|
|
import kotlinx.serialization.Serializable
|
|
import kotlinx.serialization.decodeFromString
|
|
import kotlinx.serialization.json.Json
|
|
import org.cef.CefSettings
|
|
import org.cef.browser.CefBrowser
|
|
import org.cef.browser.CefFrame
|
|
import org.cef.browser.CefMessageRouter
|
|
import org.cef.browser.CefRendering
|
|
import org.cef.browser.CefRequestContext
|
|
import org.cef.callback.CefCallback
|
|
import org.cef.callback.CefQueryCallback
|
|
import org.cef.handler.CefDisplayHandlerAdapter
|
|
import org.cef.handler.CefLoadHandler
|
|
import org.cef.handler.CefLoadHandlerAdapter
|
|
import org.cef.handler.CefMessageRouterHandlerAdapter
|
|
import org.cef.handler.CefRequestHandler
|
|
import org.cef.handler.CefRequestHandlerAdapter
|
|
import org.cef.handler.CefResourceHandler
|
|
import org.cef.handler.CefResourceHandlerAdapter
|
|
import org.cef.handler.CefResourceRequestHandler
|
|
import org.cef.handler.CefResourceRequestHandlerAdapter
|
|
import org.cef.misc.BoolRef
|
|
import org.cef.misc.IntRef
|
|
import org.cef.misc.StringRef
|
|
import org.cef.network.CefPostData
|
|
import org.cef.network.CefPostDataElement
|
|
import org.cef.network.CefRequest
|
|
import org.cef.network.CefResponse
|
|
import org.koin.mp.KoinPlatformTools
|
|
import java.io.BufferedWriter
|
|
import java.io.File
|
|
import java.io.IOException
|
|
import java.util.concurrent.Executor
|
|
import kotlin.collections.Map
|
|
import kotlin.reflect.KClass
|
|
import kotlin.reflect.KFunction
|
|
import kotlin.reflect.full.declaredMemberFunctions
|
|
import kotlin.reflect.jvm.javaMethod
|
|
|
|
class KcefWebViewProvider(
|
|
private val view: WebView,
|
|
) : WebViewProvider {
|
|
private val settings = KcefWebSettings()
|
|
private var viewClient = WebViewClient()
|
|
private var chromeClient = WebChromeClient()
|
|
private val mappings: MutableList<FunctionMapping> = mutableListOf()
|
|
private val urlHttpMapping: MutableMap<String, String> = mutableMapOf()
|
|
|
|
private var kcefClient: KCEFClient? = null
|
|
private var browser: KCEFBrowser? = null
|
|
|
|
private val handler = Handler(view.webViewLooper)
|
|
|
|
companion object {
|
|
const val TAG = "KcefWebViewProvider"
|
|
const val QUERY_FN = "__\$_suwayomiQuery"
|
|
const val QUERY_CANCEL_FN = "__\$_suwayomiQueryCancel"
|
|
|
|
private val initHandler: InitBrowserHandler by KoinPlatformTools.defaultContext().get().inject()
|
|
}
|
|
|
|
public interface InitBrowserHandler {
|
|
public fun init(provider: KcefWebViewProvider): Unit
|
|
}
|
|
|
|
private class CefWebResourceRequest(
|
|
val request: CefRequest?,
|
|
val frame: CefFrame?,
|
|
val redirect: Boolean,
|
|
) : WebResourceRequest {
|
|
override fun getUrl(): Uri = Uri.parse(request?.url)
|
|
|
|
override fun isForMainFrame(): Boolean = frame?.isMain ?: false
|
|
|
|
override fun isRedirect(): Boolean = redirect
|
|
|
|
override fun hasGesture(): Boolean = false
|
|
|
|
override fun getMethod(): String = request?.method ?: "GET"
|
|
|
|
override fun getRequestHeaders(): Map<String, String> {
|
|
val headers = mutableMapOf<String, String>()
|
|
request?.getHeaderMap(headers)
|
|
return headers
|
|
}
|
|
}
|
|
|
|
private inner class DisplayHandler : CefDisplayHandlerAdapter() {
|
|
override fun onConsoleMessage(
|
|
browser: CefBrowser,
|
|
level: CefSettings.LogSeverity,
|
|
message: String,
|
|
source: String,
|
|
line: Int,
|
|
): Boolean {
|
|
Log.v(TAG, "$source:$line[$level]: $message")
|
|
return true
|
|
}
|
|
|
|
override fun onAddressChange(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
url: String,
|
|
) {
|
|
Log.d(TAG, "Navigate to $url")
|
|
}
|
|
|
|
override fun onStatusMessage(
|
|
browser: CefBrowser,
|
|
value: String,
|
|
) {
|
|
Log.v(TAG, "Status update: $value")
|
|
}
|
|
}
|
|
|
|
private inner class LoadHandler : CefLoadHandlerAdapter() {
|
|
override fun onLoadEnd(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
httpStatusCode: Int,
|
|
) {
|
|
val url = frame.url ?: ""
|
|
Log.v(TAG, "Load end $url")
|
|
handler.post {
|
|
if (httpStatusCode == 404) {
|
|
viewClient.onReceivedError(
|
|
view,
|
|
WebViewClient.ERROR_FILE_NOT_FOUND,
|
|
"Not Found",
|
|
url,
|
|
)
|
|
}
|
|
if (httpStatusCode == 429) {
|
|
viewClient.onReceivedError(
|
|
view,
|
|
WebViewClient.ERROR_TOO_MANY_REQUESTS,
|
|
"Too Many Requests",
|
|
url,
|
|
)
|
|
}
|
|
if (httpStatusCode >= 400) {
|
|
// TODO: create request and response
|
|
// viewClient.onReceivedHttpError(_view, ...);
|
|
}
|
|
viewClient.onPageFinished(view, url)
|
|
chromeClient.onProgressChanged(view, 100)
|
|
}
|
|
}
|
|
|
|
override fun onLoadError(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
errorCode: CefLoadHandler.ErrorCode,
|
|
errorText: String,
|
|
failedUrl: String,
|
|
) {
|
|
Log.w(TAG, "Load error ($failedUrl) [$errorCode]: $errorText")
|
|
// TODO: translate correctly
|
|
handler.post {
|
|
viewClient.onReceivedError(view, WebViewClient.ERROR_UNKNOWN, errorText, frame.url)
|
|
}
|
|
}
|
|
|
|
override fun onLoadStart(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
transitionType: CefRequest.TransitionType,
|
|
) {
|
|
Log.v(TAG, "Load start, pushing mappings")
|
|
mappings.forEach {
|
|
val js =
|
|
"""
|
|
window.${it.interfaceName} = window.${it.interfaceName} || {}
|
|
window.${it.interfaceName}.${it.functionName} = async function() {
|
|
const args = await Promise.all(Array.from(arguments));
|
|
return new Promise((resolve, reject) => {
|
|
window.${QUERY_FN}({
|
|
request: JSON.stringify({
|
|
functionName: ${Json.encodeToString(it.functionName)},
|
|
interfaceName: ${Json.encodeToString(it.interfaceName)},
|
|
args,
|
|
}),
|
|
persistent: false,
|
|
onSuccess: resolve,
|
|
onFailure: (_, err) => reject(err),
|
|
})
|
|
});
|
|
}
|
|
"""
|
|
browser.executeJavaScript(js, "SUWAYOMI ${it.toNice()}", 0)
|
|
}
|
|
|
|
handler.post { viewClient.onPageStarted(view, frame.url, null) }
|
|
}
|
|
}
|
|
|
|
private inner class MessageRouterHandler : CefMessageRouterHandlerAdapter() {
|
|
override fun onQuery(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
queryId: Long,
|
|
request: String,
|
|
persistent: Boolean,
|
|
callback: CefQueryCallback,
|
|
): Boolean {
|
|
val invoke =
|
|
try {
|
|
Json.decodeFromString<FunctionCall>(request)
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, "Invalid request received $e")
|
|
return false
|
|
}
|
|
// TODO: Use a map
|
|
mappings
|
|
.find {
|
|
it.functionName == invoke.functionName &&
|
|
it.interfaceName == invoke.interfaceName
|
|
}?.let {
|
|
handler.post {
|
|
try {
|
|
Log.v(
|
|
TAG,
|
|
"Received request to invoke ${it.toNice()} with ${invoke.args.size} args",
|
|
)
|
|
// NOTE: first argument is
|
|
// implicitly this
|
|
val retval = it.fn.call(it.obj, *invoke.args)
|
|
callback.success(retval.toString())
|
|
} catch (e: Exception) {
|
|
Log.w(TAG, "JS-invoke on ${it.toNice()} failed:", e)
|
|
callback.failure(0, e.message)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
private abstract class ArrayResponseResourceHandler : CefResourceHandlerAdapter() {
|
|
protected var resolvedData: ByteArray? = null
|
|
protected var readOffset = 0
|
|
|
|
override fun getResponseHeaders(
|
|
response: CefResponse,
|
|
responseLength: IntRef,
|
|
redirectUrl: StringRef,
|
|
) {
|
|
responseLength.set(resolvedData?.size ?: 0)
|
|
response.status = 200
|
|
response.statusText = "OK"
|
|
response.mimeType = "text/html"
|
|
}
|
|
|
|
override fun readResponse(
|
|
dataOut: ByteArray,
|
|
bytesToRead: Int,
|
|
bytesRead: IntRef,
|
|
callback: CefCallback,
|
|
): Boolean {
|
|
val data = resolvedData ?: return false
|
|
val bytesToTransfer = Math.min(bytesToRead, data.size - readOffset)
|
|
Log.v(
|
|
TAG,
|
|
"readResponse: $readOffset/${data.size}, reading $bytesToRead->$bytesToTransfer",
|
|
)
|
|
data.copyInto(dataOut, startIndex = readOffset, endIndex = readOffset + bytesToTransfer)
|
|
bytesRead.set(bytesToTransfer)
|
|
readOffset += bytesToTransfer
|
|
return bytesToTransfer != 0
|
|
}
|
|
}
|
|
|
|
private inner class WebResponseResourceHandler(
|
|
val webResponse: WebResourceResponse,
|
|
) : ArrayResponseResourceHandler() {
|
|
override fun processRequest(
|
|
request: CefRequest,
|
|
callback: CefCallback,
|
|
): Boolean {
|
|
Log.v(TAG, "Handling request from client's response for ${request.url}")
|
|
try {
|
|
resolvedData = webResponse.data.readAllBytes()
|
|
} catch (e: IOException) {
|
|
}
|
|
callback.Continue()
|
|
return true
|
|
}
|
|
|
|
override fun getResponseHeaders(
|
|
response: CefResponse,
|
|
responseLength: IntRef,
|
|
redirectUrl: StringRef,
|
|
) {
|
|
super.getResponseHeaders(response, responseLength, redirectUrl)
|
|
webResponse.responseHeaders.forEach { response.setHeaderByName(it.key, it.value, true) }
|
|
response.status = webResponse.statusCode
|
|
response.mimeType = webResponse.mimeType
|
|
}
|
|
}
|
|
|
|
private inner class HtmlResponseResourceHandler(
|
|
val html: String,
|
|
) : ArrayResponseResourceHandler() {
|
|
override fun processRequest(
|
|
request: CefRequest,
|
|
callback: CefCallback,
|
|
): Boolean {
|
|
Log.v(TAG, "Handling request from HTML cache for ${request.url}")
|
|
resolvedData = html.toByteArray()
|
|
callback.Continue()
|
|
return true
|
|
}
|
|
}
|
|
|
|
private inner class ResourceRequestHandler : CefResourceRequestHandlerAdapter() {
|
|
override fun onBeforeResourceLoad(
|
|
browser: CefBrowser?,
|
|
frame: CefFrame?,
|
|
request: CefRequest,
|
|
): Boolean {
|
|
request.setHeaderByName("user-agent", settings.userAgentString, true)
|
|
|
|
// TODO: we should be calling this on the handler, since CEF calls us on its IO thread
|
|
// thus if a client tried to use WebView#loadUrl as the docs suggest, this fails
|
|
val cancel =
|
|
viewClient.shouldOverrideUrlLoading(
|
|
view,
|
|
CefWebResourceRequest(request, frame, false),
|
|
)
|
|
Log.v(TAG, "Resource ${request?.url}, result is cancel? $cancel")
|
|
|
|
handler.post { viewClient.onLoadResource(view, frame?.url) }
|
|
|
|
return cancel || settings.blockNetworkLoads
|
|
}
|
|
|
|
override fun getResourceHandler(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
request: CefRequest,
|
|
): CefResourceHandler? {
|
|
// TODO: we should be calling this on the handler, since CEF calls us on its IO thread
|
|
val response =
|
|
viewClient.shouldInterceptRequest(
|
|
view,
|
|
CefWebResourceRequest(request, frame, false),
|
|
)
|
|
if (response == null) {
|
|
// prefer user's response override
|
|
urlHttpMapping.get(request.url)?.let {
|
|
return HtmlResponseResourceHandler(it)
|
|
}
|
|
}
|
|
response ?: return null
|
|
return WebResponseResourceHandler(response)
|
|
}
|
|
}
|
|
|
|
private inner class RequestHandler : CefRequestHandlerAdapter() {
|
|
override fun getResourceRequestHandler(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
request: CefRequest,
|
|
isNavigation: Boolean,
|
|
isDownload: Boolean,
|
|
requestInitiator: String,
|
|
disableDefaultHandling: BoolRef,
|
|
): CefResourceRequestHandler? = ResourceRequestHandler()
|
|
|
|
override fun onRenderProcessTerminated(
|
|
browser: CefBrowser,
|
|
status: CefRequestHandler.TerminationStatus,
|
|
) {
|
|
handler.post {
|
|
viewClient.onRenderProcessGone(
|
|
view,
|
|
object : RenderProcessGoneDetail() {
|
|
override fun didCrash(): Boolean = status == CefRequestHandler.TerminationStatus.TS_PROCESS_CRASHED
|
|
|
|
override fun rendererPriorityAtExit(): Int = -1
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun init(
|
|
javaScriptInterfaces: Map<String, Any>?,
|
|
privateBrowsing: Boolean,
|
|
) {
|
|
Log.v(TAG, "KcefWebViewProvider: initialize")
|
|
destroy()
|
|
kcefClient =
|
|
KCEF.newClientBlocking().apply {
|
|
addDisplayHandler(DisplayHandler())
|
|
addLoadHandler(LoadHandler())
|
|
addRequestHandler(RequestHandler())
|
|
|
|
val config = CefMessageRouter.CefMessageRouterConfig()
|
|
config.jsQueryFunction = QUERY_FN
|
|
config.jsCancelFunction = QUERY_CANCEL_FN
|
|
addMessageRouter(CefMessageRouter.create(config, MessageRouterHandler()))
|
|
}
|
|
initHandler.init(this)
|
|
}
|
|
|
|
// Deprecated - should never be called
|
|
override fun setHorizontalScrollbarOverlay(overlay: Boolean): Unit = throw RuntimeException("Stub!")
|
|
|
|
// Deprecated - should never be called
|
|
override fun setVerticalScrollbarOverlay(overlay: Boolean): Unit = throw RuntimeException("Stub!")
|
|
|
|
// Deprecated - should never be called
|
|
override fun overlayHorizontalScrollbar(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
// Deprecated - should never be called
|
|
override fun overlayVerticalScrollbar(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun getVisibleTitleHeight(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun getCertificate(): SslCertificate = throw RuntimeException("Stub!")
|
|
|
|
override fun setCertificate(certificate: SslCertificate): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun savePassword(
|
|
host: String,
|
|
username: String,
|
|
password: String,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun setHttpAuthUsernamePassword(
|
|
host: String,
|
|
realm: String,
|
|
username: String,
|
|
password: String,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun getHttpAuthUsernamePassword(
|
|
host: String,
|
|
realm: String,
|
|
): Array<String> = throw RuntimeException("Stub!")
|
|
|
|
override fun destroy() {
|
|
browser?.close(true)
|
|
browser?.dispose()
|
|
browser = null
|
|
kcefClient?.dispose()
|
|
kcefClient = null
|
|
}
|
|
|
|
override fun setNetworkAvailable(networkUp: Boolean): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun saveState(outState: Bundle): WebBackForwardList = throw RuntimeException("Stub!")
|
|
|
|
override fun savePicture(
|
|
b: Bundle,
|
|
dest: File,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun restorePicture(
|
|
b: Bundle,
|
|
src: File,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun restoreState(inState: Bundle): WebBackForwardList = throw RuntimeException("Stub!")
|
|
|
|
override fun loadUrl(
|
|
loadUrl: String,
|
|
additionalHttpHeaders: Map<String, String>,
|
|
) {
|
|
browser?.close(true)
|
|
browser?.dispose()
|
|
chromeClient.onProgressChanged(view, 0)
|
|
browser =
|
|
kcefClient!!
|
|
.createBrowser(
|
|
loadUrl,
|
|
CefRendering.OFFSCREEN,
|
|
context = createContext(additionalHttpHeaders),
|
|
).apply {
|
|
// NOTE: Without this, we don't seem to be receiving any events
|
|
createImmediately()
|
|
}
|
|
Log.d(TAG, "Page loaded at URL $loadUrl")
|
|
}
|
|
|
|
override fun loadUrl(url: String) {
|
|
loadUrl(url, mapOf())
|
|
}
|
|
|
|
override fun postUrl(
|
|
url: String,
|
|
postData: ByteArray,
|
|
) {
|
|
browser?.close(true)
|
|
browser?.dispose()
|
|
chromeClient.onProgressChanged(view, 0)
|
|
browser =
|
|
kcefClient!!
|
|
.createBrowser(
|
|
url,
|
|
CefRendering.OFFSCREEN,
|
|
context = createContext(postData = postData),
|
|
).apply {
|
|
// NOTE: Without this, we don't seem to be receiving any events
|
|
createImmediately()
|
|
}
|
|
Log.d(TAG, "Page posted at URL $url")
|
|
}
|
|
|
|
override fun loadData(
|
|
data: String,
|
|
mimeType: String,
|
|
encoding: String,
|
|
) {
|
|
loadDataWithBaseURL(null, data, mimeType, encoding, null)
|
|
}
|
|
|
|
override fun loadDataWithBaseURL(
|
|
baseUrl: String?,
|
|
data: String,
|
|
mimeType: String,
|
|
encoding: String,
|
|
historyUrl: String?,
|
|
) {
|
|
browser?.close(true)
|
|
browser?.dispose()
|
|
chromeClient.onProgressChanged(view, 0)
|
|
|
|
browser =
|
|
(
|
|
baseUrl?.let { url ->
|
|
urlHttpMapping.put(url, data)
|
|
kcefClient!!.createBrowser(
|
|
url,
|
|
CefRendering.OFFSCREEN,
|
|
)
|
|
}
|
|
?: run {
|
|
kcefClient!!.createBrowserWithHtml(
|
|
data,
|
|
KCEFBrowser.BLANK_URI,
|
|
CefRendering.OFFSCREEN,
|
|
)
|
|
}
|
|
).apply {
|
|
// NOTE: Without this, we don't seem to be receiving any events
|
|
createImmediately()
|
|
}
|
|
Log.d(TAG, "Page loaded from data at base URL $baseUrl")
|
|
}
|
|
|
|
override fun evaluateJavaScript(
|
|
script: String,
|
|
resultCallback: ValueCallback<String>,
|
|
) {
|
|
browser!!.evaluateJavaScript(
|
|
script.removePrefix("javascript:"),
|
|
{
|
|
Log.v(TAG, "JS returned: $it")
|
|
it?.let { handler.post { resultCallback.onReceiveValue(it) } }
|
|
},
|
|
)
|
|
}
|
|
|
|
override fun saveWebArchive(filename: String): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun saveWebArchive(
|
|
basename: String,
|
|
autoname: Boolean,
|
|
callback: ValueCallback<String>,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun stopLoading() {
|
|
browser!!.stopLoad()
|
|
}
|
|
|
|
override fun reload() {
|
|
browser!!.reload()
|
|
}
|
|
|
|
override fun canGoBack(): Boolean = browser!!.canGoBack()
|
|
|
|
override fun goBack() {
|
|
browser!!.goBack()
|
|
}
|
|
|
|
override fun canGoForward(): Boolean = browser!!.canGoForward()
|
|
|
|
override fun goForward() {
|
|
browser!!.goForward()
|
|
}
|
|
|
|
override fun canGoBackOrForward(steps: Int): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun goBackOrForward(steps: Int): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun isPrivateBrowsingEnabled(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun pageUp(top: Boolean): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun pageDown(bottom: Boolean): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun insertVisualStateCallback(
|
|
requestId: Long,
|
|
callback: VisualStateCallback,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun clearView(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun capturePicture(): Picture = throw RuntimeException("Stub!")
|
|
|
|
override fun createPrintDocumentAdapter(documentName: String): PrintDocumentAdapter = throw RuntimeException("Stub!")
|
|
|
|
override fun getScale(): Float = throw RuntimeException("Stub!")
|
|
|
|
override fun setInitialScale(scaleInPercent: Int): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun invokeZoomPicker(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun getHitTestResult(): HitTestResult = throw RuntimeException("Stub!")
|
|
|
|
override fun requestFocusNodeHref(hrefMsg: Message): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun requestImageRef(msg: Message): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun getUrl(): String = browser!!.url
|
|
|
|
override fun getOriginalUrl(): String = browser!!.url
|
|
|
|
override fun getTitle(): String = throw RuntimeException("Stub!")
|
|
|
|
override fun getFavicon(): Bitmap = throw RuntimeException("Stub!")
|
|
|
|
override fun getTouchIconUrl(): String = throw RuntimeException("Stub!")
|
|
|
|
override fun getProgress(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun getContentHeight(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun getContentWidth(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun pauseTimers(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun resumeTimers(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onPause(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onResume(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun isPaused(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun freeMemory(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun clearCache(includeDiskFiles: Boolean): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun clearFormData(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun clearHistory(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun clearSslPreferences(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun copyBackForwardList(): WebBackForwardList = throw RuntimeException("Stub!")
|
|
|
|
override fun setFindListener(listener: WebView.FindListener): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun findNext(forward: Boolean): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun findAll(find: String): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun findAllAsync(find: String): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun showFindDialog(
|
|
text: String,
|
|
showIme: Boolean,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun clearMatches(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun documentHasImages(response: Message): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun setWebViewClient(client: WebViewClient) {
|
|
viewClient = client
|
|
}
|
|
|
|
override fun getWebViewClient(): WebViewClient = viewClient
|
|
|
|
override fun getWebViewRenderProcess(): WebViewRenderProcess? = throw RuntimeException("Stub!")
|
|
|
|
override fun setWebViewRenderProcessClient(
|
|
executor: Executor?,
|
|
client: WebViewRenderProcessClient?,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun getWebViewRenderProcessClient(): WebViewRenderProcessClient? = throw RuntimeException("Stub!")
|
|
|
|
override fun setDownloadListener(listener: DownloadListener): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun setWebChromeClient(client: WebChromeClient) {
|
|
chromeClient = client
|
|
}
|
|
|
|
override fun getWebChromeClient(): WebChromeClient = chromeClient
|
|
|
|
override fun setPictureListener(listener: PictureListener): Unit = throw RuntimeException("Stub!")
|
|
|
|
@Serializable
|
|
private data class FunctionCall(
|
|
val interfaceName: String,
|
|
val functionName: String,
|
|
val args: Array<String>,
|
|
)
|
|
|
|
private data class FunctionMapping(
|
|
val interfaceName: String,
|
|
val functionName: String,
|
|
val obj: Any,
|
|
val fn: KFunction<*>,
|
|
) {
|
|
fun toNice(): String = "$interfaceName.$functionName"
|
|
}
|
|
|
|
override fun addJavascriptInterface(
|
|
obj: Any,
|
|
interfaceName: String,
|
|
) {
|
|
val cls = obj::class as KClass<Any>
|
|
mappings.addAll(
|
|
cls.declaredMemberFunctions.map {
|
|
// This is ridiculous, but necessary, otherwise "public final" throws
|
|
it.javaMethod?.isAccessible = true
|
|
val map = FunctionMapping(interfaceName, it.name, obj, it)
|
|
Log.v(TAG, "Exposing: ${map.toNice()}")
|
|
map
|
|
},
|
|
)
|
|
}
|
|
|
|
override fun removeJavascriptInterface(interfaceName: String): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun createWebMessageChannel(): Array<WebMessagePort> = throw RuntimeException("Stub!")
|
|
|
|
override fun postMessageToMainFrame(
|
|
message: WebMessage,
|
|
targetOrigin: Uri,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun getSettings(): WebSettings = settings
|
|
|
|
override fun setMapTrackballToArrowKeys(setMap: Boolean): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun flingScroll(
|
|
vx: Int,
|
|
vy: Int,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun getZoomControls(): View = throw RuntimeException("Stub!")
|
|
|
|
override fun canZoomIn(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun canZoomOut(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun zoomBy(zoomFactor: Float): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun zoomIn(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun zoomOut(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun dumpViewHierarchyWithProperties(
|
|
out: BufferedWriter,
|
|
level: Int,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun findHierarchyView(
|
|
className: String,
|
|
hashCode: Int,
|
|
): View = throw RuntimeException("Stub!")
|
|
|
|
override fun setRendererPriorityPolicy(
|
|
rendererRequestedPriority: Int,
|
|
waivedWhenNotVisible: Boolean,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun getRendererRequestedPriority(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun getRendererPriorityWaivedWhenNotVisible(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
@SuppressWarnings("unused")
|
|
override fun setTextClassifier(textClassifier: TextClassifier?) {}
|
|
|
|
override fun getTextClassifier(): TextClassifier = TextClassifier.NO_OP
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Provider internal methods
|
|
// -------------------------------------------------------------------------
|
|
|
|
override fun getViewDelegate(): ViewDelegate = throw RuntimeException("Stub!")
|
|
|
|
override fun getScrollDelegate(): ScrollDelegate = throw RuntimeException("Stub!")
|
|
|
|
override fun notifyFindDialogDismissed(): Unit = throw RuntimeException("Stub!")
|
|
|
|
// -------------------------------------------------------------------------
|
|
// View / ViewGroup delegation methods
|
|
// -------------------------------------------------------------------------
|
|
|
|
class KcefViewDelegate : ViewDelegate {
|
|
override fun shouldDelayChildPressedState(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onProvideVirtualStructure(structure: android.view.ViewStructure): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onProvideAutofillVirtualStructure(
|
|
@SuppressWarnings("unused") structure: android.view.ViewStructure,
|
|
@SuppressWarnings("unused") flags: Int,
|
|
) {}
|
|
|
|
override fun autofill(
|
|
@SuppressWarnings("unused") values: SparseArray<AutofillValue>,
|
|
) {}
|
|
|
|
override fun isVisibleToUserForAutofill(
|
|
@SuppressWarnings("unused") virtualId: Int,
|
|
): Boolean {
|
|
return true // true is the default value returned by View.isVisibleToUserForAutofill()
|
|
}
|
|
|
|
override fun onProvideContentCaptureStructure(
|
|
@SuppressWarnings("unused") structure: android.view.ViewStructure,
|
|
@SuppressWarnings("unused") flags: Int,
|
|
) {}
|
|
|
|
override fun getAccessibilityNodeProvider(): AccessibilityNodeProvider = throw RuntimeException("Stub!")
|
|
|
|
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onInitializeAccessibilityEvent(event: AccessibilityEvent): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun performAccessibilityAction(
|
|
action: Int,
|
|
arguments: Bundle,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun setOverScrollMode(mode: Int): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun setScrollBarStyle(style: Int): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onDrawVerticalScrollBar(
|
|
canvas: Canvas,
|
|
scrollBar: Drawable,
|
|
l: Int,
|
|
t: Int,
|
|
r: Int,
|
|
b: Int,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onOverScrolled(
|
|
scrollX: Int,
|
|
scrollY: Int,
|
|
clampedX: Boolean,
|
|
clampedY: Boolean,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onWindowVisibilityChanged(visibility: Int): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onDraw(canvas: Canvas): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun setLayoutParams(layoutParams: LayoutParams): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun performLongClick(): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onConfigurationChanged(newConfig: Configuration): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection = throw RuntimeException("Stub!")
|
|
|
|
override fun onDragEvent(event: DragEvent): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onKeyMultiple(
|
|
keyCode: Int,
|
|
repeatCount: Int,
|
|
event: KeyEvent,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onKeyDown(
|
|
keyCode: Int,
|
|
event: KeyEvent,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onKeyUp(
|
|
keyCode: Int,
|
|
event: KeyEvent,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onAttachedToWindow(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onDetachedFromWindow(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onMovedToDisplay(
|
|
displayId: Int,
|
|
config: Configuration,
|
|
) {}
|
|
|
|
override fun onVisibilityChanged(
|
|
changedView: View,
|
|
visibility: Int,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onWindowFocusChanged(hasWindowFocus: Boolean): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onFocusChanged(
|
|
focused: Boolean,
|
|
direction: Int,
|
|
previouslyFocusedRect: Rect,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun setFrame(
|
|
left: Int,
|
|
top: Int,
|
|
right: Int,
|
|
bottom: Int,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onSizeChanged(
|
|
w: Int,
|
|
h: Int,
|
|
ow: Int,
|
|
oh: Int,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onScrollChanged(
|
|
l: Int,
|
|
t: Int,
|
|
oldl: Int,
|
|
oldt: Int,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onTouchEvent(ev: MotionEvent): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onHoverEvent(event: MotionEvent): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onGenericMotionEvent(event: MotionEvent): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onTrackballEvent(ev: MotionEvent): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun requestFocus(
|
|
direction: Int,
|
|
previouslyFocusedRect: Rect,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun onMeasure(
|
|
widthMeasureSpec: Int,
|
|
heightMeasureSpec: Int,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun requestChildRectangleOnScreen(
|
|
child: View,
|
|
rect: Rect,
|
|
immediate: Boolean,
|
|
): Boolean = throw RuntimeException("Stub!")
|
|
|
|
override fun setBackgroundColor(color: Int): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun setLayerType(
|
|
layerType: Int,
|
|
paint: Paint,
|
|
) {
|
|
// ignore
|
|
}
|
|
|
|
override fun preDispatchDraw(canvas: Canvas): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onStartTemporaryDetach(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onFinishTemporaryDetach(): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun onActivityResult(
|
|
requestCode: Int,
|
|
resultCode: Int,
|
|
data: Intent,
|
|
): Unit = throw RuntimeException("Stub!")
|
|
|
|
override fun getHandler(originalHandler: Handler): Handler = throw RuntimeException("Stub!")
|
|
|
|
override fun findFocus(originalFocusedView: View): View = throw RuntimeException("Stub!")
|
|
|
|
@SuppressWarnings("unused")
|
|
override fun onCheckIsTextEditor(): Boolean = false
|
|
|
|
@SuppressWarnings("unused")
|
|
override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets? = null
|
|
|
|
@SuppressWarnings("unused")
|
|
override fun onResolvePointerIcon(
|
|
event: MotionEvent,
|
|
pointerIndex: Int,
|
|
): PointerIcon? = null
|
|
}
|
|
|
|
class KcefScrollDelegate : ScrollDelegate {
|
|
override fun computeHorizontalScrollRange(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun computeHorizontalScrollOffset(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun computeVerticalScrollRange(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun computeVerticalScrollOffset(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun computeVerticalScrollExtent(): Int = throw RuntimeException("Stub!")
|
|
|
|
override fun computeScroll(): Unit = throw RuntimeException("Stub!")
|
|
}
|
|
|
|
private fun createContext(
|
|
additionalHttpHeaders: Map<String, String>? = null,
|
|
postData: ByteArray? = null,
|
|
): CefRequestContext =
|
|
CefRequestContext.createContext {
|
|
browser,
|
|
frame,
|
|
request,
|
|
isNavigation,
|
|
isDownload,
|
|
requestInitiator,
|
|
disableDefaultHandling,
|
|
->
|
|
KCEFResourceRequestHandler.globalHandler(
|
|
browser,
|
|
frame,
|
|
request.apply {
|
|
if (!additionalHttpHeaders.isNullOrEmpty()) {
|
|
additionalHttpHeaders.forEach {
|
|
setHeaderByName(it.key, it.value, true)
|
|
}
|
|
}
|
|
|
|
if (postData != null) {
|
|
this.postData =
|
|
CefPostData.create().apply {
|
|
addElement(
|
|
CefPostDataElement.create().apply {
|
|
setToBytes(postData.size, postData)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
},
|
|
isNavigation,
|
|
isDownload,
|
|
requestInitiator,
|
|
disableDefaultHandling,
|
|
)
|
|
}
|
|
}
|