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

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 (136) 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 +9 -0
  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 +31 -3
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +31 -3
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +148 -0
  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/TransitionManager.kt +202 -0
  12. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +22 -15
  13. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
  14. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
  15. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
  18. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
  23. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
  24. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
  25. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
  26. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
  27. package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
  28. package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
  29. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
  30. package/dist/native/androidSetup.js +1 -0
  31. package/dist/native/bridge/WebBridge.js +5 -5
  32. package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
  33. package/dist/native/bridge/hooks/index.js +1 -0
  34. package/dist/native/bridge/hooks/useCamera.js +9 -0
  35. package/dist/native/bridge/hooks/useDataProtection.js +2 -0
  36. package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
  37. package/dist/native/bridge/hooks/useFilePicker.js +2 -0
  38. package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
  39. package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
  40. package/dist/native/bridge/hooks/useIntent.js +1 -0
  41. package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
  42. package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
  43. package/dist/native/bridge/hooks/useNotification.js +2 -0
  44. package/dist/native/bridge/hooks/useSafeArea.js +2 -0
  45. package/dist/native/bridge/hooks/useVideoStream.js +6 -0
  46. package/dist/native/bridge/hooks/utils.js +1 -0
  47. package/dist/native/bridge/hooks.js +1 -137
  48. package/dist/native/bridge/useBaseHook.js +12 -7
  49. package/dist/native/bridge/utils/NativeBridge.js +41 -6
  50. package/dist/native/buildAndroid/assets.js +52 -0
  51. package/dist/native/buildAndroid/build.js +19 -0
  52. package/dist/native/buildAndroid/config.js +1 -0
  53. package/dist/native/buildAndroid/index.js +1 -0
  54. package/dist/native/buildAppAndroid.js +2 -150
  55. package/dist/native/buildAppIos.js +2 -312
  56. package/dist/native/buildIos/assets.js +8 -0
  57. package/dist/native/buildIos/build.js +40 -0
  58. package/dist/native/buildIos/config.js +71 -0
  59. package/dist/native/buildIos/index.js +6 -0
  60. package/dist/native/buildIos/plugins.js +45 -0
  61. package/dist/native/internalPluginUtils.js +7 -0
  62. package/dist/native/iosnativeWebView/Package.resolved +36 -0
  63. package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
  64. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
  65. package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
  66. package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
  67. package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
  68. package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
  69. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
  70. package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
  71. package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
  72. package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
  73. package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +2 -1
  74. package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
  75. package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
  76. package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
  77. package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
  78. package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
  79. package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
  80. package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
  81. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +38 -7
  82. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +6 -2
  83. package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +30 -11
  84. package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
  85. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
  86. package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
  87. package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
  88. package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
  89. package/dist/native/plugin-bridge/PluginBridge.js +1 -0
  90. package/dist/native/pluginComposerAndroid.js +28 -0
  91. package/dist/native/pluginComposerIos.js +16 -0
  92. package/dist/native/renameAndroidProject.js +3 -3
  93. package/dist/native/setupEmulatorIos.js +6 -4
  94. package/dist/native/setupServer.js +2 -3
  95. package/dist/native/utils.js +4 -1
  96. package/dist/otel.js +70 -2
  97. package/dist/scripts/build.js +1 -1
  98. package/dist/scripts/cleanCache.js +1 -0
  99. package/dist/scripts/devBuild.js +1 -1
  100. package/dist/scripts/devServe.js +1 -1
  101. package/dist/scripts/loadEnvironmentVariables.js +1 -2
  102. package/dist/scripts/plugins.js +1 -0
  103. package/dist/scripts/registerAliases.js +6 -2
  104. package/dist/scripts/scriptUtils.js +2 -1
  105. package/dist/scripts/serve.js +2 -1
  106. package/dist/scripts/start.js +7 -2
  107. package/dist/server/expressServer.js +3 -8
  108. package/dist/server/renderer/document/Body.js +4 -4
  109. package/dist/server/renderer/document/Head.js +2 -2
  110. package/dist/server/renderer/extract.js +31 -27
  111. package/dist/server/renderer/handler.js +47 -24
  112. package/dist/server/renderer/index.js +1 -1
  113. package/dist/server/startServer.js +57 -7
  114. package/dist/server/utils/userAgentUtil.js +9 -3
  115. package/dist/server/utils/validator.js +1 -1
  116. package/dist/webpack/babel-plugins/remove-client.plugin.js +2 -2
  117. package/dist/webpack/babel-plugins/remove-ssr.plugin.js +2 -2
  118. package/dist/webpack/babel.config.client.js +1 -1
  119. package/dist/webpack/babel.config.ssr.js +1 -1
  120. package/dist/webpack/base.babel.js +7 -8
  121. package/dist/webpack/development.client.babel.js +7 -16
  122. package/dist/webpack/production.client.babel.js +1 -1
  123. package/dist/webpack/production.ssr.babel.js +5 -1
  124. package/mcp_v2/knowledge-base.json +193 -100
  125. package/mcp_v2/lib/helpers.js +42 -8
  126. package/mcp_v2/lib/seed.js +88 -0
  127. package/mcp_v2/mcp.js +150 -4
  128. package/mcp_v2/setup.js +30 -193
  129. package/mcp_v2/tools/build.js +1 -1
  130. package/mcp_v2/tools/config.js +16 -4
  131. package/mcp_v2/tools/conversion.js +11 -11
  132. package/mcp_v2/tools/github.js +1117 -0
  133. package/mcp_v2/tools/knowledge.js +16 -3
  134. package/mcp_v2/tools/sync.js +179 -10
  135. package/mcp_v2/tools/tasks.js +13 -4
  136. package/package.json +20 -8
@@ -0,0 +1,337 @@
1
+ package io.yourname.androidproject
2
+
3
+ import android.graphics.Color
4
+ import android.graphics.RectF
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import android.util.Log
8
+ import android.view.MotionEvent
9
+ import android.view.View
10
+ import android.webkit.WebView
11
+ import android.widget.TextView
12
+ import androidx.camera.view.PreviewView
13
+ import com.google.mlkit.vision.barcode.common.Barcode
14
+ import io.yourname.androidproject.camera.*
15
+ import io.yourname.androidproject.utils.BridgeUtils
16
+ import org.json.JSONObject
17
+
18
+ /**
19
+ * Thin facade — wires all camera sub-components and exposes the public API
20
+ * that NativeBridge.kt calls. All logic lives in the camera/ package.
21
+ */
22
+ class NativeCameraManager(
23
+ private val activity: MainActivity,
24
+ private val previewView: PreviewView,
25
+ private val webView: WebView,
26
+ private val debugOverlay: View? = null,
27
+ private val debugQrStatus: TextView? = null,
28
+ private val debugBarcodeOverlay: View? = null
29
+ ) {
30
+
31
+ companion object {
32
+ private const val TAG = "NativeCameraManager"
33
+ const val PERMISSION_REQUEST_CODE = CameraSessionManager.PERMISSION_REQUEST_CODE
34
+
35
+ // QR overlay colors — change here to update both states
36
+ private const val COLOR_QR_IN_FRAME = "#5500FF00" // green, semi-transparent
37
+ private const val COLOR_QR_DETECTED = "#55FF6600" // orange, semi-transparent
38
+ private const val OVERLAY_HIDE_DELAY_MS = 200L
39
+ }
40
+
41
+ private val stateMachine = VideoStreamStateMachine()
42
+
43
+ private val zoomController = ZoomController(
44
+ context = activity,
45
+ stateMachine = stateMachine,
46
+ onZoomChanged = { zoomLevel, minZoom, maxZoom ->
47
+ val payload = JSONObject().apply {
48
+ // zoomLevel = raw CameraX zoomRatio (1.0=1x, 2.0=2x)
49
+ put("zoomLevel", Math.round(zoomLevel * 10.0) / 10.0)
50
+ put("minZoom", Math.round(minZoom * 10.0) / 10.0)
51
+ put("maxZoom", Math.round(maxZoom * 10.0) / 10.0)
52
+ }
53
+ BridgeUtils.notifyWebJson(webView, BridgeUtils.WebEvents.ON_ZOOM_CHANGED, payload)
54
+ // Repaint overlay at new zoom level — barcode rect shifts as camera zooms
55
+ if (showQrDetected && overlayVisible) {
56
+ lastBarcodeScreenRect?.let { repaintQrOverlay(it, overlayIsOrange) }
57
+ }
58
+ }
59
+ )
60
+
61
+ private val torchController = TorchController(
62
+ context = activity,
63
+ onTorchChanged = { enabled ->
64
+ val payload = JSONObject().apply { put("enabled", enabled) }
65
+ BridgeUtils.notifyWebJson(webView, BridgeUtils.WebEvents.ON_TORCH_CHANGED, payload)
66
+ }
67
+ )
68
+
69
+ private val barcodeDetector = BarcodeDetector(
70
+ zoomController = zoomController,
71
+ onDetected = { barcode, imgW, imgH -> onBarcodeDetected(barcode, imgW, imgH) }
72
+ )
73
+
74
+ private val sessionManager = CameraSessionManager(
75
+ activity = activity,
76
+ previewView = previewView,
77
+ stateMachine = stateMachine,
78
+ zoomController = zoomController,
79
+ torchController = torchController,
80
+ barcodeDetector = barcodeDetector,
81
+ onReady = {
82
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.ON_VIDEO_STREAM_READY)
83
+ },
84
+ onStopped = {
85
+ debugOverlay?.let { activity.runOnUiThread { it.visibility = View.GONE } }
86
+ BridgeUtils.notifyWeb(webView, BridgeUtils.WebEvents.ON_VIDEO_STREAM_STOPPED)
87
+ },
88
+ onError = { message ->
89
+ BridgeUtils.notifyWebError(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, message)
90
+ }
91
+ )
92
+
93
+ private val holdController = HoldController(
94
+ stateMachine = stateMachine,
95
+ barcodeDetector = barcodeDetector
96
+ )
97
+
98
+ // Viewfinder state — written on main thread, read on analyzer thread
99
+ @Volatile private var viewfinderScreenRect: RectF? = null
100
+
101
+ // QR detected overlay state — written on analyzer thread, read on main thread and vice versa
102
+ @Volatile private var showQrDetected: Boolean = false
103
+ @Volatile private var overlayIsOrange: Boolean = false // logical state — set before showQrOverlay
104
+ @Volatile private var overlayPaintedOrange: Boolean = false // what color was last painted
105
+ @Volatile private var overlayVisible: Boolean = false
106
+ @Volatile private var lastBarcodeScreenRect: RectF? = null
107
+ private val overlayHideHandler = Handler(Looper.getMainLooper())
108
+ @Volatile private var overlayHideRunnable: Runnable? = null
109
+
110
+ init {
111
+ stateMachine.addListener(object : VideoStreamStateListener {
112
+ override fun onStateChanged(prev: VideoStreamState, next: VideoStreamState) {
113
+ // Reset overlay when hold ends (new scan cycle) or stream stops
114
+ if ((prev == VideoStreamState.HOLD && next == VideoStreamState.STREAMING) ||
115
+ next == VideoStreamState.IDLE || next == VideoStreamState.STOPPING
116
+ ) {
117
+ hideQrOverlay()
118
+ }
119
+ }
120
+ })
121
+ }
122
+
123
+ // ---- Public API (called from NativeBridge) ----
124
+
125
+ fun start(facing: String = "back", viewfinderRectJson: JSONObject? = null, zoomOptions: JSONObject? = null, scanFormat: String = "all", fpsMin: Int? = null, fpsMax: Int? = null, showQrDetected: Boolean = false) {
126
+ if (stateMachine.isActive) return
127
+
128
+ this.showQrDetected = showQrDetected
129
+ this.overlayIsOrange = false
130
+ this.overlayPaintedOrange = false
131
+ this.overlayVisible = false
132
+ this.lastBarcodeScreenRect = null
133
+
134
+ val autoZoom = zoomOptions?.optBoolean("auto", false) ?: false
135
+ val initialZoomPct = zoomOptions?.optDouble("initial", 1.0)?.toFloat() ?: 1.0f
136
+
137
+ // Parse viewfinder rect and position debug overlay
138
+ viewfinderRectJson?.let { json ->
139
+ val result = ViewfinderMapper.parseViewfinderRect(json, webView)
140
+ viewfinderScreenRect = result?.first
141
+ result?.second?.let { jsRect -> positionDebugOverlay(jsRect) }
142
+ } ?: run {
143
+ viewfinderScreenRect = null
144
+ Log.w(TAG, "No viewfinderRect — all QR codes will be reported")
145
+ }
146
+
147
+ stateMachine.transition(VideoStreamState.STARTING)
148
+ sessionManager.start(
149
+ facing = facing,
150
+ autoZoom = autoZoom,
151
+ initialZoomPct = initialZoomPct, // now Float multiplier (1.0 = 1x)
152
+ scanFormat = scanFormat,
153
+ fpsMin = fpsMin,
154
+ fpsMax = fpsMax
155
+ )
156
+ }
157
+
158
+ fun stop() {
159
+ if (!stateMachine.isActive) return
160
+ holdController.reset()
161
+ stateMachine.transition(VideoStreamState.STOPPING)
162
+ sessionManager.stop()
163
+ }
164
+
165
+ fun flip() {
166
+ if (!stateMachine.isActive) return
167
+ holdController.reset()
168
+ stateMachine.transition(VideoStreamState.FLIPPING)
169
+ sessionManager.flip()
170
+ }
171
+
172
+ fun setZoom(multiplier: Float) = zoomController.setZoom(multiplier)
173
+
174
+ fun setTorch(on: Boolean) = torchController.setTorch(on)
175
+
176
+ fun setFps(min: Int?, max: Int?) {
177
+ if (!stateMachine.isActive) return
178
+ sessionManager.setFps(min, max)
179
+ }
180
+
181
+ fun onTouchEvent(event: MotionEvent): Boolean = zoomController.onTouchEvent(event)
182
+
183
+ fun onPermissionResult(granted: Boolean) = sessionManager.onPermissionResult(granted)
184
+
185
+ fun cleanup() {
186
+ overlayHideRunnable?.let { overlayHideHandler.removeCallbacks(it) }
187
+ overlayHideRunnable = null
188
+ sessionManager.cleanup()
189
+ }
190
+
191
+ // ---- Detection handler ----
192
+
193
+ private fun onBarcodeDetected(barcode: Barcode, imageWidth: Int, imageHeight: Int) {
194
+ val value = barcode.rawValue ?: return
195
+
196
+ // Hold gate — defensive check for in-flight frames
197
+ if (stateMachine.state == VideoStreamState.HOLD) {
198
+ Log.d(TAG, "onBarcodeDetected — in hold, skipping: $value")
199
+ return
200
+ }
201
+
202
+ // QR overlay — show green immediately (QR in frame), update position each frame
203
+ val barcodeScreenRect = barcode.boundingBox?.let { box ->
204
+ ViewfinderMapper.mapBarcodeToScreen(
205
+ box.left, box.top, box.right, box.bottom,
206
+ imageWidth, imageHeight,
207
+ previewView.width.toFloat(), previewView.height.toFloat()
208
+ )
209
+ }
210
+ if (showQrDetected && barcodeScreenRect != null) {
211
+ showQrOverlay(barcodeScreenRect, orange = overlayIsOrange)
212
+ }
213
+
214
+ // Viewfinder region filter
215
+ val regionRect = viewfinderScreenRect
216
+ if (regionRect != null) {
217
+ if (barcodeScreenRect != null) {
218
+ if (!regionRect.contains(barcodeScreenRect)) {
219
+ updateDebugQrStatus(inside = false)
220
+ return
221
+ }
222
+ updateDebugQrStatus(inside = true)
223
+ }
224
+ }
225
+
226
+ // Same-value dedup — enter hold either way
227
+ if (!holdController.isNewValue(value)) {
228
+ Log.d(TAG, "onBarcodeDetected — same value, suppressing: $value")
229
+ holdController.startHold()
230
+ return
231
+ }
232
+
233
+ // New value confirmed — flip overlay to orange before firing event
234
+ if (showQrDetected && barcodeScreenRect != null) {
235
+ overlayIsOrange = true
236
+ showQrOverlay(barcodeScreenRect, orange = true)
237
+ }
238
+
239
+ holdController.startHold()
240
+
241
+ val payload = JSONObject().apply {
242
+ put("value", value)
243
+ put("format", BarcodeDetector.formatName(barcode.format))
244
+ }
245
+ BridgeUtils.notifyWebJson(webView, BridgeUtils.WebEvents.ON_QR_DETECTED, payload)
246
+ Log.d(TAG, "QR detected (new): $value")
247
+ }
248
+
249
+ // ---- QR detected overlay ----
250
+
251
+ /**
252
+ * Called on every ML Kit frame that has a barcode.
253
+ * If overlay is already visible: only reschedule the hide — no layout/color update (prevents flicker).
254
+ * If overlay is not visible: position it, set color, show it.
255
+ * lastBarcodeScreenRect is always updated so zoom repaints use the freshest position.
256
+ */
257
+ private fun showQrOverlay(screenRect: RectF, orange: Boolean) {
258
+ lastBarcodeScreenRect = screenRect
259
+
260
+ // Reschedule hide regardless
261
+ overlayHideRunnable?.let { overlayHideHandler.removeCallbacks(it) }
262
+ val hideRunnable = Runnable { hideQrOverlayOnUiThread() }
263
+ overlayHideRunnable = hideRunnable
264
+ overlayHideHandler.postDelayed(hideRunnable, OVERLAY_HIDE_DELAY_MS)
265
+
266
+ if (overlayVisible && orange == overlayPaintedOrange) return // already painted at correct color — skip to prevent flicker
267
+
268
+ repaintQrOverlay(screenRect, orange)
269
+ }
270
+
271
+ /**
272
+ * Force-reposition and repaint the overlay. Called on first show and on zoom change.
273
+ * screenRect is in PreviewView/RelativeLayout space — no offset adjustment needed.
274
+ */
275
+ private fun repaintQrOverlay(screenRect: RectF, orange: Boolean) {
276
+ val color = Color.parseColor(if (orange) COLOR_QR_DETECTED else COLOR_QR_IN_FRAME)
277
+ overlayVisible = true
278
+ overlayPaintedOrange = orange
279
+ activity.runOnUiThread {
280
+ debugBarcodeOverlay?.let { overlay ->
281
+ val lp = overlay.layoutParams as? android.widget.RelativeLayout.LayoutParams ?: return@runOnUiThread
282
+ lp.leftMargin = screenRect.left.toInt()
283
+ lp.topMargin = screenRect.top.toInt()
284
+ lp.width = screenRect.width().toInt()
285
+ lp.height = screenRect.height().toInt()
286
+ overlay.layoutParams = lp
287
+ overlay.setBackgroundColor(color)
288
+ overlay.visibility = View.VISIBLE
289
+ }
290
+ }
291
+ }
292
+
293
+ private fun hideQrOverlay() {
294
+ overlayHideRunnable?.let { overlayHideHandler.removeCallbacks(it) }
295
+ overlayHideRunnable = null
296
+ overlayIsOrange = false
297
+ overlayPaintedOrange = false
298
+ lastBarcodeScreenRect = null
299
+ hideQrOverlayOnUiThread()
300
+ }
301
+
302
+ private fun hideQrOverlayOnUiThread() {
303
+ overlayVisible = false
304
+ activity.runOnUiThread {
305
+ debugBarcodeOverlay?.visibility = View.GONE
306
+ }
307
+ }
308
+
309
+ // ---- Debug overlay helpers ----
310
+
311
+ private fun positionDebugOverlay(jsRect: RectF) {
312
+ activity.runOnUiThread {
313
+ debugOverlay?.let { overlay ->
314
+ val lp = overlay.layoutParams as? android.widget.RelativeLayout.LayoutParams ?: return@runOnUiThread
315
+ lp.leftMargin = jsRect.left.toInt()
316
+ lp.topMargin = jsRect.top.toInt()
317
+ lp.width = jsRect.width().toInt()
318
+ lp.height = jsRect.height().toInt()
319
+ overlay.layoutParams = lp
320
+ overlay.setBackgroundResource(R.drawable.debug_viewfinder_border)
321
+ overlay.visibility = View.VISIBLE
322
+ }
323
+ }
324
+ }
325
+
326
+ private fun updateDebugQrStatus(inside: Boolean) {
327
+ activity.runOnUiThread {
328
+ if (inside) {
329
+ debugQrStatus?.setTextColor(Color.parseColor("#34C759"))
330
+ debugQrStatus?.text = "⬤ QR inside"
331
+ } else {
332
+ debugQrStatus?.setTextColor(Color.parseColor("#FF3B30"))
333
+ debugQrStatus?.text = "⬤ QR outside"
334
+ }
335
+ }
336
+ }
337
+ }
@@ -0,0 +1,202 @@
1
+ package io.yourname.androidproject
2
+
3
+ import android.animation.Animator
4
+ import android.animation.AnimatorListenerAdapter
5
+ import android.animation.ValueAnimator
6
+ import android.app.Activity
7
+ import android.graphics.Bitmap
8
+ import android.graphics.Canvas
9
+ import android.os.Handler
10
+ import android.os.Looper
11
+ import android.util.Log
12
+ import android.view.View
13
+ import android.view.ViewGroup
14
+ import android.webkit.WebView
15
+ import android.widget.FrameLayout
16
+ import android.widget.ImageView
17
+ import io.yourname.androidproject.utils.BridgeUtils
18
+
19
+ /**
20
+ * Manages native page transitions using a snapshot-overlay pattern.
21
+ *
22
+ * Flow:
23
+ * startTransition() — captures a bitmap of the current WebView, places it as an
24
+ * ImageView overlay on top of the content, covering the skeleton
25
+ * rendered by the router while the new page loads.
26
+ * commitTransition() — animates the overlay out (slide or fade) to reveal the new page.
27
+ * cancelTransition() — removes the overlay immediately without animating.
28
+ *
29
+ * Safety timer: a Handler timer fires on the main thread after `timeout` ms if
30
+ * commitTransition() was never called (JS error/hang). It force-fades the overlay
31
+ * and fires ON_TRANSITION_TIMEOUT to JS so the hook can reset its state.
32
+ *
33
+ * Thread safety: all public methods must be called on the main thread.
34
+ */
35
+ class TransitionManager(
36
+ private val activity: Activity,
37
+ private val webView: WebView
38
+ ) {
39
+ private val TAG = "TransitionManager"
40
+ private val mainHandler = Handler(Looper.getMainLooper())
41
+
42
+ private var overlayView: ImageView? = null
43
+ private var safetyRunnable: Runnable? = null
44
+ private var activeDuration: Int = 300
45
+ private var activeType: String = "slide"
46
+ private var activeDirection: String = "left"
47
+
48
+ // -------------------------------------------------------------------------
49
+ // Public API
50
+ // -------------------------------------------------------------------------
51
+
52
+ fun startTransition(type: String, direction: String, duration: Int, timeout: Int) {
53
+ // Cancel any orphaned transition first
54
+ cancelTransitionInternal(notify = false)
55
+
56
+ activeType = type
57
+ activeDirection = direction
58
+ activeDuration = duration
59
+
60
+ val snapshot = captureSnapshot() ?: run {
61
+ Log.w(TAG, "startTransition — snapshot failed, transition skipped")
62
+ return
63
+ }
64
+
65
+ val overlay = ImageView(activity).apply {
66
+ setImageBitmap(snapshot)
67
+ layoutParams = ViewGroup.LayoutParams(
68
+ ViewGroup.LayoutParams.MATCH_PARENT,
69
+ ViewGroup.LayoutParams.MATCH_PARENT
70
+ )
71
+ scaleType = ImageView.ScaleType.MATRIX
72
+ }
73
+
74
+ val root = activity.window.decorView as ViewGroup
75
+ root.addView(overlay)
76
+ overlayView = overlay
77
+
78
+ Log.d(TAG, "startTransition — overlay attached (type=$type direction=$direction duration=${duration}ms timeout=${timeout}ms)")
79
+
80
+ // Arm safety timer
81
+ val safety = Runnable {
82
+ Log.w(TAG, "startTransition — safety timeout fired, force-removing overlay")
83
+ cancelTransitionInternal(notify = false)
84
+ BridgeUtils.notifyWebSuccess(webView, BridgeUtils.WebEvents.ON_TRANSITION_TIMEOUT)
85
+ }
86
+ safetyRunnable = safety
87
+ mainHandler.postDelayed(safety, timeout.toLong())
88
+ }
89
+
90
+ fun commitTransition() {
91
+ val overlay = overlayView ?: run {
92
+ Log.w(TAG, "commitTransition — no active overlay, ignoring")
93
+ return
94
+ }
95
+
96
+ // Disarm safety timer — we're committing normally
97
+ disarmSafetyTimer()
98
+
99
+ when (activeType) {
100
+ "fade" -> animateFadeOut(overlay, activeDuration)
101
+ else -> animateSlideOut(overlay, activeDuration, activeDirection)
102
+ }
103
+ }
104
+
105
+ fun cancelTransition() {
106
+ cancelTransitionInternal(notify = true)
107
+ }
108
+
109
+ fun cleanup() {
110
+ cancelTransitionInternal(notify = false)
111
+ }
112
+
113
+ // -------------------------------------------------------------------------
114
+ // Private helpers
115
+ // -------------------------------------------------------------------------
116
+
117
+ private fun captureSnapshot(): Bitmap? {
118
+ return try {
119
+ val bmp = Bitmap.createBitmap(webView.width, webView.height, Bitmap.Config.ARGB_8888)
120
+ val canvas = Canvas(bmp)
121
+ webView.draw(canvas)
122
+ bmp
123
+ } catch (e: Exception) {
124
+ Log.e(TAG, "captureSnapshot failed", e)
125
+ null
126
+ }
127
+ }
128
+
129
+ private fun animateSlideOut(overlay: ImageView, duration: Int, direction: String) {
130
+ val root = activity.window.decorView as ViewGroup
131
+ val w = root.width.toFloat()
132
+ val h = root.height.toFloat()
133
+
134
+ val (endX, endY) = when (direction) {
135
+ "right" -> Pair(-w, 0f)
136
+ "up" -> Pair(0f, h)
137
+ "down" -> Pair(0f, -h)
138
+ else -> Pair(w, 0f) // "left" — overlay slides out to the right
139
+ }
140
+
141
+ ValueAnimator.ofFloat(0f, 1f).apply {
142
+ this.duration = duration.toLong()
143
+ addUpdateListener { anim ->
144
+ val fraction = anim.animatedFraction
145
+ overlay.translationX = endX * fraction
146
+ overlay.translationY = endY * fraction
147
+ }
148
+ addListener(object : AnimatorListenerAdapter() {
149
+ override fun onAnimationEnd(animation: Animator) {
150
+ removeOverlay(overlay)
151
+ BridgeUtils.notifyWebSuccess(webView, BridgeUtils.WebEvents.ON_TRANSITION_COMMITTED)
152
+ }
153
+ })
154
+ start()
155
+ }
156
+ }
157
+
158
+ private fun animateFadeOut(overlay: ImageView, duration: Int) {
159
+ ValueAnimator.ofFloat(1f, 0f).apply {
160
+ this.duration = duration.toLong()
161
+ addUpdateListener { anim ->
162
+ overlay.alpha = anim.animatedValue as Float
163
+ }
164
+ addListener(object : AnimatorListenerAdapter() {
165
+ override fun onAnimationEnd(animation: Animator) {
166
+ removeOverlay(overlay)
167
+ BridgeUtils.notifyWebSuccess(webView, BridgeUtils.WebEvents.ON_TRANSITION_COMMITTED)
168
+ }
169
+ })
170
+ start()
171
+ }
172
+ }
173
+
174
+ private fun cancelTransitionInternal(notify: Boolean) {
175
+ disarmSafetyTimer()
176
+
177
+ overlayView?.let { overlay ->
178
+ removeOverlay(overlay)
179
+ if (notify) {
180
+ BridgeUtils.notifyWebSuccess(webView, BridgeUtils.WebEvents.ON_TRANSITION_CANCELLED)
181
+ }
182
+ }
183
+ }
184
+
185
+ private fun removeOverlay(overlay: ImageView) {
186
+ try {
187
+ val root = activity.window.decorView as ViewGroup
188
+ root.removeView(overlay)
189
+ } catch (e: Exception) {
190
+ Log.w(TAG, "removeOverlay — view already detached: ${e.message}")
191
+ }
192
+ if (overlayView === overlay) {
193
+ overlayView = null
194
+ }
195
+ overlay.setImageBitmap(null)
196
+ }
197
+
198
+ private fun disarmSafetyTimer() {
199
+ safetyRunnable?.let { mainHandler.removeCallbacks(it) }
200
+ safetyRunnable = null
201
+ }
202
+ }
@@ -330,25 +330,27 @@ class WebCacheManager(private val context: Context, private val properties: java
330
330
  val eTag = connection.getHeaderField("ETag")
331
331
  val lastModified = connection.getHeaderField("Last-Modified")
332
332
 
333
+ // Capture all response headers for replay on cache hits
334
+ val savedHeaders = mutableMapOf<String, String>()
335
+ var i = 0
336
+ while (true) {
337
+ val key = connection.getHeaderFieldKey(i) ?: break
338
+ val value = connection.getHeaderField(i)
339
+ if (key.isNotEmpty()) savedHeaders[key] = value
340
+ i++
341
+ }
342
+
333
343
  val responseBytes = connection.inputStream.use { it.readBytes() }
334
344
  if (!isValidResponse(mimeType, responseBytes)) {
335
345
  return@withContext null
336
346
  }
337
347
 
338
348
  // Create response for immediate use
339
- val response = WebResourceResponse(
340
- mimeType,
341
- encoding,
342
- ByteArrayInputStream(responseBytes)
343
- )
349
+ val response = WebResourceResponse(mimeType, encoding, 200, "OK", savedHeaders, ByteArrayInputStream(responseBytes))
344
350
 
345
351
  // Cache the response
346
352
  val cacheEntry = CacheEntry(
347
- response = WebResourceResponse(
348
- mimeType,
349
- encoding,
350
- ByteArrayInputStream(responseBytes.copyOf())
351
- ),
353
+ response = WebResourceResponse(mimeType, encoding, 200, "OK", savedHeaders, ByteArrayInputStream(responseBytes.copyOf())),
352
354
  eTag = eTag,
353
355
  lastModified = lastModified
354
356
  )
@@ -363,7 +365,8 @@ class WebCacheManager(private val context: Context, private val properties: java
363
365
  mimeType = mimeType,
364
366
  encoding = encoding,
365
367
  eTag = eTag,
366
- lastModified = lastModified
368
+ lastModified = lastModified,
369
+ responseHeaders = savedHeaders
367
370
  ))
