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,423 @@
|
|
|
1
|
+
package com.framecapture
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.*
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import android.util.LruCache
|
|
8
|
+
import com.framecapture.models.OverlayConfig
|
|
9
|
+
import com.framecapture.models.OverlayPosition
|
|
10
|
+
import com.framecapture.models.TextStyle
|
|
11
|
+
import java.io.File
|
|
12
|
+
import java.text.SimpleDateFormat
|
|
13
|
+
import java.util.*
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Renders overlays (text and images) on captured frame bitmaps
|
|
17
|
+
* Uses caching for performance optimization
|
|
18
|
+
*/
|
|
19
|
+
class OverlayRenderer(
|
|
20
|
+
private val context: Context,
|
|
21
|
+
private val eventEmitter: ((String, com.facebook.react.bridge.WritableMap?) -> Unit)? = null,
|
|
22
|
+
cacheSize: Int = Constants.DEFAULT_OVERLAY_IMAGE_CACHE_SIZE
|
|
23
|
+
) {
|
|
24
|
+
|
|
25
|
+
companion object {
|
|
26
|
+
private const val TAG = "OverlayRenderer"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Image cache to avoid repeated file I/O
|
|
30
|
+
private var imageCache: LruCache<String, Bitmap> = object : LruCache<String, Bitmap>(cacheSize) {
|
|
31
|
+
override fun sizeOf(key: String, bitmap: Bitmap): Int {
|
|
32
|
+
return bitmap.byteCount
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Updates the cache size (recreates the cache)
|
|
38
|
+
*/
|
|
39
|
+
fun updateCacheSize(newSize: Int) {
|
|
40
|
+
clearCaches()
|
|
41
|
+
imageCache = object : LruCache<String, Bitmap>(newSize) {
|
|
42
|
+
override fun sizeOf(key: String, bitmap: Bitmap): Int {
|
|
43
|
+
return bitmap.byteCount
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Paint object pool for text rendering
|
|
49
|
+
private val textPaintCache = mutableMapOf<String, Paint>()
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Renders all overlays on the bitmap
|
|
53
|
+
*
|
|
54
|
+
* @param bitmap The bitmap to draw overlays on (modified in-place)
|
|
55
|
+
* @param overlays List of overlay configurations
|
|
56
|
+
* @param frameNumber Current frame number for variable substitution
|
|
57
|
+
* @param sessionId Current session ID for variable substitution
|
|
58
|
+
*/
|
|
59
|
+
fun renderOverlays(
|
|
60
|
+
bitmap: Bitmap,
|
|
61
|
+
overlays: List<OverlayConfig>,
|
|
62
|
+
frameNumber: Int,
|
|
63
|
+
sessionId: String
|
|
64
|
+
) {
|
|
65
|
+
if (overlays.isEmpty()) return
|
|
66
|
+
|
|
67
|
+
val canvas = Canvas(bitmap)
|
|
68
|
+
|
|
69
|
+
overlays.forEachIndexed { index, overlay ->
|
|
70
|
+
try {
|
|
71
|
+
when (overlay) {
|
|
72
|
+
is OverlayConfig.Text -> {
|
|
73
|
+
// Validate text overlay has content
|
|
74
|
+
if (overlay.content.isEmpty()) {
|
|
75
|
+
return@forEachIndexed
|
|
76
|
+
}
|
|
77
|
+
renderTextOverlay(canvas, overlay, frameNumber, sessionId)
|
|
78
|
+
}
|
|
79
|
+
is OverlayConfig.Image -> {
|
|
80
|
+
// Validate image overlay has source
|
|
81
|
+
if (overlay.source.isEmpty()) {
|
|
82
|
+
return@forEachIndexed
|
|
83
|
+
}
|
|
84
|
+
renderImageOverlay(canvas, overlay, bitmap.width, bitmap.height)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (e: Exception) {
|
|
88
|
+
Log.e(TAG, "Failed to render overlay at index $index (type: ${overlay.type}): ${e.message}", e)
|
|
89
|
+
emitOverlayError(index, overlay.type, e.message ?: "Unknown error")
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Substitutes template variables in text content
|
|
96
|
+
*/
|
|
97
|
+
private fun substituteVariables(content: String, frameNumber: Int, sessionId: String): String {
|
|
98
|
+
var result = content
|
|
99
|
+
|
|
100
|
+
// {frameNumber}
|
|
101
|
+
result = result.replace(Constants.TEMPLATE_VAR_FRAME_NUMBER, frameNumber.toString())
|
|
102
|
+
|
|
103
|
+
// {sessionId}
|
|
104
|
+
result = result.replace(Constants.TEMPLATE_VAR_SESSION_ID, sessionId)
|
|
105
|
+
|
|
106
|
+
// {timestamp}
|
|
107
|
+
val timestamp = SimpleDateFormat(Constants.OVERLAY_TIMESTAMP_FORMAT, Locale.US).apply {
|
|
108
|
+
timeZone = TimeZone.getTimeZone(Constants.OVERLAY_TIMESTAMP_TIMEZONE)
|
|
109
|
+
}.format(Date())
|
|
110
|
+
result = result.replace(Constants.TEMPLATE_VAR_TIMESTAMP, timestamp)
|
|
111
|
+
|
|
112
|
+
return result
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Calculates absolute pixel position from OverlayPosition
|
|
117
|
+
*/
|
|
118
|
+
private fun calculatePosition(
|
|
119
|
+
position: OverlayPosition,
|
|
120
|
+
canvasWidth: Int,
|
|
121
|
+
canvasHeight: Int,
|
|
122
|
+
overlayWidth: Int,
|
|
123
|
+
overlayHeight: Int
|
|
124
|
+
): Pair<Int, Int> {
|
|
125
|
+
return when (position) {
|
|
126
|
+
is OverlayPosition.Preset -> {
|
|
127
|
+
val padding = Constants.OVERLAY_DEFAULT_PADDING
|
|
128
|
+
when (position.value) {
|
|
129
|
+
Constants.POSITION_TOP_LEFT -> padding to padding
|
|
130
|
+
Constants.POSITION_TOP_RIGHT -> (canvasWidth - overlayWidth - padding) to padding
|
|
131
|
+
Constants.POSITION_BOTTOM_LEFT -> padding to (canvasHeight - overlayHeight - padding)
|
|
132
|
+
Constants.POSITION_BOTTOM_RIGHT -> (canvasWidth - overlayWidth - padding) to (canvasHeight - overlayHeight - padding)
|
|
133
|
+
Constants.POSITION_CENTER -> ((canvasWidth - overlayWidth) / 2) to ((canvasHeight - overlayHeight) / 2)
|
|
134
|
+
else -> padding to padding
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
is OverlayPosition.Coordinates -> {
|
|
138
|
+
val x = if (position.unit == Constants.POSITION_UNIT_PERCENTAGE) {
|
|
139
|
+
(position.x * canvasWidth).toInt()
|
|
140
|
+
} else {
|
|
141
|
+
position.x.toInt()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
val y = if (position.unit == Constants.POSITION_UNIT_PERCENTAGE) {
|
|
145
|
+
(position.y * canvasHeight).toInt()
|
|
146
|
+
} else {
|
|
147
|
+
position.y.toInt()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Clamp to canvas bounds
|
|
151
|
+
val clampedX = x.coerceIn(0, canvasWidth - overlayWidth)
|
|
152
|
+
val clampedY = y.coerceIn(0, canvasHeight - overlayHeight)
|
|
153
|
+
|
|
154
|
+
clampedX to clampedY
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Renders a text overlay on the canvas
|
|
161
|
+
*/
|
|
162
|
+
private fun renderTextOverlay(
|
|
163
|
+
canvas: Canvas,
|
|
164
|
+
overlay: OverlayConfig.Text,
|
|
165
|
+
frameNumber: Int,
|
|
166
|
+
sessionId: String
|
|
167
|
+
) {
|
|
168
|
+
try {
|
|
169
|
+
// Substitute template variables
|
|
170
|
+
val text = substituteVariables(overlay.content, frameNumber, sessionId)
|
|
171
|
+
|
|
172
|
+
// Get or create paint object
|
|
173
|
+
val paint = getTextPaint(overlay.style)
|
|
174
|
+
|
|
175
|
+
// Measure text bounds
|
|
176
|
+
val bounds = Rect()
|
|
177
|
+
paint.getTextBounds(text, 0, text.length, bounds)
|
|
178
|
+
|
|
179
|
+
val textWidth = bounds.width()
|
|
180
|
+
val textHeight = bounds.height()
|
|
181
|
+
|
|
182
|
+
// Calculate position
|
|
183
|
+
val (x, y) = calculatePosition(
|
|
184
|
+
overlay.position,
|
|
185
|
+
canvas.width,
|
|
186
|
+
canvas.height,
|
|
187
|
+
textWidth + overlay.style.padding * 2,
|
|
188
|
+
textHeight + overlay.style.padding * 2
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
// Draw background if specified
|
|
192
|
+
if (overlay.style.backgroundColor.isNotEmpty()) {
|
|
193
|
+
val parsedColor = parseColor(overlay.style.backgroundColor)
|
|
194
|
+
|
|
195
|
+
val bgPaint = Paint().apply {
|
|
196
|
+
color = parsedColor
|
|
197
|
+
style = Paint.Style.FILL
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
canvas.drawRect(
|
|
201
|
+
x.toFloat(),
|
|
202
|
+
y.toFloat(),
|
|
203
|
+
x + textWidth + overlay.style.padding * 2f,
|
|
204
|
+
y + textHeight + overlay.style.padding * 2f,
|
|
205
|
+
bgPaint
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Draw text
|
|
210
|
+
canvas.drawText(
|
|
211
|
+
text,
|
|
212
|
+
x + overlay.style.padding.toFloat(),
|
|
213
|
+
y + textHeight + overlay.style.padding.toFloat(),
|
|
214
|
+
paint
|
|
215
|
+
)
|
|
216
|
+
} catch (e: Exception) {
|
|
217
|
+
Log.e(TAG, "Error rendering text overlay: ${e.message}", e)
|
|
218
|
+
throw e
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Renders an image overlay on the canvas
|
|
224
|
+
*/
|
|
225
|
+
private fun renderImageOverlay(
|
|
226
|
+
canvas: Canvas,
|
|
227
|
+
overlay: OverlayConfig.Image,
|
|
228
|
+
canvasWidth: Int,
|
|
229
|
+
canvasHeight: Int
|
|
230
|
+
) {
|
|
231
|
+
try {
|
|
232
|
+
// Load image (from cache or file)
|
|
233
|
+
val image = loadImage(overlay.source)
|
|
234
|
+
?: throw IllegalArgumentException("Failed to load image from source: ${overlay.source}")
|
|
235
|
+
|
|
236
|
+
// Determine image size
|
|
237
|
+
val (width, height) = overlay.size?.let { it.width to it.height }
|
|
238
|
+
?: (image.width to image.height)
|
|
239
|
+
|
|
240
|
+
// Calculate position
|
|
241
|
+
val (x, y) = calculatePosition(
|
|
242
|
+
overlay.position,
|
|
243
|
+
canvasWidth,
|
|
244
|
+
canvasHeight,
|
|
245
|
+
width,
|
|
246
|
+
height
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
// Create paint with opacity
|
|
250
|
+
val paint = Paint().apply {
|
|
251
|
+
alpha = (overlay.opacity * 255).toInt()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Scale image if needed
|
|
255
|
+
val scaledImage = if (overlay.size != null) {
|
|
256
|
+
Bitmap.createScaledBitmap(image, width, height, true)
|
|
257
|
+
} else {
|
|
258
|
+
image
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Draw image
|
|
262
|
+
canvas.drawBitmap(scaledImage, x.toFloat(), y.toFloat(), paint)
|
|
263
|
+
|
|
264
|
+
// Clean up scaled bitmap if created
|
|
265
|
+
if (scaledImage != image) {
|
|
266
|
+
scaledImage.recycle()
|
|
267
|
+
}
|
|
268
|
+
} catch (e: Exception) {
|
|
269
|
+
Log.e(TAG, "Error rendering image overlay from ${overlay.source}: ${e.message}", e)
|
|
270
|
+
throw e
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Gets or creates a Paint object for text rendering
|
|
276
|
+
*/
|
|
277
|
+
private fun getTextPaint(style: TextStyle): Paint {
|
|
278
|
+
val cacheKey = "${style.fontSize}_${style.color}_${style.fontWeight}_${style.textAlign}"
|
|
279
|
+
|
|
280
|
+
return textPaintCache.getOrPut(cacheKey) {
|
|
281
|
+
Paint().apply {
|
|
282
|
+
color = parseColor(style.color)
|
|
283
|
+
textSize = style.fontSize.toFloat()
|
|
284
|
+
isAntiAlias = true
|
|
285
|
+
isFakeBoldText = style.fontWeight == Constants.TEXT_WEIGHT_BOLD
|
|
286
|
+
textAlign = when (style.textAlign) {
|
|
287
|
+
Constants.TEXT_ALIGN_CENTER -> Paint.Align.CENTER
|
|
288
|
+
Constants.TEXT_ALIGN_RIGHT -> Paint.Align.RIGHT
|
|
289
|
+
else -> Paint.Align.LEFT
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Loads an image from drawable resource, file, or cache
|
|
297
|
+
*
|
|
298
|
+
* Supports:
|
|
299
|
+
* - Drawable resource names: "logo", "ic_watermark" (looks in app's drawable folder)
|
|
300
|
+
* - File URIs: "file:///path/to/image.png"
|
|
301
|
+
* - Content URIs: "content://media/external/images/media/123"
|
|
302
|
+
*/
|
|
303
|
+
private fun loadImage(source: String): Bitmap? {
|
|
304
|
+
// Check cache first
|
|
305
|
+
imageCache.get(source)?.let { return it }
|
|
306
|
+
|
|
307
|
+
return try {
|
|
308
|
+
val bitmap = when {
|
|
309
|
+
// Check if it's a drawable resource name (no scheme, just a name)
|
|
310
|
+
!source.contains("://") && !source.startsWith("/") -> {
|
|
311
|
+
loadImageFromDrawable(source)
|
|
312
|
+
}
|
|
313
|
+
// File or content URI
|
|
314
|
+
else -> {
|
|
315
|
+
loadImageFromUri(source)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Cache the loaded image
|
|
320
|
+
bitmap?.let { imageCache.put(source, it) }
|
|
321
|
+
|
|
322
|
+
bitmap
|
|
323
|
+
} catch (e: Exception) {
|
|
324
|
+
null
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Loads an image from drawable resources by name
|
|
330
|
+
*/
|
|
331
|
+
private fun loadImageFromDrawable(resourceName: String): Bitmap? {
|
|
332
|
+
return try {
|
|
333
|
+
val resourceId = context.resources.getIdentifier(
|
|
334
|
+
resourceName,
|
|
335
|
+
Constants.RESOURCE_TYPE_DRAWABLE,
|
|
336
|
+
context.packageName
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if (resourceId == 0) {
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
BitmapFactory.decodeResource(context.resources, resourceId)
|
|
344
|
+
} catch (e: Exception) {
|
|
345
|
+
null
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Loads an image from file or content URI
|
|
351
|
+
*/
|
|
352
|
+
private fun loadImageFromUri(source: String): Bitmap? {
|
|
353
|
+
return try {
|
|
354
|
+
val uri = Uri.parse(source)
|
|
355
|
+
val path = when (uri.scheme) {
|
|
356
|
+
Constants.URI_SCHEME_FILE -> uri.path
|
|
357
|
+
Constants.URI_SCHEME_CONTENT -> uri.toString()
|
|
358
|
+
else -> source
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (path != null && File(path).exists()) {
|
|
362
|
+
BitmapFactory.decodeFile(path)
|
|
363
|
+
} else {
|
|
364
|
+
context.contentResolver.openInputStream(uri)?.use {
|
|
365
|
+
BitmapFactory.decodeStream(it)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch (e: Exception) {
|
|
369
|
+
null
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Parses hex color string to Android Color int
|
|
375
|
+
* Supports both #RRGGBB and #RRGGBBAA formats
|
|
376
|
+
*/
|
|
377
|
+
private fun parseColor(colorString: String): Int {
|
|
378
|
+
return try {
|
|
379
|
+
var color = colorString.trim()
|
|
380
|
+
|
|
381
|
+
// Handle #RRGGBBAA format (8 chars including #)
|
|
382
|
+
if (color.length == 9 && color.startsWith("#")) {
|
|
383
|
+
// Extract components
|
|
384
|
+
val r = color.substring(1, 3).toInt(16)
|
|
385
|
+
val g = color.substring(3, 5).toInt(16)
|
|
386
|
+
val b = color.substring(5, 7).toInt(16)
|
|
387
|
+
val a = color.substring(7, 9).toInt(16)
|
|
388
|
+
|
|
389
|
+
// Construct ARGB color
|
|
390
|
+
return Color.argb(a, r, g, b)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// For other formats, use standard parser
|
|
394
|
+
Color.parseColor(color)
|
|
395
|
+
} catch (e: Exception) {
|
|
396
|
+
Color.WHITE
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Emits overlay error event to JavaScript
|
|
402
|
+
*/
|
|
403
|
+
private fun emitOverlayError(overlayIndex: Int, overlayType: String, errorMessage: String) {
|
|
404
|
+
try {
|
|
405
|
+
val params = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
406
|
+
putInt("overlayIndex", overlayIndex)
|
|
407
|
+
putString("overlayType", overlayType)
|
|
408
|
+
putString("message", errorMessage)
|
|
409
|
+
}
|
|
410
|
+
eventEmitter?.invoke(Constants.EVENT_OVERLAY_ERROR, params)
|
|
411
|
+
} catch (e: Exception) {
|
|
412
|
+
// Silently fail - event emission is not critical
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Clears all caches
|
|
418
|
+
*/
|
|
419
|
+
fun clearCaches() {
|
|
420
|
+
imageCache.evictAll()
|
|
421
|
+
textPaintCache.clear()
|
|
422
|
+
}
|
|
423
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
package com.framecapture
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.media.projection.MediaProjectionManager
|
|
6
|
+
import com.facebook.react.bridge.Promise
|
|
7
|
+
import com.framecapture.models.ErrorCode
|
|
8
|
+
import com.framecapture.models.PermissionStatus
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handles MediaProjection permission requests and status checks
|
|
12
|
+
*
|
|
13
|
+
* Manages the complete MediaProjection permission flow:
|
|
14
|
+
* - Requests permission via system dialog
|
|
15
|
+
* - Handles activity result callbacks
|
|
16
|
+
* - Stores permission data for later use
|
|
17
|
+
* - Checks permission status
|
|
18
|
+
*
|
|
19
|
+
* Note: MediaProjection permission cannot be checked programmatically on Android.
|
|
20
|
+
* This handler only tracks whether permission data has been granted and stored.
|
|
21
|
+
*/
|
|
22
|
+
class PermissionHandler(
|
|
23
|
+
private val getActivity: () -> Activity?
|
|
24
|
+
) {
|
|
25
|
+
|
|
26
|
+
// Permission request promise
|
|
27
|
+
private var permissionPromise: Promise? = null
|
|
28
|
+
|
|
29
|
+
// Stored projection data after permission granted
|
|
30
|
+
var mediaProjectionData: Intent? = null
|
|
31
|
+
private set
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Requests MediaProjection permission from the user
|
|
35
|
+
*
|
|
36
|
+
* Opens the Android system permission dialog for screen capture access.
|
|
37
|
+
* The result is handled asynchronously via handleActivityResult().
|
|
38
|
+
*
|
|
39
|
+
* @param promise Resolves with PermissionStatus.GRANTED or rejects with error
|
|
40
|
+
*/
|
|
41
|
+
fun requestPermission(promise: Promise) {
|
|
42
|
+
try {
|
|
43
|
+
val activity = getActivity()
|
|
44
|
+
if (activity == null) {
|
|
45
|
+
promise.reject(
|
|
46
|
+
ErrorCode.SYSTEM_ERROR.value,
|
|
47
|
+
"Activity is null, cannot request permission"
|
|
48
|
+
)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if already requesting permission
|
|
53
|
+
if (permissionPromise != null) {
|
|
54
|
+
promise.reject(
|
|
55
|
+
ErrorCode.SYSTEM_ERROR.value,
|
|
56
|
+
"Permission request already in progress"
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Store promise for later resolution
|
|
62
|
+
permissionPromise = promise
|
|
63
|
+
|
|
64
|
+
// Create MediaProjection intent
|
|
65
|
+
val mediaProjectionManager = activity.getSystemService(
|
|
66
|
+
Activity.MEDIA_PROJECTION_SERVICE
|
|
67
|
+
) as? MediaProjectionManager
|
|
68
|
+
|
|
69
|
+
if (mediaProjectionManager == null) {
|
|
70
|
+
permissionPromise = null
|
|
71
|
+
promise.reject(
|
|
72
|
+
ErrorCode.NOT_SUPPORTED.value,
|
|
73
|
+
"MediaProjection service not available"
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
val intent = mediaProjectionManager.createScreenCaptureIntent()
|
|
79
|
+
activity.startActivityForResult(intent, Constants.REQUEST_MEDIA_PROJECTION, null)
|
|
80
|
+
|
|
81
|
+
} catch (e: Exception) {
|
|
82
|
+
permissionPromise = null
|
|
83
|
+
throw e
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Checks current permission status
|
|
89
|
+
*
|
|
90
|
+
* Note: MediaProjection permission cannot be checked programmatically on Android.
|
|
91
|
+
* This only verifies if permission data has been stored from a previous grant.
|
|
92
|
+
*
|
|
93
|
+
* @return PermissionStatus.GRANTED if data exists, NOT_DETERMINED otherwise
|
|
94
|
+
*/
|
|
95
|
+
fun checkPermission(): PermissionStatus {
|
|
96
|
+
return if (mediaProjectionData != null) {
|
|
97
|
+
PermissionStatus.GRANTED
|
|
98
|
+
} else {
|
|
99
|
+
PermissionStatus.NOT_DETERMINED
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Handles activity result from permission request
|
|
105
|
+
*
|
|
106
|
+
* Should be called from the activity event listener. Resolves or rejects
|
|
107
|
+
* the stored promise based on the user's permission decision.
|
|
108
|
+
*
|
|
109
|
+
* @param requestCode Activity request code
|
|
110
|
+
* @param resultCode Activity result code (RESULT_OK or RESULT_CANCELED)
|
|
111
|
+
* @param data Intent containing MediaProjection permission data
|
|
112
|
+
* @return true if this was a MediaProjection request, false otherwise
|
|
113
|
+
*/
|
|
114
|
+
fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
|
115
|
+
if (requestCode != Constants.REQUEST_MEDIA_PROJECTION) {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
val promise = permissionPromise
|
|
120
|
+
permissionPromise = null
|
|
121
|
+
|
|
122
|
+
if (promise == null) {
|
|
123
|
+
// No promise stored - result already handled or unexpected callback
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (resultCode == Activity.RESULT_OK && data != null) {
|
|
128
|
+
// Store projection data for later use
|
|
129
|
+
mediaProjectionData = data
|
|
130
|
+
promise.resolve(PermissionStatus.GRANTED.value)
|
|
131
|
+
} else {
|
|
132
|
+
// User denied permission
|
|
133
|
+
promise.reject(
|
|
134
|
+
ErrorCode.PERMISSION_DENIED.value,
|
|
135
|
+
"User denied MediaProjection permission"
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Clears stored permission data
|
|
144
|
+
* Called during module cleanup to reset state
|
|
145
|
+
*/
|
|
146
|
+
fun clear() {
|
|
147
|
+
mediaProjectionData = null
|
|
148
|
+
permissionPromise = null
|
|
149
|
+
}
|
|
150
|
+
}
|