CallKit en Xamarin.iOS

La nueva API CallKit en iOS 10 proporciona una manera de integrar aplicaciones VOIP con la interfaz de usuario de iPhone y proporcionar una interfaz y experiencia conocidas al usuario final. Con esta API, los usuarios pueden ver e interactuar con las llamadas VOIP desde la pantalla de bloqueo del dispositivo iOS y administrar contactos mediante el favoritos de la aplicación Phone y vistas recientes.

Acerca de CallKit

Según Apple, CallKit es un nuevo marco que elevará las aplicaciones de voz sobre IP (VOIP) de terceros a una experiencia de primeros en iOS 10. La API CallKit permite que las aplicaciones VOIP se integren con la interfaz de usuario de iPhone y proporcionen una interfaz y experiencia familiares al usuario final. Al igual que la aplicación de teléfono integrada, un usuario puede ver e interactuar con las llamadas VOIP desde la pantalla de bloqueo del dispositivo iOS y administrar los contactos mediante las vistas Favoritos y Recientes del teléfono.

Además, CallKit API proporciona la capacidad de crear extensiones de aplicación que pueden asociar un número de teléfono con un nombre (identificador de llamador) o indicar al sistema cuándo se debe bloquear un número (bloqueo de llamadas).

La experiencia de aplicación VOIP existente

Antes de analizar la nueva API CallKit y sus capacidades, eche un vistazo a la experiencia del usuario actual con una aplicación VOIP de terceros en iOS 9 (y menos) con una aplicación VOIP ficticia llamada MonkeyCall. MonkeyCall es una aplicación sencilla que permite al usuario enviar y recibir llamadas VOIP mediante las API de iOS existentes.

Actualmente, si el usuario recibe una llamada entrante en MonkeyCall y su iPhone está bloqueado, la notificación recibida en la pantalla de bloqueo es indistinguible de cualquier otro tipo de notificación (como las de las aplicaciones Mensajes o Correo, por ejemplo).

Si el usuario quería responder a la llamada, tendría que deslizar la notificación MonkeyCall para abrir la aplicación y escribir su código de acceso (o Touch ID del usuario) para desbloquear el teléfono antes de poder aceptar la llamada e iniciar la conversación.

La experiencia es igualmente complicada si el teléfono está desbloqueado. De nuevo, la llamada entrante de MonkeyCall se muestra como un banner de notificación estándar que se desliza desde la parte superior de la pantalla. Dado que la notificación es temporal, el usuario puede perderla fácilmente y forzado a abrir el Centro de notificaciones y encontrar la notificación específica para responder a la llamada o buscar e iniciar manualmente la aplicación MonkeyCall.

La experiencia de la aplicación VOIP CallKit

Al implementar las nuevas API de CallKit en la aplicación MonkeyCall, la experiencia del usuario con una llamada VOIP entrante se puede mejorar considerablemente en iOS 10. Tome el ejemplo del usuario que recibe una llamada VOIP cuando su teléfono está bloqueado como antes. Al implementar CallKit, la llamada aparecerá en la pantalla de bloqueo del iPhone, como si la llamada se recibiera desde la aplicación de teléfono integrada, con la pantalla completa, la interfaz de usuario nativa y la funcionalidad estándar de deslizar para responder a la llamada.

De nuevo, si se desbloquea el iPhone cuando se recibe una llamada VOIP de MonkeyCall, se presenta igualmente la pantalla completa, la interfaz de usuario nativa y la funcionalidad estándar de deslizar para responder y pulsar para rechazar de la manera que hace una aplicación integrada de teléfono y MonkeyCall tiene la opción de reproducir un tono de llamada personalizado.

CallKit proporciona funcionalidad adicional a MonkeyCall, lo que permite que sus llamadas VOIP interactúen con otros tipos de llamadas, para que aparezcan en las listas recientes y favoritas integradas, para usar las características integradas No molestar y Bloquear, iniciar llamadas MonkeyCall desde Siri y ofrece la posibilidad de que los usuarios asignen llamadas MonkeyCall a personas de la aplicación Contactos.

En las secciones siguientes se describirá la arquitectura CallKit, los flujos de llamadas entrantes y salientes y la API CallKit en detalle.

La Arquitectura de CallKit

En iOS 10, Apple ha adoptado CallKit en todos los servicios del sistema, de modo que las llamadas realizadas en CarPlay, por ejemplo, se conocen a la interfaz de usuario del sistema a través de CallKit. En el ejemplo siguiente, dado que MonkeyCall adopta CallKit, se conoce al sistema de la misma manera que estos servicios del sistema integrados y obtiene todas las mismas características:

Pila del servicio CallKit

Eche un vistazo más a la aplicación MonkeyCall del diagrama anterior. La aplicación contiene todo su código para comunicarse con su propia red y contiene sus propias interfaces de usuario. Vincula CallKit para comunicarse con el sistema:

Arquitectura de la aplicación MonkeyCall

Hay dos interfaces principales en CallKit que la aplicación usa:

  • CXProvider: esto permite que la aplicación MonkeyCall informe al sistema de las notificaciones fuera de banda que puedan producirse.
  • CXCallController: permite que la aplicación MonkeyCall informe al sistema de acciones de usuario local.

El CXProvider

Como se indicó anteriormente, CXProvider permite a una aplicación informar al sistema de las notificaciones fuera de banda que puedan producirse. Se trata de una notificación que no se produce debido a acciones de usuario locales, sino que se producen debido a eventos externos, como las llamadas entrantes.

Una aplicación debe usar el CXProvider para lo siguiente:

  • Informar una llamada entrante al sistema.
  • Informar que la llamada saliente se ha conectado al sistema.
  • Informar que el usuario remoto termina la llamada al sistema.

Cuando la aplicación quiere comunicarse con el sistema, usa la clase CXCallUpdate y cuando el sistema necesita comunicarse con la aplicación, usa la clase CXAction:

Comunicación con el sistema a través de CXProvider

El CXCallController

