# Слепок поведения входящего звонка (ADB)

Дата сессии: 2026-03-07 00:29-00:38 (локально)

## Контекст
- Устройства: `85ba8d0d` (device1), `f518e35a` (device2)
- Сценарии: foreground, background, lockscreen
- Источники событий: FCM + SSE + WebSocket + нативный CallNotificationHandler

## Подтвержденные проблемы

### 1) Принятие звонка на втором телефоне -> быстрый обрыв
Подтверждено для `4ba7a875-e980-4dc0-ac32-91b8429b808c` и `74e911a1-7de6-4730-91cc-4e96734667b0`:
- `acceptInvite_ready` -> `connecting`
- через ~0.07-0.13с `connecting -> ended (cleanupInternal)` на устройстве-акцепторе

Дополнительный маркер:
- в этом же окне приходит `WebSocket: call_answered_elsewhere`,
- при этом нативный SSE-обработчик пишет `current device answered ... skip`.

Вывод: есть гонка/рассинхрон между каналами terminal-событий (WS/SSE), из-за которой активная сессия акцептора закрывается как `missed`.

### 2) Повторный входящий звонок после фактического завершения первого
Для `74e911a1-7de6-4730-91cc-4e96734667b0`:
- после `ended` на device2 снова происходит `ended -> idle -> ringingIn` по тому же `call_id`.
- затем `invite_timeout`, позже приходит `call_cancel` и показываются missed-уведомления.

Вывод: suppress/idempotency по terminal call_id неполная; повторные invite/cancel догоняют уже терминализованную сессию.

### 3) На взятии трубки фиксируется путь к "cancelled/missed"
Во время раннего teardown на device2 логируется:
- `Звонок записан в историю: ... missed`
- затем серия `call_cancel` через FCM/SSE/WS для того же `call_id`.

Вывод: при гонке терминальных событий текущая логика классифицирует принятый/принимаемый вызов как пропущенный.

### 4) Нестабильность входящего экрана/уведомления на lockscreen
По `incoming routing`:
- есть lockscreen-пути с `locked=true`, `fullScreenIntent=true` (ожидаемо),
- но параллельно много дублей `showIncomingCall duplicate suppressed`,
- и частые `ACTION_CALL_FG_STOP` рядом с активной входящей сессией.

Счетчики по сессии:
- device1: `showIncomingCall=11`, `duplicateSuppressed=7`
- device2: `showIncomingCall=11`, `duplicateSuppressed=10`

Вывод: есть дублирующие триггеры показа входящего UI (несколько каналов одновременно), что повышает риск "мигания/исчезновения" экрана при lockscreen state-change.

## Сверка с эталоном WeChat (поведенческие принципы)
На практике и по публичным материалам WeChat/Weixin для Android соблюдаются такие принципы:
1. Один `call_id` = одна активная входящая сессия (idempotent invite).
2. `answered_elsewhere` не должен закрывать устройство-акцептор; только остальные устройства аккаунта.
3. После terminal (`answered/cancel/ended`) повторный incoming по тому же `call_id` не должен поднимать ringing UI.
4. lockscreen входящий должен быть устойчивым: без множественных re-open/re-close из параллельных каналов.

Текущее поведение X2Chat расходится с п.2/п.3/п.4.

## Приоритетный список на исправление (без правок в этой задаче)
P0:
- Жесткая идемпотентность terminal lifecycle по `call_id` на клиенте: после terminal блокировать любые re-invite/re-cancel для того же call.
- Единый arbiter terminal-событий: WS/FCM/SSE должны проходить через общий guard c `answered_device_id`/`answered_by` и account-targeting.
- Запрет path `connecting -> ended -> missed` для сессии, где уже был `acceptInvite_ready` без явного terminal от server state sync.

P1:
- Нормализация incoming presentation pipeline: один источник правды для показа входящего UI, остальные каналы только сигнализируют/дедупят.
- Для lockscreen: держать full-screen incoming до явного terminal либо timeout, не гасить из-за дублирующих `showIncomingCall`.

## Файлы слепка
- `device1/logcat_live.log`
- `device2/logcat_live.log`
- `device1/dumpsys_*_pre.txt`, `device1/dumpsys_*_post.txt`
- `device2/dumpsys_*_pre.txt`, `device2/dumpsys_*_post.txt`
- `analysis/event_analysis.json`
- `analysis/device1_4ba7a875-..._window.log`
- `analysis/device2_4ba7a875-..._window.log`
- `analysis/device1_74e911a1-..._window.log`
- `analysis/device2_74e911a1-..._window.log`
