react-native-frame-capture 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/FrameCapture.podspec +21 -0
  2. package/LICENSE +20 -0
  3. package/README.md +158 -0
  4. package/android/build.gradle +77 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +20 -0
  7. package/android/src/main/java/com/framecapture/CaptureManager.kt +831 -0
  8. package/android/src/main/java/com/framecapture/Constants.kt +196 -0
  9. package/android/src/main/java/com/framecapture/ErrorHandler.kt +165 -0
  10. package/android/src/main/java/com/framecapture/FrameCaptureModule.kt +653 -0
  11. package/android/src/main/java/com/framecapture/FrameCapturePackage.kt +32 -0
  12. package/android/src/main/java/com/framecapture/OverlayRenderer.kt +423 -0
  13. package/android/src/main/java/com/framecapture/PermissionHandler.kt +150 -0
  14. package/android/src/main/java/com/framecapture/ScreenCaptureService.kt +366 -0
  15. package/android/src/main/java/com/framecapture/StorageManager.kt +221 -0
  16. package/android/src/main/java/com/framecapture/capture/BitmapProcessor.kt +157 -0
  17. package/android/src/main/java/com/framecapture/capture/CaptureEventEmitter.kt +120 -0
  18. package/android/src/main/java/com/framecapture/models/CaptureModels.kt +302 -0
  19. package/android/src/main/java/com/framecapture/models/EnumsAndExtensions.kt +60 -0
  20. package/android/src/main/java/com/framecapture/models/OverlayModels.kt +154 -0
  21. package/android/src/main/java/com/framecapture/service/CaptureNotificationManager.kt +286 -0
  22. package/android/src/main/java/com/framecapture/storage/StorageStrategies.kt +317 -0
  23. package/android/src/main/java/com/framecapture/utils/ValidationUtils.kt +379 -0
  24. package/app.plugin.js +1 -0
  25. package/ios/FrameCapture.h +5 -0
  26. package/ios/FrameCapture.mm +21 -0
  27. package/lib/module/NativeFrameCapture.js +24 -0
  28. package/lib/module/NativeFrameCapture.js.map +1 -0
  29. package/lib/module/api.js +146 -0
  30. package/lib/module/api.js.map +1 -0
  31. package/lib/module/constants.js +67 -0
  32. package/lib/module/constants.js.map +1 -0
  33. package/lib/module/errors.js +19 -0
  34. package/lib/module/errors.js.map +1 -0
  35. package/lib/module/events.js +58 -0
  36. package/lib/module/events.js.map +1 -0
  37. package/lib/module/index.js +24 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/normalize.js +51 -0
  40. package/lib/module/normalize.js.map +1 -0
  41. package/lib/module/package.json +1 -0
  42. package/lib/module/types.js +165 -0
  43. package/lib/module/types.js.map +1 -0
  44. package/lib/module/validation.js +190 -0
  45. package/lib/module/validation.js.map +1 -0
  46. package/lib/typescript/package.json +1 -0
  47. package/lib/typescript/plugin/src/index.d.ts +4 -0
  48. package/lib/typescript/plugin/src/index.d.ts.map +1 -0
  49. package/lib/typescript/src/NativeFrameCapture.d.ts +75 -0
  50. package/lib/typescript/src/NativeFrameCapture.d.ts.map +1 -0
  51. package/lib/typescript/src/api.d.ts +66 -0
  52. package/lib/typescript/src/api.d.ts.map +1 -0
  53. package/lib/typescript/src/constants.d.ts +41 -0
  54. package/lib/typescript/src/constants.d.ts.map +1 -0
  55. package/lib/typescript/src/errors.d.ts +14 -0
  56. package/lib/typescript/src/errors.d.ts.map +1 -0
  57. package/lib/typescript/src/events.d.ts +30 -0
  58. package/lib/typescript/src/events.d.ts.map +1 -0
  59. package/lib/typescript/src/index.d.ts +12 -0
  60. package/lib/typescript/src/index.d.ts.map +1 -0
  61. package/lib/typescript/src/normalize.d.ts +43 -0
  62. package/lib/typescript/src/normalize.d.ts.map +1 -0
  63. package/lib/typescript/src/types.d.ts +247 -0
  64. package/lib/typescript/src/types.d.ts.map +1 -0
  65. package/lib/typescript/src/validation.d.ts +15 -0
  66. package/lib/typescript/src/validation.d.ts.map +1 -0
  67. package/package.json +196 -0
  68. package/plugin/build/index.js +48 -0
  69. package/src/NativeFrameCapture.ts +86 -0
  70. package/src/api.ts +189 -0
  71. package/src/constants.ts +69 -0
  72. package/src/errors.ts +21 -0
  73. package/src/events.ts +61 -0
  74. package/src/index.tsx +31 -0
  75. package/src/normalize.ts +81 -0
  76. package/src/types.ts +327 -0
  77. package/src/validation.ts +321 -0
