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
@@ -4,6 +4,7 @@ import android.content.Intent
4
4
  import android.os.Build
5
5
  import android.os.Bundle
6
6
  import android.util.Log
7
+ import android.view.MotionEvent
7
8
  import android.view.WindowManager
8
9
  import androidx.activity.OnBackPressedCallback
9
10
  import androidx.appcompat.app.AppCompatActivity
@@ -14,6 +15,7 @@ import org.json.JSONObject
14
15
  import java.util.Properties
15
16
  import io.yourname.androidproject.databinding.ActivityMainBinding
16
17
  import io.yourname.androidproject.NativeBridge
18
+ import io.yourname.androidproject.plugins.PluginBridge
17
19
  import io.yourname.androidproject.utils.BridgeUtils
18
20
  import io.yourname.androidproject.utils.KeyboardUtil
19
21
  import io.yourname.androidproject.utils.NetworkUtils
@@ -39,6 +41,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
39
41
 
40
42
  private lateinit var binding: ActivityMainBinding
41
43
  private lateinit var nativeBridge: NativeBridge
44
+ private lateinit var pluginBridge: PluginBridge
42
45
  private lateinit var customWebView: CustomWebView
43
46
  lateinit var properties: Properties
44
47
  private lateinit var metricsMonitor: MetricsMonitor
@@ -133,6 +136,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
133
136
  "X-Safe-Area-Right" to latestSafeAreaInsets.right.toString(),
134
137
  "X-Safe-Area-Bottom" to latestSafeAreaInsets.bottom.toString(),
135
138
  "X-Safe-Area-Left" to latestSafeAreaInsets.left.toString(),
139
+ "X-Catalyst-Native-WebView" to "1",
136
140
  // Prevent caching of SSR response so updated headers are always used
137
141
  "Cache-Control" to "no-cache, no-store, must-revalidate",
138
142
  "Pragma" to "no-cache"
@@ -324,10 +328,23 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
324
328
  try {
325
329
  nativeBridge = NativeBridge(this, customWebView.getWebView(), properties)
326
330
  customWebView.addJavascriptInterface(nativeBridge, "NativeBridge")
331
+
332
+ // Wire NativeCameraManager
333
+ val cameraManager = NativeCameraManager(this, binding.cameraPreview, customWebView.getWebView(), binding.debugViewfinderOverlay, binding.debugViewfinderOverlay.findViewById(R.id.debug_qr_status), binding.debugBarcodeOverlay)
334
+ nativeBridge.setCameraManager(cameraManager)
335
+ customWebView.onPageStarted = { cameraManager.stop() }
327
336
  } catch (e: Exception) {
328
337
  Log.e(TAG, "Failed to initialize NativeBridge: ${e.message}")
329
338
  }
330
339
 
340
+ // Setup isolated PluginBridge
341
+ try {
342
+ pluginBridge = PluginBridge(this, customWebView.getWebView(), properties)
343
+ customWebView.addJavascriptInterface(pluginBridge, "PluginBridge")
344
+ } catch (e: Exception) {
345
+ Log.e(TAG, "Failed to initialize PluginBridge: ${e.message}")
346
+ }
347
+
331
348
  setupSafeAreaHandling()
332
349
 
333
350
  // Setup back press handler (modern API)
@@ -369,7 +386,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
369
386
  if (BuildConfig.DEBUG) {
370
387
  Log.w(TAG, "📴 Device offline on launch, showing offline page")
371
388
  }
372
- customWebView.showOfflinePage()
389
+ customWebView.showOfflineRouteOrOfflinePage(currentUrl)
373
390
  }
374
391
  }
375
392
 
@@ -421,6 +438,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
421
438
  customWebView.onResume()
422
439
  }
423
440
 
441
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
442
+ if (::nativeBridge.isInitialized) {
443
+ nativeBridge.getCameraManager()?.onTouchEvent(ev)
444
+ }
445
+ return super.dispatchTouchEvent(ev)
446
+ }
447
+
424
448
  override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
425
449
  super.onRequestPermissionsResult(requestCode, permissions, grantResults)
426
450
  if (::nativeBridge.isInitialized) {
@@ -445,11 +469,16 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
445
469
  if (::keyboardUtil.isInitialized) {
446
470
  keyboardUtil.cleanup()
447
471
  }
448
- if (::nativeBridge.isInitialized) {
449
- nativeBridge.cleanup()
472
+ if (::customWebView.isInitialized) {
473
+ if (::pluginBridge.isInitialized) {
474
+ customWebView.removeJavascriptInterface("PluginBridge")
475
+ }
476
+ if (::nativeBridge.isInitialized) {
477
+ customWebView.removeJavascriptInterface("NativeBridge")
478
+ }
479
+ customWebView.destroy()
450
480
  }
451
481
  coroutineContext.cancelChildren()
452
- customWebView.destroy()
453
482
  super.onDestroy()
454
483
  }
455
484
 
@@ -505,7 +534,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
505
534
  loadUrlWithSafeArea(url)
506
535
  } else {
507
536
  Log.w(TAG, "📴 Offline during notification click, showing offline page")
508
- customWebView.showOfflinePage()
537
+ customWebView.showOfflineRouteOrOfflinePage(url)
509
538
  }
