Skip to content

Commit afb1906

Browse files
committed
feat(android): integrate WebViewAssetLoader for enhanced resource handling in WebView (#129)
1 parent 4cec9fb commit afb1906

5 files changed

Lines changed: 77 additions & 108 deletions

File tree

android/dimina/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ dependencies {
6565
implementation(libs.androidx.ui)
6666
implementation(libs.androidx.ui.graphics)
6767
implementation(libs.androidx.material3)
68+
implementation(libs.androidx.webkit)
6869
implementation(libs.androidx.compose.material.icons.core)
6970
implementation(libs.kotlinx.serialization.json)
7071
implementation(libs.mmkv)
@@ -120,4 +121,4 @@ tasks.register<Copy>("copySharedJssdkToAssets") {
120121
// Make the preBuild task depend on the copy tasks
121122
tasks.named("preBuild") {
122123
dependsOn("copySharedJssdkToAssets")
123-
}
124+
}

android/dimina/src/main/kotlin/com/didi/dimina/common/PathUtils.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import java.io.File
88
* Author: Doslin
99
*/
1010
object PathUtils {
11-
const val FILE_PROTOCOL = "file://"
11+
const val WEBVIEW_ASSET_DOMAIN = "appassets.androidplatform.net"
12+
const val WEBVIEW_BASE_URL = "https://$WEBVIEW_ASSET_DOMAIN"
13+
const val WEBVIEW_JSAPP_BASE_URL = "$WEBVIEW_BASE_URL/jsapp/"
14+
const val WEBVIEW_JSSDK_BASE_URL = "$WEBVIEW_BASE_URL/jssdk/"
1215
private const val VIRTUAL_DOMAIN_URL = "difile://"
1316

1417
fun isLegalPath(path: String): Boolean {
@@ -40,4 +43,4 @@ object PathUtils {
4043
null
4144
}
4245
}
43-
}
46+
}

android/dimina/src/main/kotlin/com/didi/dimina/core/Bridge.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.didi.dimina.bean.PathInfo
66
import com.didi.dimina.common.LogUtils
77
import com.didi.dimina.common.PathUtils
88
import com.didi.dimina.common.Utils
9+
import com.didi.dimina.common.VersionUtils
910
import com.didi.dimina.engine.qjs.JSValue
1011
import com.didi.dimina.ui.container.DiminaActivity
1112
import com.didi.dimina.ui.view.DiminaRenderBridge
@@ -49,7 +50,9 @@ class Bridge(
4950
), DiminaRenderBridge.TAG)
5051
}
5152
// 加载模版页面
52-
options.webview.loadUrl("${PathUtils.FILE_PROTOCOL}/pageFrame.html")
53+
options.webview.loadUrl(
54+
"${PathUtils.WEBVIEW_JSSDK_BASE_URL}${VersionUtils.getJSVersion()}/main/pageFrame.html"
55+
)
5356
}
5457

