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:
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:
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
:
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
:
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:
- La aplicación recibe una notificación de su red de comunicaciones que hay una llamada VOIP entrante.
- La aplicación usa el
CXProvider
para enviar unCXCallUpdate
al sistema para informarle de la llamada. - 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:
- La interfaz de usuario del sistema informa al sistema de que el usuario quiere responder a la llamada VOIP.
- El sistema envía un
CXAnswerCallAction
alCXProvider
de la aplicación para informarle de la intención de respuesta. - 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:
- La aplicación crea
CXEndCallAction
que se agrupa en unCXTransaction
que se envía al sistema para informarle de que la llamada finaliza. - El sistema comprueba la intención de llamada final y envía el
CXEndCallAction
de nuevo a la aplicación a través de laCXProvider
. - 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:
- La aplicación creará un Acción de iniciar llamada en función de la intención iniciar llamada que recibió del sistema.
- La aplicación usará el
CXCallController
para solicitar la Acción de iniciar llamada desde el sistema. - Si el sistema acepta la acción, se devolverá a la aplicación a través del delegado
XCProvider
. - 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:
- Iniciando: informar al sistema de que una llamada saliente está a punto de iniciarse.
- Iniciado: informar al sistema de que se ha iniciado una llamada saliente.
- Conectando: informar al sistema de que la llamada saliente se está conectando.
- 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:
- 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.
- Debido a una capacidad limitada o la ausencia de comunicación de red, se produce un error en esta conexión.
- La aplicación debe enviar el mensaje Error a la acción de iniciar llamada (
Action.Fail()
) para informar al sistema del error. - 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:
- La persona que llama está en la lista de contactos bloqueados del usuario.
- 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:
- La aplicación recibe una acción de iniciar llamada para responder a una llamada entrante.
- Antes de que la aplicación cumpla esta acción, proporciona la configuración necesaria para su
AVAudioSession
. - La aplicación informa al sistema de que se ha cumplido la acción.
- 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étodoDidActivateAudioSession
de suCXProviderDelegate
.
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:
Abra la solución de la aplicación en Visual Studio para Mac.
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.
Seleccione iOS>Extensiones>Extensiones de directorio de llamadas y haga clic en el botón Siguiente:
Escriba un nombre para la extensión y haga clic en el botón Siguiente:
Ajuste el Nombre de proyecto o Nombre de la solución si es necesario y haga clic en el botón Crear:
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.