# Full-Direction D-Pad Audit Methodology (2026-06-14)

## When to Use

Audit every directional path when debugging "stuck focus", invisible focus targets, or cross-zone navigation failures in Android TV Compose apps. Always trace ALL four directions — not just the problematic one.

## Steps

### 1. Discover all focus-managed files
```bash
find app/ -name "*.kt" | xargs grep -l "focusProperties\|focusable\|onKeyEvent\|onPreviewKeyEvent\|FocusRequester\|focusGroup\|focusRestorer" | sort
```

### 2. Read core infrastructure
- `TvFocusController` — zone state machine
- `TvFocusState` — Saver, scroll positions
- `TvFocusZone` — all defined zones
- Focus modifiers file — blockContentFocusWhenInactive, tvFocusableRow
- `FocusLinks` — may be dead code; check for callers

### 3. Build a directional matrix for every screen
```
| Element | UP | DOWN | LEFT | RIGHT |
|---|---|---|---|---|
| Card[0,0] | HeroPlay | Card[1,0] | Sidebar | Card[0,1] |
| Card[1,0] | Card[0,0] | Card[2,0] | Sidebar | Card[1,1] |
```

Mark: `Name` = explicit FocusRequester, `Cancel` = hard stop, `—` = 2D spatial search fallback.

### 4. Check ALL three dispatch layers per element
1. `focusProperties.{dir}` — focus system (resolves BEFORE key events). This IS the primary mechanism.
2. `onPreviewKeyEvent` — fires root→leaf. May duplicate focusProperties (redundant but harmless).
3. `onKeyEvent` — fires leaf→root. Only use for Back/Enter overrides.

### 5. Verify zone transitions
Every D-pad move that crosses zones must check:
- Does `contentFocusAllowed()` change?
- Is `blockContentFocusWhenInactive` in the path?
- Does `onFocusChanged` close overlays?
- Is there a 1-frame gap where focus lands on a hidden element?

### 6. Verify focus restoration
- `rememberSaveable` with custom Saver for process-death survival
- Per-zone `lastFocusedKeyPerZone` + `lastScrollPerZone`
- `requestFocusAfterFrames()` with frame-wait + retry

## Key Pitfalls (Discovered in 2026-06-14 audit)

### PITFALL 1: Parent focusProperties on non-focusable ancestor are dead code
```kotlin
// WRONG — Row is NOT focusable, these focusProperties never apply to children
Row(modifier = Modifier.focusProperties { up = sidebarRequester }) {
    Button(onClick = {}, modifier = Modifier.focusProperties { down = target }) { }
}

// CORRECT — each focusable child must have its own directional targets
Row {
    Button(onClick = {}, modifier = Modifier.focusProperties {
        up = sidebarRequester    // ← on the FOCUSABLE child
        down = target
    }) { }
}
```

### PITFALL 2: Dual UP/DOWN handling (redundant but harmless)
When a card has BOTH `focusProperties.up = target` AND `onPreviewKeyEvent` that calls `links.up?.requestFocus()`, the focus system already moved focus before the key event fires. The onPreviewKeyEvent handler is dead code for UP/DOWN:
```kotlin
.focusProperties {
    links.up?.let { up = it }  // ← focus system handles this
}
.onPreviewKeyEvent {
    if (DirectionUp) {
        links.up?.requestFocus()  // ← redundant, already moved
        return@onPreviewKeyEvent links.up != null
    }
}
```
**Not a bug** — `requestFocus()` on already-focused node is a no-op. But it's misleading.

### PITFALL 3: FocusRequester lists destroyed on data change
```kotlin
// WRONG — new List every time, focus lost
val requesters = remember(data.size) { List(data.size) { FocusRequester() } }

// CORRECT — preserve existing
val requesters: List<FocusRequester> = remember { mutableListOf() }
remember(data.size) {
    val list = requesters as MutableList
    while (list.size < data.size) list.add(FocusRequester())
    while (list.size > data.size) list.removeLastOrNull()
}
```

### PITFALL 4: Grid column index math breaks on Adaptive layouts
When using `LazyVerticalGrid` with `GridCells.Adaptive(width)`, the actual column count depends on screen width. If focus wiring uses a static `gridColumns` value, it may mismatch:
```kotlin
val gridColumns = gridMetrics.posterColumns.coerceAtLeast(1)  // clamp to avoid div-by-zero
```

### PITFALL 5: Settings `onPreviewKeyEvent` must consume Right
```kotlin
// WRONG — returns false, event continues to 2D search, focus can split
onPreviewKeyEvent { if (DirectionRight) { onMoveToDetail(); false } }

// CORRECT — returns true, event consumed
onPreviewKeyEvent { if (DirectionRight) { onMoveToDetail(); true } }
```

## Research Sources
- CosmicLearn: https://www.cosmiclearn.com/googletv/dpad-navigation.php
- Composables: https://composables.com — focusProperties/focusRestorer API
- Slack #compose-android: LazyRow focus management discussions
- Prahalad Sharma (Medium): Android TV D-Pad navigation guide
