Convert Thread+Handler to lifecycle-aware coroutines, add GPL-3.0 license

This commit is contained in:
eddy 2026-07-26 20:43:37 +02:00
commit bbf01946ac
11 changed files with 1034 additions and 100 deletions

View file

@ -1,15 +1,18 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package com.eddy.atomrelay
import android.content.Context
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Handler
import android.os.Looper
import android.util.Log
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import org.dkaukov.esp32.core.EspFlasherApi
import org.dkaukov.esp32.io.ProgressCallback
import java.io.IOException
import kotlin.coroutines.coroutineContext
class EspFlasher(private val context: Context) {
@ -19,12 +22,8 @@ class EspFlasher(private val context: Context) {
interface FlashCallback {
fun onProgress(percent: Int, message: String)
fun onComplete()
fun onError(message: String)
}
private val handler = Handler(Looper.getMainLooper())
@Volatile
private var cancelled = false
@ -32,16 +31,18 @@ class EspFlasher(private val context: Context) {
cancelled = true
}
fun flashDevice(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
suspend fun flashDevice(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
cancelled = false
Thread {
try {
try {
withContext(kotlinx.coroutines.Dispatchers.IO) {
doFlash(usbManager, device, callback)
} catch (e: Exception) {
Log.e(TAG, "Flash failed", e)
handler.post { callback.onError(e.message ?: "Onbekende fout") }
}
}.start()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Flash failed", e)
throw e
}
}
private class FlashStep(
@ -53,9 +54,9 @@ class EspFlasher(private val context: Context) {
val compress: Boolean
)
private fun doFlash(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
Thread.sleep(500)
if (cancelled) return
private suspend fun doFlash(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
kotlinx.coroutines.delay(500)
checkCancelled()
val connection = usbManager.openDevice(device)
?: throw IOException("Kan USB-apparaat niet openen")
@ -73,10 +74,10 @@ class EspFlasher(private val context: Context) {
val epIn = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_IN)
?: throw IOException("Geen USB IN endpoint gevonden")
postProgress(callback, 0, "Verbinden met ESP bootloader...")
callback.onProgress(0, "Verbinden met ESP bootloader...")
val transport = UsbSerialTransport(connection, epOut, epIn)
if (cancelled) return
checkCancelled()
val assets = context.assets
val bootloader = assets.open("firmware/bootloader.bin").readBytes()
@ -84,8 +85,6 @@ class EspFlasher(private val context: Context) {
val bootApp0 = assets.open("firmware/boot_app0.bin").readBytes()
val firmware = assets.open("firmware/firmware.bin").readBytes()
val totalSize = bootloader.size + partitions.size + bootApp0.size + firmware.size
val steps = listOf(
FlashStep("Bootloader", 0x0, bootloader, 30, 10, false),
FlashStep("Partitie tabel", 0x8000, partitions, 40, 10, false),
@ -93,7 +92,7 @@ class EspFlasher(private val context: Context) {
FlashStep("Firmware", 0x10000, firmware, 60, 35, true)
)
postProgress(callback, 5, "Sync met bootloader...")
callback.onProgress(5, "Sync met bootloader...")
val api = EspFlasherApi.connect(transport)
.withCallBack(object : ProgressCallback {
override fun onProgress(pct: Float) {
@ -104,42 +103,42 @@ class EspFlasher(private val context: Context) {
}
})
postProgress(callback, 10, "Chip detecteren...")
callback.onProgress(10, "Chip detecteren...")
val detected = api.chipDetect()
val chipId = detected.getChipId()
Log.i(TAG, "Detected chip: $chipId")
postProgress(callback, 15, "Chip: ${chipId.readableName}")
callback.onProgress(15, "Chip: ${chipId.readableName}")
postProgress(callback, 20, "Stub laden...")
callback.onProgress(20, "Stub laden...")
val stub = detected.loadStub()
postProgress(callback, 25, "Flash wissen...")
callback.onProgress(25, "Flash wissen...")
stub.eraseFlash()
for (step in steps) {
if (cancelled) return
checkCancelled()
val sizeStr = if (step.data.size > 1024) "${step.data.size / 1024}KB" else "${step.data.size} bytes"
postProgress(callback, step.baseProgress, "${step.name} schrijven ($sizeStr)...")
callback.onProgress(step.baseProgress, "${step.name} schrijven ($sizeStr)...")
val writeStub = if (step.compress) stub.withCompression(true) else stub.withCompression(false)
writeStub.writeFlash(step.address, step.data)
val endProgress = step.baseProgress + step.range
postProgress(callback, endProgress.coerceAtMost(95), "${step.name} geschreven")
callback.onProgress(endProgress.coerceAtMost(95), "${step.name} geschreven")
}
postProgress(callback, 95, "ESP resetten...")
callback.onProgress(95, "ESP resetten...")
stub.reset()
postProgress(callback, 100, "Firmware geflashed!")
handler.post { callback.onComplete() }
callback.onProgress(100, "Firmware geflashed!")
} finally {
connection.close()
}
}
private fun postProgress(callback: FlashCallback, percent: Int, message: String) {
Log.d(TAG, "Progress $percent%: $message")
handler.post { callback.onProgress(percent, message) }
private suspend fun checkCancelled() {
if (cancelled) {
coroutineContext.ensureActive()
}
}
}

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package com.eddy.atomrelay
import android.Manifest

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package com.eddy.atomrelay
import android.content.Context

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package com.eddy.atomrelay
import android.app.AlertDialog
@ -13,14 +14,17 @@ import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
@ -44,7 +48,6 @@ class SettingsActivity : AppCompatActivity() {
private var progressDialog: AlertDialog? = null
private var dialog: AlertDialog? = null
private var flasher: EspFlasher? = null
private val handler = Handler(Looper.getMainLooper())
private val usbAttachedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@ -123,9 +126,9 @@ class SettingsActivity : AppCompatActivity() {
currentAction = UsbAction.NONE
flasher?.cancel()
flasher = null
try { unregisterReceiver(usbAttachedReceiver) } catch (e: Exception) {}
try { unregisterReceiver(usbPermissionReceiver) } catch (e: Exception) {}
try { unregisterReceiver(usbDetachedReceiver) } catch (e: Exception) {}
try { unregisterReceiver(usbAttachedReceiver) } catch (_: Exception) {}
try { unregisterReceiver(usbPermissionReceiver) } catch (_: Exception) {}
try { unregisterReceiver(usbDetachedReceiver) } catch (_: Exception) {}
}
override fun onSupportNavigateUp(): Boolean {
@ -155,30 +158,30 @@ class SettingsActivity : AppCompatActivity() {
private fun doKeyExchange(usbManager: UsbManager, device: UsbDevice) {
showProgressDialog("Sleutel Uitwisselen", "Sleutel uitlezen...", STYLE_SPINNER)
Thread {
lifecycleScope.launch(Dispatchers.IO) {
val result = readKeyFromEsp(usbManager, device)
if (isFinishing || isDestroyed) return@Thread
handler.post {
withContext(Dispatchers.Main) {
if (isFinishing || isDestroyed) return@withContext
when (result) {
is KeyResult.Success -> {
val prefs = PrefsHelper.getPrefs(this@SettingsActivity)
prefs.edit().putString(PrefsHelper.PREF_KEY, result.hexKey).apply()
Toast.makeText(this, "Beveiligingssleutel gelezen", Toast.LENGTH_LONG).show()
Toast.makeText(this@SettingsActivity, "Beveiligingssleutel gelezen", Toast.LENGTH_LONG).show()
cleanupAction()
setResult(RESULT_SCAN)
finish()
}
is KeyResult.Error -> {
Toast.makeText(this, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_LONG).show()
Toast.makeText(this@SettingsActivity, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_LONG).show()
cleanupAction()
}
null -> {
Toast.makeText(this, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_SHORT).show()
Toast.makeText(this@SettingsActivity, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_SHORT).show()
cleanupAction()
}
}
}
}.start()
}
}
// === Firmware Flashing ===
@ -209,42 +212,41 @@ class SettingsActivity : AppCompatActivity() {
showProgressDialog("Firmware Flashen", "Initialiseren...", STYLE_HORIZONTAL)
flasher = EspFlasher(this)
flasher?.flashDevice(usbManager, device, object : EspFlasher.FlashCallback {
val progressCallback = object : EspFlasher.FlashCallback {
override fun onProgress(percent: Int, message: String) {
runOnUiThread {
val d = progressDialog ?: return@runOnUiThread
lifecycleScope.launch(Dispatchers.Main) {
val d = progressDialog ?: return@launch
d.findViewById<ProgressBar>(R.id.progressBar)?.progress = percent
d.findViewById<TextView>(R.id.message)?.text = message
}
}
override fun onComplete() {
runOnUiThread {
currentAction = UsbAction.NONE
progressDialog?.dismiss()
progressDialog = null
dialog = AlertDialog.Builder(this@SettingsActivity)
.setTitle("Firmware Flashen")
.setMessage("Firmware geflashed!\n\nOntkoppel de ESP en sluit opnieuw aan om de sleutel in te stellen.")
.setPositiveButton("Sluiten") { d, _ ->
d.dismiss()
dialog = null
}
.setCancelable(true)
.setOnCancelListener { dialog = null }
.show()
}
}
lifecycleScope.launch {
try {
flasher?.flashDevice(usbManager, device, progressCallback)
if (isFinishing || isDestroyed) return@launch
currentAction = UsbAction.NONE
progressDialog?.dismiss()
progressDialog = null
dialog = AlertDialog.Builder(this@SettingsActivity)
.setTitle("Firmware Flashen")
.setMessage("Firmware geflashed!\n\nOntkoppel de ESP en sluit opnieuw aan om de sleutel in te stellen.")
.setPositiveButton("Sluiten") { d, _ ->
d.dismiss()
dialog = null
}
.setCancelable(true)
.setOnCancelListener { dialog = null }
.show()
} catch (e: kotlinx.coroutines.CancellationException) {
cleanupAction()
} catch (e: Exception) {
progressDialog?.dismiss()
progressDialog = null
Toast.makeText(this@SettingsActivity, "Flashen mislukt: ${e.message}", Toast.LENGTH_LONG).show()
cleanupAction()
}
override fun onError(message: String) {
runOnUiThread {
progressDialog?.dismiss()
progressDialog = null
Toast.makeText(this@SettingsActivity, "Flashen mislukt: $message", Toast.LENGTH_LONG).show()
cleanupAction()
}
}
})
}
}
// === USB dispatch ===
@ -403,7 +405,7 @@ class SettingsActivity : AppCompatActivity() {
Log.e(TAG, "Key exchange error", e)
return KeyResult.Error("Fout: ${e.message}")
} finally {
try { connection.close() } catch (e: Exception) {}
try { connection.close() } catch (_: Exception) {}
}
}

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package com.eddy.atomrelay
import android.hardware.usb.UsbConstants

View file

@ -1,3 +1,4 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package com.eddy.atomrelay
import android.hardware.usb.UsbConstants