Skip to content

Commit 481b025

Browse files
committed
Fix: Enhance local file loading and zoom functionality
This commit introduces several improvements and fixes related to local file loading and zoom behavior across platforms: **Android:** - Added `enableSandbox` and `sandboxSubdomain` options to `PlatformWebSettings.androidWebSettings`. When `enableSandbox` is true, WebView uses `WebViewAssetLoader` with an `InternalStoragePathHandler` to serve local files (assets/resources/internal storage) over secure virtual `https://` URLs. This improves security and compatibility with modern web features like cookies and Service Workers. - The `user-scalable` viewport meta tag is now correctly set based on `state.webSettings.supportZoom`. - Removed redundant `setSupportZoom` call as it's handled by the viewport meta tag. **Desktop:** - Increased the delay before loading HTML content in `DesktopWebView.loadHtml` to 500ms to allow more time for file operations, potentially improving reliability. - When loading HTML data, the desktop WebView now initially loads `KCEFBrowser.BLANK_URI` and then injects the HTML content. This change is to ensure proper initialization before content loading. - Adjusted the zoom level calculation in `WebEngineExt.kt` for desktop to better align with other platforms. The new formula is `(percentage - 100.0) / 25.0`. **Common:** - `WebViewNavigator.loadHtmlFile` now accepts a `readType: WebViewFileReadType` parameter to specify how the HTML file should be read (e.g., from assets or resources). This parameter is passed down to the platform-specific implementations. - `IWebView.loadHtml` and its implementations are now suspend functions to accommodate asynchronous operations like file reading or network requests if needed in the future. **Internal:** - Added `InternalStoragePathHandler.kt` for Android to handle serving files from internal storage via `WebViewAssetLoader`. **Build:** - Incremented `VERSION_NAME` to `2.0.1`.
1 parent dec0029 commit 481b025

12 files changed

Lines changed: 125 additions & 22 deletions

File tree

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ android.minSdk=21
1515
#Versions
1616
GROUP=io.github.kevinnzou
1717
POM_ARTIFACT_ID=compose-webview-multiplatform
18-
VERSION_NAME=2.0.0
18+
VERSION_NAME=2.0.1
1919
POM_NAME=Compose WebView Multiplatform
2020
POM_INCEPTION_YEAR=2023
2121
POM_DESCRIPTION=WebView for JetBrains Compose Multiplatform
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.multiplatform.webview.util
2+
3+
import android.util.Log
4+
import android.webkit.WebResourceResponse
5+
import androidx.webkit.WebViewAssetLoader
6+
import java.io.File
7+
import java.io.FileInputStream
8+
9+
class InternalStoragePathHandler() : WebViewAssetLoader.PathHandler {
10+
override fun handle(path: String): WebResourceResponse? {
11+
Log.d("InternalStorageHandler", "Intercepted: $path")
12+
val file = File(path.removePrefix("/"))
13+
if (!file.exists() || !file.isFile) return null
14+
15+
val mimeType = when {
16+
path.endsWith(".html") -> "text/html"
17+
path.endsWith(".js") -> "application/javascript"
18+
path.endsWith(".css") -> "text/css"
19+
path.endsWith(".json") -> "application/json"
20+
path.endsWith(".png") -> "image/png"
21+
path.endsWith(".jpg") || path.endsWith(".jpeg") -> "image/jpeg"
22+
path.endsWith(".svg") -> "image/svg+xml"
23+
path.endsWith(".webp") -> "image/webp"
24+
path.endsWith(".ico") -> "image/x-icon"
25+
path.endsWith(".woff") -> "font/woff"
26+
path.endsWith(".woff2") -> "font/woff2"
27+
path.endsWith(".ttf") -> "font/ttf"
28+
path.endsWith(".mp4") -> "video/mp4"
29+
path.endsWith(".webm") -> "video/webm"
30+
path.endsWith(".ogg") -> "video/ogg"
31+
path.endsWith(".mp3") -> "audio/mpeg"
32+
path.endsWith(".wav") -> "audio/wav"
33+
path.endsWith(".wasm") -> "application/wasm"
34+
path.endsWith(".pdf") -> "application/pdf"
35+
path.endsWith(".zip") -> "application/zip"
36+
path.endsWith(".csv") -> "text/csv"
37+
else -> "application/octet-stream"
38+
}
39+
40+
return WebResourceResponse(mimeType, "utf-8", FileInputStream(file))
41+
}
42+
}

webview/src/androidMain/kotlin/com/multiplatform/webview/web/AccompanistWebView.kt

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import android.webkit.PermissionRequest
1010
import android.webkit.WebChromeClient
1111
import android.webkit.WebResourceError
1212
import android.webkit.WebResourceRequest
13+
import android.webkit.WebResourceResponse
1314
import android.webkit.WebView
1415
import android.webkit.WebViewClient
1516
import android.widget.FrameLayout
@@ -24,10 +25,12 @@ import androidx.compose.ui.viewinterop.AndroidView
2425
import androidx.core.content.ContextCompat
2526
import androidx.core.graphics.createBitmap
2627
import androidx.webkit.WebSettingsCompat
28+
import androidx.webkit.WebViewAssetLoader
2729
import androidx.webkit.WebViewFeature
2830
import com.multiplatform.webview.jsbridge.WebViewJsBridge
2931
import com.multiplatform.webview.request.WebRequest
3032
import com.multiplatform.webview.request.WebRequestInterceptResult
33+
import com.multiplatform.webview.util.InternalStoragePathHandler
3134
import com.multiplatform.webview.util.KLogger
3235