El CXCallController permite a una aplicación informar al sistema de acciones de usuario locales, como el usuario que inicia una llamada VOIP. Al implementar un CXCallController la aplicación se pone en juego con otros tipos de llamadas en el sistema. Por ejemplo, si ya hay una llamada de telefonía activa en curso, CXCallController puede permitir que la aplicación VOIP coloque esa llamada en espera e inicie o responda a una llamada VOIP.

Una aplicación debe usar el CXCallController para lo siguiente:

  • Informar cuando el usuario ha iniciado una llamada saliente al sistema.
  • Informar cuando el usuario responde a una llamada entrante al sistema.
  • Informar cuando el usuario finaliza una llamada al sistema.

Cuando la aplicación quiere comunicar las acciones de usuario local al sistema, usa la clase CXTransaction:

Informes al sistema mediante CXCallController

Implementación de CallKit

En las secciones siguientes se muestra cómo implementar CallKit en una aplicación VOIP de Xamarin.iOS. Por ejemplo, este documento usará código de la aplicación ficticia MonkeyCall VOIP. El código que se presenta aquí representa varias clases auxiliares, las partes específicas de CallKit se tratarán en detalle en las secciones siguientes.

La clase ActiveCall

La aplicación MonkeyCall usa la clase ActiveCall para contener toda la información sobre una llamada VOIP que está activa actualmente de la siguiente manera:

using System;
using CoreFoundation;
using Foundation;

namespace MonkeyCall
{
    public class ActiveCall
    {
        #region Private Variables
        private bool isConnecting;
        private bool isConnected;
        private bool isOnhold;
        #endregion

        #region Computed Properties
        public NSUuid UUID { get; set; }
        public bool isOutgoing { get; set; }
        public string Handle { get; set; }
        public DateTime StartedConnectingOn { get; set;}
        public DateTime ConnectedOn { get; set;}
        public DateTime EndedOn { get; set; }

        public bool IsConnecting {
            get { return isConnecting; }
            set {
                isConnecting = value;
                if (isConnecting) StartedConnectingOn = DateTime.Now;
                RaiseStartingConnectionChanged ();
            }
        }

        public bool IsConnected {
            get { return isConnected; }
            set {
                isConnected = value;
                if (isConnected) {
                    ConnectedOn = DateTime.Now;
                } else {
                    EndedOn = DateTime.Now;
                }
                RaiseConnectedChanged ();
            }
        }

        public bool IsOnHold {
            get { return isOnhold; }
            set {
                isOnhold = value;
            }
        }
        #endregion

        #region Constructors
        public ActiveCall ()
        {
        }

        public ActiveCall (NSUuid uuid, string handle, bool outgoing)
        {
            // Initialize
            this.UUID = uuid;
            this.Handle = handle;
            this.isOutgoing = outgoing;
        }
        #endregion

        #region Public Methods
        public void StartCall (ActiveCallbackDelegate completionHandler)
        {
            // Simulate the call starting successfully
            completionHandler (true);

            // Simulate making a starting and completing a connection
            DispatchQueue.MainQueue.DispatchAfter (new DispatchTime(DispatchTime.Now, 3000), () => {
                // Note that the call is starting
                IsConnecting = true;

                // Simulate pause before connecting
                DispatchQueue.MainQueue.DispatchAfter (new DispatchTime (DispatchTime.Now, 1500), () => {
                    // Note that the call has connected
                    IsConnecting = false;
                    IsConnected = true;
                });
            });
        }

        public void AnswerCall (ActiveCallbackDelegate completionHandler)
        {
            // Simulate the call being answered
            IsConnected = true;
            completionHandler (true);
        }

        public void EndCall (ActiveCallbackDelegate completionHandler)
        {
            // Simulate the call ending
            IsConnected = false;
            completionHandler (true);
        }
        #endregion

        #region Events
        public delegate void ActiveCallbackDelegate (bool successful);
        public delegate void ActiveCallStateChangedDelegate (ActiveCall call);

        public event ActiveCallStateChangedDelegate StartingConnectionChanged;
        internal void RaiseStartingConnectionChanged ()
        {
            if (this.StartingConnectionChanged != null) this.StartingConnectionChanged (this);
        }

        public event ActiveCallStateChangedDelegate ConnectedChanged;
        internal void RaiseConnectedChanged ()
        {
            if (this.ConnectedChanged != null) this.ConnectedChanged (this);
        }
        #endregion
    }
}

ActiveCall contiene varias propiedades que definen el estado de la llamada y dos eventos que se pueden generar cuando cambia el estado de la llamada. Dado que solo se trata de un ejemplo, hay tres métodos que se usan para simular el inicio, la respuesta y la finalización de una llamada.

La clase StartCallRequest

La clase estática StartCallRequest proporciona algunos métodos auxiliares que se usarán al trabajar con llamadas salientes:

using System;
using Foundation;
using Intents;

namespace MonkeyCall
{
    public static class StartCallRequest
    {
        public static string URLScheme {
            get { return "monkeycall"; }
        }

        public static string ActivityType {
            get { return INIntentIdentifier.StartAudioCall.GetConstant ().ToString (); }
        }

        public static string CallHandleFromURL (NSUrl url)
        {
            // Is this a MonkeyCall handle?
            if (url.Scheme == URLScheme) {
                // Yes, return host
                return url.Host;
            } else {
                // Not handled
                return null;
            }
        }

        public static string CallHandleFromActivity (NSUserActivity activity)
        {
            // Is this a start call activity?
            if (activity.ActivityType == ActivityType) {
                // Yes, trap any errors
                try {
                    // Get first contact
                    var interaction = activity.GetInteraction ();
                    var startAudioCallIntent = interaction.Intent as INStartAudioCallIntent;
                    var contact = startAudioCallIntent.Contacts [0];

                    // Get the person handle
                    return contact.PersonHandle.Value;
                } catch {
                    // Error, report null
                    return null;
                }
            } else {
                // Not handled
                return null;
            }
        }
    }
}

