catalyst-core 0.1.0-canary.8 → 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.
- package/README.md +1 -1
- package/changelog.md +5 -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/CustomWebview.kt +19 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +13 -0
- 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 +22 -15
- 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/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/androidSetup.js +1 -0
- 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/buildAndroid/assets.js +52 -0
- package/dist/native/buildAndroid/build.js +19 -0
- package/dist/native/buildAndroid/config.js +1 -0
- package/dist/native/buildAndroid/index.js +1 -0
- package/dist/native/buildAppAndroid.js +2 -150
- package/dist/native/buildAppIos.js +2 -305
- package/dist/native/buildIos/assets.js +8 -0
- package/dist/native/buildIos/build.js +40 -0
- package/dist/native/buildIos/config.js +71 -0
- package/dist/native/buildIos/index.js +6 -0
- package/dist/native/buildIos/plugins.js +45 -0
- package/dist/native/internalPluginUtils.js +7 -1
- 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/Constants/ConfigConstants.swift +2 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +214 -2
- package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +32 -7
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +6 -2
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +30 -11
- package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +1 -1
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +4 -2
- package/dist/native/pluginComposerAndroid.js +23 -4
- package/dist/native/pluginComposerIos.js +11 -2
- package/dist/native/renameAndroidProject.js +3 -3
- package/dist/native/setupEmulatorIos.js +6 -4
- package/dist/native/setupServer.js +2 -3
- package/dist/native/utils.js +4 -1
- package/dist/otel.js +70 -2
- package/dist/scripts/build.js +1 -1
- package/dist/scripts/cleanCache.js +1 -0
- package/dist/scripts/devBuild.js +1 -1
- package/dist/scripts/devServe.js +1 -1
- package/dist/scripts/loadEnvironmentVariables.js +1 -2
- 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 +7 -2
- package/dist/server/expressServer.js +3 -8
- package/dist/server/renderer/document/Body.js +4 -4
- package/dist/server/renderer/document/Head.js +2 -2
- package/dist/server/renderer/extract.js +31 -27
- package/dist/server/renderer/handler.js +47 -24
- package/dist/server/renderer/index.js +1 -1
- package/dist/server/startServer.js +57 -7
- package/dist/server/utils/userAgentUtil.js +9 -3
- package/dist/server/utils/validator.js +1 -1
- package/dist/webpack/babel-plugins/remove-client.plugin.js +2 -2
- package/dist/webpack/babel-plugins/remove-ssr.plugin.js +2 -2
- package/dist/webpack/babel.config.client.js +1 -1
- package/dist/webpack/babel.config.ssr.js +1 -1
- package/dist/webpack/base.babel.js +7 -8
- package/dist/webpack/development.client.babel.js +7 -16
- package/dist/webpack/production.client.babel.js +1 -1
- package/dist/webpack/production.ssr.babel.js +5 -1
- package/mcp_v2/knowledge-base.json +193 -100
- package/mcp_v2/lib/helpers.js +52 -12
- package/mcp_v2/lib/seed.js +88 -0
- package/mcp_v2/mcp.js +154 -7
- package/mcp_v2/setup.js +34 -195
- 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/github.js +1117 -0
- package/mcp_v2/tools/knowledge.js +16 -3
- package/mcp_v2/tools/sync.js +179 -10
- package/mcp_v2/tools/tasks.js +13 -4
- package/package.json +25 -21
- package/.release-meta.json +0 -4
- package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +0 -43
- package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +0 -28
- package/dist/native/internal-plugins/device-info-plugin/manifest.json +0 -19
- package/license +0 -10
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
|
@@ -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("
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
package io.yourname.androidproject.camera
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import android.util.Size
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.hardware.camera2.CaptureRequest
|
|
9
|
+
import android.util.Range
|
|
10
|
+
import androidx.camera.camera2.interop.Camera2Interop
|
|
11
|
+
import androidx.camera.core.Camera
|
|
12
|
+
import androidx.camera.core.CameraSelector
|
|
13
|
+
import androidx.camera.core.ImageAnalysis
|
|
14
|
+
import androidx.camera.core.Preview
|
|
15
|
+
import androidx.camera.core.resolutionselector.ResolutionFilter
|
|
16
|
+
import androidx.camera.core.resolutionselector.ResolutionSelector
|
|
17
|
+
import androidx.camera.core.resolutionselector.ResolutionStrategy
|
|
18
|
+
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
19
|
+
import androidx.camera.view.PreviewView
|
|
20
|
+
import androidx.core.content.ContextCompat
|
|
21
|
+
import androidx.lifecycle.LifecycleOwner
|
|
22
|
+
import io.yourname.androidproject.MainActivity
|
|
23
|
+
import io.yourname.androidproject.utils.BridgeUtils
|
|
24
|
+
import java.util.concurrent.ExecutorService
|
|
25
|
+
import java.util.concurrent.Executors
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Owns the CameraX session lifecycle: bind, stop, flip, setFps.
|
|
29
|
+
*/
|
|
30
|
+
class CameraSessionManager(
|
|
31
|
+
private val activity: MainActivity,
|
|
32
|
+
private val previewView: PreviewView,
|
|
33
|
+
private val stateMachine: VideoStreamStateMachine,
|
|
34
|
+
private val zoomController: ZoomController,
|
|
35
|
+
private val torchController: TorchController,
|
|
36
|
+
private val barcodeDetector: BarcodeDetector,
|
|
37
|
+
private val onReady: () -> Unit,
|
|
38
|
+
private val onStopped: () -> Unit,
|
|
39
|
+
private val onError: (message: String) -> Unit
|
|
40
|
+
) {
|
|
41
|
+
|
|
42
|
+
companion object {
|
|
43
|
+
private const val TAG = "CameraSessionManager"
|
|
44
|
+
const val PERMISSION_REQUEST_CODE = 1001
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private var cameraProvider: ProcessCameraProvider? = null
|
|
48
|
+
private var camera: Camera? = null
|
|
49
|
+
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
50
|
+
|
|
51
|
+
private var previewUseCase: Preview? = null
|
|
52
|
+
private var imageAnalysisUseCase: ImageAnalysis? = null
|
|
53
|
+
|
|
54
|
+
// Session params — stored so flip/setFps can rebind with same settings
|
|
55
|
+
private var currentFacing: String = "back"
|
|
56
|
+
private var currentFpsMin: Int? = null
|
|
57
|
+
private var currentFpsMax: Int? = null
|
|
58
|
+
private var currentScanFormat: String = "all"
|
|
59
|
+
private var currentAutoZoom: Boolean = false
|
|
60
|
+
private var currentInitialZoom: Float = 1.0f
|
|
61
|
+
|
|
62
|
+
// ---- Public API ----
|
|
63
|
+
|
|
64
|
+
fun start(
|
|
65
|
+
facing: String,
|
|
66
|
+
autoZoom: Boolean,
|
|
67
|
+
initialZoomPct: Float,
|
|
68
|
+
scanFormat: String,
|
|
69
|
+
fpsMin: Int?,
|
|
70
|
+
fpsMax: Int?
|
|
71
|
+
) {
|
|
72
|
+
if (cameraProvider != null) {
|
|
73
|
+
Log.w(TAG, "start() called while already streaming — ignoring")
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
currentFacing = facing
|
|
77
|
+
currentAutoZoom = autoZoom
|
|
78
|
+
currentInitialZoom = initialZoomPct
|
|
79
|
+
currentScanFormat = scanFormat
|
|
80
|
+
currentFpsMin = fpsMin
|
|
81
|
+
currentFpsMax = fpsMax
|
|
82
|
+
|
|
83
|
+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
|
|
84
|
+
== PackageManager.PERMISSION_GRANTED
|
|
85
|
+
) {
|
|
86
|
+
bindCamera()
|
|
87
|
+
} else {
|
|
88
|
+
activity.requestPermissions(arrayOf(Manifest.permission.CAMERA), PERMISSION_REQUEST_CODE)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fun stop() {
|
|
93
|
+
cameraProvider?.unbindAll()
|
|
94
|
+
cameraProvider = null
|
|
95
|
+
camera = null
|
|
96
|
+
previewUseCase = null
|
|
97
|
+
imageAnalysisUseCase = null
|
|
98
|
+
barcodeDetector.close()
|
|
99
|
+
zoomController.detachCamera()
|
|
100
|
+
torchController.detachCamera()
|
|
101
|
+
activity.runOnUiThread {
|
|
102
|
+
previewView.visibility = View.INVISIBLE
|
|
103
|
+
stateMachine.transition(VideoStreamState.IDLE)
|
|
104
|
+
onStopped()
|
|
105
|
+
}
|
|
106
|
+
Log.d(TAG, "Camera stopped")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fun flip() {
|
|
110
|
+
val newFacing = if (currentFacing == "back") "front" else "back"
|
|
111
|
+
Log.d(TAG, "flip() — $currentFacing → $newFacing")
|
|
112
|
+
currentFacing = newFacing
|
|
113
|
+
cleanupForRebind()
|
|
114
|
+
bindCamera()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fun setFps(min: Int?, max: Int?) {
|
|
118
|
+
if (min != null && max != null && min > max) {
|
|
119
|
+
Log.w(TAG, "setFps() — invalid range min=$min > max=$max, ignoring")
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
Log.d(TAG, "setFps($min, $max) — restarting session")
|
|
123
|
+
currentFpsMin = min
|
|
124
|
+
currentFpsMax = max
|
|
125
|
+
cleanupForRebind()
|
|
126
|
+
bindCamera()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private fun cleanupForRebind() {
|
|
130
|
+
barcodeDetector.close()
|
|
131
|
+
cameraProvider?.unbindAll()
|
|
132
|
+
camera = null
|
|
133
|
+
previewUseCase = null
|
|
134
|
+
imageAnalysisUseCase = null
|
|
135
|
+
zoomController.detachCamera()
|
|
136
|
+
torchController.detachCamera()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fun onPermissionResult(granted: Boolean) {
|
|
140
|
+
if (granted) {
|
|
141
|
+
bindCamera()
|
|
142
|
+
} else {
|
|
143
|
+
stateMachine.transition(VideoStreamState.IDLE)
|
|
144
|
+
onError("Camera permission denied")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun cleanup() {
|
|
149
|
+
stop()
|
|
150
|
+
cameraExecutor.shutdown()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- Internal ----
|
|
154
|
+
|
|
155
|
+
private fun bindCamera() {
|
|
156
|
+
val future = ProcessCameraProvider.getInstance(activity)
|
|
157
|
+
future.addListener({
|
|
158
|
+
try {
|
|
159
|
+
cameraProvider = future.get()
|
|
160
|
+
|
|
161
|
+
val previewBuilder = Preview.Builder()
|
|
162
|
+
.setResolutionSelector(
|
|
163
|
+
ResolutionSelector.Builder()
|
|
164
|
+
.setResolutionFilter { supportedSizes, _ ->
|
|
165
|
+
supportedSizes.sortedByDescending { it.width * it.height }
|
|
166
|
+
}
|
|
167
|
+
.build()
|
|
168
|
+
)
|
|
169
|
+
val fpsMin = currentFpsMin
|
|
170
|
+
val fpsMax = currentFpsMax
|
|
171
|
+
if (fpsMin != null && fpsMax != null) {
|
|
172
|
+
if (fpsMin <= fpsMax) {
|
|
173
|
+
Camera2Interop.Extender(previewBuilder)
|
|
174
|
+
.setCaptureRequestOption(
|
|
175
|
+
CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
|
|
176
|
+
Range(fpsMin, fpsMax)
|
|
177
|
+
)
|
|
178
|
+
} else {
|
|
179
|
+
Log.w(TAG, "bindCamera — invalid FPS range min=$fpsMin > max=$fpsMax, skipping FPS override")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
previewUseCase = previewBuilder.build().also {
|
|
183
|
+
it.setSurfaceProvider { request ->
|
|
184
|
+
Log.d(TAG, "Preview resolution negotiated: ${request.resolution.width}x${request.resolution.height}")
|
|
185
|
+
previewView.surfaceProvider.onSurfaceRequested(request)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
val resolutionSelector = ResolutionSelector.Builder()
|
|
190
|
+
.setResolutionStrategy(
|
|
191
|
+
ResolutionStrategy(
|
|
192
|
+
Size(1920, 1080),
|
|
193
|
+
ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
.build()
|
|
197
|
+
|
|
198
|
+
imageAnalysisUseCase = barcodeDetector.buildImageAnalysis(
|
|
199
|
+
executor = cameraExecutor,
|
|
200
|
+
resolutionSelector = resolutionSelector,
|
|
201
|
+
autoZoomEnabled = currentAutoZoom,
|
|
202
|
+
scanFormat = currentScanFormat
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
cameraProvider?.unbindAll()
|
|
206
|
+
val boundCamera = cameraProvider?.bindToLifecycle(
|
|
207
|
+
activity as LifecycleOwner,
|
|
208
|
+
cameraSelector(currentFacing),
|
|
209
|
+
previewUseCase!!,
|
|
210
|
+
imageAnalysisUseCase!!
|
|
211
|
+
)
|
|
212
|
+
if (boundCamera == null) {
|
|
213
|
+
Log.e(TAG, "bindToLifecycle returned null — aborting camera start")
|
|
214
|
+
stateMachine.transition(VideoStreamState.IDLE)
|
|
215
|
+
onError("Failed to bind camera to lifecycle")
|
|
216
|
+
return@addListener
|
|
217
|
+
}
|
|
218
|
+
camera = boundCamera
|
|
219
|
+
zoomController.attachCamera(boundCamera)
|
|
220
|
+
torchController.attachCamera(boundCamera, currentFacing)
|
|
221
|
+
|
|
222
|
+
zoomController.applyZoomMultiplier(currentInitialZoom)
|
|
223
|
+
torchController.notifyReset()
|
|
224
|
+
|
|
225
|
+
stateMachine.transition(VideoStreamState.STREAMING)
|
|
226
|
+
|
|
227
|
+
activity.runOnUiThread {
|
|
228
|
+
previewView.visibility = View.VISIBLE
|
|
229
|
+
onReady()
|
|
230
|
+
}
|
|
231
|
+
Log.d(TAG, "Camera bound — facing=$currentFacing autoZoom=$currentAutoZoom")
|
|
232
|
+
|
|
233
|
+
} catch (e: Exception) {
|
|
234
|
+
Log.e(TAG, "Failed to bind camera", e)
|
|
235
|
+
stateMachine.transition(VideoStreamState.IDLE)
|
|
236
|
+
onError("Failed to start camera: ${e.message}")
|
|
237
|
+
}
|
|
238
|
+
}, ContextCompat.getMainExecutor(activity))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private fun cameraSelector(facing: String) =
|
|
242
|
+
if (facing == "front") CameraSelector.DEFAULT_FRONT_CAMERA
|
|
243
|
+
else CameraSelector.DEFAULT_BACK_CAMERA
|
|
244
|
+
}
|