catalyst-core 0.1.0-canary.9 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/catalyst.js +8 -1
- package/changelog.md +6 -0
- package/dist/native/androidProject/app/build.gradle.kts +34 -8
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +270 -78
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +34 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +149 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +156 -39
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
- package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
- package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
- package/dist/native/androidSetup.js +1 -0
- package/dist/native/bridge/WebBridge.js +5 -5
- package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
- package/dist/native/bridge/hooks/index.js +1 -0
- package/dist/native/bridge/hooks/useCamera.js +9 -0
- package/dist/native/bridge/hooks/useDataProtection.js +2 -0
- package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
- package/dist/native/bridge/hooks/useFilePicker.js +2 -0
- package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
- package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
- package/dist/native/bridge/hooks/useIntent.js +1 -0
- package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
- package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
- package/dist/native/bridge/hooks/useNotification.js +2 -0
- package/dist/native/bridge/hooks/useSafeArea.js +2 -0
- package/dist/native/bridge/hooks/useVideoStream.js +6 -0
- package/dist/native/bridge/hooks/utils.js +1 -0
- package/dist/native/bridge/hooks.js +1 -137
- package/dist/native/bridge/useBaseHook.js +12 -7
- package/dist/native/bridge/utils/NativeBridge.js +41 -6
- package/dist/native/buildAndroid/assets.js +52 -0
- package/dist/native/buildAndroid/build.js +19 -0
- package/dist/native/buildAndroid/config.js +1 -0
- package/dist/native/buildAndroid/index.js +1 -0
- package/dist/native/buildAppAndroid.js +2 -150
- package/dist/native/buildAppIos.js +2 -312
- package/dist/native/buildIos/assets.js +8 -0
- package/dist/native/buildIos/build.js +40 -0
- package/dist/native/buildIos/config.js +71 -0
- package/dist/native/buildIos/index.js +6 -0
- package/dist/native/buildIos/plugins.js +45 -0
- package/dist/native/internalPluginUtils.js +7 -0
- package/dist/native/iosnativeWebView/Package.resolved +36 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +39 -4
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
- package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
- package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +53 -8
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +88 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +30 -11
- package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
- package/dist/native/plugin-bridge/PluginBridge.js +1 -0
- package/dist/native/pluginComposerAndroid.js +28 -0
- package/dist/native/pluginComposerIos.js +16 -0
- package/dist/native/renameAndroidProject.js +5 -5
- package/dist/native/setupEmulatorIos.js +6 -4
- package/dist/native/setupServer.js +2 -3
- package/dist/native/utils.js +4 -1
- package/dist/offline/catalyst-sw.js +1 -0
- package/dist/offline/registerServiceWorker.js +1 -0
- package/dist/otel.js +70 -2
- package/dist/router/ClientRouter.js +1 -1
- package/dist/scripts/build.js +1 -1
- package/dist/scripts/cleanCache.js +1 -0
- package/dist/scripts/devBuild.js +1 -1
- package/dist/scripts/devServe.js +1 -1
- package/dist/scripts/generateOfflineManifest.js +1 -0
- package/dist/scripts/loadEnvironmentVariables.js +1 -2
- package/dist/scripts/plugins.js +1 -0
- package/dist/scripts/registerAliases.js +6 -2
- package/dist/scripts/scriptUtils.js +2 -1
- package/dist/scripts/serve.js +2 -1
- package/dist/scripts/start.js +7 -2
- package/dist/server/expressServer.js +4 -8
- package/dist/server/renderer/document/Body.js +4 -4
- package/dist/server/renderer/document/Head.js +2 -2
- package/dist/server/renderer/extract.js +31 -27
- package/dist/server/renderer/handler.js +47 -24
- package/dist/server/renderer/index.js +1 -1
- package/dist/server/startServer.js +57 -7
- package/dist/server/utils/userAgentUtil.js +9 -3
- package/dist/server/utils/validator.js +1 -1
- package/dist/webpack/babel-plugins/remove-client.plugin.js +2 -2
- package/dist/webpack/babel-plugins/remove-ssr.plugin.js +2 -2
- package/dist/webpack/babel.config.client.js +1 -1
- package/dist/webpack/babel.config.ssr.js +1 -1
- package/dist/webpack/base.babel.js +7 -8
- package/dist/webpack/development.client.babel.js +7 -16
- package/dist/webpack/production.client.babel.js +1 -1
- package/dist/webpack/production.ssr.babel.js +5 -1
- package/mcp_v2/knowledge-base.json +206 -100
- package/mcp_v2/lib/helpers.js +42 -8
- package/mcp_v2/lib/seed.js +88 -0
- package/mcp_v2/mcp.js +150 -4
- package/mcp_v2/setup.js +30 -193
- package/mcp_v2/tools/build.js +1 -1
- package/mcp_v2/tools/config.js +16 -4
- package/mcp_v2/tools/conversion.js +11 -11
- package/mcp_v2/tools/github.js +1117 -0
- package/mcp_v2/tools/knowledge.js +16 -3
- package/mcp_v2/tools/sync.js +179 -10
- package/mcp_v2/tools/tasks.js +13 -4
- package/package.json +20 -8
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -31,10 +31,12 @@ class CustomWebView(
|
|
|
31
31
|
) : CoroutineScope {
|
|
32
32
|
|
|
33
33
|
private val TAG = "WebViewDebug"
|
|
34
|
+
private val FLOW_TAG = "CatalystOfflineFlow"
|
|
34
35
|
private val job = SupervisorJob()
|
|
35
36
|
override val coroutineContext = Dispatchers.Main + job
|
|
36
37
|
|
|
37
38
|
private var cacheManager: WebCacheManager
|
|
39
|
+
private var offlineCacheService: OfflineCacheService
|
|
38
40
|
private val metricsMonitor = MetricsMonitor.getInstance(context)
|
|
39
41
|
private var buildType: String = "debug" // Default to debug
|
|
40
42
|
private var cachePatterns: List<String> = emptyList()
|
|
@@ -50,18 +52,19 @@ class CustomWebView(
|
|
|
50
52
|
private val offlineAssetUrl = "file:///android_asset/$offlineAssetPath"
|
|
51
53
|
private var offlinePageVisible = false
|
|
52
54
|
private var lastTargetUrl: String? = null
|
|
55
|
+
private var activeOfflineRouteOrigin: String? = null
|
|
56
|
+
private var visibleOfflineSnapshotUrl: String? = null
|
|
53
57
|
private var defaultRequestHeaders: Map<String, String> = emptyMap()
|
|
58
|
+
var onPageStarted: (() -> Unit)? = null
|
|
54
59
|
|
|
55
60
|
// Counters for asset loading statistics
|
|
56
61
|
private var assetLoadAttempts = 0
|
|
57
62
|
private var assetLoadFailures = 0
|
|
58
63
|
|
|
59
|
-
// Track ongoing cache fetches to prevent duplicates
|
|
60
|
-
private val ongoingCacheFetches = mutableSetOf<String>()
|
|
61
|
-
|
|
62
64
|
init {
|
|
63
65
|
setupFromProperties()
|
|
64
66
|
cacheManager = WebCacheManager(context, properties)
|
|
67
|
+
offlineCacheService = OfflineCacheService(context)
|
|
65
68
|
setupWebView()
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -96,7 +99,7 @@ class CustomWebView(
|
|
|
96
99
|
|
|
97
100
|
// Access control toggle
|
|
98
101
|
accessControlEnabled = properties
|
|
99
|
-
.getProperty("accessControl.enabled", "
|
|
102
|
+
.getProperty("accessControl.enabled", "false")
|
|
100
103
|
.equals("true", ignoreCase = true)
|
|
101
104
|
|
|
102
105
|
// Initialize URLWhitelistManager with access control configuration
|
|
@@ -136,6 +139,15 @@ class CustomWebView(
|
|
|
136
139
|
|
|
137
140
|
private fun loadUrlInternal(url: String) {
|
|
138
141
|
val isNetworkUrl = url.startsWith("http://") || url.startsWith("https://")
|
|
142
|
+
val isOnline = NetworkUtils.getCurrentStatus(context).isOnline
|
|
143
|
+
|
|
144
|
+
if (BuildConfig.DEBUG) {
|
|
145
|
+
Log.d(FLOW_TAG, "WEBVIEW load-url start online=$isOnline url=$url headers=${defaultRequestHeaders.keys.sorted()}")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isNetworkUrl && isOnline) {
|
|
149
|
+
offlineCacheService.refreshManifestAsync(url, defaultRequestHeaders)
|
|
150
|
+
}
|
|
139
151
|
|
|
140
152
|
if (isNetworkUrl && defaultRequestHeaders.isNotEmpty()) {
|
|
141
153
|
if (BuildConfig.DEBUG) {
|
|
@@ -145,11 +157,16 @@ class CustomWebView(
|
|
|
145
157
|
} else {
|
|
146
158
|
webView.loadUrl(url)
|
|
147
159
|
}
|
|
160
|
+
if (BuildConfig.DEBUG) {
|
|
161
|
+
Log.d(FLOW_TAG, "WEBVIEW load-url dispatched url=$url")
|
|
162
|
+
}
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
fun loadUrl(url: String) {
|
|
151
166
|
lastTargetUrl = url
|
|
152
167
|
offlinePageVisible = false
|
|
168
|
+
visibleOfflineSnapshotUrl = null
|
|
169
|
+
activeOfflineRouteOrigin = null
|
|
153
170
|
loadUrlInternal(url)
|
|
154
171
|
}
|
|
155
172
|
|
|
@@ -166,6 +183,8 @@ class CustomWebView(
|
|
|
166
183
|
try {
|
|
167
184
|
context.assets.open(offlineAssetPath).close()
|
|
168
185
|
offlinePageVisible = true
|
|
186
|
+
visibleOfflineSnapshotUrl = null
|
|
187
|
+
activeOfflineRouteOrigin = null
|
|
169
188
|
loadUrlInternal(offlineAssetUrl)
|
|
170
189
|
if (BuildConfig.DEBUG) {
|
|
171
190
|
Log.d(TAG, "📴 Showing offline page from assets: $offlineAssetPath")
|
|
@@ -191,6 +210,32 @@ class CustomWebView(
|
|
|
191
210
|
|
|
192
211
|
fun clearAllCache() {
|
|
193
212
|
cacheManager.clearAll()
|
|
213
|
+
offlineCacheService.clearAll()
|
|
214
|
+
activeOfflineRouteOrigin = null
|
|
215
|
+
visibleOfflineSnapshotUrl = null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fun showOfflineRouteOrOfflinePage(url: String) {
|
|
219
|
+
val snapshotUrl = normalizeUrlWithoutFragment(url)
|
|
220
|
+
if (visibleOfflineSnapshotUrl == snapshotUrl) {
|
|
221
|
+
if (BuildConfig.DEBUG) {
|
|
222
|
+
Log.d(TAG, "📴 Cached offline route snapshot already visible: $url")
|
|
223
|
+
}
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (offlineCacheService.hasRouteSnapshot(url)) {
|
|
228
|
+
offlinePageVisible = false
|
|
229
|
+
visibleOfflineSnapshotUrl = snapshotUrl
|
|
230
|
+
updateActiveOfflineRoute(url, true)
|
|
231
|
+
loadUrlInternal(url)
|
|
232
|
+
if (BuildConfig.DEBUG) {
|
|
233
|
+
Log.d(TAG, "📴 Loading cached offline route through request interceptor: $url")
|
|
234
|
+
}
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
showOfflinePage()
|
|
194
239
|
}
|
|
195
240
|
|
|
196
241
|
fun canGoBack(): Boolean = webView.canGoBack()
|
|
@@ -233,7 +278,7 @@ class CustomWebView(
|
|
|
233
278
|
}
|
|
234
279
|
|
|
235
280
|
// Additional security check: only allow whitelisted interface names
|
|
236
|
-
val allowedInterfaces = setOf("NativeBridge", "AndroidBridge")
|
|
281
|
+
val allowedInterfaces = setOf("NativeBridge", "AndroidBridge", "PluginBridge")
|
|
237
282
|
if (name !in allowedInterfaces) {
|
|
238
283
|
Log.e(TAG, "❌ Security: Interface name '$name' is not in whitelist. Refusing to add interface.")
|
|
239
284
|
return
|
|
@@ -246,6 +291,17 @@ class CustomWebView(
|
|
|
246
291
|
}
|
|
247
292
|
}
|
|
248
293
|
|
|
294
|
+
fun removeJavascriptInterface(name: String) {
|
|
295
|
+
try {
|
|
296
|
+
webView.removeJavascriptInterface(name)
|
|
297
|
+
if (BuildConfig.DEBUG) {
|
|
298
|
+
Log.d(TAG, "🔌 Removed JavaScript interface: $name")
|
|
299
|
+
}
|
|
300
|
+
} catch (e: Exception) {
|
|
301
|
+
Log.w(TAG, "⚠️ Failed to remove JavaScript interface '$name': ${e.message}")
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
249
305
|
fun destroy() {
|
|
250
306
|
job.cancel()
|
|
251
307
|
webView.destroy()
|
|
@@ -292,25 +348,112 @@ class CustomWebView(
|
|
|
292
348
|
}
|
|
293
349
|
}
|
|
294
350
|
|
|
295
|
-
private fun
|
|
351
|
+
private fun shouldCacheRequest(request: WebResourceRequest, url: String): Boolean {
|
|
296
352
|
if (BuildConfig.DEBUG) {
|
|
297
353
|
Log.d(TAG, "🔍 CACHE_CHECK: Checking if URL should be cached: $url")
|
|
298
354
|
Log.d(TAG, "🔍 CACHE_CHECK: Cache patterns: $cachePatterns")
|
|
299
355
|
}
|
|
300
356
|
|
|
301
|
-
val
|
|
357
|
+
val matchesPattern = matchesCachePattern(url, cachePatterns)
|
|
358
|
+
val offlineRouteSubresource = shouldCacheOfflineRouteSubresource(request, url)
|
|
359
|
+
val shouldCache = matchesPattern || offlineRouteSubresource
|
|
302
360
|
|
|
303
361
|
if (BuildConfig.DEBUG) {
|
|
304
362
|
if (shouldCache) {
|
|
305
|
-
Log.d(TAG, "✅ CACHE_CHECK: URL WILL be cached: $url")
|
|
363
|
+
Log.d(TAG, "✅ CACHE_CHECK: URL WILL be cached: $url (pattern=$matchesPattern, offlineRouteSubresource=$offlineRouteSubresource)")
|
|
364
|
+
Log.d(FLOW_TAG, "CACHE eligible=true pattern=$matchesPattern offlineRouteSubresource=$offlineRouteSubresource mainFrame=${request.isForMainFrame} method=${request.method} dest=${requestHeader(request, "Sec-Fetch-Dest") ?: "none"} accept=${requestHeader(request, "Accept") ?: "none"} activeOrigin=${activeOfflineRouteOrigin ?: "none"} url=$url")
|
|
306
365
|
} else {
|
|
307
366
|
Log.d(TAG, "❌ CACHE_CHECK: URL will NOT be cached (no pattern match): $url")
|
|
367
|
+
Log.d(FLOW_TAG, "CACHE eligible=false pattern=$matchesPattern offlineRouteSubresource=$offlineRouteSubresource mainFrame=${request.isForMainFrame} method=${request.method} dest=${requestHeader(request, "Sec-Fetch-Dest") ?: "none"} accept=${requestHeader(request, "Accept") ?: "none"} activeOrigin=${activeOfflineRouteOrigin ?: "none"} url=$url")
|
|
308
368
|
}
|
|
309
369
|
}
|
|
310
370
|
|
|
311
371
|
return shouldCache
|
|
312
372
|
}
|
|
313
373
|
|
|
374
|
+
private fun shouldCacheOfflineRouteSubresource(request: WebResourceRequest, url: String): Boolean {
|
|
375
|
+
if (request.isForMainFrame || request.method != "GET") return false
|
|
376
|
+
if (!isHttpUrl(url) || isApiCall(url) || isInternalOfflineRuntimeUrl(url)) return false
|
|
377
|
+
|
|
378
|
+
val activeOrigin = activeOfflineRouteOrigin ?: return false
|
|
379
|
+
if (originForUrl(url) != activeOrigin) return false
|
|
380
|
+
|
|
381
|
+
val destination = requestHeader(request, "Sec-Fetch-Dest")?.lowercase()
|
|
382
|
+
if (destination == "document" || destination == "empty") return false
|
|
383
|
+
if (destination in setOf("script", "style", "image", "font", "audio", "video", "track", "manifest")) {
|
|
384
|
+
return true
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
val accept = requestHeader(request, "Accept")?.lowercase().orEmpty()
|
|
388
|
+
if (accept.contains("text/html") ||
|
|
389
|
+
accept.contains("application/json") ||
|
|
390
|
+
accept.contains("text/event-stream")
|
|
391
|
+
) {
|
|
392
|
+
return false
|
|
393
|
+
}
|
|
394
|
+
if (accept.contains("text/css") ||
|
|
395
|
+
accept.contains("javascript") ||
|
|
396
|
+
accept.contains("image/") ||
|
|
397
|
+
accept.contains("font/") ||
|
|
398
|
+
accept.contains("application/font") ||
|
|
399
|
+
accept.contains("application/wasm")
|
|
400
|
+
) {
|
|
401
|
+
return true
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return offlineCacheService.shouldCacheAssetUrl(url)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private fun requestHeader(request: WebResourceRequest, name: String): String? {
|
|
408
|
+
return request.requestHeaders.entries.firstOrNull {
|
|
409
|
+
it.key.equals(name, ignoreCase = true)
|
|
410
|
+
}?.value
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private fun updateActiveOfflineRoute(url: String, active: Boolean) {
|
|
414
|
+
activeOfflineRouteOrigin = if (active) originForUrl(url) else null
|
|
415
|
+
if (BuildConfig.DEBUG) {
|
|
416
|
+
Log.d(FLOW_TAG, "ROUTE active=$active activeOrigin=${activeOfflineRouteOrigin ?: "none"} url=$url")
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private fun isVisibleOfflineSnapshot(url: String?): Boolean {
|
|
421
|
+
if (url == null) return false
|
|
422
|
+
return visibleOfflineSnapshotUrl == normalizeUrlWithoutFragment(url)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private fun normalizeUrlWithoutFragment(url: String): String {
|
|
426
|
+
return try {
|
|
427
|
+
Uri.parse(url).buildUpon().fragment(null).build().toString()
|
|
428
|
+
} catch (_: Exception) {
|
|
429
|
+
url
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private fun originForUrl(url: String): String? {
|
|
434
|
+
return try {
|
|
435
|
+
val uri = Uri.parse(url)
|
|
436
|
+
val scheme = uri.scheme?.lowercase() ?: return null
|
|
437
|
+
val authority = uri.authority ?: return null
|
|
438
|
+
if (scheme != "http" && scheme != "https") return null
|
|
439
|
+
"$scheme://$authority"
|
|
440
|
+
} catch (_: Exception) {
|
|
441
|
+
null
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private fun isHttpUrl(url: String): Boolean {
|
|
446
|
+
val scheme = Uri.parse(url).scheme?.lowercase()
|
|
447
|
+
return scheme == "http" || scheme == "https"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private fun isInternalOfflineRuntimeUrl(url: String): Boolean {
|
|
451
|
+
val path = Uri.parse(url).path ?: return false
|
|
452
|
+
return path == "/catalyst-offline-manifest.json" ||
|
|
453
|
+
path == "/catalyst-sw.js" ||
|
|
454
|
+
path == "/offline.html"
|
|
455
|
+
}
|
|
456
|
+
|
|
314
457
|
/**
|
|
315
458
|
* Handle special URL schemes (tel:, mailto:, sms:)
|
|
316
459
|
*/
|
|
@@ -453,6 +596,18 @@ class CustomWebView(
|
|
|
453
596
|
return "build/public/$fileName"
|
|
454
597
|
}
|
|
455
598
|
|
|
599
|
+
private fun withCorsHeaders(response: WebResourceResponse): WebResourceResponse {
|
|
600
|
+
val responseHeaders = response.responseHeaders
|
|
601
|
+
if (responseHeaders.isNullOrEmpty()) {
|
|
602
|
+
response.responseHeaders = mutableMapOf(
|
|
603
|
+
"Access-Control-Allow-Origin" to "*",
|
|
604
|
+
"Access-Control-Allow-Methods" to "GET, OPTIONS",
|
|
605
|
+
"Access-Control-Allow-Headers" to "*"
|
|
606
|
+
)
|
|
607
|
+
}
|
|
608
|
+
return response
|
|
609
|
+
}
|
|
610
|
+
|
|
456
611
|
private fun getMimeType(path: String): String {
|
|
457
612
|
return when {
|
|
458
613
|
path.endsWith(".html") -> "text/html"
|
|
@@ -478,11 +633,13 @@ class CustomWebView(
|
|
|
478
633
|
serviceWorkerController.setServiceWorkerClient(object : ServiceWorkerClientCompat() {
|
|
479
634
|
override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
|
|
480
635
|
val url = request.url.toString()
|
|
636
|
+
val isOnline = NetworkUtils.getCurrentStatus(context).isOnline
|
|
481
637
|
|
|
482
638
|
if (BuildConfig.DEBUG) {
|
|
483
639
|
Log.d(TAG, "🔧 SERVICE_WORKER: Intercepting request: $url")
|
|
484
640
|
Log.d(TAG, "🔧 SERVICE_WORKER: Method: ${request.method}")
|
|
485
641
|
Log.d(TAG, "🔧 SERVICE_WORKER: Headers: ${request.requestHeaders}")
|
|
642
|
+
Log.d(FLOW_TAG, "SW intercept method=${request.method} online=$isOnline mainFrame=${request.isForMainFrame} url=$url")
|
|
486
643
|
}
|
|
487
644
|
|
|
488
645
|
// Check if URL is allowed
|
|
@@ -502,7 +659,7 @@ class CustomWebView(
|
|
|
502
659
|
}
|
|
503
660
|
|
|
504
661
|
// Check if this should be cached
|
|
505
|
-
if (request.method == "GET" &&
|
|
662
|
+
if (request.method == "GET" && shouldCacheRequest(request, url)) {
|
|
506
663
|
if (BuildConfig.DEBUG) {
|
|
507
664
|
Log.d(TAG, "🎯 SERVICE_WORKER: Using cache system for: $url")
|
|
508
665
|
}
|
|
@@ -516,49 +673,22 @@ class CustomWebView(
|
|
|
516
673
|
}
|
|
517
674
|
|
|
518
675
|
// Get cached response synchronously (non-blocking for ServiceWorker)
|
|
519
|
-
val response =
|
|
676
|
+
val response = if (NetworkUtils.getCurrentStatus(context).isOnline) {
|
|
677
|
+
cacheManager.getCachedResponseOrFetchSync(url, headers)
|
|
678
|
+
} else {
|
|
679
|
+
cacheManager.getCachedResponseSync(url, headers)
|
|
680
|
+
}
|
|
520
681
|
if (response != null) {
|
|
521
682
|
if (BuildConfig.DEBUG) {
|
|
522
683
|
Log.d(TAG, "✅ SERVICE_WORKER: Served from cache: $url")
|
|
684
|
+
Log.d(FLOW_TAG, "SW response source=cache-or-fetch mime=${response.mimeType ?: "unknown"} encoding=${response.encoding ?: "unknown"} url=$url")
|
|
523
685
|
}
|
|
524
|
-
response
|
|
686
|
+
withCorsHeaders(response)
|
|
525
687
|
} else {
|
|
526
688
|
if (BuildConfig.DEBUG) {
|
|
527
689
|
Log.d(TAG, "❌ SERVICE_WORKER: Cache miss: $url")
|
|
690
|
+
Log.d(FLOW_TAG, "SW response source=network-fallback reason=cache-null url=$url")
|
|
528
691
|
}
|
|
529
|
-
|
|
530
|
-
// Trigger async cache population for next request (with deduplication)
|
|
531
|
-
val shouldFetch = synchronized(ongoingCacheFetches) {
|
|
532
|
-
ongoingCacheFetches.add(url)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (shouldFetch) {
|
|
536
|
-
if (BuildConfig.DEBUG) {
|
|
537
|
-
Log.d(TAG, "🔄 SERVICE_WORKER: Triggering async cache fetch for: $url")
|
|
538
|
-
}
|
|
539
|
-
launch(Dispatchers.IO) {
|
|
540
|
-
try {
|
|
541
|
-
cacheManager.getCachedResponse(url, headers)
|
|
542
|
-
if (BuildConfig.DEBUG) {
|
|
543
|
-
Log.d(TAG, "✅ SERVICE_WORKER: Async cache fetch completed: $url")
|
|
544
|
-
}
|
|
545
|
-
} catch (e: Exception) {
|
|
546
|
-
if (BuildConfig.DEBUG) {
|
|
547
|
-
Log.e(TAG, "❌ SERVICE_WORKER: Async cache fetch failed: $url - ${e.message}")
|
|
548
|
-
}
|
|
549
|
-
} finally {
|
|
550
|
-
synchronized(ongoingCacheFetches) {
|
|
551
|
-
ongoingCacheFetches.remove(url)
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
} else {
|
|
556
|
-
if (BuildConfig.DEBUG) {
|
|
557
|
-
Log.d(TAG, "⏭️ SERVICE_WORKER: Cache fetch already in progress for: $url")
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Let this request fall back to network
|
|
562
692
|
null
|
|
563
693
|
}
|
|
564
694
|
} catch (e: Exception) {
|
|
@@ -570,7 +700,7 @@ class CustomWebView(
|
|
|
570
700
|
} else {
|
|
571
701
|
if (BuildConfig.DEBUG) {
|
|
572
702
|
Log.d(TAG, "⏭️ SERVICE_WORKER: Skipping cache for: $url")
|
|
573
|
-
Log.d(TAG, "⏭️ SERVICE_WORKER: Method: ${request.method}, shouldCache: ${
|
|
703
|
+
Log.d(TAG, "⏭️ SERVICE_WORKER: Method: ${request.method}, shouldCache: ${shouldCacheRequest(request, url)}")
|
|
574
704
|
}
|
|
575
705
|
}
|
|
576
706
|
|
|
@@ -606,6 +736,9 @@ class CustomWebView(
|
|
|
606
736
|
domStorageEnabled = true
|
|
607
737
|
allowFileAccess = true
|
|
608
738
|
allowContentAccess = true
|
|
739
|
+
setSupportZoom(false)
|
|
740
|
+
builtInZoomControls = false
|
|
741
|
+
displayZoomControls = false
|
|
609
742
|
|
|
610
743
|
// TODO: Enable these when build optimization feature is implemented
|
|
611
744
|
// These are deprecated but may be needed for local file access in development
|
|
@@ -660,6 +793,7 @@ class CustomWebView(
|
|
|
660
793
|
request: WebResourceRequest
|
|
661
794
|
): WebResourceResponse? {
|
|
662
795
|
val url = request.url.toString()
|
|
796
|
+
val isOnline = NetworkUtils.getCurrentStatus(context).isOnline
|
|
663
797
|
|
|
664
798
|
// Track all network requests
|
|
665
799
|
metricsMonitor.recordNetworkRequest(url)
|
|
@@ -670,6 +804,7 @@ class CustomWebView(
|
|
|
670
804
|
Log.d(TAG, "🔄 INTERCEPT: Thread: ${Thread.currentThread().name}")
|
|
671
805
|
Log.d(TAG, "🔄 INTERCEPT: File extension: ${url.substringAfterLast('.', "none")}")
|
|
672
806
|
Log.d(TAG, "🔄 INTERCEPT: isInitialPageLoaded: $isInitialPageLoaded")
|
|
807
|
+
Log.d(FLOW_TAG, "WV intercept method=${request.method} online=$isOnline mainFrame=${request.isForMainFrame} initialLoaded=$isInitialPageLoaded dest=${requestHeader(request, "Sec-Fetch-Dest") ?: "none"} accept=${requestHeader(request, "Accept") ?: "none"} url=$url")
|
|
673
808
|
}
|
|
674
809
|
|
|
675
810
|
if (!URLWhitelistManager.isUrlAllowed(url)) {
|
|
@@ -686,6 +821,51 @@ class CustomWebView(
|
|
|
686
821
|
}
|
|
687
822
|
}
|
|
688
823
|
|
|
824
|
+
val isMainFrameDocument = request.isForMainFrame &&
|
|
825
|
+
request.method == "GET" &&
|
|
826
|
+
(request.url.scheme == "http" || request.url.scheme == "https") &&
|
|
827
|
+
!isApiCall(url)
|
|
828
|
+
|
|
829
|
+
if (isMainFrameDocument) {
|
|
830
|
+
val headers = defaultRequestHeaders.toMutableMap().apply {
|
|
831
|
+
putAll(request.requestHeaders)
|
|
832
|
+
}
|
|
833
|
+
if (BuildConfig.DEBUG) {
|
|
834
|
+
Log.d(FLOW_TAG, "DOCUMENT intercept online=$isOnline url=$url headers=${headers.keys.sorted()}")
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (isOnline) {
|
|
838
|
+
offlineCacheService.refreshManifestIfMissing(url, headers)
|
|
839
|
+
val isOfflineRoute = offlineCacheService.isOfflineRouteUrl(url)
|
|
840
|
+
updateActiveOfflineRoute(url, isOfflineRoute)
|
|
841
|
+
visibleOfflineSnapshotUrl = null
|
|
842
|
+
if (BuildConfig.DEBUG) {
|
|
843
|
+
Log.d(FLOW_TAG, "DOCUMENT online routeEligible=$isOfflineRoute action=allow-network-and-background-snapshot url=$url")
|
|
844
|
+
}
|
|
845
|
+
offlineCacheService.storeRouteSnapshotAsync(url, headers)
|
|
846
|
+
} else {
|
|
847
|
+
val snapshotResponse = offlineCacheService.getRouteSnapshotResponse(url)
|
|
848
|
+
if (snapshotResponse != null) {
|
|
849
|
+
updateActiveOfflineRoute(url, true)
|
|
850
|
+
visibleOfflineSnapshotUrl = normalizeUrlWithoutFragment(url)
|
|
851
|
+
if (BuildConfig.DEBUG) {
|
|
852
|
+
Log.d(TAG, "📴 INTERCEPT: Serving offline route snapshot: $url")
|
|
853
|
+
Log.d(FLOW_TAG, "DOCUMENT offline response=route-snapshot mime=${snapshotResponse.mimeType ?: "unknown"} url=$url")
|
|
854
|
+
}
|
|
855
|
+
return withCorsHeaders(snapshotResponse)
|
|
856
|
+
}
|
|
857
|
+
updateActiveOfflineRoute(url, false)
|
|
858
|
+
if (BuildConfig.DEBUG) {
|
|
859
|
+
Log.d(FLOW_TAG, "DOCUMENT offline response=network-fallback reason=snapshot-miss url=$url")
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
} else if (request.isForMainFrame &&
|
|
863
|
+
request.method == "GET" &&
|
|
864
|
+
(request.url.scheme == "http" || request.url.scheme == "https")
|
|
865
|
+
) {
|
|
866
|
+
updateActiveOfflineRoute(url, false)
|
|
867
|
+
}
|
|
868
|
+
|
|
689
869
|
// TODO: Add HTML file workflow - index.html not available yet
|
|
690
870
|
// Handle the initial route request - intercept first request regardless of host
|
|
691
871
|
// if (!isInitialApiCalled && request.method == "GET") {
|
|
@@ -720,12 +900,12 @@ class CustomWebView(
|
|
|
720
900
|
if (BuildConfig.DEBUG) {
|
|
721
901
|
Log.d(TAG, "🔍 STEP_4_CHECK: Initial page not loaded yet, checking for static resources")
|
|
722
902
|
Log.d(TAG, "🔍 STEP_4_CHECK: isStaticResourceRequest($url) = ${isStaticResourceRequest(url)}")
|
|
723
|
-
Log.d(TAG, "🔍 STEP_4_CHECK:
|
|
903
|
+
Log.d(TAG, "🔍 STEP_4_CHECK: shouldCacheRequest($url) = ${shouldCacheRequest(request, url)}")
|
|
724
904
|
}
|
|
725
905
|
|
|
726
906
|
// IMPORTANT: Skip asset loading if the URL matches a cache pattern
|
|
727
907
|
// This allows cached resources to be served from the cache system instead
|
|
728
|
-
if (request.method == "GET" && isStaticResourceRequest(url) && !
|
|
908
|
+
if (request.method == "GET" && isStaticResourceRequest(url) && !shouldCacheRequest(request, url)) {
|
|
729
909
|
val assetPath = extractAssetPath(url)
|
|
730
910
|
assetLoadAttempts++
|
|
731
911
|
|
|
@@ -763,7 +943,7 @@ class CustomWebView(
|
|
|
763
943
|
}
|
|
764
944
|
// Unexpected error - fall through to cache check below
|
|
765
945
|
}
|
|
766
|
-
} else if (
|
|
946
|
+
} else if (shouldCacheRequest(request, url)) {
|
|
767
947
|
if (BuildConfig.DEBUG) {
|
|
768
948
|
Log.d(TAG, "⏭️ STEP_4_SKIP: Skipping asset loading for cached URL: $url")
|
|
769
949
|
}
|
|
@@ -778,10 +958,10 @@ class CustomWebView(
|
|
|
778
958
|
if (BuildConfig.DEBUG) {
|
|
779
959
|
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Evaluating cache conditions")
|
|
780
960
|
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Request method: ${request.method} (need GET)")
|
|
781
|
-
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK:
|
|
961
|
+
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: shouldCacheRequest($url) = ${shouldCacheRequest(request, url)}")
|
|
782
962
|
}
|
|
783
963
|
|
|
784
|
-
if (request.method == "GET" &&
|
|
964
|
+
if (request.method == "GET" && shouldCacheRequest(request, url)) {
|
|
785
965
|
// Track that this request was evaluated for caching
|
|
786
966
|
metricsMonitor.recordCacheEvaluation(url)
|
|
787
967
|
if (BuildConfig.DEBUG) {
|
|
@@ -804,45 +984,27 @@ class CustomWebView(
|
|
|
804
984
|
}
|
|
805
985
|
|
|
806
986
|
// Use synchronous cache check to avoid blocking
|
|
807
|
-
val response =
|
|
987
|
+
val response = if (NetworkUtils.getCurrentStatus(context).isOnline) {
|
|
988
|
+
cacheManager.getCachedResponseOrFetchSync(url, headers)
|
|
989
|
+
} else {
|
|
990
|
+
cacheManager.getCachedResponseSync(url, headers)
|
|
991
|
+
}
|
|
808
992
|
|
|
809
993
|
if (response != null) {
|
|
810
994
|
if (BuildConfig.DEBUG) {
|
|
811
995
|
val duration = System.currentTimeMillis() - startTime
|
|
812
996
|
Log.d(TAG, "✅ WEBVIEW_CLIENT: Served from cache in ${duration}ms: $url")
|
|
997
|
+
Log.d(FLOW_TAG, "ASSET response source=cache-or-fetch online=${NetworkUtils.getCurrentStatus(context).isOnline} durationMs=$duration mime=${response.mimeType ?: "unknown"} encoding=${response.encoding ?: "unknown"} url=$url")
|
|
813
998
|
}
|
|
814
999
|
metricsMonitor.recordCacheHit(url)
|
|
815
|
-
return response
|
|
1000
|
+
return withCorsHeaders(response)
|
|
816
1001
|
} else {
|
|
817
1002
|
if (BuildConfig.DEBUG) {
|
|
818
|
-
Log.d(TAG, "❌ WEBVIEW_CLIENT: Cache miss
|
|
1003
|
+
Log.d(TAG, "❌ WEBVIEW_CLIENT: Cache miss: $url")
|
|
1004
|
+
Log.d(FLOW_TAG, "ASSET response source=network-fallback reason=cache-null online=${NetworkUtils.getCurrentStatus(context).isOnline} url=$url")
|
|
819
1005
|
}
|
|
820
1006
|
metricsMonitor.recordCacheMiss(url)
|
|
821
1007
|
|
|
822
|
-
// Trigger async cache population for next time
|
|
823
|
-
val shouldFetch = synchronized(ongoingCacheFetches) {
|
|
824
|
-
ongoingCacheFetches.add(url)
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
if (shouldFetch) {
|
|
828
|
-
launch(Dispatchers.IO) {
|
|
829
|
-
try {
|
|
830
|
-
cacheManager.getCachedResponse(url, headers)
|
|
831
|
-
if (BuildConfig.DEBUG) {
|
|
832
|
-
Log.d(TAG, "✅ WEBVIEW_CLIENT: Async cache fetch completed: $url")
|
|
833
|
-
}
|
|
834
|
-
} catch (e: Exception) {
|
|
835
|
-
if (BuildConfig.DEBUG) {
|
|
836
|
-
Log.e(TAG, "❌ WEBVIEW_CLIENT: Async cache fetch failed: $url - ${e.message}")
|
|
837
|
-
}
|
|
838
|
-
} finally {
|
|
839
|
-
synchronized(ongoingCacheFetches) {
|
|
840
|
-
ongoingCacheFetches.remove(url)
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
|
|
846
1008
|
// Return null to let network handle this request
|
|
847
1009
|
return null
|
|
848
1010
|
}
|
|
@@ -870,7 +1032,7 @@ class CustomWebView(
|
|
|
870
1032
|
Log.d(TAG, "❌ STEP_5_FAIL: URL doesn't match cache criteria, skipping cache")
|
|
871
1033
|
Log.d(TAG, "❌ STEP_5_FAIL: URL: $url")
|
|
872
1034
|
Log.d(TAG, "❌ STEP_5_FAIL: Request method: ${request.method} (need GET)")
|
|
873
|
-
Log.d(TAG, "❌ STEP_5_FAIL:
|
|
1035
|
+
Log.d(TAG, "❌ STEP_5_FAIL: shouldCacheRequest result: ${shouldCacheRequest(request, url)}")
|
|
874
1036
|
Log.d(TAG, "❌ STEP_5_FAIL: This request will go through normal network loading")
|
|
875
1037
|
}
|
|
876
1038
|
}
|
|
@@ -882,6 +1044,10 @@ class CustomWebView(
|
|
|
882
1044
|
super.onPageStarted(view, url, favicon)
|
|
883
1045
|
if (url != null && url != offlineAssetUrl) {
|
|
884
1046
|
offlinePageVisible = false
|
|
1047
|
+
if (!isVisibleOfflineSnapshot(url)) {
|
|
1048
|
+
visibleOfflineSnapshotUrl = null
|
|
1049
|
+
}
|
|
1050
|
+
onPageStarted?.invoke()
|
|
885
1051
|
}
|
|
886
1052
|
progressBar.visibility = View.VISIBLE
|
|
887
1053
|
val startTime = System.currentTimeMillis()
|
|
@@ -889,6 +1055,7 @@ class CustomWebView(
|
|
|
889
1055
|
url?.let { metricsMonitor.trackPageLoadStart(it) }
|
|
890
1056
|
if (BuildConfig.DEBUG) {
|
|
891
1057
|
Log.d(TAG, "⏳ Page load started for: $url - Hardware Acceleration: $isHardwareAccelerationEnabled")
|
|
1058
|
+
Log.d(FLOW_TAG, "PAGE started url=$url online=${NetworkUtils.getCurrentStatus(context).isOnline} offlinePageVisible=$offlinePageVisible visibleSnapshot=${visibleOfflineSnapshotUrl ?: "none"} activeOrigin=${activeOfflineRouteOrigin ?: "none"}")
|
|
892
1059
|
}
|
|
893
1060
|
}
|
|
894
1061
|
|
|
@@ -902,6 +1069,7 @@ class CustomWebView(
|
|
|
902
1069
|
val startTime = view?.tag as? Long ?: return
|
|
903
1070
|
val loadTime = System.currentTimeMillis() - startTime
|
|
904
1071
|
Log.d(TAG, "✅ Page load finished for: $url - Load time: ${loadTime}ms - Hardware Acceleration: $isHardwareAccelerationEnabled")
|
|
1072
|
+
Log.d(FLOW_TAG, "PAGE finished url=$url loadTimeMs=$loadTime online=${NetworkUtils.getCurrentStatus(context).isOnline} offlinePageVisible=$offlinePageVisible visibleSnapshot=${visibleOfflineSnapshotUrl ?: "none"} activeOrigin=${activeOfflineRouteOrigin ?: "none"}")
|
|
905
1073
|
Log.d(TAG, "📊 Asset loading stats: Attempted: $assetLoadAttempts, Failed: $assetLoadFailures (${String.format("%.1f", assetLoadFailures * 100.0 / assetLoadAttempts.coerceAtLeast(1))}%)")
|
|
906
1074
|
Log.d(TAG, "📊 ${metricsMonitor.getCacheStats()}")
|
|
907
1075
|
|
|
@@ -924,7 +1092,30 @@ class CustomWebView(
|
|
|
924
1092
|
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
|
925
1093
|
if (error != null) {
|
|
926
1094
|
Log.e(TAG, "❌ Error loading ${request?.url}: ${error.errorCode} ${error.description}")
|
|
1095
|
+
if (BuildConfig.DEBUG) {
|
|
1096
|
+
Log.e(FLOW_TAG, "PAGE resource-error mainFrame=${request?.isForMainFrame} method=${request?.method} code=${error.errorCode} description=${error.description} url=${request?.url}")
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
val failedUrl = request?.url?.toString()
|
|
1101
|
+
if (request?.isForMainFrame == true && isVisibleOfflineSnapshot(failedUrl)) {
|
|
1102
|
+
if (BuildConfig.DEBUG) {
|
|
1103
|
+
Log.d(TAG, "📴 Ignoring repeated error for visible offline route snapshot: $failedUrl")
|
|
1104
|
+
}
|
|
1105
|
+
return
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
if (request?.isForMainFrame == true &&
|
|
1109
|
+
failedUrl != null &&
|
|
1110
|
+
failedUrl != offlineAssetUrl &&
|
|
1111
|
+
!NetworkUtils.getCurrentStatus(context).isOnline
|
|
1112
|
+
) {
|
|
1113
|
+
view?.post {
|
|
1114
|
+
showOfflineRouteOrOfflinePage(failedUrl)
|
|
1115
|
+
}
|
|
1116
|
+
return
|
|
927
1117
|
}
|
|
1118
|
+
|
|
928
1119
|
super.onReceivedError(view, request, error)
|
|
929
1120
|
}
|
|
930
1121
|
|
|
@@ -976,6 +1167,7 @@ class CustomWebView(
|
|
|
976
1167
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
|
977
1168
|
if (BuildConfig.DEBUG) {
|
|
978
1169
|
Log.d(TAG, "Console: ${consoleMessage?.message()} -- From line ${consoleMessage?.lineNumber()} of ${consoleMessage?.sourceId()}")
|
|
1170
|
+
Log.d(FLOW_TAG, "CONSOLE level=${consoleMessage?.messageLevel()} line=${consoleMessage?.lineNumber()} source=${consoleMessage?.sourceId()} message=${consoleMessage?.message()}")
|
|
979
1171
|
}
|
|
980
1172
|
return true
|
|
981
1173
|
}
|