Files
utun2/tools/chatgui-android/doc/ARCHITECTURE.md
T

91 KiB
Raw Blame History

chatgui-android — архитектура

Состав и сборка

Android-клиент использует общее ядро из lib/ и src/: ETCP с UDP-линками и STCP-линками поверх TCP, NCD, группы/BGP, ETCP-router, чат, DM и media-сервисы. Отдельной Android-реализации chat_sync/merkle_sync и отдельного lite-instance нет. STCP передаёт ETCP-пакеты; его потоковое шифрование — AES-CTR, пакетное ETCP — AES-CCM. Контракты транспорта описаны в ../../../src/transport_layer/stcp.h и etcp.h.

libutun_lite/utun_sources.cmake собирает lib/*.c, LMDB и рекурсивно src/*.c, исключая utun.c, каталоги lwip_orig/uip/transport_layer/BBR и файлы отдельного голосового стека. Это источник актуального состава, а не список «только TCP». Файлы Android-обёртки перечислены в _cfg_src; старая копия invite_link_c.c туда не входит.

NDK-сборка в app/src/main/cpp/CMakeLists.txt создаёт OBJECT-библиотеку utun_core и итоговую libutun.so: ядро, JNI, UDP-лог, отдельная статическая utun_voice, OpenSSL Crypto, Opus, SpeexDSP, ONNX Runtime. JNI распределён между android_jni_bridge.c, android_stream_jni.c, android_call_video_jni.c и android_call_output_jni.c. Голосовой стек собирается по src/call/voice_sources.cmake.

Параметры приложения определяет app/build.gradle.kts: minSdk 26, compile/targetSdk 36, NDK 29.0.14206865, JVM 17, ABI arm64-v8a/x86_64. UI — Kotlin, Jetpack Compose и Material3; CameraX отвечает за захват фото/видео, MediaCodec — за live video, AudioRecord/AudioTrack — за аудио. Сборка использует Gradle 8.13, AGP 8.13.2, Kotlin/Compose compiler 2.2.20 и CameraX 1.7.0-rc01; минимальный Android приложения остаётся API 26.

CameraRecordScreen объединяет режимы «Фото / Видео» для канала и PM. Адресат, поколение PM и канальный ответ фиксируются до открытия камеры. Фото запрашивает только CAMERA; RECORD_AUDIO требуется в режиме видео. Привязывается Preview и один выбранный use case (ImageCapture либо VideoCapture). После снимка доступны просмотр и пересъёмка; переключение режима разрешено в состоянии готовности. Экран удаляет отменённые cache-файлы и игнорирует запоздавшие callbacks. При отправке владение файлом переходит ViewModel, а после принятия JPEG/PNG — attachment_send.

Общие с трансляцией иконки открывают меню камеры, разрешения, кодека и битрейта. Видеозапись использует H.264/H.265 и AAC в MP4; доступные кодеки и разрешения проверяются через Recorder.getVideoCapabilities(cameraInfo, mime) для выбранной камеры. Разрешение и битрейт независимы. Перед готовностью/отправкой MediaExtractor проверяет, что фактический MIME видеодорожки совпадает с выбранным кодеком. В фото отдельное меню JPEG quality (10..100); ImageCapture.setJpegQuality() задаёт сжатие при съёмке. Предпросмотр читает полученный JPEG с EXIF и сохраняет артефакты сжатия; масштабирование позволяет рассмотреть их. Декодирование ограничено 4096 по большей стороне, на SM-A525F штатные снимки читаются без уменьшения.

JPG/JPEG/PNG из выбора файла и камеры проходят общий sendPhotoMessage: фактический MIME и размеры с учётом EXIF читаются в IO, затем передаются через JNI. В PM метаданные JPEG/PNG шифруются вместе с остальными метаданными вложений. Превью декодируется с ограничением размера; подпись содержит разрешение и размер файла. Каналы и PM используют PhotoViewerScreen с масштабированием и перемещением.

Владение ядром и потоки

NativeLib.init(dbPath, controlPort) сохраняет config/log callbacks и настраивает voice_recorder. Он не запускает ядро и не открывает control-listener; controlPort в этой ветке только логируется. Затем NativeLib.start(configText) передаёт INI в instance_lite_start().

Один worker pthread создаёт uasync и UTUN_INSTANCE, принудительно задаёт CLIENT_TYPE_MOBILE и запускает utun_core_start() + chat_service_start(). UTUN-сервис включается отдельно через [gui] utun_enabled=1, по умолчанию выключен. Наличие TUN/routing/NAT-кода в сборке само по себе сервис не включает.

До CHAT_EVT_SERVICE_STARTED worker привязывает stream bridge и call_video_port, а при UTUN_WITH_VOICE — call_audio. Успех start означает создание потока либо уже существующий запуск; он не подтверждает готовность сервисов. Готовность проверяется по событию и instance_lite_is_running().

Команды UI обычно копируют аргументы и ставят работу в uasync. При этом C JSON-getters и некоторые настройки читают SQLite/живые структуры синхронно в вызывающем потоке; C utun_bridge_set_member_nick() пишет локальный nickname напрямую. C setter chat_setting также синхронный, тогда как JNI nativeSetChatSetting использует trampoline. Нельзя считать весь bridge автоматически выполняющимся в потоке ядра. Заимствованные inst/ua не удерживают ресурс: frontend должен сериализовать вызовы по ним с stop/restart; обход живых списков требует исключить их параллельное изменение.

JNI получает JNIEnv для native-потока и вызывает Kotlin onEvent(type, ByteArray?). Payload копируется в byte array; его формат зависит от CHAT_EVT_* из src/chat/chat_event.h. Это не универсальный JSON. Отдельный C utun_event_fn не является обработчиком бинарных событий instance_lite. ViewModel переносит обновление UI в свой coroutine context.

Перед stop/restart Kotlin сбрасывает controllers и останавливает внешние media workers. Worker снимает media-привязки, уничтожает instance, снимает standby/UDP-таймеры и уничтожает uasync. instance_lite_stop() ждёт до двух секунд: при timeout отсоединяет поток, поэтому возврат не гарантирует завершённый cleanup. Без опубликованного instance он возвращается сразу. Restart пересоздаёт instance и uasync. Дополнительный опрос is_running в bridge до 5с может увидеть ещё старое работающее ядро; подтверждение нового запуска — SERVICE_STARTED. Общие правила освобождения: ../../../doc/service_lifecycle.md.

Данные и конфигурация

Ядро владеет общей SQLite db_path/chats.db; путь без db_path даёт in-memory БД. Chat использует тот же handle, что и топология. Схему создают общие модули src/routing_layer/topo_node_sqlite.c, src/chat/, src/dm/ и media-подсистемы. Room/DbRepository и отдельного Kotlin SQL-соединения в приложении нет: ChatRepository получает JSON через JNI и разбирает события, ViewModel хранит StateFlow. Имена/форматы таблиц не следует дублировать в Android-коде.

ConfigProvider хранит настройки в DataStore, читает ключи и формирует общий INI через buildConfigText/buildConfigFromServers. Callback-ключи вроде node.name и db.path не являются именами INI-секций. Native config API копирует таблицу callbacks по значению; функции и их окружение остаются у провайдера. Init без config и destroy снимают прежнюю регистрацию; смену провайдера нужно сериализовать с cfg_get_*. Минимальный пример INI:

[global]
my_node_name=Android
# При отсутствии ключей начальный запуск сгенерирует X25519-пару.
db_path=/data/user/0/com.utun.chat/files
db_sync_enabled=1
chatserver_enabled=1
auto_sockets=android

[gui]
utun_enabled=0

[allowed_keys]
allow_all=yes

[ntp]
enabled=no

[debug]
chat=info
socket=info

Секции [server:<имя>] с addr/type/transport/netif строятся из интерфейсов устройства. NetworkChangeMonitor передаёт новый INI в updateSockets: uasync согласует сокеты через auto_socket_reload_config, сохраняя ядро/чат. В auto_sockets=android общий auto_socket_on_network_change() не запускает scan: нужен именно reload из Kotlin. Настройки голоса, рации, stream, standby и хранения передаются в [chat], UDP-лог — в [log_udp] ip/port; полный набор задаёт ConfigProvider. Начальная генерация ключей пытается сохранить их в db_path/keys.conf. instance_lite_regenerate_keys() меняет ключи/node_id на месте, без пересоздания соединений.

Язык интерфейса

AppLanguage.supportedTags читает общий res/xml/locales_config.xml: имена нормализуются в BCP47, пустые/повторные/некорректные записи отклоняются атомарно. Parser закрывается также при ошибке. Успешный каталог кэшируется; провал lazy логируется и не запрещает следующую попытку чтения.

selectedTag() возвращает requested список Android/AppCompat через запятую, пустая строка означает системный выбор. Effective ресурсы получают отдельно через context()/text(), в том числе вне Activity: для этого используется ContextCompat.getContextForLanguage. nativeName() учитывает приоритетный список и именует каждый тег на его языке.

select() вызывается на main после создания Activity, принимает один тег каталога либо пустую строку. true означает уже выбранный/принятый запрос, false — отказ, а не подтверждение отката или завершения смены ресурсов. Выбор хранит Android/AppCompat с autoStoreLocales, без дублирования в DataStore; механизм хранения/пересоздания описан в AppCompatDelegate.setApplicationLocales. UI перечитывает requested состояние после попытки, включая частичный отказ; ошибки чтения и выбора показываются отдельно с возможностью повторить действие.

applied() на main получает effective locale и при каждом сигнале конфигурации/ создании Activity заново запрашивает обновление четырёх владельцев уведомлений: сеть, входящий звонок/рация, текущий звонок и radio overlay. Кэша «этот locale уже применён» нет: владелец может обработать отказ внутри себя или стать активным после прежнего сигнала. Обновляются каналы/тексты; ядро, media и сигналы вызова не перезапускаются. Ошибка одного владельца не прерывает остальные. При отказе чтения конфигурации сигнал логируется и пропускается до следующего вызова.

Каналы, приглашения и DM

Создание канала ставится в uasync; channel_id, переданный старому bridge API создания, игнорируется: ядро генерирует ID. connect_node(address,port,pubkey) остался заглушкой. Для подключения канала/участника используются общие chat_core/topo_group API.

InviteLink.kt получает предпросмотр через NativeLib.decodeInvite() — JNI вызывает общий src/chat/invite_link.c. Ссылка utun://... содержит версию 0x04 и kind: CHANNEL несёт пароль/channel_id/join_key, SERVER — 32-байтовый add_key без полей канала. Далее идут REALITY-параметры и блоки публичного ключа/адресов. В Kotlin-preview SERVER имеет channelId/joinKey=0 и пустой password; add_key остаётся в исходной ссылке. JOIN передаёт исходную ссылку и request ID через потокобезопасный chat_join_port; принятый запрос завершается CONNECT_RESULT с тем же request. Отрицательный возврат submit означает отказ без события. Детали: src/chat/chat_join.h, chat_join_port.h.

Регистрация join_key подтверждается до выдачи ссылки. JOIN добавляет подписанного мембера, Merkle распространяет запись; доступ к группе определяется локальной таблицей участников. JOIN_READY добавления и READY групповой сессии — разные состояния.

LAN-приглашения хранит app-scoped LocalDiscovery; AppEventHandler передаёт ему события напрямую на main, без replay ViewModel. Список устройств 61 заменяется целиком; входящая канальная ссылка 62 только ставится в очередь показа, без JOIN. Кадры 61..63 имеют BE-поля ID/длин, 64 содержит байт активности. Принимаются не более 128 устройств, имена 1..127 UTF-8 байтов без ASCII control, ссылка до 1100 байтов; входящая UI-очередь ограничена 8 приглашениями. Повреждённые кадры, невалидный UTF-8, нулевые/повторные ID устройств не заменяют прежнее состояние.

Одна исходящая LAN-отправка на приложение хранит request и снимок адресата. Событие 63 применяется только к pending с совпавшими request и peer; статусы 0/1/2 означают failed/received/forbidden. ACK подтверждает приём ссылки, не JOIN. Отказ/исключение JNI завершает pending ошибкой. Stop очищает устройства и завершает pending ошибкой, сохраняет уже полученные приглашения; поздний ACK не возобновляет завершённый запрос.

Android MulticastLock следует эффективной активности native discovery, а не видимости Activity. Владелец сохраняется до configure/acquire и при ошибке query/release: следующий ACTIVE/STOP повторяет операцию с тем же lock. В логе указаны точный этап и сохранён ли владелец; на повторном ACTIVE удерживаемый lock не захватывается ещё раз. Постоянный таймер повторов здесь не запускается.

InviteLinkDialog получает candidates/link/pending от владельца генерации. Он показывает QR или текст, запрашивает LAN-снимок и отображает результат последней отправки вместе с её адресатом. Закрытие не отменяет pending-доставку. Ошибка QR логируется без ссылки, текст остаётся доступен; ошибка clipboard показывается в диалоге. Смена ссылки сбрасывает подтверждение копирования.

JoinChannelDialog автоматически передаёт валидную ссылку из текста/QR/clipboard в callback запуска; сетевой результат показывает владелец join. Ошибка clipboard не блокирует ручной ввод, ошибка запуска разрешает повторить ту же ссылку. QR preview владеет пассивной QrScannerSession до вызова start. Она снимает только свои CameraX use cases; ImageProxy закрывается после завершения ML Kit Task, включая отказ регистрации listener. Dispose отменяет доставку результата, но ждёт завершения распознавания перед закрытием scanner. Ошибка SDK cleanup сохраняет владельца независимо от composition и повторяется на main с backoff 250 мс..30 с.

DM-команды используют conv_id и поколение беседы. История приходит событием CHAT_EVT_DM_MESSAGES, список — CHAT_EVT_DM_CONVERSATIONS. Frontend отбрасывает ответы старого поколения/другой беседы. Voice/video принимают target dm:<conv_id>; отдельные dm_* функции принимают conv_id без префикса. Общее устройство DM: ../../../doc/dm_arch.md.

Вложения и media

Канальные text/file/photo используют chat_core_submit_trampoline. reply_to — ровно 32 байта исходного message_id, нули означают отсутствие ответа. request_id связывает публикацию с CHAT_EVT_MSG_SUBMITTED; успешная постановка не означает доставку пиру. Обычный файл, MP4 и JPEG/PNG передают владение временным cache общему worker; он удаляет источник также при отказе подготовки. Дополнительные GIF/BMP/WebP канала остаются у caller до результата асинхронной индексации. Фото здесь не декодируется и не масштабируется.

voice_recorder принимает int16 mono 48kHz, отсоединяет PCM/compressor при stop и передаёт общий attachment_send. Запись короче 500мс по монотонным часам отбрасывается. Готовый MP4 передаётся без транскода; при принятом video-запросе cache-файл принадлежит задаче и удаляется также при ошибке/отмене. То же правило у отдельной DM-отправки файла. При отказе постановки файл остаётся caller. Generation защищает публикацию в DM; reply_to канала и request_id канала/PM фиксируются до начала подготовки. ConversationComposer сохраняет draft, URI и тип вложения через пересоздание Activity; поколение PM входит в сохранённый target. Возврат picker и восстановленный выбор проверяются против текущего адресата. UI владеет cache до передачи задачи ViewModel, отмена копирования удаляет непереданный cache в NonCancellable. Ошибка edit/picker показывается ресурсом интерфейса; ошибка cleanup логируется отдельно. После передачи ViewModel владеет всем пакетом «вложение + текст», продолжает обе части после уничтожения Activity и сохраняет отдельные результаты. Composer восстанавливает token, ревизию текста и ID выбора: подтверждение очищает только свой прежний draft/URI. Reply снимается после сохранения обеих частей. Повторный пакет тому же target до завершения запрещён. SAF export в канале и PM сообщает об отказе запуска picker/записи. Null output stream не считается успешным сохранением; ожидающий путь переживает пересоздание Activity. PM text передаётся JNI как обычные UTF-8 bytes, generation и ненулевой request ID. Успешный enqueue не очищает draft: composer ждёт MSG_SUBMITTED после транзакции истории/outbox. При stale generation/SQL error draft остаётся, отказ очереди завершает ожидание сразу. Тот же MSG_SUBMITTED подтверждает commit PM-файла, фото, видео и голоса; не готовность первого этапа подготовки и не доставку пиру. Ошибка/отмена каждого этапа даёт отрицательный результат.

ChatRepository применяет авторитетные JSON-снимки целиком: null означает отказ, а пустой список — корректный пустой снимок. ViewModel сохраняет прежние каналы, историю и сокеты при ошибке и проверяет владельца/revision после фоновых запросов. SERVICE_STOPPED инвалидирует старый repository до следующего DB_READY.

Звонок и рация используют общий C голосовой стек. Kotlin владеет AudioRecord/AudioTrack, feed/pull workers и маршрутом Android Telecom; DSP и сетевой сигналинг разделены. Вызовы lifecycle DSP выполняются после остановки его внешних feed/pull callers. Контракты PCM/PTS: src/call/call_audio.h, src/radio/radio_audio.h, jni_bridge/android_jni_bridge.h.

CameraRecordScreen сохраняет CameraCaptureResources до захвата и снимает только свои use cases. Cache фото живёт до OnImageSaved/onError, видео — до Finalize; discard/выход UI сначала запрашивает закрытие записи и ждёт writer перед удалением файла. Ошибки Recording.close, unbind, отключения ориентации и удаления независимы и сохраняются в process registry с retry. Поздний callback не восстанавливает UI и не удаляет переданный sender файл. Pause/resume подтверждаются событиями CameraX; разрешения перечитываются при ON_RESUME. VideoFileTracks проверяет MIME/геометрию готового MP4; MediaExtractor остаётся до release ACK, ошибка чтения сохраняется при дополнительной ошибке cleanup. Контракты CameraX: Recording.close, PendingRecording.start.

Локальный VideoPlayerScreen сохраняет пассивный VideoFilePlayer до start. UI отражает результаты MediaPlayer start/pause/seek, ошибка команды завершает playback и оставляет доступным закрытие экрана. Пользовательская пауза сохраняется при пересоздании Surface. TextureView и player держат отдельные leases output; неудавшийся release MediaPlayer сохраняет его ссылки до SDK ACK с retry 250 мс..30 с. Поздний destroy прежнего texture не отключает новый output, callbacks после dispose не запускают playback/навигацию. Metadata читаются в IO через отдельный владелец retriever; ошибка release сохраняет его для повтора и не подменяет исходную ошибку read. Поворот и панели окна возвращаются к прежним значениям при выходе. VideoFilePlayerTest/VideoFileMetadataTest проверяют эти контракты на управляемых SDK-границах; аппаратное декодирование в JVM не выполняется.

TelecomCallManager на main регистрирует self-managed account и connections по call ID; lookup разрешён workers. Поздний/дублирующий Connection не заменяет живого владельца и не возвращается framework как новый активный вызов. Ошибки markDisconnected не прерывают destroy. Неудавшийся destroy сохраняет отдельного retiring owner до ACK; Handler повторяет очистку, явный remove/requireReleased работают также после отказа очереди retry. Этот barrier запрещает новое media/core teardown. Восстановление рации ждёт освобождения сервиса и Telecom. ConnectionService проверяет ненулевые call/peer IDs и ловит ошибки extras/SDK создания; controller получает failure по известному ID. UtunConnection подтверждает маршрут callbacks SDK: request outcome не заменяет фактический endpoint. Последнее желание пользователя сохраняется до завершения запроса, включая позднее подтверждение старого маршрута. После disconnect системный ввод, routes и старые outcomes игнорируются; ошибки callbacks не выходят в SDK. Hold не рекламируется, поскольку приложение его не реализует. Решение о fallback/завершении при Telecom failure принимает controller.

Прямое превью Camera2 передаёт UI, ожидающей команде и каждой camera session отдельный StreamPreviewTarget на общий Surface/SurfaceTexture. retain создаёт новую ссылку, release одной ссылки идемпотентен; идентичность источника проверяется через sameSource. Ошибка listener отменяет предложенную ссылку, queued receiver пропускает released target. Detach/close продолжают освобождение независимо от ошибки доставки команды; поздний update не активирует закрытое превью. Последняя ссылка передаёт физическую очистку process-владельцу: Surface и texture подтверждаются независимо, retry идёт через 250 мс..30 с. При отказе Handler повторный release завершает её явно.

ProximityManager сохраняет прежний sensor listener и его manager при отказе unregister: start ждёт cleanup, Handler повторяет его через 250 мс..30 с, повторный stop работает после отказа таймера. Старая регистрация и повреждённые показания не дают ложного FAR; после lookup failure start повторяет поиск.

CallAudioRouter сохраняет device callback и каждый полученный Bluetooth proxy до успешного unregister/close. stop инвалидирует callbacks и снимки устройств сразу, очищает независимые ресурсы и бросает ошибку при неподтверждённом результате. Собственные communication device/SCO-заявки отмечаются до API и снимаются отдельно; перед передачей в Telecom глобальный маршрут должен быть освобождён. Повтор start сначала завершает прежнюю очистку, сохраняя заранее настроенную Telecom Connection. Отказы удерживаются process-ссылкой; Handler повторяет очистку через 250 мс..30 с. При отказе Handler владелец сохраняется для явного stop/start. Принятый запрос getProfileProxy ожидает callback; поздний proxy после stop также получает владельца и повтор close при ошибке. При активном новом маршруте очищаются лишь старые proxy. Дубликат callback освобождённого/заменённого proxy не делает его текущим снова.

MediaAudioSession.close требует отсутствия потребителей и сразу запрещает новые подписки, outputs и refresh. Hardware и router очищаются независимо; ошибка оставляет close повторяемым и сохраняет executor до подтверждения обеих частей. Пока close не завершён, awaitIdle также отвергает передачу ресурсов звонку.

RadioCallController на main управляет адресным приглашением участника в обычную рацию канала. Это отдельный сигналинг src/radio/radio_call.h, без отдельного аудиозвонка. START резервирует busy до первого события ядра: быстрое повторное нажатие не создаёт второй запрос, а CoreActivity удерживает активность также на этом промежутке. Отказ очереди освобождает резерв, reset отменяет pending. Неготовое ядро сообщает недоступность; занятая попытка или P2P-звонок — занятость.

Событие 65 содержит ровно 27 little-endian байтов: id/group/peer по 8, затем phase/reason/incoming по 1. ID ненулевые; Long хранит все биты uint64, строка канала — десятичный uint64. Проверяются диапазоны и сочетания роли/фазы/причины, а у своей попытки — вся тройка и incoming. Посторонние/устаревшие события не меняют живую попытку и не публикуют результат от старой. При недоступных именах repository UI использует ID, READY по-прежнему отправляется.

Входящий сначала публикует оповещение и отправляет READY. Ответ из уведомления ждёт onStart Activity, разрешение микрофона перепроверяется; callbacks старой попытки не принимают новую. Accept публикует PREPARING/activation, Activity включает обычную рацию и возвращает activated(id, success). Только тогда входящий отправляет ACCEPT либо FAIL и ждёт FINISHED. У исходящего ACCEPTED запрашивает локальную активацию; её успешное завершение сразу снимает карточку. Поздние RINGING/WAIT_DECISION/повторный FINISHED не откатывают исходящий PREPARING. CONTINUE разрешён только своей WAIT_DECISION-попытке и сохраняет id.

Отказ/исключение JNI логируется с action/id/group/peer. Неотправленный READY, ACCEPT/FAIL или CONTINUE завершает локальную попытку; DECLINE/CANCEL очищает UI независимо от очереди. Блокирующая захват трансляция отправляет FAIL и закрывает оповещение. Ошибка CoreActivity не прерывает сигналинг и независимую очистку: CoreActivity сохраняет намерение для следующего sync. Deadline приглашения остаётся в ядре; приложение владеет только отменяемой задержкой 2,5 с удаления ENDED-карточки. Новая попытка/reset отменяет её, callback проверяет сам снимок.

Завершение приглашения не выключает обычную рацию. StateFlow повторяет текущие state/activation для пересозданной Activity; история завершённых вызовов не восстанавливается. RadioCallDialog отображает этот снимок и передаёт действия контроллеру, не управляя deadline/аудио. Результат видимой Activity показывается Toast, в фоне — отдельным уведомлением 1006, без замены уведомлений сервисов. Отказ уведомления/локализации не мешает удалению карточки.

RadioPttHolds на main хранит по одному ручному удержанию BUTTON/OVERLAY/VOLUME. Повтор press того же источника идемпотентен; несколько volume-клавиш агрегирует Activity и отпускает общий VOLUME после последней клавиши. Release возвращает true только при удалении последнего владельца; неизвестное отпускание не гасит чужую передачу. Cancel возвращает отсутствие владельцев также для уже пустого набора: это позволяет отменить хвост передачи после release. Null снимает всех. Агрегатор не управляет VAD, аудио или таймерами; их владелец — RadioAudioEngine.

Ручные talkBegin/talkEnd/cancelTalk в RadioAudioEngine проверяют main-поток до изменения владельцев/таймера. Неактивная рация отказывает с WARN; отсутствие RECORD_AUDIO запрашивает разрешение без сохранения удержания. После выдачи пользователь нажимает снова. Принятый PTT — асинхронный запрос, не подтверждение первого PCM/TX: эти этапы видны отдельно в логах capture/native и transmittingGroup.

Последний talkEnd оставляет 250 мс хвоста; новое нажатие снимает его таймер, cancel последнего владельца снимает хвост сразу. Если postDelayed отказал, ERROR содержит source/group/generation; движок немедленно снимает ручной запрос и ставит согласование capture в io. Сообщение scheduled появляется только при успешной постановке. Когда захват больше не нужен, подписка закрывается и native capture останавливается. При работающем измерителе/автоматике захват сохраняется, а отпускание PTT проходит общей очередью управления/PCM до native radioTalkEnd. Снятие ручного PTT само по себе не выключает автоматическую передачу. Stop/смена группы сбрасывает удержания и прежний хвост; io проверяет generation.

Start/stop рации также вызываются на main. Start отвергает нулевую группу, проверяет размер буфера AudioTrack и выполняет на io сначала очистку прежних ресурсов, затем setup. Даже отказ первоначальной очистки завершает onReady(false) на main. Отложенный результат перепроверяет generation: он не подтверждает и не очищает новый запрос. Ошибка текущего запуска снимает activeGroup после callback, если callback не начал новый start/stop. Это позволяет restoreAfterCall сначала выключить native-членство неудачно восстановленной группы. Исключение пользовательского callback логируется и передаётся main-caller. Если main-очередь отвергла доставку, callback на io не вызывается, новые аудиоресурсы очищаются с ERROR.

Stop снимает логическое участие сразу; его Future завершается после io-очистки либо возвращает первый критический отказ. Ждать Future нужно вне main. Receiver, capture, output lease/AudioTrack и native DSP очищаются независимо. Перед остановкой DSP движок дожидается своих capture/play workers; interruption не пропускает join, флаг восстанавливается после очистки. Ссылка на отказавший receiver/lease/track сохраняется для следующего stop/start. При ошибке detach lease трек ещё принадлежит MediaAudioController и не получает release. Успешно очищенные ссылки сбрасываются; native audio stop остаётся идемпотентной завершающей попыткой после join. Новая группа не заменяет владельцев при неподтверждённой очистке.

Capture сохраняет подписку до native start и отдельно помнит возможное владение encoder. Даже исключение после частичного эффекта API получает rollback; неудачный native capture stop сохраняется для повторной попытки независимо от закрытой PCM-подписки. Подтверждённый capture/audio stop сбрасывает наблюдаемый TX и режим детектора. Отказ автоматического детектора закрывает автоматический захват; ручной PTT или измеритель могут продолжить работу в режиме OFF.

Оба worker сохраняются до Thread.start. Capture принимает ровно 960 PCM frames на канал; старые кадры отбрасываются с границей перед следующим свежим кадром. Ошибка poll, PTT-команды, границы или feed логируется с этапом/группой/кадром, после чего io закрывает захват, сохраняя приём. False от feed означает отказ native-захвата; это не потеря сетевого пакета. Новый пользовательский запрос может повторить захват после устранения причины. Очистка проверяет подписку и generation; rollback старого запуска не снимает PTT нового канала.

Playback использует переданные при запуске track/группу/поколение. Pull допускает 0..1920 стереосэмплов с выравниванием по двум каналам, дополняя короткий результат тишиной. Положительные частичные write передают остаток того же кадра; ноль, отрицательный, слишком большой либо нечётный результат работающего write — ошибка. Ошибка head/pull/write/sleep освобождает свою сессию на io до уведомления main; отказ main-очереди не удерживает ресурсы. Identity трека защищает новую сессию от запоздалой очистки. Interruption при штатном stop и результат разблокированного им write считаются отменой; interruption работающего worker логируется как ошибка.

Screen receiver удерживается до вызова registerReceiver, включая частичный отказ IPC. IllegalArgumentException от unregister подтверждает отсутствие регистрации; прочие ошибки сохраняют владельца. Доставленный после замены receiver проверяет identity и исходное поколение до изменения состояния экрана. Проверка settings ACK имеет собственный отменяемый Runnable: ACK, новая настройка и stop снимают его. Отказ постановки ACK-timeout логируется, захват по-прежнему ждёт подтверждения. Отказ таймера фоновой автоматики закрывает её окно; ручной PTT/измеритель могут сохранить захват в режиме OFF. Логи содержат группу, поколение, этап отказа и join.

Reconfigure инвалидирует прежний settings request до чтения новых настроек. Ошибка чтения/применения/обновления capture логируется с этапом; приём остаётся включённым, capture ждёт повторной перенастройки и нового ACK. Прежний ACK не возобновляет захват после отказа. Частично созданный capture получает rollback.

RadioTalkState хранит отдельный снимок говорящих для активной группы. Включение/ смена канала создаёт новый снимок, выключение устанавливает groupId=0. Событие 43 разбирается строго как 19 little-endian байтов: group/node/stream/on; ID ненулевые, stream=0..65535, on=0/1. Неверное событие возвращает null и логируется ViewModel. START/STOP обновляет только набор node ID своей группы; чужая группа и повтор состояния сохраняют прежний объект. В C radio_talker_on заменяет stream одного узла без STOP старого потока, поэтому stream не является ключом UI-набора. Индикатор речи не зависит от обновления таблицы участников или локального mute; выключенная рация игнорирует события. Эти снимки не управляют ручным PTT.

Live stream bridge владеет одним publisher и независимыми viewers поверх общих stream_publisher/stream_viewer. UI использует session IDs, не inst/ua. Данные идут через pull, а каталог источников — через CHAT_EVT_STREAM_SOURCES. Pull возвращает owned буфер записей с 28-байтовым BE-заголовком; освобождение — u_free. Demand ограничивает батч viewer; GAP/CLOSED требуют сбросить прежний frontend-буфер. Точный формат, лимиты и срок жизни signal: libutun_lite/stream_bridge.h.

Viewer сохраняет пассивные video/DSP owners до их start. GAP очищает прежние записи в StreamReadQueue, сохраняя последующий KEY того же batch. Ошибка reset не теряет старый DSP handle. Video close ждёт worker до 250 мс и подтверждает release decoder; отказ запрещает fallback на другой codec и передачу Surface обратно UI. Audio worker независимо повторяет native stop, video/DSP cleanup, затем закрывает signal и выполняет callbacks освобождения Surface. Успешные операции не повторяются. Незавершённый viewer остаётся в process-реестре; native stop/restart/destroy запрещены до его cleanup. Микшер удерживает единственный AudioTrack до успешного close, включая частичную initialize/play. Новые inputs ждут освобождения прежнего выхода; awaitIdle ограничивает join 250 мс за попытку. Ошибки listener detach сохраняют отдельную неактивную регистрацию до SDK ACK, включая failed attach без lease; поздний routing callback не читает released track. Повтор очистки выполняет media control executor. Viewer UI передаёт физический Surface/texture cleanup идемпотентному StreamPreviewTarget. Прямой CameraPreviewView снимает свои leases при onRelease, не ожидая destroy TextureView; отдельная camera lease сохраняет output до ACK сессии. Поздний destroy возвращает false и не закрывает texture средствами framework. Смена имени источника обновляет плитку без смены reader.

Publisher service сохраняет пассивный capture до start, CPU lock — до acquire, foreground-флаг — до IPC. Capture stop=true подтверждает camera/session callbacks, encoder/input Surface, ориентацию, подписку микрофона и Opus worker. False сохраняет engine у сервиса; ошибки SDK/native DSP повторяются независимо. Таймаут camera close не подменяет ACK. Смена/восстановление режима ждут полного video cleanup; stop отменяет продолжение pending switch. FGS cleanup переживает onDestroy, сохраняет ближайший retry и только после capture ACK отправляет native FIN. Обычная остановка ждёт END до 4 секунд; abort/destroy переходят сразу к освобождению lock/tokens/foreground/stopSelf. Controller остаётся STOPPING до подтверждения сервиса, включая незавершённый старт FGS; native teardown проверяет этот barrier. Null от startForegroundService считается отказом. Старые IDs callbacks/notification не управляют новой публикацией. Malformed TX snapshot сохраняет прошлую статистику; ошибка poll завершает publisher через тот же cleanup.

Фон и диагностика

CallService на main владеет audio/video движками, PARTIAL_WAKE_LOCK, power receiver, foreground-уведомлением 1002 и собственными PendingIntent. Запуск сначала подтверждает phoneCall|microphone, CPU lock и receiver, затем сообщает serviceReady. Ошибка канала в onCreate логируется; onStart повторяет её до захвата ресурсов. Ошибка диагностического чтения канала не отменяет успешный create. Null от startForegroundService считается отказом, а отменённый запрос завершает foreground-handshake без микрофона/CPU lock и останавливается.

Владельцы сохраняются до IPC: частичные acquire/register/promote получают rollback. startAudio проверяет свой ID и CPU lock; повтор не заменяет живые движки. Ошибка audio start/video constructor откатывает сервис. Неудачный camera promote сохраняет последний подтверждённый флаг; caller не меняет capture при false. Notification update/язык и power diagnostics ловят ошибки независимо от media. Токены open/END имеют identity своего звонка, повторно используются при обновлении и отменяются при shutdown. Старый END не завершает другой звонок; после stop уведомление не публикуется. Chronometer использует startedAtElapsedRealtime из controller.

Shutdown независимо пытается остановить video/audio, снять receiver, CPU lock и токены. Успешные операции не повторяются, отказавшие владельцы сохраняются. IllegalArgumentException от unregister подтверждает отсутствие receiver; отказ release после эффекта подтверждается следующим isHeld=false. Foreground и stopSelf(lastStartId) снимаются после успешных stop API обоих движков. Последний owned call ID сохраняется отдельно для serviceLost после локального освобождения. Audio stop снимает active и callbacks, пытается разблокировать capture и ждёт каждый worker до 250 мс за попытку; playback также ждёт свой reporter. Живые потоки сохраняются, main не ждёт device lock занятого worker. Маршрут и AEC откладываются до освобождения lock с проверкой call ID, диагностика владельцев не обращается к SDK устройств. устройства освобождаются после завершения обоих workers. Ошибки release оставляют AudioRecord/CallAudioOutput у движка, включая частичный запуск и отказ после эффекта. Остановка router, удаление communication listener и восстановление собственного AudioManager mode/speaker повторяются независимо; после передачи режима в Telecom fallback его не восстанавливает. Незавершённая очистка бросает ошибку сервису и запрещает новый start. Native audio stop вызывается после подтверждения этих операций, его отказ также допускает повтор. Сервис сначала сохраняет пассивный CallVideoEngine, затем вызывает start: частичный запуск имеет владельца для rollback. Video stop возвращает true только после RX join/освобождения decoder, playout и Surface, SDK-подтверждения закрытия камеры/сессий, освобождения Images/readers/encoder и native close. До этого false сохраняет engine у сервиса; исключения RX/post допускают повтор stop. Отказ RX не прерывает независимую очистку TX, но запрещает закрывать native port. TX хранит ресурсы до подтверждения SDK, повторяет отказы с backoff и лишь затем завершает worker. onConfigureFailed уже подтверждает закрытие сессии: ждать дополнительного onClosed нельзя. Если создание сессии бросило исключение, её выходы сохраняются до callback сессии или закрытия устройства. VideoCodecResources отдельно удерживает codec и input Images при отказах release, включая ошибку конструктора и аппаратной пробы. Его retry не обращается к native-ядру. RX join ограничен 250 мс за попытку; decoder не теряется при отказе release, его Surface остаётся до подтверждённого освобождения codec. Остальные Surface и playout очищаются независимо. Очередь Surface владеет callback release до успешного возврата, включая позднюю передачу после stop; callback должен допускать повтор после исключения. CallVideoTexture передаёт callback идемпотентного StreamPreviewTarget: частичный отказ физического release не теряет второй ресурс. Старый destroy не отсоединяет новый output. CallScreen фиксирует ориентацию окна, immersive возвращает прежние панели; Bluetooth permission связан с call ID, camera permission — с конкретным engine. Process-ссылка и Handler сохраняют cleanup после выхода RX, без переноса уже назначенного раннего таймера. Отказ post не теряет владельцев и допускает явный stop.

Shutdown возвращает Deferred, успешный результат означает подтверждение всех владельцев сервиса. При отказе process-ссылка удерживает экземпляр даже после onDestroy, Handler повторяет попытку с задержкой 250 мс..30 с. Отказ post не теряет владельца: shutdown/requireReleased повторяет очистку явно. Отмена Deferred не подтверждает освобождение. CallController снимает карточку независимо от retry, blocksMedia запрещает другой audio, а восстановление рации ждёт Deferred и mediaPreparation. NativeLib перед stop/restart требует подтверждённый сервис и завершённую подготовку media; иначе возвращает ошибку без разрушения ядра.

CallController проверяет приём каждой команды сигналинга в native-очередь. Отказ start/accept завершает локальный запуск, отказ continue оставляет выбор для повтора. Непринятые decline/hangup сохраняются по call ID и повторяются с задержкой 250 мс..30 с; CALL_ENDED/DECLINED отменяет повтор. До приёма завершающей команды blocksMedia запрещает новый вызов, восстановление рации ждёт также эту очередь и завершение Telecom. Reset очищает повторы до остановки ядра. Ошибки записи маршрута и показа результата логируются независимо от завершения звонка и очистки сервиса.

RadioOverlayService на main обеспечивает foreground-режим микрофона при активной рации и включённом оверлее либо автоматике. Сам PCM захватывает RadioAudioEngine; разрешённый экран/фоновое окно по-прежнему проверяются движком. Старт FGS откладывается до видимой Activity. Разрешения запрашиваются только явной командой пользователя; отказ launcher снимает ожидание и допускает повтор. Ошибки настроек, проверки разрешения и обновления языка логируются с причиной.

Starting удерживается от запроса ОС до onCreate; stop в этом промежутке ставит pendingStop. Сервис сначала завершает foreground-handshake, затем запрашивает остановку. Новый запрос во время stop ждёт onDestroy. Отказ stopService/исключение не фиксирует pendingStop навсегда и допускает повтор. Ошибки создания канала, promote и получения WindowManager попадают в общий rollback запуска. Уведомление оверлея имеет отдельный ID 1005, без замены звонкового FGS (1002), трансляции (1004) и результатов вызовов (1003/1006). Обновление языка не публикует уведомление остановленного владельца.

Окно и LayoutParams сохраняются до addView: исключение после эффекта API получает remove. Успешный remove либо IllegalArgumentException подтверждает отсутствие окна; прочие отказы сохраняют ссылку и отключают обработку касаний. IsShown означает добавленное либо ещё не подтверждённо удалённое окно. После onDestroy такой владелец остаётся в currentService, и sync повторяет очистку до замены новым. Повторный/поздний onDestroy не гасит новый сервис. Touch/layout callbacks проверяют identity окна и владельца; старый callback позиционирования не сбрасывает флаг нового.

Обычное отпускание OVERLAY оставляет хвост 250 мс. Два коротких тапа фиксируют PTT; следующее нажатие снимает latch. Drag начинается только в первые 300 мс удержания, снимает PTT с обычным хвостом и сохраняет выбранную позицию до нового окна. ACTION_CANCEL, ошибка перемещения, remove и stop отменяют OVERLAY сразу, сохраняя BUTTON/VOLUME. Ошибка окна и отзыв разрешения также останавливают ненужный FGS, если автоматическая передача его не удерживает. При разрешённой автоматике сервис может продолжать работу без плавающей кнопки. Ошибка observer не уходит в необработанное исключение: свой PTT/окно закрываются, сервис запрашивает stop.

IncomingCallManager на main владеет одним входящим оповещением P2P/рации. Повтор show с теми же ID/каналом/пиром/видом обновляет только уведомление. Замена сначала снимает прежнее оповещение. Ошибки wake lock и уведомления не мешают запуску RingtonePlayer; обновление языка не повторяет звук, пробуждение или full-screen intent.

stopRinging сразу снимает active, затем вызывает остановку RingtonePlayer и независимо очищает wake lock, кнопки и уведомление. Receiver проверяет полный 64-битный call ID и вид P2P/рация: уже доставленный DECLINE после stop/замены игнорируется. PendingIntent различаются URI с видом/ID, а не усечённым requestCode.

Wake lock один, без подсчёта ссылок, с таймаутом 30 секунд. Менеджер хранит его до успешного isHeld/release; если ОС уже сняла lock, второй release не нужен. При отказе старый lock не перезаписывается новым: следующий звонок получает звук/уведомление, но пропускает новое пробуждение до очистки прежнего lock. Каждый PendingIntent хранится с владельцем до успешного cancel. Необходимость отмены уведомления отмечается до вызова notify: исключение может прийти после эффекта в ОС, поэтому ошибка публикации не освобождает от последующей отмены.

Повтор stopRinging, init или refreshLanguage без active повторяет незавершённую очистку, не трогая уже успешно очищенные ресурсы. Пока канал не подготовлен, init/refreshLanguage также отменяет возможное уведомление прежнего процесса; отказ создания канала не блокирует очистку. Отдельного таймера повторных попыток нет. При активном оповещении обновление языка сохраняет его кнопки, звук и lock. Логи InCall содержат ID/вид вызова для кнопок, фазу/идентификатор lock и признак оставшейся очистки после stop.

RingtonePlayer читает системный режим звонка при play: NORMAL даёт звук и вибрацию, VIBRATE — вибрацию, SILENT — ни одного эффекта. API 28+ использует зацикленный Ringtone, API 26–27 — MediaPlayer с профилем звонка после выдачи audio focus. Отказ одного эффекта не запрещает другой. Потеря focus запрашивает остановку звука, сохраняя вибрацию; последующий GAIN не перезапускает мелодию.

stop сначала запрещает callbacks звука и снимает таймер вибрации. Владельцы Ringtone/MediaPlayer/Vibrator остаются до успешного stop/release/cancel, а исходная пара AudioManager/AudioFocusRequest — до GRANTED от abandonAudioFocusRequest. Новый звук не запускается, пока остаётся старый плеер или focus; новый импульс не запускается при незавершённой отмене вибрации. Повтор stop/начало play повторяет только незавершённую очистку. IncomingCallManager вызывает её также при повторных stop/init/обновлении языка без активного оповещения.

Вибрация состоит из конечных импульсов 1000 мс с повторным запуском через 2000 мс. Отказ импульса или постановки таймера отключает повторение и запрашивает cancel. Даже при ошибке cancel новые импульсы не ставятся в очередь. Ошибка API после возможного эффекта сохраняет владельца для следующей попытки; отдельного таймера очистки нет. Логи Ringtone показывают идентификатор ресурса, причину отказа, успешное освобождение и оставшиеся sound/focus/vibration после запроса stop.

CallRingback на main владеет одним зацикленным MediaPlayer с профилем MEDIA: connecting и ringing выбирают разные ресурсы. Повтор активной фазы сохраняет плеер. Смена фазы, stop, error и неожиданный completion сначала запрещают callbacks, затем запрашивают release. При исключении ссылка и прежний resource ID остаются для следующего stop/start. Пока release не подтверждён, новая фаза пропускается с WARN; запоздалые callbacks не повторяют очистку и не трогают замену. После восстановления start может заново запустить ту же фазу. Пропущенный start автоматически не воспроизводится; таймера повторов нет.

NotificationSound на main запускает отдельный однократный плеер с профилем уведомлений; сигналы разных сообщений могут перекрываться. Объект приложения сильно удерживает каждый плеер от получения из create до успешного release, включая настройку listeners и проигрывание: это предотвращает преждевременное завершение звука сборщиком мусора, описанное в контракте MediaPlayer. ID звука — канал либо dm:<ID>; enabled/mutedChannels — снимки настроек для новых сигналов. Смена настроек не останавливает уже играющие сигналы.

Completion/error/ошибка настройки или запуска запрашивает release своего плеера. Исключение сохраняет владельца с признаком незавершённой очистки; следующий callback либо play повторяет попытку. В начале play повторяются только такие release, включая случаи disabled/mute и последующий отказ create. Неудача одного плеера не мешает очистить остальные или запустить новый сигнал; ещё играющие плееры остаются нетронутыми. После успешного release владелец удаляется, поздние callbacks игнорируются. Автоматического таймера очистки нет. Логи Ringback/Sound отмечают успех только после возврата release без исключения; при отказе показывают ресурс/канал, идентификатор плеера и причину попытки.

BatteryOptimizationPermission читает power allowlist Android для текущего пакета: true/false — ответ ОС, null — ошибка чтения сервиса/статуса с ERROR-диагностикой. Флаг в DataStore не сохраняется и результат не кэшируется. Это исключение из оптимизации, а не снятие всех фоновых ограничений; контракт ОС описан в PowerManager.

Компонент BatteryOptimizationSetting используется в первоначальной настройке и настройках приложения. Он перечитывает статус на каждом RESUME. При null показывает ошибку и «Повторить»: эта кнопка только повторяет чтение. При false кнопка «Разрешить» сначала перечитывает ОС, затем открывает пакетный запрос, а при отказе запуска — общий список исключений. Успешный startActivity означает лишь открытие системного экрана; выдача исключения определяется после возврата. Неизвестный результат не считается отказом пользователя. Pending запроса переживает пересоздание UI, оба неудачных запуска снимают его с видимой ошибкой. Настройка не перезапускает ядро и не блокирует завершение первоначальной настройки.

Android HeadlessService — foreground service с уведомлением и общим native-ядром, а не Linux control-loop. Он запускает NetworkChangeMonitor и NetworkActivityMonitor. Foreground подтверждается до запуска native; отказ завершает ожидание startup ошибкой. Уведомления и независимые части onDestroy логируют ошибки отдельно. CoreShutdown переживает Service и повторяет отказ stop с backoff 250 мс..30 с; новый init ждёт успешный stop. Переход UI в фон передаёт client_activity в uasync. Standby чередует ACTIVE/SLEEP; сам event loop продолжает работать. Ожидания SLEEP будятся после перехода в ACTIVE, сетевой activity может разбудить после min_sleep. Контракт отмены: libutun_lite/standby.h.

AppEventHandler передаёт каждый native event одним main-заданием и независимо вызывает обработчики владельцев. Сбой одного обработчика не отменяет остальные и доставку ViewModel. SharedFlow не повторяет старые команды/ACK новой ViewModel; начальный снимок читается при DB_READY или проверке уже готового ядра. Emit при заполнении буфера ждёт на main coroutine, без блокировки native-потока. Отказ main-поста и ошибка обработки конкретного события логируются.

AudioRecorderManager сохраняется в process registry с начала захвата либо playback до release ACK. ViewModel.onCleared запрашивает асинхронную очистку, рабочий поток переживает UI. Native cancel и Android release повторяются независимо; неудачный AudioTrack/decoder close остаётся у playback worker также после completion callback. Capture cleanup не держит playback lock. NativeLib запрещает новый voice capture во время core teardown/звонка; stop ядра и подготовка звонка ждут освобождения этих владельцев до доступа к native recorder/звонковому I/O.

DEBUG_* фильтруются общими уровнями/категориями. Bridge переводит их в logcat и, при включённой подписке, Kotlin LogManager. Debug hook также пишет в UDP-буфер: 16KiB threshold либо timer 200мс, неблокирующая отправка без повторов. ERROR вызывает flush. Target — числовой IPv4; attach/detach таймера выполняет uasync worker при каждом запуске/рестарте. Ошибки самого UDP-логгера идут в отдельный utun-udplog, минуя debug hook.

LogManager хранит последние 100000 строк в памяти процесса, в порядке приёма. Прямой Kotlin addLog сохраняет важность logcat, но не проходит native-фильтр и не доставляет строку в native-файл/UDP. videoDebug/videoTrace проходят общий video-фильтр через JNI. JNI callback получает уровни bridge 0=TRACE..4=ERROR, а не числовые DEBUG_LEVEL_*; неизвестный уровень фиксируется предупреждением.

Строки логов передаются через onNativeLog(int, byte[], byte[]) как обычный UTF-8: это сохраняет кириллицу/emoji без ограничений Modified UTF-8 NewStringUTF. Повреждённые UTF-8-байты Kotlin отображает как U+FFFD. На каждый вызов JNI берёт свой local ref callback под mutex; замена/очистка global ref не инвалидирует уже начатую доставку. Java-код вызывается после освобождения mutex. Указатель C log callback читается/меняется атомарно; отключение не ждёт начатых вызовов. init включает пересылку, Activity подписывается на экране логов и отключает её при выходе; stop снимает C callback, destroy дополнительно освобождает global ref.

Ошибки JNI-доставки и повторный вход в callback того же потока фиксируются напрямую в logcat, минуя debug hook, чтобы исключить рекурсию. Проверяются attach, ссылки, method lookup, выделение/копирование массивов и Java callback. Все свои local refs освобождаются; detach выполняется только после своего attach. Исключение от собственной операции/callback описывается и снимается; исключение, уже существовавшее при входе в logger, сохраняется, доставка этой строки пропускается. Неудачная доставка не повторяется. Native logcat/файл/UDP работают независимо.

Экран логов показывает последние 10000 строк истории, обновляя снимок не чаще кадра. StateFlow передаёт объединяемую ревизию, строки читаются из общей истории: задержка UI и открытие экрана после последнего сообщения не теряют строки в отдельной очереди событий. Лимиты истории/экрана всё равно вытесняют старые строки. Фильтр выбирает точный уровень; пауза отключает только прокрутку, сбор продолжается. Автопрокрутка следует последней видимой строке после фильтрации и возобновления.

Linux headless и проверки

Каталог headless/ — отдельный каркас: headless_main запускает select-loop и JSON-line control socket, но не создаёт uasync/instance и не запускает чат. -p меняет порт, -l только печатает имя файла. ping/status/connections/debug_level возвращают заглушки, subscribe/quit управляют клиентом, ряд команд даёт not implemented. Текстовые команды вида send <channel> <text> этот каркас не поддерживает. JOIN/settings требуют уже запущенное ядро и исполнения в его потоке; каркас не заменяет рабочий headless API.

Для Linux сетевых сценариев используются основной src/utun, [chatserver] headless_control_bind=127.0.0.1:9999 и tools/chatcli. Для APK: cd tools/chatgui-android && ./gradlew assembleDebug. :app:testDebugUnitTest включает :router-tests:test: настоящие IncomingCallManager/receiver/RingtonePlayer проверяются на JVM с моделями Android-служб. Сценарии включают отказы до/после эффекта acquire/release, notify/cancel и отмены PendingIntent, stop/release проигрывателя, request/abandon audio focus, импульса/cancel вибрации и постановки таймера. Проверяются независимая повторная очистка, замена вызова и устаревшие callbacks. Эти модели не проверяют поведение системных служб/OEM на устройстве. Для CallRingback/NotificationSound проверяются также запрет смены фазы при неосвобождённом loop, независимость перекрывающихся сигналов, очистка без установленных callbacks и повтор release при выключенном звуке/ошибке create. RadioPttLifecycleTest использует настоящие RadioAudioEngine, MicrophoneCapture и MediaAudioController с host-границами Android/JNI. Проверяется не только UI-флаг, но и команда native capture stop либо упорядоченный radioTalkEnd при сохранённом измерителе: отказ/обычный хвост таймера, повторное нажатие, независимые владельцы, отмена, смена группы, RECORD_AUDIO и запрет фоновых PTT-вызовов. PCM-подписки и workers настоящие; звукового оборудования, DSP и сетевой передачи в модели нет. RadioAudioLifecycleTest проверяет отказы регистрации/очистки receiver и AudioTrack, зависимость release от detach lease, interruption, ошибки native/start/settings, отложенные и повторно входящие callbacks, восстановление после звонка, отказ перенастройки с последующим старым ACK и отказы таймеров ACK/автоматики. Проверяется и сохранение владельцев после частичного эффекта API, и продолжение независимой очистки перед повторной попыткой. RadioWorkerLifecycleTest проверяет исключения и неверные результаты pull/write, целостность частичных PCM-записей и заполнение тишиной, запуск capture с частичным native-эффектом, отказ feed/PTT/границы, отсутствие детектора, interruption и отказ main-очереди. Барьеры потоков воспроизводят ошибки старых playback/feed/start после запроса новой группы. Проверяются освобождение подписок/encoder/track и сохранение нового PTT. Android/JNI в этих сценариях моделируются на границах; DSP, звуковое оборудование и сетевой эфир не запускаются. CallAudioLifecycleTest компилирует настоящие CallAudioEngine/CallAudioOutput и workers. Проверяются отказы release до/после эффекта, частичная подготовка AudioRecord/AudioTrack, регистрация/удаление communication listener, native stop, восстановление режима, поздний Telecom handoff и запрет stop вне main/нулевого ID. Барьер capture доказывает, что до выхода worker устройства/native сохраняются и новая сессия не запускается. Android hardware и DSP моделируются; AAudio/OEM поведение на устройстве не проверяется. :video-tests:test проверяет настоящий CallVideoPlayer, uAV2 KEY framing и смену поколений на JVM. Управляемые границы MediaCodec/probe/JNI позволяют воспроизвести отказы частичного configure/start/stop/release, Surface callback, playout destroy, join и retry Handler. Проверяются независимое освобождение, запрет release Surface до codec, старый detach, ACK показа/PTS и EOS drain без смены Surface. OEM/реальное декодирование остаются вне модели. StreamPreviewLifecycleTest проверяет реальные preview/leases: потерю/частичный эффект callback, отменённые команды, независимые и конкурентные владельцы, физические отказы release до/после эффекта, retry Handler, detach/visibility и поздние update. Также проверяются onRelease без destroy, позднее появление texture после dispose и destroy прежнего output. QrScannerSessionTest проверяет настоящего владельца CameraX/ML Kit с управляемыми use cases, Task и SDK отказами. Dispose во время распознавания, поздняя camera future, частичный bind, отказ listener и независимый cleanup не требуют камеры для воспроизведения. RadioCallLifecycleTest использует настоящие RadioCallController, IncomingCallManager, CoreActivity и гудки. Управляемые Android/JNI/repository-границы проверяют все команды сигналинга, pending START, валидацию событий/полной identity, активацию, старые callbacks/таймеры, PHONE_BUSY/CONTINUE, uint64 ID, отказы активности и публикации результатов. IncomingCallLifecycleTest также использует настоящий рационный контроллер при доставке DECLINE. Радиоэфир и C-сигналинг в JVM не запускаются; native-протокол проверяется отдельным test_radio. CallServiceLifecycleTest в :call-service-tests использует настоящие CallService, CallController, CallState, CoreActivity и router. Управляемые OS/DSP/camera/Telecom границы воспроизводят частичные startup/stop, независимую очистку, повтор после onDestroy, отказ очереди retry и отмену Deferred. Проверяются запрет другого media/core teardown, ожидание восстановления рации, старая identity callbacks, повторное использование notification tokens, camera flag и conversation clock. Настоящие codec/камеры, Telecom и ограничения FGS/OEM в JVM не запускаются. :app:testDebugUnitTest включает этот модуль как зависимость задачи, без host-классов в APK. StreamServiceLifecycleTest использует настоящие StreamService/StreamController и управляемый capture: частичный старт, повтор cleanup после destroy, pending SDK ACK, отказы lock/native stop/tokens/foreground, FIN, повреждённые stats, исключение poll и старые notification IDs. StreamCaptureEngineLifecycleTest в :video-tests запускает настоящий capture worker с управляемыми Camera2/MediaCodec/microphone/JNI: поздние/старые callbacks, отмену switch, отказ encoder/Surface/subscription/Opus и запрет ACK по таймауту. StreamVideoPlaybackLifecycleTest и StreamPlayerEngineLifecycleTest используют настоящие video/viewer/mixer workers: cleanup decoder/DSP/signal, GAP+KEY одного batch, fallback, ограниченный join и запрет второго AudioTrack до release. Это проверки владения и порядка операций; реальное декодирование, capture rate и OEM остаются вне JVM-модели. TelecomLifecycleTest в том же модуле компилирует настоящие TelecomCallManager/UtunConnection/UtunConnectionService: account/SDK failures, дубликаты/late owners, destroy и повтор, uint64 extras, malformed requests, оба порядка endpoint outcome/confirmation, legacy masks, stale callbacks и исключения listener. OS Telecom IPC и фактические аудиомаршруты заменены управляемыми границами. RadioOverlayLifecycleTest использует настоящие overlay service, RadioAudioEngine, микрофонные подписки и MediaAudioController. Host-модели service/window IPC проверяют частичные add/remove, повторную очистку, отказы channel/promote/stop, старые touch/layout/destroy callbacks, PTT/latch/drag, отзыв разрешений и изолированные ID уведомлений. Settings ACK позволяет выполнять настоящие PCM workers через управляемую JNI-границу. WindowManager/foreground restrictions, отрисовка и OEM-поведение на устройстве в JVM не проверяются. На Linux :app:testDebugUnitTest также запускает 25 fault-сценариев настоящего JNI logger через :app:testJniLog; окружение и границы модели: tests/jni_log/README.md. Проверки native bridge/standby доступны в tests/Makefile.am; проверка независимости media workers на устройстве описана в ../tests/stream_worker_smoke/README.md и не проверяет сетевую доставку/UI.