Skip to content

Commit f40b28a

Browse files
authored
Merge pull request #317 from amirghm/feature/improve-zoom-support-and-add-asset-loader
Feature/improve zoom support and add android webview asset loader
2 parents 0814f9d + e715100 commit f40b28a

File tree

12 files changed

+124
-22
lines changed

12 files changed

+124
-22
lines changed

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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 =
16+
when {
17+
path.endsWith(".html") -> "text/html"
18+
path.endsWith(".js") -> "application/javascript"
19+
path.endsWith(".css") -> "text/css"
20+
path.endsWith(".json") -> "application/json"
21+
path.endsWith(".png") -> "image/png"
22+
path.endsWith(".jpg") || path.endsWith(".jpeg") -> "image/jpeg"
23+
path.endsWith(".svg") -> "image/svg+xml"
24+
path.endsWith(".webp") -> "image/webp"
25+
path.endsWith(".ico") -> "image/x-icon"
26+
path.endsWith(".woff") -> "font/woff"
27+
path.endsWith(".woff2") -> "font/woff2"
28+
path.endsWith(".ttf") -> "font/ttf"
29+
path.endsWith(".mp4") -> "video/mp4"
30+
path.endsWith(".webm") -> "video/webm"
31+
path.endsWith(".ogg") -> "video/ogg"
32+
path.endsWith(".mp3") -> "audio/mpeg"
33+
path.endsWith(".wav") -> "audio/wav"
34+
path.endsWith(".wasm") -> "application/wasm"
35+
path.endsWith(".pdf") -> "application/pdf"
36+
path.endsWith(".zip") -> "application/zip"
37+
path.endsWith(".csv") -> "text/csv"
38+
else -> "application/octet-stream"
39+
}
40+
41+
return WebResourceResponse(mimeType, "utf-8", FileInputStream(file))
42+
}
43+
}

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

Lines changed: 28 additions & 2 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,16 @@ fun AccompanistWebView(
208210
loadsImagesAutomatically = it.loadsImagesAutomatically
209211
domStorageEnabled = it.domStorageEnabled
210212
mediaPlaybackRequiresUserGesture = it.mediaPlaybackRequiresUserGesture
213+
214+
if (it.enableSandbox) {
215+
client.assetLoader =
216+
WebViewAssetLoader
217+
.Builder()
218+
.addPathHandler(
219+
it.sandboxSubdomain,
220+
InternalStoragePathHandler(),
221+
).build()
222+
}
211223
}
212224
}
213225
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
@@ -261,6 +273,8 @@ open class AccompanistWebViewClient : WebViewClient() {
261273
internal set
262274
private var isRedirect = false
263275

276+
var assetLoader: WebViewAssetLoader? = null
277+
264278
override fun onPageStarted(
265279
view: WebView,
266280
url: String?,
@@ -274,14 +288,26 @@ open class AccompanistWebViewClient : WebViewClient() {
274288
state.errorsForCurrentRequest.clear()
275289
state.pageTitle = null
276290
state.lastLoadedUrl = url
291+
val supportZoom = if (state.webSettings.supportZoom) "yes" else "no"
277292

278293
// set scale level
279294
@Suppress("ktlint:standard:max-line-length")
280295
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);"
296+
"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);"
282297
navigator.evaluateJavaScript(script)
283298
}
284299

300+
override fun shouldInterceptRequest(
301+
view: WebView?,
302+
request: WebResourceRequest?,
303+
): WebResourceResponse? {
304+
val url = request?.url
305+
KLogger.d { "Intercepting request for URL: $url" }
306+
return url?.let {
307+
assetLoader?.shouldInterceptRequest(it)
308+
} ?: super.shouldInterceptRequest(view, request)
309+
}
310+
285311
override fun onPageFinished(
286312
view: WebView,
287313
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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,37 @@ 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+
* The virtual subdomain prefix to be used with WebViewAssetLoader for local file access.
212+
*
213+
* This is typically set to something like "/app/" or "/assets/" and must match the path
214+
* used in your PathHandler configuration inside WebViewAssetLoader.
215+
*
216+
* When you load a URL such as `https://appassets.androidplatform.net/app/index.html`
217+
* (the standard host used by WebViewAssetLoader) in your WebView,
218+
* the WebViewAssetLoader will map it to the correct local file or asset if configured properly.
219+
* This URL should be used instead of file:// URLs to ensure secure and modern WebView behavior.
220+
*/
221+
var sandboxSubdomain: String = "/app/",
191222
) : PlatformWebSettings() {
192223
object LayerType {
193224
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: 7 additions & 2 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
}

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 & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import org.cef.handler.CefDisplayHandler
1111
import org.cef.handler.CefLoadHandler
1212
import org.cef.handler.CefRequestHandlerAdapter
1313
import org.cef.network.CefRequest
14-
import kotlin.math.abs
15-
import kotlin.math.ln
1614

1715
/**
1816
* Created By Kevin Zou On 2023/9/12
@@ -37,13 +35,12 @@ internal fun CefBrowser.addDisplayHandler(state: WebViewState) {
3735
) {
3836
// https://magpcss.org/ceforum/viewtopic.php?t=11491
3937
// https://github.com/KevinnZou/compose-webview-multiplatform/issues/46
38+
// I found this formula much near to the other platforms, so I replace it
4039
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-
}
40+
41+
val percentage = givenZoomLevel * 100.0
42+
val realZoomLevel = (percentage - 100.0) / 25.0
43+
4744
KLogger.d { "titleProperty: $title" }
4845
zoomLevel = realZoomLevel
4946
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)