catalyst-core 0.1.0-beta.2 → 0.1.0-beta.4
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.
- package/README.md +1 -1
- package/bin/catalyst.js +8 -1
- package/changelog.md +6 -0
- package/dist/native/androidProject/app/build.gradle.kts +9 -0
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +17 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +31 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +148 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +3 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
- package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
- package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
- package/dist/native/androidSetup.js +1 -1
- package/dist/native/bridge/WebBridge.js +5 -5
- package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
- package/dist/native/bridge/hooks/index.js +1 -0
- package/dist/native/bridge/hooks/useCamera.js +9 -0
- package/dist/native/bridge/hooks/useDataProtection.js +2 -0
- package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
- package/dist/native/bridge/hooks/useFilePicker.js +2 -0
- package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
- package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
- package/dist/native/bridge/hooks/useIntent.js +1 -0
- package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
- package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
- package/dist/native/bridge/hooks/useNotification.js +2 -0
- package/dist/native/bridge/hooks/useSafeArea.js +2 -0
- package/dist/native/bridge/hooks/useVideoStream.js +6 -0
- package/dist/native/bridge/hooks/utils.js +1 -0
- package/dist/native/bridge/hooks.js +1 -137
- package/dist/native/bridge/useBaseHook.js +12 -7
- package/dist/native/bridge/utils/NativeBridge.js +41 -6
- package/dist/native/buildAppAndroid.js +7 -9
- package/dist/native/buildAppIos.js +14 -22
- package/dist/native/internalPluginUtils.js +1 -0
- package/dist/native/iosnativeWebView/Package.resolved +36 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
- package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +38 -7
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +6 -2
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +15 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
- package/dist/native/plugin-bridge/PluginBridge.js +1 -0
- package/dist/native/pluginComposerAndroid.js +9 -0
- package/dist/native/pluginComposerIos.js +7 -0
- package/dist/native/renameAndroidProject.js +3 -3
- package/dist/native/setupServer.js +2 -3
- package/dist/native/utils.js +4 -1
- package/dist/otel.js +70 -2
- package/dist/scripts/devServe.js +1 -1
- package/dist/scripts/plugins.js +1 -0
- package/dist/scripts/registerAliases.js +6 -2
- package/dist/scripts/scriptUtils.js +2 -1
- package/dist/scripts/serve.js +2 -1
- package/dist/scripts/start.js +4 -1
- package/dist/server/renderer/extract.js +5 -5
- package/dist/server/renderer/handler.js +15 -14
- package/dist/server/utils/validator.js +1 -1
- package/dist/webpack/base.babel.js +2 -1
- package/dist/webpack/development.client.babel.js +6 -2
- package/dist/webpack/production.ssr.babel.js +5 -1
- package/mcp_v2/knowledge-base.json +1564 -1067
- package/mcp_v2/lib/helpers.js +19 -7
- package/mcp_v2/tools/build.js +1 -1
- package/mcp_v2/tools/config.js +16 -4
- package/mcp_v2/tools/conversion.js +11 -11
- package/mcp_v2/tools/knowledge.js +16 -3
- package/mcp_v2/tools/tasks.js +13 -4
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt
ADDED
|
@@ -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
|
+
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt
CHANGED
|
@@ -404,12 +404,12 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
404
404
|
|
|
405
405
|
private fun generateCacheKey(url: String): String {
|
|
406
406
|
try {
|
|
407
|
-
val md = MessageDigest.getInstance("
|
|
407
|
+
val md = MessageDigest.getInstance("SHA-256")
|
|
408
408
|
val digest = md.digest(url.toByteArray())
|
|
409
409
|
return digest.joinToString("") { "%02x".format(it) }
|
|
410
410
|
} catch (e: Exception) {
|
|
411
411
|
Log.e(TAG, "Error generating cache key: ${e.message}")
|
|
412
|
-
// Fallback to a simple hash code if
|
|
412
|
+
// Fallback to a simple hash code if SHA-256 fails
|
|
413
413
|
return url.hashCode().toString()
|
|
414
414
|
}
|
|
415
415
|
}
|
|
@@ -459,4 +459,4 @@ class WebCacheManager(private val context: Context, private val properties: java
|
|
|
459
459
|
)
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
|
-
}
|
|
462
|
+
}
|
|
@@ -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
|
+
}
|