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,653 @@
1
+ package com.framecapture
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.media.projection.MediaProjectionManager
6
+ import android.util.Log
7
+ import com.facebook.react.bridge.*
8
+ import com.facebook.react.bridge.BaseActivityEventListener
9
+ import com.facebook.react.module.annotations.ReactModule
10
+ import com.facebook.react.modules.core.DeviceEventManagerModule
11
+ import com.framecapture.models.*
12
+ import com.framecapture.utils.ValidationResult
13
+ import com.framecapture.utils.ValidationUtils
14
+ import java.io.IOException
15
+
16
+ /**
17
+ * Main TurboModule implementation for React Native screen capture
18
+ *
19
+ * This module provides the bridge between JavaScript and native Android screen capture
20
+ * functionality. It coordinates between StorageManager, CaptureManager, and the
21
+ * ScreenCaptureService to provide reliable foreground screen capture.
22
+ *
23
+ * Key responsibilities:
24
+ * - Permission management (MediaProjection)
25
+ * - Capture session lifecycle (start/stop/pause/resume)
26
+ * - Event emission to JavaScript
27
+ * - Error handling and reporting
28
+ */
29
+ @ReactModule(name = FrameCaptureModule.NAME)
30
+ class FrameCaptureModule(reactContext: ReactApplicationContext) :
31
+ NativeFrameCaptureSpec(reactContext),
32
+ LifecycleEventListener {
33
+
34
+ // Component dependencies
35
+ private val storageManager: StorageManager
36
+ private var captureManager: CaptureManager? = null
37
+
38
+ // State management
39
+ private var currentSession: CaptureSession? = null
40
+ private var captureState: CaptureState = CaptureState.IDLE
41
+
42
+ // Specialized handlers
43
+ private val permissionHandler: PermissionHandler
44
+ private val errorHandler: ErrorHandler
45
+
46
+ // Activity event listener
47
+ private val activityEventListener = object : BaseActivityEventListener() {
48
+ override fun onActivityResult(
49
+ activity: Activity,
50
+ requestCode: Int,
51
+ resultCode: Int,
52
+ data: Intent?
53
+ ) {
54
+ permissionHandler.handleActivityResult(requestCode, resultCode, data)
55
+ }
56
+ }
57
+
58
+ init {
59
+ // Initialize helper components
60
+ storageManager = StorageManager(reactContext, ::sendEvent)
61
+
62
+ // Initialize handlers
63
+ permissionHandler = PermissionHandler { currentActivity }
64
+ errorHandler = ErrorHandler(
65
+ getStorageSpace = { storageManager.checkStorageSpace() },
66
+ getCaptureState = { captureState }
67
+ )
68
+
69
+ // Register lifecycle listener
70
+ reactContext.addLifecycleEventListener(this)
71
+ reactContext.addActivityEventListener(activityEventListener)
72
+ }
73
+
74
+ override fun getName(): String {
75
+ return NAME
76
+ }
77
+
78
+ // ========== Permission Management ==========
79
+
80
+ /**
81
+ * Requests MediaProjection permission from the user
82
+ *
83
+ * Opens the Android system permission dialog for screen capture access.
84
+ * The result is handled asynchronously through the activity result callback.
85
+ */
86
+ override fun requestPermission(promise: Promise) {
87
+ try {
88
+ permissionHandler.requestPermission(promise)
89
+ } catch (e: Exception) {
90
+ handleError(e, promise)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Checks current MediaProjection permission status
96
+ *
97
+ * Note: MediaProjection permission cannot be checked programmatically on Android.
98
+ * This method only verifies if permission data has been stored from a previous grant.
99
+ *
100
+ * @return PermissionStatus.GRANTED if permission data exists, NOT_DETERMINED otherwise
101
+ */
102
+ override fun checkPermission(promise: Promise) {
103
+ try {
104
+ val status = permissionHandler.checkPermission()
105
+ promise.resolve(status.value)
106
+ } catch (e: Exception) {
107
+ handleError(e, promise)
108
+ }
109
+ }
110
+
111
+ // ========== Capture Control ==========
112
+
113
+ /**
114
+ * Starts a new screen capture session with the specified options
115
+ *
116
+ * Creates and starts a foreground service (ScreenCaptureService) to handle
117
+ * capture in the background. This ensures capture continues even when the
118
+ * app is minimized or the screen is off.
119
+ *
120
+ * @param options Capture configuration (interval, format, quality, etc.)
121
+ * @param promise Resolves with session information or rejects with error
122
+ */
123
+ override fun startCapture(options: ReadableMap, promise: Promise) {
124
+ try {
125
+ // Check if already capturing
126
+ if (captureState == CaptureState.CAPTURING || captureState == CaptureState.PAUSED) {
127
+ promise.reject(
128
+ ErrorCode.ALREADY_CAPTURING.value,
129
+ "Capture session is already active",
130
+ errorHandler.createErrorMap(
131
+ ErrorCode.ALREADY_CAPTURING,
132
+ "Capture session is already active",
133
+ mapOf(Constants.ERROR_DETAIL_CURRENT_STATE to captureState.value)
134
+ )
135
+ )
136
+ return
137
+ }
138
+
139
+ // Check if MediaProjection permission granted
140
+ val mediaProjectionData = permissionHandler.mediaProjectionData
141
+ if (mediaProjectionData == null) {
142
+ promise.reject(
143
+ ErrorCode.PERMISSION_DENIED.value,
144
+ "MediaProjection permission not granted. Call requestPermission() first.",
145
+ errorHandler.createErrorMap(
146
+ ErrorCode.PERMISSION_DENIED,
147
+ "MediaProjection permission not granted",
148
+ mapOf(Constants.ERROR_DETAIL_PERMISSION to "MEDIA_PROJECTION")
149
+ )
150
+ )
151
+ return
152
+ }
153
+
154
+ // Parse and validate options
155
+ val captureOptions = CaptureOptions.fromReadableMap(options)
156
+ val validationResult = ValidationUtils.validateOptions(captureOptions)
157
+
158
+ if (validationResult is ValidationResult.Error) {
159
+ val errorMessage = ValidationUtils.formatValidationError(validationResult)
160
+ ?: "Invalid options"
161
+ promise.reject(
162
+ ErrorCode.INVALID_OPTIONS.value,
163
+ errorMessage,
164
+ errorHandler.createErrorMap(
165
+ ErrorCode.INVALID_OPTIONS,
166
+ errorMessage,
167
+ mapOf(Constants.ERROR_DETAIL_ERRORS to validationResult.messages)
168
+ )
169
+ )
170
+ return
171
+ }
172
+
173
+ // Use foreground service for reliable background capture
174
+ // This ensures capture continues when app is backgrounded
175
+
176
+ // Set module reference so service can emit events to JavaScript
177
+ ScreenCaptureService.setFrameCaptureModule(this)
178
+
179
+ // Create intent for ScreenCaptureService
180
+ val serviceIntent = Intent(reactApplicationContext, ScreenCaptureService::class.java).apply {
181
+ action = Constants.ACTION_START_CAPTURE
182
+
183
+ // Convert options ReadableMap to Bundle
184
+ val optionsBundle = Arguments.toBundle(options)
185
+ putExtra(Constants.EXTRA_CAPTURE_OPTIONS, optionsBundle)
186
+
187
+ // Add projection data
188
+ putExtra(Constants.EXTRA_PROJECTION_DATA, mediaProjectionData)
189
+ }
190
+
191
+ // Start service (use startForegroundService for API 26+)
192
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
193
+ reactApplicationContext.startForegroundService(serviceIntent)
194
+ } else {
195
+ reactApplicationContext.startService(serviceIntent)
196
+ }
197
+
198
+ // Update state
199
+ captureState = CaptureState.CAPTURING
200
+
201
+ // Create session object
202
+ val sessionId = java.util.UUID.randomUUID().toString()
203
+ currentSession = CaptureSession(
204
+ id = sessionId,
205
+ startTime = System.currentTimeMillis(),
206
+ frameCount = 0,
207
+ options = captureOptions
208
+ )
209
+
210
+ // Return session information
211
+ promise.resolve(currentSession?.toWritableMap())
212
+
213
+ } catch (e: Exception) {
214
+ captureState = CaptureState.IDLE
215
+ handleError(e, promise)
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Stops the active capture session
221
+ *
222
+ * Sends a stop action to the ScreenCaptureService, which will terminate
223
+ * the capture, clean up resources, and stop the foreground service.
224
+ *
225
+ * @param promise Resolves when stop is initiated or rejects with error
226
+ */
227
+ override fun stopCapture(promise: Promise) {
228
+ try {
229
+ if (captureState == CaptureState.IDLE) {
230
+ promise.reject(
231
+ ErrorCode.SYSTEM_ERROR.value,
232
+ "No active capture session to stop"
233
+ )
234
+ return
235
+ }
236
+
237
+ // Send stop action to service
238
+ val serviceIntent = Intent(reactApplicationContext, ScreenCaptureService::class.java).apply {
239
+ action = Constants.ACTION_STOP_CAPTURE
240
+ }
241
+ reactApplicationContext.startService(serviceIntent)
242
+
243
+ // Reset state
244
+ captureState = CaptureState.IDLE
245
+ currentSession = null
246
+
247
+ promise.resolve(null)
248
+
249
+ } catch (e: Exception) {
250
+ handleError(e, promise)
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Pauses the active capture session
256
+ *
257
+ * Stops capturing frames but keeps the service running. The session can be
258
+ * resumed later without losing configuration or session state.
259
+ *
260
+ * @param promise Resolves when pause is initiated or rejects with error
261
+ */
262
+ override fun pauseCapture(promise: Promise) {
263
+ try {
264
+ if (captureState != CaptureState.CAPTURING) {
265
+ promise.reject(
266
+ ErrorCode.SYSTEM_ERROR.value,
267
+ "No active capture session to pause"
268
+ )
269
+ return
270
+ }
271
+
272
+ // Send pause action to service (captureManager is null in service mode)
273
+ if (captureManager == null) {
274
+ val serviceIntent = Intent(reactApplicationContext, ScreenCaptureService::class.java).apply {
275
+ action = Constants.ACTION_PAUSE_CAPTURE
276
+ }
277
+ reactApplicationContext.startService(serviceIntent)
278
+ } else {
279
+ // Fallback: Direct mode (not currently used)
280
+ captureManager?.pause()
281
+ }
282
+
283
+ captureState = CaptureState.PAUSED
284
+
285
+ // Emit pause event to JavaScript
286
+ val sessionId = currentSession?.id ?: ""
287
+ val params = Arguments.createMap().apply {
288
+ putString("sessionId", sessionId)
289
+ }
290
+ sendEvent(Constants.EVENT_CAPTURE_PAUSE, params)
291
+
292
+ promise.resolve(null)
293
+
294
+ } catch (e: Exception) {
295
+ handleError(e, promise)
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Resumes a paused capture session
301
+ *
302
+ * Restarts frame capture at the configured interval. The session continues
303
+ * with the same configuration and session ID.
304
+ *
305
+ * @param promise Resolves when resume is initiated or rejects with error
306
+ */
307
+ override fun resumeCapture(promise: Promise) {
308
+ try {
309
+ if (captureState != CaptureState.PAUSED) {
310
+ promise.reject(
311
+ ErrorCode.SYSTEM_ERROR.value,
312
+ "Capture session is not paused"
313
+ )
314
+ return
315
+ }
316
+
317
+ // Send resume action to service (captureManager is null in service mode)
318
+ if (captureManager == null) {
319
+ val serviceIntent = Intent(reactApplicationContext, ScreenCaptureService::class.java).apply {
320
+ action = Constants.ACTION_RESUME_CAPTURE
321
+ }
322
+ reactApplicationContext.startService(serviceIntent)
323
+ } else {
324
+ // Fallback: Direct mode (not currently used)
325
+ captureManager?.resume()
326
+ }
327
+
328
+ captureState = CaptureState.CAPTURING
329
+
330
+ // Emit resume event to JavaScript
331
+ val sessionId = currentSession?.id ?: ""
332
+ val params = Arguments.createMap().apply {
333
+ putString("sessionId", sessionId)
334
+ }
335
+ sendEvent(Constants.EVENT_CAPTURE_RESUME, params)
336
+
337
+ promise.resolve(null)
338
+
339
+ } catch (e: Exception) {
340
+ handleError(e, promise)
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Gets the current capture status including state and session information
346
+ *
347
+ * Returns the current capture state (IDLE, CAPTURING, PAUSED) along with
348
+ * session details like frame count, session ID, and options.
349
+ *
350
+ * @param promise Resolves with CaptureStatus object or rejects with error
351
+ */
352
+ override fun getCaptureStatus(promise: Promise) {
353
+ try {
354
+ val status = if (captureManager != null) {
355
+ // Fallback: Direct mode (not currently used)
356
+ captureManager?.getStatus()
357
+ } else {
358
+ // Service mode - create status with frame count from service
359
+ currentSession?.let { session ->
360
+ CaptureStatus(
361
+ state = captureState,
362
+ session = session.copy(frameCount = ScreenCaptureService.currentFrameCount),
363
+ isPaused = captureState == CaptureState.PAUSED
364
+ )
365
+ }
366
+ } ?: CaptureStatus(
367
+ state = captureState,
368
+ session = currentSession,
369
+ isPaused = captureState == CaptureState.PAUSED
370
+ )
371
+
372
+ promise.resolve(status.toWritableMap())
373
+
374
+ } catch (e: Exception) {
375
+ handleError(e, promise)
376
+ }
377
+ }
378
+
379
+ // ========== Notification Permission (Android 13+) ==========
380
+
381
+ /**
382
+ * Checks if notification permission is granted (Android 13+)
383
+ *
384
+ * On Android 13 (API 33) and above, apps need runtime permission to post notifications.
385
+ * This method checks if that permission has been granted.
386
+ *
387
+ * Note: This only checks the permission status. To request the permission,
388
+ * use React Native's PermissionsAndroid.request() API with POST_NOTIFICATIONS.
389
+ *
390
+ * On Android 12 and below, this always returns GRANTED as no permission is needed.
391
+ *
392
+ * @param promise Resolves with PermissionStatus (GRANTED or DENIED)
393
+ */
394
+ override fun checkNotificationPermission(promise: Promise) {
395
+ try {
396
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
397
+ val hasPermission = reactApplicationContext.checkSelfPermission(
398
+ android.Manifest.permission.POST_NOTIFICATIONS
399
+ ) == android.content.pm.PackageManager.PERMISSION_GRANTED
400
+
401
+ val status = if (hasPermission) {
402
+ PermissionStatus.GRANTED
403
+ } else {
404
+ PermissionStatus.DENIED
405
+ }
406
+ promise.resolve(status.value)
407
+ } else {
408
+ // Below Android 13, notification permission is not required
409
+ promise.resolve(PermissionStatus.GRANTED.value)
410
+ }
411
+ } catch (e: Exception) {
412
+ handleError(e, promise)
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Manually cleans up all temporary frame files
418
+ *
419
+ * Temporary files are created when saveFrames is false. They are automatically
420
+ * cleaned on app startup, but this method allows manual cleanup when needed
421
+ * (e.g., after processing frames or to free up storage space).
422
+ *
423
+ * @param promise Resolves when cleanup is complete or rejects with error
424
+ */
425
+ override fun cleanupTempFrames(promise: Promise) {
426
+ try {
427
+ storageManager.cleanupAllTempFiles()
428
+ promise.resolve(null)
429
+ } catch (e: Exception) {
430
+ handleError(e, promise)
431
+ }
432
+ }
433
+
434
+ // ========== Event Emission ==========
435
+
436
+ /**
437
+ * Sends events to JavaScript using TurboModule EventEmitter
438
+ *
439
+ * Routes events to the appropriate emit method based on event name.
440
+ * Uses the generated emit methods from NativeFrameCaptureSpec.
441
+ *
442
+ * @param eventName The event type (e.g., EVENT_FRAME_CAPTURED)
443
+ * @param params Event data to send to JavaScript
444
+ */
445
+ private fun sendEvent(eventName: String, params: WritableMap?) {
446
+ try {
447
+ // Use the generated emit methods from NativeFrameCaptureSpec
448
+ when (eventName) {
449
+ Constants.EVENT_FRAME_CAPTURED -> params?.let { emitOnFrameCaptured(it) }
450
+ Constants.EVENT_CAPTURE_ERROR -> params?.let { emitOnCaptureError(it) }
451
+ Constants.EVENT_CAPTURE_STOP -> params?.let { emitOnCaptureStop(it) }
452
+ Constants.EVENT_CAPTURE_START -> params?.let { emitOnCaptureStart(it) }
453
+ Constants.EVENT_STORAGE_WARNING -> params?.let { emitOnStorageWarning(it) }
454
+ Constants.EVENT_CAPTURE_PAUSE -> params?.let { emitOnCapturePause(it) }
455
+ Constants.EVENT_CAPTURE_RESUME -> params?.let { emitOnCaptureResume(it) }
456
+ Constants.EVENT_OVERLAY_ERROR -> params?.let { emitOnOverlayError(it) }
457
+ }
458
+ } catch (e: Exception) {
459
+ Log.e(NAME, "Failed to send event $eventName: ${e.message}", e)
460
+ }
461
+ }
462
+
463
+
464
+
465
+ /**
466
+ * Emits frame captured event to JavaScript
467
+ * Contains frame information (path, size, timestamp, frame number)
468
+ */
469
+ private fun emitFrameCapturedEvent(frameInfo: FrameInfo, frameNumber: Int) {
470
+ sendEvent(Constants.EVENT_FRAME_CAPTURED, frameInfo.toWritableMap(frameNumber))
471
+ }
472
+
473
+ /**
474
+ * Emits capture error event to JavaScript
475
+ * Contains error code, message, and optional details
476
+ */
477
+ private fun emitCaptureErrorEvent(code: ErrorCode, message: String, details: Map<String, Any>?) {
478
+ val params = errorHandler.createErrorMap(code, message, details)
479
+ sendEvent(Constants.EVENT_CAPTURE_ERROR, params)
480
+ }
481
+
482
+ /**
483
+ * Emits capture stop event to JavaScript with session statistics
484
+ * Contains session ID, total frames captured, and duration
485
+ */
486
+ private fun emitCaptureStopEvent(sessionId: String, totalFrames: Int, duration: Long) {
487
+ val params = Arguments.createMap().apply {
488
+ putString("sessionId", sessionId)
489
+ putInt("totalFrames", totalFrames)
490
+ putDouble("duration", duration.toDouble())
491
+ }
492
+ sendEvent(Constants.EVENT_CAPTURE_STOP, params)
493
+ }
494
+
495
+ /**
496
+ * Emits capture start event to JavaScript with session information
497
+ * Contains session ID and capture options
498
+ */
499
+ private fun emitCaptureStartEvent(session: CaptureSession) {
500
+ val params = Arguments.createMap().apply {
501
+ putString("sessionId", session.id)
502
+ putMap("options", session.options.toWritableMap())
503
+ }
504
+ sendEvent(Constants.EVENT_CAPTURE_START, params)
505
+ }
506
+
507
+ // ========== Lifecycle Management ==========
508
+
509
+ /**
510
+ * Initializes the module
511
+ * Called when the module is first created by React Native
512
+ */
513
+ override fun initialize() {
514
+ super.initialize()
515
+ // Activity event listener already registered in init block
516
+ }
517
+
518
+ /**
519
+ * Cleans up resources when module is invalidated
520
+ *
521
+ * Called when the React Native bridge is destroyed (app reload, app close).
522
+ * Ensures all capture sessions are stopped and resources are released.
523
+ */
524
+ override fun invalidate() {
525
+ try {
526
+ // Stop capture if active
527
+ if (captureState == CaptureState.CAPTURING || captureState == CaptureState.PAUSED) {
528
+ captureManager?.stop()
529
+ }
530
+
531
+ // Cleanup all resources
532
+ captureManager?.cleanup()
533
+ captureManager = null
534
+
535
+ // Reset state
536
+ captureState = CaptureState.IDLE
537
+ currentSession = null
538
+ permissionHandler.clear()
539
+
540
+ // Remove listeners
541
+ reactApplicationContext.removeLifecycleEventListener(this)
542
+ reactApplicationContext.removeActivityEventListener(activityEventListener)
543
+
544
+ } catch (e: Exception) {
545
+ // Silent cleanup failure
546
+ }
547
+
548
+ super.invalidate()
549
+ }
550
+
551
+ /**
552
+ * Called when the host activity resumes
553
+ * Lifecycle callback - currently unused but available for future enhancements
554
+ */
555
+ override fun onHostResume() {
556
+ // Can be used to resume capture if needed
557
+ }
558
+
559
+ /**
560
+ * Called when the host activity pauses
561
+ * Lifecycle callback - currently unused but available for future enhancements
562
+ */
563
+ override fun onHostPause() {
564
+ // Can be used to pause capture if needed
565
+ }
566
+
567
+ /**
568
+ * Called when the host activity is destroyed
569
+ * Ensures cleanup is performed to prevent resource leaks
570
+ */
571
+ override fun onHostDestroy() {
572
+ // Ensure cleanup is called
573
+ try {
574
+ if (captureState == CaptureState.CAPTURING || captureState == CaptureState.PAUSED) {
575
+ captureManager?.stop()
576
+ captureManager?.cleanup()
577
+ }
578
+ } catch (e: Exception) {
579
+ // Silent cleanup failure
580
+ }
581
+ }
582
+
583
+ // ========== Error Handling ==========
584
+
585
+ /**
586
+ * Handles errors and classifies them into appropriate error codes
587
+ *
588
+ * Delegates to ErrorHandler for classification and reporting. Emits error
589
+ * events to JavaScript and performs cleanup if needed.
590
+ *
591
+ * @param error The exception that occurred
592
+ * @param promise Optional promise to reject with error details
593
+ */
594
+ private fun handleError(error: Exception, promise: Promise? = null) {
595
+ errorHandler.handleError(
596
+ error = error,
597
+ promise = promise,
598
+ onEmitError = { code, message, details ->
599
+ emitCaptureErrorEvent(code, message, details)
600
+ },
601
+ onCleanup = {
602
+ if (captureState != CaptureState.IDLE) {
603
+ captureManager?.cleanup()
604
+ captureState = CaptureState.IDLE
605
+ currentSession = null
606
+ }
607
+ }
608
+ )
609
+ }
610
+
611
+ // ========== Service State Synchronization ==========
612
+
613
+ /**
614
+ * Updates module state from the ScreenCaptureService
615
+ *
616
+ * This keeps the module's state synchronized when actions are triggered
617
+ * from notification buttons (pause/resume/stop). Also emits appropriate
618
+ * events to JavaScript to keep the JS side in sync.
619
+ *
620
+ * @param isPaused True if paused, false if resumed, null if no change
621
+ * @param isStopped True if capture was stopped
622
+ */
623
+ fun updateStateFromService(isPaused: Boolean? = null, isStopped: Boolean = false) {
624
+ try {
625
+ if (isStopped) {
626
+ // Service was stopped
627
+ captureState = CaptureState.IDLE
628
+ currentSession = null
629
+ } else if (isPaused != null) {
630
+ // Pause/resume state changed
631
+ captureState = if (isPaused) CaptureState.PAUSED else CaptureState.CAPTURING
632
+
633
+ // Emit pause/resume event to JavaScript
634
+ val sessionId = currentSession?.id ?: ""
635
+ val params = Arguments.createMap().apply {
636
+ putString("sessionId", sessionId)
637
+ }
638
+
639
+ if (isPaused) {
640
+ sendEvent(Constants.EVENT_CAPTURE_PAUSE, params)
641
+ } else {
642
+ sendEvent(Constants.EVENT_CAPTURE_RESUME, params)
643
+ }
644
+ }
645
+ } catch (e: Exception) {
646
+ Log.e(NAME, "Error updating state from service", e)
647
+ }
648
+ }
649
+
650
+ companion object {
651
+ const val NAME = "FrameCapture"
652
+ }
653
+ }
@@ -0,0 +1,32 @@
1
+ package com.framecapture
2
+
3
+ import com.facebook.react.TurboReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ class FrameCapturePackage : TurboReactPackage() {
10
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
11
+ return if (name == FrameCaptureModule.NAME) {
12
+ FrameCaptureModule(reactContext)
13
+ } else {
14
+ null
15
+ }
16
+ }
17
+
18
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
19
+ return ReactModuleInfoProvider {
20
+ val moduleInfos: MutableMap<String, ReactModuleInfo> = mutableMapOf()
21
+ moduleInfos[FrameCaptureModule.NAME] = ReactModuleInfo(
22
+ FrameCaptureModule.NAME,
23
+ FrameCaptureModule.NAME,
24
+ false, // canOverrideExistingModule
25
+ false, // needsEagerInit
26
+ false, // isCxxModule
27
+ true // isTurboModule
28
+ )
29
+ moduleInfos
30
+ }
31
+ }
32
+ }