catalyst-core 0.1.0-canary.9 → 0.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +1 -1
  2. package/bin/catalyst.js +8 -1
  3. package/changelog.md +6 -0
  4. package/dist/native/androidProject/app/build.gradle.kts +34 -8
  5. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +2 -1
  6. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +270 -78
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +34 -5
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +149 -1
  10. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
  11. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +156 -39
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
  18. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
  23. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
  24. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
  25. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
  26. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
  27. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
  28. package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
  29. package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
  30. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
  31. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
  32. package/dist/native/androidSetup.js +1 -0
  33. package/dist/native/bridge/WebBridge.js +5 -5
  34. package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
  35. package/dist/native/bridge/hooks/index.js +1 -0
  36. package/dist/native/bridge/hooks/useCamera.js +9 -0
  37. package/dist/native/bridge/hooks/useDataProtection.js +2 -0
  38. package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
  39. package/dist/native/bridge/hooks/useFilePicker.js +2 -0
  40. package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
  41. package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
  42. package/dist/native/bridge/hooks/useIntent.js +1 -0
  43. package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
  44. package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
  45. package/dist/native/bridge/hooks/useNotification.js +2 -0
  46. package/dist/native/bridge/hooks/useSafeArea.js +2 -0
  47. package/dist/native/bridge/hooks/useVideoStream.js +6 -0
  48. package/dist/native/bridge/hooks/utils.js +1 -0
  49. package/dist/native/bridge/hooks.js +1 -137
  50. package/dist/native/bridge/useBaseHook.js +12 -7
  51. package/dist/native/bridge/utils/NativeBridge.js +41 -6
  52. package/dist/native/buildAndroid/assets.js +52 -0
  53. package/dist/native/buildAndroid/build.js +19 -0
  54. package/dist/native/buildAndroid/config.js +1 -0
  55. package/dist/native/buildAndroid/index.js +1 -0
  56. package/dist/native/buildAppAndroid.js +2 -150
  57. package/dist/native/buildAppIos.js +2 -312
  58. package/dist/native/buildIos/assets.js +8 -0
  59. package/dist/native/buildIos/build.js +40 -0
  60. package/dist/native/buildIos/config.js +71 -0
  61. package/dist/native/buildIos/index.js +6 -0
  62. package/dist/native/buildIos/plugins.js +45 -0
  63. package/dist/native/internalPluginUtils.js +7 -0
  64. package/dist/native/iosnativeWebView/Package.resolved +36 -0
  65. package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
  66. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
  67. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
  68. package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
  69. package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
  70. package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
  71. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
  72. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
  73. package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
  74. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
  75. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
  76. package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
  77. package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
  78. package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
  79. package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +39 -4
  80. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
  81. package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
  82. package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
  83. package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
  84. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
  85. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
  86. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
  87. package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
  88. package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
  89. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +53 -8
  90. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +88 -4
  91. package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +30 -11
  92. package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
  93. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
  94. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
  95. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
  96. package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
  97. package/dist/native/plugin-bridge/PluginBridge.js +1 -0
  98. package/dist/native/pluginComposerAndroid.js +28 -0
  99. package/dist/native/pluginComposerIos.js +16 -0
  100. package/dist/native/renameAndroidProject.js +5 -5
  101. package/dist/native/setupEmulatorIos.js +6 -4
  102. package/dist/native/setupServer.js +2 -3
  103. package/dist/native/utils.js +4 -1
  104. package/dist/offline/catalyst-sw.js +1 -0
  105. package/dist/offline/registerServiceWorker.js +1 -0
  106. package/dist/otel.js +70 -2
  107. package/dist/router/ClientRouter.js +1 -1
  108. package/dist/scripts/build.js +1 -1
  109. package/dist/scripts/cleanCache.js +1 -0
  110. package/dist/scripts/devBuild.js +1 -1
  111. package/dist/scripts/devServe.js +1 -1
  112. package/dist/scripts/generateOfflineManifest.js +1 -0
  113. package/dist/scripts/loadEnvironmentVariables.js +1 -2
  114. package/dist/scripts/plugins.js +1 -0
  115. package/dist/scripts/registerAliases.js +6 -2
  116. package/dist/scripts/scriptUtils.js +2 -1
  117. package/dist/scripts/serve.js +2 -1
  118. package/dist/scripts/start.js +7 -2
  119. package/dist/server/expressServer.js +4 -8
  120. package/dist/server/renderer/document/Body.js +4 -4
  121. package/dist/server/renderer/document/Head.js +2 -2
  122. package/dist/server/renderer/extract.js +31 -27
  123. package/dist/server/renderer/handler.js +47 -24
  124. package/dist/server/renderer/index.js +1 -1
  125. package/dist/server/startServer.js +57 -7
  126. package/dist/server/utils/userAgentUtil.js +9 -3
  127. package/dist/server/utils/validator.js +1 -1
  128. package/dist/webpack/babel-plugins/remove-client.plugin.js +2 -2
  129. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +2 -2
  130. package/dist/webpack/babel.config.client.js +1 -1
  131. package/dist/webpack/babel.config.ssr.js +1 -1
  132. package/dist/webpack/base.babel.js +7 -8
  133. package/dist/webpack/development.client.babel.js +7 -16
  134. package/dist/webpack/production.client.babel.js +1 -1
  135. package/dist/webpack/production.ssr.babel.js +5 -1
  136. package/mcp_v2/knowledge-base.json +206 -100
  137. package/mcp_v2/lib/helpers.js +42 -8
  138. package/mcp_v2/lib/seed.js +88 -0
  139. package/mcp_v2/mcp.js +150 -4
  140. package/mcp_v2/setup.js +30 -193
  141. package/mcp_v2/tools/build.js +1 -1
  142. package/mcp_v2/tools/config.js +16 -4
  143. package/mcp_v2/tools/conversion.js +11 -11
  144. package/mcp_v2/tools/github.js +1117 -0
  145. package/mcp_v2/tools/knowledge.js +16 -3
  146. package/mcp_v2/tools/sync.js +179 -10
  147. package/mcp_v2/tools/tasks.js +13 -4
  148. package/package.json +20 -8
