# Recurring Bug Patterns — Full-Stack Audit (June 2026)

Updated: 2026-06-02 — added C13 (JIT OOM from composable params) and C14 (Stremio string-list serialization).

## Critical (crash, data loss, security)

### C13 — ART JIT compiler OOM from Composable parameter bloat

**Symptom:** Total device reboot, not just app crash. Logcat shows `Compiler allocated ~5MB to compile void ...Composable(...27 params...)` near `Sending signal. PID: N SIG: 9`.

**Root cause:** `@Composable` function with >20 parameters in a `LazyRow`/`LazyColumn` — each instance triggers JIT compilation allocating ~5MB. Many instances (cards in rows, rows in columns) multiply to hundreds of MB.

**Found in:** `PremiumContentCard` — 27 params, 175+ instances across 25 catalog rows.

**Fix pattern:** Group display parameters into `@Stable` data class:
```kotlin
// BEFORE: 27 params
@Composable fun Card(poster: String?, name: String, type: String?, year: String?, ...)

// AFTER: 13 params  
@Stable data class CardDisplay(val poster: String?, val name: String, val type: String?, ...)
@Composable fun Card(display: CardDisplay, ...)
```

**Detection:** `grep -c ','` on `@Composable fun` signatures — flag any with >15 parameters. Cross-reference with `items()` / `LazyRow` / `LazyColumn` call sites to estimate instance count.

### C14 — Stremio addon API returns string instead of string array

**Symptom:** `JsonDecodingException: Expected start of the array '[', but had '"'` at `path $.meta.<field>`.

**Root cause:** `MetaItem` fields typed `List<String>?` but Stremio addons return single strings like `"director":"Frank Darabont"`.

**Fix pattern:** Custom `StringOrListSerializer` that handles both `JsonArray` and `JsonPrimitive`. Apply to all `List<String>?` fields receiving Stremio JSON.

See `references/stremio-string-list-serializer.md` for full implementation.

## Earlier Patterns

### C1 — `withTimeout` kills parallel addon results (StremioAddonClientImpl)

**Symptom:** One slow addon (e.g. something on ElfHosted) causes ALL parallel addon results to be discarded.

**Root cause:** `withTimeout()` throws `TimeoutCancellationException` which propagates up through `coroutineScope{}` → cancels sibling coroutines per structured concurrency.

**Fix:** `withTimeout()` → `withTimeoutOrNull()` per Kotlin docs.

### C2 — CancellationException swallowed (StremioAddonClientImpl)

**Symptom:** Addon results silently dropped when parent coroutine cancelled.

**Root cause:** Generic `catch (e: Exception)` swallows `CancellationException` — must be re-thrown.

**Fix:**
```kotlin
} catch (e: CancellationException) {
    throw e  // re-throw before generic Exception catch
} catch (e: Exception) {
    ...
}
```

### C3 — AddonRepository multi-flow race (RMW on _addons, _systemAddons, _userAddons)

**Symptom:** Interleaved updates from parallel `refreshAllSystemManifests()` calls corrupt addon state.

**Fix:** `Mutex` guard on all 3-flow update methods.

### C4 — PlayerViewModel double-initialize without cancellation

**Symptom:** Two parallel `initialize()` calls create overlapping playback preparation.

**Fix:** `initJob?.cancel()` at top of `initialize()`.

### C5 — PlayerManager TOCTOU race on `_player`

**Symptom:** `release()` called between `_player` null-check and usage.

**Fix:** All methods access through synchronized `player`/`existingPlayer` properties.

### C6 — PlaybackOrchestrator ignores cancellation

**Symptom:** Long-running stream preparation can't be cancelled.

**Fix:** `yield()` at each iteration boundary.

### C7 — SourceFilterChips focus trap (Home screen)

**Symptom:** D-pad DOWN from filter chips goes nowhere — can't reach content rows.

**Fix:** `focusProperties { down = firstRowRequester }` on last chip.

### C8 — PreferencesManager plaintext secrets in DataStore

**Symptom:** API keys stored in XML DataStore, readable by any app with storage permissions.

**Fix:** `EncryptedSharedPreferences` migration (AES-256-GCM, KeyStore-backed).

### C9 — PlayerManager 100 MB buffer on low-RAM devices

**Symptom:** OOM during playback on devices with <512 MB RAM.

**Fix:** Gate on `ActivityManager.memoryClass` — 50 MB if <256 MB, else 100 MB.

### C10 — SourceRanker Regex re-instantiation

**Symptom:** `Regex(pattern)` created 400× per ranking pass.

**Fix:** `ConcurrentHashMap<String, Regex>` compile-once cache.

### C11 — PremiumContentCard dead code (`settledFocus`)

**Symptom:** Parameter computed but never consumed.

**Fix:** Removed unused parameter.

### C12 — FallbackEngine boolean logic error

**Symptom:** Free sources filtered out even when user wants free + debrid.

**Root cause:** `isDebridCached` is `Boolean?` (null = unknown). `null == false` is false.

**Fix:** `(if (preferDebrid) rs.stream.isDebridCached == true else rs.stream.isDebridCached != true)`