510
539
 
511
540
  } catch (e: Exception) {
@@ -151,9 +151,16 @@ class NativeBridge(
151
151
 
152
152
  private var networkMonitor: NetworkMonitor? = null
153
153
 
154
+ // Video stream manager
155
+ private var nativeCameraManager: NativeCameraManager? = null
156
+ private var webViewOriginalBackground: Int = android.graphics.Color.WHITE
157
+
154
158
  // Unified notification manager
155
159
  private val notificationManager = AppNotificationManager(mainActivity, mainActivity.properties)
156
160
 
161
+ // Transition manager — snapshot overlay pattern for native page transitions
162
+ private val transitionManager = TransitionManager(mainActivity, webView)
163
+
157
164
  companion object {
158
165
  private const val TAG = "NativeBridge"
159
166
  private const val GOOGLE_SIGN_IN_SAFE_EXECUTE_ACTION = "start Google Sign-In"
@@ -206,7 +213,7 @@ class NativeBridge(
206
213
  .filter { it.isNotEmpty() }
207
214
 
208
215
  accessControlEnabled = properties
209
- .getProperty("accessControl.enabled", "true")
216
+ .getProperty("accessControl.enabled", "false")
210
217
  .equals("true", ignoreCase = true)
211
218
 
212
219
  if (accessControlEnabled && allowedUrls.isNotEmpty()) {
@@ -239,6 +246,90 @@ class NativeBridge(
239
246
  }
240
247
  }
241
248
 
249
+ fun setCameraManager(manager: NativeCameraManager) {
250
+ nativeCameraManager = manager
251
+ }
252
+
253
+ fun getCameraManager(): NativeCameraManager? = nativeCameraManager
254
+
255
+ @JavascriptInterface
256
+ fun startVideoStream(optionsRaw: String?) {
257
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "start video stream") {
258
+ android.util.Log.d("NativeBridge", "startVideoStream raw options: $optionsRaw")
259
+ val options = try { JSONObject(optionsRaw ?: "{}") } catch (e: Exception) { JSONObject() }
260
+ val facing = options.optString("facing", "back")
261
+ val viewfinderRect = options.optJSONObject("viewfinderRect")
262
+ val zoomOptions = options.optJSONObject("zoom")
263
+ val scanFormat = options.optString("format", "all")
264
+ val showQrDetected = options.optBoolean("showQrDetected", false)
265
+ android.util.Log.d("NativeBridge", "startVideoStream parsed: facing=$facing zoom=$zoomOptions scanFormat=$scanFormat viewfinderRect=$viewfinderRect showQrDetected=$showQrDetected")
266
+ mainActivity.runOnUiThread {
267
+ webViewOriginalBackground = webView.solidColor.takeIf { it != 0 } ?: android.graphics.Color.WHITE
268
+ webView.setBackgroundColor(android.graphics.Color.TRANSPARENT)
269
+ nativeCameraManager?.start(facing, viewfinderRect, zoomOptions, scanFormat, showQrDetected = showQrDetected)
270
+ }
271
+ }
272
+ }
273
+
274
+ @JavascriptInterface
275
+ fun stopVideoStream(optionsRaw: String?) {
276
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "stop video stream") {
277
+ mainActivity.runOnUiThread {
278
+ webView.setBackgroundColor(webViewOriginalBackground)
279
+ nativeCameraManager?.stop()
280
+ }
281
+ }
282
+ }
283
+
284
+ @JavascriptInterface
285
+ fun setVideoStreamZoom(multiplierRaw: String?) {
286
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "set video stream zoom") {
287
+ val multiplier = multiplierRaw?.trim()?.toFloatOrNull() ?: run {
288
+ android.util.Log.w("NativeBridge", "setVideoStreamZoom — invalid value: $multiplierRaw, ignoring")
289
+ return@safeExecute
290
+ }
291
+ android.util.Log.d("NativeBridge", "setVideoStreamZoom — multiplier=${multiplier}x")
292
+ mainActivity.runOnUiThread {
293
+ nativeCameraManager?.setZoom(multiplier)
294
+ }
295
+ }
296
+ }
297
+
298
+ @JavascriptInterface
299
+ fun setVideoStreamTorch(optionsRaw: String?) {
300
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "set video stream torch") {
301
+ val options = try { JSONObject(optionsRaw ?: "{}") } catch (e: Exception) { JSONObject() }
302
+ val on = options.optBoolean("on", false)
303
+ android.util.Log.d("NativeBridge", "setVideoStreamTorch — on=$on")
304
+ mainActivity.runOnUiThread {
305
+ nativeCameraManager?.setTorch(on)
306
+ }
307
+ }
308
+ }
309
+
310
+ @JavascriptInterface
311
+ fun setVideoStreamFps(optionsRaw: String?) {
312
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "set video stream fps") {
313
+ val options = try { JSONObject(optionsRaw ?: "{}") } catch (e: Exception) { JSONObject() }
314
+ val min = if (options.isNull("min")) null else options.optInt("min")
315
+ val max = if (options.isNull("max")) null else options.optInt("max")
316
+ android.util.Log.d("NativeBridge", "setVideoStreamFps — min=$min max=$max")
317
+ mainActivity.runOnUiThread {
318
+ nativeCameraManager?.setFps(min, max)
319
+ }
320
+ }
321
+ }
322
+
323
+ @JavascriptInterface
324
+ fun flipVideoStream(optionsRaw: String?) {
325
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "flip video stream") {
326
+ android.util.Log.d("NativeBridge", "flipVideoStream called")
327
+ mainActivity.runOnUiThread {
328
+ nativeCameraManager?.flip()
329
+ }
330
+ }
331
+ }
332
+
242
333
  @JavascriptInterface