@@ -14,6 +14,7 @@ import java.util.concurrent.TimeUnit
14
14
 
15
15
  class WebCacheManager(private val context: Context, private val properties: java.util.Properties? = null) {
16
16
  private val TAG = "WebCacheManager"
17
+ private val FLOW_TAG = "CatalystOfflineFlow"
17
18
  private val cacheDir = File(context.cacheDir, "webview_cache")
18
19
  private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
19
20
  private val BUFFER_SIZE = 32 * 1024
@@ -51,18 +52,33 @@ class WebCacheManager(private val context: Context, private val properties: java
51
52
  }
52
53
  }
53
54
 
54
- data class CacheEntry(
55
- val response: WebResourceResponse,
55
+ private data class CacheEntry(
56
+ val data: ByteArray,
57
+ val mimeType: String,
58
+ val encoding: String,
59
+ val responseHeaders: Map<String, String> = emptyMap(),
56
60
  val timestamp: Long = System.currentTimeMillis(),
57
61
  val eTag: String? = null,
58
62
  val lastModified: String? = null
59
- )
63
+ ) {
64
+ fun toResponse(): WebResourceResponse {
65
+ return WebResourceResponse(
66
+ mimeType,
67
+ encoding,
68
+ 200,
69
+ "OK",
70
+ responseHeaders.ifEmpty { null },
71
+ ByteArrayInputStream(data)
72
+ )
73
+ }
74
+ }
60
75
 
61
76
  /**
62
77
  * Synchronous cache check - only returns cached content if immediately available
63
78
  * Does not perform network requests or background revalidation
64
79
  * Used by ServiceWorker to avoid blocking
65
80
  */
