mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 19:34:35 -05:00
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>
This commit is contained in:
@@ -51,11 +51,10 @@ 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 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
|
||||
@@ -87,7 +86,7 @@ import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executor
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.math.min
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.declaredMemberFunctions
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
@@ -102,8 +101,8 @@ class KcefWebViewProvider(
|
||||
private val urlHttpMapping: MutableMap<String, String> = mutableMapOf()
|
||||
private var initialRequestData: InitialRequestData? = null
|
||||
|
||||
private var kcefClient: KCEFClient? = null
|
||||
private var browser: KCEFBrowser? = null
|
||||
private var kcefClient: CefClient? = null
|
||||
private var browser: CefBrowser? = null
|
||||
|
||||
private val handler = Handler(view.webViewLooper)
|
||||
|
||||
@@ -115,8 +114,8 @@ class KcefWebViewProvider(
|
||||
private val initHandler: InitBrowserHandler by KoinPlatformTools.defaultContext().get().inject()
|
||||
}
|
||||
|
||||
public interface InitBrowserHandler {
|
||||
public fun init(provider: KcefWebViewProvider): Unit
|
||||
interface InitBrowserHandler {
|
||||
fun init(provider: KcefWebViewProvider): Unit
|
||||
}
|
||||
|
||||
private data class InitialRequestData(
|
||||
@@ -192,7 +191,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DisplayHandler : CefDisplayHandlerAdapter() {
|
||||
private class DisplayHandler : CefDisplayHandlerAdapter() {
|
||||
override fun onConsoleMessage(
|
||||
browser: CefBrowser,
|
||||
level: CefSettings.LogSeverity,
|
||||
@@ -220,6 +219,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private inner class LoadHandler : CefLoadHandlerAdapter() {
|
||||
override fun onLoadEnd(
|
||||
browser: CefBrowser,
|
||||
@@ -366,7 +366,7 @@ class KcefWebViewProvider(
|
||||
callback: CefCallback,
|
||||
): Boolean {
|
||||
val data = resolvedData ?: return false
|
||||
val bytesToTransfer = Math.min(bytesToRead, data.size - readOffset)
|
||||
val bytesToTransfer = min(bytesToRead, data.size - readOffset)
|
||||
Log.v(
|
||||
TAG,
|
||||
"readResponse: $readOffset/${data.size}, reading $bytesToRead->$bytesToTransfer",
|
||||
@@ -378,7 +378,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private inner class WebResponseResourceHandler(
|
||||
private class WebResponseResourceHandler(
|
||||
val webResponse: WebResourceResponse,
|
||||
) : ArrayResponseResourceHandler() {
|
||||
override fun processRequest(
|
||||
@@ -408,7 +408,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
private inner class HtmlResponseResourceHandler(
|
||||
private class HtmlResponseResourceHandler(
|
||||
val html: String,
|
||||
) : ArrayResponseResourceHandler() {
|
||||
override fun processRequest(
|
||||
@@ -439,7 +439,7 @@ class KcefWebViewProvider(
|
||||
view,
|
||||
CefWebResourceRequest(request, frame, false),
|
||||
)
|
||||
Log.v(TAG, "Resource ${request?.url}, result is cancel? $cancel")
|
||||
Log.v(TAG, "Resource ${request.url}, result is cancel? $cancel")
|
||||
|
||||
handler.post { viewClient.onLoadResource(view, frame?.url) }
|
||||
|
||||
@@ -466,7 +466,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
if (response == null) {
|
||||
// prefer user's response override
|
||||
urlHttpMapping.get(request.url.trimEnd('/'))?.let {
|
||||
urlHttpMapping[request.url.trimEnd('/')]?.let {
|
||||
return HtmlResponseResourceHandler(it)
|
||||
}
|
||||
}
|
||||
@@ -475,6 +475,7 @@ class KcefWebViewProvider(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private inner class RequestHandler : CefRequestHandlerAdapter() {
|
||||
override fun getResourceRequestHandler(
|
||||
browser: CefBrowser,
|
||||
@@ -484,11 +485,13 @@ class KcefWebViewProvider(
|
||||
isDownload: Boolean,
|
||||
requestInitiator: String,
|
||||
disableDefaultHandling: BoolRef,
|
||||
): CefResourceRequestHandler? = ResourceRequestHandler()
|
||||
): CefResourceRequestHandler = ResourceRequestHandler()
|
||||
|
||||
override fun onRenderProcessTerminated(
|
||||
browser: CefBrowser,
|
||||
status: CefRequestHandler.TerminationStatus,
|
||||
errorCode: Int,
|
||||
errorString: String,
|
||||
) {
|
||||
handler.post {
|
||||
viewClient.onRenderProcessGone(
|
||||
@@ -507,13 +510,13 @@ class KcefWebViewProvider(
|
||||
override fun onRequestMediaAccessPermission(
|
||||
browser: CefBrowser,
|
||||
frame: CefFrame,
|
||||
requesting_url: String,
|
||||
requested_permissions: Int,
|
||||
requestingUrl: String,
|
||||
requestedPermissions: Int,
|
||||
callback: CefMediaAccessCallback,
|
||||
): Boolean {
|
||||
handler.post {
|
||||
Log.v(TAG, "Checking permission for $requesting_url: $requested_permissions")
|
||||
chromeClient.onPermissionRequest(CefPermissionRequest(requesting_url, requested_permissions, callback))
|
||||
Log.v(TAG, "Checking permission for $requestingUrl: $requestedPermissions")
|
||||
chromeClient.onPermissionRequest(CefPermissionRequest(requestingUrl, requestedPermissions, callback))
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -526,16 +529,18 @@ class KcefWebViewProvider(
|
||||
Log.v(TAG, "KcefWebViewProvider: initialize")
|
||||
destroy()
|
||||
kcefClient =
|
||||
KCEF.newClientBlocking().apply {
|
||||
addDisplayHandler(DisplayHandler())
|
||||
addLoadHandler(LoadHandler())
|
||||
addRequestHandler(RequestHandler())
|
||||
addPermissionHandler(PermissionHandler())
|
||||
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()))
|
||||
val config = CefMessageRouter.CefMessageRouterConfig()
|
||||
config.jsQueryFunction = QUERY_FN
|
||||
config.jsCancelFunction = QUERY_CANCEL_FN
|
||||
addMessageRouter(CefMessageRouter.create(config, MessageRouterHandler()))
|
||||
}
|
||||
}
|
||||
initHandler.init(this)
|
||||
}
|
||||
@@ -613,6 +618,7 @@ class KcefWebViewProvider(
|
||||
.createBrowser(
|
||||
loadUrl,
|
||||
CefRendering.OFFSCREEN,
|
||||
false,
|
||||
).apply {
|
||||
// NOTE: Without this, we don't seem to be receiving any events
|
||||
createImmediately()
|
||||
@@ -637,6 +643,7 @@ class KcefWebViewProvider(
|
||||
.createBrowser(
|
||||
url,
|
||||
CefRendering.OFFSCREEN,
|
||||
false,
|
||||
).apply {
|
||||
// NOTE: Without this, we don't seem to be receiving any events
|
||||
createImmediately()
|
||||
@@ -662,27 +669,19 @@ class KcefWebViewProvider(
|
||||
browser?.close(true)
|
||||
browser?.dispose()
|
||||
chromeClient.onProgressChanged(view, 0)
|
||||
val url = baseUrl ?: "about:blank"
|
||||
urlHttpMapping[url.trimEnd('/')] = data
|
||||
|
||||
browser =
|
||||
(
|
||||
baseUrl?.let { url ->
|
||||
urlHttpMapping.put(url.trimEnd('/'), data)
|
||||
kcefClient!!.createBrowser(
|
||||
url,
|
||||
CefRendering.OFFSCREEN,
|
||||
)
|
||||
kcefClient!!
|
||||
.createBrowser(
|
||||
url,
|
||||
CefRendering.OFFSCREEN,
|
||||
false,
|
||||
).apply {
|
||||
// NOTE: Without this, we don't seem to be receiving any events
|
||||
createImmediately()
|
||||
}
|
||||
?: 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")
|
||||
}
|
||||
|
||||
@@ -692,11 +691,11 @@ class KcefWebViewProvider(
|
||||
) {
|
||||
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!")
|
||||
@@ -838,6 +837,7 @@ class KcefWebViewProvider(
|
||||
|
||||
override fun getWebChromeClient(): WebChromeClient = chromeClient
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun setPictureListener(listener: PictureListener): Unit = throw RuntimeException("Stub!")
|
||||
|
||||
@Serializable
|
||||
@@ -860,7 +860,7 @@ class KcefWebViewProvider(
|
||||
obj: Any,
|
||||
interfaceName: String,
|
||||
) {
|
||||
val cls = obj::class as KClass<Any>
|
||||
val cls = obj::class
|
||||
mappings.addAll(
|
||||
cls.declaredMemberFunctions.map {
|
||||
// This is ridiculous, but necessary, otherwise "public final" throws
|
||||
@@ -922,7 +922,8 @@ class KcefWebViewProvider(
|
||||
override fun getRendererPriorityWaivedWhenNotVisible(): Boolean = throw RuntimeException("Stub!")
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
override fun setTextClassifier(textClassifier: TextClassifier?) {}
|
||||
override fun setTextClassifier(textClassifier: TextClassifier?) {
|
||||
}
|
||||
|
||||
override fun getTextClassifier(): TextClassifier = TextClassifier.NO_OP
|
||||
|
||||
@@ -948,11 +949,13 @@ class KcefWebViewProvider(
|
||||
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,
|
||||
@@ -963,7 +966,8 @@ class KcefWebViewProvider(
|
||||
override fun onProvideContentCaptureStructure(
|
||||
@SuppressWarnings("unused") structure: android.view.ViewStructure,
|
||||
@SuppressWarnings("unused") flags: Int,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
override fun getAccessibilityNodeProvider(): AccessibilityNodeProvider = throw RuntimeException("Stub!")
|
||||
|
||||
@@ -1033,7 +1037,8 @@ class KcefWebViewProvider(
|
||||
override fun onMovedToDisplay(
|
||||
displayId: Int,
|
||||
config: Configuration,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onVisibilityChanged(
|
||||
changedView: View,
|
||||
|
||||
Reference in New Issue
Block a user