Кор-Телеком

Библиотека Core-Telecom упрощает процесс интеграции вашего приложения для совершения звонков с платформой Android, предоставляя надежный и согласованный набор API.

Если вы хотите ознакомиться с практическими примерами, вы можете найти их на GitHub:

Настройка Core-Telecom

Добавьте зависимость androidx.core:core-telecom в файл build.gradle вашего приложения:

dependencies {
    implementation ("androidx.core:core-telecom:1.0.0")
}

Укажите разрешение MANAGE_OWN_CALLS в файле AndroidManifest.xml :

<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />

Зарегистрируйте свое приложение

Зарегистрируйте свое приложение для звонков в Android с помощью CallsManager , чтобы начать добавлять звонки в систему. При регистрации укажите возможности вашего приложения (например, поддержка аудио и видео):

val callsManager = CallsManager(context)

val capabilities: @CallsManager.Companion.Capability Int =
    (CallsManager.CAPABILITY_BASELINE or
          CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)

callsManager.registerAppWithTelecom(capabilities)

Управление звонками

Используйте API Core-Telecom для создания и управления жизненным циклом вызовов.

Создать вызов

Объект CallAttributesCompat определяет свойства уникального вызова, который может обладать следующими характеристиками:

  • displayName : имя вызывающего абонента.
  • address : Адрес для связи (например, номер телефона, ссылка на встречу).
  • direction : Входящее или исходящее.
  • callType : Аудио или видео.
  • callCapabilities : Поддерживает перевод и удержание вызова.

Вот пример того, как создать входящий вызов:

fun createIncomingCallAttributes(
    callerName: String,
    callerNumber: String,
    isVideoCall: Boolean): CallAttributesCompat {
    val addressUri = Uri.parse("YourAppScheme:$callerNumber")

    // Define capabilities supported by your call.
    val callCapabilities = CallAttributesCompat.CallCapability(
        supportsSetInactive = CallAttributesCompat.SUPPORTS_SET_INACTIVE // Call can be made inactive (implies hold)
    )

    return CallAttributesCompat(
        displayName = callerName,
        address = addressUri,
        direction = CallAttributesCompat.DIRECTION_INCOMING,
        callType = if (isVideoCall) CallAttributesCompat.CALL_TYPE_VIDEO_CALL else CallAttributesCompat.CALL_TYPE_AUDIO_CALL,
        callCapabilitiesCompat = callCapabilities
    )
}

Добавить звонок

Используйте callsManager.addCall с CallAttributesCompat и коллбэками для добавления нового вызова в систему и управления удаленными обновлениями интерфейса. Область callControlScope внутри блока addCall в основном позволяет вашему приложению изменять состояние вызова и получать аудиообновления:

try {
    callsManager.addCall(
        INCOMING_CALL_ATTRIBUTES,
        onAnswerCall, // Watch needs to know if it can answer the call.
        onSetCallDisconnected,
        onSetCallActive,
        onSetCallInactive
    ) {
        // The call was successfully added once this scope runs.
        callControlScope = this
    }
}
catch(addCallException: Exception){
   // Handle the addCall failure.
}

Ответить на звонок

Ответьте на входящий вызов в пределах области CallControlScope :