81
+ @Suppress("UNUSED_PARAMETER")
66
82
  fun getCachedResponseSync(url: String, headers: Map<String, String>): WebResourceResponse? {
67
83
  return try {
68
84
  val cacheKey = generateCacheKey(url)
@@ -76,8 +92,12 @@ class WebCacheManager(private val context: Context, private val properties: java
76
92
  if (age <= maxAge + staleWhileRevalidate) {
77
93
  if (BuildConfig.DEBUG) {
78
94
  Log.d(TAG, "✅ Serving from memory cache (sync): $url")
95
+ Log.d(FLOW_TAG, "ASSET cache-hit source=memory ageMs=$age url=$url")
79
96
  }
80
- return memoryCacheEntry.response
97
+ return memoryCacheEntry.toResponse()
98
+ }
99
+ if (BuildConfig.DEBUG) {
100
+ Log.d(FLOW_TAG, "ASSET cache-expired source=memory ageMs=$age url=$url")
81
101
  }
82
102
  }
83
103
 
@@ -88,20 +108,48 @@ class WebCacheManager(private val context: Context, private val properties: java
88
108
  if (fileAge <= maxAge + staleWhileRevalidate) {
89
109
  val metadata = loadMetadata(cacheKey)
90
110
  val response = createResponseFromCache(cacheFile, metadata)
111
+ memoryCache.put(cacheKey, createEntryFromCache(cacheFile, metadata, cacheFile.lastModified()))
91
112
  if (BuildConfig.DEBUG) {
92
113
  Log.d(TAG, "✅ Serving from disk cache (sync): $url")
114
+ Log.d(FLOW_TAG, "ASSET cache-hit source=disk ageMs=$fileAge bytes=${cacheFile.length()} mime=${metadata.mimeType} encoding=${metadata.encoding} url=$url")
93
115
  }
94
116
  return response
95
117
  }
118
+ if (BuildConfig.DEBUG) {
119
+ Log.d(FLOW_TAG, "ASSET cache-expired source=disk ageMs=$fileAge bytes=${cacheFile.length()} url=$url")
120
+ }
96
121
  }
97
122
 
98
123
  // No valid cache available
99
124
  if (BuildConfig.DEBUG) {
100
125
  Log.d(TAG, "❌ No valid cache available (sync): $url")
126
+ Log.d(FLOW_TAG, "ASSET cache-miss url=$url")
101
127
  }
102
128
  null
103
129
  } catch (e: Exception) {
104
130
  Log.e(TAG, "Error in getCachedResponseSync for URL: $url: ${e.message}")
131
+ if (BuildConfig.DEBUG) {
132
+ Log.e(FLOW_TAG, "ASSET cache-error phase=read url=$url error=${e.message}", e)
133
+ }
134
+ null
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Synchronous cache-or-network path for WebView interception.
140
+ *
141
+ * shouldInterceptRequest runs off the UI thread, so it is safe to block here.
142
+ * Returning the fetched response from this method guarantees that the same
143
+ * request which populated the cache also succeeds in the WebView.
144
+ */
145
+ fun getCachedResponseOrFetchSync(url: String, headers: Map<String, String>): WebResourceResponse? {
146
+ return try {
147
+ getCachedResponseSync(url, headers) ?: fetchAndCacheResourceBlocking(url, headers, generateCacheKey(url))
148
+ } catch (e: Exception) {
149
+ Log.e(TAG, "Error in getCachedResponseOrFetchSync for URL: $url: ${e.message}")
150
+ if (BuildConfig.DEBUG) {
151
+ Log.e(FLOW_TAG, "ASSET cache-error phase=cache-or-fetch url=$url error=${e.message}", e)
152
+ }
105
153
  null
106
154
  }
107
155
  }
@@ -123,7 +171,7 @@ class WebCacheManager(private val context: Context, private val properties: java
123
171
  if (BuildConfig.DEBUG) {
124
172
  Log.d(TAG, "Serving fresh content from memory cache: $url")
125
173
  }
126
- return@withContext memoryCacheEntry.response
174
+ return@withContext memoryCacheEntry.toResponse()
127
175
  }
128
176
  age <= maxAge + staleWhileRevalidate -> {
129
177
  // Stale content, but within revalidate window
@@ -131,7 +179,7 @@ class WebCacheManager(private val context: Context, private val properties: java
131
179
  Log.d(TAG, "Serving stale content while revalidating: $url")
132
180
  }
133
181
  revalidateInBackground(url, headers, cacheKey, memoryCacheEntry)
134
- return@withContext memoryCacheEntry.response
182
+ return@withContext memoryCacheEntry.toResponse()
135
183
  }
136
184
  }
137
185
  }
