catalyst-core 0.0.3-canary.9 → 0.1.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.
Files changed (182) hide show
  1. package/README.md +4 -5
  2. package/bin/catalyst.js +28 -15
  3. package/changelog.md +139 -30
  4. package/dist/native/androidProject/app/build.gradle.kts +125 -15
  5. package/dist/native/androidProject/app/proguard-rules.pro +85 -14
  6. package/dist/native/androidProject/app/src/debug/res/xml/network_security_config.xml +8 -0
  7. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +10 -3
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +552 -0
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +109 -0
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +605 -124
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +405 -47
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +341 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +954 -71
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +111 -0
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/URLWhitelistManager.kt +231 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/Utils.kt +49 -55
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +81 -8
  18. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/design/DesignTokens.kt +307 -0
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/EmulatorDetector.kt +203 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +187 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertHandler.kt +170 -0
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertUI.kt +366 -0
  23. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityBottomSheet.kt +138 -0
  24. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckManager.kt +511 -0
  25. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckScheduler.kt +68 -0
  26. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +47 -11
  27. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DeviceInfoUtils.kt +38 -13
  28. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DownloadUtils.kt +36 -14
  29. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FileUtils.kt +3 -2
  30. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +57 -8
  31. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/IntentUtils.kt +31 -6
  32. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +82 -0
  33. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NetworkUtils.kt +105 -0
  34. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationConstants.kt +28 -0
  35. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationUtils.kt +639 -0
  36. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/SafeAreaUtils.kt +62 -0
  37. package/dist/native/androidProject/app/src/main/res/drawable/splashscreen_fallback.png +0 -0
  38. package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +1 -0
  39. package/dist/native/androidProject/app/src/main/res/layout/activity_splash.xml +20 -0
  40. package/dist/native/androidProject/app/src/main/res/values/themes.xml +23 -0
  41. package/dist/native/androidProject/app/src/main/res/values-night/themes.xml +11 -0
  42. package/dist/native/androidProject/app/src/main/res/xml/network_security_config.xml +12 -0
  43. package/dist/native/androidProject/app/src/noFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +166 -0
  44. package/dist/native/androidProject/app/src/noFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +94 -0
  45. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeMessageValidatorTest.kt +267 -0
  46. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +286 -0
  47. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +296 -0
  48. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +594 -0
  49. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +466 -0
  50. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +187 -0
  51. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +304 -0
  52. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +424 -0
  53. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +469 -0
  54. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/SecurityBridgeTest.kt +199 -0
  55. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/URLWhitelistManagerTest.kt +462 -0
  56. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +391 -0
  57. package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +224 -0
  58. package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +399 -0
  59. package/dist/native/androidProject/build.gradle.kts +6 -0
  60. package/dist/native/androidProject/gradle/libs.versions.toml +8 -2
  61. package/dist/native/androidSetup.js +4 -3
  62. package/dist/native/assets/catalyst.png +0 -0
  63. package/dist/native/bridge/WebBridge.js +16 -3
  64. package/dist/native/bridge/constants/NativeInterfaces.js +28 -10
  65. package/dist/native/bridge/errors.js +1 -1
  66. package/dist/native/bridge/hooks.js +76 -16
  67. package/dist/native/bridge/safeArea.js +5 -0
  68. package/dist/native/bridge/standardizedHooks.js +23 -5
  69. package/dist/native/bridge/useBaseHook.js +7 -9
  70. package/dist/native/bridge/utils/CameraUtils.js +58 -0
  71. package/dist/native/bridge/utils/FileObjectConverter.js +40 -0
  72. package/dist/native/bridge/utils/NativeBridge.js +57 -8
  73. package/dist/native/buildAppAndroid.js +115 -43
  74. package/dist/native/buildAppIos.js +291 -26
  75. package/dist/native/iosnativeWebView/Configurations/Debug.xcconfig +21 -0
  76. package/dist/native/iosnativeWebView/Configurations/Release.xcconfig +24 -0
  77. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +50 -0
  78. package/dist/native/iosnativeWebView/Package.resolved +32 -0
  79. package/dist/native/iosnativeWebView/Package.swift +37 -0
  80. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/LocalNotificationHandler.swift +293 -0
  81. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +299 -0
  82. package/dist/native/iosnativeWebView/Sources/CatalystNotifications/PushNotificationHandler.swift +199 -0
  83. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +125 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +70 -0
  85. package/dist/native/iosnativeWebView/Sources/Core/Notifications/NotificationModels.swift +156 -0
  86. package/dist/native/iosnativeWebView/Sources/Core/Protocols/NotificationHandlerProtocol.swift +35 -0
  87. package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +26 -0
  88. package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/CacheManager.swift +69 -36
  89. package/dist/native/iosnativeWebView/Sources/Core/Utils/CameraTransportUtils.swift +363 -0
  90. package/dist/native/iosnativeWebView/Sources/Core/Utils/DeviceInfoUtils.swift +67 -0
  91. package/dist/native/iosnativeWebView/Sources/Core/Utils/FilePickerHandler.swift +1053 -0
  92. package/dist/native/iosnativeWebView/Sources/Core/Utils/FrameworkServerUtils.swift +901 -0
  93. package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/ImageHandler.swift +206 -20
  94. package/dist/native/iosnativeWebView/Sources/Core/Utils/NetworkMonitor.swift +112 -0
  95. package/dist/native/iosnativeWebView/Sources/Core/Utils/NotificationHandlerProvider.swift +20 -0
  96. package/dist/native/iosnativeWebView/Sources/Core/Utils/NullNotificationHandler.swift +85 -0
  97. package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/ResourceURLProtocol.swift +60 -25
  98. package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +132 -0
  99. package/dist/native/iosnativeWebView/Sources/Core/Utils/ScreenSecureManager.swift +131 -0
  100. package/dist/native/iosnativeWebView/Sources/Core/Utils/URLWhitelistManager.swift +224 -0
  101. package/dist/native/iosnativeWebView/Sources/Core/Utils/WebKitConfig.swift +43 -0
  102. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +991 -0
  103. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeDelegateHandler.swift +166 -0
  104. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +340 -0
  105. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeJavaScriptInterface.swift +149 -0
  106. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +611 -0
  107. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +334 -0
  108. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +95 -0
  109. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +86 -0
  110. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +181 -0
  111. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +182 -0
  112. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +395 -0
  113. package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +84 -0
  114. package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/AccentColor.colorset/Contents.json +8 -8
  115. package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/AppIcon.appiconset/Contents.json +10 -10
  116. package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/Contents.json +4 -4
  117. package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +65 -10
  118. package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +21 -0
  119. package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +13 -0
  120. package/dist/native/iosnativeWebView/iosnativeWebView/Preview Content/Preview Assets.xcassets/Contents.json +4 -4
  121. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +12 -0
  122. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +80 -0
  123. package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
  124. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +118 -100
  125. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +69 -0
  126. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/xcshareddata/xcschemes/iosnativeWebView.xcscheme +9 -4
  127. package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +47 -0
  128. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +236 -0
  129. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeCommandHandlerSecurityTests.swift +212 -0
  130. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +418 -0
  131. package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +390 -0
  132. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +299 -0
  133. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +383 -0
  134. package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +391 -0
  135. package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +581 -0
  136. package/dist/native/iosnativeWebView/iosnativeWebViewTests/NotificationHandlerTests.swift +430 -0
  137. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ScreenSecureManagerTests.swift +121 -0
  138. package/dist/native/iosnativeWebView/iosnativeWebViewTests/URLWhitelistManagerTests.swift +367 -0
  139. package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +440 -0
  140. package/dist/native/renameAndroidProject.js +80 -70
  141. package/dist/native/setupEmulatorIos.js +22 -5
  142. package/dist/native/setupServer.js +11 -10
  143. package/dist/native/terminalProgress.js +10 -10
  144. package/dist/native/utils.js +4 -5
  145. package/dist/otel.js +4 -0
  146. package/dist/scripts/build.js +2 -2
  147. package/dist/scripts/devBuild.js +2 -2
  148. package/dist/scripts/loadEnvironmentVariables.js +3 -1
  149. package/dist/scripts/scriptUtils.js +2 -2
  150. package/dist/sentry.js +4 -1
  151. package/dist/server/renderer/document/Body.js +4 -4
  152. package/dist/server/renderer/handler.js +7 -5
  153. package/dist/webpack/development.client.babel.js +1 -1
  154. package/mcp_v2/conversion-tasks.json +326 -0
  155. package/mcp_v2/knowledge-base.json +1068 -0
  156. package/mcp_v2/lib/helpers.js +165 -0
  157. package/mcp_v2/mcp.js +563 -0
  158. package/mcp_v2/package.json +13 -0
  159. package/mcp_v2/schema.sql +88 -0
  160. package/mcp_v2/setup.js +278 -0
  161. package/mcp_v2/tools/build.js +686 -0
  162. package/mcp_v2/tools/config.js +453 -0
  163. package/mcp_v2/tools/conversion.js +799 -0
  164. package/mcp_v2/tools/debug.js +113 -0
  165. package/mcp_v2/tools/knowledge.js +219 -0
  166. package/mcp_v2/tools/sync.js +23 -0
  167. package/mcp_v2/tools/tasks.js +945 -0
  168. package/package.json +11 -15
  169. package/dist/native/androidProject/.idea/.name +0 -1
  170. package/dist/native/androidProject/.idea/AndroidProjectSystem.xml +0 -6
  171. package/dist/native/androidProject/.idea/compiler.xml +0 -6
  172. package/dist/native/androidProject/.idea/deploymentTargetSelector.xml +0 -10
  173. package/dist/native/androidProject/.idea/gradle.xml +0 -18
  174. package/dist/native/androidProject/.idea/migrations.xml +0 -10
  175. package/dist/native/androidProject/.idea/misc.xml +0 -10
  176. package/dist/native/androidProject/.idea/runConfigurations.xml +0 -17
  177. package/dist/native/androidProject/.idea/vcs.xml +0 -6
  178. package/dist/native/iosnativeWebView/iosnativeWebView/NativeBridge.swift +0 -217
  179. package/dist/native/iosnativeWebView/iosnativeWebView/WebView.swift +0 -96
  180. package/dist/native/iosnativeWebView/iosnativeWebView/WebViewModel.swift +0 -47
  181. package/dist/native/iosnativeWebView/iosnativeWebView/WebViewNavigationDelegate.swift +0 -140
  182. package/license +0 -10