3336
/**
@@ -189,7 +192,6 @@ fun AccompanistWebView(
189192
userAgentString = it.customUserAgentString
190193
allowFileAccessFromFileURLs = it.allowFileAccessFromFileURLs
191194
allowUniversalAccessFromFileURLs = it.allowUniversalAccessFromFileURLs
192-
setSupportZoom(it.supportZoom)
193195
}
194196

195197
state.webSettings.androidWebSettings.let {
@@ -208,6 +210,15 @@ fun AccompanistWebView(
208210
loadsImagesAutomatically = it.loadsImagesAutomatically
209211
domStorageEnabled = it.domStorageEnabled
210212
mediaPlaybackRequiresUserGesture = it.mediaPlaybackRequiresUserGesture
213+
214+
if (it.enableSandbox) {
215+
client.assetLoader = WebViewAssetLoader.Builder()
216+
.addPathHandler(
217+
it.sandboxSubdomain,
218+
InternalStoragePathHandler()
219+
)
220+
.build()
221+
}
211222
}
212223
}
213224
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
@@ -261,6 +272,8 @@ open class AccompanistWebViewClient : WebViewClient() {
261272
internal set
262273
private var isRedirect = false
263274

275+
var assetLoader: WebViewAssetLoader? = null
276+
264277
override fun onPageStarted(
265278
view: WebView,
266279
url: String?,
@@ -274,14 +287,25 @@ open class AccompanistWebViewClient : WebViewClient() {
274287
state.errorsForCurrentRequest.clear()
275288
state.pageTitle = null
276289
state.lastLoadedUrl = url
277-
290+
val supportZoom = if (state.webSettings.supportZoom) "yes" else "no"
278291
// set scale level
279292
@Suppress("ktlint:standard:max-line-length")
280293
val script =
281-
"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);"
294+
"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);"
282295
navigator.evaluateJavaScript(script)
283296
}
284297

298+
override fun shouldInterceptRequest(
299+
view: WebView?,
300+
request: WebResourceRequest?
301+
): WebResourceResponse? {
302+
val url = request?.url
303+
KLogger.d { "Intercepting request for URL: $url" }
304+
return url?.let {
305+
assetLoader?.shouldInterceptRequest(it)
306+
} ?: super.shouldInterceptRequest(view, request)
307+
}
308+
285309
override fun onPageFinished(
286310
view: WebView,
287311
url: String?,

webview/src/androidMain/kotlin/com/multiplatform/webview/web/AndroidWebView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class AndroidWebView(
3737
webView.loadUrl(url, additionalHttpHeaders)
3838
}
3939

40-
override fun loadHtml(
40+
override suspend fun loadHtml(
4141
html: String?,
4242
baseUrl: String?,
4343
mimeType: String?,

webview/src/commonMain/kotlin/com/multiplatform/webview/setting/PlatformWebSettings.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,38 @@ sealed class PlatformWebSettings {
188188
* Default is [LayerType.HARDWARE]
189189
*/
190190
var layerType: Int = LayerType.HARDWARE,
191+
/**
192+
* Enables sandboxing of local file access via WebViewAssetLoader.
193+
*
194+
* When true, instead of using file:// URLs (which are insecure and restrict modern features),
195+
* the WebView uses WebViewAssetLoader to serve local files (assets/resources/internal storage)
196+
* over secure virtual https:// URLs. This improves compatibility with cookies, service workers,
197+
* and CSP (Content Security Policy), and prevents file access vulnerabilities.
198+
*
199+
* This must be used in combination with a proper PathHandler setup in your WebView client
200+
* (e.g., mapping /app/ to internal files or app assets).
201+
*
202+
* For example, if your WebViewAssetLoader maps the path "/app/" to your internal storage,
203+
* you can load a file by navigating to a virtual URL like:
204+
* `https://appassets.androidplatform.net/app/index.html`
205+
* (the standard host used by WebViewAssetLoader)
206+
* This URL will internally resolve to your app's internal file path. and enable cookies
207+
* for them as well
208+
*/
209+
var enableSandbox: Boolean = false,
210+
211+
/**
212+
* The virtual subdomain prefix to be used with WebViewAssetLoader for local file access.
213+
*
214+
* This is typically set to something like "/app/" or "/assets/" and must match the path
215+
* used in your PathHandler configuration inside WebViewAssetLoader.
216+
*
217+
* When you load a URL such as `https://appassets.androidplatform.net/app/index.html`
218+
* (the standard host used by WebViewAssetLoader) in your WebView,
219+
* the WebViewAssetLoader will map it to the correct local file or asset if configured properly.
220+
* This URL should be used instead of file:// URLs to ensure secure and modern WebView behavior.
221+
*/
222+
var sandboxSubdomain: String = "/app/"
191223
) : PlatformWebSettings() {
192224
object LayerType {
193225
const val NONE = 0

webview/src/commonMain/kotlin/com/multiplatform/webview/web/IWebView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ interface IWebView {
5555
* @param encoding The encoding of the data in the string.
5656
* @param historyUrl The history URL for the loaded HTML. Leave null to use about:blank.
5757
*/
58-
fun loadHtml(
58+
suspend fun loadHtml(
5959
html: String? = null,
6060
baseUrl: String? = null,
6161
mimeType: String? = "text/html",

webview/src/commonMain/kotlin/com/multiplatform/webview/web/WebViewNavigator.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class WebViewNavigator(
7474

7575
data class LoadHtmlFile(
7676
val fileName: String,
77+
val readType: WebViewFileReadType,
7778
) : NavigationEvent
7879

7980
/**
@@ -140,7 +141,7 @@ class WebViewNavigator(
140141
)
141142

142143
is NavigationEvent.LoadHtmlFile -> {
143-
loadHtmlFile(event.fileName)
144+
loadHtmlFile(event.fileName, event.readType)
144145
}
145146

146147
is NavigationEvent.LoadUrl -> {
@@ -218,11 +219,15 @@ class WebViewNavigator(
218219
}
219220
}
220221

221-
fun loadHtmlFile(fileName: String) {
222+
fun loadHtmlFile(
223+
fileName: String,
224+
readType: WebViewFileReadType = WebViewFileReadType.ASSET_RESOURCES
225+
) {
222226
coroutineScope.launch {
223227
navigationEvents.emit(
224228
NavigationEvent.LoadHtmlFile(
225229
fileName,
230+
readType
226231
),
227232
)
228233
}
@@ -305,4 +310,5 @@ class WebViewNavigator(
305310
fun rememberWebViewNavigator(
306311
coroutineScope: CoroutineScope = rememberCoroutineScope(),
307312
requestInterceptor: RequestInterceptor? = null,
308-
): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope, requestInterceptor) }
313+
): WebViewNavigator =
314+
remember(coroutineScope) { WebViewNavigator(coroutineScope, requestInterceptor) }

webview/src/desktopMain/kotlin/com/multiplatform/webview/web/DesktopWebView.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class DesktopWebView(
6060
}
6161
}
6262

63-
override fun loadHtml(
63+
override suspend fun loadHtml(
6464
html: String?,
6565
baseUrl: String?,
6666
mimeType: String?,
@@ -72,6 +72,7 @@ class DesktopWebView(
7272
}
7373
if (html != null) {
7474
try {
75+
delay(500)
7576
webView.loadHtml(html, baseUrl ?: KCEFBrowser.BLANK_URI)
7677
} catch (e: Exception) {
7778
KLogger.e { "DesktopWebView loadHtml error: ${e.message}" }
@@ -99,7 +100,7 @@ class DesktopWebView(
99100
throw Exception("Resource not found: $attemptedResourcePath (for readType: $readType)")
100101
}
101102

102-
val outFile = java.io.File(tempDirectory, path.substringAfterLast("/"))
103+
val outFile = File(tempDirectory, path.substringAfterLast("/"))
103104
outFile.outputStream().use { output ->
104105
inputStream.copyTo(output)
105106
}

webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebEngineExt.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,12 @@ internal fun CefBrowser.addDisplayHandler(state: WebViewState) {
3737
) {
3838
// https://magpcss.org/ceforum/viewtopic.php?t=11491
3939
// https://github.com/KevinnZou/compose-webview-multiplatform/issues/46
40+
// I found this formula much near to the other platforms, so I replace it
4041
val givenZoomLevel = state.webSettings.zoomLevel
41-
val realZoomLevel =
42-
if (givenZoomLevel >= 0.0) {
43-
ln(abs(givenZoomLevel)) / ln(1.2)
44-
} else {
45-
-ln(abs(givenZoomLevel)) / ln(1.2)
46-
}
42+
43+
val percentage = givenZoomLevel * 100.0
44+
val realZoomLevel = (percentage - 100.0) / 25.0
45+
4746
KLogger.d { "titleProperty: $title" }
4847
zoomLevel = realZoomLevel
4948
state.pageTitle = title

webview/src/desktopMain/kotlin/com/multiplatform/webview/web/WebView.desktop.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,8 @@ actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView =
7272
param.requestContext,
7373
)
7474
is WebContent.Data ->
75-
param.client.createBrowserWithHtml(
76-
content.data,
77-
content.baseUrl ?: KCEFBrowser.BLANK_URI,
75+
param.client.createBrowser(
76+
KCEFBrowser.BLANK_URI,
7877
param.rendering,
7978
param.transparent,
8079
)

0 commit comments

Comments
 (0)