@@ -146,18 +194,13 @@ class WebCacheManager(private val context: Context, private val properties: java
146
194
  fileAge <= maxAge -> {
147
195
  // Fresh content from disk
148
196
  val response = createResponseFromCache(cacheFile, metadata)
149
- memoryCache.put(cacheKey, CacheEntry(response))
197
+ memoryCache.put(cacheKey, createEntryFromCache(cacheFile, metadata, cacheFile.lastModified()))
150
198
  return@withContext response
151
199
  }
152
200
  fileAge <= maxAge + staleWhileRevalidate -> {
153
201
  // Stale content from disk, revalidate in background
154
202
  val response = createResponseFromCache(cacheFile, metadata)
155
- val cacheEntry = CacheEntry(
156
- response = response,
157
- timestamp = cacheFile.lastModified(),
158
- eTag = metadata.eTag,
159
- lastModified = metadata.lastModified
160
- )
203
+ val cacheEntry = createEntryFromCache(cacheFile, metadata, cacheFile.lastModified())
161
204
  revalidateInBackground(url, headers, cacheKey, cacheEntry)
162
205
  return@withContext response
163
206
  }
@@ -168,7 +211,7 @@ class WebCacheManager(private val context: Context, private val properties: java
168
211
  if (BuildConfig.DEBUG) {
169
212
  Log.d(TAG, "❌ Cache miss, fetching fresh content: $url")
170
213
  }
171
- fetchAndCacheResource(url, headers, cacheKey)
214
+ fetchAndCacheResourceBlocking(url, headers, cacheKey)
172
215
  } catch (e: Exception) {
173
216
  Log.e(TAG, "Error in getCachedResponse for URL: $url: ${e.message}")
174
217
  if (BuildConfig.DEBUG) {
@@ -282,7 +325,7 @@ class WebCacheManager(private val context: Context, private val properties: java
282
325
  if (BuildConfig.DEBUG) {
283
326
  Log.d(TAG, "⚡ Content changed, updating cache: $url")
284
327
  }
285
- fetchAndCacheResource(url, headers, cacheKey)
328
+ fetchAndCacheResourceBlocking(url, headers, cacheKey)
286
329
  }
287
330
  }
288
331
  } catch (e: Exception) {
@@ -308,47 +351,63 @@ class WebCacheManager(private val context: Context, private val properties: java
308
351
  }
309
352
  }
310
353
 
311
- private suspend fun fetchAndCacheResource(
354
+ private fun fetchAndCacheResourceBlocking(
312
355
  url: String,
313
356
  headers: Map<String, String>,
314
357
  cacheKey: String
315
- ): WebResourceResponse? = withContext(Dispatchers.IO) {
358
+ ): WebResourceResponse? {
316
359
  var connection: HttpURLConnection? = null
317
360
  try {
318
361
  connection = URL(url).openConnection() as HttpURLConnection
319
362
  headers.forEach { (key, value) ->
320
- connection.setRequestProperty(key, value)
363
+ if (!key.equals("Accept-Encoding", ignoreCase = true)) {
364
+ connection.setRequestProperty(key, value)
365
+ }
321
366
  }
367
+ connection.setRequestProperty("Accept-Encoding", "identity")
322
368
 
323
369
  connection.connectTimeout = 15000
324
370
  connection.readTimeout = 15000
371
+ if (BuildConfig.DEBUG) {
372
+ Log.d(FLOW_TAG, "ASSET network-fetch start url=$url headers=${headers.keys.sorted()}")
373
+ }
325
374
  connection.connect()
326
375
 
327
376
  if (connection.responseCode == HttpURLConnection.HTTP_OK) {
328
- val mimeType = connection.contentType ?: "application/octet-stream"
329
- val encoding = connection.contentEncoding ?: "utf-8"
377
+ val contentType = connection.contentType ?: "application/octet-stream"
378
+ val mimeType = mimeTypeFromContentType(contentType)
379
+ val encoding = charsetFromContentType(contentType)
330
380
  val eTag = connection.getHeaderField("ETag")
331
381
  val lastModified = connection.getHeaderField("Last-Modified")
332
382
 
383
+ // Capture all response headers for replay on cache hits
384
+ val savedHeaders = mutableMapOf<String, String>()
385
+ var i = 0
386
+ while (true) {
387
+ val key = connection.getHeaderFieldKey(i) ?: break
388
+ val value = connection.getHeaderField(i)
389
+ if (key.isNotEmpty() && shouldReplayHeader(key)) savedHeaders[key] = value
390
+ i++
391
+ }
392
+ savedHeaders["Content-Type"] = contentType
393
+
333
394
  val responseBytes = connection.inputStream.use { it.readBytes() }
334
395
  if (!isValidResponse(mimeType, responseBytes)) {
335
- return@withContext null
396
+ if (BuildConfig.DEBUG) {
397
+ Log.w(FLOW_TAG, "ASSET network-fetch invalid-response status=${connection.responseCode} contentType=$contentType bytes=${responseBytes.size} url=$url")
398
+ }
399
+ return null
336
400
  }
337
401
 
338
402
  // Create response for immediate use
339
- val response = WebResourceResponse(
340
- mimeType,
341
- encoding,
342
- ByteArrayInputStream(responseBytes)
343
- )
403
+ val response = WebResourceResponse(mimeType, encoding, 200, "OK", savedHeaders, ByteArrayInputStream(responseBytes))
344
404
 
345
405
  // Cache the response
346
406
  val cacheEntry = CacheEntry(
347
- response = WebResourceResponse(
348
- mimeType,
349
- encoding,
350
- ByteArrayInputStream(responseBytes.copyOf())
351
- ),
407
+ data = responseBytes.copyOf(),
408
+ mimeType = mimeType,
409
+ encoding = encoding,
410
+ responseHeaders = savedHeaders,
352
411
  eTag = eTag,
353
412
  lastModified = lastModified
354
413
  )
@@ -363,25 +422,39 @@ class WebCacheManager(private val context: Context, private val properties: java
363
422
  mimeType = mimeType,
364
423
  encoding = encoding,
365
424
  eTag = eTag,
366
- lastModified = lastModified
425
+ lastModified = lastModified,
426
+ responseHeaders = savedHeaders
367
427
  ))
368
428
  if (BuildConfig.DEBUG) {
369
429
  Log.d(TAG, "✅ Successfully cached response for: $url")
430
+ Log.d(FLOW_TAG, "ASSET stored bytes=${responseBytes.size} mime=$mimeType encoding=$encoding url=$url")
370
431
  }
371
432
  } catch (e: Exception) {
372
433
  Log.e(TAG, "Error saving to disk cache for URL: $url: ${e.message}")
434
+ if (BuildConfig.DEBUG) {
435
+ Log.e(FLOW_TAG, "ASSET store-error url=$url error=${e.message}", e)
436
+ }
373
437
  }
374
438
  }
375
439
 
376
- return@withContext response
440
+ if (BuildConfig.DEBUG) {
441
+ Log.d(FLOW_TAG, "ASSET network-fetch success status=${connection.responseCode} bytes=${responseBytes.size} contentType=$contentType replayMime=$mimeType replayEncoding=$encoding url=$url")
442
+ }
443
+ return response
444
+ }
445
+ if (BuildConfig.DEBUG) {
446
+ Log.w(FLOW_TAG, "ASSET network-fetch non-200 status=${connection.responseCode} contentType=${connection.contentType} url=$url")
377
447
  }
378
- return@withContext null
448
+ return null
379
449
  } catch (e: Exception) {
380
450
  Log.e(TAG, "Error fetching resource: ${e.message}")
451
+ if (BuildConfig.DEBUG) {
452
+ Log.e(FLOW_TAG, "ASSET network-fetch error url=$url error=${e.message}", e)
453
+ }
381
454
  if (BuildConfig.DEBUG) {
382
455
  e.printStackTrace()
383
456
  }
384
- return@withContext null
457
+ return null
385
458
  } finally {
386
459
  connection?.disconnect()
387
460
  }
