CallKit en Xamarin.iOS

La nueva API de CallKit en iOS 10 proporciona una manera de que las aplicaciones VOIP se integren con la interfaz de usuario de iPhone y proporcionen una interfaz y experiencia familiares 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 los contactos mediante las vistas Favoritos y Recientes de la aplicación Teléfono.

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 primera entidad en iOS 10. CallKit API 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 Teléfono integrada, un usuario puede ver e interactuar con llamadas VOIP desde la pantalla de bloqueo del dispositivo iOS y administrar contactos mediante las vistas Favoritos y Recientes de la aplicación 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 autor de llamada) o decir al sistema cuándo se debe bloquear un número (bloqueo de llamadas).

La experiencia de la aplicación VOIP existente

Antes de analizar la nueva API de CallKit y sus capacidades, echa un vistazo a la experiencia del usuario actual con una aplicación VOIP de terceros en iOS 9 (y menos) mediante una aplicación VOIP ficticia denominada 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 no se puede diferenciar de cualquier otro tipo de notificación (como las de las aplicaciones Mensajes o Correo, por ejemplo).

Si el usuario quisiera responder a la llamada, tendría que deslizar la notificación de 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. Puesto que la notificación es temporal, el usuario puede perderla fácilmente forzándola a abrir el Centro de notificaciones y encontrar la notificación específica para responder y, a continuación, llamar a o buscar e iniciar manualmente la aplicación MonkeyCall.

La experiencia de aplicación VOIP de 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 en gran medida en iOS 10. Tome el ejemplo del usuario que recibe una llamada VOIP cuando su teléfono está bloqueado desde arriba. Al implementar CallKit, la llamada aparecerá en la pantalla bloqueo de iPhone, igual que lo haría si la llamada se recibiera desde la aplicación integrada de Teléfono, con la pantalla completa, la interfaz de usuario nativa y la funcionalidad estándar de deslizar rápidamente para responder.

De nuevo, si el iPhone se desbloquea cuando se recibe una llamada VOIP de MonkeyCall, se presenta la misma interfaz de usuario nativa de pantalla completa y la funcionalidad estándar de deslizar rápidamente para responder y pulsar para rechazar de la aplicación integrada de Teléfono y MonkeyCall tiene la opción de reproducir un toque 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 insertar y bloquear, iniciar llamadas de MonkeyCall desde Siri y ofrece a los usuarios la posibilidad de asignar llamadas de MonkeyCall a personas de la aplicación Contactos.

En las secciones siguientes se explica detalladamente la arquitectura de CallKit, los flujos de llamadas entrantes y salientes y la API de CallKit.

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 para la interfaz de usuario del sistema a través de CallKit. En el ejemplo siguiente, dado que MonkeyCall adopta CallKit, el sistema lo conoce de la misma manera que estos servicios del sistema integrados y obtiene todas las mismas características:

Pila del servicio CallKit

Echa un vistazo más de cerca 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. Se vincula en CallKit para comunicarse con el sistema:

Arquitectura de aplicaciones de MonkeyCall

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

  • 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 las acciones del usuario local.

The CXProvider

Como se indicó anteriormente, permite que una aplicación informe al sistema de las notificaciones fuera de CXProvider banda que puedan producirse. Se trata de notificaciones que no se producen debido a acciones del usuario local, pero que se producen debido a eventos externos como llamadas entrantes.

Una aplicación debe usar para CXProvider lo siguiente:

  • Notificar una llamada entrante al sistema.
  • Informe de que la llamada saliente se ha conectado al sistema.
  • Informe al usuario remoto que finaliza la llamada al sistema.

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

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

The CXCallController

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

Una aplicación debe usar para CXCallController lo siguiente:

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

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

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, en este documento se va a usar código de la aplicación ficticia MonoCall VOIP. El código que se presenta aquí representa varias clases de soporte, 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 para contener toda la información sobre una llamada VOIP que está activa actualmente de ActiveCall 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. Puesto que este es solo un ejemplo, hay tres métodos que se usan para simular el inicio, la respuesta y el final de una llamada.

Clase StartCallRequest

La StartCallRequest clase estática 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 CallHandleFromURL clases y se usan en AppDelegate para obtener el identificador de contacto de la persona a la que se llama CallHandleFromActivity 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 ActiveCallManager clase 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
    }
}