@@ -0,0 +1,366 @@
1
+ package com.framecapture
2
+
3
+ import android.app.Service
4
+ import android.content.Intent
5
+ import android.os.IBinder
6
+ import android.util.Log
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.WritableMap
9
+ import com.framecapture.models.CaptureOptions
10
+ import com.framecapture.models.NotificationOptions
11
+ import com.framecapture.service.CaptureNotificationManager
12
+
13
+ /**
14
+ * Foreground service for reliable background screen capture
15
+ *
16
+ * This service ensures screen capture continues even when the app is minimized,
17
+ * backgrounded, or the screen is off. It displays a persistent notification
18
+ * (required for foreground services) and manages the CaptureManager lifecycle.
19
+ *
20
+ * Key responsibilities:
21
+ * - Manages CaptureManager lifecycle (start/stop/pause/resume)
22
+ * - Displays and updates foreground notification
23
+ * - Bridges events between CaptureManager and FrameCaptureModule
24
+ * - Handles notification button actions (pause/resume/stop)
25
+ */
26
+ class ScreenCaptureService : Service() {
27
+
28
+ companion object {
29
+ private const val TAG = "ScreenCaptureService"
30
+
31
+ // Notification constants
32
+ const val CHANNEL_ID = Constants.CHANNEL_ID
33
+ const val NOTIFICATION_ID = Constants.NOTIFICATION_ID
34
+
35
+ // Service actions
36
+ const val ACTION_START = Constants.ACTION_START_CAPTURE
37
+ const val ACTION_STOP = Constants.ACTION_STOP_CAPTURE
38
+ const val ACTION_PAUSE = Constants.ACTION_PAUSE_CAPTURE
39
+ const val ACTION_RESUME = Constants.ACTION_RESUME_CAPTURE
40
+
41
+ // Intent extras
42
+ const val EXTRA_OPTIONS = Constants.EXTRA_CAPTURE_OPTIONS
43
+ const val EXTRA_PROJECTION_DATA = Constants.EXTRA_PROJECTION_DATA
44
+
45
+ // Static reference to FrameCaptureModule for event emission
46
+ @Volatile
47
+ private var FrameCaptureModule: FrameCaptureModule? = null
48
+
49
+ // Static frame count shared between service and module
50
+ @Volatile
51
+ var currentFrameCount: Int = 0
52
+ private set
53
+
54
+ /**
55
+ * Sets the FrameCaptureModule reference for event emission
56
+ *
57
+ * Must be called from FrameCaptureModule before starting the service.
58
+ * This allows the service to emit events to JavaScript.
59
+ */
60
+ fun setFrameCaptureModule(module: FrameCaptureModule?) {
61
+ FrameCaptureModule = module
62
+ }
63
+
64
+ /**
65
+ * Updates the frame count
66
+ * Called from the service to keep the count synchronized with the module
67
+ */
68
+ fun updateFrameCount(count: Int) {
69
+ currentFrameCount = count
70
+ }
71
+
72
+ /**
73
+ * Resets the frame count to zero
74
+ * Called when a capture session starts or stops
75
+ */
76
+ fun resetFrameCount() {
77
+ currentFrameCount = 0
78
+ }
79
+ }
80
+
81
+ // CaptureManager instance for handling screen capture
82
+ private var captureManager: CaptureManager? = null
83
+
84
+ // StorageManager for saving frames
85
+ private var storageManager: StorageManager? = null
86
+
87
+ // Notification manager for handling notifications
88
+ private var notificationManager: CaptureNotificationManager? = null
89
+
90
+ // Frame counter for notification updates
91
+ private var frameCounter: Int = 0
92
+
93
+ // Paused state for notification updates
94
+ private var isPaused: Boolean = false
95
+
96
+ override fun onCreate() {
97
+ super.onCreate()
98
+
99
+ // Notification channel is created in onStartCommand after parsing options
100
+ }
101
+
102
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
103
+ when (intent?.action) {
104
+ ACTION_START -> {
105
+ // Extract options and projection data from intent
106
+ val options = intent.getParcelableExtra<android.os.Bundle>(EXTRA_OPTIONS)
107
+ val projectionData = intent.getParcelableExtra<Intent>(EXTRA_PROJECTION_DATA)
108
+
109
+ if (options != null && projectionData != null) {
110
+ try {
111
+ // Parse notification options before creating notifications
112
+ val optionsMap = Arguments.fromBundle(options)
113
+ val captureOptions = CaptureOptions.fromReadableMap(optionsMap)
114
+
115
+ val notificationOptions = captureOptions.notification ?: NotificationOptions()
116
+
117
+ // Create notification manager with custom options
118
+ notificationManager = CaptureNotificationManager(this, notificationOptions)
119
+
120
+ // Create notification channel (Android 8.0+)
121
+ notificationManager?.createNotificationChannel()
122
+
123
+ // Start as foreground service (required for background capture)
124
+ startForeground(NOTIFICATION_ID, notificationManager?.createNotification())
125
+
126
+ // Initialize and start capture
127
+ startCapture(options, projectionData, notificationOptions)
128
+ } catch (e: Exception) {
129
+ Log.e(TAG, "Failed to start capture service", e)
130
+ stopSelf()
131
+ }
132
+ } else {
133
+ stopSelf()
134
+ }
135
+ }
136
+
137
+ ACTION_STOP -> {
138
+ stopCapture()
139
+ stopSelf()
140
+ }
141
+
142
+ ACTION_PAUSE -> {
143
+ pauseCapture()
144
+ }
145
+
146
+ ACTION_RESUME -> {
147
+ resumeCapture()
148
+ }
149
+
150
+ else -> {
151
+ stopSelf()
152
+ }
153
+ }
154
+
155
+ // START_STICKY: System will restart service if killed
156
+ return START_STICKY
157
+ }
158
+
159
+ override fun onBind(intent: Intent?): IBinder? {
160
+ // This is not a bound service
161
+ return null
162
+ }
163
+
164
+ override fun onDestroy() {
165
+ // Ensure capture is stopped and resources are released
166
+ try {
167
+ stopCapture()
168
+ } catch (e: Exception) {
169
+ Log.e(TAG, "Error stopping capture in onDestroy", e)
170
+ }
171
+
172
+ super.onDestroy()
173
+ }
174
+
175
+ // ========== Capture Management ==========
176
+
177
+ /**
178
+ * Starts screen capture with the specified options
179
+ *
180
+ * Creates and initializes CaptureManager and StorageManager, sets up event
181
+ * handlers for frame capture and notification updates, then starts capture.
182
+ *
183
+ * @param optionsBundle Capture options as Android Bundle
184
+ * @param projectionData MediaProjection permission data
185
+ * @param notificationOptions Notification customization options
186
+ */
187
+ private fun startCapture(optionsBundle: android.os.Bundle, projectionData: Intent, notificationOptions: NotificationOptions) {
188
+ try {
189
+ // Convert Bundle to CaptureOptions
190
+ val optionsMap = Arguments.fromBundle(optionsBundle)
191
+ val captureOptions = CaptureOptions.fromReadableMap(optionsMap)
192
+
193
+ // Create StorageManager if needed
194
+ if (storageManager == null) {
195
+ // Event emitter for StorageManager (handles storage warnings)
196
+ val eventEmitter: (String, WritableMap?) -> Unit = { eventName, params ->
197
+ // Handle frame captured events for notification updates
198
+ if (eventName == Constants.EVENT_FRAME_CAPTURED) {
199
+ params?.let {
200
+ if (it.hasKey("frameNumber")) {
201
+ frameCounter = it.getInt("frameNumber") + 1
202
+
203
+ // Update notification based on configured interval
204
+ if (notificationOptions.showFrameCount &&
205
+ notificationOptions.updateInterval > 0 &&
206
+ frameCounter % notificationOptions.updateInterval == 0) {
207
+ notificationManager?.updateNotification(frameCounter)
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ // Forward storage warnings to JavaScript
214
+ if (eventName == Constants.EVENT_STORAGE_WARNING) {
215
+ try {
216
+ val module = FrameCaptureModule
217
+ if (module != null && params != null) {
218
+ module.emitOnStorageWarning(params)
219
+ }
220
+ } catch (e: Exception) {
221
+ Log.e(TAG, "Failed to emit storage warning: ${e.message}", e)
222
+ }
223
+ }
224
+ }
225
+
226
+ // Initialize StorageManager for frame persistence
227
+ storageManager = StorageManager(
228
+ applicationContext,
229
+ eventEmitter
230
+ )
231
+ }
232
+
233
+ // Initialize CaptureManager with event handlers
234
+ captureManager = CaptureManager(
235
+ applicationContext,
236
+ storageManager!!,
237
+ { eventName, params ->
238
+ // Handle frame captured events
239
+ if (eventName == Constants.EVENT_FRAME_CAPTURED) {
240
+ frameCounter++
241
+ // Sync frame count with module
242
+ updateFrameCount(frameCounter)
243
+
244
+ // Update notification based on configuration
245
+ if (!isPaused &&
246
+ notificationOptions.showFrameCount &&
247
+ notificationOptions.updateInterval > 0 &&
248
+ frameCounter % notificationOptions.updateInterval == 0) {
249
+ notificationManager?.updateNotification(frameCounter)
250
+ }
251
+ }
252
+
253
+ // Forward events to JavaScript via FrameCaptureModule
254
+ try {
255
+ val module = FrameCaptureModule
256
+ if (module != null && params != null) {
257
+ when (eventName) {
258
+ Constants.EVENT_FRAME_CAPTURED -> module.emitOnFrameCaptured(params)
259
+ Constants.EVENT_CAPTURE_ERROR -> module.emitOnCaptureError(params)
260
+ Constants.EVENT_CAPTURE_STOP -> module.emitOnCaptureStop(params)
261
+ Constants.EVENT_CAPTURE_START -> module.emitOnCaptureStart(params)
262
+ Constants.EVENT_STORAGE_WARNING -> module.emitOnStorageWarning(params)
263
+ Constants.EVENT_OVERLAY_ERROR -> module.emitOnOverlayError(params)
264
+ }
265
+ }
266
+ } catch (e: Exception) {
267
+ Log.e(TAG, "Failed to send event: $eventName", e)
268
+ }
269
+ }
270
+ )
271
+
272
+ // Initialize with MediaProjection permission and options
273
+ captureManager?.initialize(projectionData, captureOptions)
274
+
275
+ // Start capture
276
+ captureManager?.start()
277
+
278
+ // Reset counters and state for new session
279
+ frameCounter = 0
280
+ resetFrameCount()
281
+ isPaused = false
282
+
283
+ } catch (e: Exception) {
284
+ Log.e(TAG, "Failed to start capture in service", e)
285
+ throw e
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Stops screen capture and cleans up all resources
291
+ *
292
+ * Notifies the module, stops capture, releases CaptureManager,
293
+ * and resets all state variables.
294
+ */
295
+ private fun stopCapture() {
296
+ try {
297
+ // Notify module first to update state
298
+ FrameCaptureModule?.updateStateFromService(isStopped = true)
299
+
300
+ // Stop capture (emits onCaptureStop event)
301
+ captureManager?.stop()
302
+
303
+ // Cleanup resources
304
+ captureManager?.cleanup()
305
+ captureManager = null
306
+
307
+ // Reset counters and state
308
+ frameCounter = 0
309
+ resetFrameCount()
310
+ isPaused = false
311
+
312
+ } catch (e: Exception) {
313
+ Log.e(TAG, "Error stopping capture in service", e)
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Pauses screen capture
319
+ *
320
+ * Stops capturing frames but keeps the service and notification active.
321
+ * Updates notification to show paused state with resume button.
322
+ */
323
+ private fun pauseCapture() {
324
+ try {
325
+ // Pause capture
326
+ captureManager?.pause()
327
+
328
+ // Update paused state
329
+ isPaused = true
330
+
331
+ // Update notification to show paused state
332
+ notificationManager?.updateNotificationPaused(frameCounter)
333
+
334
+ // Notify module about pause
335
+ FrameCaptureModule?.updateStateFromService(isPaused = true)
336
+
337
+ } catch (e: Exception) {
338
+ Log.e(TAG, "Error pausing capture in service", e)
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Resumes screen capture from paused state
344
+ *
345
+ * Restarts frame capture and updates notification to show active state
346
+ * with pause button.
347
+ */
348
+ private fun resumeCapture() {
349
+ try {
350
+ // Resume capture
351
+ captureManager?.resume()
352
+
353
+ // Update paused state
354
+ isPaused = false
355
+
356
+ // Update notification to show active state
357
+ notificationManager?.updateNotification(frameCounter)
358
+
359
+ // Notify module about resume
360
+ FrameCaptureModule?.updateStateFromService(isPaused = false)
361
+
362
+ } catch (e: Exception) {
363
+ Log.e(TAG, "Error resuming capture in service", e)
364
+ }
365
+ }
366
+ }
@@ -0,0 +1,221 @@
1
+ package com.framecapture
2
+
3
+ import android.graphics.Bitmap
4
+ import android.os.Build
5
+ import android.os.StatFs
6
+ import android.util.Log
7
+ import com.framecapture.models.CaptureOptions
8
+ import com.framecapture.models.FrameInfo
9
+ import com.framecapture.storage.StorageStrategies
10
+ import java.io.File
11
+ import java.text.SimpleDateFormat
12
+ import java.util.*
13
+
14
+ /**
15
+ * Manages storage operations for captured frames
16
+ *
17
+ * Coordinates frame storage across different Android versions and storage locations.
18
+ * Delegates actual storage operations to StorageStrategies for different scenarios:
19
+ * - Temporary storage (when saveFrames is false)
20
+ * - App-specific storage (default, no permissions needed)
21
+ * - Public storage via MediaStore (Android 10+)
22
+ * - Custom directory storage
23
+ *
24
+ * Also handles storage space monitoring and warning events.
25
+ */
26
+ class StorageManager(
27
+ private val context: android.content.Context,
28
+ private val eventEmitter: ((String, com.facebook.react.bridge.WritableMap?) -> Unit)? = null
29
+ ) {
30
+
31
+ // Storage strategies handler
32
+ private val storageStrategies = StorageStrategies(context)
33
+
34
+ init {
35
+ // Clean up temp files from previous sessions on startup
36
+ cleanupAllTempFiles()
37
+ }
38
+
39
+ /**
40
+ * Returns the app-specific pictures directory
41
+ *
42
+ * This directory doesn't require storage permissions and is automatically
43
+ * cleaned when the app is uninstalled.
44
+ *
45
+ * @return File pointing to app-specific pictures directory
46
+ */
47
+ fun getAppSpecificDirectory(): File {
48
+ return storageStrategies.getAppSpecificDirectory()
49
+ }
50
+
51
+ /**
52
+ * Cleans up all temporary frame files
53
+ *
54
+ * Automatically called on app startup to remove temp files from previous sessions.
55
+ * Can also be called manually via cleanupTempFrames() API.
56
+ */
57
+ fun cleanupAllTempFiles() {
58
+ storageStrategies.cleanupAllTempFiles()
59
+ }
60
+
61
+ /**
62
+ * Generates a unique filename for a captured frame
63
+ *
64
+ * Format: <prefix><sessionId>_<frameNumber>_<timestamp>.<format>
65
+ * Example: capture_abc123_00042_20231215_143022_456.jpg
66
+ *
67
+ * @param sessionId Unique session identifier
68
+ * @param frameNumber Frame number (zero-padded based on config)
69
+ * @param format Image format ("png" or "jpg")
70
+ * @param fileNamingConfig File naming configuration
71
+ * @return Generated filename
72
+ */
73
+ fun generateFilename(
74
+ sessionId: String,
75
+ frameNumber: Int,
76
+ format: String,
77
+ fileNamingConfig: com.framecapture.models.FileNamingConfig = com.framecapture.models.FileNamingConfig()
78
+ ): String {
79
+ val timestamp = SimpleDateFormat(fileNamingConfig.dateFormat, Locale.US).format(Date())
80
+ val extension = when (format.lowercase()) {
81
+ Constants.FORMAT_EXTENSION_PNG -> Constants.FORMAT_EXTENSION_PNG
82
+ else -> Constants.FORMAT_EXTENSION_JPEG
83
+ }
84
+ val paddingFormat = "%0${fileNamingConfig.framePadding}d"
85
+ return "${fileNamingConfig.prefix}${sessionId}_${String.format(paddingFormat, frameNumber)}_${timestamp}.${extension}"
86
+ }
87
+
88
+
89
+
90
+ /**
91
+ * Saves a captured frame to storage
92
+ *
93
+ * Storage location is determined by options in this priority order:
94
+ * 1. Temporary storage (if saveFrames is false)
95
+ * 2. Custom directory (if outputDirectory is specified)
96
+ * 3. Public storage via MediaStore (if storageLocation is "public" and Android 10+)
97
+ * 4. App-specific directory (default)
98
+ *
99
+ * @param bitmap The captured frame as a Bitmap
100
+ * @param sessionId Unique session identifier
101
+ * @param frameNumber Frame number in the session
102
+ * @param options Capture options (format, quality, storage location)
103
+ * @return FrameInfo containing file path, size, and timestamp
104
+ * @throws IOException if the save operation fails
105
+ */
106
+ fun saveFrame(
107
+ bitmap: Bitmap,
108
+ sessionId: String,
109
+ frameNumber: Int,
110
+ options: CaptureOptions
111
+ ): FrameInfo {
112
+ val filename = generateFilename(sessionId, frameNumber, options.format, options.advanced.fileNaming)
113
+ val format = storageStrategies.getCompressFormat(options.format)
114
+ val quality = options.quality
115
+
116
+ // If saveFrames is false, save to temp directory
117
+ if (!options.saveFrames) {
118
+ return storageStrategies.saveToTempDirectory(bitmap, sessionId, filename, format, quality)
119
+ }
120
+
121
+ return when {
122
+ // 1. Custom directory takes precedence
123
+ options.outputDirectory != null -> {
124
+ storageStrategies.saveToCustomDirectory(bitmap, options.outputDirectory, filename, format, quality)
125
+ }
126
+
127
+ // 2. Public storage request
128
+ options.storageLocation == Constants.STORAGE_LOCATION_PUBLIC -> {
129
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
130
+ // Android 10+: Use MediaStore for public Pictures folder
131
+ storageStrategies.saveToMediaStore(bitmap, sessionId, filename, options.format, quality)
132
+ } else {
133
+ // Android 9 and below: Fall back to app-specific storage
134
+ storageStrategies.saveToAppSpecificDirectory(bitmap, sessionId, filename, format, quality)
135
+ }
136
+ }
137
+
138
+ // 3. Default: App-specific directory (no permissions needed)
139
+ else -> {
140
+ storageStrategies.saveToAppSpecificDirectory(bitmap, sessionId, filename, format, quality)
141
+ }
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Checks available storage space in bytes
147
+ *
148
+ * Uses the app-specific directory to determine available space.
149
+ * Handles API level differences for StatFs methods.
150
+ *
151
+ * @return Available storage space in bytes, or 0 if check fails
152
+ */
153
+ fun checkStorageSpace(): Long {
154
+ return try {
155
+ val directory = getAppSpecificDirectory()
156
+ val stat = StatFs(directory.path)
157
+
158
+ // Use API level-appropriate method
159
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
160
+ stat.availableBlocksLong * stat.blockSizeLong
161
+ } else {
162
+ @Suppress("DEPRECATION")
163
+ stat.availableBlocks.toLong() * stat.blockSize.toLong()
164
+ }
165
+ } catch (e: Exception) {
166
+ Log.e(Constants.MODULE_NAME, "Failed to check storage space: ${e.message}", e)
167
+ 0L
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Checks if sufficient storage is available
173
+ *
174
+ * Compares available space against the configured warning threshold.
175
+ * Emits a warning event to JavaScript if below threshold.
176
+ *
177
+ * @param threshold Storage warning threshold in bytes (0 to disable warnings)
178
+ * @return true if storage is sufficient, false if below threshold
179
+ */
180
+ fun isStorageAvailable(threshold: Long = Constants.DEFAULT_STORAGE_WARNING_THRESHOLD): Boolean {
181
+ // If threshold is 0, warnings are disabled
182
+ if (threshold == 0L) {
183
+ return true
184
+ }
185
+
186
+ val availableSpace = checkStorageSpace()
187
+ val isAvailable = availableSpace >= threshold
188
+
189
+ // Debug logging
190
+
191
+ // Emit warning if below threshold
192
+ if (!isAvailable && eventEmitter != null) {
193
+ emitStorageWarning(availableSpace, threshold)
194
+ } else if (!isAvailable) {
195
+ Log.w(Constants.MODULE_NAME, "Storage below threshold but eventEmitter is null!")
196
+ }
197
+
198
+ return isAvailable
199
+ }
200
+
201
+ /**
202
+ * Emits a storage warning event to JavaScript
203
+ *
204
+ * Sends available space and threshold values to allow the app to
205
+ * handle low storage situations (e.g., stop capture, notify user).
206
+ *
207
+ * @param availableSpace Current available storage in bytes
208
+ * @param threshold The configured warning threshold
209
+ */
210
+ private fun emitStorageWarning(availableSpace: Long, threshold: Long) {
211
+ try {
212
+ val params = com.facebook.react.bridge.Arguments.createMap().apply {
213
+ putDouble(Constants.EVENT_KEY_AVAILABLE_SPACE, availableSpace.toDouble())
214
+ putDouble(Constants.EVENT_KEY_THRESHOLD, threshold.toDouble())
215
+ }
216
+ eventEmitter?.invoke(Constants.EVENT_STORAGE_WARNING, params)
217
+ } catch (e: Exception) {
218
+ // Silently fail - event emission is not critical
219
+ }
220
+ }
221
+ }