when (val result = answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
    is CallControlResult.Success -> { /* Call answered */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

Отклонить вызов

Отклоните вызов с помощью disconnect() с DisconnectCause.REJECTED в рамках области CallControlScope :

disconnect(DisconnectCause(DisconnectCause.REJECTED))

Активировать исходящий вызов

После ответа удаленного абонента исходящий вызов должен быть активирован:

when (val result = setActive()) {
    is CallControlResult.Success -> { /* Call active */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

Поставьте звонок на удержание.

Используйте setInactive() , чтобы поставить звонок на удержание:

when (val result = setInactive()) {
    is CallControlResult.Success -> { /* Call on hold */ }
    is CallControlResult.Error -> { /* Handle error */ }
}

Завершить вызов

Разъединить вызов можно с помощью disconnect() с указанием DisconnectCause :

disconnect(DisconnectCause(DisconnectCause.LOCAL))

Управление аудиотерминалами вызовов

Отслеживайте и управляйте аудиотерминалами с помощью currentCallEndpoint , availableEndpoints и isMuted в рамках Flow CallControlScope

fun observeAudioStateChanges(callControlScope: CallControlScope) {
    with(callControlScope) {
        launch { currentCallEndpoint.collect { /* Update UI */ } }
        launch { availableEndpoints.collect { /* Update UI */ } }
        launch { isMuted.collect { /* Handle mute state */ } }
    }
}

Изменить активное аудиоустройство можно с помощью requestEndpointChange() :

coroutineScope.launch {
     callControlScope.requestEndpointChange(callEndpoint)
}

Поддержка переднего плана

Библиотека использует ConnectionService (Android 13, уровень API 33 и ниже) или foregroundtypes (Android 14, уровень API 34 и выше) для поддержки работы в фоновом режиме.

В рамках требований к работе в фоновом режиме приложение должно отправлять уведомление пользователям о том, что приложение работает в фоновом режиме.

Чтобы обеспечить приоритет выполнения вашего приложения в фоновом режиме, создайте уведомление после добавления вызова к платформе. Приоритет выполнения в фоновом режиме снимается, когда ваше приложение завершает вызов или ваше уведомление перестает быть действительным.

Узнайте больше о службах переднего плана .

Удалённая поддержка Surface

Удаленные устройства (умные часы, Bluetooth-гарнитуры, Android Auto) способны управлять вызовами без прямого взаимодействия с телефоном. Ваше приложение должно реализовывать лямбда-функции обратного вызова ( onAnswerCall , onSetCallDisconnected , onSetCallActive , onSetCallInactive ), передаваемые в метод CallsManager.addCall , для обработки действий, инициированных этими устройствами.

При выполнении удалённого действия вызывается соответствующая лямбда-функция.

Успешное завершение работы лямбда-функции означает, что команда обработана. Если команда не может быть выполнена, лямбда-функция должна сгенерировать исключение.

Правильная реализация обеспечивает бесперебойное управление вызовами на разных устройствах. Проведите тщательное тестирование на различных удаленных платформах.

Добавочные номера телефонов

Помимо управления состоянием вызова и маршрутизацией звука, библиотека также поддерживает расширения вызовов — дополнительные функции, которые ваше приложение может реализовать для более удобного использования вызовов на удаленных устройствах, таких как Android Auto. К таким функциям относятся переговорные комнаты, отключение звука во время вызова и дополнительные значки вызовов. Когда ваше приложение реализует расширение, предоставляемая им информация будет синхронизироваться со всеми подключенными устройствами, которые также поддерживают отображение этих расширений в своем пользовательском интерфейсе. Это означает, что эти функции будут доступны пользователям и на удаленных устройствах.

Создать звонок с добавочными номерами

При создании вызова вместо использования CallManager#addCall можно использовать CallManager#addCallWithExtensions , который предоставляет приложению доступ к другой области видимости, называемой ExtensionInitializationScope . Эта область видимости позволяет приложению инициализировать набор поддерживаемых им дополнительных расширений. Кроме того, эта область видимости предоставляет дополнительный метод onCall , который возвращает приложению объект CallControlScope после завершения обмена данными о возможностях расширений и их инициализации.

scope.launch {
    mCallsManager.addCallWithExtensions(
        attributes,
        onAnswer,
        onDisconnect,
        onSetActive,
        onSetInactive
    ) {
        // Initialize extension-specific code...

        // After the call has been initialized, perform in-call actions
        onCall {
            // Example: process call state updates
            callStateFlow.onEach { newState ->
                // handle call state updates and notify telecom
            }.launchIn(this)

            // Use initialized extensions...
        }
    }
}

Участники телефонной конференции поддержки

Если ваше приложение поддерживает вызов участников для совещаний или групповых звонков, используйте addParticipantExtension , чтобы объявить о поддержке этого расширения, и используйте соответствующие API для обновления удаленных интерфейсов при изменении участников.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial participants state in the call.
        val participantExtension = addParticipantExtension(
            initialParticipants,
            initialActiveParticipant
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
        }
    }

Помимо уведомления удаленных устройств о том, кто участвует в звонке, активного участника также можно обновить с помощью ParticipantExtension#updateActiveParticipant .

Также поддерживаются дополнительные действия, связанные с участниками звонка. Приложение может использовать ParticipantExtension#addRaiseHandSupport для поддержки идеи поднятия рук участниками звонка и отображения того, кто еще поднял руки.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Notifies Jetpack that this app supports the participant
        // extension and provides the initial list of participants in the call.
        val participantExtension = addParticipantExtension(initialParticipants)
        // Notifies Jetpack that this app supports the notion of participants
        // being able to raise and lower their hands.
        val raiseHandState = participantExtension.addRaiseHandSupport(
                initialRaisedHands
            ) { onHandRaisedStateChanged ->
                // handle this user's raised hand state changed updates from
                // remote surfaces.
            }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // Example: update remote surfaces when the call participants change
            participantsFlow.onEach { newParticipants ->
                participantExtension.updateParticipants(newParticipants)
            }.launchIn(this)
            // notify remote surfaces of which of the participants have their
            // hands raised
            raisedHandsFlow.onEach { newRaisedHands ->
                raiseHandState.updateRaisedHands(newRaisedHands)
            }.launchIn(this)
        }
    }

Поддержка Звонок Молчание

Функция отключения звука во время звонка позволяет пользователю запросить у приложения отключение исходящего звука во время звонка без физического отключения микрофона устройства. Эта функция управляется для каждого звонка отдельно, поэтому Jetpack берет на себя сложную задачу управления глобальным состоянием отключения звука для текущих сотовых звонков во время активного VoIP-звонка. Это делает отключение исходящего звука менее подверженным ошибкам в сценариях с несколькими звонками, а также позволяет использовать полезные функции, такие как индикация «Вы говорите?», когда пользователь говорит, не зная, что функция отключения звука включена.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for locally silencing the call's outgoing audio and
        // register a handler for when the user changes the call silence state
        // from a remote surface.
        val callSilenceExtension = addLocalCallSilenceExtension(
            initialCallSilenceState = false
        ) { newCallSilenceStateRequest ->
            // handle the user's request to enable/disable call silence from
            // a remote surface
        }

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's call silence state changes, update remote
            // surfaces of the new state.
            callSilenceState.onEach { isSilenced ->
                callSilenceExtension.updateIsLocallySilenced(isSilenced)
            }.launchIn(this)
        }
    }

Иконки вызова службы поддержки

Значок вызова позволяет приложению указать пользовательский значок, представляющий вызов, который будет отображаться на удаленных устройствах во время разговора. Этот значок также может быть обновлен в течение всего времени разговора.

mCallsManager.addCallWithExtensions(...) {
        // Initialize extensions...

        // Add support for a custom call icon to be displayed during the
        // lifetime of the call.
        val callIconExtension = addCallIconExtension(
            initialCallIconUri = initialUri
        )

        // After the call has been initialized, perform in-call control actions
        onCall {
            // other in-call control and extension actions...

            // When the call's icon changes, update remote surfaces by providing
            // the new URI.
            callIconUri.onEach { newIconUri ->
                callIconExtension.updateCallIconUri(newIconUri)
            }.launchIn(this)
        }
    }

Интеграция с журналом системных вызовов

Core-Telecom позволяет вашему приложению интегрироваться с системным журналом вызовов. Эта функция позволяет отображать звонки, совершенные или принятые с помощью вашего VoIP-приложения, в истории вызовов системного дозвонщика, что дает пользователям возможность использовать дозвонщик для инициирования обратного звонка.

Включить интеграцию журнала звонков

Для включения этой функции ваше приложение должно зарегистрироваться для обработки интента TelecomManager.ACTION_CALL_BACK в файле AndroidManifest.xml и предоставить Activity для обработки обратного вызова. Например, пусть VoipCallbackActivity будет Activity, которая обрабатывает обратный вызов, тогда ваш манифест должен выглядеть следующим образом:

Зарегистрируйте намерение в манифесте.

<activity
    android:name=".VoipCallbackActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.telecom.action.CALL_BACK" />
    </intent-filter>
</activity>

Обработайте намерение обратного вызова

Когда пользователь инициирует обратный вызов из системного дозвонщика, запускается зарегистрированное вами действие с действием TelecomManager.ACTION_CALL_BACK . Интент содержит TelecomManager.EXTRA_UUID , предоставленный вашему приложению при первоначальном добавлении вызова с помощью CallsManager .

Метод ` getCallId CallControlScope возвращает уникальный идентификатор вызова.

В приложении следует сохранить всю информацию, необходимую для начала вызова, идентифицированного по UUID (например, список телефонных номеров для группового вызова), чтобы можно было восстановить и повторно совершить вызов при срабатывании функции обратного вызова.

class VoipCallbackActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (intent?.action == TelecomManager.ACTION_CALL_BACK) {
            val uuidString = intent.getStringExtra(TelecomManager.EXTRA_UUID)
            if (uuidString != null) {
                val uuid = UUID.fromString(uuidString)
                
                // Retrieve your persisted call info using the UUID
                val callData = CallRepository.getCallByUuid(uuid)
                
                if (callData != null) {
                    // Place the call again using your app's logic
                    // ...
                }
            }
        }
        finish()
    }
}

Отказаться от интеграции журнала звонков

По умолчанию звонки регистрируются в системном журнале вызовов. Вы можете отключить эту функцию для каждого вызова отдельно, установив параметр isLogged в значение false при создании CallAttributesCompat :

val callAttributes = CallAttributesCompat(
    displayName = callerName,
    address = addressUri,
    direction = CallAttributesCompat.DIRECTION_OUTGOING,
    isLogExcluded = true // Opt-out of system call logging
)