Telecomunicaciones

La nueva biblioteca de Android Telecom Jetpack te permite indicarle fácilmente a la plataforma en qué estado se encuentra tu llamada. Puedes encontrar el código fuente y una app de muestra en GitHub.

Dependencias y permisos

Primero, abre el archivo build.gradle del módulo de tu app y agrega una dependencia para el módulo de androidx Telecom:

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

En el manifiesto de la app, declara que usa el permiso MANAGE_OWN_CALLS:

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

Registra la aplicación

Para permitir que Android conozca tu app, debes registrarla y sus funciones. Esto le indica a Android qué funciones admite tu app, como las videollamadas, la transmisión de llamadas y las llamadas en espera. Esta información es importante para que Android pueda configurarse a fin de funcionar con las funciones de tu 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)

Integración de plataformas

Las dos situaciones de llamada más comunes para cualquier aplicación que hacen llamadas son las llamadas entrantes y salientes. Para registrar correctamente la dirección de la llamada y notificar adecuadamente al usuario mediante notificaciones, usa las siguientes APIs.

Registrar una llamada

En este ejemplo, se muestra cómo registrar una llamada entrante:

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)
}

El objeto callAttributes puede tener las siguientes propiedades:

  • displayName: Es el nombre del emisor, la reunión o la sesión.
  • address: Es la dirección de la llamada. Ten en cuenta que esto se puede extender a un vínculo de la reunión.
  • direction: Es la dirección de la llamada, como entrante o saliente.
  • callType: Es la información relacionada con los datos que se transmiten, como video y audio.
  • callCapabilities: Es un objeto que especifica las capacidades de la llamada.

El objeto callCapabilities puede tener las siguientes propiedades:

  • streaming: Indica si la llamada admite la transmisión de audio a otro dispositivo con Android.
  • transfer: Indica si se puede transferir la llamada.
  • hold: Indica si la llamada se puede poner en espera.

Agregar una llamada

El método addCall() muestra una excepción si el dispositivo no admite telecomunicaciones o si se produjo un error durante la configuración de la llamada.

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

Cómo responder una llamada

Una vez que realices una llamada entrante, debes responderla o rechazarla. En este ejemplo, se muestra cómo responder una llamada:

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

    }

    is CallControlResult.Error -> {

    }
}

Si hay otra llamada en curso, answer() mostrará CallControlResult.Error, que informa por qué no se pudo responder la llamada. En este caso, el usuario debe poner la otra llamada en espera.

Cómo rechazar una llamada

Para rechazar una llamada, desconéctala con DisconnectCause.Rejected.

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

Llamada saliente

Cuando realizas una llamada saliente, una vez que responde el tercero remoto, debes establecer la llamada como active para que la plataforma sepa que la llamada está en curso:

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

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

Coloca una llamada en espera

Si tu app de llamadas admite la espera de llamadas, usa setInActive para indicarle a la plataforma que la llamada no está activa y que el micrófono y la cámara están disponibles para que otras apps puedan usarlos:

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

    }

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

Desconectar

Para desconectar una llamada, proporciona una causa válida a fin de informar a la pila de Telecom que se desconecte:

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

Audio de ruta

Durante una llamada, los usuarios a veces cambian de dispositivos, como una bocina, un auricular o un dispositivo Bluetooth. Usa las APIs de availableEndpoints y currentCallEndpoint para obtener una lista de todos los dispositivos disponibles para el usuario y los que están activos.

En este ejemplo, se combinan ambos flujos para crear un objeto de IU a fin de mostrarle al usuario una lista de dispositivos y cuál está activo:

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

Para cambiar un dispositivo activo, usa el requestEndpointChange con el CallEndpoint al que quieras cambiar.

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

Asistencia en primer plano

La biblioteca de Telecom es compatible con el primer plano. Esta biblioteca usa ConnectionService para dispositivos que ejecutan Android 13 y versiones anteriores. En Android 14 y versiones posteriores, usa el micrófono y la cámara de primer plano para admitir correctamente los servicios en primer plano. Obtén más información sobre los servicios en primer plano.

Como parte de los requisitos en primer plano, la aplicación debe publicar una notificación para que los usuarios sepan que se está ejecutando en primer plano.

Para asegurarte de que tu app tenga prioridad de ejecución en primer plano, crea una notificación una vez que registres la llamada con la plataforma. Se quita la prioridad en primer plano cuando la app finaliza la llamada o la notificación ya no es válida.

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

Compatibilidad con plataformas

Los relojes tienen una aplicación receptora de extremos genérica. Esta aplicación le proporciona al usuario una interfaz básica, como responder, rechazar y desconectar llamadas. La aplicación admite estas acciones mediante la implementación de funciones lambda que informan a la plataforma que realizaste la acción en el dispositivo.

Si la aplicación no responde, se agota el tiempo de espera de cada función lambda después de 5 segundos con una transacción con errores.

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 = {}