@@ -391,25 +464,69 @@ class WebCacheManager(private val context: Context, private val properties: java
391
464
  val mimeType: String,
392
465
  val encoding: String,
393
466
  val eTag: String? = null,
394
- val lastModified: String? = null
467
+ val lastModified: String? = null,
468
+ val responseHeaders: Map<String, String> = emptyMap()
395
469
  ) : Serializable
396
470
 
397
471
  private fun createResponseFromCache(cacheFile: File, metadata: CacheMetadata): WebResourceResponse {
398
472
  return WebResourceResponse(
399
473
  metadata.mimeType,
400
474
  metadata.encoding,
475
+ 200,
476
+ "OK",
477
+ metadata.responseHeaders.ifEmpty { null },
401
478
  BufferedInputStream(FileInputStream(cacheFile), BUFFER_SIZE)
402
479
  )
403
480
  }
404
481
 
482
+ private fun mimeTypeFromContentType(contentType: String): String {
483
+ return contentType.substringBefore(";").trim().ifEmpty { "application/octet-stream" }
484
+ }
485
+
486
+ private fun charsetFromContentType(contentType: String): String {
487
+ val charsetPart = contentType
488
+ .split(";")
489
+ .map { it.trim() }
490
+ .firstOrNull { it.startsWith("charset=", ignoreCase = true) }
491
+ return charsetPart
492
+ ?.substringAfter("=")
493
+ ?.trim()
494
+ ?.trim('"')
495
+ ?.ifEmpty { null }
496
+ ?: "utf-8"
497
+ }
498
+
499
+ private fun shouldReplayHeader(header: String): Boolean {
500
+ return !header.equals("Content-Encoding", ignoreCase = true) &&
501
+ !header.equals("Transfer-Encoding", ignoreCase = true) &&
502
+ !header.equals("Content-Length", ignoreCase = true) &&
503
+ !header.equals("Connection", ignoreCase = true)
504
+ }
505
+
506
+ private fun createEntryFromCache(
507
+ cacheFile: File,
508
+ metadata: CacheMetadata,
509
+ timestamp: Long
510
+ ): CacheEntry {
511
+ return CacheEntry(
512
+ data = cacheFile.readBytes(),
513
+ mimeType = metadata.mimeType,
514
+ encoding = metadata.encoding,
515
+ responseHeaders = metadata.responseHeaders,
516
+ timestamp = timestamp,
517
+ eTag = metadata.eTag,
518
+ lastModified = metadata.lastModified
519
+ )
520
+ }
521
+
405
522
  private fun generateCacheKey(url: String): String {
406
523
  try {
407
- val md = MessageDigest.getInstance("MD5")
524
+ val md = MessageDigest.getInstance("SHA-256")
408
525
  val digest = md.digest(url.toByteArray())
409
526
  return digest.joinToString("") { "%02x".format(it) }
410
527
  } catch (e: Exception) {
411
528
  Log.e(TAG, "Error generating cache key: ${e.message}")
412
- // Fallback to a simple hash code if MD5 fails
529
+ // Fallback to a simple hash code if SHA-256 fails
413
530
  return url.hashCode().toString()
414
531
  }
415
532
  }
