# Code Review Fixes Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Fix all Critical, Important, and selected Minor issues from the code review to make the Android app production-ready. **Architecture:** Incremental fixes to existing files. New `UsbUtils.kt` utility object to eliminate code duplication. No architectural changes — just bug fixes, lifecycle hardening, and cleanup. **Tech Stack:** Kotlin, Android SDK API 24-34, USB Host API, BLE API, esp32-flash-lib --- ## File Structure | File | Action | Responsibility | |---|---|---| | `UsbUtils.kt` | **Create** | Shared USB utility functions (findCdcDataInterface, findBulkEndpoint) | | `MainActivity.kt` | **Modify** | Fix receiver flag, handler cleanup, onPause scan stop, boolean state | | `SettingsActivity.kt` | **Modify** | Thread lifecycle check, use UsbUtils | | `EspFlasher.kt` | **Modify** | try/finally connection cleanup, progress mapping, use UsbUtils | | `UsbSerialTransport.kt` | **Modify** | Throw IOException on read error | | `AndroidManifest.xml` | **Modify** | allowBackup=false | | `activity_settings.xml` | **Modify** | Fix download-modus text | --- ### Task 1: Create UsbUtils utility object **Files:** - Create: `android/app/src/main/java/com/eddy/atomrelay/UsbUtils.kt` - [ ] **Step 1: Create UsbUtils.kt** ```kotlin package com.eddy.atomrelay import android.hardware.usb.UsbConstants import android.hardware.usb.UsbDevice import android.hardware.usb.UsbEndpoint import android.hardware.usb.UsbInterface object UsbUtils { private const val CDC_DATA_INTERFACE_CLASS = 0x0A fun findCdcDataInterface(device: UsbDevice): UsbInterface? { for (i in 0 until device.interfaceCount) { val intf = device.getInterface(i) if (intf.interfaceClass == CDC_DATA_INTERFACE_CLASS) { return intf } } return null } fun findBulkEndpoint(intf: UsbInterface, direction: Int): UsbEndpoint? { for (i in 0 until intf.endpointCount) { val ep = intf.getEndpoint(i) if (ep.type == UsbConstants.USB_ENDPOINT_XFER_BULK && ep.direction == direction) { return ep } } return null } } ``` - [ ] **Step 2: Build to verify compilation** Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add android/app/src/main/java/com/eddy/atomrelay/UsbUtils.kt git commit -m "Add UsbUtils utility object for shared USB functions" ``` --- ### Task 2: Fix UsbSerialTransport.read() — throw IOException on error **Files:** - Modify: `android/app/src/main/java/com/eddy/atomrelay/UsbSerialTransport.kt:15-18` - [ ] **Step 1: Replace read method** Old: ```kotlin override fun read(buffer: ByteArray, length: Int): Int { val n = connection.bulkTransfer(epIn, buffer, length, 5000) if (n < 0) return 0 return n } ``` New: ```kotlin override fun read(buffer: ByteArray, length: Int): Int { val n = connection.bulkTransfer(epIn, buffer, length, 5000) if (n < 0) throw IOException("USB read failed") return n } ``` - [ ] **Step 2: Build to verify** Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug` Expected: BUILD SUCCESSFUL - [ ] **Step 3: Commit** ```bash git add android/app/src/main/java/com/eddy/atomrelay/UsbSerialTransport.kt git commit -m "Fix UsbSerialTransport.read() to throw IOException on error" ``` --- ### Task 3: Fix EspFlasher — connection leak + progress mapping + UsbUtils **Files:** - Modify: `android/app/src/main/java/com/eddy/atomrelay/EspFlasher.kt` - [ ] **Step 1: Replace doFlash with try/finally and progress mapping** Replace the entire `doFlash` method and the `progressCallback` with: ```kotlin private fun doFlash(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) { Thread.sleep(500) val connection = usbManager.openDevice(device) ?: throw IOException("Kan USB-apparaat niet openen") try { val intf = UsbUtils.findCdcDataInterface(device) ?: throw IOException("Geen CDC data interface gevonden") if (!connection.claimInterface(intf, true)) { throw IOException("Kan USB interface niet claimen") } val epOut = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_OUT) ?: throw IOException("Geen USB OUT endpoint gevonden") val epIn = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_IN) ?: throw IOException("Geen USB IN endpoint gevonden") postProgress(callback, 0, "Verbinden met ESP bootloader...") val transport = UsbSerialTransport(connection, epOut, epIn) val assets = context.assets val bootloader = assets.open("firmware/bootloader.bin").readBytes() val partitions = assets.open("firmware/partitions.bin").readBytes() 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 data class FlashStep(val name: String, val address: Int, val data: ByteArray, val baseProgress: Int, val range: Int, val compress: Boolean) val steps = listOf( FlashStep("Bootloader", 0x0, bootloader, 30, 10, false), FlashStep("Partitie tabel", 0x8000, partitions, 40, 10, false), FlashStep("Boot app0", 0xe000, bootApp0, 50, 10, false), FlashStep("Firmware", 0x10000, firmware, 60, 35, true) ) postProgress(callback, 5, "Sync met bootloader...") val api = EspFlasherApi.connect(transport) .withCallBack(object : ProgressCallback { override fun onProgress(pct: Float) { Log.d(TAG, "Lib progress: $pct%") } override fun onInfo(value: String) { Log.d(TAG, "Info: $value") } }) postProgress(callback, 10, "Chip detecteren...") val detected = api.chipDetect() val chipId = detected.getChipId() Log.i(TAG, "Detected chip: $chipId") postProgress(callback, 15, "Chip: ${chipId.readableName}") postProgress(callback, 20, "Stub laden...") val stub = detected.loadStub() postProgress(callback, 25, "Flash wissen...") stub.eraseFlash() for (step in steps) { 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)...") val writeStub = if (step.compress) stub.withCompression(true) else stub.withCompression(false) writeStub.writeFlash(step.address, step.data) val written = steps.takeWhile { it !== step }.sumOf { it.data.size } + step.data.size val overallProgress = step.baseProgress + (step.range * step.data.size / totalSize).coerceAtLeast(1) + step.range postProgress(callback, minOf(step.baseProgress + step.range, 95), "${step.name} geschreven") } postProgress(callback, 95, "ESP resetten...") stub.reset() postProgress(callback, 100, "Firmware geflashed!") handler.post { callback.onComplete() } } finally { connection.close() } } ``` - [ ] **Step 2: Remove the now-duplicated private methods** Delete `findCdcDataInterface` and `findBulkEndpoint` from EspFlasher.kt (they are now in UsbUtils). - [ ] **Step 3: Build to verify** Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug` Expected: BUILD SUCCESSFUL - [ ] **Step 4: Commit** ```bash git add android/app/src/main/java/com/eddy/atomrelay/EspFlasher.kt git commit -m "Fix EspFlasher: connection leak, progress mapping, use UsbUtils" ``` --- ### Task 4: Fix SettingsActivity — UsbUtils + thread lifecycle **Files:** - Modify: `android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt` - [ ] **Step 1: Replace findCdcDataInterface and findBulkEndpoint calls with UsbUtils** In `readKeyFromEsp`, replace: ```kotlin val intf = findCdcDataInterface(device) ``` with: ```kotlin val intf = UsbUtils.findCdcDataInterface(device) ``` Replace: ```kotlin val epOut = findBulkEndpoint(intf, UsbConstants.USB_DIR_OUT) ``` with: ```kotlin val epOut = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_OUT) ``` Replace: ```kotlin val epIn = findBulkEndpoint(intf, UsbConstants.USB_DIR_IN) ``` with: ```kotlin val epIn = UsbUtils.findBulkEndpoint(intf, UsbConstants.USB_DIR_IN) ``` - [ ] **Step 2: Delete the duplicated private methods** Delete `findCdcDataInterface` and `findBulkEndpoint` from SettingsActivity.kt. - [ ] **Step 3: Add lifecycle check in thread callback** In `doKeyExchange`, replace the `Thread` block: ```kotlin Thread { val result = readKeyFromEsp(usbManager, device) handler.post { ``` with: ```kotlin Thread { val result = readKeyFromEsp(usbManager, device) if (isFinishing || isDestroyed) return@Thread handler.post { ``` - [ ] **Step 4: Build to verify** Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug` Expected: BUILD SUCCESSFUL - [ ] **Step 5: Commit** ```bash git add android/app/src/main/java/com/eddy/atomrelay/SettingsActivity.kt git commit -m "Fix SettingsActivity: use UsbUtils, add lifecycle guard" ``` --- ### Task 5: Fix MainActivity — receiver flag, handler cleanup, onPause, boolean state **Files:** - Modify: `android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt` - [ ] **Step 1: Fix registerReceiver flag** In `onCreate`, replace: ```kotlin registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) ``` with: ```kotlin registerReceiver(bondReceiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED), Context.RECEIVER_NOT_EXPORTED) ``` - [ ] **Step 2: Add SCAN_TIMEOUT_MS constant** In companion object, add: ```kotlin private const val SCAN_TIMEOUT_MS = 10000L ``` - [ ] **Step 3: Add boolean for scan timeout state** Add field: ```kotlin private var scanTimedOut = false ``` - [ ] **Step 4: Fix stopScan to use boolean** Replace `stopScan`: ```kotlin @SuppressLint("MissingPermission") private fun stopScan() { if (!isScanning) return isScanning = false val adapter = BluetoothAdapter.getDefaultAdapter() adapter?.bluetoothLeScanner?.stopScan(scanCallback) if (statusText.text == "Scanning...") { statusText.text = "Device not found" } } ``` with: ```kotlin @SuppressLint("MissingPermission") private fun stopScan() { if (!isScanning) return isScanning = false val adapter = BluetoothAdapter.getDefaultAdapter() adapter?.bluetoothLeScanner?.stopScan(scanCallback) if (scanTimedOut) { statusText.text = "Device not found" } } ``` - [ ] **Step 5: Fix startScan to use constant and boolean** In `startScan`, replace: ```kotlin handler.postDelayed({ if (isScanning) stopScan() }, 10000) ``` with: ```kotlin scanTimedOut = true handler.postDelayed({ if (isScanning) stopScan() }, SCAN_TIMEOUT_MS) ``` And add `scanTimedOut = false` after `isScanning = true`: ```kotlin isScanning = true scanTimedOut = false statusText.text = "Scanning..." ``` - [ ] **Step 6: Add onPause** Add after `onResume`: ```kotlin override fun onPause() { super.onPause() stopScan() } ``` - [ ] **Step 7: Fix onDestroy — add handler cleanup** Replace `onDestroy`: ```kotlin @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onDestroy() { super.onDestroy() stopScan() bluetoothGatt?.close() unregisterReceiver(bondReceiver) } ``` with: ```kotlin override fun onDestroy() { super.onDestroy() handler.removeCallbacksAndMessages(null) stopScan() bluetoothGatt?.close() try { unregisterReceiver(bondReceiver) } catch (e: Exception) {} } ``` - [ ] **Step 8: Build to verify** Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug` Expected: BUILD SUCCESSFUL - [ ] **Step 9: Commit** ```bash git add android/app/src/main/java/com/eddy/atomrelay/MainActivity.kt git commit -m "Fix MainActivity: receiver flag, handler cleanup, onPause, boolean state" ``` --- ### Task 6: Fix AndroidManifest — allowBackup=false **Files:** - Modify: `android/app/src/main/AndroidManifest.xml:14` - [ ] **Step 1: Change allowBackup** Replace: ```xml android:allowBackup="true" ``` with: ```xml android:allowBackup="false" ``` - [ ] **Step 2: Commit** ```bash git add android/app/src/main/AndroidManifest.xml git commit -m "Disable allowBackup to protect HMAC key from extraction" ``` --- ### Task 7: Fix activity_settings.xml — download-modus text **Files:** - Modify: `android/app/src/main/res/layout/activity_settings.xml:31` - [ ] **Step 1: Fix text** Replace: ```xml android:text="Sluit de ESP aan op USB en zet de ESP in download-modus (houd reset 2s in tot de groene LED knippert). Tap dan op 'Firmware Flashen'." ``` with: ```xml android:text="Zet de ESP in download-modus (houd reset 2s in tot de groene LED aangaat) en tik op 'Firmware Flashen'." ``` - [ ] **Step 2: Commit** ```bash git add android/app/src/main/res/layout/activity_settings.xml git commit -m "Fix download-modus instruction text in settings layout" ``` --- ### Task 8: Final build and install - [ ] **Step 1: Build** Run: `JAVA_HOME=/home/linuxbrew/.linuxbrew/Cellar/openjdk@21/21.0.12/libexec ./gradlew assembleDebug` Expected: BUILD SUCCESSFUL - [ ] **Step 2: Install** Run: `adb install -r android/app/build/outputs/apk/debug/app-debug.apk` Expected: Success - [ ] **Step 3: Commit plan document** ```bash git add docs/superpowers/plans/2026-07-26-code-review-fixes.md git commit -m "Add code review fixes implementation plan" ```