Реализация ConnectionService для Android (интеграция с системными звонками)
Большинство VoIP-приложений на Android работают через собственный UI: кастомный экран звонка поверх всего, системный телефон при этом ничего не знает о происходящем. Это работает — до момента, когда пользователь ожидает, что звонок из вашего приложения ведёт себя как обычный: отображается на экране блокировки, паузирует музыку через аудио-фокус, виден в системном журнале звонков, совместим с Bluetooth-гарнитурой и автомобильными системами Android Auto.
Именно для этого существует ConnectionService — компонент Telecom Framework, который позволяет приложению зарегистрироваться как полноправный телефонный провайдер в системе Android. Реализация нетривиальна: API требует точного следования жизненному циклу Connection, работы с PhoneAccount, обработки системных событий типа DTMF и audio routing.
Что именно реализует ConnectionService
ConnectionService — абстрактный класс из пакета android.telecom. Приложение наследуется от него и регистрирует реализацию в манифесте как <service> с permission android.permission.BIND_TELECOM_CONNECTION_SERVICE. Система Telecom вызывает колбэки сервиса при входящих и исходящих звонках.
Центральный объект — Connection. Для каждого звонка создаётся свой экземпляр Connection с набором состояний:
NEW → DIALING → RINGING → ACTIVE → HOLDING → DISCONNECTED
Каждый переход — явный вызов соответствующего метода: setDialing(), setRinging(), setActive(), setOnHold(), setDisconnected(DisconnectCause). Если переход не вызван — система считает звонок зависшим. Это одна из самых распространённых ошибок в первых реализациях: VoIP-стек получает ответ сервера, но Connection остаётся в состоянии DIALING вечно.
PhoneAccount — идентификатор провайдера в системе. Регистрируется через TelecomManager.registerPhoneAccount(). Требует иконку, метку, указание поддерживаемых URI-схем (tel, sip или кастомных), флагов возможностей (CAPABILITY_CALL_PROVIDER, CAPABILITY_VIDEO_CALLING и т.д.).
Пользователь должен явно включить PhoneAccount в настройках системы — приложение не может сделать это автоматически. Первый запуск требует навигации в Settings → Apps → [Приложение] → Phone accounts. Это UX-момент, который нужно проектировать отдельно.
Где ломается большинство реализаций
Audio routing
Когда Connection переходит в ACTIVE, система ожидает, что приложение возьмёт аудио-фокус и настроит маршрутизацию звука. Делается через AudioManager.requestAudioFocus() с AudioFocusRequest (API 26+) или AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE. Без этого другие приложения (музыкальный плеер, навигатор) не получат сигнал о паузе.
Переключение между динамиком, наушниками и Bluetooth — через ConnectionService.onCallAudioStateChanged(). Система передаёт CallAudioState с текущим маршрутом и bitmask доступных маршрутов. Приложение должно синхронизировать своё состояние с системным. Частая ошибка — приложение меняет маршрут через AudioManager напрямую, игнорируя CallAudioState, и система показывает неправильное состояние кнопок в системном UI.
Входящий звонок на заблокированном экране
Входящий звонок, инициированный приложением через TelecomManager.addNewIncomingCall(), должен сопровождаться IncomingCallUi — либо системным экраном вызова, либо кастомным Activity с флагами FLAG_SHOW_WHEN_LOCKED | FLAG_TURN_SCREEN_ON | FLAG_KEEP_SCREEN_ON. С API 27 используется setShowWhenLocked(true) и setTurnScreenOn(true) на Activity.
Уведомление о входящем звонке с API 31 требует Notification.CallStyle.forIncomingCall() — без этого система может не показать полноэкранный интент на некоторых устройствах. На Samsung One UI поведение отличается от AOSP: полноэкранный интент иногда игнорируется в пользу системного notification shade.
Hold и конференции
CAPABILITY_HOLD на Connection означает, что звонок можно поставить на удержание. Но если VoIP-бэкенд не поддерживает hold через SIP re-INVITE с a=sendonly — capability нужно убрать, иначе система будет отправлять onHold(), а приложение не сможет его выполнить. Конференция через Conference объект — отдельный уровень сложности: управление participantами, merge, swap.
Android Auto и WearOS
ConnectionService автоматически интегрируется с Android Auto — системный интерфейс автомобиля покажет карточку звонка. Но если приложение переопределяет аудио-маршрутизацию напрямую, это конфликтует с Bluetooth-профилями HFP. Тестирование в эмуляторе Android Auto обязательно.
Разрешения и ограничения
READ_PHONE_STATE — для получения состояния телефона. MANAGE_OWN_CALLS — если приложение управляет звонками без регистрации как полноценного провайдера (упрощённый режим, не даёт интеграции с системным журналом). RECORD_AUDIO — для захвата микрофона. С Android 10+ есть дополнительные ограничения на запуск Activity из фона — полноэкранный интент требует USE_FULL_SCREEN_INTENT permission, которое с API 34 требует явного разрешения пользователя.
На устройствах с кастомными оболочками (MIUI, One UI, ColorOS) поведение TelecomManager отличается от AOSP. Тестирование только на эмуляторе недостаточно — нужны реальные устройства Xiaomi, Samsung, OPPO.
Процесс и сроки
Реализация ConnectionService включает несколько этапов: проектирование архитектуры (как VoIP-стек сигнализирует о звонках в ConnectionService), реализацию жизненного цикла Connection, интеграцию с UI, тестирование audio routing на нескольких устройствах.
Интеграция зависит от существующего VoIP-стека: если используется готовый SIP-стек (LinphoneSDK, PJSIP через Android wrapper, WebRTC через Google's libwebrtc), его события нужно транслировать в переходы Connection. Если стек разрабатывается с нуля — сроки вырастают существенно.
Оценка: 2-3 недели для базовой интеграции входящих/исходящих звонков с системным UI, 4-6 недель для полного функционала с hold, conference, DTMF, Android Auto. Стоимость — после анализа существующего VoIP-стека и требований.