Las clases CallHandleFromURL y CallHandleFromActivity se usan en AppDelegate para obtener el identificador de contacto de la persona a la que se llama en una llamada saliente. Para obtener más información, consulte la sección Control de llamadas salientes a continuación.

La clase ActiveCallManager

La clase ActiveCallManager controla todas las llamadas abiertas en la aplicación MonkeyCall.

using System;
using System.Collections.Generic;
using Foundation;
using CallKit;

namespace MonkeyCall
{
    public class ActiveCallManager
    {
        #region Private Variables
        private CXCallController CallController = new CXCallController ();
        #endregion

        #region Computed Properties
        public List<ActiveCall> Calls { get; set; }
        #endregion

        #region Constructors
        public ActiveCallManager ()
        {
            // Initialize
            this.Calls = new List<ActiveCall> ();
        }
        #endregion

        #region Private Methods
        private void SendTransactionRequest (CXTransaction transaction)
        {
            // Send request to call controller
            CallController.RequestTransaction (transaction, (error) => {
                // Was there an error?
                if (error == null) {
                    // No, report success
                    Console.WriteLine ("Transaction request sent successfully.");
                } else {
                    // Yes, report error
                    Console.WriteLine ("Error requesting transaction: {0}", error);
                }
            });
        }
        #endregion

        #region Public Methods
        public ActiveCall FindCall (NSUuid uuid)
        {
            // Scan for requested call
            foreach (ActiveCall call in Calls) {
                if (call.UUID.Equals(uuid)) return call;
            }

            // Not found
            return null;
        }

        public void StartCall (string contact)
        {
            // Build call action
            var handle = new CXHandle (CXHandleType.Generic, contact);
            var startCallAction = new CXStartCallAction (new NSUuid (), handle);

            // Create transaction
            var transaction = new CXTransaction (startCallAction);

            // Inform system of call request
            SendTransactionRequest (transaction);
        }

        public void EndCall (ActiveCall call)
        {
            // Build action
            var endCallAction = new CXEndCallAction (call.UUID);

            // Create transaction
            var transaction = new CXTransaction (endCallAction);

            // Inform system of call request
            SendTransactionRequest (transaction);
        }

        public void PlaceCallOnHold (ActiveCall call)
        {
            // Build action
            var holdCallAction = new CXSetHeldCallAction (call.UUID, true);

            // Create transaction
            var transaction = new CXTransaction (holdCallAction);

            // Inform system of call request
            SendTransactionRequest (transaction);
        }

        public void RemoveCallFromOnHold (ActiveCall call)
        {
            // Build action
            var holdCallAction = new CXSetHeldCallAction (call.UUID, false);

            // Create transaction
            var transaction = new CXTransaction (holdCallAction);

            // Inform system of call request
            SendTransactionRequest (transaction);
        }
        #endregion
    }
}

De nuevo, dado que se trata de una simulación solo, el ActiveCallManager solo mantiene una colección de objetos ActiveCall y tiene una rutina para buscar una llamada determinada por su propiedad UUID. También incluye métodos para iniciar, finalizar y cambiar el estado en espera de una llamada saliente. Para obtener más información, consulte la sección Control de llamadas salientes a continuación.

La clase ProviderDelegate

Como se explicó anteriormente, un CXProvider proporciona comunicación bidireccional entre la aplicación y el sistema para las notificaciones fuera de banda. El desarrollador debe proporcionar un CXProviderDelegate personalizado y adjuntarlo al CXProvider para que la aplicación controle eventos CallKit fuera de banda. MonkeyCall usa el siguiente CXProviderDelegate:

using System;
using Foundation;
using CallKit;
using UIKit;

namespace MonkeyCall
{
    public class ProviderDelegate : CXProviderDelegate
    {
        #region Computed Properties
        public ActiveCallManager CallManager { get; set;}
        public CXProviderConfiguration Configuration { get; set; }
        public CXProvider Provider { get; set; }
        #endregion

        #region Constructors
        public ProviderDelegate (ActiveCallManager callManager)
        {
            // Save connection to call manager
            CallManager = callManager;

            // Define handle types
            var handleTypes = new [] { (NSNumber)(int)CXHandleType.PhoneNumber };

            // Get Image Template
            var templateImage = UIImage.FromFile ("telephone_receiver.png");

            // Setup the initial configurations
            Configuration = new CXProviderConfiguration ("MonkeyCall") {
                MaximumCallsPerCallGroup = 1,
                SupportedHandleTypes = new NSSet<NSNumber> (handleTypes),
                IconTemplateImageData = templateImage.AsPNG(),
                RingtoneSound = "musicloop01.wav"
            };

            // Create a new provider
            Provider = new CXProvider (Configuration);

            // Attach this delegate
            Provider.SetDelegate (this, null);

        }
        #endregion

        #region Override Methods
        public override void DidReset (CXProvider provider)
        {
            // Remove all calls
            CallManager.Calls.Clear ();
        }

        public override void PerformStartCallAction (CXProvider provider, CXStartCallAction action)
        {
            // Create new call record
            var activeCall = new ActiveCall (action.CallUuid, action.CallHandle.Value, true);

            // Monitor state changes
            activeCall.StartingConnectionChanged += (call) => {
                if (call.isConnecting) {
                    // Inform system that the call is starting
                    Provider.ReportConnectingOutgoingCall (call.UUID, call.StartedConnectingOn.ToNSDate());
                }
            };

            activeCall.ConnectedChanged += (call) => {
                if (call.isConnected) {
                    // Inform system that the call has connected
                    provider.ReportConnectedOutgoingCall (call.UUID, call.ConnectedOn.ToNSDate ());
                }
            };

            // Start call
            activeCall.StartCall ((successful) => {
                // Was the call able to be started?
                if (successful) {
                    // Yes, inform the system
                    action.Fulfill ();

                    // Add call to manager
                    CallManager.Calls.Add (activeCall);
                } else {
                    // No, inform system
                    action.Fail ();
                }
            });
        }

