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
@@ -0,0 +1,111 @@
1
+ package io.yourname.androidproject
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.graphics.Color
6
+ import android.os.Build
7
+ import android.os.Bundle
8
+ import android.os.Handler
9
+ import android.os.Looper
10
+ import android.util.TypedValue
11
+ import androidx.appcompat.app.AppCompatActivity
12
+ import androidx.core.content.ContextCompat
13
+ import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
14
+ import androidx.core.graphics.drawable.toBitmap
15
+ import io.yourname.androidproject.databinding.ActivitySplashBinding
16
+ import java.util.Properties
17
+
18
+ class SplashActivity : AppCompatActivity() {
19
+
20
+ private lateinit var binding: ActivitySplashBinding
21
+ private lateinit var properties: Properties
22
+ private var isFinishing = false
23
+
24
+ override fun onCreate(savedInstanceState: Bundle?) {
25
+ super.onCreate(savedInstanceState)
26
+
27
+ properties = Properties()
28
+ try {
29
+ assets.open("webview_config.properties").use { properties.load(it) }
30
+ } catch (e: Exception) {
31
+ startMainActivity()
32
+ return
33
+ }
34
+
35
+ if (!properties.getProperty("splashScreen.enabled", "false").toBoolean()) {
36
+ startMainActivity()
37
+ return
38
+ }
39
+
40
+ binding = ActivitySplashBinding.inflate(layoutInflater)
41
+ setContentView(binding.root)
42
+
43
+ configureSplashScreen()
44
+ startSplashTimer()
45
+ }
46
+
47
+ private fun configureSplashScreen() {
48
+ val backgroundColor = properties.getProperty("splashScreen.backgroundColor", "#ffffff")
49
+ val imageWidth = properties.getProperty("splashScreen.imageWidth", "120").toIntOrNull() ?: 120
50
+ val imageHeight = properties.getProperty("splashScreen.imageHeight", "120").toIntOrNull() ?: 120
51
+ val cornerRadius = properties.getProperty("splashScreen.cornerRadius", "20").toFloatOrNull() ?: 20f
52
+
53
+ try {
54
+ binding.splashContainer.setBackgroundColor(Color.parseColor(backgroundColor))
55
+ } catch (e: Exception) {
56
+ binding.splashContainer.setBackgroundColor(Color.WHITE)
57
+ }
58
+
59
+ val widthPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, imageWidth.toFloat(), resources.displayMetrics).toInt()
60
+ val heightPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, imageHeight.toFloat(), resources.displayMetrics).toInt()
61
+
62
+ binding.splashImage.layoutParams.apply {
63
+ width = widthPx
64
+ height = heightPx
65
+ }
66
+
67
+ val drawable = ContextCompat.getDrawable(this,
68
+ resources.getIdentifier("splashscreen", "drawable", packageName).takeIf { it != 0 }
69
+ ?: R.mipmap.ic_launcher
70
+ ) ?: return
71
+
72
+ if (cornerRadius > 0) {
73
+ val radiusPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, cornerRadius, resources.displayMetrics)
74
+ val roundedDrawable = RoundedBitmapDrawableFactory.create(resources, drawable.toBitmap(widthPx, heightPx))
75
+ roundedDrawable.cornerRadius = radiusPx
76
+ binding.splashImage.setImageDrawable(roundedDrawable)
77
+ } else {
78
+ binding.splashImage.setImageDrawable(drawable)
79
+ }
80
+ }
81
+
82
+ private fun startSplashTimer() {
83
+ val duration = properties.getProperty("splashScreen.duration")?.toLongOrNull() ?: 1000
84
+ Handler(Looper.getMainLooper()).postDelayed({ dismissSplash() }, duration)
85
+ }
86
+
87
+ private fun dismissSplash() {
88
+ if (isFinishing) return
89
+ isFinishing = true
90
+ startActivity(Intent(this, MainActivity::class.java))
91
+ finish()
92
+
93
+ // Use modern API for Android 14+ (API 34+)
94
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
95
+ overrideActivityTransition(
96
+ Activity.OVERRIDE_TRANSITION_CLOSE,
97
+ android.R.anim.fade_in,
98
+ android.R.anim.fade_out
99
+ )
100
+ } else {
101
+ // Fallback to deprecated API for Android 13 and below
102
+ @Suppress("DEPRECATION")
103
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
104
+ }
105
+ }
106
+
107
+ private fun startMainActivity() {
108
+ startActivity(Intent(this, MainActivity::class.java))
109
+ finish()
110
+ }
111
+ }
@@ -0,0 +1,231 @@
1
+ package io.yourname.androidproject
2
+
3
+ import android.util.Log
4
+
5
+ /**
6
+ * Manager for URL whitelisting and access control
7
+ * Provides centralized URL validation with pattern matching support
8
+ *
9
+ * Pattern matching rules:
10
+ * - *text* or text (no wildcard) -> checks if URL contains "text"
11
+ * - text* -> checks if URL starts with "text"
12
+ * - *text -> checks if URL ends with "text"
13
+ *
14
+ * URL processing:
15
+ * - Query parameters and hash fragments are stripped before matching
16
+ * - Matching is case-insensitive
17
+ * - Ports are kept as-is (user must include port in pattern if URL has explicit port)
18
+ */
19
+ object URLWhitelistManager {
20
+
21
+ private const val TAG = "URLWhitelistManager"
22
+
23
+ /**
24
+ * Data class to hold categorized URL patterns for efficient matching
25
+ */
26
+ private data class CategorizedPatterns(
27
+ val contains: List<String>,
28
+ val prefix: List<String>,
29
+ val suffix: List<String>
30
+ )
31
+
32
+ @Volatile
33
+ private var accessControlEnabled: Boolean = false
34
+
35
+ @Volatile
36
+ private var patterns: CategorizedPatterns? = null
37
+
38
+ /**
39
+ * Initialize the whitelist manager with configuration
40
+ * Should be called once during app initialization
41
+ */
42
+ @Synchronized
43
+ fun initialize(enabled: Boolean, allowedUrls: List<String>) {
44
+ this.accessControlEnabled = enabled
45
+ this.patterns = categorizePatterns(allowedUrls)
46
+
47
+ try {
48
+ if (BuildConfig.DEBUG) {
49
+ Log.d(TAG, "URLWhitelistManager initialized")
50
+ Log.d(TAG, "Access Control Enabled: $enabled")
51
+ Log.d(TAG, "Allowed URLs: $allowedUrls")
52
+ patterns?.let {
53
+ Log.d(TAG, "Categorized - Contains: ${it.contains}")
54
+ Log.d(TAG, "Categorized - Prefix: ${it.prefix}")
55
+ Log.d(TAG, "Categorized - Suffix: ${it.suffix}")
56
+ }
57
+ }
58
+ } catch (e: RuntimeException) {
59
+ // Log not available in unit tests - silently ignore
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Check if access control is enabled
65
+ */
66
+ fun isAccessControlEnabled(): Boolean {
67
+ return accessControlEnabled
68
+ }
69
+
70
+ /**
71
+ * Check if a URL is allowed according to the whitelist patterns
72
+ * If access control is disabled, all URLs are allowed
73
+ */
74
+ fun isUrlAllowed(url: String): Boolean {
75
+ // If access control is disabled, allow everything
76
+ if (!accessControlEnabled) {
77
+ return true
78
+ }
79
+
80
+ // Always allow framework server URLs (localhost with /framework- pattern)
81
+ // These are internal infrastructure URLs used for large file handling
82
+ val frameworkUrlPattern = Regex("^https?://(?:localhost|127\\.0\\.0\\.1)(?::\\d+)?/framework-[a-zA-Z0-9_-]+", RegexOption.IGNORE_CASE)
83
+ if (frameworkUrlPattern.containsMatchIn(url)) {
84
+ return true
85
+ }
86
+
87
+ val patterns = this.patterns ?: return false
88
+
89
+ // If all pattern lists are empty, block everything
90
+ if (patterns.contains.isEmpty() && patterns.prefix.isEmpty() && patterns.suffix.isEmpty()) {
91
+ return false
92
+ }
93
+
94
+ val cleanUrl = cleanUrl(url)
95
+
96
+ // Check contains patterns
97
+ if (patterns.contains.any { cleanUrl.contains(it) }) {
98
+ try {
99
+ if (BuildConfig.DEBUG) {
100
+ Log.d(TAG, "✅ URL allowed (contains match): $url")
101
+ }
102
+ } catch (e: RuntimeException) { /* Ignore in tests */ }
103
+ return true
104
+ }
105
+
106
+ // Check prefix patterns
107
+ if (patterns.prefix.any { cleanUrl.startsWith(it) }) {
108
+ try {
109
+ if (BuildConfig.DEBUG) {
110
+ Log.d(TAG, "✅ URL allowed (prefix match): $url")
111
+ }
112
+ } catch (e: RuntimeException) { /* Ignore in tests */ }
113
+ return true
114
+ }
115
+
116
+ // Check suffix patterns
117
+ if (patterns.suffix.any { cleanUrl.endsWith(it) }) {
118
+ try {
119
+ if (BuildConfig.DEBUG) {
120
+ Log.d(TAG, "✅ URL allowed (suffix match): $url")
121
+ }
122
+ } catch (e: RuntimeException) { /* Ignore in tests */ }
123
+ return true
124
+ }
125
+
126
+ try {
127
+ if (BuildConfig.DEBUG) {
128
+ Log.w(TAG, "🚫 URL blocked by access control: $url")
129
+ Log.w(TAG, "🚫 Clean URL: $cleanUrl")
130
+ }
131
+ } catch (e: RuntimeException) { /* Ignore in tests */ }
132
+
133
+ return false
134
+ }
135
+
136
+ /**
137
+ * Check if a URL is from an external domain (not in whitelist)
138
+ * If access control is disabled, nothing is considered external
139
+ */
140
+ fun isExternalDomain(url: String): Boolean {
141
+ // If access control is disabled, nothing is external
142
+ if (!accessControlEnabled) {
143
+ return false
144
+ }
145
+
146
+ // Simply the inverse of isUrlAllowed
147
+ return !isUrlAllowed(url)
148
+ }
149
+
150
+ /**
151
+ * Clean URL by removing query parameters and hash, then lowercase for case-insensitive matching
152
+ * Also decodes URL-encoded characters to prevent bypass attempts
153
+ */
154
+ private fun cleanUrl(url: String): String {
155
+ // Decode URL-encoded characters first to prevent bypass (e.g., %3F for ?, %23 for #)
156
+ val decoded = try {
157
+ java.net.URLDecoder.decode(url, "UTF-8")
158
+ } catch (e: Exception) {
159
+ // If decode fails, use original URL
160
+ url
161
+ }
162
+
163
+ var cleanUrl = decoded
164
+
165
+ // Remove hash fragment (# and everything after)
166
+ val hashIndex = cleanUrl.indexOf('#')
167
+ if (hashIndex != -1) {
168
+ cleanUrl = cleanUrl.substring(0, hashIndex)
169
+ }
170
+
171
+ // Remove query parameters (? and everything after)
172
+ val queryIndex = cleanUrl.indexOf('?')
173
+ if (queryIndex != -1) {
174
+ cleanUrl = cleanUrl.substring(0, queryIndex)
175
+ }
176
+
177
+ return cleanUrl.lowercase()
178
+ }
179
+
180
+ /**
181
+ * Categorize patterns into contains, prefix, and suffix for efficient matching
182
+ */
183
+ private fun categorizePatterns(allowedUrls: List<String>): CategorizedPatterns {
184
+ val contains = mutableListOf<String>()
185
+ val prefix = mutableListOf<String>()
186
+ val suffix = mutableListOf<String>()
187
+
188
+ allowedUrls.forEach { pattern ->
189
+ if (pattern.isEmpty()) return@forEach
190
+
191
+ val startsWithWildcard = pattern.startsWith('*')
192
+ val endsWithWildcard = pattern.endsWith('*')
193
+
194
+ when {
195
+ startsWithWildcard && endsWithWildcard -> {
196
+ // *text* -> contains check
197
+ if (pattern.length <= 2) return@forEach // Pattern is just * or ** - ignore
198
+ val extracted = pattern.substring(1, pattern.length - 1).lowercase()
199
+ if (extracted.isNotEmpty()) {
200
+ contains.add(extracted)
201
+ }
202
+ }
203
+ endsWithWildcard -> {
204
+ // text* -> prefix check
205
+ if (pattern.length <= 1) return@forEach // Pattern is just * - ignore
206
+ val extracted = pattern.substring(0, pattern.length - 1).lowercase()
207
+ if (extracted.isNotEmpty()) {
208
+ prefix.add(extracted)
209
+ }
210
+ }
211
+ startsWithWildcard -> {
212
+ // *text -> suffix check
213
+ if (pattern.length <= 1) return@forEach // Pattern is just * - ignore
214
+ val extracted = pattern.substring(1).lowercase()
215
+ if (extracted.isNotEmpty()) {
216
+ suffix.add(extracted)
217
+ }
218
+ }
219
+ else -> {
220
+ // No wildcard -> treat as contains check
221
+ val extracted = pattern.lowercase()
222
+ if (extracted.isNotEmpty()) {
223
+ contains.add(extracted)
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ return CategorizedPatterns(contains, prefix, suffix)
230
+ }
231
+ }
@@ -2,63 +2,57 @@ package io.yourname.androidproject
2
2
 
3
3
  import android.net.Uri
4
4
 
5
- fun isUrlAllowed(url: String, allowedUrls: List<String>): Boolean {
6
- if (allowedUrls.isEmpty()) return false
7
-
8
- val parsedUrl = Uri.parse(url) ?: return false
9
- val urlHost = "${parsedUrl.scheme}://${parsedUrl.host}${if (parsedUrl.port != -1) ":${parsedUrl.port}" else ""}"
10
-
11
- return allowedUrls.any { pattern ->
12
- when {
13
- pattern.contains("*") -> {
14
- val regex = pattern
15
- .replace(".", "\\.")
16
- .replace("*", ".*")
17
- .toRegex(RegexOption.IGNORE_CASE)
18
- regex.matches(url) || regex.matches(urlHost)
19
- }
20
- else -> {
21
- val patternUri = Uri.parse(pattern)
22
- val patternHost = "${patternUri.scheme}://${patternUri.host}${if (patternUri.port != -1) ":${patternUri.port}" else ""}"
23
-
24
- // Exact match or pattern without port matches URL with any port
25
- urlHost.equals(patternHost, ignoreCase = true) ||
26
- (patternUri.port == -1 && urlHost.startsWith("${patternUri.scheme}://${patternUri.host}", ignoreCase = true))
27
- }
28
- }
29
- }
30
- }
5
+ /**
6
+ * Checks if a URL matches any of the cache patterns
7
+ * Handles query parameters, hashes, and version strings
8
+ *
9
+ * Examples:
10
+ * - Pattern "*.css" matches:
11
+ * - "http://example.com/app.css"
12
+ * - "http://example.com/app.d4e5dea6.css"
13
+ * - "http://example.com/app.css?v=123"
14
+ * - "http://example.com/path/to/style.css#section"
15
+ *
16
+ * - Pattern "*.js" matches:
17
+ * - "http://example.com/bundle.js"
18
+ * - "http://example.com/bundle.abc123.js"
19
+ * - "http://example.com/bundle.js?t=456"
20
+ */
21
+ fun matchesCachePattern(url: String, cachePatterns: List<String>): Boolean {
22
+ if (cachePatterns.isEmpty()) return false
23
+
24
+ try {
25
+ val parsedUrl = Uri.parse(url) ?: return false
26
+ // Get the path without query params or hash
27
+ val path = parsedUrl.path ?: return false
28
+
29
+ // Extract filename from path
30
+ val filename = path.substringAfterLast('/')
31
+ if (filename.isEmpty()) return false
32
+
33
+ return cachePatterns.any { pattern ->
34
+ when {
35
+ pattern.contains("*") -> {
36
+ // Convert wildcard pattern to regex
37
+ // *.css becomes .*\.css$
38
+ val regexPattern = pattern
39
+ .replace(".", "\\.") // Escape dots
40
+ .replace("*", ".*") // Convert * to .*
41
+ .let { "^$it$" } // Anchor at start and end
42
+
43
+ val regex = regexPattern.toRegex(RegexOption.IGNORE_CASE)
31
44
 
32
- fun isExternalDomain(url: String, allowedUrls: List<String>): Boolean {
33
- if (allowedUrls.isEmpty()) return true
34
-
35
- val parsedUrl = Uri.parse(url)
36
- val urlHost = parsedUrl.host ?: return true
37
- val urlScheme = parsedUrl.scheme ?: return true
38
- val urlPort = parsedUrl.port
39
-
40
- return !allowedUrls.any { pattern ->
41
- if (pattern.startsWith("*.")) {
42
- val domain = pattern.substring(2)
43
- urlHost.equals(domain, ignoreCase = true) || urlHost.endsWith(".$domain", ignoreCase = true)
44
- } else {
45
- val patternUri = Uri.parse(pattern)
46
- val patternHost = patternUri?.host ?: return@any false
47
- val patternScheme = patternUri?.scheme ?: return@any false
48
- val patternPort = patternUri?.port ?: -1
49
-
50
- val schemeMatches = urlScheme.equals(patternScheme, ignoreCase = true)
51
- val hostMatches = if (patternHost.startsWith("*.")) {
52
- val domain = patternHost.substring(2)
53
- urlHost.equals(domain, ignoreCase = true) || urlHost.endsWith(".$domain", ignoreCase = true)
54
- } else {
55
- urlHost.equals(patternHost, ignoreCase = true)
45
+ // Try matching against filename
46
+ regex.matches(filename)
47
+ }
48
+ else -> {
49
+ // Exact match against filename
50
+ filename.equals(pattern, ignoreCase = true)
51
+ }
56
52
  }
57
- val portMatches = patternPort == -1 || urlPort == patternPort ||
58
- (urlPort == -1 && ((patternPort == 443 && patternScheme.equals("https", ignoreCase = true)) ||
59
- (patternPort == 80 && patternScheme.equals("http", ignoreCase = true))))
60
-
61
- schemeMatches && hostMatches && portMatches
62
53
  }
54
+ } catch (e: Exception) {
55
+ // If parsing fails, return false
56
+ return false
63
57
  }
64
58
  }
@@ -12,27 +12,40 @@ import java.net.URL
12
12
  import java.security.MessageDigest
13
13
  import java.util.concurrent.TimeUnit
14
14
 
15
- class WebCacheManager(private val context: Context) {
15
+ class WebCacheManager(private val context: Context, private val properties: java.util.Properties? = null) {
16
16
  private val TAG = "WebCacheManager"
17
17
  private val cacheDir = File(context.cacheDir, "webview_cache")
18
18
  private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
19
19
  private val BUFFER_SIZE = 32 * 1024
20
20
 
21
- // Cache timing configurations
22
- private val maxAge = TimeUnit.HOURS.toMillis(24) // Time until content becomes stale
23
- private val staleWhileRevalidate = TimeUnit.HOURS.toMillis(1) // Additional time content can be served while revalidating
21
+ // Cache timing configurations - now configurable via properties
22
+ private val maxAge = properties?.getProperty("cache.maxAge", "24")?.toLongOrNull()?.let { TimeUnit.HOURS.toMillis(it) }
23
+ ?: TimeUnit.HOURS.toMillis(24) // Default 24 hours
24
+ private val staleWhileRevalidate = properties?.getProperty("cache.staleWhileRevalidate", "1")?.toLongOrNull()?.let { TimeUnit.HOURS.toMillis(it) }
25
+ ?: TimeUnit.HOURS.toMillis(1) // Default 1 hour
26
+ private val maxCacheSize = properties?.getProperty("cache.maxSize", "100")?.toLongOrNull()?.let { it * 1024 * 1024 }
27
+ ?: (100 * 1024 * 1024) // Default 100MB
28
+ private val memoryFraction = properties?.getProperty("cache.memoryFraction", "8")?.toIntOrNull() ?: 8 // Default 1/8 of memory
24
29
 
25
30
  // Track ongoing revalidations to prevent duplicate requests
26
31
  private val ongoingRevalidations = mutableSetOf<String>()
27
32
 
28
- // Memory cache
33
+ // Memory cache - now configurable
29
34
  private val memoryCache: LruCache<String, CacheEntry> = LruCache<String, CacheEntry>(
30
- (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
35
+ (Runtime.getRuntime().maxMemory() / 1024 / memoryFraction).toInt()
31
36
  )
32
37
 
33
38
  init {
34
39
  try {
35
40
  cacheDir.mkdirs()
41
+ if (BuildConfig.DEBUG) {
42
+ Log.d(TAG, "📦 Cache configuration:")
43
+ Log.d(TAG, " - Max age: ${maxAge / (1000 * 60 * 60)} hours")
44
+ Log.d(TAG, " - Stale while revalidate: ${staleWhileRevalidate / (1000 * 60 * 60)} hours")
45
+ Log.d(TAG, " - Max cache size: ${maxCacheSize / (1024 * 1024)} MB")
46
+ Log.d(TAG, " - Memory cache size: ${memoryCache.maxSize() / 1024} KB")
47
+ Log.d(TAG, " - Cache directory: ${cacheDir.absolutePath}")
48
+ }
36
49
  } catch (e: Exception) {
37
50
  Log.e(TAG, "Failed to create cache directory: ${e.message}")
38
51
  }
@@ -45,6 +58,54 @@ class WebCacheManager(private val context: Context) {
45
58
  val lastModified: String? = null
46
59
  )
47
60
 
61
+ /**
62
+ * Synchronous cache check - only returns cached content if immediately available
63
+ * Does not perform network requests or background revalidation
64
+ * Used by ServiceWorker to avoid blocking
65
+ */
66
+ fun getCachedResponseSync(url: String, headers: Map<String, String>): WebResourceResponse? {
67
+ return try {
68
+ val cacheKey = generateCacheKey(url)
69
+ val currentTime = System.currentTimeMillis()
70
+
71
+ // Check memory cache first
72
+ val memoryCacheEntry = memoryCache.get(cacheKey)
73
+ if (memoryCacheEntry != null) {
74
+ val age = currentTime - memoryCacheEntry.timestamp
75
+ // Only return if fresh or within stale-while-revalidate window
76
+ if (age <= maxAge + staleWhileRevalidate) {
77
+ if (BuildConfig.DEBUG) {
78
+ Log.d(TAG, "✅ Serving from memory cache (sync): $url")
79
+ }
80
+ return memoryCacheEntry.response
81
+ }
82
+ }
83
+
84
+ // Check disk cache synchronously
85
+ val cacheFile = File(cacheDir, cacheKey)
86
+ if (cacheFile.exists()) {
87
+ val fileAge = currentTime - cacheFile.lastModified()
88
+ if (fileAge <= maxAge + staleWhileRevalidate) {
89
+ val metadata = loadMetadata(cacheKey)
90
+ val response = createResponseFromCache(cacheFile, metadata)
91
+ if (BuildConfig.DEBUG) {
92
+ Log.d(TAG, "✅ Serving from disk cache (sync): $url")
93
+ }
94
+ return response
95
+ }
96
+ }
97
+
98
+ // No valid cache available
99
+ if (BuildConfig.DEBUG) {
100
+ Log.d(TAG, "❌ No valid cache available (sync): $url")
101
+ }
102
+ null
103
+ } catch (e: Exception) {
104
+ Log.e(TAG, "Error in getCachedResponseSync for URL: $url: ${e.message}")
105
+ null
106
+ }
107
+ }
108
+
48
109
  suspend fun getCachedResponse(url: String, headers: Map<String, String>): WebResourceResponse? =
49
110
  withContext(Dispatchers.IO) {
50
111
  try {
@@ -117,11 +178,23 @@ class WebCacheManager(private val context: Context) {
117
178
  }
118
179
  }
119
180
 
181
+ fun clearAll() {
182
+ try {
183
+ memoryCache.evictAll()
184
+ cacheDir.listFiles()?.forEach { it.delete() }
185
+ if (BuildConfig.DEBUG) {
186
+ Log.d(TAG, "clearAll: memory cache and disk cache wiped")
187
+ }
188
+ } catch (e: Exception) {
189
+ Log.e(TAG, "clearAll failed: ${e.message}")
190
+ }
191
+ }
192
+
120
193
  suspend fun cleanup() {
121
194
  withContext(Dispatchers.IO) {
122
195
  try {
123
196
  val currentTime = System.currentTimeMillis()
124
- val maxCacheSize = 100 * 1024 * 1024 // 100MB max cache size
197
+ // Use configurable max cache size
125
198
  var totalSize = 0L
126
199
 
127
200
  // Get all cache files sorted by last modified time (oldest first)
@@ -274,7 +347,7 @@ class WebCacheManager(private val context: Context) {
274
347
  response = WebResourceResponse(
275
348
  mimeType,
276
349
  encoding,
277
- ByteArrayInputStream(responseBytes.clone())
350
+ ByteArrayInputStream(responseBytes.copyOf())
278
351
  ),
279
352
  eTag = eTag,
280
353
  lastModified = lastModified