package com.levinencrypteduploader import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.os.Build import android.util.Log import android.webkit.MimeTypeMap import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.core.DeviceEventManagerModule import com.levinencrypteduploader.uploadservice.UploadService import java.io.File import java.util.concurrent.Executors import java.util.UUID import java.net.URL import javax.crypto.Cipher import javax.crypto.CipherInputStream import javax.crypto.CipherOutputStream import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import java.util.Base64 import java.io.IOException @ReactModule(name = LevinEncryptedUploaderModule.NAME) class LevinEncryptedUploaderModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { companion object { const val NAME = "LevinEncryptedUploader" } private val TAG = "LevinEncryptedUploader" private var notificationChannelID = "BackgroundUploadChannel" private val executor = Executors.newSingleThreadExecutor() private val downloadTasks = mutableMapOf() private val uploadService: UploadService by lazy { UploadService.getInstance(reactContext) } private val eventEmitter: DeviceEventManagerModule.RCTDeviceEventEmitter by lazy { reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) } private var currentUploadId: String? = null override fun getName(): String = NAME @ReactMethod fun addListener(eventName: String) { // Keep: Required for RN built in Event Emitter } @ReactMethod fun removeListeners(count: Int) { // Keep: Required for RN built in Event Emitter } @ReactMethod fun getFileInfo(path: String, promise: Promise) { try { val file = File(path) val exists = file.exists() val name = file.name val extension = name.substringAfterLast('.', "") val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "application/octet-stream" val size = if (exists) file.length() else 0 val result = Arguments.createMap().apply { putString("mimeType", mimeType) putDouble("size", size.toDouble()) putBoolean("exists", exists) putString("name", name) putString("extension", extension) } promise.resolve(result) } catch (e: Exception) { Log.e(TAG, "Error getting file info", e) promise.reject("E_FILE_INFO_ERROR", e.message, e) } } @ReactMethod fun startUpload(options: ReadableMap, promise: Promise) { try { val uploadUrl = options.getString("url") ?: throw IllegalArgumentException("URL is required") val fileUri = options.getString("path") ?: throw IllegalArgumentException("File path is required") val method = options.getString("method") ?: "POST" val headers = options.getMap("headers")?.toHashMap()?.mapValues { it.value.toString() } ?: emptyMap() val encryptionKey = options.getString("encryptionKey") ?: throw IllegalArgumentException("Encryption key is required") val encryptionNonce = options.getString("encryptionNonce") ?: throw IllegalArgumentException("Encryption nonce is required") Log.d(TAG, "Received file URI: $fileUri") // Convert file URI to actual file path val filePath = if (fileUri.startsWith("file://")) { fileUri.substring(7) } else { fileUri } Log.d(TAG, "Starting upload to $uploadUrl") Log.d(TAG, "Converted file path: $filePath") Log.d(TAG, "Method: $method") Log.d(TAG, "Headers: $headers") val file = File(filePath) if (!file.exists()) { Log.e(TAG, "File does not exist at path: $filePath") throw IllegalArgumentException("File does not exist: $filePath") } Log.d(TAG, "File exists, size: ${file.length()} bytes") currentUploadId = uploadService.startUpload( url = uploadUrl, filePath = filePath, method = method, headers = headers, encryptionKey = encryptionKey, encryptionNonce = encryptionNonce, onProgress = { progress -> val params = Arguments.createMap().apply { putString("uploadId", currentUploadId) putInt("progress", progress) } eventEmitter.emit("uploadProgress", params) }, onComplete = { id -> val params = Arguments.createMap().apply { putString("uploadId", id) } eventEmitter.emit("uploadCompleted", params) promise.resolve(id) }, onError = { error -> val params = Arguments.createMap().apply { putString("uploadId", currentUploadId) putString("error", error.message) } eventEmitter.emit("uploadError", params) promise.reject("UPLOAD_ERROR", error) } ) } catch (e: Exception) { Log.e(TAG, "Error starting upload", e) promise.reject("UPLOAD_ERROR", e) } } @ReactMethod fun cancelUpload(uploadId: String, promise: Promise) { try { uploadService.cancelUpload(uploadId) promise.resolve(null) } catch (e: Exception) { Log.e(TAG, "Error canceling upload", e) promise.reject("CANCEL_ERROR", e) } } @ReactMethod fun startDownload(options: ReadableMap, promise: Promise) { try { val url = options.getString("url") ?: throw IllegalArgumentException("URL is required") val path = options.getString("path") ?: throw IllegalArgumentException("Path is required") val method = options.getString("method") ?: "GET" val headers = options.getMap("headers") val customTransferId = options.getString("customTransferId") val taskId = customTransferId ?: UUID.randomUUID().toString() executor.execute { try { val connection = URL(url).openConnection() as java.net.HttpURLConnection connection.requestMethod = method headers?.let { val iterator = it.keySetIterator() while (iterator.hasNextKey()) { val headerKey = iterator.nextKey() connection.setRequestProperty(headerKey, it.getString(headerKey)) } } val responseCode = connection.responseCode if (responseCode in 200..299) { val file = File(path) file.parentFile?.mkdirs() connection.inputStream.use { input -> file.outputStream().use { output -> input.copyTo(output) } } promise.resolve(taskId) } else { throw Exception("Download failed with status code: $responseCode") } } catch (e: Exception) { promise.reject("E_DOWNLOAD_ERROR", e.message, e) } } } catch (e: Exception) { promise.reject("E_DOWNLOAD_ERROR", e.message, e) } } @ReactMethod fun cancelDownload(downloadId: String, promise: Promise) { try { val task = downloadTasks[downloadId] if (task != null) { downloadTasks.remove(downloadId) promise.resolve(true) } else { promise.reject("E_INVALID_ARGUMENT", "Invalid download ID") } } catch (e: Exception) { promise.reject("E_CANCEL_ERROR", e.message, e) } } @ReactMethod fun downloadAndDecrypt(options: ReadableMap, promise: Promise) { try { val url = options.getString("url") ?: throw IllegalArgumentException("URL is required") val destination = options.getString("destination") ?: throw IllegalArgumentException("Destination is required") val headers = options.getMap("headers") val encryptionKey = options.getString("encryptionKey") ?: throw IllegalArgumentException("Encryption key is required") val encryptionNonce = options.getString("encryptionNonce") ?: throw IllegalArgumentException("Encryption nonce is required") Log.d(TAG, "Starting download from: $url") Log.d(TAG, "Destination: $destination") executor.execute { try { val connection = URL(url).openConnection() as java.net.HttpURLConnection connection.requestMethod = "GET" headers?.let { val iterator = it.keySetIterator() while (iterator.hasNextKey()) { val key = iterator.nextKey() connection.setRequestProperty(key, it.getString(key)) } } val responseCode = connection.responseCode if (responseCode in 200..299) { // Convert file URI to actual file path val filePath = if (destination.startsWith("file:")) { destination.substring(5) } else { destination } Log.d(TAG, "Writing to file path: $filePath") val file = File(filePath) // Ensure parent directories exist file.parentFile?.mkdirs() if (!file.parentFile?.exists()!!) { throw IOException("Failed to create directory: ${file.parentFile?.absolutePath}") } val cipher = Cipher.getInstance("AES/GCM/NoPadding") val keySpec = SecretKeySpec(Base64.getDecoder().decode(encryptionKey), "AES") val ivSpec = IvParameterSpec(Base64.getDecoder().decode(encryptionNonce)) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) connection.inputStream.use { input -> CipherOutputStream(file.outputStream(), cipher).use { output -> input.copyTo(output) } } Log.d(TAG, "File downloaded and decrypted successfully") val result = Arguments.createMap().apply { putString("path", destination) } promise.resolve(result) } else { throw Exception("Download failed with status code: $responseCode") } } catch (e: Exception) { Log.e(TAG, "Error downloading and decrypting file", e) promise.reject("E_DOWNLOAD_ERROR", e.message, e) } } } catch (e: Exception) { Log.e(TAG, "Error in downloadAndDecrypt", e) promise.reject("E_DOWNLOAD_ERROR", e.message, e) } } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= 26) { val channel = NotificationChannel( notificationChannelID, "Background Upload Channel", NotificationManager.IMPORTANCE_LOW ).apply { description = "Channel for background upload notifications" enableLights(false) enableVibration(false) setShowBadge(false) } val manager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager manager.createNotificationChannel(channel) } } }