        public override void PerformAnswerCallAction (CXProvider provider, CXAnswerCallAction action)
        {
            // Find requested call
            var call = CallManager.FindCall (action.CallUuid);

            // Found?
            if (call == null) {
                // No, inform system and exit
                action.Fail ();
                return;
            }

            // Attempt to answer call
            call.AnswerCall ((successful) => {
                // Was the call successfully answered?
                if (successful) {
                    // Yes, inform system
                    action.Fulfill ();
                } else {
                    // No, inform system
                    action.Fail ();
                }
            });
        }

        public override void PerformEndCallAction (CXProvider provider, CXEndCallAction action)
        {
            // Find requested call
            var call = CallManager.FindCall (action.CallUuid);

            // Found?
            if (call == null) {
                // No, inform system and exit
                action.Fail ();
                return;
            }

            // Attempt to answer call
            call.EndCall ((successful) => {
                // Was the call successfully answered?
                if (successful) {
                    // Remove call from manager's queue
                    CallManager.Calls.Remove (call);

                    // Yes, inform system
                    action.Fulfill ();
                } else {
                    // No, inform system
                    action.Fail ();
                }
            });
        }

        public override void PerformSetHeldCallAction (CXProvider provider, CXSetHeldCallAction action)
        {
            // Find requested call
            var call = CallManager.FindCall (action.CallUuid);

            // Found?
            if (call == null) {
                // No, inform system and exit
                action.Fail ();
                return;
            }

            // Update hold status
            call.isOnHold = action.OnHold;

            // Inform system of success
            action.Fulfill ();
        }

        public override void TimedOutPerformingAction (CXProvider provider, CXAction action)
        {
            // Inform user that the action has timed out
        }

        public override void DidActivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
        {
            // Start the calls audio session here
        }

        public override void DidDeactivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
        {
            // End the calls audio session and restart any non-call
            // related audio
        }
        #endregion

        #region Public Methods
        public void ReportIncomingCall (NSUuid uuid, string handle)
        {
            // Create update to describe the incoming call and caller
            var update = new CXCallUpdate ();
            update.RemoteHandle = new CXHandle (CXHandleType.Generic, handle);

            // Report incoming call to system
            Provider.ReportNewIncomingCall (uuid, update, (error) => {
                // Was the call accepted
                if (error == null) {
                    // Yes, report to call manager
                    CallManager.Calls.Add (new ActiveCall (uuid, handle, false));
                } else {
                    // Report error to user here
                    Console.WriteLine ("Error: {0}", error);
                }
            });
        }
        #endregion
    }
}

Cuando se crea una instancia de este delegado, se pasa el ActiveCallManager que usará para controlar cualquier actividad de llamada. A continuación, define los tipos de identificador (CXHandleType) a los que responderá el CXProvider:

// Define handle types
var handleTypes = new [] { (NSNumber)(int)CXHandleType.PhoneNumber };

Y obtiene la imagen de plantilla que se aplicará al icono de la aplicación cuando una llamada esté en curso:

// Get Image Template
var templateImage = UIImage.FromFile ("telephone_receiver.png");

Estos valores se agrupan en un CXProviderConfiguration que se usará para configurar el CXProvider:

// Setup the initial configurations
Configuration = new CXProviderConfiguration ("MonkeyCall") {
    MaximumCallsPerCallGroup = 1,
    SupportedHandleTypes = new NSSet<NSNumber> (handleTypes),
    IconTemplateImageData = templateImage.AsPNG(),
    RingtoneSound = "musicloop01.wav"
};

A continuación, el delegado crea una nueva CXProvider con estas configuraciones y se adjunta a ella:

// Create a new provider
Provider = new CXProvider (Configuration);

// Attach this delegate
Provider.SetDelegate (this, null);

Al usar CallKit, la aplicación ya no creará ni controlará sus propias sesiones de audio, sino que tendrá que configurar y usar una sesión de audio que el sistema creará y controlará para ella.

Si se trata de una aplicación real, el método DidActivateAudioSession se usaría para iniciar la llamada con una AVAudioSession preconfigurada que proporcionó el sistema:

public override void DidActivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
    // Start the call's audio session here...
}

También usaría el método DidDeactivateAudioSession para finalizar y liberar su conexión a la sesión de audio proporcionada por el sistema:

public override void DidDeactivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
    // End the calls audio session and restart any non-call
    // releated audio
}

El resto del código se tratará en detalle en las secciones siguientes.

La clase AppDelegate

MonkeyCall usa AppDelegate para contener instancias del ActiveCallManager y CXProviderDelegate que se usarán en toda la aplicación:

using Foundation;
using UIKit;
using Intents;
using System;

namespace MonkeyCall
{
    [Register ("AppDelegate")]
    public class AppDelegate : UIApplicationDelegate
    {
        #region Constructors
        public override UIWindow Window { get; set; }
        public ActiveCallManager CallManager { get; set; }
        public ProviderDelegate CallProviderDelegate { get; set; }
        #endregion

        #region Override Methods
        public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
        {
            // Initialize the call handlers
            CallManager = new ActiveCallManager ();
            CallProviderDelegate = new ProviderDelegate (CallManager);

            return true;
        }

        public override bool OpenUrl (UIApplication app, NSUrl url, NSDictionary options)
        {
            // Get handle from url
            var handle = StartCallRequest.CallHandleFromURL (url);

            // Found?
            if (handle == null) {
                // No, report to system
                Console.WriteLine ("Unable to get call handle from URL: {0}", url);
                return false;
            } else {
                // Yes, start call and inform system
                CallManager.StartCall (handle);
                return true;
            }
        }

