Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ android.minSdk=21
#Versions
GROUP=io.github.kevinnzou
POM_ARTIFACT_ID=compose-webview-multiplatform
VERSION_NAME=2.0.0
VERSION_NAME=2.0.1
POM_NAME=Compose WebView Multiplatform
POM_INCEPTION_YEAR=2023
POM_DESCRIPTION=WebView for JetBrains Compose Multiplatform
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.multiplatform.webview.util

import android.util.Log
import android.webkit.WebResourceResponse
import androidx.webkit.WebViewAssetLoader
import java.io.File
import java.io.FileInputStream

class InternalStoragePathHandler : WebViewAssetLoader.PathHandler {
override fun handle(path: String): WebResourceResponse? {
Log.d("InternalStorageHandler", "Intercepted: $path")
val file = File(path.removePrefix("/"))
if (!file.exists() || !file.isFile) return null

val mimeType =
when {
path.endsWith(".html") -> "text/html"
path.endsWith(".js") -> "application/javascript"
path.endsWith(".css") -> "text/css"
path.endsWith(".json") -> "application/json"
path.endsWith(".png") -> "image/png"
path.endsWith(".jpg") || path.endsWith(".jpeg") -> "image/jpeg"
path.endsWith(".svg") -> "image/svg+xml"
path.endsWith(".webp") -> "image/webp"
path.endsWith(".ico") -> "image/x-icon"
path.endsWith(".woff") -> "font/woff"
path.endsWith(".woff2") -> "font/woff2"
path.endsWith(".ttf") -> "font/ttf"
path.endsWith(".mp4") -> "video/mp4"
path.endsWith(".webm") -> "video/webm"
path.endsWith(".ogg") -> "video/ogg"
path.endsWith(".mp3") -> "audio/mpeg"
path.endsWith(".wav") -> "audio/wav"
path.endsWith(".wasm") -> "application/wasm"
path.endsWith(".pdf") -> "application/pdf"
path.endsWith(".zip") -> "application/zip"
path.endsWith(".csv") -> "text/csv"
else -> "application/octet-stream"
}

return WebResourceResponse(mimeType, "utf-8", FileInputStream(file))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
Expand All @@ -24,10 +25,12 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.core.graphics.createBitmap
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewFeature
import com.multiplatform.webview.jsbridge.WebViewJsBridge
import com.multiplatform.webview.request.WebRequest
import com.multiplatform.webview.request.WebRequestInterceptResult
import com.multiplatform.webview.util.InternalStoragePathHandler
import com.multiplatform.webview.util.KLogger

/**
Expand Down Expand Up @@ -189,7 +192,6 @@ fun AccompanistWebView(
userAgentString = it.customUserAgentString
allowFileAccessFromFileURLs = it.allowFileAccessFromFileURLs
allowUniversalAccessFromFileURLs = it.allowUniversalAccessFromFileURLs
setSupportZoom(it.supportZoom)
}

state.webSettings.androidWebSettings.let {
Expand All @@ -208,6 +210,16 @@ fun AccompanistWebView(
loadsImagesAutomatically = it.loadsImagesAutomatically
domStorageEnabled = it.domStorageEnabled
mediaPlaybackRequiresUserGesture = it.mediaPlaybackRequiresUserGesture

if (it.enableSandbox) {
client.assetLoader =
WebViewAssetLoader
.Builder()
.addPathHandler(
it.sandboxSubdomain,
InternalStoragePathHandler(),
).build()
}
}
}
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
Expand Down Expand Up @@ -261,6 +273,8 @@ open class AccompanistWebViewClient : WebViewClient() {
internal set
private var isRedirect = false

var assetLoader: WebViewAssetLoader? = null

override fun onPageStarted(
view: WebView,
url: String?,
Expand All @@ -274,14 +288,26 @@ open class AccompanistWebViewClient : WebViewClient() {
state.errorsForCurrentRequest.clear()
state.pageTitle = null
state.lastLoadedUrl = url
val supportZoom = if (state.webSettings.supportZoom) "yes" else "no"

// set scale level
@Suppress("ktlint:standard:max-line-length")
val script =
"var meta = document.createElement('meta');meta.setAttribute('name', 'viewport');meta.setAttribute('content', 'width=device-width, initial-scale=${state.webSettings.zoomLevel}, maximum-scale=10.0, minimum-scale=0.1,user-scalable=yes');document.getElementsByTagName('head')[0].appendChild(meta);"
"var meta = document.createElement('meta');meta.setAttribute('name', 'viewport');meta.setAttribute('content', 'width=device-width, initial-scale=${state.webSettings.zoomLevel}, maximum-scale=10.0, minimum-scale=0.1,user-scalable=$supportZoom');document.getElementsByTagName('head')[0].appendChild(meta);"
navigator.evaluateJavaScript(script)
}

override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?,
): WebResourceResponse? {
val url = request?.url
KLogger.d { "Intercepting request for URL: $url" }
return url?.let {
assetLoader?.shouldInterceptRequest(it)
} ?: super.shouldInterceptRequest(view, request)
}

override fun onPageFinished(
view: WebView,
url: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class AndroidWebView(
webView.loadUrl(url, additionalHttpHeaders)
}

override fun loadHtml(
override suspend fun loadHtml(
html: String?,
baseUrl: String?,
mimeType: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,37 @@ sealed class PlatformWebSettings {
* Default is [LayerType.HARDWARE]
*/
var layerType: Int = LayerType.HARDWARE,
/**
* Enables sandboxing of local file access via WebViewAssetLoader.
*
* When true, instead of using file:// URLs (which are insecure and restrict modern features),
* the WebView uses WebViewAssetLoader to serve local files (assets/resources/internal storage)
* over secure virtual https:// URLs. This improves compatibility with cookies, service workers,
* and CSP (Content Security Policy), and prevents file access vulnerabilities.
*
* This must be used in combination with a proper PathHandler setup in your WebView client
* (e.g., mapping /app/ to internal files or app assets).
*
* For example, if your WebViewAssetLoader maps the path "/app/" to your internal storage,
* you can load a file by navigating to a virtual URL like:
* `https://appassets.androidplatform.net/app/index.html`
* (the standard host used by WebViewAssetLoader)
* This URL will internally resolve to your app's internal file path. and enable cookies
* for them as well
*/
var enableSandbox: Boolean = false,
/**
* The virtual subdomain prefix to be used with WebViewAssetLoader for local file access.
*
* This is typically set to something like "/app/" or "/assets/" and must match the path
* used in your PathHandler configuration inside WebViewAssetLoader.
*
* When you load a URL such as `https://appassets.androidplatform.net/app/index.html`
* (the standard host used by WebViewAssetLoader) in your WebView,
* the WebViewAssetLoader will map it to the correct local file or asset if configured properly.
* This URL should be used instead of file:// URLs to ensure secure and modern WebView behavior.
*/
var sandboxSubdomain: String = "/app/",
) : PlatformWebSettings() {
object LayerType {
const val NONE = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ interface IWebView {
* @param encoding The encoding of the data in the string.
* @param historyUrl The history URL for the loaded HTML. Leave null to use about:blank.
*/
fun loadHtml(
suspend fun loadHtml(
html: String? = null,
baseUrl: String? = null,
mimeType: String? = "text/html",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class WebViewNavigator(

data class LoadHtmlFile(
val fileName: String,
val readType: WebViewFileReadType,
) : NavigationEvent

/**
Expand Down Expand Up @@ -140,7 +141,7 @@ class WebViewNavigator(
)

is NavigationEvent.LoadHtmlFile -> {
loadHtmlFile(event.fileName)
loadHtmlFile(event.fileName, event.readType)
}

is NavigationEvent.LoadUrl -> {
Expand Down Expand Up @@ -218,11 +219,15 @@ class WebViewNavigator(
}
}

fun loadHtmlFile(fileName: String) {
fun loadHtmlFile(
fileName: String,
readType: WebViewFileReadType = WebViewFileReadType.ASSET_RESOURCES,
) {
coroutineScope.launch {
navigationEvents.emit(
NavigationEvent.LoadHtmlFile(
fileName,
readType,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class DesktopWebView(
}
}

override fun loadHtml(
override suspend fun loadHtml(
html: String?,
baseUrl: String?,
mimeType: String?,
Expand All @@ -72,6 +72,7 @@ class DesktopWebView(
}
if (html != null) {
try {
delay(500)
webView.loadHtml(html, baseUrl ?: KCEFBrowser.BLANK_URI)
} catch (e: Exception) {
KLogger.e { "DesktopWebView loadHtml error: ${e.message}" }
Expand Down Expand Up @@ -99,7 +100,7 @@ class DesktopWebView(
throw Exception("Resource not found: $attemptedResourcePath (for readType: $readType)")
}

val outFile = java.io.File(tempDirectory, path.substringAfterLast("/"))
val outFile = File(tempDirectory, path.substringAfterLast("/"))
outFile.outputStream().use { output ->
inputStream.copyTo(output)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import org.cef.handler.CefDisplayHandler
import org.cef.handler.CefLoadHandler
import org.cef.handler.CefRequestHandlerAdapter
import org.cef.network.CefRequest
import kotlin.math.abs
import kotlin.math.ln

/**
* Created By Kevin Zou On 2023/9/12
Expand All @@ -37,13 +35,12 @@ internal fun CefBrowser.addDisplayHandler(state: WebViewState) {
) {
// https://magpcss.org/ceforum/viewtopic.php?t=11491
// https://github.com/KevinnZou/compose-webview-multiplatform/issues/46
// I found this formula much near to the other platforms, so I replace it
val givenZoomLevel = state.webSettings.zoomLevel
val realZoomLevel =
if (givenZoomLevel >= 0.0) {
ln(abs(givenZoomLevel)) / ln(1.2)
} else {
-ln(abs(givenZoomLevel)) / ln(1.2)
}

val percentage = givenZoomLevel * 100.0
val realZoomLevel = (percentage - 100.0) / 25.0

KLogger.d { "titleProperty: $title" }
zoomLevel = realZoomLevel
state.pageTitle = title
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,8 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView =
param.requestContext,
)
is WebContent.Data ->
param.client.createBrowserWithHtml(
content.data,
content.baseUrl ?: KCEFBrowser.BLANK_URI,
param.client.createBrowser(
KCEFBrowser.BLANK_URI,
param.rendering,
param.transparent,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class IOSWebView(
)
}

override fun loadHtml(
override suspend fun loadHtml(
html: String?,
baseUrl: String?,
mimeType: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class WasmJsWebView(
}
}

override fun loadHtml(
override suspend fun loadHtml(
html: String?,
baseUrl: String?,
mimeType: String?,
Expand Down