catalyst-core 0.2.0-beta.1 → 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/dist/native/androidProject/app/build.gradle.kts +25 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +239 -75
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +3 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +1 -1
- 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/WebCacheManager.kt +136 -26
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
- package/dist/native/buildAndroid/assets.js +2 -2
- 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/WebView.swift +15 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +82 -2
- package/dist/native/renameAndroidProject.js +2 -2
- package/dist/offline/catalyst-sw.js +1 -0
- package/dist/offline/registerServiceWorker.js +1 -0
- package/dist/router/ClientRouter.js +1 -1
- package/dist/scripts/build.js +1 -1
- package/dist/scripts/devBuild.js +1 -1
- package/dist/scripts/generateOfflineManifest.js +1 -0
- package/dist/server/expressServer.js +2 -1
- package/dist/server/renderer/document/Body.js +1 -1
- package/dist/server/renderer/handler.js +3 -3
- package/dist/webpack/base.babel.js +1 -1
- package/mcp_v2/knowledge-base.json +13 -0
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ buildscript {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
val configPath
|
|
13
|
+
val configPath = findProperty("configPath") as? String
|
|
14
14
|
val keystorePassword: String? by project.properties // Changed from keyStorePassword to keystorePassword
|
|
15
15
|
val keyAlias: String? by project.properties
|
|
16
16
|
val keyPassword: String? by project.properties
|
|
@@ -29,15 +29,32 @@ fun isAllowBackupEnabled(): Boolean {
|
|
|
29
29
|
|
|
30
30
|
fun isNotificationsEnabled(): Boolean {
|
|
31
31
|
return try {
|
|
32
|
-
if (configPath
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
if (configPath != null) {
|
|
33
|
+
val configFile = File(configPath)
|
|
34
|
+
if (configFile.exists()) {
|
|
35
|
+
val json = JSONObject(configFile.readText())
|
|
36
|
+
if (json.has("WEBVIEW_CONFIG")) {
|
|
37
|
+
val webviewConfig = json.getJSONObject("WEBVIEW_CONFIG")
|
|
38
|
+
return webviewConfig.optJSONObject("notifications")?.optBoolean("enabled", false) ?: false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
35
42
|
|
|
36
|
-
val
|
|
37
|
-
if (
|
|
43
|
+
val generatedBuildProps = File("${project.projectDir}/catalyst-build.properties")
|
|
44
|
+
if (generatedBuildProps.exists()) {
|
|
45
|
+
val props = Properties()
|
|
46
|
+
props.load(generatedBuildProps.inputStream())
|
|
47
|
+
return props.getProperty("notifications.enabled", "false").trim().lowercase() == "true"
|
|
48
|
+
}
|
|
38
49
|
|
|
39
|
-
val
|
|
40
|
-
|
|
50
|
+
val webviewProps = File("${project.projectDir}/src/main/assets/webview_config.properties")
|
|
51
|
+
if (webviewProps.exists()) {
|
|
52
|
+
val props = Properties()
|
|
53
|
+
props.load(webviewProps.inputStream())
|
|
54
|
+
return props.getProperty("notifications.enabled", "false").trim().lowercase() == "true"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
false
|
|
41
58
|
} catch (e: Exception) {
|
|
42
59
|
false
|
|
43
60
|
}
|
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,6 +52,8 @@ 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()
|
|
54
58
|
var onPageStarted: (() -> Unit)? = null
|
|
55
59
|
|
|
@@ -57,12 +61,10 @@ class CustomWebView(
|
|
|
57
61
|
private var assetLoadAttempts = 0
|
|
58
62
|
private var assetLoadFailures = 0
|
|
59
63
|
|
|
60
|
-
// Track ongoing cache fetches to prevent duplicates
|
|
61
|
-
private val ongoingCacheFetches = mutableSetOf<String>()
|
|
62
|
-
|
|
63
64
|
init {
|
|
64
65
|
setupFromProperties()
|
|
65
66
|
cacheManager = WebCacheManager(context, properties)
|
|
67
|
+
offlineCacheService = OfflineCacheService(context)
|
|
66
68
|
setupWebView()
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -97,7 +99,7 @@ class CustomWebView(
|
|
|
97
99
|
|
|
98
100
|
// Access control toggle
|
|
99
101
|
accessControlEnabled = properties
|
|
100
|
-
.getProperty("accessControl.enabled", "
|
|
102
|
+
.getProperty("accessControl.enabled", "false")
|
|
101
103
|
.equals("true", ignoreCase = true)
|
|
102
104
|
|
|
103
105
|
// Initialize URLWhitelistManager with access control configuration
|
|
@@ -137,6 +139,15 @@ class CustomWebView(
|
|
|
137
139
|
|
|
138
140
|
private fun loadUrlInternal(url: String) {
|
|
139
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
|
+
}
|
|
140
151
|
|
|
141
152
|
if (isNetworkUrl && defaultRequestHeaders.isNotEmpty()) {
|
|
142
153
|
if (BuildConfig.DEBUG) {
|
|
@@ -146,11 +157,16 @@ class CustomWebView(
|
|
|
146
157
|
} else {
|
|
147
158
|
webView.loadUrl(url)
|
|
148
159
|
}
|
|
160
|
+
if (BuildConfig.DEBUG) {
|
|
161
|
+
Log.d(FLOW_TAG, "WEBVIEW load-url dispatched url=$url")
|
|
162
|
+
}
|
|
149
163
|
}
|
|
150
164
|
|
|
151
165
|
fun loadUrl(url: String) {
|
|
152
166
|
lastTargetUrl = url
|
|
153
167
|
offlinePageVisible = false
|
|
168
|
+
visibleOfflineSnapshotUrl = null
|
|
169
|
+
activeOfflineRouteOrigin = null
|
|
154
170
|
loadUrlInternal(url)
|
|
155
171
|
}
|
|
156
172
|
|
|
@@ -167,6 +183,8 @@ class CustomWebView(
|
|
|
167
183
|
try {
|
|
168
184
|
context.assets.open(offlineAssetPath).close()
|
|
169
185
|
offlinePageVisible = true
|
|
186
|
+
visibleOfflineSnapshotUrl = null
|
|
187
|
+
activeOfflineRouteOrigin = null
|
|
170
188
|
loadUrlInternal(offlineAssetUrl)
|
|
171
189
|
if (BuildConfig.DEBUG) {
|
|
172
190
|
Log.d(TAG, "📴 Showing offline page from assets: $offlineAssetPath")
|
|
@@ -192,6 +210,32 @@ class CustomWebView(
|
|
|
192
210
|
|
|
193
211
|
fun clearAllCache() {
|
|
194
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()
|
|
195
239
|
}
|
|
196
240
|
|
|
197
241
|
fun canGoBack(): Boolean = webView.canGoBack()
|
|
@@ -304,25 +348,112 @@ class CustomWebView(
|
|
|
304
348
|
}
|
|
305
349
|
}
|
|
306
350
|
|
|
307
|
-
private fun
|
|
351
|
+
private fun shouldCacheRequest(request: WebResourceRequest, url: String): Boolean {
|
|
308
352
|
if (BuildConfig.DEBUG) {
|
|
309
353
|
Log.d(TAG, "🔍 CACHE_CHECK: Checking if URL should be cached: $url")
|
|
310
354
|
Log.d(TAG, "🔍 CACHE_CHECK: Cache patterns: $cachePatterns")
|
|
311
355
|
}
|
|
312
356
|
|
|
313
|
-
val
|
|
357
|
+
val matchesPattern = matchesCachePattern(url, cachePatterns)
|
|
358
|
+
val offlineRouteSubresource = shouldCacheOfflineRouteSubresource(request, url)
|
|
359
|
+
val shouldCache = matchesPattern || offlineRouteSubresource
|
|
314
360
|
|
|
315
361
|
if (BuildConfig.DEBUG) {
|
|
316
362
|
if (shouldCache) {
|
|
317
|
-
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")
|
|
318
365
|
} else {
|
|
319
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")
|
|
320
368
|
}
|
|
321
369
|
}
|
|
322
370
|
|
|
323
371
|
return shouldCache
|
|
324
372
|
}
|
|
325
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
|
+
|
|
326
457
|
/**
|
|
327
458
|
* Handle special URL schemes (tel:, mailto:, sms:)
|
|
328
459
|
*/
|
|
@@ -502,11 +633,13 @@ class CustomWebView(
|
|
|
502
633
|
serviceWorkerController.setServiceWorkerClient(object : ServiceWorkerClientCompat() {
|
|
503
634
|
override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
|
|
504
635
|
val url = request.url.toString()
|
|
636
|
+
val isOnline = NetworkUtils.getCurrentStatus(context).isOnline
|
|
505
637
|
|
|
506
638
|
if (BuildConfig.DEBUG) {
|
|
507
639
|
Log.d(TAG, "🔧 SERVICE_WORKER: Intercepting request: $url")
|
|
508
640
|
Log.d(TAG, "🔧 SERVICE_WORKER: Method: ${request.method}")
|
|
509
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")
|
|
510
643
|
}
|
|
511
644
|
|
|
512
645
|
// Check if URL is allowed
|
|
@@ -526,7 +659,7 @@ class CustomWebView(
|
|
|
526
659
|
}
|
|
527
660
|
|
|
528
661
|
// Check if this should be cached
|
|
529
|
-
if (request.method == "GET" &&
|
|
662
|
+
if (request.method == "GET" && shouldCacheRequest(request, url)) {
|
|
530
663
|
if (BuildConfig.DEBUG) {
|
|
531
664
|
Log.d(TAG, "🎯 SERVICE_WORKER: Using cache system for: $url")
|
|
532
665
|
}
|
|
@@ -540,49 +673,22 @@ class CustomWebView(
|
|
|
540
673
|
}
|
|
541
674
|
|
|
542
675
|
// Get cached response synchronously (non-blocking for ServiceWorker)
|
|
543
|
-
val response =
|
|
676
|
+
val response = if (NetworkUtils.getCurrentStatus(context).isOnline) {
|
|
677
|
+
cacheManager.getCachedResponseOrFetchSync(url, headers)
|
|
678
|
+
} else {
|
|
679
|
+
cacheManager.getCachedResponseSync(url, headers)
|
|
680
|
+
}
|
|
544
681
|
if (response != null) {
|
|
545
682
|
if (BuildConfig.DEBUG) {
|
|
546
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")
|
|
547
685
|
}
|
|
548
686
|
withCorsHeaders(response)
|
|
549
687
|
} else {
|
|
550
688
|
if (BuildConfig.DEBUG) {
|
|
551
689
|
Log.d(TAG, "❌ SERVICE_WORKER: Cache miss: $url")
|
|
690
|
+
Log.d(FLOW_TAG, "SW response source=network-fallback reason=cache-null url=$url")
|
|
552
691
|
}
|
|
553
|
-
|
|
554
|
-
// Trigger async cache population for next request (with deduplication)
|
|
555
|
-
val shouldFetch = synchronized(ongoingCacheFetches) {
|
|
556
|
-
ongoingCacheFetches.add(url)
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
if (shouldFetch) {
|
|
560
|
-
if (BuildConfig.DEBUG) {
|
|
561
|
-
Log.d(TAG, "🔄 SERVICE_WORKER: Triggering async cache fetch for: $url")
|
|
562
|
-
}
|
|
563
|
-
launch(Dispatchers.IO) {
|
|
564
|
-
try {
|
|
565
|
-
cacheManager.getCachedResponse(url, headers)
|
|
566
|
-
if (BuildConfig.DEBUG) {
|
|
567
|
-
Log.d(TAG, "✅ SERVICE_WORKER: Async cache fetch completed: $url")
|
|
568
|
-
}
|
|
569
|
-
} catch (e: Exception) {
|
|
570
|
-
if (BuildConfig.DEBUG) {
|
|
571
|
-
Log.e(TAG, "❌ SERVICE_WORKER: Async cache fetch failed: $url - ${e.message}")
|
|
572
|
-
}
|
|
573
|
-
} finally {
|
|
574
|
-
synchronized(ongoingCacheFetches) {
|
|
575
|
-
ongoingCacheFetches.remove(url)
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
} else {
|
|
580
|
-
if (BuildConfig.DEBUG) {
|
|
581
|
-
Log.d(TAG, "⏭️ SERVICE_WORKER: Cache fetch already in progress for: $url")
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Let this request fall back to network
|
|
586
692
|
null
|
|
587
693
|
}
|
|
588
694
|
} catch (e: Exception) {
|
|
@@ -594,7 +700,7 @@ class CustomWebView(
|
|
|
594
700
|
} else {
|
|
595
701
|
if (BuildConfig.DEBUG) {
|
|
596
702
|
Log.d(TAG, "⏭️ SERVICE_WORKER: Skipping cache for: $url")
|
|
597
|
-
Log.d(TAG, "⏭️ SERVICE_WORKER: Method: ${request.method}, shouldCache: ${
|
|
703
|
+
Log.d(TAG, "⏭️ SERVICE_WORKER: Method: ${request.method}, shouldCache: ${shouldCacheRequest(request, url)}")
|
|
598
704
|
}
|
|
599
705
|
}
|
|
600
706
|
|
|
@@ -687,6 +793,7 @@ class CustomWebView(
|
|
|
687
793
|
request: WebResourceRequest
|
|
688
794
|
): WebResourceResponse? {
|
|
689
795
|
val url = request.url.toString()
|
|
796
|
+
val isOnline = NetworkUtils.getCurrentStatus(context).isOnline
|
|
690
797
|
|
|
691
798
|
// Track all network requests
|
|
692
799
|
metricsMonitor.recordNetworkRequest(url)
|
|
@@ -697,6 +804,7 @@ class CustomWebView(
|
|
|
697
804
|
Log.d(TAG, "🔄 INTERCEPT: Thread: ${Thread.currentThread().name}")
|
|
698
805
|
Log.d(TAG, "🔄 INTERCEPT: File extension: ${url.substringAfterLast('.', "none")}")
|
|
699
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")
|
|
700
808
|
}
|
|
701
809
|
|
|
702
810
|
if (!URLWhitelistManager.isUrlAllowed(url)) {
|
|
@@ -713,6 +821,51 @@ class CustomWebView(
|
|
|
713
821
|
}
|
|
714
822
|
}
|
|
715
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
|
+
|
|
716
869
|
// TODO: Add HTML file workflow - index.html not available yet
|
|
717
870
|
// Handle the initial route request - intercept first request regardless of host
|
|
718
871
|
// if (!isInitialApiCalled && request.method == "GET") {
|
|
@@ -747,12 +900,12 @@ class CustomWebView(
|
|
|
747
900
|
if (BuildConfig.DEBUG) {
|
|
748
901
|
Log.d(TAG, "🔍 STEP_4_CHECK: Initial page not loaded yet, checking for static resources")
|
|
749
902
|
Log.d(TAG, "🔍 STEP_4_CHECK: isStaticResourceRequest($url) = ${isStaticResourceRequest(url)}")
|
|
750
|
-
Log.d(TAG, "🔍 STEP_4_CHECK:
|
|
903
|
+
Log.d(TAG, "🔍 STEP_4_CHECK: shouldCacheRequest($url) = ${shouldCacheRequest(request, url)}")
|
|
751
904
|
}
|
|
752
905
|
|
|
753
906
|
// IMPORTANT: Skip asset loading if the URL matches a cache pattern
|
|
754
907
|
// This allows cached resources to be served from the cache system instead
|
|
755
|
-
if (request.method == "GET" && isStaticResourceRequest(url) && !
|
|
908
|
+
if (request.method == "GET" && isStaticResourceRequest(url) && !shouldCacheRequest(request, url)) {
|
|
756
909
|
val assetPath = extractAssetPath(url)
|
|
757
910
|
assetLoadAttempts++
|
|
758
911
|
|
|
@@ -790,7 +943,7 @@ class CustomWebView(
|
|
|
790
943
|
}
|
|
791
944
|
// Unexpected error - fall through to cache check below
|
|
792
945
|
}
|
|
793
|
-
} else if (
|
|
946
|
+
} else if (shouldCacheRequest(request, url)) {
|
|
794
947
|
if (BuildConfig.DEBUG) {
|
|
795
948
|
Log.d(TAG, "⏭️ STEP_4_SKIP: Skipping asset loading for cached URL: $url")
|
|
796
949
|
}
|
|
@@ -805,10 +958,10 @@ class CustomWebView(
|
|
|
805
958
|
if (BuildConfig.DEBUG) {
|
|
806
959
|
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Evaluating cache conditions")
|
|
807
960
|
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Request method: ${request.method} (need GET)")
|
|
808
|
-
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK:
|
|
961
|
+
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: shouldCacheRequest($url) = ${shouldCacheRequest(request, url)}")
|
|
809
962
|
}
|
|
810
963
|
|
|
811
|
-
if (request.method == "GET" &&
|
|
964
|
+
if (request.method == "GET" && shouldCacheRequest(request, url)) {
|
|
812
965
|
// Track that this request was evaluated for caching
|
|
813
966
|
metricsMonitor.recordCacheEvaluation(url)
|
|
814
967
|
if (BuildConfig.DEBUG) {
|
|
@@ -831,45 +984,27 @@ class CustomWebView(
|
|
|
831
984
|
}
|
|
832
985
|
|
|
833
986
|
// Use synchronous cache check to avoid blocking
|
|
834
|
-
val response =
|
|
987
|
+
val response = if (NetworkUtils.getCurrentStatus(context).isOnline) {
|
|
988
|
+
cacheManager.getCachedResponseOrFetchSync(url, headers)
|
|
989
|
+
} else {
|
|
990
|
+
cacheManager.getCachedResponseSync(url, headers)
|
|
991
|
+
}
|
|
835
992
|
|
|
836
993
|
if (response != null) {
|
|
837
994
|
if (BuildConfig.DEBUG) {
|
|
838
995
|
val duration = System.currentTimeMillis() - startTime
|
|
839
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")
|
|
840
998
|
}
|
|
841
999
|
metricsMonitor.recordCacheHit(url)
|
|
842
1000
|
return withCorsHeaders(response)
|
|
843
1001
|
} else {
|
|
844
1002
|
if (BuildConfig.DEBUG) {
|
|
845
|
-
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")
|
|
846
1005
|
}
|
|
847
1006
|
metricsMonitor.recordCacheMiss(url)
|
|
848
1007
|
|
|
849
|
-
// Trigger async cache population for next time
|
|
850
|
-
val shouldFetch = synchronized(ongoingCacheFetches) {
|
|
851
|
-
ongoingCacheFetches.add(url)
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
if (shouldFetch) {
|
|
855
|
-
launch(Dispatchers.IO) {
|
|
856
|
-
try {
|
|
857
|
-
cacheManager.getCachedResponse(url, headers)
|
|
858
|
-
if (BuildConfig.DEBUG) {
|
|
859
|
-
Log.d(TAG, "✅ WEBVIEW_CLIENT: Async cache fetch completed: $url")
|
|
860
|
-
}
|
|
861
|
-
} catch (e: Exception) {
|
|
862
|
-
if (BuildConfig.DEBUG) {
|
|
863
|
-
Log.e(TAG, "❌ WEBVIEW_CLIENT: Async cache fetch failed: $url - ${e.message}")
|
|
864
|
-
}
|
|
865
|
-
} finally {
|
|
866
|
-
synchronized(ongoingCacheFetches) {
|
|
867
|
-
ongoingCacheFetches.remove(url)
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
1008
|
// Return null to let network handle this request
|
|
874
1009
|
return null
|
|
875
1010
|
}
|
|
@@ -897,7 +1032,7 @@ class CustomWebView(
|
|
|
897
1032
|
Log.d(TAG, "❌ STEP_5_FAIL: URL doesn't match cache criteria, skipping cache")
|
|
898
1033
|
Log.d(TAG, "❌ STEP_5_FAIL: URL: $url")
|
|
899
1034
|
Log.d(TAG, "❌ STEP_5_FAIL: Request method: ${request.method} (need GET)")
|
|
900
|
-
Log.d(TAG, "❌ STEP_5_FAIL:
|
|
1035
|
+
Log.d(TAG, "❌ STEP_5_FAIL: shouldCacheRequest result: ${shouldCacheRequest(request, url)}")
|
|
901
1036
|
Log.d(TAG, "❌ STEP_5_FAIL: This request will go through normal network loading")
|
|
902
1037
|
}
|
|
903
1038
|
}
|
|
@@ -909,6 +1044,9 @@ class CustomWebView(
|
|
|
909
1044
|
super.onPageStarted(view, url, favicon)
|
|
910
1045
|
if (url != null && url != offlineAssetUrl) {
|
|
911
1046
|
offlinePageVisible = false
|
|
1047
|
+
if (!isVisibleOfflineSnapshot(url)) {
|
|
1048
|
+
visibleOfflineSnapshotUrl = null
|
|
1049
|
+
}
|
|
912
1050
|
onPageStarted?.invoke()
|
|
913
1051
|
}
|
|
914
1052
|
progressBar.visibility = View.VISIBLE
|
|
@@ -917,6 +1055,7 @@ class CustomWebView(
|
|
|
917
1055
|
url?.let { metricsMonitor.trackPageLoadStart(it) }
|
|
918
1056
|
if (BuildConfig.DEBUG) {
|
|
919
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"}")
|
|
920
1059
|
}
|
|
921
1060
|
}
|
|
922
1061
|
|
|
@@ -930,6 +1069,7 @@ class CustomWebView(
|
|
|
930
1069
|
val startTime = view?.tag as? Long ?: return
|
|
931
1070
|
val loadTime = System.currentTimeMillis() - startTime
|
|
932
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"}")
|
|
933
1073
|
Log.d(TAG, "📊 Asset loading stats: Attempted: $assetLoadAttempts, Failed: $assetLoadFailures (${String.format("%.1f", assetLoadFailures * 100.0 / assetLoadAttempts.coerceAtLeast(1))}%)")
|
|
934
1074
|
Log.d(TAG, "📊 ${metricsMonitor.getCacheStats()}")
|
|
935
1075
|
|
|
@@ -952,7 +1092,30 @@ class CustomWebView(
|
|
|
952
1092
|
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
|
953
1093
|
if (error != null) {
|
|
954
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
|
+
}
|
|
955
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
|
|
1117
|
+
}
|
|
1118
|
+
|
|
956
1119
|
super.onReceivedError(view, request, error)
|
|
957
1120
|
}
|
|
958
1121
|
|
|
@@ -1004,6 +1167,7 @@ class CustomWebView(
|
|
|
1004
1167
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
|
1005
1168
|
if (BuildConfig.DEBUG) {
|
|
1006
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()}")
|
|
1007
1171
|
}
|
|
1008
1172
|
return true
|
|
1009
1173
|
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt
CHANGED
|
@@ -136,6 +136,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
136
136
|
"X-Safe-Area-Right" to latestSafeAreaInsets.right.toString(),
|
|
137
137
|
"X-Safe-Area-Bottom" to latestSafeAreaInsets.bottom.toString(),
|
|
138
138
|
"X-Safe-Area-Left" to latestSafeAreaInsets.left.toString(),
|
|
139
|
+
"X-Catalyst-Native-WebView" to "1",
|
|
139
140
|
// Prevent caching of SSR response so updated headers are always used
|
|
140
141
|
"Cache-Control" to "no-cache, no-store, must-revalidate",
|
|
141
142
|
"Pragma" to "no-cache"
|
|
@@ -385,7 +386,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
385
386
|
if (BuildConfig.DEBUG) {
|
|
386
387
|
Log.w(TAG, "📴 Device offline on launch, showing offline page")
|
|
387
388
|
}
|
|
388
|
-
customWebView.
|
|
389
|
+
customWebView.showOfflineRouteOrOfflinePage(currentUrl)
|
|
389
390
|
}
|
|
390
391
|
}
|
|
391
392
|
|
|
@@ -533,7 +534,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
533
534
|
loadUrlWithSafeArea(url)
|
|
534
535
|
} else {
|
|
535
536
|
Log.w(TAG, "📴 Offline during notification click, showing offline page")
|
|
536
|
-
customWebView.
|
|
537
|
+
customWebView.showOfflineRouteOrOfflinePage(url)
|
|
537
538
|
}
|
|
538
539
|
|
|
539
540
|
} catch (e: Exception) {
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt
CHANGED
|
@@ -213,7 +213,7 @@ class NativeBridge(
|
|
|
213
213
|
.filter { it.isNotEmpty() }
|
|
214
214
|
|
|
215
215
|
accessControlEnabled = properties
|
|
216
|
-
.getProperty("accessControl.enabled", "
|
|
216
|
+
.getProperty("accessControl.enabled", "false")
|
|
217
217
|
.equals("true", ignoreCase = true)
|
|
218
218
|
|
|
219
219
|
if (accessControlEnabled && allowedUrls.isNotEmpty()) {
|