From e116f2b49340b58e691d033014e3516eeed0a01b Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 26 Jul 2026 17:02:06 +0200 Subject: [PATCH 1/4] Make EspFlasher cancellable to prevent resource leak Add @Volatile cancelled flag with cancel() method. Check flag at 3 checkpoints (after initial sleep, after transport setup, before each flash step) so the thread exits early and closes the USB connection in the finally block. SettingsActivity.onDestroy now calls cancel(). Reviewed in codereview.md (recommendation 3.2). --- .../src/main/java/com/eddy/atomrelay/EspFlasher.kt | 11 +++++++++++ .../main/java/com/eddy/atomrelay/SettingsActivity.kt | 7 +++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/eddy/atomrelay/EspFlasher.kt b/android/app/src/main/java/com/eddy/atomrelay/EspFlasher.kt index 03e3eef..480c410 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/EspFlasher.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/EspFlasher.kt @@ -25,7 +25,15 @@ class EspFlasher(private val context: Context) { private val handler = Handler(Looper.getMainLooper()) + @Volatile + private var cancelled = false + + fun cancel() { + cancelled = true + } + fun flashDevice(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) { + cancelled = false Thread { try { doFlash(usbManager, device, callback) @@ -47,6 +55,7 @@ class EspFlasher(private val context: Context) { private fun doFlash(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) { Thread.sleep(500) + if (cancelled) return val connection = usbManager.openDevice(device) ?: throw IOException("Kan USB-apparaat niet openen") @@ -67,6 +76,7 @@ class EspFlasher(private val context: Context) { postProgress(callback, 0, "Verbinden met ESP bootloader...") val transport = UsbSerialTransport(connection, epOut, epIn) + if (cancelled) return val assets = context.assets val bootloader = assets.open("firmware/bootloader.bin").readBytes() @@ -107,6 +117,7 @@ class EspFlasher(private val context: Context) { stub.eraseFlash() for (step in steps) { + if (cancelled) return 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)...") diff --git a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt index ccfecb3..3b6563b 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt @@ -43,6 +43,7 @@ class SettingsActivity : AppCompatActivity() { private var currentAction = UsbAction.NONE 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() { @@ -120,6 +121,8 @@ class SettingsActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() 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) {} @@ -203,8 +206,8 @@ class SettingsActivity : AppCompatActivity() { private fun doFlashing(usbManager: UsbManager, device: UsbDevice) { showProgressDialog("Firmware Flashen", "Initialiseren...", STYLE_HORIZONTAL) - val flasher = EspFlasher(this) - flasher.flashDevice(usbManager, device, object : EspFlasher.FlashCallback { + flasher = EspFlasher(this) + flasher?.flashDevice(usbManager, device, object : EspFlasher.FlashCallback { override fun onProgress(percent: Int, message: String) { runOnUiThread { val d = progressDialog ?: return@runOnUiThread From 5acde17efb018b8fc75e73bac78daaeac2a1bd58 Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 26 Jul 2026 17:14:50 +0200 Subject: [PATCH 2/4] Fix flash dialog stacking and key exchange reliability 1. Dismiss waiting dialog when flash progress dialog starts, preventing orphaned spinner dialog after flash completes. 2. Increase key exchange retries from 3 to 5 and initial delay from 500ms to 1000ms to give ESP more time to be ready after USB permission grant. --- .../src/main/java/com/eddy/atomrelay/SettingsActivity.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt index 3b6563b..503982b 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt @@ -204,6 +204,8 @@ class SettingsActivity : AppCompatActivity() { } private fun doFlashing(usbManager: UsbManager, device: UsbDevice) { + dialog?.dismiss() + dialog = null showProgressDialog("Firmware Flashen", "Initialiseren...", STYLE_HORIZONTAL) flasher = EspFlasher(this) @@ -338,8 +340,8 @@ class SettingsActivity : AppCompatActivity() { try { setupCdcControl(connection, device) - for (attempt in 1..3) { - Thread.sleep(500) + for (attempt in 1..5) { + Thread.sleep(1000L) flushReadBuffer(connection, epIn) @@ -392,7 +394,7 @@ class SettingsActivity : AppCompatActivity() { Log.w(TAG, "Attempt $attempt: no KEY response, retrying") } - return KeyResult.Error("Geen sleutel gevonden na 3 pogingen") + return KeyResult.Error("Geen sleutel gevonden na 5 pogingen") } catch (e: Exception) { Log.e(TAG, "Key exchange error", e) return KeyResult.Error("Fout: ${e.message}") From 1d17251341fcfdd5072a30f33e7c6240a33c547b Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 26 Jul 2026 17:20:49 +0200 Subject: [PATCH 3/4] Add retry hint to key exchange errors and cancel button to flash dialog 1. Append 'probeer het opnieuw' to key exchange error toasts. 2. Add 'Annuleren' button to flash progress dialog that calls flasher.cancel() and cleanupAction(). --- .../src/main/java/com/eddy/atomrelay/SettingsActivity.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt index 503982b..44aa4f1 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt @@ -169,11 +169,11 @@ class SettingsActivity : AppCompatActivity() { finish() } is KeyResult.Error -> { - Toast.makeText(this, result.message, Toast.LENGTH_LONG).show() + Toast.makeText(this, "${result.message} — probeer het opnieuw", Toast.LENGTH_LONG).show() cleanupAction() } null -> { - Toast.makeText(this, "Sleutel niet gevonden", Toast.LENGTH_SHORT).show() + Toast.makeText(this, "Sleutel niet gevonden — probeer het opnieuw", Toast.LENGTH_SHORT).show() cleanupAction() } } @@ -287,6 +287,10 @@ class SettingsActivity : AppCompatActivity() { progressDialog = AlertDialog.Builder(this) .setTitle(title) .setView(view) + .setNegativeButton("Annuleren") { _, _ -> + flasher?.cancel() + cleanupAction() + } .setCancelable(false) .show() } From d21a560d0c9890ea6235dd0da64614625a9bf884 Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 26 Jul 2026 17:23:55 +0200 Subject: [PATCH 4/4] Shorten key exchange error toast text --- .../app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt index 44aa4f1..5172b92 100644 --- a/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt +++ b/android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt @@ -169,11 +169,11 @@ class SettingsActivity : AppCompatActivity() { finish() } is KeyResult.Error -> { - Toast.makeText(this, "${result.message} — probeer het opnieuw", Toast.LENGTH_LONG).show() + Toast.makeText(this, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_LONG).show() cleanupAction() } null -> { - Toast.makeText(this, "Sleutel niet gevonden — probeer het opnieuw", Toast.LENGTH_SHORT).show() + Toast.makeText(this, "Sleutel uitwisselen mislukt - probeer opnieuw", Toast.LENGTH_SHORT).show() cleanupAction() } }