docs(calls): refresh android.md and telecom migration journal
android.md: minSdk 26 correction + a Calls (Telecom) orientation section. telecom_migration.md: journal entry for the 2026-07-03 internal review — findings applied, the deferred set with rationale, and the on-device validation checklist.
This commit is contained in:
parent
71a71fa56b
commit
28d266964e
2 changed files with 84 additions and 1 deletions
|
|
@ -10,7 +10,7 @@
|
|||
## Config
|
||||
|
||||
- [`capacitor.config.ts`](../../capacitor.config.ts) — `appId: chat.vojo.app`, `webDir: dist`
|
||||
- `android/` — generated Android Studio project, `targetSdkVersion 36`, `compileSdkVersion 36`, `minSdkVersion 24`
|
||||
- `android/` — generated Android Studio project, `targetSdkVersion 36`, `compileSdkVersion 36`, `minSdkVersion 26` (raised from 24 for the self-managed Telecom call backend, whose `CallsManager` is `@RequiresApi(26)`; Android 7.x ≈ 0% in 2026)
|
||||
|
||||
## Build scripts
|
||||
|
||||
|
|
@ -242,3 +242,47 @@ Sygnal config or until a real-world report justifies the parser complexity.
|
|||
3. `adb connect <ip>:<connect-port>`
|
||||
|
||||
The pair port and the connect port are different — don't mix them up.
|
||||
|
||||
## Calls (Telecom)
|
||||
|
||||
DM calls are the only call surface. Media is **Element Call (LiveKit/WebRTC) in a
|
||||
WebView iframe**, driven by widget-api actions (`JoinCall`/`HangupCall`/`DeviceMute`)
|
||||
via [`CallControl.ts`](../../src/app/plugins/call/CallControl.ts) — there is no
|
||||
native WebRTC. Android adds a **self-managed Telecom** layer beside it for system
|
||||
call presence (GSM interop, audio focus, BT/Auto/Wear, a legitimate `phoneCall`
|
||||
FGS that starts from killed). The full design + work journal is
|
||||
[`docs/plans/telecom_migration.md`](../../docs/plans/telecom_migration.md); this is
|
||||
the orientation map.
|
||||
|
||||
Layers, outermost to innermost:
|
||||
|
||||
- **Signalling** — MatrixRTC `m.rtc.notification` (ring) + call-membership state,
|
||||
over `/sync` when the app is alive. The **ring is emitted by the EC widget** as
|
||||
the first session member (matrix-js-sdk gates it on `oldMemberships.length === 0`),
|
||||
NOT by our code — so a stale own membership from a just-ended call silently drops
|
||||
the outgoing ring; the DM-call entry point waits for it to clear first (see
|
||||
[`useSwitchOrStartDmCall.ts`](../../src/app/hooks/useSwitchOrStartDmCall.ts) and
|
||||
[`useCallEmbed.ts`](../../src/app/hooks/useCallEmbed.ts) `createCallEmbed`).
|
||||
- **Background/killed delivery** — Synapse → Sygnal → FCM. Sygnal's V1-flatten
|
||||
forwards only **string** `content` fields one level deep (numbers/dicts drop on
|
||||
the killed path), so ring metadata that must survive is string-mirrored.
|
||||
- **Native ring registry** — [`VojoFirebaseMessagingService.java`](../../android/app/src/main/java/chat/vojo/app/VojoFirebaseMessagingService.java):
|
||||
`ringRegistry` + tombstones (`registryLock`), the single Telecom-ring slot
|
||||
`telecomRingEventId` + Doze-proof wakelock/`postDelayed` ceiling (`telecomLock`),
|
||||
expiry alarms. Renders CallStyle + full-screen [`IncomingCallActivity`](../../android/app/src/main/java/chat/vojo/app/IncomingCallActivity.java)
|
||||
and starts a `phoneCall` FGS ([`CallForegroundService`](../../android/app/src/main/java/chat/vojo/app/CallForegroundService.java)).
|
||||
**Lock discipline:** `telecomLock` and the Telecom-session start are held together
|
||||
across the whole ring start; `registryLock` never nests under `telecomLock`/Binder.
|
||||
- **Telecom session** — [`VojoCallsManager.kt`](../../android/app/src/main/java/chat/vojo/app/VojoCallsManager.kt),
|
||||
the only Kotlin file. One live self-managed session, serialized by an
|
||||
`AtomicLong sessionGen` + `lock`; `isBusy()`/`currentRoomId` is the authoritative
|
||||
"slot taken?" (not `controlScope != null`, which is briefly null while forming).
|
||||
- **JS lifecycle hooks** — `useTelecomConnectionSync` / `useAndroidCallForegroundSync`
|
||||
key the Telecom session + FGS to the widget's JoinCall (`useCallJoined`), with an
|
||||
embed-lifecycle backstop; `useIncomingRtcNotifications` owns the ring atom/registry;
|
||||
`useCallerAutoHangup` tears a call down on peer-leave / no-answer.
|
||||
- **Answer provenance** — MainActivity is exported and Capacitor fires
|
||||
`pushNotificationActionPerformed` on any `google.message_id` intent, so every
|
||||
Answer PendingIntent carries a per-install secret
|
||||
([`CallActionToken`](../../android/app/src/main/java/chat/vojo/app/CallActionToken.java))
|
||||
the JS consumer verifies before auto-joining — blocks a forged hot-mic join.
|
||||
|
|
|
|||
|
|
@ -512,3 +512,42 @@ Telecom — чисто Android-нативный слой; web/Electron/iOS ег
|
|||
(c) F15 same-room eviction по parent `callSessionId` (упирается в тот же Sygnal-starvation — нужен string-mirror полей в
|
||||
cleartext-ринге); (d) decline-id из send-response вместо скрейпа таймлайна. **Все RED требуют прогона на Samsung
|
||||
(Doze, answer-from-killed, logout-во-время-звонка, 2 устройства).**
|
||||
- **2026-07-03 — Внутреннее multi-agent ревью (архитектура/QA/бэкенд/стиль + отдельный разбор glare-гонки, 77 агентов,
|
||||
40 подтв. находок) + фиксы. Корень репортнутого юзером бага найден и закрыт.** tsc + eslint --max-warnings 0 + prettier +
|
||||
полный `:app:assembleDebug` — зелёное; на железе НЕ гонялось.
|
||||
- **Корень «перезвон после сорвавшегося glare-звонка не проходит» (F1, blocker):** ring-событие шлёт ТОЛЬКО EC-виджет
|
||||
и ТОЛЬКО как первый член сессии (matrix-js-sdk `MatrixRTCSession`: notify лишь при `oldMemberships.length===0`, и
|
||||
`pendingNotificationToSend` безвозвратно гасится при `memberships.length>0`). Своё stale-membership после
|
||||
glare+cancel держалось до delayed-leave и (а) уводило `createCallEmbed` в `join_existing` intent (ring не взводится),
|
||||
(б) заваливало SDK-гейт. **Фикс:** `createCallEmbed` считает `ongoing` для DM peer-scoped (своё-текущее-устройство не
|
||||
в счёт); `useSwitchOrStartDmCall` перед исходящим startCall ждёт (bounded, fail-open) исчезновения своего membership
|
||||
(`waitOwnMembershipGone`); zombie-ветка same-room даёт joined-виджету короткий `waitLeave` ДО dispose, чтобы leave
|
||||
реально ушёл (F2). Хост-сайд собственный ring (defense-in-depth для неотвечающего виджета на не-MSC4140 HS) —
|
||||
сознательно НЕ делаем (риск дублей/санитайза), задокументировано ниже.
|
||||
- **Прочий glare/приёмная сторона:** F4 — suppress входящего ринга больше не срабатывает по stale own-device
|
||||
membership (только live-embed-здесь ИЛИ membership на ДРУГОМ устройстве); F5 — `useCallJoined` ресетит `joined` на
|
||||
смену embed A→B (не только на `undefined`); F6 — ответ (`incoming`) всегда `join_existing` intent (не рингует
|
||||
«обратно»); F8 — healthy-no-op по user-scoped пиру (своё 2-е устройство не пир); F19 — no-answer-таймер
|
||||
перевзводится на реально отправленный свой ring; F3 (нативно) — same-room join помечает Telecom-сессию answered
|
||||
(ceiling/suppress не рвёт живой звонок); F14 — старт ring-сессии целиком под `telecomLock` (нет orphan-слота);
|
||||
F11 — провал Telecom на killed-пути деградирует в notification-only, не сносит видимый ринг.
|
||||
- **Безопасность (F15) + M9-добор (F22):** каждый Answer-PendingIntent несёт per-install секрет
|
||||
(`CallActionToken`, приватные prefs, переживает смерть процесса); JS-консюмер (`usePushNotifications`) верифицирует
|
||||
его перед авто-джойном (fail-closed на mismatch, fail-open на ошибку плагина), MainActivity гейтит over-lock тем же.
|
||||
Трамплин-ресивер НЕ используем (бан notification-trampoline на Android 12+) — MainActivity сам зовёт нативный
|
||||
`answerCall` на shade-Answer (закрывает cold-answer окно M9). F17 — onResume-бэкстоп over-lock ждёт settle-окно
|
||||
после входа (не рвёт экран в turnScreenOn-transient).
|
||||
- **Мелочь:** F12 (suppress шлёт closeWebRing), F20 (SW self-dismiss сверяет eventId), F24 (нативный ring-экран
|
||||
таймаутит по реальному lifetime), F25 (`IncomingRing.data` → ConcurrentHashMap), F26 (Telecom-disconnect чистит
|
||||
атом при мёртвом виджете), F30 (mute-mirror по last-sent, explicit-state), F31 (decline-тумбстон на MAX lifetime),
|
||||
F32 (строки FGS-нотификации/канала через PushStrings en/ru), F33 (`dlog`-гейт в VojoCallsManager), F34 (мёртвый
|
||||
`telecomCall.setActive`/события удалены), F37/F41 (именованные/шаренные константы), F42/F43/F44 (dead i18n-ключ,
|
||||
isAndroidPlatform-гейт, unused import). docs/ai/android.md обновлён (minSdk 26 + секция «Calls (Telecom)»).
|
||||
- **СОЗНАТЕЛЬНО ОТЛОЖЕНО (риск регрессии / нужен девайс / крупный рефактор — не код-фиксим наспех):**
|
||||
F1(c) хост-сайд ring для неотвечающего виджета; **F9** ceiling для notification-only ринга под Doze (нужен
|
||||
per-ring wakelock/слот-промоушен — риск утечки); **F16** split-brain при cross-room supersede живого ринга
|
||||
(нужна аккуратная cross-lock нотификация координатора); **F28** Telecom-сессия для foreground-ринга; **F18**
|
||||
декомпозиция VFMS (2499 строк — высокий риск); **F35/F36** чистка stale API-24-комментов и DAWN-палитра в ресурсы;
|
||||
**F38** OverLockCallScreen inline-styles → css.ts (риск визуальной регрессии); **F39/F40** nit-рефакторы.
|
||||
Всё glare-критичное на железе (2 аккаунта Samsung): перекрёстный звонок → cancel → перезвон в окне 8s; decline→перезвон;
|
||||
hangup одного при живом втором → перезвон; answer-from-killed под lock с токеном.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue