package com.margelo.nitro.co.zyke.dfu import android.content.Context import android.net.Uri import no.nordicsemi.android.dfu.DfuBaseService import no.nordicsemi.android.dfu.DfuProgressListenerAdapter import no.nordicsemi.android.dfu.DfuServiceController import no.nordicsemi.android.dfu.DfuServiceInitiator import no.nordicsemi.android.dfu.DfuServiceListenerHelper class NativeDfuManager : HybridNativeDfuNitroSpec() { private var controller: DfuServiceController? = null private var stateCallback: ((DfuState) -> Unit)? = null private var progressCallback: ((Double, Double, Double, Double, Double) -> Unit)? = null private var errorCallback: ((DfuError, String) -> Unit)? = null override val memorySize: Long = 0 override fun startDfu( options: DfuOptions, stateCallback: (state: DfuState) -> Unit, progressCallback: (part: Double, totalParts: Double, progress: Double, currentSpeed: Double, avgSpeed: Double) -> Unit, errorCallback: (error: DfuError, message: String) -> Unit ) { this.stateCallback = stateCallback this.progressCallback = progressCallback this.errorCallback = errorCallback try { val context = getContext() // Ensure notification channel exists (required for Android 8+ foreground service) DfuServiceInitiator.createDfuNotificationChannel(context) DfuServiceListenerHelper.registerProgressListener(context, dfuProgressListener) val initiator = DfuServiceInitiator(options.deviceId) .setZip(Uri.parse(options.filePath)) options.packetReceiptNotificationParameter?.let { prn -> val prnInt = prn.toInt() initiator.setPacketsReceiptNotificationsEnabled(prnInt > 0) initiator.setPacketsReceiptNotificationsValue(prnInt) } options.forceDfu?.let { initiator.setForceDfu(it) } options.enableUnsafeExperimentalButtonlessDfu?.let { initiator.setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(it) } options.forceScanningForNewAddressInLegacyDfu?.let { initiator.setForceScanningForNewAddressInLegacyDfu(it) } options.numberOfRetries?.let { initiator.setNumberOfRetries(it.toInt()) } controller = initiator.start(context, DfuService::class.java) } catch (e: Exception) { errorCallback.invoke(DfuError.INVALIDINTERNALSTATE, "Failed to start DFU: ${e.message}") cleanup() } } override fun pauseDfu() { controller?.pause() } override fun resumeDfu() { controller?.resume() } override fun abortDfu() { controller?.abort() } private val dfuProgressListener = object : DfuProgressListenerAdapter() { override fun onDeviceConnecting(deviceAddress: String) { stateCallback?.invoke(DfuState.CONNECTING) } override fun onDfuProcessStarting(deviceAddress: String) { stateCallback?.invoke(DfuState.STARTING) } override fun onEnablingDfuMode(deviceAddress: String) { stateCallback?.invoke(DfuState.ENABLINGDFUMODE) } override fun onFirmwareValidating(deviceAddress: String) { stateCallback?.invoke(DfuState.VALIDATING) } override fun onDeviceDisconnecting(deviceAddress: String?) { stateCallback?.invoke(DfuState.DISCONNECTING) } override fun onDfuCompleted(deviceAddress: String) { stateCallback?.invoke(DfuState.COMPLETED) cleanup() } override fun onDfuAborted(deviceAddress: String) { stateCallback?.invoke(DfuState.ABORTED) cleanup() } override fun onProgressChanged( deviceAddress: String, percent: Int, speed: Float, avgSpeed: Float, currentPart: Int, partsTotal: Int ) { stateCallback?.invoke(DfuState.UPLOADING) progressCallback?.invoke( currentPart.toDouble(), partsTotal.toDouble(), percent.toDouble(), speed.toDouble(), avgSpeed.toDouble() ) } override fun onError(deviceAddress: String, error: Int, errorType: Int, message: String?) { val dfuError = mapAndroidError(error, errorType) errorCallback?.invoke(dfuError, message ?: "Unknown error") cleanup() } } private fun cleanup() { try { val context = getContext() DfuServiceListenerHelper.unregisterProgressListener(context, dfuProgressListener) } catch (_: Exception) { } controller = null } companion object { private var appContext: Context? = null fun setContext(context: Context) { appContext = context.applicationContext } fun getContext(): Context { appContext?.let { return it } // Fallback: try to get context from React Native via reflection try { val clazz = Class.forName("android.app.ActivityThread") val method = clazz.getMethod("currentApplication") val app = method.invoke(null) if (app != null) { appContext = (app as Context).applicationContext return appContext!! } } catch (_: Exception) { } throw IllegalStateException("No context available. Make sure NitroDfuPackage is registered.") } private fun mapAndroidError(error: Int, errorType: Int): DfuError { // Strip error masks to get the base error val baseError = error and 0x0FFF // Check if it's a remote error if (error and DfuBaseService.ERROR_REMOTE_MASK != 0) { return mapRemoteError(baseError, errorType) } // Check if it's a connection error if (error and DfuBaseService.ERROR_CONNECTION_MASK != 0) { return DfuError.DEVICEDISCONNECTED } // Local errors return when (error) { DfuBaseService.ERROR_DEVICE_DISCONNECTED -> DfuError.DEVICEDISCONNECTED DfuBaseService.ERROR_FILE_NOT_FOUND -> DfuError.FILENOTSPECIFIED DfuBaseService.ERROR_FILE_ERROR -> DfuError.FILEINVALID DfuBaseService.ERROR_FILE_INVALID -> DfuError.FILEINVALID DfuBaseService.ERROR_FILE_IO_EXCEPTION -> DfuError.FILEIOERROR DfuBaseService.ERROR_SERVICE_DISCOVERY_NOT_STARTED -> DfuError.SERVICEDISCOVERYFAILED DfuBaseService.ERROR_SERVICE_NOT_FOUND -> DfuError.DEVICENOTSUPPORTED DfuBaseService.ERROR_INVALID_RESPONSE -> DfuError.UNSUPPORTEDRESPONSE DfuBaseService.ERROR_FILE_TYPE_UNSUPPORTED -> DfuError.FILETYPEUNSUPPORTED DfuBaseService.ERROR_BLUETOOTH_DISABLED -> DfuError.BLUETOOTHDISABLED DfuBaseService.ERROR_INIT_PACKET_REQUIRED -> DfuError.INITPACKETREQUIRED DfuBaseService.ERROR_FILE_SIZE_INVALID -> DfuError.FILESIZEINVALID DfuBaseService.ERROR_CRC_ERROR -> DfuError.CRCERROR DfuBaseService.ERROR_DEVICE_NOT_BONDED -> DfuError.DEVICENOTBONDED else -> DfuError.INVALIDINTERNALSTATE } } private fun mapRemoteError(baseError: Int, errorType: Int): DfuError { return when (errorType) { DfuBaseService.ERROR_REMOTE_TYPE_LEGACY -> when (baseError) { 2 -> DfuError.REMOTELEGACYDFUINVALIDSTATE 3 -> DfuError.REMOTELEGACYDFUNOTSUPPORTED 4 -> DfuError.REMOTELEGACYDFUDATAEXCEEDSLIMIT 5 -> DfuError.REMOTELEGACYDFUCRCERROR 6 -> DfuError.REMOTELEGACYDFUOPERATIONFAILED else -> DfuError.INVALIDINTERNALSTATE } DfuBaseService.ERROR_REMOTE_TYPE_SECURE -> when (baseError) { 2 -> DfuError.REMOTESECUREDFUOPCODENOTSUPPORTED 3 -> DfuError.REMOTESECUREDFUINVALIDPARAMETER 4 -> DfuError.REMOTESECUREDFUINSUFFICIENTRESOURCES 5 -> DfuError.REMOTESECUREDFUINVALIDOBJECT 6 -> DfuError.REMOTESECUREDFUSIGNATUREMISMATCH 7 -> DfuError.REMOTESECUREDFUUNSUPPORTEDTYPE 8 -> DfuError.REMOTESECUREDFUOPERATIONNOTPERMITTED 10 -> DfuError.REMOTESECUREDFUOPERATIONFAILED 11 -> DfuError.REMOTESECUREDFUEXTENDEDERROR else -> DfuError.INVALIDINTERNALSTATE } DfuBaseService.ERROR_REMOTE_TYPE_SECURE_EXTENDED -> when (baseError) { 0x02 -> DfuError.REMOTEEXTENDEDERRORWRONGCOMMANDFORMAT 0x03 -> DfuError.REMOTEEXTENDEDERRORUNKNOWNCOMMAND 0x04 -> DfuError.REMOTEEXTENDEDERRORINITCOMMANDINVALID 0x05 -> DfuError.REMOTEEXTENDEDERRORFWVERSIONFAILURE 0x06 -> DfuError.REMOTEEXTENDEDERRORHWVERSIONFAILURE 0x07 -> DfuError.REMOTEEXTENDEDERRORSDVERSIONFAILURE 0x08 -> DfuError.REMOTEEXTENDEDERRORSIGNATUREMISSING 0x09 -> DfuError.REMOTEEXTENDEDERRORWRONGHASHTYPE 0x0A -> DfuError.REMOTEEXTENDEDERRORHASHFAILED 0x0B -> DfuError.REMOTEEXTENDEDERRORWRONGSIGNATURETYPE 0x0C -> DfuError.REMOTEEXTENDEDERRORVERIFICATIONFAILED 0x0D -> DfuError.REMOTEEXTENDEDERRORINSUFFICIENTSPACE else -> DfuError.INVALIDINTERNALSTATE } DfuBaseService.ERROR_REMOTE_TYPE_SECURE_BUTTONLESS -> when (baseError) { 2 -> DfuError.REMOTEBUTTONLESSDFUOPCODENOTSUPPORTED 4 -> DfuError.REMOTEBUTTONLESSDFUOPERATIONFAILED 5 -> DfuError.REMOTEBUTTONLESSDFUINVALIDADVERTISEMENTNAME 6 -> DfuError.REMOTEBUTTONLESSDFUBUSY 7 -> DfuError.REMOTEBUTTONLESSDFUNOTBONDED else -> DfuError.INVALIDINTERNALSTATE } else -> DfuError.INVALIDINTERNALSTATE } } } }