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