Android UI Inspection (Koder Konsul-first)

mandatory

Em sessão com Android conectado via ADB, preferir Koder Konsul (HTTP API em 127.0.0.1:9876 após `adb forward tcp:9876 tcp:9876`) a screenshots crus. ~10× menos tokens, bounds/IDs/estado exatos do OS. Cair pra screencap só em casos visuais (ícone, cor, canvas, FLAG_SECURE).

Em qualquer sessão com dispositivo Android conectado via ADB, preferir o Koder Konsul a screenshots crus para inspecionar a UI. Aplicável a qualquer agente de IA (Claude Code, Codex, Gemini CLI, etc.) que opere no monorepo Koder.

Por quê

Vision custa ~10× mais tokens que JSON da accessibility tree. Eye dá:

  • Bounds exatos ([x, y, w, h]) — screenshot força adivinhar coordenadas
  • View IDs (dev.koder.konsul:id/start_btn) — invisíveis em pixel
  • Estado direto do OS: enabled, clickable, checked, focused
  • Texto exato de TextViewButtonEditText/Switch — sem erros de OCR
  • Diff estrutural entre snapshots — duas screenshots podem parecer idênticas e diferir em estado
  • Long-poll de eventos (/events?wait=10000) — sem polling de screencap

Custo por inspeção:

Ação Tokens
Screenshot enviado ao chat ≈1500 (image)
GET /screen/text 50–500 (text)
GET /screen/buttons 100–400 (text)
GET /screen/diff 50–800 (text, ≈0 quando nada mudou)

10 screenshots ≈ 15 000 tokens vs. 10 queries Eye ≈ 2 000 tokens.

Setup obrigatório por sessão

Fazer antes de declarar Eye indisponível:

adb -s <serial> forward tcp:9876 tcp:9876
curl -s localhost:9876/healthz   # endpoint é /healthz, NÃO /health
# Esperado: {"ok":true,"version":"...","accessibility":true,"media_projection":false}

Se /healthz responder, Eye está pronto.

Se conexão for recusada após forward, o servidor HTTP do Eye está parado — é o estado default. Eye é desenhado pra exigir consent explícito antes do servidor ouvir em :9876.

Eye ≥ v0.4 (post-eye#036): use o broadcast — um único comando, sem Activity launch:

adb -s <serial> shell am broadcast \
  -a dev.koder.konsul.action.START_SERVER \
  -p dev.koder.konsul
sleep 1
curl -s localhost:9876/healthz
# => {"ok":true,"version":"...","accessibility":true,"media_projection":false}

O receiver é gated por android.permission.DUMP — disponível pra ADB shell por default, indisponível pra apps regulares. Sem auth bypass.

NUNCA use monkey para abrir/resumir um app (iris#057). O comando monkey -p <pkg> -c android.intent.category.LAUNCHER 1 religa accelerometer_rotation do device como efeito colateral (reproduzido no SM-S948B, 2026-06-08 e 2026-06-20). Para abrir/resumir um app use sempre am start -n <pkg>/<activity> (ou cmd activity start) — inócuo à rotação, assim como uiautomator dump/screencap. Se alguma ferramenta precisar mexer em rotação, salve e restaure ao fim: R=$(adb shell settings get system accelerometer_rotation); …; adb shell settings put system accelerometer_rotation $R.

Eye ≤ v0.3 (releases antigos sem o receiver): fallback é tap manual via uiautomator dump + input tap:

# 1. Launch
adb -s <serial> shell am start -n dev.koder.konsul/.MainActivity
sleep 2

# 2. Discover the "Iniciar Servidor" button bounds (dump + rm no mesmo comando — §Higiene de arquivo)
BOUNDS=$(adb -s <serial> shell 'uiautomator dump /sdcard/ui.xml >/dev/null && cat /sdcard/ui.xml; rm -f /sdcard/ui.xml' | grep -oE 'text="Iniciar Servidor"[^>]*bounds="\[[0-9,]+\]\[[0-9,]+\]"' | grep -oE '\[[0-9,]+\]\[[0-9,]+\]')
# BOUNDS looks like [395,1131][686,1191]
# Compute center: x = (395+686)/2 = 540, y = (1131+1191)/2 = 1161

# 3. Tap center
adb -s <serial> shell input tap 540 1161

# 4. Verify
sleep 2
curl -s localhost:9876/healthz

Detect which path applies (broadcast vs tap) por probe:

# Try the broadcast first — fails silent on old Eye
adb -s <serial> shell am broadcast -a dev.koder.konsul.action.START_SERVER -p dev.koder.konsul 2>&1 | head -1
# If "Broadcast completed: result=0", it worked. Else fall back to the tap recipe.

Se Eye não está habilitado (One UI Auto-blocker desativa apps recém-instalados; dumpsys package dev.koder.konsul | grep enabled= retorna enabled=0):

adb -s <serial> shell pm enable dev.koder.konsul
adb -s <serial> shell pm grant dev.koder.konsul android.permission.POST_NOTIFICATIONS

Se accessibility service não está bound (/healthz retorna "accessibility":false):

EXISTING=$(adb -s <serial> shell settings get secure enabled_accessibility_services | tr -d '\r')
adb -s <serial> shell settings put secure enabled_accessibility_services \
  "${EXISTING}:dev.koder.konsul/dev.koder.konsul.KonsulAccessibilityService"
adb -s <serial> shell settings put secure accessibility_enabled 1

Classe correta = KonsulAccessibilityService (não EyeAccessibilityService, resíduo do rename Eye→Iris — iris#053). A receita antiga bindava o componente errado.

Higiene de estado do device — snapshot → restore (OBRIGATÓRIO)

O fluxo de acesso ADB muta settings do device do owner (serviço de acessibilidade acima; e empiricamente a Rotação Automática accelerometer_rotation aparece habilitada após o acesso — owner reportou 2026-06-10). Regra: o device do owner deve voltar exatamente ao estado anterior ao acesso. Antes de tocar no device, fazer snapshot dos settings que o fluxo altera; ao terminar (mesmo em erro — use trap), restaurar.

# --- SNAPSHOT (antes de qualquer ação no device) ---
S=<serial>
ROT0=$(adb -s $S shell settings get system accelerometer_rotation | tr -d '\r')
TMO0=$(adb -s $S shell settings get system screen_off_timeout | tr -d '\r')
A11Y0=$(adb -s $S shell settings get secure enabled_accessibility_services | tr -d '\r')
A11YEN0=$(adb -s $S shell settings get secure accessibility_enabled | tr -d '\r')

# --- RESTORE (ao final — armar com trap pra cobrir falhas) ---
restore_device() {
  [ "$ROT0" = null ] || adb -s $S shell settings put system accelerometer_rotation "$ROT0"
  [ "$TMO0" = null ] || adb -s $S shell settings put system screen_off_timeout "$TMO0"
  adb -s $S shell settings put secure enabled_accessibility_services "$A11Y0"
  adb -s $S shell settings put secure accessibility_enabled "$A11YEN0"
  # Higiene de ARQUIVO (R5 de scratch-location.kmd): remover qualquer screencap/
  # dump de IA vazado pra raiz do /sdcard — scratch NUNCA fica no device do owner.
  adb -s $S shell 'rm -f /sdcard