Telecomunicações

A nova biblioteca do Jetpack para telecomunicações do Android facilita informar à plataforma o estado da chamada. O código-fonte e um app de exemplo estão disponíveis no GitHub (em inglês).

Dependências e permissões

Primeiro, abra o arquivo build.gradle do módulo do app e adicione uma dependência ao módulo androidx Telecom:

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

No manifesto do app, declare que ele usa a permissão MANAGE_OWN_CALLS:

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

Registrar o aplicativo

Para informar o Android sobre seu app, você precisa registrá-lo e os recursos dele. Isso informa ao Android quais recursos são compatíveis com o app, como videochamadas, streaming de chamadas e chamadas em espera. Essas informações são importantes para que o Android possa se configurar para trabalhar com os recursos do seu app.

 private val callsManager = CallsManager(context)

var capabilities: @CallsManager.Companion.Capability Int =
    CallsManager.CAPABILITY_BASELINE or
          CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING or
          CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING

callsManager.registerAppWithTelecom(capabilities)

Integração com plataformas

Os dois cenários de chamada mais comuns para qualquer aplicativo de chamada são chamadas recebidas e efetuadas. Para registrar corretamente a direção da chamada e notificar o usuário adequadamente com notificações, use as APIs abaixo.

Registrar uma chamada

Este exemplo demonstra como registrar uma chamada recebida:

companion object {
  const val APP_SCHEME = "MyCustomScheme"
  const val ALL_CALL_CAPABILITIES = (CallAttributes.SUPPORTS_SET_INACTIVE
    or CallAttributes.SUPPORTS_STREAM or CallAttributes.SUPPORTS_TRANSFER)

  const val INCOMING_NAME = "Luke"
  val INCOMING_URI: Uri = Uri.fromParts(APP_SCHEME, "", "")
  // Define all possible properties for CallAttributes
  val INCOMING_CALL_ATTRIBUTES =
    CallAttributes(
      INCOMING_NAME,
      INCOMING_URI,
      DIRECTION_INCOMING,
      CALL_TYPE_VIDEO_CALL,
      ALL_CALL_CAPABILITIES)
}

O objeto callAttributes pode ter as seguintes propriedades:

  • displayName: o nome do autor da chamada, da reunião ou da sessão.
  • address: o endereço da chamada. Observe que isso pode ser ampliado para um link de reunião.
  • direction: a direção da chamada, como recebida ou realizada.
  • callType: informações relacionadas aos dados que estão sendo transmitidos, como vídeo e áudio.
  • callCapabilities: um objeto que especifica os recursos da chamada.

O objeto callCapabilities pode ter as seguintes propriedades:

  • streaming: indica se a chamada oferece suporte a streaming de áudio para outro dispositivo Android.
  • transfer: indica se a chamada pode ser transferida.
  • hold: indica se a chamada pode ser colocada em espera.

Adicionar uma ligação

O método addCall() retornará uma exceção se o dispositivo não oferecer suporte a telecomunicações ou se ocorrer um erro ao configurar a chamada.

try {
    callsManager.addCall(
        INCOMING_CALL_ATTRIBUTES,
        onIsCallAnswered, // Watch needs to know if it can answer the call
        onIsCallDisconnected,
        onIsCallActive,
        onIsCallInactive
    ) {
        callControlScope = this
    }
}

Atender uma ligação

Depois de receber uma chamada, você deve atendê-la ou rejeitá-la. Este exame demonstra como atender uma ligação:

when (answer(CallAttributesCompat.CALL_TYPE_AUDIO_CALL)) {
    is CallControlResult.Success -> {

    }

    is CallControlResult.Error -> {

    }
}

Se outra chamada estiver em andamento, answer() retornará CallControlResult.Error, que informa por que a chamada não pôde ser atendida. Nesse caso, o usuário precisa colocar a outra chamada em espera.

Rejeitar uma chamada

Para rejeitar uma chamada, desconecte-a com DisconnectCause.Rejected.

fun onRejectCall(){
    coroutineScope.launch {
        callControlScope?.let {
            it.disconnect(DisconnectCause(DisconnectCause.REJECTED))
        }
    }
}

Chamada realizada