@@ -459,4 +576,4 @@ class WebCacheManager(private val context: Context, private val properties: java
459
576
  )
460
577
  }
461
578
  }
462
- }
579
+ }
@@ -0,0 +1,123 @@
1
+ package io.yourname.androidproject.camera
2
+
3
+ import android.util.Log
4
+ import androidx.camera.core.ImageAnalysis
5
+ import androidx.camera.core.ImageProxy
6
+ import com.google.mlkit.vision.barcode.BarcodeScanner
7
+ import com.google.mlkit.vision.barcode.BarcodeScannerOptions
8
+ import com.google.mlkit.vision.barcode.BarcodeScanning
9
+ import com.google.mlkit.vision.barcode.ZoomSuggestionOptions
10
+ import com.google.mlkit.vision.barcode.common.Barcode
11
+ import com.google.mlkit.vision.common.InputImage
12
+ import java.util.concurrent.ExecutorService
13
+
14
+ /**
15
+ * Wraps ML Kit barcode scanning.
16
+ * Builds the scanner with format filter and optional auto-zoom.
17
+ * Fires [onDetected] for each barcode found in a frame.
18
+ */
19
+ class BarcodeDetector(
20
+ private val zoomController: ZoomController,
21
+ private val onDetected: (barcode: Barcode, imageWidth: Int, imageHeight: Int) -> Unit
22
+ ) {
23
+
24
+ private val TAG = "BarcodeDetector"
25
+
26
+ private var scanner: BarcodeScanner? = null
27
+
28
+ /** When true, frames are still analyzed (no flicker) but results are not forwarded. */
29
+ @Volatile var suppressResults = false
30
+
31
+ /** scan: "qr" | "barcode" | "all" */
32
+ fun buildImageAnalysis(
33
+ executor: ExecutorService,
34
+ resolutionSelector: androidx.camera.core.resolutionselector.ResolutionSelector,
35
+ autoZoomEnabled: Boolean,
36
+ scanFormat: String
37
+ ): ImageAnalysis {
38
+ val formats = resolveFormats(scanFormat)
39
+ val optionsBuilder = BarcodeScannerOptions.Builder()
40
+ .setBarcodeFormats(formats.first(), *formats.drop(1).toIntArray())
41
+
42
+ if (autoZoomEnabled) {
43
+ Log.d(TAG, "Auto-zoom enabled, wiring ZoomSuggestionOptions")
44
+ optionsBuilder.setZoomSuggestionOptions(
45
+ ZoomSuggestionOptions.Builder { suggestedRatio ->
46
+ zoomController.onAutoZoomSuggestion(suggestedRatio)
47
+ }
48
+ .setMaxSupportedZoomRatio(zoomController.currentMaxZoomRatio())
49
+ .build()
50
+ )
51
+ }
52
+
53
+ scanner?.close()
54
+ val newScanner = BarcodeScanning.getClient(optionsBuilder.build())
55
+ scanner = newScanner
56
+
57
+ return ImageAnalysis.Builder()
58
+ .setResolutionSelector(resolutionSelector)
59
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
60
+ .build()
61
+ .also { analysis ->
62
+ // Capture newScanner (not the field) so this analyzer always uses
63
+ // the scanner instance that was live when buildImageAnalysis() ran.
64
+ analysis.setAnalyzer(executor) { imageProxy ->
65
+ processFrame(imageProxy, newScanner)
66
+ }
67
+ }
68
+ }
69
+
70
+ fun close() {
71
+ scanner?.close()
72
+ scanner = null
73
+ }
74
+
75
+ private fun processFrame(imageProxy: ImageProxy, activeScanner: BarcodeScanner) {
76
+ val mediaImage = imageProxy.image
77
+ if (mediaImage == null) {
78
+ imageProxy.close()
79
+ return
80
+ }
81
+ val rotDeg = imageProxy.imageInfo.rotationDegrees
82
+ val image = InputImage.fromMediaImage(mediaImage, rotDeg)
83
+ // Swap w/h for 90°/270° — ML Kit boxes are in post-rotation space
84
+ val imgW = if (rotDeg == 90 || rotDeg == 270) imageProxy.height else imageProxy.width
85
+ val imgH = if (rotDeg == 90 || rotDeg == 270) imageProxy.width else imageProxy.height
86
+
87
+ activeScanner.process(image)
88
+ .addOnSuccessListener { barcodes ->
89
+ if (!suppressResults) {
90
+ barcodes.forEach { onDetected(it, imgW, imgH) }
91
+ }
92
+ }
93
+ .addOnFailureListener { e -> Log.w(TAG, "Barcode processing failed", e) }
94
+ .addOnCompleteListener { imageProxy.close() }
95
+ }
96
+
97
+ private fun resolveFormats(scan: String): List<Int> = when (scan) {
98
+ "qr" -> listOf(Barcode.FORMAT_QR_CODE)
99
+ "barcode" -> listOf(
100
+ Barcode.FORMAT_EAN_13, Barcode.FORMAT_EAN_8,
101
+ Barcode.FORMAT_CODE_128, Barcode.FORMAT_CODE_39,
102
+ Barcode.FORMAT_UPC_A, Barcode.FORMAT_UPC_E,
103
+ Barcode.FORMAT_PDF417
104
+ )
105
+ else -> listOf(Barcode.FORMAT_ALL_FORMATS)
106
+ }
107
+
108
+ companion object {
109
+ fun formatName(format: Int): String = when (format) {
110
+ Barcode.FORMAT_QR_CODE -> "QR"
111
+ Barcode.FORMAT_EAN_13 -> "EAN_13"
112
+ Barcode.FORMAT_EAN_8 -> "EAN_8"
113
+ Barcode.FORMAT_CODE_128 -> "CODE_128"
114
+ Barcode.FORMAT_CODE_39 -> "CODE_39"
115
+ Barcode.FORMAT_DATA_MATRIX -> "DATA_MATRIX"
116
+ Barcode.FORMAT_PDF417 -> "PDF417"
117
+ Barcode.FORMAT_AZTEC -> "AZTEC"
118
+ Barcode.FORMAT_UPC_A -> "UPC_A"
119
+ Barcode.FORMAT_UPC_E -> "UPC_E"
120
+ else -> "UNKNOWN"
121
+ }
122
+ }
123
+ }