5558
/**
@@ -63,7 +66,8 @@ class Bridge(
6366
"bridgeId" to id,
6467
"appId" to options.appId,
6568
"pagePath" to options.pathInfo.pagePath,
66-
"root" to options.root
69+
"root" to options.root,
70+
"baseUrl" to PathUtils.WEBVIEW_JSAPP_BASE_URL
6771
)
6872
)
6973

@@ -85,7 +89,8 @@ class Bridge(
8589
"bridgeId" to id,
8690
"appId" to options.appId,
8791
"pagePath" to options.pathInfo.pagePath,
88-
"root" to options.root
92+
"root" to options.root,
93+
"baseUrl" to PathUtils.WEBVIEW_JSAPP_BASE_URL
8994
)
9095
)
9196

@@ -303,4 +308,4 @@ class Bridge(
303308

304309
LogUtils.d(tag, "Bridge options updated: pathInfo=$pathInfo, root=$root")
305310
}
306-
}
311+
}

android/dimina/src/main/kotlin/com/didi/dimina/ui/view/WebViewCacheManager.kt

Lines changed: 59 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ import android.webkit.WebResourceResponse
1414
import android.webkit.WebSettings
1515
import android.webkit.WebView
1616
import android.webkit.WebViewClient
17+
import androidx.webkit.WebViewAssetLoader
1718
import com.didi.dimina.common.LogUtils
18-
import com.didi.dimina.common.PathUtils.FILE_PROTOCOL
19+
import com.didi.dimina.common.PathUtils
1920
import com.didi.dimina.common.VersionUtils
2021
import java.io.File
21-
import java.io.FileInputStream
2222
import java.lang.ref.WeakReference
2323
import java.util.concurrent.ConcurrentHashMap
2424
import java.util.concurrent.LinkedBlockingQueue
@@ -276,7 +276,7 @@ object WebViewCacheManager : ComponentCallbacks2 {
276276
webView.clearCache(true)
277277

278278
// 重新设置WebViewClient
279-
webView.webViewClient = createWebViewClientWithInterceptor { onPageLoadFinished() }
279+
webView.webViewClient = createWebViewClientWithInterceptor(webView.context) { onPageLoadFinished() }
280280

281281
} catch (e: Exception) {
282282
LogUtils.e(TAG, "Failed to reset WebView", e)
@@ -474,39 +474,68 @@ object WebViewCacheManager : ComponentCallbacks2 {
474474
}
475475

476476
// 文件级别的TAG常量,用于日志记录
477-
private const val WEBVIEW_TAG = "WebViewInterceptor"
477+
private const val WEBVIEW_TAG = "WebViewAssetLoader"
478478

479-
/**
480-
* 根据给定的URL获取文件对象
481-
* 此函数用于区分jsapp和jssdk类型的URL,并返回相应的文件对象
482-
*
483-
* @param context 上下文对象,用于访问应用程序的文件目录
484-
* @param url 需要解析的URL,用于确定文件路径
485-
* @return 返回一个File对象,表示解析后的文件路径
486-
*/
487-
internal fun getFilesFile(context: Context, url: String): File {
488-
val filesDir = context.filesDir
489-
val appIdRegex = "(wx|dd)[0-9a-zA-Z]{16}".toRegex()
490-
val matchResult = appIdRegex.find(url)
491-
return if (matchResult != null) {
492-
// jsapp url,使用 appId 并构造路径
493-
File(filesDir, "jsapp/$url")
494-
} else {
495-
// jssdk url,使用版本号构造路径
496-
File(filesDir, "jssdk/${VersionUtils.getJSVersion()}/main/$url")
479+
private class DiminaPathHandler(
480+
private val filesDir: File,
481+
private val currentJsVersion: Int
482+
) : WebViewAssetLoader.PathHandler {
483+
override fun handle(path: String): WebResourceResponse? {
484+
val normalizedPath = path.trimStart('/')
485+
if (normalizedPath.isEmpty()) {
486+
return null
487+
}
488+
489+
val targetFile = when {
490+
normalizedPath.startsWith("assets/") -> {
491+
File(filesDir, "jssdk/$currentJsVersion/main/$normalizedPath")
492+
}
493+
normalizedPath.startsWith("jssdk/") -> {
494+
File(filesDir, normalizedPath)
495+
}
496+
normalizedPath.startsWith("jsapp/") -> {
497+
File(filesDir, normalizedPath)
498+
}
499+
else -> {
500+
// 编译产物中的资源常以 /<appId>/... 的绝对路径输出。
501+
File(filesDir, "jsapp/$normalizedPath")
502+
}
503+
}
504+
505+
val filesCanonical = filesDir.canonicalFile
506+
val targetCanonical = targetFile.canonicalFile
507+
if (!targetCanonical.path.startsWith(filesCanonical.path) || !targetCanonical.exists()) {
508+
return null
509+
}
510+
511+
val mimeType = MimeTypeMap.getSingleton()
512+
.getMimeTypeFromExtension(targetCanonical.extension)
513+
?: "application/octet-stream"
514+
return WebResourceResponse(mimeType, "UTF-8", targetCanonical.inputStream())
497515
}
498516
}
499517

518+
private fun createWebViewAssetLoader(context: Context): WebViewAssetLoader {
519+
val appContext = context.applicationContext
520+
val currentJsVersion = VersionUtils.getJSVersion()
521+
return WebViewAssetLoader.Builder()
522+
.setDomain(PathUtils.WEBVIEW_ASSET_DOMAIN)
523+
.addPathHandler(
524+
"/",
525+
DiminaPathHandler(appContext.filesDir, currentJsVersion)
526+
)
527+
.build()
528+
}
529+
500530
/**
501-
* 创建统一的文件拦截处理器
502-
* 用于拦截WebView的文件协议请求,从本地文件系统加载资源
503-
*
504-
* @param onPageFinished 页面加载完成的回调
505-
* @return WebViewClient实例
531+
* 创建统一的资源拦截处理器。
532+
* 使用 WebViewAssetLoader 暴露 app 私有目录中的 jssdk/jsapp 资源,统一通过 https 域访问。
506533
*/
507534
internal fun createWebViewClientWithInterceptor(
535+
context: Context,
508536
onPageFinished: (String) -> Unit = {}
509537
): WebViewClient {
538+
val assetLoader = createWebViewAssetLoader(context)
510539
return object : WebViewClient() {
511540
override fun onPageFinished(view: WebView, url: String) {
512541
super.onPageFinished(view, url)
@@ -515,83 +544,12 @@ internal fun createWebViewClientWithInterceptor(
515544
onPageFinished(url)
516545
}
517546
}
518-
547+
519548
override fun shouldInterceptRequest(
520549
view: WebView,
521550
request: WebResourceRequest
522-
): WebResourceResponse? {
523-
return handleFileInterceptRequest(view.context, request)
524-
}
525-
}
526-
}
527-
528-
/**
529-
* 统一处理文件拦截请求的逻辑
530-
* 拦截file://协议的请求,优先从本地文件系统加载资源
531-
*
532-
* 处理策略:
533-
* 1. 优先尝试作为本地文件加载
534-
* 2. 如果文件不存在,则判断可能是被错误解析的协议相对URL,转换为https://协议加载
535-
*
536-
* 这样可以兼容两种情况:
537-
* - 真正的本地文件:直接加载
538-
* - 被错误解析的网络资源:自动转换为https加载
539-
*
540-
* @param context 上下文对象
541-
* @param request WebView资源请求
542-
* @return 如果是文件协议请求且文件存在,返回WebResourceResponse;如果文件不存在则尝试网络加载;否则返回null
543-
*/
544-
internal fun handleFileInterceptRequest(
545-
context: Context,
546-
request: WebResourceRequest
547-
): WebResourceResponse? {
548-
val url = request.url.toString()
549-
550-
if (url.startsWith(FILE_PROTOCOL)) {
551-
try {
552-
// 提取file://协议后的路径部分
553-
val pathAfterProtocol = url.substring(FILE_PROTOCOL.length)
554-
555-
// 优先尝试作为本地文件加载
556-
val localFile = getFilesFile(context, pathAfterProtocol)
557-
if (localFile.exists()) {
558-
LogUtils.d(WEBVIEW_TAG, "Loading file from local: $url")
559-
val mimeType = MimeTypeMap.getSingleton()
560-
.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(url))
561-
?: "text/html" // Fallback MIME type
562-
return WebResourceResponse(mimeType, "UTF-8", FileInputStream(localFile))
563-
}
564-
565-
// 文件不存在,可能是被错误解析的协议相对URL,尝试转换为https://协议加载网络资源
566-
LogUtils.d(WEBVIEW_TAG, "Local file not found: $url")
567-
LogUtils.d(WEBVIEW_TAG, "Attempting to load as network resource via https")
568-
569-
val correctedUrl = "https://${pathAfterProtocol.trimStart('/')}" // 转换为 https://domain/path
570-
LogUtils.d(WEBVIEW_TAG, "Corrected URL: $correctedUrl")
571-
572-
try {
573-
val connection = java.net.URL(correctedUrl).openConnection()
574-
connection.connectTimeout = 5000
575-
connection.readTimeout = 10000
576-
val inputStream = connection.getInputStream()
577-
val mimeType = connection.contentType?.split(";")?.firstOrNull()?.trim()
578-
?: MimeTypeMap.getSingleton()
579-
.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(correctedUrl))
580-
?: "application/octet-stream"
581-
582-
LogUtils.d(WEBVIEW_TAG, "Successfully loaded network resource: $correctedUrl")
583-
return WebResourceResponse(mimeType, "UTF-8", inputStream)
584-
} catch (e: Exception) {
585-
LogUtils.e(WEBVIEW_TAG, "Failed to load network resource: $correctedUrl", e)
586-
// 返回null,让WebView按默认方式处理
587-
return null
588-
}
589-
} catch (e: Exception) {
590-
LogUtils.e(WEBVIEW_TAG, "Error intercepting file: $url", e)
591-
}
551+
) = assetLoader.shouldInterceptRequest(request.url)
592552
}
593-
594-
return null
595553
}
596554

597555
/**
@@ -630,7 +588,7 @@ internal fun createWebView(context: Context, onPageLoadFinished: () -> Unit): We
630588
}
631589

632590
// Configure WebViewClient with file interceptor
633-
webViewClient = createWebViewClientWithInterceptor { onPageLoadFinished() }
591+
webViewClient = createWebViewClientWithInterceptor(context) { onPageLoadFinished() }
634592
}
635593
}
636594

android/gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ materialIconsCore = "1.7.8"
1717
mmkv = "2.2.4"
1818
okhttp = "5.2.1"
1919
uiTooling = "1.8.2"
20+
webkit = "1.15.0"
2021

2122
[libraries]
2223
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -41,6 +42,7 @@ material = { group = "com.google.android.material", name = "material", version.r
4142
mmkv = { module = "com.tencent:mmkv", version.ref = "mmkv" }
4243
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
4344
ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "uiTooling" }
45+
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
4446

4547
[plugins]
4648
android-application = { id = "com.android.application", version.ref = "agp" }

0 commit comments

Comments
 (0)