Ao fazer uma chamada, quando a outra parte atende, você precisa definir a chamada como active para que a plataforma saiba que ela está em andamento:

when (setActive()) {
    is CallControlResult.Success -> {
        onIsCallActive()
    }

    is CallControlResult.Error -> {
        updateCurrentCall {
            copy(errorCode = result.errorCode)
        }
    }
}

Colocar uma chamada em espera

Se o app de chamadas for compatível com chamadas, use setInActive para informar à plataforma que a chamada não está ativa e que o microfone e a câmera podem ser usados por outros apps:

when (setInActive()) {
    is CallControlResult.Success -> {

    }

    is CallControlResult.Error -> {
        updateCurrentCall {
            copy(errorCode = result.errorCode)
        }
    }
}

Desconectar

Para desconectar uma chamada, informe uma causa válida para que a pilha de telecomunicações se desconecte:

coroutineScope.launch {
    callControlScope?.disconnect(DisconnectCause(DisconnectCause.LOCAL))
}

Encaminhar áudio

Durante uma chamada, às vezes os usuários alternam entre dispositivos, como alto-falante, fone de ouvido ou dispositivo Bluetooth. Use as APIs availableEndpoints e currentCallEndpoint para conferir uma lista de todos os dispositivos disponíveis para o usuário e qual dispositivo está ativo.

Este exemplo combina os dois fluxos para criar um objeto de interface que mostra ao usuário uma lista de dispositivos e qual deles está ativo:

availableEndpoint = combine(callControlScope.availableEndpoints,
    callControlScope.currentCallEndpoint) {
    availableDevices: List<CallEndpoint>, activeDevice : CallEndpoint ->
    availableDevices.map {
        EndPointUI(
            isActive = activeDevice.endpointName == it.endpointName, it
        )
    }
}

Para mudar um dispositivo ativo, use o requestEndpointChange com o CallEndpoint que você quer usar.

coroutineScope.launch {
     callControlScope?.requestEndpointChange(callEndpoint)
}

Suporte em primeiro plano

A biblioteca Telecom oferece suporte em primeiro plano. Essa biblioteca usa ConnectionService para dispositivos com o Android 13 e versões anteriores. No Android 14 e versões mais recentes, ele usa o microfone e a câmera dos tipos em primeiro plano para oferecer suporte correta aos serviços em primeiro plano. Saiba mais sobre os serviços em primeiro plano.

Como parte dos requisitos de primeiro plano, o aplicativo precisa postar uma notificação para os usuários saberem que o aplicativo está sendo executado em primeiro plano.

Para garantir que o app tenha prioridade de execução em primeiro plano, crie uma notificação depois de registrar a chamada com a plataforma. A prioridade em primeiro plano é removida quando o app encerra a chamada ou a notificação não é mais válida.

is TelecomCall.Registered -> {
    val notification = createNotification(call)
    notificationManager.notify(TELECOM_NOTIFICATION_ID, notification)
}

Suporte a superfícies

Os smartwatches têm um aplicativo receptor de endpoint genérico. Esse aplicativo fornece ao usuário uma interface básica, como atender, rejeitar e desconectar chamadas. O aplicativo oferece suporte a essas ações implementando funções lambda que informam à plataforma que você executou a ação no dispositivo.

Cada função lambda expira após cinco segundos com uma transação com falha se o aplicativo não responde.

callsManager.addCall(
        attributes,
        onIsCallAnswered, // Watch/Auto need to know if they can answer the call
        onIsCallDisconnected,
        onIsCallActive,
        onIsCallInactive
    ) {
//Call Scope
}
/**
  *  Can the call be successfully answered??
  *  TIP: Check the connection/call state to see if you can answer a call
  *  Example you may need to wait for another call to hold.
  **/
val onIsCallAnswered: suspend(type: Int) -> Unit = {}

/**
  * Can the call perform a disconnect
  */
val onIsCallDisconnected: suspend (cause: DisconnectCause) -> Unit = {}

/**
  *  Check is see if you can make the call active.
  *  Other calls and state might stop us from activating the call
  */
val onIsCallActive: suspend () -> Unit = {
    updateCurrentCall {
    }
}

/**
  * Check to see if you can make the call inactivate
  */
val onIsCallInactive: suspend () -> Unit = {}