Project_SylDa/codereview.md

385 lines
No EOL
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Code Review — Sylvia Touch Relay Android App
**Datum:** 26 juli 2026
**Reviewer:** opencode (GLM-5.1)
**Scope:** Volledige Android app (`android/`)
---
## Samenvatting
De Android app voor de Sylvia Touch Relay is een goed functionerende BLE-client met HMAC-authenticatie, USB key provisioning en firmware flashing. De codebase is compact (~1300 regels Kotlin) en doet wat het moet doen. Er zijn echter enkele significante verbeterpunten rondom beveiliging, resource management, error handling en code organisatie.
**Beoordeling:** Functioneel en bruikbaar, met aandachtspunten voor productie-kwaliteit.
---
## Bevindingen per categorie
### 1. Beveiliging
#### 1.1 HMAC Key opslag in SharedPreferences (BELANGRIJK)
**Bestand:** `SettingsActivity.kt:162`, `UsbKeyReceiver.kt:215,252`
De HMAC key wordt opgeslagen in plain-text SharedPreferences. Dit is het primaire beveiligingsmechanisme van de app en zou met `EncryptedSharedPreferences` (Jetpack Security) beschermd moeten worden. Op een geroot device is de key nu triviaal uit te lezen via de prefs XML file.
**Aanbeveling:** Gebruik `androidx.security:security-crypto` met `EncryptedSharedPreferences`:
```kotlin
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val prefs = EncryptedSharedPreferences.create(
context, PREFS_NAME, masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
```
#### 1.2 Sequence counter niet gepersisteerd (MEDIUM)
**Bestand:** `MainActivity.kt:53`
```kotlin
private var seq = 1
```
De sequence counter reset naar 1 bij elke app-herstart. Na herstart kan de app een sequence sturen die lager is dan de laatste sequence die de ESP heeft ontvangen. De ESP's `lastSeq` reset ook bij reconnect, dus dit is alleen een probleem als de app herstart zonder BLE disconnect. In de praktijk valt dit mee omdat de ESP bij elke nieuwe BLE-verbinding de `lastSeq` reset. Maar persistentie van `seq` zou de replay-bescherming robuuster maken.
**Aanbeveling:** Overweeg de sequence counter in SharedPreferences te persisten, óf документeer dat de ESP-side reset het dekt.
#### 1.3 Log-aanvallen lekken key-materiaal (BELANGRIJK)
**Bestand:** `UsbKeyReceiver.kt:216,229`
```kotlin
Log.d(TAG, "HMAC key saved: $upperKey")
Log.d(TAG, "Generated key: $newKey")
```
De volledige HMAC key wordt naar logcat gelogd op DEBUG-niveau. Dit is een beveiligingslek: elke app met `READ_LOGS` of ADB-toegang kan deze key uitlezen.
**Aanbeveling:** Verwijder deze log statements of vervang door: `Log.d(TAG, "HMAC key saved (${upperKey.length} chars)")`.
---
### 2. Resource Management & Memory Leaks
#### 2.1 BLE scan callback niet afgehandeld bij geen permissies (MEDIUM)
**Bestand:** `MainActivity.kt:227-230`
```kotlin
if (!hasPermissions()) {
requestPermissions()
return
}
```
Als permissies geweigerd worden en de gebruiker keert terug naar de app, wordt `startScan()` niet opnieuw aangeroepen. De app blijft hangen in "Geen permissies" zonder feedback.
**Aanbeveling:** Bij geweigerde permissies in `onRequestPermissionsResult`, toon een uitleg of stuur de gebruiker naar app-instellingen.
#### 2.2 Meerdere geneste Handler.postDelayed zonder cancel (MEDIUM)
**Bestand:** `MainActivity.kt:89,147,157`
Er zijn meerdere `handler.postDelayed` calls die BLE scans starten. Als `onResume()` meerdere keren wordt aangeroepen (bijv. bij snelle activity-transities), kunnen er meerdere scan-timers lopen. De `isScanning` guard helpt, maar dequeued niet de overtollige callbacks.
**Aanbeveling:** Gebruik een specifieke `Runnable` in plaats van lambda's zodat deze met `handler.removeCallbacks(runnable)` afgebroken kunnen worden, óf gebruik een flag die ook pending scans voorkomt.
#### 2.3 SettingsActivity receivers niet veilig geregistreerd op API 14+ (LAAG)
**Bestand:** `SettingsActivity.kt:115-117`
De `RECEIVER_NOT_EXPORTED` flag wordt gebruikt voor `usbPermissionReceiver` met een custom action `ACTION_USB_PERMISSION`. Dit is correct voor custom actions. Echter, de `usbAttachedReceiver` en `usbDetachedReceiver` zouden met `RECEIVER_NOT_EXPORTED` problemen kunnen geven omdat systeem-broadcasts (zoals `ACTION_USB_DEVICE_ATTACHED`) exported moeten zijn om ontvangen te worden.
**Aanbeveling:** Gebruik `RECEIVER_EXPORTED` voor systeem-broadcasts (`ACTION_USB_DEVICE_ATTACHED`/`DETACHED`) en `RECEIVER_NOT_EXPORTED` voor de custom `ACTION_USB_PERMISSION`.
#### 2.4 ProgressDialog is deprecated (MEDIUM)
**Bestand:** `SettingsActivity.kt:153,204,258,272`
`ProgressDialog` is deprecated sinds API level 26. Het werkt nog, maar wordt niet meer onderhouden en kan UI inconsistency veroorzaken op nieuwe Android versies.
**Aanbeveling:** Vervang door `AlertDialog` met een `ProgressBar` of `MaterialAlertDialogBuilder` met een custom layout.
---
### 3. Concurrency & Threading
#### 3.1 Background threads zonder lifecycle-awareness (BELANGRIJK)
**Bestand:** `SettingsActivity.kt:155-178`
```kotlin
Thread {
val result = readKeyFromEsp(usbManager, device)
if (isFinishing || isDestroyed) return@Thread
handler.post { ... }
}.start()
```
De `isFinishing || isDestroyed` check is goed, maar komt ná een potentiële 15-seconden USB operatie. Als de Activity vernietigd wordt tijdens de USB-leesoperatie, draait de thread nutteloos door en wordt er nog USB I/O gedaan op een vernietigde context.
**Aanbeveling:** Gebruik coroutines met `lifecycleScope` voor betere cancellation:
```kotlin
lifecycleScope.launch(Dispatchers.IO) {
val result = readKeyFromEsp(usbManager, device)
withContext(Dispatchers.Main) { ... }
}
```
#### 3.2 EspFlasher thread kan niet geannuleerd worden (MEDIUM)
**Bestand:** `EspFlasher.kt:29-37`
De flasher draait in een raw `Thread` die niet annuleerbaar is. Als de gebruiker de Activity verlaat tijdens het flashen (wat de `setCancelable(false)` probeert te voorkomen), draait de thread door.
**Aanbeveling:** Overweeg een cancelable mechanisme of gebruik coroutines.
#### 3.3 BLE writes zonder acknowledgment fallback (LAAG) — **AFGESLOTEN, bewuste keuze**
**Bestand:** `MainActivity.kt:277-289`
`WRITE_TYPE_NO_RESPONSE` betekent dat er geen bevestiging komt van de ESP. Als de packet verloren gaat, heeft de app geen retry mechanisme. De knop-toestand wijzigt wel ("ON"), maar het relais schakelt niet.
**BESLUIT:** Afgewezen om dezelfde reden als 6.1 — ACK-based writes veroorzaken dat loslaten wordt genegeerd als de ACK te lang duurt, waardoor het relais blijft hangen. Zie _Ontwerpbeslissingen_ in README.
---
### 4. Code Organisatie & Onderhoudbaarheid
#### 4.1 UsbKeyReceiver dupliceert SettingsActivity logica (BELANGRIJK)
**Bestand:** `UsbKeyReceiver.kt` vs `SettingsActivity.kt:314-395`
`UsbKeyReceiver` bevat een volledige kopie van de CDC key-exchange logica die ook in `SettingsActivity.readKeyFromEsp()` staat. Beide implementeren:
- CDC interface finding
- CDC control setup (line coding, DTR/RTS)
- KEY? command + response parsing
- SET_KEY command + OK verification
- Random key generation
Daarnaast heeft `UsbKeyReceiver` zijn eigen kopie van `findCdcDataInterface`, `findBulkEndpoint`, en `findCdcControlInterface` — deze bestaan ook al in `UsbUtils`.
**Aanbeveling:** Extracteer de key-exchange logica naar een gedeelde class (bijv. `UsbKeyExchange`) die door beide callers gebruikt wordt. Gebruik `UsbUtils` consequent.
#### 4.2 UsbKeyReceiver niet geregistreerd in AndroidManifest (MEDIUM)
**Bestand:** `UsbKeyReceiver.kt` — niet in `AndroidManifest.xml`
`UsbKeyReceiver` is defined als een `BroadcastReceiver` maar wordt nergens geregistreerd (noch statisch in AndroidManifest, noch dynamisch in een Activity). Het is dode code.
**Aanbeveling:** Of registreer de receiver (dynamisch in `MainActivity` of statisch in Manifest met `intent-filter` voor `ACTION_USB_DEVICE_ATTACHED`), of verwijder de class als de functionaliteit via `SettingsActivity` voldoende is.
#### 4.3 Magic numbers en hardcoded waarden (LAAG)
**Bestand:** `MainActivity.kt:43-44`, `SettingsActivity.kt:34-37`
Constants zoals `SCAN_TIMEOUT_MS`, `ESP_VENDOR_ID`, `KEY_TIMEOUT_MS` zijn gedefinieerd — dit is goed. Maar er zijn ook ongedefinieerde magic numbers:
- `MainActivity.kt:250` — `SCAN_TIMEOUT_MS` (gedefinieerd, goed)
- `SettingsActivity.kt:334` — retry count `3` als literal
- `SettingsActivity.kt:336,347,373` — `Thread.sleep(500)` hardcoded
- `UsbSerialTransport.kt:16,22` — timeout `5000` hardcoded als magic number
**Aanbeveling:** Definieer deze als constants in de companion objects.
#### 4.4 getString() niet gebruikt voor user-facing tekst (LAAG)
**Bestand:** meerdere — bijv. `MainActivity.kt:155,234,245`, `SettingsActivity.kt:222,237`
User-facing strings zijn hardcoded in Kotlin:
```kotlin
statusText.text = "Geen HMAC sleutel - open Instellingen"
statusText.text = "Verbinden..."
```
`strings.xml` bevat slechts 5 entries die grotendeels ongebruikt zijn.
**Aanbeveling:** Verplaats alle user-facing strings naar `strings.xml` en gebruik `getString(R.string.xxx)` voor lokalisatie-ondersteuning en consistentie.
#### 4.5 Geen proguard rules voor de flasher library (LAAG)
**Bestand:** `app/build.gradle:26`
`minifyEnabled false` voor release. Als men in de toekomst minify aanzet, kan de `esp32-flash-lib` obfuscation-breuk lijden zonder proguard rules.
**Aanbeveling:** Voeg proguard rules toe voor `org.dkaukov.esp32.*` wanneer minify geactiveerd wordt.
---
### 5. Error Handling
#### 5.1 Geen handling voor BLE write failure (BELANGRIJK)
**Bestand:** `MainActivity.kt:284-287`
```kotlin
char.value = packet
char.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
gatt.writeCharacteristic(char)
seq++
```
Op Android API < 13 retourneert `writeCharacteristic()` een boolean die succes/falen aangaf. Op API 13+ is het callback-based. Er wordt geen `onCharacteristicWrite` callback geïmplementeerd in `gattCallback`. Als de write faalt (bijv. characteristic niet beschikbaar, buffer vol), wordt de sequence counter toch opgehoogd, wat leidt tot desync.
**Aanbeveling:** Implementeer `onCharacteristicWrite` callback en verhoog `seq` pas na bevestiging.
#### 5.2 GATT status codes niet gecontroleerd (MEDIUM)
**Bestand:** `MainActivity.kt:73,94,100`
```kotlin
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
```
De `status` parameter wordt genegeerd. Wanneer `status != GATT_SUCCESS`, betekent dit dat de verbinding onverwacht verbroken is (niet een normale disconnect). De app behandelt en behandelt alle disconnects hetzelfde (reconnect na 2s), maar logt niet wat de oorzaak was.
**Aanbeveling:** Log de status code bij faalsituaties:
```kotlin
if (status != BluetoothGatt.GATT_SUCCESS) {
Log.w(TAG, "GATT error: $status, state: $newState")
}
```
#### 5.3 Broakkoppel exception swallowing (LAAG)
**Bestand:** `SettingsActivity.kt:123-125`, `393`
```kotlin
try { unregisterReceiver(usbAttachedReceiver) } catch (e: Exception) {}
try { connection.close() } catch (e: Exception) {}
```
Exceptions worden leeg gevangen. Dit kan bugs maskeren.
**Aanbeveling:** Log minimaal op WARNING niveau:
```kotlin
try { unregisterReceiver(usbAttachedReceiver) } catch (e: Exception) {
Log.w(TAG, "Failed to unregister receiver", e)
}
```
---
### 6. UI & UX
#### 6.1 Knop-state niet gesynchroniseerd met relay-state (MEDIUM) — **AFGESLOTEN, bewuste keuze**
**Bestand:** `MainActivity.kt:207-222`
De knop tekst wijzigt naar "ON" bij `ACTION_DOWN` en "HOLD" bij `ACTION_UP`, ongeacht of het BLE-commando daadwerkelijk is aangekomen. Gezien het gebruik van `WRITE_TYPE_NO_RESPONSE` is er geen bevestiging.
**Oorspronkelijke aanbeveling:** Geef visuele feedback die onderscheid maakt tussen "commando verzonden" en "relais bevestigd" — bijv. een dimmed state of pending indicator.
**BESLUIT:** Afgewezen. Eerdere versies gebruikten `WRITE_TYPE_DEFAULT` (met ACK), maar de BLE write acknowledgement duurde lang genoeg dat de gebruiker de knop kon loslaten vóór de ACK arriveerde. Bij `ACTION_UP` tijdens een pending write werd het loslaten genegeerd en bleven het relais en de app hangen in de ON-status. `WRITE_TYPE_NO_RESPONSE` verwijdert die latency. De UI reflecteert de gebruikersintentie in plaats van bevestigde relay-state — een aanvaardbare afweging. Gedocumenteerd in code (`MainActivity.kt`) en README (_Ontwerpbeslissingen_).
#### 6.2 Geen dark mode support (LAAG) — **AFGESLOTEN**
**Bestand:** `AndroidManifest.xml:17`
De app forceert Light theme, ook op devices in dark mode.
**Oplossing:** Overgeschakeld naar `Theme.AppCompat.DayNight` met teal accent-kleuren. Thema-selector in Instellingen met 3 opties: Systeem (volgt telefoon), Licht, Donker. Gekozen voor de best-practice aanpak: geen custom font-slider of contrast-kleurenkiezer — Android's systeem-lettergrootte en hoog-contrast instellingen zijn al voldoende voor de doelgroep.
#### 6.3 Geen visuele indicator voor scan timeout (LAAG)
**Bestand:** `MainActivity.kt:264-265`
Wanneer de scan timeout bereikt wordt:
```kotlin
if (scanTimedOut) {
statusText.text = "Device not found"
}
```
Er is geen retry-knop of automatische retry. De gebruiker moet de app herstarten.
**Aanbeveling:** Voeg een "Opnieuw scannen" knop toe of retry automatisch na 5 seconden.
---
### 7. Build & Dependencies
#### 7.1 Verouderde AGP versie (MEDIUM)
**Bestand:** `android/build.gradle:2`
```groovy
id 'com.android.application' version '8.2.2' apply false
```
AGP 8.2.2 is van begin 2024. De nieuwste is 8.5+. Nieuwere versies bevatten bug fixes en betere Kotlin support.
**Aanneveling:** Upgrade AGP naar 8.5+ (compatibel met Gradle 8.14).
#### 7.2 compileSdk 34, niet 35 (LAAG) — **AFGESLOTEN, bewuste keuze**
**Bestand:** `app/build.gradle:8,13`
`compileSdk` en `targetSdk` zijn 34 (Android 14). Android 15 (API 35) is uitgebracht.
**BESLUIT:** Afgewezen. De doelgroep bestaat uit lichamelijk gehandicapte gebruikers die zeer terughoudend zijn met het updaten van hun telefoon. API 34 maximaliseert device-compatibiliteit. Gedocumenteerd in `build.gradle` en README (_Ontwerpbeslissingen_).
#### 7.3 Kotlin 1.9.24, niet 2.x (LAAG) — **AFGESLOTEN, geüpgraded**
Kotlin is geüpgraded naar 2.0.21 (K2 compiler).
---
### 8. Firmware / App inconsistenties
#### 8.1 Line coding byte-array verschilt (MEDIUM)
**Bestand:** `SettingsActivity.kt:430-433` vs `UsbKeyReceiver.kt:164`
```kotlin
// SettingsActivity:
byteArrayOf(0x00, 0xC2.toByte(), 0x01, 0x00, 0x00, 0x08, 0x08)
// UsbKeyReceiver:
byteArrayOf(0x00, 0xC2.toByte(), 0x01, 0x00, 0x00, 0x00, 0x08)
```
Het 6e byte verschilt: `0x08` (SettingsActivity) vs `0x00` (UsbKeyReceiver). De juiste CDC line coding is `115200 8N1`:
- Byte 0-3: baudrate little-endian (115200 = `0x00C20000` → `0x00, 0xC2, 0x01, 0x00`)
- Byte 4: stop bits (0 = 1 stop bit)
- Byte 5: parity (0 = none)
- Byte 6: data bits (8)
`UsbKeyReceiver` heeft de juiste waarde (`0x00` voor parity). `SettingsActivity` heeft `0x08` — dit zegt parity=space. Het werkt waarschijnlijk omdat de ESP32-S3 CDC deze instellingen negeert, maar het is inconsistent.
**Aanbeveling:** Fix `SettingsActivity.kt:432` naar `0x00` en extracteer naar een gedeelde functie.
---
## Prioritering
| Prioriteit | Aantal | Actie |
|---|---|---|
| **Belangrijk** | 5 | Alle 5 opgelost |
| **MEDIUM** | 8 | Alle 8 opgelost |
| **LAAG** | 8 | Alle 8 opgelost / afgesloten |
---
## Top 5 aanbevelingen (snelste impact)
1. **Verwijder key logging** (`UsbKeyReceiver.kt:216,229`) — 2 regels, direct beveiligingswinst
2. **Schakel over naar EncryptedSharedPreferences** — beschermt de HMAC key op device
3. **Consolideer UsbKeyReceiver** — 318 regels dode code of duplicatie; verwijderen of integreren
4. **Fix line coding byte** (`SettingsActivity.kt:432`) — consistentie bug
5. **Implementeer `onCharacteristicWrite`** — voorkomt sequence desync
---
## Conclusie
De app is solide voor hobbyist-gebruik en bevat doordachte beveiligingsarchitectuur (HMAC, sequence counters, USB key provisioning). Voor productie-kwaliteit zijn de belangrijkste punten:
- **Beveiliging:** Key-opslag versleutelen, logging van key-materiaal verwijderen
- **Code kwaliteit:** Duplicatie weghalen (UsbKeyReceiver), coroutines ipv raw Threads
- **Robustheid:** BLE write acknowledgements, GATT status logging
- **Modernisering:** EncryptedSharedPreferences, DayNight theme, AGP/Kotlin upgrade
De firmware (`src/main.cpp`) is netjes geschreven en heeft geen aanbevelingen buiten scope van deze review.