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.
- package/FrameCapture.podspec +21 -0
- package/LICENSE +20 -0
- package/README.md +158 -0
- package/android/build.gradle +77 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/com/framecapture/CaptureManager.kt +831 -0
- package/android/src/main/java/com/framecapture/Constants.kt +196 -0
- package/android/src/main/java/com/framecapture/ErrorHandler.kt +165 -0
- package/android/src/main/java/com/framecapture/FrameCaptureModule.kt +653 -0
- package/android/src/main/java/com/framecapture/FrameCapturePackage.kt +32 -0
- package/android/src/main/java/com/framecapture/OverlayRenderer.kt +423 -0
- package/android/src/main/java/com/framecapture/PermissionHandler.kt +150 -0
- package/android/src/main/java/com/framecapture/ScreenCaptureService.kt +366 -0
- package/android/src/main/java/com/framecapture/StorageManager.kt +221 -0
- package/android/src/main/java/com/framecapture/capture/BitmapProcessor.kt +157 -0
- package/android/src/main/java/com/framecapture/capture/CaptureEventEmitter.kt +120 -0
- package/android/src/main/java/com/framecapture/models/CaptureModels.kt +302 -0
- package/android/src/main/java/com/framecapture/models/EnumsAndExtensions.kt +60 -0
- package/android/src/main/java/com/framecapture/models/OverlayModels.kt +154 -0
- package/android/src/main/java/com/framecapture/service/CaptureNotificationManager.kt +286 -0
- package/android/src/main/java/com/framecapture/storage/StorageStrategies.kt +317 -0
- package/android/src/main/java/com/framecapture/utils/ValidationUtils.kt +379 -0
- package/app.plugin.js +1 -0
- package/ios/FrameCapture.h +5 -0
- package/ios/FrameCapture.mm +21 -0
- package/lib/module/NativeFrameCapture.js +24 -0
- package/lib/module/NativeFrameCapture.js.map +1 -0
- package/lib/module/api.js +146 -0
- package/lib/module/api.js.map +1 -0
- package/lib/module/constants.js +67 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/errors.js +19 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/events.js +58 -0
- package/lib/module/events.js.map +1 -0
- package/lib/module/index.js +24 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalize.js +51 -0
- package/lib/module/normalize.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +165 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/validation.js +190 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/plugin/src/index.d.ts +4 -0
- package/lib/typescript/plugin/src/index.d.ts.map +1 -0
- package/lib/typescript/src/NativeFrameCapture.d.ts +75 -0
- package/lib/typescript/src/NativeFrameCapture.d.ts.map +1 -0
- package/lib/typescript/src/api.d.ts +66 -0
- package/lib/typescript/src/api.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +41 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +14 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/events.d.ts +30 -0
- package/lib/typescript/src/events.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +12 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/normalize.d.ts +43 -0
- package/lib/typescript/src/normalize.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +247 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/validation.d.ts +15 -0
- package/lib/typescript/src/validation.d.ts.map +1 -0
- package/package.json +196 -0
- package/plugin/build/index.js +48 -0
- package/src/NativeFrameCapture.ts +86 -0
- package/src/api.ts +189 -0
- package/src/constants.ts +69 -0
- package/src/errors.ts +21 -0
- package/src/events.ts +61 -0
- package/src/index.tsx +31 -0
- package/src/normalize.ts +81 -0
- package/src/types.ts +327 -0
- 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
|
+
}
|