        public override bool ContinueUserActivity (UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
        {
            var handle = StartCallRequest.CallHandleFromActivity (userActivity);

            // Found?
            if (handle == null) {
                // No, report to system
                Console.WriteLine ("Unable to get call handle from User Activity: {0}", userActivity);
                return false;
            } else {
                // Yes, start call and inform system
                CallManager.StartCall (handle);
                return true;
            }
        }

        ...
        #endregion
    }
}

Los métodos de invalidación OpenUrl y ContinueUserActivity se usan cuando la aplicación procesa una llamada saliente. Para obtener más información, consulte la sección Control de llamadas salientes a continuación.

Control de llamadas entrantes

Hay varios estados y procesos a los que una llamada VOIP entrante puede pasar durante un flujo de trabajo de llamada entrante típico, como:

  • Informar al usuario (y el sistema) de que existe una llamada entrante.
  • Recibir notificación cuando el usuario quiere responder a la llamada e inicializar la llamada con el otro usuario.
  • Informar al sistema y a la red de comunicación cuando el usuario quiera finalizar la llamada actual.

En las secciones siguientes se examinará detalladamente cómo una aplicación puede usar CallKit para controlar el flujo de trabajo de llamada entrante, de nuevo con la aplicación MonkeyCall VOIP como ejemplo.

Informar al usuario de la llamada entrante

Cuando un usuario remoto ha iniciado una conversación VOIP con el usuario local, se produce lo siguiente:

Un usuario remoto ha iniciado una conversación VOIP

  1. La aplicación recibe una notificación de su red de comunicaciones que hay una llamada VOIP entrante.
  2. La aplicación usa el CXProvider para enviar un CXCallUpdate al sistema para informarle de la llamada.
  3. El sistema publica la llamada a la interfaz de usuario del sistema, los servicios del sistema y cualquier otra aplicación VOIP mediante CallKit.

Por ejemplo, en el CXProviderDelegate:

public void ReportIncomingCall (NSUuid uuid, string handle)
{
    // Create update to describe the incoming call and caller
    var update = new CXCallUpdate ();
    update.RemoteHandle = new CXHandle (CXHandleType.Generic, handle);

    // Report incoming call to system
    Provider.ReportNewIncomingCall (uuid, update, (error) => {
        // Was the call accepted
        if (error == null) {
            // Yes, report to call manager
            CallManager.Calls.Add (new ActiveCall (uuid, handle, false));
        } else {
            // Report error to user here
            Console.WriteLine ("Error: {0}", error);
        }
    });
}

Este código crea una nueva instancia de CXCallUpdate y adjunta un identificador a ella que identificará al autor de la llamada. A continuación, usa el método ReportNewIncomingCall de la clase CXProvider para informar al sistema de la llamada. Si se ejecuta correctamente, la llamada se agrega a la colección de llamadas activas de la aplicación, si no es así, el error debe notificarse al usuario.

Responder a la llamada entrante por parte del usuario

Si el usuario quiere responder a la llamada VOIP entrante, se produce lo siguiente:

El usuario responde a la llamada VOIP entrante

  1. La interfaz de usuario del sistema informa al sistema de que el usuario quiere responder a la llamada VOIP.
  2. El sistema envía un CXAnswerCallAction al CXProvider de la aplicación para informarle de la intención de respuesta.
  3. La aplicación informa a su red de comunicación de que el usuario responde a la llamada y la llamada VOIP continúa como de costumbre.

Por ejemplo, en el CXProviderDelegate:

public override void PerformAnswerCallAction (CXProvider provider, CXAnswerCallAction action)
{
    // Find requested call
    var call = CallManager.FindCall (action.CallUuid);

    // Found?
    if (call == null) {
        // No, inform system and exit
        action.Fail ();
        return;
    }

    // Attempt to answer call
    call.AnswerCall ((successful) => {
        // Was the call successfully answered?
        if (successful) {
            // Yes, inform system
            action.Fulfill ();
        } else {
            // No, inform system
            action.Fail ();
        }
    });
}

En primer lugar, este código busca la llamada dada en su lista de llamadas activas. Si no se encuentra la llamada, se notifica al sistema y se cierra el método. Si se encuentra, se llama al método AnswerCall de la clase ActiveCall para iniciar la llamada y el sistema es información si se ejecuta correctamente o se produce un error.

Finalizar a la llamada entrante por parte del usuario

Si el usuario desea finalizar la llamada desde la interfaz de usuario de la aplicación, se produce lo siguiente:

El usuario finaliza la llamada desde la interfaz de usuario de la aplicación

  1. La aplicación crea CXEndCallAction que se agrupa en un CXTransaction que se envía al sistema para informarle de que la llamada finaliza.
  2. El sistema comprueba la intención de llamada final y envía el CXEndCallAction de nuevo a la aplicación a través de la CXProvider.
  3. A continuación, la aplicación informa a su red de comunicación de que la llamada finaliza.

Por ejemplo, en el CXProviderDelegate:

public override void PerformEndCallAction (CXProvider provider, CXEndCallAction action)
{
    // Find requested call
    var call = CallManager.FindCall (action.CallUuid);

    // Found?
    if (call == null) {
        // No, inform system and exit
        action.Fail ();
        return;
    }

    // Attempt to answer call
    call.EndCall ((successful) => {
        // Was the call successfully answered?
        if (successful) {
            // Remove call from manager's queue
            CallManager.Calls.Remove (call);

            // Yes, inform system
            action.Fulfill ();
        } else {
            // No, inform system
            action.Fail ();
        }
    });
}

En primer lugar, este código busca la llamada dada en su lista de llamadas activas. Si no se encuentra la llamada, se notifica al sistema y se cierra el método. Si se encuentra, se llama al método EndCall de la clase ActiveCall para finalizar la llamada y el sistema es información si se ejecuta correctamente o se produce un error. Si se ejecuta correctamente, la llamada se quita de la colección de llamadas activas.

Administración de varias llamadas

La mayoría de las aplicaciones VOIP pueden controlar varias llamadas a la vez. Por ejemplo, si actualmente hay una llamada VOIP activa y la aplicación recibe una notificación de que hay una nueva llamada entrante, el usuario puede pausar o bloquear la primera llamada para responder a la segunda.

En la situación anterior, el sistema enviará un CXTransaction a la aplicación que incluirá una lista de varias acciones (como el CXEndCallAction y el CXAnswerCallAction). Todas estas acciones deberán cumplirse individualmente para que el sistema pueda actualizar la interfaz de usuario de forma adecuada.

Controlar llamadas salientes

Si el usuario pulsa una entrada de la lista Recientes (en la aplicación del teléfono), por ejemplo, que procede de una llamada que pertenece a la aplicación, el sistema enviará un Intención de iniciar llamada:

Recepción de una intención de llamada de inicio