243
334
  fun openCamera(options: String?) {
244
335
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_CAMERA_ERROR, "open camera") {
@@ -1023,6 +1114,17 @@ class NativeBridge(
1023
1114
  * Handle permission request results
1024
1115
  */
1025
1116
  fun handlePermissionResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
1117
+ if (requestCode == NativeCameraManager.PERMISSION_REQUEST_CODE) {
1118
+ val granted = grantResults.isNotEmpty() &&
1119
+ grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED
1120
+ val cam = nativeCameraManager
1121
+ if (cam == null) {
1122
+ Log.w(TAG, "handlePermissionResult — nativeCameraManager is null, ignoring camera permission result")
1123
+ return
1124
+ }
1125
+ cam.onPermissionResult(granted)
1126
+ return
1127
+ }
1026
1128
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.NOTIFICATION_PERMISSION_STATUS, "handle permission result") {
1027
1129
  // Delegate to notification manager which handles notification permissions
1028
1130
  notificationManager.getNotificationUtils().handlePermissionResult(requestCode, permissions, grantResults)
@@ -1133,6 +1235,45 @@ class NativeBridge(
1133
1235
  }
1134
1236
  }
1135
1237
 
1238
+ @JavascriptInterface
1239
+ fun startTransition(optionsRaw: String?) {
1240
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_TRANSITION_CANCELLED, "start transition") {
1241
+ val options = try {
1242
+ JSONObject(optionsRaw ?: "{}")
1243
+ } catch (e: Exception) {
1244
+ Log.w(TAG, "startTransition — malformed JSON, using defaults: $optionsRaw")
1245
+ JSONObject()
1246
+ }
1247
+
1248
+ val type = options.optString("type", "slide")
1249
+ val direction = options.optString("direction", "left")
1250
+ val duration = options.optInt("duration", 300).coerceIn(0, 2000)
1251
+ val timeout = options.optInt("timeout", maxOf(duration * 3, 800))
1252
+
1253
+ mainActivity.runOnUiThread {
1254
+ transitionManager.startTransition(type, direction, duration, timeout)
1255
+ }
1256
+ }
1257
+ }
1258
+
1259
+ @JavascriptInterface
1260
+ fun commitTransition(optionsRaw: String?) {
1261
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_TRANSITION_CANCELLED, "commit transition") {
1262
+ mainActivity.runOnUiThread {
1263
+ transitionManager.commitTransition()
1264
+ }
1265
+ }
1266
+ }
1267
+
1268
+ @JavascriptInterface
1269
+ fun cancelTransition(optionsRaw: String?) {
1270
+ BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_TRANSITION_CANCELLED, "cancel transition") {
1271
+ mainActivity.runOnUiThread {
1272
+ transitionManager.cancelTransition()
1273
+ }
1274
+ }
1275
+ }
1276
+
1136
1277
  @JavascriptInterface
1137
1278
  fun getSafeArea(data: String? = null) {
1138
1279
  BridgeUtils.safeExecute(webView, BridgeUtils.WebEvents.ON_SAFE_AREA_INSETS_UPDATED, "get safe area") {
@@ -1164,6 +1305,13 @@ class NativeBridge(
1164
1305
  // Cleanup NotificationManager
1165
1306
  notificationManager.cleanup()
1166
1307
 
1308
+ // Cancel any in-flight transition and release overlay
1309
+ transitionManager.cleanup()
1310
+
1311
+ // Stop video stream
1312
+ nativeCameraManager?.cleanup()
1313
+ nativeCameraManager = null
1314
+
1167
1315
  // Stop network monitoring
1168
1316
  networkMonitor?.stop()
1169
1317
  networkMonitor = null
@@ -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
+ }