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,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
|
+
}
|