@@ -14,11 +14,11 @@ import android.webkit.WebResourceResponse
1414import android.webkit.WebSettings
1515import android.webkit.WebView
1616import android.webkit.WebViewClient
17+ import androidx.webkit.WebViewAssetLoader
1718import com.didi.dimina.common.LogUtils
18- import com.didi.dimina.common.PathUtils.FILE_PROTOCOL
19+ import com.didi.dimina.common.PathUtils
1920import com.didi.dimina.common.VersionUtils
2021import java.io.File
21- import java.io.FileInputStream
2222import java.lang.ref.WeakReference
2323import java.util.concurrent.ConcurrentHashMap
2424import 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 */
507534internal 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
0 commit comments