catalyst-core 0.2.0-beta.1 → 0.2.0-beta.3
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 +26 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +381 -78
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +29 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +7 -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 +162 -26
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -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/ProfilerLifecycleTest.kt +51 -0
- package/dist/native/bridge/WebBridge.js +2 -2
- package/dist/native/bridge/constants/NativeInterfaces.js +2 -1
- package/dist/native/bridge/perf/collectors/ApiCollector.js +22 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.js +40 -0
- package/dist/native/bridge/perf/collectors/HookCollector.js +43 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +47 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +38 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +25 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +53 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +7 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.js +40 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +25 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +27 -0
- package/dist/native/bridge/perf/core/constants.js +43 -0
- package/dist/native/bridge/perf/core/measure.js +40 -0
- package/dist/native/bridge/perf/core/session.js +34 -0
- package/dist/native/bridge/perf/core/store.js +1 -0
- package/dist/native/bridge/perf/core/summary.js +1 -0
- package/dist/native/bridge/perf/core/traceExport.js +1 -0
- package/dist/native/bridge/perf/core/waterfalls.js +1 -0
- package/dist/native/bridge/perf/index.js +81 -0
- package/dist/native/buildAndroid/assets.js +2 -2
- package/dist/native/buildIos/config.js +1 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +49 -4
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
- 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/PerfEventBuffer.swift +373 -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/BridgeFileHandler.swift +35 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +141 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +157 -3
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
- 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
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -7,18 +7,26 @@ import android.graphics.Bitmap
|
|
|
7
7
|
import android.net.Uri
|
|
8
8
|
import android.os.Bundle
|
|
9
9
|
import android.util.Log
|
|
10
|
+
import android.view.GestureDetector
|
|
11
|
+
import android.view.MotionEvent
|
|
10
12
|
import android.view.View
|
|
11
13
|
import android.webkit.*
|
|
12
14
|
import androidx.webkit.ServiceWorkerClientCompat
|
|
13
15
|
import androidx.webkit.ServiceWorkerControllerCompat
|
|
16
|
+
import androidx.webkit.WebViewCompat
|
|
17
|
+
import androidx.webkit.WebViewFeature
|
|
14
18
|
import androidx.webkit.WebResourceRequestCompat
|
|
15
19
|
import android.widget.ProgressBar
|
|
16
20
|
import androidx.webkit.WebViewAssetLoader
|
|
17
21
|
import io.yourname.androidproject.WebCacheManager
|
|
18
22
|
import io.yourname.androidproject.URLWhitelistManager
|
|
19
23
|
import io.yourname.androidproject.matchesCachePattern
|
|
24
|
+
import io.yourname.androidproject.utils.BridgeUtils
|
|
20
25
|
import io.yourname.androidproject.utils.CameraUtils
|
|
21
26
|
import io.yourname.androidproject.utils.NetworkUtils
|
|
27
|
+
import io.yourname.androidproject.utils.PerfEventBuffer
|
|
28
|
+
import io.yourname.androidproject.utils.ProfilerNavigationState
|
|
29
|
+
import org.json.JSONObject
|
|
22
30
|
import kotlinx.coroutines.*
|
|
23
31
|
import java.io.FileNotFoundException
|
|
24
32
|
import java.util.Properties
|
|
@@ -31,10 +39,12 @@ class CustomWebView(
|
|
|
31
39
|
) : CoroutineScope {
|
|
32
40
|
|
|
33
41
|
private val TAG = "WebViewDebug"
|
|
42
|
+
private val FLOW_TAG = "CatalystOfflineFlow"
|
|
34
43
|
private val job = SupervisorJob()
|
|
35
44
|
override val coroutineContext = Dispatchers.Main + job
|
|
36
45
|
|
|
37
46
|
private var cacheManager: WebCacheManager
|
|
47
|
+
private var offlineCacheService: OfflineCacheService
|
|
38
48
|
private val metricsMonitor = MetricsMonitor.getInstance(context)
|
|
39
49
|
private var buildType: String = "debug" // Default to debug
|
|
40
50
|
private var cachePatterns: List<String> = emptyList()
|
|
@@ -43,13 +53,19 @@ class CustomWebView(
|
|
|
43
53
|
private var isInitialApiCalled: Boolean = false // Flag to track initial page load
|
|
44
54
|
private var isInitialPageLoaded: Boolean = false
|
|
45
55
|
private var buildOptimisation: Boolean = false // Added property for build optimization
|
|
56
|
+
private var profilerEnabled: Boolean = false
|
|
57
|
+
private val profilerNavigationState = ProfilerNavigationState()
|
|
46
58
|
private lateinit var assetLoader: WebViewAssetLoader
|
|
47
59
|
private var allowedUrls: List<String> = emptyList()
|
|
48
60
|
private var accessControlEnabled: Boolean = false
|
|
49
61
|
private val offlineAssetPath = "offline/offline.html"
|
|
50
62
|
private val offlineAssetUrl = "file:///android_asset/$offlineAssetPath"
|
|
63
|
+
private var scrollSessionOpen = false
|
|
64
|
+
private lateinit var scrollGestureDetector: GestureDetector
|
|
51
65
|
private var offlinePageVisible = false
|
|
52
66
|
private var lastTargetUrl: String? = null
|
|
67
|
+
private var activeOfflineRouteOrigin: String? = null
|
|
68
|
+
private var visibleOfflineSnapshotUrl: String? = null
|
|
53
69
|
private var defaultRequestHeaders: Map<String, String> = emptyMap()
|
|
54
70
|
var onPageStarted: (() -> Unit)? = null
|
|
55
71
|
|
|
@@ -57,12 +73,18 @@ class CustomWebView(
|
|
|
57
73
|
private var assetLoadAttempts = 0
|
|
58
74
|
private var assetLoadFailures = 0
|
|
59
75
|
|
|
60
|
-
// Track ongoing cache fetches to prevent duplicates
|
|
61
|
-
private val ongoingCacheFetches = mutableSetOf<String>()
|
|
62
|
-
|
|
63
76
|
init {
|
|
77
|
+
if (BuildConfig.DEBUG) {
|
|
78
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
79
|
+
put("type", "boot-webview-constructed")
|
|
80
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
81
|
+
put("thread", Thread.currentThread().name)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
64
84
|
setupFromProperties()
|
|
65
85
|
cacheManager = WebCacheManager(context, properties)
|
|
86
|
+
if (profilerEnabled) metricsMonitor.attachWebView(webView)
|
|
87
|
+
offlineCacheService = OfflineCacheService(context)
|
|
66
88
|
setupWebView()
|
|
67
89
|
}
|
|
68
90
|
|
|
@@ -88,6 +110,7 @@ class CustomWebView(
|
|
|
88
110
|
|
|
89
111
|
// Parse buildOptimisation property
|
|
90
112
|
buildOptimisation = properties.getProperty("buildOptimisation", "false").toBoolean()
|
|
113
|
+
profilerEnabled = PerfEventBuffer.isEnabled()
|
|
91
114
|
|
|
92
115
|
// Load allowed URLs from properties
|
|
93
116
|
allowedUrls = properties.getProperty("accessControl.allowedUrls", "")
|
|
@@ -97,7 +120,7 @@ class CustomWebView(
|
|
|
97
120
|
|
|
98
121
|
// Access control toggle
|
|
99
122
|
accessControlEnabled = properties
|
|
100
|
-
.getProperty("accessControl.enabled", "
|
|
123
|
+
.getProperty("accessControl.enabled", "false")
|
|
101
124
|
.equals("true", ignoreCase = true)
|
|
102
125
|
|
|
103
126
|
// Initialize URLWhitelistManager with access control configuration
|
|
@@ -137,6 +160,15 @@ class CustomWebView(
|
|
|
137
160
|
|
|
138
161
|
private fun loadUrlInternal(url: String) {
|
|
139
162
|
val isNetworkUrl = url.startsWith("http://") || url.startsWith("https://")
|
|
163
|
+
val isOnline = NetworkUtils.getCurrentStatus(context).isOnline
|
|
164
|
+
|
|
165
|
+
if (BuildConfig.DEBUG) {
|
|
166
|
+
Log.d(FLOW_TAG, "WEBVIEW load-url start online=$isOnline url=$url headers=${defaultRequestHeaders.keys.sorted()}")
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (isNetworkUrl && isOnline) {
|
|
170
|
+
offlineCacheService.refreshManifestAsync(url, defaultRequestHeaders)
|
|
171
|
+
}
|
|
140
172
|
|
|
141
173
|
if (isNetworkUrl && defaultRequestHeaders.isNotEmpty()) {
|
|
142
174
|
if (BuildConfig.DEBUG) {
|
|
@@ -146,12 +178,31 @@ class CustomWebView(
|
|
|
146
178
|
} else {
|
|
147
179
|
webView.loadUrl(url)
|
|
148
180
|
}
|
|
181
|
+
if (BuildConfig.DEBUG) {
|
|
182
|
+
Log.d(FLOW_TAG, "WEBVIEW load-url dispatched url=$url")
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private fun loadTopLevelUrl(url: String) {
|
|
187
|
+
if (BuildConfig.DEBUG) {
|
|
188
|
+
val navigation = profilerNavigationState.begin(url)
|
|
189
|
+
if (navigation.shouldReset) PerfEventBuffer.reset()
|
|
190
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
191
|
+
put("type", "boot-load-url")
|
|
192
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
193
|
+
put("url", navigation.url)
|
|
194
|
+
put("thread", Thread.currentThread().name)
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
loadUrlInternal(url)
|
|
149
198
|
}
|
|
150
199
|
|
|
151
200
|
fun loadUrl(url: String) {
|
|
152
201
|
lastTargetUrl = url
|
|
153
202
|
offlinePageVisible = false
|
|
154
|
-
|
|
203
|
+
visibleOfflineSnapshotUrl = null
|
|
204
|
+
activeOfflineRouteOrigin = null
|
|
205
|
+
loadTopLevelUrl(url)
|
|
155
206
|
}
|
|
156
207
|
|
|
157
208
|
fun updateLastTargetUrl(url: String) {
|
|
@@ -167,7 +218,9 @@ class CustomWebView(
|
|
|
167
218
|
try {
|
|
168
219
|
context.assets.open(offlineAssetPath).close()
|
|
169
220
|
offlinePageVisible = true
|
|
170
|
-
|
|
221
|
+
visibleOfflineSnapshotUrl = null
|
|
222
|
+
activeOfflineRouteOrigin = null
|
|
223
|
+
loadTopLevelUrl(offlineAssetUrl)
|
|
171
224
|
if (BuildConfig.DEBUG) {
|
|
172
225
|
Log.d(TAG, "📴 Showing offline page from assets: $offlineAssetPath")
|
|
173
226
|
}
|
|
@@ -192,6 +245,32 @@ class CustomWebView(
|
|
|
192
245
|
|
|
193
246
|
fun clearAllCache() {
|
|
194
247
|
cacheManager.clearAll()
|
|
248
|
+
offlineCacheService.clearAll()
|
|
249
|
+
activeOfflineRouteOrigin = null
|
|
250
|
+
visibleOfflineSnapshotUrl = null
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fun showOfflineRouteOrOfflinePage(url: String) {
|
|
254
|
+
val snapshotUrl = normalizeUrlWithoutFragment(url)
|
|
255
|
+
if (visibleOfflineSnapshotUrl == snapshotUrl) {
|
|
256
|
+
if (BuildConfig.DEBUG) {
|
|
257
|
+
Log.d(TAG, "📴 Cached offline route snapshot already visible: $url")
|
|
258
|
+
}
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (offlineCacheService.hasRouteSnapshot(url)) {
|
|
263
|
+
offlinePageVisible = false
|
|
264
|
+
visibleOfflineSnapshotUrl = snapshotUrl
|
|
265
|
+
updateActiveOfflineRoute(url, true)
|
|
266
|
+
loadTopLevelUrl(url)
|
|
267
|
+
if (BuildConfig.DEBUG) {
|
|
268
|
+
Log.d(TAG, "📴 Loading cached offline route through request interceptor: $url")
|
|
269
|
+
}
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
showOfflinePage()
|
|
195
274
|
}
|
|
196
275
|
|
|
197
276
|
fun canGoBack(): Boolean = webView.canGoBack()
|
|
@@ -260,6 +339,7 @@ class CustomWebView(
|
|
|
260
339
|
|
|
261
340
|
fun destroy() {
|
|
262
341
|
job.cancel()
|
|
342
|
+
metricsMonitor.detachWebView()
|
|
263
343
|
webView.destroy()
|
|
264
344
|
}
|
|
265
345
|
|
|
@@ -291,6 +371,13 @@ class CustomWebView(
|
|
|
291
371
|
if (BuildConfig.DEBUG) {
|
|
292
372
|
Log.d(TAG, "🚀 Hardware acceleration enabled - Thread: ${Thread.currentThread().name}")
|
|
293
373
|
}
|
|
374
|
+
BridgeUtils.emitPerfEvent(webView, JSONObject().apply {
|
|
375
|
+
put("type", "hw-accel-change")
|
|
376
|
+
put("state", "on")
|
|
377
|
+
put("trigger", "cleanup")
|
|
378
|
+
put("thread", Thread.currentThread().name)
|
|
379
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
380
|
+
})
|
|
294
381
|
}
|
|
295
382
|
}
|
|
296
383
|
|
|
@@ -301,28 +388,122 @@ class CustomWebView(
|
|
|
301
388
|
if (BuildConfig.DEBUG) {
|
|
302
389
|
Log.d(TAG, "⚫ Hardware acceleration disabled - Thread: ${Thread.currentThread().name}")
|
|
303
390
|
}
|
|
391
|
+
BridgeUtils.emitPerfEvent(webView, JSONObject().apply {
|
|
392
|
+
put("type", "hw-accel-change")
|
|
393
|
+
put("state", "off")
|
|
394
|
+
put("trigger", "cache-serve")
|
|
395
|
+
put("thread", Thread.currentThread().name)
|
|
396
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
397
|
+
})
|
|
304
398
|
}
|
|
305
399
|
}
|
|
306
400
|
|
|
307
|
-
private fun
|
|
401
|
+
private fun shouldCacheRequest(request: WebResourceRequest, url: String): Boolean {
|
|
308
402
|
if (BuildConfig.DEBUG) {
|
|
309
403
|
Log.d(TAG, "🔍 CACHE_CHECK: Checking if URL should be cached: $url")
|
|
310
404
|
Log.d(TAG, "🔍 CACHE_CHECK: Cache patterns: $cachePatterns")
|
|
311
405
|
}
|
|
312
406
|
|
|
313
|
-
val
|
|
407
|
+
val matchesPattern = matchesCachePattern(url, cachePatterns)
|
|
408
|
+
val offlineRouteSubresource = shouldCacheOfflineRouteSubresource(request, url)
|
|
409
|
+
val shouldCache = matchesPattern || offlineRouteSubresource
|
|
314
410
|
|
|
315
411
|
if (BuildConfig.DEBUG) {
|
|
316
412
|
if (shouldCache) {
|
|
317
|
-
Log.d(TAG, "✅ CACHE_CHECK: URL WILL be cached: $url")
|
|
413
|
+
Log.d(TAG, "✅ CACHE_CHECK: URL WILL be cached: $url (pattern=$matchesPattern, offlineRouteSubresource=$offlineRouteSubresource)")
|
|
414
|
+
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
415
|
} else {
|
|
319
416
|
Log.d(TAG, "❌ CACHE_CHECK: URL will NOT be cached (no pattern match): $url")
|
|
417
|
+
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
418
|
}
|
|
321
419
|
}
|
|
322
420
|
|
|
323
421
|
return shouldCache
|
|
324
422
|
}
|
|
325
423
|
|
|
424
|
+
private fun shouldCacheOfflineRouteSubresource(request: WebResourceRequest, url: String): Boolean {
|
|
425
|
+
if (request.isForMainFrame || request.method != "GET") return false
|
|
426
|
+
if (!isHttpUrl(url) || isApiCall(url) || isInternalOfflineRuntimeUrl(url)) return false
|
|
427
|
+
|
|
428
|
+
val activeOrigin = activeOfflineRouteOrigin ?: return false
|
|
429
|
+
if (originForUrl(url) != activeOrigin) return false
|
|
430
|
+
|
|
431
|
+
val destination = requestHeader(request, "Sec-Fetch-Dest")?.lowercase()
|
|
432
|
+
if (destination == "document" || destination == "empty") return false
|
|
433
|
+
if (destination in setOf("script", "style", "image", "font", "audio", "video", "track", "manifest")) {
|
|
434
|
+
return true
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
val accept = requestHeader(request, "Accept")?.lowercase().orEmpty()
|
|
438
|
+
if (accept.contains("text/html") ||
|
|
439
|
+
accept.contains("application/json") ||
|
|
440
|
+
accept.contains("text/event-stream")
|
|
441
|
+
) {
|
|
442
|
+
return false
|
|
443
|
+
}
|
|
444
|
+
if (accept.contains("text/css") ||
|
|
445
|
+
accept.contains("javascript") ||
|
|
446
|
+
accept.contains("image/") ||
|
|
447
|
+
accept.contains("font/") ||
|
|
448
|
+
accept.contains("application/font") ||
|
|
449
|
+
accept.contains("application/wasm")
|
|
450
|
+
) {
|
|
451
|
+
return true
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return offlineCacheService.shouldCacheAssetUrl(url)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private fun requestHeader(request: WebResourceRequest, name: String): String? {
|
|
458
|
+
return request.requestHeaders.entries.firstOrNull {
|
|
459
|
+
it.key.equals(name, ignoreCase = true)
|
|
460
|
+
}?.value
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private fun updateActiveOfflineRoute(url: String, active: Boolean) {
|
|
464
|
+
activeOfflineRouteOrigin = if (active) originForUrl(url) else null
|
|
465
|
+
if (BuildConfig.DEBUG) {
|
|
466
|
+
Log.d(FLOW_TAG, "ROUTE active=$active activeOrigin=${activeOfflineRouteOrigin ?: "none"} url=$url")
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private fun isVisibleOfflineSnapshot(url: String?): Boolean {
|
|
471
|
+
if (url == null) return false
|
|
472
|
+
return visibleOfflineSnapshotUrl == normalizeUrlWithoutFragment(url)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private fun normalizeUrlWithoutFragment(url: String): String {
|
|
476
|
+
return try {
|
|
477
|
+
Uri.parse(url).buildUpon().fragment(null).build().toString()
|
|
478
|
+
} catch (_: Exception) {
|
|
479
|
+
url
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private fun originForUrl(url: String): String? {
|
|
484
|
+
return try {
|
|
485
|
+
val uri = Uri.parse(url)
|
|
486
|
+
val scheme = uri.scheme?.lowercase() ?: return null
|
|
487
|
+
val authority = uri.authority ?: return null
|
|
488
|
+
if (scheme != "http" && scheme != "https") return null
|
|
489
|
+
"$scheme://$authority"
|
|
490
|
+
} catch (_: Exception) {
|
|
491
|
+
null
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private fun isHttpUrl(url: String): Boolean {
|
|
496
|
+
val scheme = Uri.parse(url).scheme?.lowercase()
|
|
497
|
+
return scheme == "http" || scheme == "https"
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private fun isInternalOfflineRuntimeUrl(url: String): Boolean {
|
|
501
|
+
val path = Uri.parse(url).path ?: return false
|
|
502
|
+
return path == "/catalyst-offline-manifest.json" ||
|
|
503
|
+
path == "/catalyst-sw.js" ||
|
|
504
|
+
path == "/offline.html"
|
|
505
|
+
}
|
|
506
|
+
|
|
326
507
|
/**
|
|
327
508
|
* Handle special URL schemes (tel:, mailto:, sms:)
|
|
328
509
|
*/
|
|
@@ -341,7 +522,7 @@ class CustomWebView(
|
|
|
341
522
|
}
|
|
342
523
|
offlinePageVisible = false
|
|
343
524
|
lastTargetUrl = target
|
|
344
|
-
|
|
525
|
+
loadTopLevelUrl(target)
|
|
345
526
|
} else if (BuildConfig.DEBUG) {
|
|
346
527
|
Log.w(TAG, "🔄 Retry requested but no target URL is known")
|
|
347
528
|
}
|
|
@@ -502,11 +683,13 @@ class CustomWebView(
|
|
|
502
683
|
serviceWorkerController.setServiceWorkerClient(object : ServiceWorkerClientCompat() {
|
|
503
684
|
override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
|
|
504
685
|
val url = request.url.toString()
|
|
686
|
+
val isOnline = NetworkUtils.getCurrentStatus(context).isOnline
|
|
505
687
|
|
|
506
688
|
if (BuildConfig.DEBUG) {
|
|
507
689
|
Log.d(TAG, "🔧 SERVICE_WORKER: Intercepting request: $url")
|
|
508
690
|
Log.d(TAG, "🔧 SERVICE_WORKER: Method: ${request.method}")
|
|
509
691
|
Log.d(TAG, "🔧 SERVICE_WORKER: Headers: ${request.requestHeaders}")
|
|
692
|
+
Log.d(FLOW_TAG, "SW intercept method=${request.method} online=$isOnline mainFrame=${request.isForMainFrame} url=$url")
|
|
510
693
|
}
|
|
511
694
|
|
|
512
695
|
// Check if URL is allowed
|
|
@@ -526,7 +709,7 @@ class CustomWebView(
|
|
|
526
709
|
}
|
|
527
710
|
|
|
528
711
|
// Check if this should be cached
|
|
529
|
-
if (request.method == "GET" &&
|
|
712
|
+
if (request.method == "GET" && shouldCacheRequest(request, url)) {
|
|
530
713
|
if (BuildConfig.DEBUG) {
|
|
531
714
|
Log.d(TAG, "🎯 SERVICE_WORKER: Using cache system for: $url")
|
|
532
715
|
}
|
|
@@ -540,49 +723,22 @@ class CustomWebView(
|
|
|
540
723
|
}
|
|
541
724
|
|
|
542
725
|
// Get cached response synchronously (non-blocking for ServiceWorker)
|
|
543
|
-
val response =
|
|
726
|
+
val response = if (NetworkUtils.getCurrentStatus(context).isOnline) {
|
|
727
|
+
cacheManager.getCachedResponseOrFetchSync(url, headers)
|
|
728
|
+
} else {
|
|
729
|
+
cacheManager.getCachedResponseSync(url, headers)
|
|
730
|
+
}
|
|
544
731
|
if (response != null) {
|
|
545
732
|
if (BuildConfig.DEBUG) {
|
|
546
733
|
Log.d(TAG, "✅ SERVICE_WORKER: Served from cache: $url")
|
|
734
|
+
Log.d(FLOW_TAG, "SW response source=cache-or-fetch mime=${response.mimeType ?: "unknown"} encoding=${response.encoding ?: "unknown"} url=$url")
|
|
547
735
|
}
|
|
548
736
|
withCorsHeaders(response)
|
|
549
737
|
} else {
|
|
550
738
|
if (BuildConfig.DEBUG) {
|
|
551
739
|
Log.d(TAG, "❌ SERVICE_WORKER: Cache miss: $url")
|
|
740
|
+
Log.d(FLOW_TAG, "SW response source=network-fallback reason=cache-null url=$url")
|
|
552
741
|
}
|
|
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
742
|
null
|
|
587
743
|
}
|
|
588
744
|
} catch (e: Exception) {
|
|
@@ -594,7 +750,7 @@ class CustomWebView(
|
|
|
594
750
|
} else {
|
|
595
751
|
if (BuildConfig.DEBUG) {
|
|
596
752
|
Log.d(TAG, "⏭️ SERVICE_WORKER: Skipping cache for: $url")
|
|
597
|
-
Log.d(TAG, "⏭️ SERVICE_WORKER: Method: ${request.method}, shouldCache: ${
|
|
753
|
+
Log.d(TAG, "⏭️ SERVICE_WORKER: Method: ${request.method}, shouldCache: ${shouldCacheRequest(request, url)}")
|
|
598
754
|
}
|
|
599
755
|
}
|
|
600
756
|
|
|
@@ -612,6 +768,13 @@ class CustomWebView(
|
|
|
612
768
|
|
|
613
769
|
@SuppressLint("SetJavaScriptEnabled")
|
|
614
770
|
private fun setupWebView() {
|
|
771
|
+
if (profilerEnabled && WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
|
|
772
|
+
WebViewCompat.addDocumentStartJavaScript(
|
|
773
|
+
webView,
|
|
774
|
+
"window.__CATALYST_PROFILER_ENABLED = true;",
|
|
775
|
+
setOf("*")
|
|
776
|
+
)
|
|
777
|
+
}
|
|
615
778
|
if (BuildConfig.DEBUG) {
|
|
616
779
|
Log.d(TAG, "🌐 Setting up WebView on thread: ${Thread.currentThread().name}")
|
|
617
780
|
}
|
|
@@ -687,6 +850,7 @@ class CustomWebView(
|
|
|
687
850
|
request: WebResourceRequest
|
|
688
851
|
): WebResourceResponse? {
|
|
689
852
|
val url = request.url.toString()
|
|
853
|
+
val isOnline = NetworkUtils.getCurrentStatus(context).isOnline
|
|
690
854
|
|
|
691
855
|
// Track all network requests
|
|
692
856
|
metricsMonitor.recordNetworkRequest(url)
|
|
@@ -697,6 +861,7 @@ class CustomWebView(
|
|
|
697
861
|
Log.d(TAG, "🔄 INTERCEPT: Thread: ${Thread.currentThread().name}")
|
|
698
862
|
Log.d(TAG, "🔄 INTERCEPT: File extension: ${url.substringAfterLast('.', "none")}")
|
|
699
863
|
Log.d(TAG, "🔄 INTERCEPT: isInitialPageLoaded: $isInitialPageLoaded")
|
|
864
|
+
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
865
|
}
|
|
701
866
|
|
|
702
867
|
if (!URLWhitelistManager.isUrlAllowed(url)) {
|
|
@@ -713,6 +878,51 @@ class CustomWebView(
|
|
|
713
878
|
}
|
|
714
879
|
}
|
|
715
880
|
|
|
881
|
+
val isMainFrameDocument = request.isForMainFrame &&
|
|
882
|
+
request.method == "GET" &&
|
|
883
|
+
(request.url.scheme == "http" || request.url.scheme == "https") &&
|
|
884
|
+
!isApiCall(url)
|
|
885
|
+
|
|
886
|
+
if (isMainFrameDocument) {
|
|
887
|
+
val headers = defaultRequestHeaders.toMutableMap().apply {
|
|
888
|
+
putAll(request.requestHeaders)
|
|
889
|
+
}
|
|
890
|
+
if (BuildConfig.DEBUG) {
|
|
891
|
+
Log.d(FLOW_TAG, "DOCUMENT intercept online=$isOnline url=$url headers=${headers.keys.sorted()}")
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (isOnline) {
|
|
895
|
+
offlineCacheService.refreshManifestIfMissing(url, headers)
|
|
896
|
+
val isOfflineRoute = offlineCacheService.isOfflineRouteUrl(url)
|
|
897
|
+
updateActiveOfflineRoute(url, isOfflineRoute)
|
|
898
|
+
visibleOfflineSnapshotUrl = null
|
|
899
|
+
if (BuildConfig.DEBUG) {
|
|
900
|
+
Log.d(FLOW_TAG, "DOCUMENT online routeEligible=$isOfflineRoute action=allow-network-and-background-snapshot url=$url")
|
|
901
|
+
}
|
|
902
|
+
offlineCacheService.storeRouteSnapshotAsync(url, headers)
|
|
903
|
+
} else {
|
|
904
|
+
val snapshotResponse = offlineCacheService.getRouteSnapshotResponse(url)
|
|
905
|
+
if (snapshotResponse != null) {
|
|
906
|
+
updateActiveOfflineRoute(url, true)
|
|
907
|
+
visibleOfflineSnapshotUrl = normalizeUrlWithoutFragment(url)
|
|
908
|
+
if (BuildConfig.DEBUG) {
|
|
909
|
+
Log.d(TAG, "📴 INTERCEPT: Serving offline route snapshot: $url")
|
|
910
|
+
Log.d(FLOW_TAG, "DOCUMENT offline response=route-snapshot mime=${snapshotResponse.mimeType ?: "unknown"} url=$url")
|
|
911
|
+
}
|
|
912
|
+
return withCorsHeaders(snapshotResponse)
|
|
913
|
+
}
|
|
914
|
+
updateActiveOfflineRoute(url, false)
|
|
915
|
+
if (BuildConfig.DEBUG) {
|
|
916
|
+
Log.d(FLOW_TAG, "DOCUMENT offline response=network-fallback reason=snapshot-miss url=$url")
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
} else if (request.isForMainFrame &&
|
|
920
|
+
request.method == "GET" &&
|
|
921
|
+
(request.url.scheme == "http" || request.url.scheme == "https")
|
|
922
|
+
) {
|
|
923
|
+
updateActiveOfflineRoute(url, false)
|
|
924
|
+
}
|
|
925
|
+
|
|
716
926
|
// TODO: Add HTML file workflow - index.html not available yet
|
|
717
927
|
// Handle the initial route request - intercept first request regardless of host
|
|
718
928
|
// if (!isInitialApiCalled && request.method == "GET") {
|
|
@@ -747,12 +957,12 @@ class CustomWebView(
|
|
|
747
957
|
if (BuildConfig.DEBUG) {
|
|
748
958
|
Log.d(TAG, "🔍 STEP_4_CHECK: Initial page not loaded yet, checking for static resources")
|
|
749
959
|
Log.d(TAG, "🔍 STEP_4_CHECK: isStaticResourceRequest($url) = ${isStaticResourceRequest(url)}")
|
|
750
|
-
Log.d(TAG, "🔍 STEP_4_CHECK:
|
|
960
|
+
Log.d(TAG, "🔍 STEP_4_CHECK: shouldCacheRequest($url) = ${shouldCacheRequest(request, url)}")
|
|
751
961
|
}
|
|
752
962
|
|
|
753
963
|
// IMPORTANT: Skip asset loading if the URL matches a cache pattern
|
|
754
964
|
// This allows cached resources to be served from the cache system instead
|
|
755
|
-
if (request.method == "GET" && isStaticResourceRequest(url) && !
|
|
965
|
+
if (request.method == "GET" && isStaticResourceRequest(url) && !shouldCacheRequest(request, url)) {
|
|
756
966
|
val assetPath = extractAssetPath(url)
|
|
757
967
|
assetLoadAttempts++
|
|
758
968
|
|
|
@@ -790,7 +1000,7 @@ class CustomWebView(
|
|
|
790
1000
|
}
|
|
791
1001
|
// Unexpected error - fall through to cache check below
|
|
792
1002
|
}
|
|
793
|
-
} else if (
|
|
1003
|
+
} else if (shouldCacheRequest(request, url)) {
|
|
794
1004
|
if (BuildConfig.DEBUG) {
|
|
795
1005
|
Log.d(TAG, "⏭️ STEP_4_SKIP: Skipping asset loading for cached URL: $url")
|
|
796
1006
|
}
|
|
@@ -805,10 +1015,10 @@ class CustomWebView(
|
|
|
805
1015
|
if (BuildConfig.DEBUG) {
|
|
806
1016
|
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Evaluating cache conditions")
|
|
807
1017
|
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Request method: ${request.method} (need GET)")
|
|
808
|
-
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK:
|
|
1018
|
+
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: shouldCacheRequest($url) = ${shouldCacheRequest(request, url)}")
|
|
809
1019
|
}
|
|
810
1020
|
|
|
811
|
-
if (request.method == "GET" &&
|
|
1021
|
+
if (request.method == "GET" && shouldCacheRequest(request, url)) {
|
|
812
1022
|
// Track that this request was evaluated for caching
|
|
813
1023
|
metricsMonitor.recordCacheEvaluation(url)
|
|
814
1024
|
if (BuildConfig.DEBUG) {
|
|
@@ -831,45 +1041,27 @@ class CustomWebView(
|
|
|
831
1041
|
}
|
|
832
1042
|
|
|
833
1043
|
// Use synchronous cache check to avoid blocking
|
|
834
|
-
val response =
|
|
1044
|
+
val response = if (NetworkUtils.getCurrentStatus(context).isOnline) {
|
|
1045
|
+
cacheManager.getCachedResponseOrFetchSync(url, headers)
|
|
1046
|
+
} else {
|
|
1047
|
+
cacheManager.getCachedResponseSync(url, headers)
|
|
1048
|
+
}
|
|
835
1049
|
|
|
836
1050
|
if (response != null) {
|
|
837
1051
|
if (BuildConfig.DEBUG) {
|
|
838
1052
|
val duration = System.currentTimeMillis() - startTime
|
|
839
1053
|
Log.d(TAG, "✅ WEBVIEW_CLIENT: Served from cache in ${duration}ms: $url")
|
|
1054
|
+
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
1055
|
}
|
|
841
1056
|
metricsMonitor.recordCacheHit(url)
|
|
842
1057
|
return withCorsHeaders(response)
|
|
843
1058
|
} else {
|
|
844
1059
|
if (BuildConfig.DEBUG) {
|
|
845
|
-
Log.d(TAG, "❌ WEBVIEW_CLIENT: Cache miss
|
|
1060
|
+
Log.d(TAG, "❌ WEBVIEW_CLIENT: Cache miss: $url")
|
|
1061
|
+
Log.d(FLOW_TAG, "ASSET response source=network-fallback reason=cache-null online=${NetworkUtils.getCurrentStatus(context).isOnline} url=$url")
|
|
846
1062
|
}
|
|
847
1063
|
metricsMonitor.recordCacheMiss(url)
|
|
848
1064
|
|
|
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
1065
|
// Return null to let network handle this request
|
|
874
1066
|
return null
|
|
875
1067
|
}
|
|
@@ -897,7 +1089,7 @@ class CustomWebView(
|
|
|
897
1089
|
Log.d(TAG, "❌ STEP_5_FAIL: URL doesn't match cache criteria, skipping cache")
|
|
898
1090
|
Log.d(TAG, "❌ STEP_5_FAIL: URL: $url")
|
|
899
1091
|
Log.d(TAG, "❌ STEP_5_FAIL: Request method: ${request.method} (need GET)")
|
|
900
|
-
Log.d(TAG, "❌ STEP_5_FAIL:
|
|
1092
|
+
Log.d(TAG, "❌ STEP_5_FAIL: shouldCacheRequest result: ${shouldCacheRequest(request, url)}")
|
|
901
1093
|
Log.d(TAG, "❌ STEP_5_FAIL: This request will go through normal network loading")
|
|
902
1094
|
}
|
|
903
1095
|
}
|
|
@@ -909,6 +1101,9 @@ class CustomWebView(
|
|
|
909
1101
|
super.onPageStarted(view, url, favicon)
|
|
910
1102
|
if (url != null && url != offlineAssetUrl) {
|
|
911
1103
|
offlinePageVisible = false
|
|
1104
|
+
if (!isVisibleOfflineSnapshot(url)) {
|
|
1105
|
+
visibleOfflineSnapshotUrl = null
|
|
1106
|
+
}
|
|
912
1107
|
onPageStarted?.invoke()
|
|
913
1108
|
}
|
|
914
1109
|
progressBar.visibility = View.VISIBLE
|
|
@@ -917,6 +1112,28 @@ class CustomWebView(
|
|
|
917
1112
|
url?.let { metricsMonitor.trackPageLoadStart(it) }
|
|
918
1113
|
if (BuildConfig.DEBUG) {
|
|
919
1114
|
Log.d(TAG, "⏳ Page load started for: $url - Hardware Acceleration: $isHardwareAccelerationEnabled")
|
|
1115
|
+
Log.d(FLOW_TAG, "PAGE started url=$url online=${NetworkUtils.getCurrentStatus(context).isOnline} offlinePageVisible=$offlinePageVisible visibleSnapshot=${visibleOfflineSnapshotUrl ?: "none"} activeOrigin=${activeOfflineRouteOrigin ?: "none"}")
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (profilerEnabled) {
|
|
1119
|
+
val nativeNow = android.os.SystemClock.elapsedRealtime()
|
|
1120
|
+
view?.evaluateJavascript(
|
|
1121
|
+
"window.__CATALYST_PROFILER_ENABLED = true; window.__NATIVE_TIME_OFFSET = $nativeNow - performance.now();",
|
|
1122
|
+
null
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
1125
|
+
if (profilerEnabled && url != null) {
|
|
1126
|
+
val nativeNow = android.os.SystemClock.elapsedRealtime()
|
|
1127
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
1128
|
+
put("type", "boot-page-started")
|
|
1129
|
+
put("nativeTime", nativeNow)
|
|
1130
|
+
put("url", url)
|
|
1131
|
+
})
|
|
1132
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
1133
|
+
put("type", "page-load-start")
|
|
1134
|
+
put("nativeTime", nativeNow)
|
|
1135
|
+
put("url", url)
|
|
1136
|
+
})
|
|
920
1137
|
}
|
|
921
1138
|
}
|
|
922
1139
|
|
|
@@ -924,12 +1141,31 @@ class CustomWebView(
|
|
|
924
1141
|
progressBar.visibility = View.GONE
|
|
925
1142
|
if(!isInitialPageLoaded){
|
|
926
1143
|
isInitialPageLoaded = true
|
|
1144
|
+
metricsMonitor.markAppStartComplete()
|
|
927
1145
|
}
|
|
928
1146
|
url?.let { metricsMonitor.trackPageLoadEnd(it) }
|
|
1147
|
+
|
|
1148
|
+
val pageFinishedNative = android.os.SystemClock.elapsedRealtime()
|
|
1149
|
+
if (profilerEnabled && url != null) {
|
|
1150
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
1151
|
+
put("type", "boot-page-finished")
|
|
1152
|
+
put("nativeTime", pageFinishedNative)
|
|
1153
|
+
put("url", url)
|
|
1154
|
+
})
|
|
1155
|
+
val pageStart = view?.tag as? Long
|
|
1156
|
+
PerfEventBuffer.add(JSONObject().apply {
|
|
1157
|
+
put("type", "page-load-end")
|
|
1158
|
+
put("nativeTime", pageFinishedNative)
|
|
1159
|
+
put("url", url)
|
|
1160
|
+
put("durationMs", if (pageStart != null) System.currentTimeMillis() - pageStart else 0L)
|
|
1161
|
+
})
|
|
1162
|
+
if (view != null) PerfEventBuffer.scheduleFlush(view)
|
|
1163
|
+
}
|
|
929
1164
|
if (BuildConfig.DEBUG) {
|
|
930
1165
|
val startTime = view?.tag as? Long ?: return
|
|
931
1166
|
val loadTime = System.currentTimeMillis() - startTime
|
|
932
1167
|
Log.d(TAG, "✅ Page load finished for: $url - Load time: ${loadTime}ms - Hardware Acceleration: $isHardwareAccelerationEnabled")
|
|
1168
|
+
Log.d(FLOW_TAG, "PAGE finished url=$url loadTimeMs=$loadTime online=${NetworkUtils.getCurrentStatus(context).isOnline} offlinePageVisible=$offlinePageVisible visibleSnapshot=${visibleOfflineSnapshotUrl ?: "none"} activeOrigin=${activeOfflineRouteOrigin ?: "none"}")
|
|
933
1169
|
Log.d(TAG, "📊 Asset loading stats: Attempted: $assetLoadAttempts, Failed: $assetLoadFailures (${String.format("%.1f", assetLoadFailures * 100.0 / assetLoadAttempts.coerceAtLeast(1))}%)")
|
|
934
1170
|
Log.d(TAG, "📊 ${metricsMonitor.getCacheStats()}")
|
|
935
1171
|
|
|
@@ -952,7 +1188,39 @@ class CustomWebView(
|
|
|
952
1188
|
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
|
953
1189
|
if (error != null) {
|
|
954
1190
|
Log.e(TAG, "❌ Error loading ${request?.url}: ${error.errorCode} ${error.description}")
|
|
1191
|
+
if (BuildConfig.DEBUG) {
|
|
1192
|
+
Log.e(FLOW_TAG, "PAGE resource-error mainFrame=${request?.isForMainFrame} method=${request?.method} code=${error.errorCode} description=${error.description} url=${request?.url}")
|
|
1193
|
+
}
|
|
1194
|
+
if (view != null && request?.isForMainFrame == true) {
|
|
1195
|
+
BridgeUtils.emitPerfEvent(view, JSONObject().apply {
|
|
1196
|
+
put("type", "page-load-error")
|
|
1197
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
1198
|
+
put("url", request.url?.toString() ?: "")
|
|
1199
|
+
put("errorCode", error.errorCode)
|
|
1200
|
+
put("description", error.description?.toString() ?: "")
|
|
1201
|
+
})
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
val failedUrl = request?.url?.toString()
|
|
1206
|
+
if (request?.isForMainFrame == true && isVisibleOfflineSnapshot(failedUrl)) {
|
|
1207
|
+
if (BuildConfig.DEBUG) {
|
|
1208
|
+
Log.d(TAG, "📴 Ignoring repeated error for visible offline route snapshot: $failedUrl")
|
|
1209
|
+
}
|
|
1210
|
+
return
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (request?.isForMainFrame == true &&
|
|
1214
|
+
failedUrl != null &&
|
|
1215
|
+
failedUrl != offlineAssetUrl &&
|
|
1216
|
+
!NetworkUtils.getCurrentStatus(context).isOnline
|
|
1217
|
+
) {
|
|
1218
|
+
view?.post {
|
|
1219
|
+
showOfflineRouteOrOfflinePage(failedUrl)
|
|
1220
|
+
}
|
|
1221
|
+
return
|
|
955
1222
|
}
|
|
1223
|
+
|
|
956
1224
|
super.onReceivedError(view, request, error)
|
|
957
1225
|
}
|
|
958
1226
|
|
|
@@ -1004,6 +1272,7 @@ class CustomWebView(
|
|
|
1004
1272
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
|
1005
1273
|
if (BuildConfig.DEBUG) {
|
|
1006
1274
|
Log.d(TAG, "Console: ${consoleMessage?.message()} -- From line ${consoleMessage?.lineNumber()} of ${consoleMessage?.sourceId()}")
|
|
1275
|
+
Log.d(FLOW_TAG, "CONSOLE level=${consoleMessage?.messageLevel()} line=${consoleMessage?.lineNumber()} source=${consoleMessage?.sourceId()} message=${consoleMessage?.message()}")
|
|
1007
1276
|
}
|
|
1008
1277
|
return true
|
|
1009
1278
|
}
|
|
@@ -1040,5 +1309,39 @@ class CustomWebView(
|
|
|
1040
1309
|
|
|
1041
1310
|
// Only enable WebView debugging in debug builds
|
|
1042
1311
|
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
|
1312
|
+
|
|
1313
|
+
if (profilerEnabled) {
|
|
1314
|
+
scrollGestureDetector = GestureDetector(context,
|
|
1315
|
+
object : GestureDetector.SimpleOnGestureListener() {
|
|
1316
|
+
override fun onScroll(
|
|
1317
|
+
e1: MotionEvent?,
|
|
1318
|
+
e2: MotionEvent,
|
|
1319
|
+
distanceX: Float,
|
|
1320
|
+
distanceY: Float
|
|
1321
|
+
): Boolean {
|
|
1322
|
+
if (!scrollSessionOpen) {
|
|
1323
|
+
scrollSessionOpen = true
|
|
1324
|
+
BridgeUtils.emitPerfEvent(webView, JSONObject().apply {
|
|
1325
|
+
put("type", "scroll-start")
|
|
1326
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
1327
|
+
})
|
|
1328
|
+
}
|
|
1329
|
+
return false
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
)
|
|
1333
|
+
webView.setOnTouchListener { _, event ->
|
|
1334
|
+
scrollGestureDetector.onTouchEvent(event)
|
|
1335
|
+
if (scrollSessionOpen &&
|
|
1336
|
+
(event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL)) {
|
|
1337
|
+
scrollSessionOpen = false
|
|
1338
|
+
BridgeUtils.emitPerfEvent(webView, JSONObject().apply {
|
|
1339
|
+
put("type", "scroll-end")
|
|
1340
|
+
put("nativeTime", android.os.SystemClock.elapsedRealtime())
|
|
1341
|
+
})
|
|
1342
|
+
}
|
|
1343
|
+
false
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1043
1346
|
}
|
|
1044
1347
|
}
|