Una vez más, puesto que se trata solo de una simulación, el único mantiene una colección de objetos y tiene una rutina para buscar una llamada determinada ActiveCallManagerActiveCall 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 ha descrito anteriormente, proporciona una comunicación bidireccional entre la aplicación y el sistema para notificaciones CXProvider fuera de banda. El desarrollador debe proporcionar un personalizado y adjuntarlo a para que la aplicación controle los CXProviderDelegateCXProvider eventos de CallKit fuera de banda. MonkeyCall usa lo CXProviderDelegate siguiente:

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 que ActiveCallManager usará para controlar cualquier actividad de llamada. A continuación, define los tipos de identificador ( CXHandleType ) a los que CXProvider responderá:

// 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 haya una llamada en curso:

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

Estos valores se agrupan en CXProviderConfiguration un que se usará para configurar 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 un nuevo con CXProvider estas configuraciones y se asocia a él:

// 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 tratase de una aplicación real, el método se usaría para iniciar la llamada con un configurado previamente DidActivateAudioSessionAVAudioSession que el sistema proporcionaba:

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

También usaría el método para finalizar y liberar su conexión a la DidDeactivateAudioSession 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 trata en detalle en las secciones siguientes.

La clase AppDelegate

MonkeyCall usa AppDelegate para contener instancias de y ActiveCallManagerCXProviderDelegate 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 OpenUrlContinueUserActivity métodos de invalidación y se usan cuando la aplicación está procesando 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 por los que una llamada VOIP entrante puede pasar durante un flujo de trabajo de llamada entrante típico, como:

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

Las secciones siguientes echarán un vistazo detallado a cómo una aplicación puede usar CallKit para controlar el flujo de trabajo de llamadas entrantes, usando de nuevo la aplicación VoIP de MonkeyCall como ejemplo.

Informar al usuario de la llamada entrante

Cuando un usuario remoto ha iniciado una conversación voIP con el usuario local, ocurre lo siguiente:

Un usuario remoto ha iniciado una conversación voIP

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

Por ejemplo, en 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 CXCallUpdate instancia de y le adjunta un identificador que identificará al autor de la llamada. A continuación, usa ReportNewIncomingCall el método de la clase para informar al sistema de la CXProvider llamada. Si se realiza correctamente, la llamada se agrega a la colección de llamadas activas de la aplicación; si no es así, el error debe informarse al usuario.

Usuario que responde a la llamada entrante

Si el usuario desea responder a la llamada VOIP entrante, ocurre 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 a la aplicación para informarle de CXAnswerCallActionCXProvider 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 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 ();
        }
    });
}

Este código busca primero 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 de la clase para iniciar la llamada y system es información si se realiza AnswerCallActiveCall correctamente o se produce un error.

Llamada entrante final del usuario

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

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

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

Por ejemplo, en 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 ();
        }
    });
}

Este código busca primero 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 de la clase para finalizar la llamada y system es información si se realiza EndCallActiveCall correctamente o se produce un error. Si se realiza 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 detener la primera llamada para responder a la segunda.

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

Control de llamadas salientes

Si el usuario pulsa una entrada de la lista Recientes (en la aplicación Teléfono), por ejemplo, que es de una llamada que pertenece a la aplicación, el sistema enviará una intención de llamada inicial:

