package com.levinencrypteduploader.uploadservice import android.content.Context import android.util.Log import okhttp3.* import java.io.File import java.io.FileInputStream import java.io.IOException import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger import javax.crypto.Cipher import javax.crypto.CipherInputStream import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import java.util.Base64 import okhttp3.MediaType.Companion.toMediaType import okhttp3.Headers.Companion.toHeaders class UploadService private constructor(private val context: Context) { private val executor = Executors.newFixedThreadPool(3) private val activeUploads = mutableMapOf() private val uploadIdCounter = AtomicInteger(0) private val client = OkHttpClient.Builder() .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS) .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) .build() companion object { private const val TAG = "UploadService" private var instance: UploadService? = null @Synchronized fun getInstance(context: Context): UploadService { if (instance == null) { instance = UploadService(context.applicationContext) } return instance!! } } fun startUpload( url: String, filePath: String, method: String = "POST", headers: Map = emptyMap(), encryptionKey: String, encryptionNonce: String, onProgress: (Int) -> Unit, onComplete: (String) -> Unit, onError: (Exception) -> Unit ): String { val uploadId = uploadIdCounter.incrementAndGet().toString() executor.execute { try { val file = File(filePath) if (!file.exists()) { throw IOException("File does not exist: $filePath") } // Set up encryption val keySpec = SecretKeySpec(Base64.getDecoder().decode(encryptionKey), "AES") val ivSpec = IvParameterSpec(Base64.getDecoder().decode(encryptionNonce)) val cipher = Cipher.getInstance("AES/GCM/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) val inputStream = FileInputStream(file) val cipherInputStream = CipherInputStream(inputStream, cipher) val requestBuilder = Request.Builder() .url(url) .method(method, object : RequestBody() { override fun contentType(): MediaType = "application/octet-stream".toMediaType() override fun contentLength(): Long { // For AES/GCM, the encrypted size is the original size plus 16 bytes for the auth tag return file.length() + 16 } override fun writeTo(sink: okio.BufferedSink) { val buffer = ByteArray(8192) var bytesRead: Int var totalBytesRead = 0L val fileSize = file.length() while (cipherInputStream.read(buffer).also { bytesRead = it } != -1) { sink.write(buffer, 0, bytesRead) totalBytesRead += bytesRead val progress = ((totalBytesRead.toFloat() / fileSize) * 100).toInt() onProgress(progress) } } }) // Add headers headers.forEach { (key, value) -> requestBuilder.addHeader(key, value) } val request = requestBuilder.build() val call = client.newCall(request) val uploadTask = UploadTask( uploadId = uploadId, call = call, onComplete = onComplete, onError = onError ) activeUploads[uploadId] = uploadTask uploadTask.start() } catch (e: Exception) { Log.e(TAG, "Error starting upload", e) onError(e) } } return uploadId } fun cancelUpload(uploadId: String) { activeUploads[uploadId]?.let { task -> task.cancel() activeUploads.remove(uploadId) } } private inner class UploadTask( val uploadId: String, private val call: Call, private val onComplete: (String) -> Unit, private val onError: (Exception) -> Unit ) { private var isCancelled = false fun start() { try { call.enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { if (!isCancelled) { onError(e) } activeUploads.remove(uploadId) } override fun onResponse(call: Call, response: Response) { if (!isCancelled) { if (response.isSuccessful) { onComplete(uploadId) } else { onError(IOException("Upload failed with response code: ${response.code}")) } } response.close() activeUploads.remove(uploadId) } }) } catch (e: Exception) { if (!isCancelled) { onError(e) } activeUploads.remove(uploadId) } } fun cancel() { isCancelled = true call.cancel() } } }