From 3daa9598f073c599decca3b86d14e93096ecb698 Mon Sep 17 00:00:00 2001 From: Md Abubakar Date: Tue, 5 Aug 2025 19:12:15 +0530 Subject: [PATCH 1/2] fix(android): initialize CameraDevicesManager after React instance setup to avoid JS module access crash --- .../camera/react/CameraDevicesManager.kt | 60 ++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/react/CameraDevicesManager.kt b/package/android/src/main/java/com/mrousavy/camera/react/CameraDevicesManager.kt index 21ac2b2842..6e66f8c4c4 100644 --- a/package/android/src/main/java/com/mrousavy/camera/react/CameraDevicesManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/react/CameraDevicesManager.kt @@ -27,11 +27,11 @@ class CameraDevicesManager(private val reactContext: ReactApplicationContext) : private val cameraManager = reactContext.getSystemService(Context.CAMERA_SERVICE) as CameraManager private var cameraProvider: ProcessCameraProvider? = null private var extensionsManager: ExtensionsManager? = null + private var pendingDevices: ReadableArray? = null private val callback = object : CameraManager.AvailabilityCallback() { private var deviceIds = cameraManager.cameraIdList.toMutableList() - // Check if device is still physically connected (even if onCameraUnavailable() is called) private fun isDeviceConnected(cameraId: String): Boolean = try { cameraManager.getCameraCharacteristics(cameraId) @@ -44,7 +44,7 @@ class CameraDevicesManager(private val reactContext: ReactApplicationContext) : Log.i(TAG, "Camera #$cameraId is now available.") if (!deviceIds.contains(cameraId)) { deviceIds.add(cameraId) - sendAvailableDevicesChangedEvent() + safeSendAvailableDevicesChangedEvent() } } @@ -52,15 +52,22 @@ class CameraDevicesManager(private val reactContext: ReactApplicationContext) : Log.i(TAG, "Camera #$cameraId is now unavailable.") if (deviceIds.contains(cameraId) && !isDeviceConnected(cameraId)) { deviceIds.remove(cameraId) - sendAvailableDevicesChangedEvent() + safeSendAvailableDevicesChangedEvent() } } } override fun getName(): String = TAG - // Init cameraProvider + manager as early as possible - init { + // removed the init { } block — initialization now happens in initialize() + + override fun initialize() { + super.initialize() + + // Register availability callback immediately so we don't miss events + cameraManager.registerAvailabilityCallback(callback, null) + + // Do the heavy camera provider + extensions init on the background executor coroutineScope.launch { try { Log.i(TAG, "Initializing ProcessCameraProvider...") @@ -71,14 +78,22 @@ class CameraDevicesManager(private val reactContext: ReactApplicationContext) : } catch (error: Throwable) { Log.e(TAG, "Failed to initialize ProcessCameraProvider/ExtensionsManager! Error: ${error.message}", error) } - sendAvailableDevicesChangedEvent() + + // Safe send (will buffer if JS not yet ready) + safeSendAvailableDevicesChangedEvent() } - } - // Note: initialize() will be called after getConstants on new arch! - override fun initialize() { - super.initialize() - cameraManager.registerAvailabilityCallback(callback, null) + // If anything was buffered before (rare), attempt to deliver it here also + pendingDevices?.let { + try { + val emitter = reactContext.getJSModule(RCTDeviceEventEmitter::class.java) + emitter.emit("CameraDevicesChanged", it) + } catch (e: IllegalStateException) { + Log.w(TAG, "JS still not ready in initialize(): ${e.message}") + } finally { + pendingDevices = null + } + } } override fun invalidate() { @@ -98,12 +113,28 @@ class CameraDevicesManager(private val reactContext: ReactApplicationContext) : return devices } - fun sendAvailableDevicesChangedEvent() { - val eventEmitter = reactContext.getJSModule(RCTDeviceEventEmitter::class.java) + /** + * Safe send: if JS is ready, emit immediately; otherwise buffer for later. + */ + private fun safeSendAvailableDevicesChangedEvent() { val devices = getDevicesJson() - eventEmitter.emit("CameraDevicesChanged", devices) + if (reactContext.hasActiveCatalystInstance()) { + try { + val eventEmitter = reactContext.getJSModule(RCTDeviceEventEmitter::class.java) + eventEmitter.emit("CameraDevicesChanged", devices) + } catch (e: IllegalStateException) { + Log.w(TAG, "Race condition while emitting CameraDevicesChanged: ${e.message}") + pendingDevices = devices + } + } else { + Log.i(TAG, "Buffering CameraDevicesChanged until JS is ready") + pendingDevices = devices + } } + // keep this simple wrapper to avoid accidental direct calls elsewhere + fun sendAvailableDevicesChangedEvent() = safeSendAvailableDevicesChangedEvent() + override fun getConstants(): MutableMap { val devices = getDevicesJson() val preferredDevice = if (devices.size() > 0) devices.getMap(0) else null @@ -114,7 +145,6 @@ class CameraDevicesManager(private val reactContext: ReactApplicationContext) : ) } - // Required for NativeEventEmitter, this is just a dummy implementation: @Suppress("unused", "UNUSED_PARAMETER") @ReactMethod fun addListener(eventName: String) {} From b766b4a51d315eb3bfb9642258ad9177b3b9e7bf Mon Sep 17 00:00:00 2001 From: Md Abubakar Date: Sat, 1 Nov 2025 10:41:30 +0530 Subject: [PATCH 2/2] Merge upstream changes --- package/android/.project | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/package/android/.project b/package/android/.project index 0e0a1bac2d..f7c5891f30 100644 --- a/package/android/.project +++ b/package/android/.project @@ -14,4 +14,15 @@ org.eclipse.buildship.core.gradleprojectnature + + + 1761973124158 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + +