A biblioteca Core-Telecom simplifica o processo de integração do aplicativo de chamadas com a plataforma Android, fornecendo um conjunto robusto e consistente de APIs.
Se você quiser conferir implementações práticas, encontre exemplos de aplicativos no GitHub:
- Exemplo de app leve: um exemplo mínimo que demonstra o uso da API
Core-Telecom. Ideal para entender rapidamente conceitos fundamentais. - Exemplo de app abrangente (desenvolvido pela equipe do Core-Telecom Team): um aplicativo mais rico em recursos que mostra funcionalidades avançadas de telecomunicações e práticas recomendadas. Esse é um ótimo recurso para entender cenários de integração complexos.
Configurar o Core-Telecom
Adicione a dependência androidx.core:core-telecom ao arquivo build.gradle do app:
dependencies {
implementation ("androidx.core:core-telecom:1.0.0")
}
Declare a permissão MANAGE_OWN_CALLS no AndroidManifest.xml:
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
Registrar seu aplicativo
Registre seu app de chamadas no Android usando CallsManager para começar a adicionar chamadas ao sistema. Ao registrar, especifique os recursos do app (por exemplo, suporte a áudio e vídeo):
val callsManager = CallsManager(context)
val capabilities: @CallsManager.Companion.Capability Int =
(CallsManager.CAPABILITY_BASELINE or
CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)
callsManager.registerAppWithTelecom(capabilities)
Gerenciamento de chamadas
Use as APIs do Core-Telecom para criar e gerenciar um ciclo de vida de chamadas.
Criar uma chamada
O objeto CallAttributesCompat define as propriedades de uma chamada exclusiva, que pode ter as seguintes características:
displayName: nome do autor da chamada.address: endereço da chamada (por exemplo, número de telefone, link da reunião).direction: recebida ou realizada.callType: áudio ou vídeo.callCapabilities: oferece suporte a transferência e espera.
Confira um exemplo de como criar uma chamada recebida:
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
)
}
Adicionar uma chamada
Use callsManager.addCall com CallAttributesCompat e callbacks para adicionar uma nova chamada ao sistema e gerenciar atualizações de superfície remota. O callControlScope no bloco addCall permite principalmente que o app faça a transição do estado da chamada e receba atualizações de áudio:
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.
}
Atender uma chamada
Atenda uma chamada recebida no CallControlScope:
when (val result = answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
is CallControlResult.Success -> { /* Call answered */ }
is CallControlResult.Error -> { /* Handle error */ }
}
Rejeitar uma chamada
Rejeite uma chamada usando disconnect() com DisconnectCause.REJECTED no CallControlScope:
disconnect(DisconnectCause(DisconnectCause.REJECTED))
Ativar uma ligação efetuada
Defina uma ligação efetuada como ativa quando a parte remota atender:
when (val result = setActive()) {
is CallControlResult.Success -> { /* Call active */ }
is CallControlResult.Error -> { /* Handle error */ }
}
Colocar uma chamada em espera
Use setInactive() para colocar uma chamada em espera:
when (val result = setInactive()) {
is CallControlResult.Success -> { /* Call on hold */ }
is CallControlResult.Error -> { /* Handle error */ }
}
Encerrar uma chamada
Encerre uma chamada usando disconnect() com um DisconnectCause:
disconnect(DisconnectCause(DisconnectCause.LOCAL))
Gerenciar endpoints de áudio de chamadas
Observe e gerencie endpoints de áudio usando currentCallEndpoint, availableEndpoints e isMuted Flows no CallControlScope.
fun observeAudioStateChanges(callControlScope: CallControlScope) {
with(callControlScope) {
launch { currentCallEndpoint.collect { /* Update UI */ } }
launch { availableEndpoints.collect { /* Update UI */ } }
launch { isMuted.collect { /* Handle mute state */ } }
}
}
Mude o dispositivo de áudio ativo usando requestEndpointChange():
coroutineScope.launch {
callControlScope.requestEndpointChange(callEndpoint)
}
Suporte em primeiro plano
A biblioteca usa ConnectionService (nível 33 da API Android e versões anteriores) ou
foregroundtypes (nível 34 da API Android e versões mais recentes) para suporte em primeiro plano.
Como parte dos requisitos de primeiro plano, o aplicativo precisa postar uma notificação para que os usuários saibam que ele está em execução em primeiro plano.
Para garantir que o app tenha prioridade de execução em primeiro plano, crie uma notificação depois de adicionar a chamada à plataforma. A prioridade em primeiro plano é removida quando o app encerra a chamada ou a notificação não é mais válida.
Saiba mais sobre os serviços em primeiro plano.
Suporte a superfícies remotas
Dispositivos remotos (smartwatches, fones de ouvido Bluetooth, Android Auto) podem gerenciar chamadas sem interação direta com o smartphone. O app precisa implementar lambdas de callback (onAnswerCall, onSetCallDisconnected, onSetCallActive, onSetCallInactive) fornecidos para CallsManager.addCall para processar ações iniciadas por esses dispositivos.
Quando uma ação remota ocorre, a lambda correspondente é invocada.
A conclusão da lambda indica que o comando foi processado. Se o comando não puder ser obedecido, a lambda vai gerar uma exceção.
A implementação adequada garante o controle de chamadas em diferentes dispositivos. Teste bem com várias superfícies remotas.
Extensões de chamadas
Além de gerenciar o estado da chamada e a rota de áudio das chamadas, a biblioteca também oferece suporte a extensões de chamadas, que são recursos opcionais que o app pode implementar para uma experiência de chamada mais rica em plataformas remotas, como o Android Auto. Esses recursos incluem salas de reunião, silenciamento de chamadas e ícones de chamadas adicionais. Quando o app implementa uma extensão, as informações fornecidas por ele são sincronizadas com todos os dispositivos conectados que também oferecem suporte à exibição dessas extensões na interface. Isso significa que esses recursos também estarão disponíveis em dispositivos remotos para que os usuários interajam.
Criar uma chamada com extensões
Ao criar uma chamada, em vez de usar CallManager#addCall para criar a chamada,
você pode usar CallManager#addCallWithExtensions, que dá ao
app acesso a um escopo diferente chamado ExtensionInitializationScope. Esse escopo permite que o aplicativo inicialize o conjunto de extensões opcionais que ele oferece suporte. Além disso, esse escopo fornece um método extra, onCall, que fornece um CallControlScope de volta ao app após a conclusão da troca e inicialização da capacidade de extensão.
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...
}
}
}
Oferecer suporte a participantes de chamadas
Se o app oferece suporte a participantes de chamadas para reuniões ou chamadas em grupo, use addParticipantExtension para declarar suporte a essa extensão e use as APIs relacionadas para atualizar superfícies remotas quando os participantes mudarem.
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)
}
}
Além de notificar as superfícies remotas sobre quais participantes estão na chamada, o participante ativo também pode ser atualizado usando ParticipantExtension#updateActiveParticipant.
Também há suporte para ações opcionais relacionadas aos participantes da chamada.
O app pode usar ParticipantExtension#addRaiseHandSupport para oferecer suporte à noção de participantes levantando a mão na chamada e ver quais outros participantes também levantaram a mão.
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)
}
}
Oferecer suporte ao silenciamento de chamadas
O silenciamento de chamadas permite que um usuário solicite que o app silencie o áudio de saída de uma chamada sem silenciar fisicamente o microfone do dispositivo. Esse recurso é gerenciado por chamada. Assim, o Jetpack processa a complexidade de gerenciar o estado de silenciamento global de chamadas de celular em andamento enquanto uma chamada VOIP está ativa. Isso torna o silenciamento do áudio de saída menos propenso a erros em cenários de várias chamadas, além de permitir recursos úteis, como indicações de "você está falando" quando o usuário está falando sem perceber que o silenciamento de chamadas está ativado.
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)
}
}
Oferecer suporte a ícones de chamadas
Um ícone de chamada permite que o app especifique um ícone personalizado que representa a chamada a ser exibida em plataformas remotas durante a chamada. Esse ícone também pode ser atualizado durante o ciclo de vida da chamada.
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)
}
}
Integrar com o registro de chamadas do sistema
O Core-Telecom permite que o aplicativo seja integrado ao registro de chamadas do sistema. Esse recurso permite que as chamadas feitas ou recebidas usando o aplicativo VoIP apareçam no histórico de ligações do discador do sistema, permitindo que os usuários usem o discador para iniciar um callback.
Ativar a integração do registro de chamadas
Para ativar esse recurso, o aplicativo precisa ser registrado para processar a intent TelecomManager.ACTION_CALL_BACK no AndroidManifest.xml e fornecer uma Activity para processar o callback. Por exemplo, se VoipCallbackActivity for a atividade que processa o callback, o manifesto será assim:
Registrar a intent no manifesto
<activity
android:name=".VoipCallbackActivity"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.action.CALL_BACK" />
</intent-filter>
</activity>
Processar a intent de callback
Quando o usuário inicia um callback no discador do sistema, a atividade registrada
é iniciada com a TelecomManager.ACTION_CALL_BACK ação.
A intent contém TelecomManager.EXTRA_UUID fornecido ao app quando
você adicionou a chamada originalmente usando CallsManager.
O método CallControlScope getCallId retorna esse ID exclusivo para a
chamada.
Salve todas as informações necessárias para iniciar a chamada identificada pelo UUID no app (por exemplo, uma lista de números de telefone para uma chamada em grupo) para que você possa reconstruir e fazer a chamada novamente quando o callback for acionado.
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()
}
}
Desativar a integração do registro de chamadas
Por padrão, as chamadas são registradas no registro de chamadas do sistema. Você pode desativar esse
recurso por chamada definindo o isLogged parâmetro como false
ao criar CallAttributesCompat:
val callAttributes = CallAttributesCompat(
displayName = callerName,
address = addressUri,
direction = CallAttributesCompat.DIRECTION_OUTGOING,
isLogExcluded = true // Opt-out of system call logging
)