Recibir una intención de llamada de inicio

  1. La aplicación creará una acción iniciar llamada basada en la intención iniciar llamada que recibió del sistema.
  2. La aplicación usará para CXCallController solicitar la acción Iniciar llamada del sistema.
  3. Si el sistema acepta la acción, se devolverá a la aplicación a través del XCProvider delegado .
  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 intents and intents ui extensions (Extensiones de la interfaz de usuario de intenciones y 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 del ciclo de vida:

  1. Iniciar: informa al sistema de que una llamada saliente está a punto de iniciarse.
  2. Iniciado: informa al sistema de que se ha iniciado una llamada saliente.
  3. Conectar: informa al sistema de que la llamada saliente se está conectando.
  4. Conectado: informa de que la llamada saliente se ha conectado 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 objeto y lo usa para configurar un que se agrupa en un que se envía al sistema mediante CXHandle el método de la clase CXStartCallActionCXTransactionRequestTransactionCXCallController . Al llamar al método , el sistema puede colocar las llamadas existentes en espera, independientemente del origen RequestTransaction (aplicación Teléfono, FaceTime, VOIP, etc.), antes de que se inicie la nueva llamada.

La solicitud para iniciar una llamada VOIP saliente puede procede de varios orígenes diferentes, como Siri, una entrada en una tarjeta Contacto (en la aplicación Contactos) o de la lista Recientes (en la aplicación Teléfono). En estas situaciones, se enviará a la aplicación una intención de llamada de inicio dentro de un y NSUserActivity 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 de la clase auxiliar para obtener el identificador de la persona a la que se llama CallHandleFromActivityStartCallRequest (consulte CallHandleFromActivity anterior).

El PerformStartCallAction método de la clase PerformStartCallAction 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 (para contener información sobre la llamada en curso) y se rellena ActiveCall con la persona a la que se llama. Los StartingConnectionChanged eventos y se usan para supervisar e informar del ciclo de vida de las llamadas ConnectedChanged salientes. Se inicia la llamada y el sistema informa de que se ha cumplido la acción.

Finalización de una llamada saliente

Cuando el usuario ha terminado con una llamada saliente y desea 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 objeto con el UUID de la llamada a end, lo agrupa en un objeto que se envía al sistema mediante el CXEndCallAction método de la clase CXTransactionRequestTransactionCXCallController .

Detalles adicionales de CallKit

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

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

Configuración de 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 In-Call nativa) al trabajar con CallKit.

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

  • Mostrar un nombre localizado.
  • Habilite la compatibilidad con videollamadas.
  • Personalice los botones de la interfaz In-Call usuario 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 las acciones que no se pueden realizar correctamente y mantener al usuario informado del estado de acción en todo momento.

Tome en cuenta el ejemplo siguiente:

  1. La aplicación ha recibido una acción iniciar llamada y ha iniciado el proceso de inicialización de una nueva llamada VOIP con su red de comunicación.
  2. Debido a una funcionalidad de comunicación de red limitada o ninguna, se produce un error en esta conexión.
  3. La aplicación debe devolver el mensaje de conmutación por error a la acción iniciar llamada ( ) para informar al sistema del error.
  4. Esto permite que el sistema informe 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 deberá responder a los errores de tiempo de espera que pueden producirse cuando una acción esperada no se puede procesar en 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, lo que mantiene el sistema operativo fluido y también responde.

Hay varios métodos en el delegado de proveedor ( ) que también se deben invalidar para controlar correctamente estas situaciones de CXProviderDelegate 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 llamadores bloqueados del usuario.
  2. El dispositivo iOS del usuario está en el modo No interrumpir.

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 necesitará 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 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 de CarPlay activa o una configuración de accesibilidad.

Durante el ciclo de vida de una llamada VOIP típica mediante CallKit, la aplicación deberá configurar la secuencia de audio que CallKit le proporcionará. Vea el ejemplo siguiente:

Secuencia de acción iniciar llamada

  1. La aplicación recibe una acción de llamada de inicio 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 prioridad alta que coincide con AVAudioSession la configuración solicitada por la aplicación. Se notificará a la aplicación a través DidActivateAudioSession del método de su CXProviderDelegate .

Trabajar con extensiones de directorio de llamadas

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

Implementación de una extensión de directorio call

Para implementar una extensión de directorio de llamadas en una aplicación 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 Explorador de soluciones seleccione Agregaragregar nuevo Project.

  3. Seleccione Extensiones de iOSLlamar a extensiones de directorio 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 Project nombre onombre de la solución si es necesario y haga clic en el botón Crear:

    Creación del proyecto

Esto agregará una CallDirectoryHandler.cs clase al proyecto con un aspecto similar al 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 BeginRequest método del controlador de directorios 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 alguna de las solicitudes produce un error por cualquier motivo, cree para NSError describir el error y pasarlo al método de la clase CancelRequestCXCallDirectoryExtensionContext .

Para establecer los números bloqueados, use AddBlockingEntry el método de la clase CXCallDirectoryExtensionContext . Los números proporcionados al método deben estar en orden ascendente numéricamente. Para obtener un rendimiento óptimo y un 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 de entrega automática 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 de la clase y proporcione el número y una AddIdentificationEntryCXCallDirectoryExtensionContext etiqueta de identificación. De nuevo, los números proporcionados al método deben estar en orden numéricamente ascendente. Para obtener un rendimiento óptimo y un 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 de entrega automática para liberar objetos asignados durante cada lote de números que se cargan.

Resumen

En este artículo se ha abordado la nueva API de 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 iOS en ubicaciones como bloqueo y pantallas de inicio, a través de interacciones de Siri y a través de las aplicaciones Contactos.