Files
Suwayomi-Server/AndroidCompat/src/main/java/xyz/nulldev/androidcompat/webkit/KcefWebViewProvider.kt
Constantin Piber 00861d7750 Switch to JCEF (#2038)
* Switch to JCEF

This is a full implementation, but it does not yet include downloading
CEF as KCEF did

* Download CEF automatically

* Handle and propagate CEF init errors

* Lint

* Simplify jcef version extract

* CEF: Download async

* Copy StartupAsync to support handling errors

Startup failures are simply swallowed, since they are recorded in the
future, but there is no way to get that exception

* CEF: Search for release file recursively

On Mac, the file is buried a bit deeper than first level, like on Win
and Linux

* KcefWebViewProvider: Suppress deprecation

We need to send those events, even if they are deprecated

* Update readme

* Optimize imports

* Suggestion

Co-authored-by: Mitchell Syer <syer10@users.noreply.github.com>

* Refactor: stick to `Path` instead of `File`

Also extracts the downloading of CEF to a separate method

* Lint

* Support disabling CEF

Co-authored-by: Kolby Moroz Liebl <31669092+kolbyml@users.noreply.github.com>

* Move JBR version to build constants

Allows embedding into Manifest so docker can later extract the proper version

* Create test to verify JCEF dependency matches downloaded JBR

* Update server/src/main/kotlin/suwayomi/tachidesk/server/util/CEFManager.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Fix compile, apply Path suggestions

* Download progress

* Lint

* Fix exception on non-posix

* Delete recursively

Others can be non-empty

* Support disabling CEF at will

Not really functional, but nice

* Fix test

* Exclude masstest unless explicitly requested

* PR-CI: Run tests

* Add Changelog entry

---------

Co-authored-by: Mitchell Syer <syer10@users.noreply.github.com>
Co-authored-by: Kolby Moroz Liebl <31669092+kolbyml@users.noreply.github.com>
2026-05-19 17:05:59 -04:00

1155 lines
40 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.PermissionRequest
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 kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.cef.CefClient
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.callback.CefCallback
import org.cef.callback.CefMediaAccessCallback
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.CefPermissionHandler
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.math.min
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 initialRequestData: InitialRequestData? = null
private var kcefClient: CefClient? = null
private var browser: CefBrowser? = 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()
}
interface InitBrowserHandler {
fun init(provider: KcefWebViewProvider): Unit
}
private data class InitialRequestData(
private val additionalHttpHeaders: Map<String, String>? = null,
private val myPostData: ByteArray? = null,
) {
fun apply(request: CefRequest?) {
request?.apply {
Log.v(TAG, "Initial request: applying headers and post data")
if (!additionalHttpHeaders.isNullOrEmpty()) {
additionalHttpHeaders.forEach {
setHeaderByName(it.key, it.value, true)
}
}
if (myPostData != null) {
this.postData =
CefPostData.create().apply {
addElement(
CefPostDataElement.create().apply {
setToBytes(myPostData.size, myPostData)
},
)
}
}
}
}
}
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 class CefPermissionRequest(
private val url: String,
private val permissionMask: Int,
private val callback: CefMediaAccessCallback,
) : PermissionRequest() {
override fun getOrigin(): Uri = Uri.parse(url)
override fun getResources(): Array<String> {
val retVal = mutableListOf<String>()
if ((permissionMask and (1 shl 0)) > 0) retVal.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE)
if ((permissionMask and (1 shl 1)) > 0) retVal.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
return retVal.toTypedArray()
}
override fun grant(resources: Array<String>) {
// TODO: respect given resource grant
callback.Continue(permissionMask)
}
override fun deny() {
callback.Cancel()
}
}
private 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")
}
}
@Suppress("DEPRECATION")
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 = 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 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()
Log.v(TAG, "Resolved client response for ${resolvedData?.size} bytes")
} catch (e: IOException) {
Log.w(TAG, "Failed to read client data", e)
}
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 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 {
initialRequestData?.apply(request)
initialRequestData = null
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? {
val isInitialLoad = frame.url == "" && request.method == "GET"
Log.v(TAG, "Request ${request.method} ${request.url} is initial? $isInitialLoad")
// NOTE: we should be calling this on the handler, since CEF calls us on its IO thread
// but docs say "This method is called on a thread other than the UI thread" so should be fine
val response =
if (isInitialLoad) {
null
} else {
viewClient.shouldInterceptRequest(
view,
CefWebResourceRequest(request, frame, false),
)
}
if (response == null) {
// prefer user's response override
urlHttpMapping[request.url.trimEnd('/')]?.let {
return HtmlResponseResourceHandler(it)
}
}
response ?: return null
return WebResponseResourceHandler(response)
}
}
@Suppress("DEPRECATION")
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,
errorCode: Int,
errorString: String,
) {
handler.post {
viewClient.onRenderProcessGone(
view,
object : RenderProcessGoneDetail() {
override fun didCrash(): Boolean = status == CefRequestHandler.TerminationStatus.TS_PROCESS_CRASHED
override fun rendererPriorityAtExit(): Int = -1
},
)
}
}
}
private inner class PermissionHandler : CefPermissionHandler {
override fun onRequestMediaAccessPermission(
browser: CefBrowser,
frame: CefFrame,
requestingUrl: String,
requestedPermissions: Int,
callback: CefMediaAccessCallback,
): Boolean {
handler.post {
Log.v(TAG, "Checking permission for $requestingUrl: $requestedPermissions")
chromeClient.onPermissionRequest(CefPermissionRequest(requestingUrl, requestedPermissions, callback))
}
return true
}
}
override fun init(
javaScriptInterfaces: Map<String, Any>?,
privateBrowsing: Boolean,
) {
Log.v(TAG, "KcefWebViewProvider: initialize")
destroy()
kcefClient =
runBlocking {
CefHelper.createClient().apply {
addDisplayHandler(DisplayHandler())
addLoadHandler(LoadHandler())
addRequestHandler(RequestHandler())
addPermissionHandler(PermissionHandler())
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)
initialRequestData = InitialRequestData(additionalHttpHeaders = additionalHttpHeaders)
browser =
kcefClient!!
.createBrowser(
loadUrl,
CefRendering.OFFSCREEN,
false,
).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)
initialRequestData = InitialRequestData(myPostData = postData)
browser =
kcefClient!!
.createBrowser(
url,
CefRendering.OFFSCREEN,
false,
).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)
val url = baseUrl ?: "about:blank"
urlHttpMapping[url.trimEnd('/')] = data
browser =
kcefClient!!
.createBrowser(
url,
CefRendering.OFFSCREEN,
false,
).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
@Suppress("DEPRECATION")
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
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!")
}
}