  1. La aplicación creará un Acción de iniciar llamada en función de la intención iniciar llamada que recibió del sistema.
  2. La aplicación usará el CXCallController para solicitar la Acción de iniciar llamada desde el sistema.
  3. Si el sistema acepta la acción, se devolverá a la aplicación a través del delegado XCProvider.
  4. La aplicación inicia la llamada saliente con su red de comunicación.

Para obtener más información sobre las intenciones, consulte nuestra documentación de Intenciones y extensiones de UI sobre intenciones.

Ciclo de vida de la llamada saliente

Al trabajar con CallKit y una llamada saliente, la aplicación deberá informar al sistema de los siguientes eventos de ciclo de vida:

  1. Iniciando: informar al sistema de que una llamada saliente está a punto de iniciarse.
  2. Iniciado: informar al sistema de que se ha iniciado una llamada saliente.
  3. Conectando: informar al sistema de que la llamada saliente se está conectando.
  4. Conectado: informar que la llamada saliente está conectada y que ambas partes pueden hablar ahora.

Por ejemplo, el código siguiente iniciará una llamada saliente:

private CXCallController CallController = new CXCallController ();
...

private void SendTransactionRequest (CXTransaction transaction)
{
    // Send request to call controller
    CallController.RequestTransaction (transaction, (error) => {
        // Was there an error?
        if (error == null) {
            // No, report success
            Console.WriteLine ("Transaction request sent successfully.");
        } else {
            // Yes, report error
            Console.WriteLine ("Error requesting transaction: {0}", error);
        }
    });
}

public void StartCall (string contact)
{
    // Build call action
    var handle = new CXHandle (CXHandleType.Generic, contact);
    var startCallAction = new CXStartCallAction (new NSUuid (), handle);

    // Create transaction
    var transaction = new CXTransaction (startCallAction);

    // Inform system of call request
    SendTransactionRequest (transaction);
}

Crea un CXHandle y lo usa para configurar un CXStartCallAction que se agrupa en un CXTransaction que se envía al sistema mediante el método RequestTransaction de la clase CXCallController. Al llamar al método RequestTransaction, el sistema puede realizar cualquier llamada existente en espera, independientemente del origen (aplicación de teléfono, FaceTime, VOIP, etc.), antes de que se inicie la nueva llamada.

La solicitud para iniciar una llamada VOIP saliente puede provenir de varios orígenes diferentes, como Siri, una entrada en una tarjeta de contacto (en la aplicación Contactos) o de la lista de recientes (en la aplicación del teléfono). En estas situaciones, la aplicación se enviará una intención de llamada de inicio dentro de un NSUserActivity y AppDelegate tendrá que controlarla:

public override bool ContinueUserActivity (UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
    var handle = StartCallRequest.CallHandleFromActivity (userActivity);

    // Found?
    if (handle == null) {
        // No, report to system
        Console.WriteLine ("Unable to get call handle from User Activity: {0}", userActivity);
        return false;
    } else {
        // Yes, start call and inform system
        CallManager.StartCall (handle);
        return true;
    }
}

Aquí se usa el método CallHandleFromActivity de la clase auxiliar StartCallRequest para obtener el identificador de la persona a la que se llama (consulte la Clase StartCallRequest de antes).

El método PerformStartCallAction de la Clase ProviderDelegate se usa para iniciar finalmente la llamada saliente real e informar al sistema de su ciclo de vida:

public override void PerformStartCallAction (CXProvider provider, CXStartCallAction action)
{
    // Create new call record
    var activeCall = new ActiveCall (action.CallUuid, action.CallHandle.Value, true);

    // Monitor state changes
    activeCall.StartingConnectionChanged += (call) => {
        if (call.IsConnecting) {
            // Inform system that the call is starting
            Provider.ReportConnectingOutgoingCall (call.UUID, call.StartedConnectingOn.ToNSDate());
        }
    };

    activeCall.ConnectedChanged += (call) => {
        if (call.IsConnected) {
            // Inform system that the call has connected
            Provider.ReportConnectedOutgoingCall (call.UUID, call.ConnectedOn.ToNSDate ());
        }
    };

    // Start call
    activeCall.StartCall ((successful) => {
        // Was the call able to be started?
        if (successful) {
            // Yes, inform the system
            action.Fulfill ();

            // Add call to manager
            CallManager.Calls.Add (activeCall);
        } else {
            // No, inform system
            action.Fail ();
        }
    });
}

Crea una instancia de la clase ActiveCall (para contener información sobre la llamada en curso) y se rellena con la persona a la que se llama. Los eventos StartingConnectionChanged y ConnectedChanged se usan para supervisar y notificar el ciclo de vida de las llamadas salientes. La llamada se inicia y el sistema informó de que se cumplió la acción.

Finalizar una llamada saliente

Cuando el usuario haya terminado con una llamada saliente y desee finalizarla, se puede usar el código siguiente:

private CXCallController CallController = new CXCallController ();
...

private void SendTransactionRequest (CXTransaction transaction)
{
    // Send request to call controller
    CallController.RequestTransaction (transaction, (error) => {
        // Was there an error?
        if (error == null) {
            // No, report success
            Console.WriteLine ("Transaction request sent successfully.");
        } else {
            // Yes, report error
            Console.WriteLine ("Error requesting transaction: {0}", error);
        }
    });
}

public void EndCall (ActiveCall call)
{
    // Build action
    var endCallAction = new CXEndCallAction (call.UUID);

    // Create transaction
    var transaction = new CXTransaction (endCallAction);

    // Inform system of call request
    SendTransactionRequest (transaction);
}

Si crea un CXEndCallAction con el UUID de la llamada al final, lo agrupa en un CXTransaction que se envía al sistema mediante el método RequestTransaction de la clase CXCallController.

Detalles adicionales de CallKit

En esta sección se tratarán algunos detalles adicionales que el desarrollador tendrá que tener en cuenta al trabajar con CallKit, como:

  • Configuración del proveedor
  • Errores de acción
  • Restricciones del sistema
  • Audio VOIP

Configuración del proveedor

La configuración del proveedor permite que una aplicación VOIP de iOS 10 personalice la experiencia del usuario (dentro de la interfaz de usuario nativa en llamada) al trabajar con CallKit.

Una aplicación puede realizar los siguientes tipos de personalizaciones:

  • Mostrar un nombre localizado.
  • Habilitar la compatibilidad con llamadas de vídeo.
  • Personalizar los botones de la interfaz de usuario en llamada mediante la presentación de su propio icono de imagen de plantilla. La interacción del usuario con botones personalizados se envía directamente a la aplicación que se va a procesar.

Errores de acción

Las aplicaciones VOIP de iOS 10 que usan CallKit deben controlar acciones con errores correctamente y mantener al usuario informado del estado acción en todo momento.

Tenga en cuenta el ejemplo siguiente:

  1. La aplicación ha recibido una acción de iniciar llamada y ha comenzado el proceso de inicializar una nueva llamada VOIP con su red de comunicación.
  2. Debido a una capacidad limitada o la ausencia de comunicación de red, se produce un error en esta conexión.
  3. La aplicación debe enviar el mensaje Error a la acción de iniciar llamada (Action.Fail()) para informar al sistema del error.
  4. Esto permite al sistema informar al usuario del estado de la llamada. Por ejemplo, para mostrar la interfaz de usuario de error de llamada.

Además, una aplicación VOIP de iOS 10 tendrá que responder a errores de tiempo de espera que pueden producirse cuando una acción esperada no se puede procesar dentro de un período de tiempo determinado. Cada tipo de acción proporcionado por CallKit tiene un valor de tiempo de espera máximo asociado. Estos valores de tiempo de espera garantizan que cualquier acción de CallKit solicitada por el usuario se controle de forma dinámica, manteniendo así el fluido del sistema operativo y con capacidad de respuesta.

Hay varios métodos en el delegado de proveedor (CXProviderDelegate) que deben invalidarse para controlar correctamente esta situación de tiempo de espera.

Restricciones del sistema

En función del estado actual del dispositivo iOS que ejecuta la aplicación VOIP de iOS 10, se pueden aplicar ciertas restricciones del sistema.

Por ejemplo, el sistema puede restringir una llamada VOIP entrante si:

  1. La persona que llama está en la lista de contactos bloqueados del usuario.
  2. El dispositivo iOS del usuario está en el modo No molestar.

Si una llamada VOIP está restringida por cualquiera de estas situaciones, use el código siguiente para controlarla:

public class ProviderDelegate : CXProviderDelegate
{
...

    public void ReportIncomingCall (NSUuid uuid, string handle)
    {
        // Create update to describe the incoming call and caller
        var update = new CXCallUpdate ();
        update.RemoteHandle = new CXHandle (CXHandleType.Generic, handle);

        // Report incoming call to system
        Provider.ReportNewIncomingCall (uuid, update, (error) => {
            // Was the call accepted
            if (error == null) {
                // Yes, report to call manager
                CallManager.Calls.Add (new ActiveCall (uuid, handle, false));
            } else {
                // Report error to user here
                if (error.Code == (int)CXErrorCodeIncomingCallError.CallUuidAlreadyExists) {
                    // Handle duplicate call ID
                } else if (error.Code == (int)CXErrorCodeIncomingCallError.FilteredByBlockList) {
                    // Handle call from blocked user
                } else if (error.Code == (int)CXErrorCodeIncomingCallError.FilteredByDoNotDisturb) {
                    // Handle call while in do-not-disturb mode
                } else {
                    // Handle unknown error
                }
            }
        });
    }

}

Audio VOIP

CallKit proporciona varias ventajas para controlar los recursos de audio que requerirá una aplicación VOIP de iOS 10 durante una llamada VOIP en directo. Una de las mayores ventajas es que la sesión de audio de la aplicación tendrá prioridades elevadas al ejecutarse en iOS 10. Este es el mismo nivel de prioridad que las aplicaciones integradas de Teléfono y FaceTime y este nivel de prioridad mejorado impedirá que otras aplicaciones en ejecución interrumpan la sesión de audio de la aplicación VOIP.

Además, CallKit tiene acceso a otras sugerencias de enrutamiento de audio que pueden mejorar el rendimiento y enrutar de forma inteligente el audio VOIP a dispositivos de salida específicos durante una llamada en directo en función de las preferencias del usuario y los estados del dispositivo. Por ejemplo, en función de dispositivos conectados, como auriculares bluetooth, una conexión CarPlay dinámica o una configuración de accesibilidad.

Durante el ciclo de vida de una llamada VOIP típica mediante CallKit, la aplicación tendrá que configurar la secuencia de audio que CallKit proporcionará. Eche un vistazo al ejemplo siguiente:

Secuencia de acción iniciar llamada

  1. La aplicación recibe una acción de iniciar llamada para responder a una llamada entrante.
  2. Antes de que la aplicación cumpla esta acción, proporciona la configuración necesaria para su AVAudioSession.
  3. La aplicación informa al sistema de que se ha cumplido la acción.
  4. Antes de que se conecte la llamada, CallKit proporciona una AVAudioSession de alta prioridad que coincide con la configuración solicitada por la aplicación. La aplicación se notificará a través del método DidActivateAudioSession de su CXProviderDelegate.

Trabajar con extensiones de directorio de llamadas

Al trabajar con CallKit, Extensiones de directorio de llamadas proporcionar una manera de agregar números de llamada bloqueados e identificar números específicos de una aplicación VOIP determinada a los contactos de la aplicación Contact en el dispositivo iOS.

Implementación de una extensión de directorio de llamadas

Para implementar una extensión de directorio de llamadas en una aplicación de Xamarin.iOS, haga lo siguiente:

  1. Abra la solución de la aplicación en Visual Studio para Mac.

  2. Haga clic con el botón derecho en el nombre de la solución en el Explorador de soluciones y seleccione Agregar>Agregar nuevo proyecto.

  3. Seleccione iOS>Extensiones>Extensiones de directorio de llamadas y haga clic en el botón Siguiente:

    Creación de una nueva extensión de directorio de llamadas

  4. Escriba un nombre para la extensión y haga clic en el botón Siguiente:

    Escribir un nombre para la extensión

  5. Ajuste el Nombre de proyecto o Nombre de la solución si es necesario y haga clic en el botón Crear:

    Creación del proyecto

Esto agregará una clase CallDirectoryHandler.cs al proyecto similar a la siguiente:

using System;

using Foundation;
using CallKit;

namespace MonkeyCallDirExtension
{
    [Register ("CallDirectoryHandler")]
    public class CallDirectoryHandler : CXCallDirectoryProvider, ICXCallDirectoryExtensionContextDelegate
    {
        #region Constructors
        protected CallDirectoryHandler (IntPtr handle) : base (handle)
        {
            // Note: this .ctor should not contain any initialization logic.
        }
        #endregion

        #region Override Methods
        public override void BeginRequest (CXCallDirectoryExtensionContext context)
        {
            context.Delegate = this;

            if (!AddBlockingPhoneNumbers (context)) {
                Console.WriteLine ("Unable to add blocking phone numbers");
                var error = new NSError (new NSString ("CallDirectoryHandler"), 1, null);
                context.CancelRequest (error);
                return;
            }

            if (!AddIdentificationPhoneNumbers (context)) {
                Console.WriteLine ("Unable to add identification phone numbers");
                var error = new NSError (new NSString ("CallDirectoryHandler"), 2, null);
                context.CancelRequest (error);
                return;
            }

            context.CompleteRequest (null);
        }
        #endregion

        #region Private Methods
        private bool AddBlockingPhoneNumbers (CXCallDirectoryExtensionContext context)
        {
            // Retrieve phone numbers to block from data store. For optimal performance and memory usage when there are many phone numbers,
            // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
            //
            // Numbers must be provided in numerically ascending order.

            long [] phoneNumbers = { 14085555555, 18005555555 };

            foreach (var phoneNumber in phoneNumbers)
                context.AddBlockingEntry (phoneNumber);

            return true;
        }

        private bool AddIdentificationPhoneNumbers (CXCallDirectoryExtensionContext context)
        {
            // Retrieve phone numbers to identify and their identification labels from data store. For optimal performance and memory usage when there are many phone numbers,
            // consider only loading a subset of numbers at a given time and using autorelease pool(s) to release objects allocated during each batch of numbers which are loaded.
            //
            // Numbers must be provided in numerically ascending order.

            long [] phoneNumbers = { 18775555555, 18885555555 };
            string [] labels = { "Telemarketer", "Local business" };

            for (var i = 0; i < phoneNumbers.Length; i++) {
                long phoneNumber = phoneNumbers [i];
                string label = labels [i];
                context.AddIdentificationEntry (phoneNumber, label);
            }

            return true;
        }
        #endregion

        #region Public Methods
        public void RequestFailed (CXCallDirectoryExtensionContext extensionContext, NSError error)
        {
            // An error occurred while adding blocking or identification entries, check the NSError for details.
            // For Call Directory error codes, see the CXErrorCodeCallDirectoryManagerError enum.
            //
            // This may be used to store the error details in a location accessible by the extension's containing app, so that the
            // app may be notified about errors which occurred while loading data even if the request to load data was initiated by
            // the user in Settings instead of via the app itself.
        }
        #endregion
    }
}

El método BeginRequest del controlador de directorio de llamadas deberá modificarse para proporcionar la funcionalidad necesaria. En el caso del ejemplo anterior, intenta establecer la lista de números bloqueados y disponibles en la base de datos de contactos de la aplicación VOIP. Si se produce un error en cualquiera de las solicitudes por cualquier motivo, cree un NSError para describir el error y páselo el método CancelRequest de la clase CXCallDirectoryExtensionContext.

Para establecer los números bloqueados, use el método AddBlockingEntry de la clase CXCallDirectoryExtensionContext. Los números proporcionados al método deben estar en orden ascendente numéricamente. Para optimizar el rendimiento y el uso de memoria cuando hay muchos números de teléfono, considere la posibilidad de cargar solo un subconjunto de números en un momento dado y usar grupos autoliberados para liberar objetos asignados durante cada lote de números que se cargan.

Para informar a la aplicación Contact de los números de contacto conocidos para la aplicación VOIP, use el método AddIdentificationEntry de la clase CXCallDirectoryExtensionContext y proporcione tanto el número como una etiqueta de identificación. De nuevo, los números proporcionados al método deben estar en orden ascendente numéricamente. Para optimizar el rendimiento y el uso de memoria cuando hay muchos números de teléfono, considere la posibilidad de cargar solo un subconjunto de números en un momento dado y usar grupos autoliberados para liberar objetos asignados durante cada lote de números que se cargan.

Resumen

En este artículo se ha tratado la nueva API CallKit que Apple publicó en iOS 10 y cómo implementarla en aplicaciones VOIP de Xamarin.iOS. Ha mostrado cómo CallKit permite que una aplicación se integre en el sistema iOS, cómo proporciona paridad de características con aplicaciones integradas (como teléfono) y cómo aumenta la visibilidad de una aplicación en ubicaciones como bloqueos y pantallas domésticas, a través de interacciones de Siri y a través de las aplicaciones contactos.