368
371
  if (BuildConfig.DEBUG) {
369
372
  Log.d(TAG, "✅ Successfully cached response for: $url")
@@ -391,25 +394,29 @@ class WebCacheManager(private val context: Context, private val properties: java
391
394
  val mimeType: String,
392
395
  val encoding: String,
393
396
  val eTag: String? = null,
394
- val lastModified: String? = null
397
+ val lastModified: String? = null,
398
+ val responseHeaders: Map<String, String> = emptyMap()
395
399
  ) : Serializable
396
400
 
397
401
  private fun createResponseFromCache(cacheFile: File, metadata: CacheMetadata): WebResourceResponse {
398
402
  return WebResourceResponse(
399
403
  metadata.mimeType,
400
404
  metadata.encoding,
405
+ 200,
406
+ "OK",
407
+ metadata.responseHeaders.ifEmpty { null },
401
408
  BufferedInputStream(FileInputStream(cacheFile), BUFFER_SIZE)
402
409
  )
403
410
  }
404
411
 
405
412
  private fun generateCacheKey(url: String): String {
406
413
  try {
407
- val md = MessageDigest.getInstance("MD5")
414
+ val md = MessageDigest.getInstance("SHA-256")
408
415
  val digest = md.digest(url.toByteArray())
409
416
  return digest.joinToString("") { "%02x".format(it) }
410
417
  } catch (e: Exception) {
411
418
  Log.e(TAG, "Error generating cache key: ${e.message}")
412
- // Fallback to a simple hash code if MD5 fails
419
+ // Fallback to a simple hash code if SHA-256 fails
413
420
  return url.hashCode().toString()
414
421
  }
415
422
  }
@@ -459,4 +466,4 @@ class WebCacheManager(private val context: Context, private val properties: java
459
466
  )
460
467
  }
461
468
  }
462
- }
469
+ }