@@ -9,12 +9,18 @@ import android.os.Bundle
9
9
  import android.util.Log
10
10
  import android.view.View
11
11
  import android.webkit.*
12
+ import androidx.webkit.ServiceWorkerClientCompat
13
+ import androidx.webkit.ServiceWorkerControllerCompat
14
+ import androidx.webkit.WebResourceRequestCompat
12
15
  import android.widget.ProgressBar
13
16
  import androidx.webkit.WebViewAssetLoader
14
17
  import io.yourname.androidproject.WebCacheManager
15
- import io.yourname.androidproject.isUrlAllowed
16
- import io.yourname.androidproject.isExternalDomain
18
+ import io.yourname.androidproject.URLWhitelistManager
19
+ import io.yourname.androidproject.matchesCachePattern
20
+ import io.yourname.androidproject.utils.CameraUtils
21
+ import io.yourname.androidproject.utils.NetworkUtils
17
22
  import kotlinx.coroutines.*
23
+ import java.io.FileNotFoundException
18
24
  import java.util.Properties
19
25
 
20
26
  class CustomWebView(
@@ -29,6 +35,7 @@ class CustomWebView(
29
35
  override val coroutineContext = Dispatchers.Main + job
30
36
 
31
37
  private var cacheManager: WebCacheManager
38
+ private val metricsMonitor = MetricsMonitor.getInstance(context)
32
39
  private var buildType: String = "debug" // Default to debug
33
40
  private var cachePatterns: List<String> = emptyList()
34
41
  private var isHardwareAccelerationEnabled = false
@@ -39,35 +46,62 @@ class CustomWebView(
39
46
  private lateinit var assetLoader: WebViewAssetLoader
40
47
  private var allowedUrls: List<String> = emptyList()
41
48
  private var accessControlEnabled: Boolean = false
49
+ private val offlineAssetPath = "offline/offline.html"
50
+ private val offlineAssetUrl = "file:///android_asset/$offlineAssetPath"
51
+ private var offlinePageVisible = false
52
+ private var lastTargetUrl: String? = null
53
+ private var defaultRequestHeaders: Map<String, String> = emptyMap()
42
54
 
43
55
  // Counters for asset loading statistics
44
56
  private var assetLoadAttempts = 0
45
57
  private var assetLoadFailures = 0
46
58
 
59
+ // Track ongoing cache fetches to prevent duplicates
60
+ private val ongoingCacheFetches = mutableSetOf<String>()
61
+
47
62
  init {
48
- cacheManager = WebCacheManager(context)
49
63
  setupFromProperties()
64
+ cacheManager = WebCacheManager(context, properties)
50
65
  setupWebView()
51
66
  }
52
67
 
53
68
  private fun setupFromProperties() {
54
69
  buildType = properties.getProperty("buildType", "debug")
55
70
  apiBaseUrl = properties.getProperty("apiBaseUrl", "")
56
- cachePatterns = properties.getProperty("cachePattern", "")
71
+ // Try to get cache patterns from multiple possible property keys
72
+ val cachePatternProperty = properties.getProperty("cachePattern", "")
73
+ .takeIf { it.isNotEmpty() }
74
+ ?: properties.getProperty("android.cachePattern", "")
75
+
76
+ cachePatterns = cachePatternProperty
57
77
  .split(",")
58
78
  .map { it.trim() }
59
79
  .filter { it.isNotEmpty() }
60
80
 
81
+ if (BuildConfig.DEBUG) {
82
+ Log.d(TAG, "📦 Cache patterns loaded: $cachePatterns (count: ${cachePatterns.size})")
83
+ Log.d(TAG, "📦 Raw cachePattern property: '${properties.getProperty("cachePattern", "")}'")
84
+ Log.d(TAG, "📦 Raw android.cachePattern property: '${properties.getProperty("android.cachePattern", "")}'")
85
+ Log.d(TAG, "📦 Selected cache pattern property: '$cachePatternProperty'")
86
+ }
87
+
61
88
  // Parse buildOptimisation property
62
89
  buildOptimisation = properties.getProperty("buildOptimisation", "false").toBoolean()
63
90
 
64
- // Load access control settings from properties
65
- accessControlEnabled = properties.getProperty("accessControl.enabled", "false").toBoolean()
91
+ // Load allowed URLs from properties
66
92
  allowedUrls = properties.getProperty("accessControl.allowedUrls", "")
67
93
  .split(",")
68
94
  .map { it.trim() }
69
95
  .filter { it.isNotEmpty() }
70
96
 
97
+ // Access control toggle
98
+ accessControlEnabled = properties
99
+ .getProperty("accessControl.enabled", "true")
100
+ .equals("true", ignoreCase = true)
101
+
102
+ // Initialize URLWhitelistManager with access control configuration
103
+ URLWhitelistManager.initialize(accessControlEnabled, allowedUrls)
104
+
71
105
  // Set initial flags based on buildOptimisation
72
106
  if (buildOptimisation) {
73
107
  isInitialApiCalled = false
@@ -96,10 +130,52 @@ class CustomWebView(
96
130
  .build()
97
131
  }
98
132
 
133
+ fun setDefaultRequestHeaders(headers: Map<String, String>) {
134
+ defaultRequestHeaders = headers.toMap()
135
+ }
136
+
137
+ private fun loadUrlInternal(url: String) {
138
+ val isNetworkUrl = url.startsWith("http://") || url.startsWith("https://")
139
+
140
+ if (isNetworkUrl && defaultRequestHeaders.isNotEmpty()) {
141
+ if (BuildConfig.DEBUG) {
142
+ Log.d(TAG, "🌐 Loading with headers: $url, headers=$defaultRequestHeaders")
143
+ }
144
+ webView.loadUrl(url, defaultRequestHeaders)
145
+ } else {
146
+ webView.loadUrl(url)
147
+ }
148
+ }
149
+
99
150
  fun loadUrl(url: String) {
100
- webView.loadUrl(url)
151
+ lastTargetUrl = url
152
+ offlinePageVisible = false
153
+ loadUrlInternal(url)
101
154
  }
102
155
 
156
+ fun updateLastTargetUrl(url: String) {
157
+ lastTargetUrl = url
158
+ }
159
+
160
+ fun showOfflinePage() {
161
+ if (offlinePageVisible) {
162
+ return
163
+ }
164
+
165
+ // Verify offline asset exists to avoid crash
166
+ try {
167
+ context.assets.open(offlineAssetPath).close()
168
+ offlinePageVisible = true
169
+ loadUrlInternal(offlineAssetUrl)
170
+ if (BuildConfig.DEBUG) {
171
+ Log.d(TAG, "📴 Showing offline page from assets: $offlineAssetPath")
172
+ }
173
+ } catch (e: Exception) {
174
+ Log.e(TAG, "❌ offline.html not found in assets, cannot show offline page", e)
175
+ }
176
+ }
177
+
178
+
103
179
  fun saveState(outState: Bundle) {
104
180
  webView.saveState(outState)
105
181
  }
@@ -113,6 +189,10 @@ class CustomWebView(
113
189
  webView.clearHistory()
114
190
  }
115
191
 
192
+ fun clearAllCache() {
193
+ cacheManager.clearAll()
194
+ }
195
+
116
196
  fun canGoBack(): Boolean = webView.canGoBack()
117
197
 
118
198
  fun goBack() {
@@ -131,20 +211,35 @@ class CustomWebView(
131
211
  return webView
132
212
  }
133
213
 
134
- @SuppressLint("JavascriptInterface")
135
214
  fun addJavascriptInterface(obj: Any, name: String) {
136
- // Check if we're on API 17 or higher
137
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
138
- // Verify that the object has at least one method annotated with @JavascriptInterface
139
- val hasAnnotatedMethods = obj.javaClass.declaredMethods.any {
140
- it.isAnnotationPresent(android.webkit.JavascriptInterface::class.java)
141
- }
215
+ // Minimum API level check - JavascriptInterface annotation is safe from API 17+
216
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
217
+ Log.e(TAG, "❌ JavaScript interfaces are not secure on API level < 17. Refusing to add interface: $name")
218
+ return
219
+ }
142
220
 
143
- if (!hasAnnotatedMethods) {
144
- Log.e(TAG, "Error: No methods in ${obj.javaClass.simpleName} are annotated with @JavascriptInterface")
145
- }
221
+ // Security: Verify that the object has at least one method annotated with @JavascriptInterface
222
+ // Check both declared methods and inherited methods, excluding Object class methods
223
+ val allMethods = obj.javaClass.methods.filter {
224
+ it.declaringClass != Object::class.java
225
+ }
226
+ val hasAnnotatedMethods = allMethods.any {
227
+ it.isAnnotationPresent(android.webkit.JavascriptInterface::class.java)
228
+ }
229
+
230
+ if (!hasAnnotatedMethods) {
231
+ Log.e(TAG, "❌ Security: No methods in ${obj.javaClass.simpleName} are annotated with @JavascriptInterface. Refusing to add interface.")
232
+ return
146
233
  }
147
234
 
235
+ // Additional security check: only allow whitelisted interface names
236
+ val allowedInterfaces = setOf("NativeBridge", "AndroidBridge")
237
+ if (name !in allowedInterfaces) {
238
+ Log.e(TAG, "❌ Security: Interface name '$name' is not in whitelist. Refusing to add interface.")
239
+ return
240
+ }
241
+
242
+ @Suppress("JavascriptInterface")
148
243
  webView.addJavascriptInterface(obj, name)
149
244
  if (BuildConfig.DEBUG) {
150
245
  Log.d(TAG, "🔗 Added JavaScript interface: $name")
@@ -198,21 +293,108 @@ class CustomWebView(
198
293
  }
199
294
 
200
295
  private fun shouldCacheUrl(url: String): Boolean {
201
- if (cachePatterns.isEmpty()) return false
202
-
203
- fun String.wildcardToRegex(): String {
204
- return this.replace(".", "\\.")
205
- .replace("*", ".*")
206
- .let { "^$it$" }
296
+ if (BuildConfig.DEBUG) {
297
+ Log.d(TAG, "🔍 CACHE_CHECK: Checking if URL should be cached: $url")
298
+ Log.d(TAG, "🔍 CACHE_CHECK: Cache patterns: $cachePatterns")
207
299
  }
208
300
 
209
- return cachePatterns.any { pattern ->
210
- val regex = pattern.wildcardToRegex().toRegex(RegexOption.IGNORE_CASE)
211
- regex.matches(url) || url.endsWith(pattern.removePrefix("*"))
301
+ val shouldCache = matchesCachePattern(url, cachePatterns)
302
+
303
+ if (BuildConfig.DEBUG) {
304
+ if (shouldCache) {
305
+ Log.d(TAG, "✅ CACHE_CHECK: URL WILL be cached: $url")
306
+ } else {
307
+ Log.d(TAG, "❌ CACHE_CHECK: URL will NOT be cached (no pattern match): $url")
308
+ }
212
309
  }
310
+
311
+ return shouldCache
213
312
  }
214
313
 
314
+ /**
315
+ * Handle special URL schemes (tel:, mailto:, sms:)
316
+ */
317
+ private fun handleSpecialScheme(url: String): Boolean {
318
+ val uri = Uri.parse(url)
319
+ val scheme = uri.scheme?.lowercase() ?: return false
320
+
321
+ // Handle offline retry scheme
322
+ if (scheme == "catalyst" && (uri.host?.lowercase() == "retry" || uri.schemeSpecificPart?.lowercase() == "retry")) {
323
+ val status = NetworkUtils.getCurrentStatus(context)
324
+ if (status.isOnline) {
325
+ val target = lastTargetUrl
326
+ if (target != null) {
327
+ if (BuildConfig.DEBUG) {
328
+ Log.d(TAG, "🔄 Retry requested, online. Reloading: $target")
329
+ }
330
+ offlinePageVisible = false
331
+ lastTargetUrl = target
332
+ loadUrlInternal(target)
333
+ } else if (BuildConfig.DEBUG) {
334
+ Log.w(TAG, "🔄 Retry requested but no target URL is known")
335
+ }
336
+ } else if (BuildConfig.DEBUG) {
337
+ Log.d(TAG, "🔄 Retry requested but still offline; staying on offline page")
338
+ }
339
+ return true
340
+ }
341
+
342
+ // Only handle tel, mailto, sms
343
+ if (scheme !in listOf("tel", "mailto", "sms")) {
344
+ return false
345
+ }
215
346
 
347
+ try {
348
+ val intent = when (scheme) {
349
+ "tel" -> Intent(Intent.ACTION_DIAL, uri)
350
+ "mailto" -> Intent(Intent.ACTION_SENDTO, uri)
351
+ "sms" -> Intent(Intent.ACTION_VIEW, uri)
352
+ else -> return false
353
+ }
354
+
355
+ // For mailto, check if any apps can handle it before showing chooser
356
+ if (scheme == "mailto") {
357
+ val packageManager = context.packageManager
358
+ val activities = packageManager.queryIntentActivities(intent, 0)
359
+
360
+ if (activities.isNotEmpty()) {
361
+ // Apps available - show chooser
362
+ val chooser = Intent.createChooser(intent, "Send email")
363
+ chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
364
+ context.startActivity(chooser)
365
+ } else {
366
+ // No email apps - try Gmail app, then browser
367
+ try {
368
+ val gmailIntent = Intent(Intent.ACTION_SENDTO, uri).apply {
369
+ setPackage("com.google.android.gm")
370
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
371
+ }
372
+ context.startActivity(gmailIntent)
373
+ if (BuildConfig.DEBUG) {
374
+ Log.d(TAG, "Opened Gmail app")
375
+ }
376
+ } catch (e: android.content.ActivityNotFoundException) {
377
+ // Gmail app not available, open in browser
378
+ openInInAppBrowser("https://mail.google.com")
379
+ if (BuildConfig.DEBUG) {
380
+ Log.d(TAG, "Opened Gmail in browser")
381
+ }
382
+ }
383
+ }
384
+ } else {
385
+ context.startActivity(intent)
386
+ }
387
+ return true
388
+ } catch (e: android.content.ActivityNotFoundException) {
389
+ if (BuildConfig.DEBUG) {
390
+ Log.d(TAG, "No app to handle $scheme")
391
+ }
392
+ return true
393
+ } catch (e: Exception) {
394
+ Log.e(TAG, "Error handling $scheme: ${e.message}")
395
+ return true
396
+ }
397
+ }
216
398
 
217
399
  private fun openInInAppBrowser(url: String) {
218
400
  try {
@@ -253,10 +435,11 @@ class CustomWebView(
253
435
  // Remove leading slash if present
254
436
  var cleanPath = if (path.startsWith("/")) path.substring(1) else path
255
437
 
438
+ // TODO: Add HTML file workflow - index.html not available yet
256
439
  // If path is empty or just "/", return index.html
257
- if (cleanPath.isEmpty()) {
258
- return "build/public/index.html"
259
- }
440
+ // if (cleanPath.isEmpty()) {
441
+ // return "build/public/index.html"
442
+ // }
260
443
 
261
444
  // Special case for favicon.ico which might be in root
262
445
  if (cleanPath == "favicon.ico") {
@@ -289,6 +472,120 @@ class CustomWebView(
289
472
  }
290
473
  }
291
474
 
475
+ private fun setupServiceWorker() {
476
+ try {
477
+ val serviceWorkerController = ServiceWorkerControllerCompat.getInstance()
478
+ serviceWorkerController.setServiceWorkerClient(object : ServiceWorkerClientCompat() {
479
+ override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
480
+ val url = request.url.toString()
481
+
482
+ if (BuildConfig.DEBUG) {
483
+ Log.d(TAG, "🔧 SERVICE_WORKER: Intercepting request: $url")
484
+ Log.d(TAG, "🔧 SERVICE_WORKER: Method: ${request.method}")
485
+ Log.d(TAG, "🔧 SERVICE_WORKER: Headers: ${request.requestHeaders}")
486
+ }
487
+
488
+ // Check if URL is allowed
489
+ if (!URLWhitelistManager.isUrlAllowed(url)) {
490
+ if (BuildConfig.DEBUG) {
491
+ Log.w(TAG, "🙫 SERVICE_WORKER: Request blocked by access control: $url")
492
+ }
493
+ return WebResourceResponse("text/plain", "utf-8", null)
494
+ }
495
+
496
+ // Let API calls go through normally (skip caching)
497
+ if (isApiCall(url)) {
498
+ if (BuildConfig.DEBUG) {
499
+ Log.d(TAG, "🌐 SERVICE_WORKER: API call detected, skipping cache: $url")
500
+ }
501
+ return null
502
+ }
503
+
504
+ // Check if this should be cached
505
+ if (request.method == "GET" && shouldCacheUrl(url)) {
506
+ if (BuildConfig.DEBUG) {
507
+ Log.d(TAG, "🎯 SERVICE_WORKER: Using cache system for: $url")
508
+ }
509
+
510
+ return try {
511
+ // Use synchronous cache check to avoid blocking UI thread
512
+ val headers = request.requestHeaders.toMutableMap().apply {
513
+ if (!containsKey("Cache-Control")) {
514
+ put("Cache-Control", "max-age=86400")
515
+ }
516
+ }
517
+
518
+ // Get cached response synchronously (non-blocking for ServiceWorker)
519
+ val response = cacheManager.getCachedResponseSync(url, headers)
520
+ if (response != null) {
521
+ if (BuildConfig.DEBUG) {
522
+ Log.d(TAG, "✅ SERVICE_WORKER: Served from cache: $url")
523
+ }
524
+ response
525
+ } else {
526
+ if (BuildConfig.DEBUG) {
527
+ Log.d(TAG, "❌ SERVICE_WORKER: Cache miss: $url")
528
+ }
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
+ null
563
+ }
564
+ } catch (e: Exception) {
565
+ if (BuildConfig.DEBUG) {
566
+ Log.e(TAG, "❌ SERVICE_WORKER: Error processing cache request: ${e.message}")
567
+ }
568
+ null
569
+ }
570
+ } else {
571
+ if (BuildConfig.DEBUG) {
572
+ Log.d(TAG, "⏭️ SERVICE_WORKER: Skipping cache for: $url")
573
+ Log.d(TAG, "⏭️ SERVICE_WORKER: Method: ${request.method}, shouldCache: ${shouldCacheUrl(url)}")
574
+ }
575
+ }
576
+
577
+ return null // Let default handling proceed
578
+ }
579
+ })
580
+
581
+ if (BuildConfig.DEBUG) {
582
+ Log.d(TAG, "✅ ServiceWorker intercept configured successfully")
583
+ }
584
+ } catch (e: Exception) {
585
+ Log.e(TAG, "❌ Failed to setup ServiceWorker: ${e.message}")
586
+ }
587
+ }
588
+
292
589
  @SuppressLint("SetJavaScriptEnabled")
293
590
  private fun setupWebView() {
294
591
  if (BuildConfig.DEBUG) {
@@ -304,45 +601,54 @@ class CustomWebView(
304
601
  WebSettings.MIXED_CONTENT_NEVER_ALLOW
305
602
  }
306
603
 
307
- setRenderPriority(WebSettings.RenderPriority.HIGH)
308
604
  cacheMode = WebSettings.LOAD_DEFAULT
309
605
  databaseEnabled = true
310
606
  domStorageEnabled = true
311
607
  allowFileAccess = true
312
608
  allowContentAccess = true
313
- setEnableSmoothTransition(true)
314
609
 
315
- // Allow access to file URLs and JavaScript interfaces - restrict in production if possible
316
- allowFileAccessFromFileURLs = BuildConfig.DEBUG
317
- allowUniversalAccessFromFileURLs = BuildConfig.DEBUG
610
+ // TODO: Enable these when build optimization feature is implemented
611
+ // These are deprecated but may be needed for local file access in development
612
+ // allowFileAccessFromFileURLs = BuildConfig.DEBUG
613
+ // allowUniversalAccessFromFileURLs = BuildConfig.DEBUG
318
614
  }
319
615
 
616
+ // Setup ServiceWorker to intercept requests that bypass normal WebView intercept
617
+ setupServiceWorker()
618
+
320
619
  webView.webViewClient = object : WebViewClient() {
321
620
  override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
322
621
  request?.url?.let { url ->
323
622
  val urlString = url.toString()
324
-
325
- if (accessControlEnabled) {
326
- // Check if URL is an external domain
327
- if (url.scheme in listOf("http", "https") && isExternalDomain(urlString, allowedUrls)) {
328
- if (BuildConfig.DEBUG) {
329
- Log.d(TAG, "🌍 External domain detected, opening in in-app browser: $urlString")
330
- }
331
- openInInAppBrowser(urlString)
332
- return true
623
+
624
+ // Handle special URL schemes (tel:, mailto:, sms:) HERE to prevent page loading
625
+ if (handleSpecialScheme(urlString)) {
626
+ if (BuildConfig.DEBUG) {
627
+ Log.d(TAG, "📞 Special scheme handled in shouldOverrideUrlLoading: $urlString")
333
628
  }
334
-
335
- // Check if URL is allowed for internal navigation
336
- if (!isUrlAllowed(urlString, allowedUrls)) {
337
- if (BuildConfig.DEBUG) {
338
- Log.w(TAG, "🚫 URL blocked by access control: $urlString")
339
- }
340
- return true
629
+ return true // Prevent WebView from trying to load the URL
630
+ }
631
+
632
+ // Check if URL is an external domain
633
+ if (url.scheme in listOf("http", "https") && URLWhitelistManager.isExternalDomain(urlString)) {
634
+ if (BuildConfig.DEBUG) {
635
+ Log.d(TAG, "🌍 External domain detected, opening in in-app browser: $urlString")
636
+ }
637
+ openInInAppBrowser(urlString)
638
+ return true
639
+ }
640
+
641
+ // Check if URL is allowed for internal navigation
642
+ if (!URLWhitelistManager.isUrlAllowed(urlString)) {
643
+ if (BuildConfig.DEBUG) {
644
+ Log.w(TAG, "🚫 URL blocked by access control: $urlString")
341
645
  }
646
+ return true
342
647
  }
343
-
648
+
344
649
  // Let WebView handle loading non-API HTTP/HTTPS URLs
345
650
  if (url.scheme in listOf("http", "https")) {
651
+ lastTargetUrl = urlString
346
652
  return false
347
653
  }
348
654
  }
@@ -354,118 +660,218 @@ class CustomWebView(
354
660
  request: WebResourceRequest
355
661
  ): WebResourceResponse? {
356
662
  val url = request.url.toString()
663
+
664
+ // Track all network requests
665
+ metricsMonitor.recordNetworkRequest(url)
666
+
357
667
  if (BuildConfig.DEBUG) {
358
- Log.d(TAG, "🔄 Intercepting request for: $url on thread: ${Thread.currentThread().name}")
668
+ Log.d(TAG, "🔄 INTERCEPT: URL: $url")
669
+ Log.d(TAG, "🔄 INTERCEPT: Method: ${request.method}")
670
+ Log.d(TAG, "🔄 INTERCEPT: Thread: ${Thread.currentThread().name}")
671
+ Log.d(TAG, "🔄 INTERCEPT: File extension: ${url.substringAfterLast('.', "none")}")
672
+ Log.d(TAG, "🔄 INTERCEPT: isInitialPageLoaded: $isInitialPageLoaded")
359
673
  }
360
674
 
361
- if (accessControlEnabled && !isUrlAllowed(url, allowedUrls)) {
675
+ if (!URLWhitelistManager.isUrlAllowed(url)) {
362
676
  if (BuildConfig.DEBUG) {
363
- Log.w(TAG, "🚫 Network request blocked by access control: $url")
677
+ Log.w(TAG, "🚫 STEP_1_FAIL: Network request blocked by access control: $url")
364
678
  }
365
679
  // Return an empty response to block the request
366
680
  return WebResourceResponse("text/plain", "utf-8", null)
681
+ } else if (BuildConfig.DEBUG) {
682
+ if (URLWhitelistManager.isAccessControlEnabled()) {
683
+ Log.d(TAG, "✅ STEP_1_PASS: URL allowed by access control: $url")
684
+ } else {
685
+ Log.d(TAG, "⚙️ STEP_1_SKIP: Access control disabled, allowing URL: $url")
686
+ }
367
687
  }
368
688
 
689
+ // TODO: Add HTML file workflow - index.html not available yet
369
690
  // Handle the initial route request - intercept first request regardless of host
370
- if (!isInitialApiCalled && request.method == "GET") {
371
- isInitialApiCalled = true
691
+ // if (!isInitialApiCalled && request.method == "GET") {
692
+ // isInitialApiCalled = true
693
+ // if (BuildConfig.DEBUG) {
694
+ // Log.d(TAG, "📄 STEP_2_INITIAL: Serving initial index.html from assets for route: $url")
695
+ // Log.d(TAG, "📄 STEP_2_INITIAL: This request will NOT go through cache system")
696
+ // }
697
+ //
698
+ // try {
699
+ // val indexHtml = context.assets.open("build/public/index.html")
700
+ // return WebResourceResponse(
701
+ // "text/html",
702
+ // "utf-8",
703
+ // indexHtml
704
+ // )
705
+ // } catch (e: Exception) {
706
+ // Log.e(TAG, "❌ Error loading index.html from assets", e)
707
+ // }
708
+ // }
709
+
710
+ // Let API calls go through normally (before any other checks)
711
+ if (isApiCall(url)) {
372
712
  if (BuildConfig.DEBUG) {
373
- Log.d(TAG, "📄 Serving initial index.html from assets for route: $url")
374
- }
375
-
376
- try {
377
- val indexHtml = context.assets.open("build/public/index.html")
378
- return WebResourceResponse(
379
- "text/html",
380
- "utf-8",
381
- indexHtml
382
- )
383
- } catch (e: Exception) {
384
- Log.e(TAG, "❌ Error loading index.html from assets", e)
713
+ Log.d(TAG, "🌐 STEP_3_API: API call detected, letting it go through network: $url")
714
+ Log.d(TAG, "🌐 STEP_3_API: This request will NOT go through cache or asset system")
385
715
  }
716
+ return null
386
717
  }
387
718
 
388
719
  if(!isInitialPageLoaded) {
389
- // Check if this is a static resource that should be loaded from assets
390
- if (request.method == "GET" && isStaticResourceRequest(url)) {
720
+ if (BuildConfig.DEBUG) {
721
+ Log.d(TAG, "🔍 STEP_4_CHECK: Initial page not loaded yet, checking for static resources")
722
+ Log.d(TAG, "🔍 STEP_4_CHECK: isStaticResourceRequest($url) = ${isStaticResourceRequest(url)}")
723
+ Log.d(TAG, "🔍 STEP_4_CHECK: shouldCacheUrl($url) = ${shouldCacheUrl(url)}")
724
+ }
725
+
726
+ // IMPORTANT: Skip asset loading if the URL matches a cache pattern
727
+ // This allows cached resources to be served from the cache system instead
728
+ if (request.method == "GET" && isStaticResourceRequest(url) && !shouldCacheUrl(url)) {
391
729
  val assetPath = extractAssetPath(url)
392
730
  assetLoadAttempts++
393
731
 
394
732
  try {
395
733
  val mimeType = getMimeType(assetPath)
396
734
  if (BuildConfig.DEBUG) {
397
- Log.d(TAG, "📦 Attempting to serve from assets: $assetPath")
735
+ Log.d(TAG, "📦 Attempting to serve from assets: $assetPath (not in cache patterns)")
398
736
  }
399
737
  val inputStream = context.assets.open(assetPath)
400
738
  if (BuildConfig.DEBUG) {
401
739
  Log.d(TAG, "✅ Successfully loaded from assets: $assetPath")
402
740
  }
403
741
  return WebResourceResponse(mimeType, "utf-8", inputStream)
742
+ } catch (e: FileNotFoundException) {
743
+ assetLoadFailures++
744
+ if (BuildConfig.DEBUG) {
745
+ Log.w(TAG, "📁 Asset not found: $assetPath - falling through to cache check")
746
+ }
747
+ // Asset not found - fall through to cache check below
748
+ } catch (e: SecurityException) {
749
+ assetLoadFailures++
750
+ Log.e(TAG, "🚫 Security error loading asset: $assetPath", e)
751
+ return WebResourceResponse("text/plain", "utf-8", null)
752
+ } catch (e: OutOfMemoryError) {
753
+ assetLoadFailures++
754
+ Log.e(TAG, "📢 Out of memory loading asset: $assetPath", e)
755
+ // Try to free some memory
756
+ System.gc()
757
+ return WebResourceResponse("text/plain", "utf-8", null)
404
758
  } catch (e: Exception) {
405
759
  assetLoadFailures++
406
760
  if (BuildConfig.DEBUG) {
407
- Log.e(TAG, "❌ Error loading asset: $assetPath", e)
408
- Log.d(TAG, "⚠️ Falling back to network for: $url")
761
+ Log.e(TAG, "❌ Unexpected error loading asset: $assetPath", e)
762
+ Log.d(TAG, "⚠️ Falling through to cache check for: $url")
409
763
  }
410
- // Return null to let the WebView load from network
411
- return null
764
+ // Unexpected error - fall through to cache check below
412
765
  }
413
- }
414
-
415
- // Let API calls go through normally
416
- if (isApiCall(url)) {
766
+ } else if (shouldCacheUrl(url)) {
417
767
  if (BuildConfig.DEBUG) {
418
- Log.d(TAG, "🌐 API call detected, letting it go through network: $url")
768
+ Log.d(TAG, "⏭️ STEP_4_SKIP: Skipping asset loading for cached URL: $url")
419
769
  }
420
- return null
770
+ }
771
+ } else {
772
+ if (BuildConfig.DEBUG) {
773
+ Log.d(TAG, "✅ STEP_3_PASS: Initial page loaded, skipping asset loading logic")
421
774
  }
422
775
  }
423
776
 
424
777
  // For non-API HTTP requests that match cache patterns, use cache system
778
+ if (BuildConfig.DEBUG) {
779
+ Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Evaluating cache conditions")
780
+ Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Request method: ${request.method} (need GET)")
781
+ Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: shouldCacheUrl($url) = ${shouldCacheUrl(url)}")
782
+ }
783
+
425
784
  if (request.method == "GET" && shouldCacheUrl(url)) {
426
- return runBlocking {
427
- if (BuildConfig.DEBUG) {
428
- Log.d(TAG, "⚙️ Processing cacheable request in coroutine on thread: ${Thread.currentThread().name}")
429
- }
430
- val startTime = System.currentTimeMillis()
431
- disableHardwareAcceleration()
432
- try {
433
- val headers = request.requestHeaders.toMutableMap().apply {
434
- if (!containsKey("Cache-Control")) {
435
- put("Cache-Control", "max-age=86400")
436
- }
437
- if (!containsKey("Pragma")) {
438
- put("Pragma", "cache")
439
- }
785
+ // Track that this request was evaluated for caching
786
+ metricsMonitor.recordCacheEvaluation(url)
787
+ if (BuildConfig.DEBUG) {
788
+ Log.d(TAG, "🎯 STEP_5_SUCCESS: Using cache system for URL: $url")
789
+ Log.d(TAG, "🎯 STEP_5_SUCCESS: Request method: ${request.method}")
790
+ Log.d(TAG, "🎯 STEP_5_SUCCESS: isInitialPageLoaded: $isInitialPageLoaded")
791
+ }
792
+
793
+ val startTime = System.currentTimeMillis()
794
+ disableHardwareAcceleration()
795
+
796
+ try {
797
+ val headers = request.requestHeaders.toMutableMap().apply {
798
+ if (!containsKey("Cache-Control")) {
799
+ put("Cache-Control", "max-age=86400")
800
+ }
801
+ if (!containsKey("Pragma")) {
802
+ put("Pragma", "cache")
440
803
  }
804
+ }
441
805
 
442
- var response = cacheManager.getCachedResponse(url, headers)
806
+ // Use synchronous cache check to avoid blocking
807
+ val response = cacheManager.getCachedResponseSync(url, headers)
443
808
 
444
- if (response != null) {
445
- if (BuildConfig.DEBUG) {
446
- val duration = System.currentTimeMillis() - startTime
447
- Log.d(TAG, "✅ Served from cache in ${duration}ms: $url")
448
- }
449
- response
450
- } else {
451
- if (BuildConfig.DEBUG) {
452
- Log.d(TAG, "❌ Cache miss for: $url")
453
- }
454
- null
809
+ if (response != null) {
810
+ if (BuildConfig.DEBUG) {
811
+ val duration = System.currentTimeMillis() - startTime
812
+ Log.d(TAG, "✅ WEBVIEW_CLIENT: Served from cache in ${duration}ms: $url")
455
813
  }
456
- } catch (e: Exception) {
457
- Log.e(TAG, "Error processing request for URL: $url: ${e.message}")
814
+ metricsMonitor.recordCacheHit(url)
815
+ return response
816
+ } else {
458
817
  if (BuildConfig.DEBUG) {
459
- e.printStackTrace()
818
+ Log.d(TAG, "❌ WEBVIEW_CLIENT: Cache miss, triggering async fetch: $url")
460
819
  }
461
- null
462
- } finally {
463
- enableHardwareAcceleration()
820
+ metricsMonitor.recordCacheMiss(url)
821
+
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
+ // Return null to let network handle this request
847
+ return null
464
848
  }
849
+ } catch (e: OutOfMemoryError) {
850
+ Log.e(TAG, "📢 Out of memory processing cache request: $url", e)
851
+ metricsMonitor.recordCacheMiss(url)
852
+ System.gc()
853
+ return null
854
+ } catch (e: SecurityException) {
855
+ Log.e(TAG, "🚫 Security error in cache processing: $url", e)
856
+ metricsMonitor.recordCacheMiss(url)
857
+ return null
858
+ } catch (e: Exception) {
859
+ Log.e(TAG, "❌ Unexpected error processing cache request: $url: ${e.message}")
860
+ if (BuildConfig.DEBUG) {
861
+ e.printStackTrace()
862
+ }
863
+ metricsMonitor.recordCacheMiss(url)
864
+ return null
865
+ } finally {
866
+ enableHardwareAcceleration()
465
867
  }
466
868
  } else {
467
869
  if (BuildConfig.DEBUG) {
468
- Log.d(TAG, "⏭️ URL doesn't match cache criteria, skipping cache: $url")
870
+ Log.d(TAG, "❌ STEP_5_FAIL: URL doesn't match cache criteria, skipping cache")
871
+ Log.d(TAG, "❌ STEP_5_FAIL: URL: $url")
872
+ Log.d(TAG, "❌ STEP_5_FAIL: Request method: ${request.method} (need GET)")
873
+ Log.d(TAG, "❌ STEP_5_FAIL: shouldCacheUrl result: ${shouldCacheUrl(url)}")
874
+ Log.d(TAG, "❌ STEP_5_FAIL: This request will go through normal network loading")
469
875
  }
470
876
  }
471
877
 
@@ -474,9 +880,13 @@ class CustomWebView(
474
880
 
475
881
  override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
476
882
  super.onPageStarted(view, url, favicon)
883
+ if (url != null && url != offlineAssetUrl) {
884
+ offlinePageVisible = false
885
+ }
477
886
  progressBar.visibility = View.VISIBLE
478
887
  val startTime = System.currentTimeMillis()
479
888
  view?.tag = startTime // Store start time for performance tracking
889
+ url?.let { metricsMonitor.trackPageLoadStart(it) }
480
890
  if (BuildConfig.DEBUG) {
481
891
  Log.d(TAG, "⏳ Page load started for: $url - Hardware Acceleration: $isHardwareAccelerationEnabled")
482
892
  }
@@ -487,11 +897,26 @@ class CustomWebView(
487
897
  if(!isInitialPageLoaded){
488
898
  isInitialPageLoaded = true
489
899
  }
900
+ url?.let { metricsMonitor.trackPageLoadEnd(it) }
490
901
  if (BuildConfig.DEBUG) {
491
902
  val startTime = view?.tag as? Long ?: return
492
903
  val loadTime = System.currentTimeMillis() - startTime
493
904
  Log.d(TAG, "✅ Page load finished for: $url - Load time: ${loadTime}ms - Hardware Acceleration: $isHardwareAccelerationEnabled")
494
905
  Log.d(TAG, "📊 Asset loading stats: Attempted: $assetLoadAttempts, Failed: $assetLoadFailures (${String.format("%.1f", assetLoadFailures * 100.0 / assetLoadAttempts.coerceAtLeast(1))}%)")
906
+ Log.d(TAG, "📊 ${metricsMonitor.getCacheStats()}")
907
+
908
+ // Log files served from cache
909
+ val cachedFiles = metricsMonitor.getFilesServedFromCache()
910
+ if (cachedFiles.isNotEmpty()) {
911
+ Log.d(TAG, "📁 FILES_FROM_CACHE: ${cachedFiles.size} files served from cache on this page:")
912
+ cachedFiles.takeLast(5).forEach { cachedUrl ->
913
+ Log.d(TAG, "📁 ✅ ${cachedUrl.substringAfterLast("/")}")
914
+ }
915
+ }
916
+
917
+ // Log brief performance summary
918
+ Log.d(TAG, "📊 PERFORMANCE_SUMMARY: Page finished, showing current metrics")
919
+ metricsMonitor.logAllMetrics()
495
920
  }
496
921
  super.onPageFinished(view, url)
497
922
  }
@@ -499,18 +924,45 @@ class CustomWebView(
499
924
  override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
500
925
  if (error != null) {
501
926
  Log.e(TAG, "❌ Error loading ${request?.url}: ${error.errorCode} ${error.description}")
502
-
503
- // Only show a fallback page for main frame errors in production
504
- if (request?.isForMainFrame == true && !BuildConfig.DEBUG) {
505
- try {
506
- view?.loadUrl("file:///android_asset/build/public/error.html")
507
- } catch (e: Exception) {
508
- Log.e(TAG, "Failed to load error page: ${e.message}")
509
- }
510
- }
511
927
  }
512
928
  super.onReceivedError(view, request, error)
513
929
  }
930
+
931
+ override fun onReceivedSslError(view: WebView?, handler: android.webkit.SslErrorHandler?, error: android.net.http.SslError?) {
932
+ if (error != null) {
933
+ val errorMsg = when (error.primaryError) {
934
+ android.net.http.SslError.SSL_EXPIRED -> "Certificate expired"
935
+ android.net.http.SslError.SSL_IDMISMATCH -> "Certificate hostname mismatch"
936
+ android.net.http.SslError.SSL_NOTYETVALID -> "Certificate not yet valid"
937
+ android.net.http.SslError.SSL_UNTRUSTED -> "Certificate authority not trusted"
938
+ android.net.http.SslError.SSL_DATE_INVALID -> "Certificate date invalid"
939
+ android.net.http.SslError.SSL_INVALID -> "Certificate invalid"
940
+ else -> "Unknown SSL error"
941
+ }
942
+
943
+ Log.e(TAG, "🔒 SSL Error for ${error.url}: $errorMsg")
944
+
945
+ // NEVER proceed on SSL errors in production
946
+ // Only allow in debug mode for development/testing with local certificates
947
+ if (BuildConfig.DEBUG) {
948
+ Log.w(TAG, "⚠️ DEBUG MODE: Ignoring SSL error - DO NOT DO THIS IN PRODUCTION!")
949
+ handler?.proceed()
950
+ } else {
951
+ // Cancel the request in production
952
+ handler?.cancel()
953
+
954
+ // TODO: Add HTML file workflow - error.html not available yet
955
+ // Show error page for main frame SSL errors
956
+ // try {
957
+ // view?.loadUrl("file:///android_asset/build/public/error.html")
958
+ // } catch (e: Exception) {
959
+ // Log.e(TAG, "Failed to load SSL error page: ${e.message}")
960
+ // }
961
+ }
962
+ } else {
963
+ handler?.cancel()
964
+ }
965
+ }
514
966
  }
515
967
 
516
968
  webView.webChromeClient = object : WebChromeClient() {
@@ -527,9 +979,38 @@ class CustomWebView(
527
979
  }
528
980
  return true
529
981
  }
982
+
983
+ override fun onPermissionRequest(request: PermissionRequest) {
984
+ if (BuildConfig.DEBUG) {
985
+ Log.d(TAG, "Permission request from web content: ${request.resources?.joinToString()}")
986
+ }
987
+
988
+ // Check if camera permission is being requested
989
+ val cameraRequested = request.resources?.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE) == true
990
+
991
+ if (cameraRequested) {
992
+ // Check if native app has camera permission
993
+ if (CameraUtils.hasCameraPermission(context as android.app.Activity)) {
994
+ if (BuildConfig.DEBUG) {
995
+ Log.d(TAG, "Granting camera permission to WebView - native permission available")
996
+ }
997
+ request.grant(request.resources)
998
+ } else {
999
+ if (BuildConfig.DEBUG) {
1000
+ Log.d(TAG, "Denying camera permission to WebView - native permission not granted")
1001
+ }
1002
+ request.deny()
1003
+ }
1004
+ } else {
1005
+ if (BuildConfig.DEBUG) {
1006
+ Log.d(TAG, "Denying non-camera permission request: ${request.resources?.joinToString()}")
1007
+ }
1008
+ request.deny()
1009
+ }
1010
+ }
530
1011
  }
531
1012
 
532
1013
  // Only enable WebView debugging in debug builds
533
1014
  WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
534
1015
  }
535
- }
1016
+ }