Реалізація ConnectionService для Android (інтеграція з системними звонками)
Більшість VoIP-застосунків на Android працюють через власний UI: кастомний екран звонку поверх всього, системний телефон при цьому нічого не знає про відбуває. Це працює — до моменту, коли користувач очікує, що звонок з вашого застосунку ведеться як звичайний: відображається на екрані блокування, паузує музику через аудіо-фокус, виден у системному журналі звонків, сумісний з Bluetooth-гарнітурою та автомобільними системами Android Auto.
Саме для цього існує ConnectionService — компонент Telecom Framework, який дозволяє застосунку зареєструватися як повноправний телефонний провайдер у системі Android. Реалізація нетривіальна: API вимагає точного дотримання життєвого циклу Connection, роботи з PhoneAccount, обробки системних подій типу DTMF та audio routing.
Що саме реалізує ConnectionService
ConnectionService — абстрактний клас з пакету android.telecom. Застосунок наслідується від нього та реєструє реалізацію у маніфесті як <service> з дозволом 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, і система показує неправильний стан кнопок.
Входящий звонок на заблокованому екрані
Входящий звонок, ініційований через 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() — без цього система може не показати fullscreen intent на деяких пристроях. На Samsung One UI поведінка відрізняється від AOSP: fullscreen intent іноді ігнорується на користь системного notification shade.
Hold та конференції
CAPABILITY_HOLD на Connection означає, що звонок можна поставити на утримання. Але якщо VoIP-бекенд не підтримує hold через SIP re-INVITE з a=sendonly — capability потрібно убрати, інакше система буде надсилати onHold(), а застосунок не зможе його виконати. Конференція через об'єкт Conference — окремий рівень складності: управління учасниками, merge, swap.
Android Auto та WearOS
ConnectionService автоматично інтегрується з Android Auto — системний інтерфейс автомобіля показує карточку звонку. Але якщо застосунок переопиває аудіо-маршрутизацію напрямку, це конфліктує з Bluetooth-профілями HFP. Тестування в емуляторі Android Auto обов'язково.
Дозволи та обмеження
READ_PHONE_STATE — для отримання стану телефону. MANAGE_OWN_CALLS — якщо застосунок керує звонками без реєстрації як повноцінного провайдера (спрощений режим, не дає інтеграції з системним журналом). RECORD_AUDIO — для захопту мікрофона. З Android 10+ є додаткові обмеження на запуск Activity з фона — fullscreen intent вимагає 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 libwebrtc), його події потрібно транслювати в переходи Connection. Якщо стек розробляється з нуля — строки вирростають істотно.
Оцінка: 2-3 тижні для базової інтеграції входящих/вихідних звонків з системним UI, 4-6 тижнів для повного функціоналу з hold, conference, DTMF, Android Auto. Вартість — після аналізу існуючого VoIP-стека та вимог.







