CallKit no Xamarin.iOS
A nova API do CallKit no iOS 10 fornece uma maneira para os aplicativos VOIP se integrarem à interface do usuário do iPhone e fornecerem uma interface e experiência familiares para o usuário final. Com essa API, os usuários podem visualizar e interagir com chamadas VOIP a partir da tela de bloqueio do dispositivo iOS e gerenciar contatos usando as visualizações Favoritos e Recentes do aplicativo Telefone.
Sobre o CallKit
De acordo com a Apple, o CallKit é uma nova estrutura que elevará os aplicativos de Voz sobre IP (VOIP) de terceiros para uma experiência de 1ª parte no iOS 10. A API do CallKit permite que os aplicativos VOIP se integrem à interface do usuário do iPhone e forneçam uma interface e experiência familiares ao usuário final. Assim como o aplicativo Telefone integrado, um usuário pode visualizar e interagir com chamadas VOIP a partir da Tela de Bloqueio do dispositivo iOS e gerenciar contatos usando as visualizações Favoritos e Recentes do aplicativo Telefone.
Além disso, a API do CallKit fornece a capacidade de criar extensões de aplicativo que podem associar um número de telefone a um nome (ID do chamador) ou informar ao sistema quando um número deve ser bloqueado (bloqueio de chamadas).
A experiência de aplicativo VOIP existente
Antes de discutir a nova API do CallKit e suas habilidades, dê uma olhada na experiência atual do usuário com um aplicativo VOIP de terceiros no iOS 9 (e menor) usando um aplicativo VOIP fictício chamado MonkeyCall. MonkeyCall é um aplicativo simples que permite ao usuário enviar e receber chamadas VOIP usando as APIs iOS existentes.
Atualmente, se o usuário estiver recebendo uma chamada no MonkeyCall e seu iPhone estiver bloqueado, a notificação recebida na tela de bloqueio é indistinguível de qualquer outro tipo de notificação (como as dos aplicativos Mensagens ou Mail, por exemplo).
Se o usuário quisesse atender a chamada, ele teria que deslizar a notificação do MonkeyCall para abrir o aplicativo e inserir sua senha (ou ID de toque do usuário) para desbloquear o telefone antes de poder aceitar a chamada e iniciar a conversa.
A experiência é igualmente complicada se o telefone estiver desbloqueado. Novamente, a chamada MonkeyCall recebida é exibida como um banner de notificação padrão que desliza da parte superior da tela. Como a notificação é temporária, ela pode ser facilmente perdida pelo usuário, forçando-o a abrir a Central de Notificações e encontrar a notificação específica para atender e ligar ou localizar e iniciar o aplicativo MonkeyCall manualmente.
A experiência do aplicativo VOIP do CallKit
Ao implementar as novas APIs do CallKit no aplicativo MonkeyCall, a experiência do usuário com uma chamada VOIP recebida pode ser muito melhorada no iOS 10. Veja o exemplo do usuário que recebe uma chamada VOIP quando seu telefone está bloqueado de cima. Ao implementar o CallKit, a chamada aparecerá na tela de bloqueio do iPhone, assim como apareceria se a chamada estivesse sendo recebida do aplicativo de telefone integrado, com a tela cheia, a interface do usuário nativa e a funcionalidade padrão de deslizar para responder.
Novamente, se o iPhone for desbloqueado quando uma chamada VOIP do MonkeyCall for recebida, a mesma interface do usuário nativa em tela cheia e a funcionalidade padrão de deslizar para responder e tocar para recusar do aplicativo de telefone integrado será apresentada e o MonkeyCall terá a opção de reproduzir um toque personalizado.
O CallKit fornece funcionalidade adicional ao MonkeyCall, permitindo que suas chamadas VOIP interajam com outros tipos de chamadas, apareçam nas listas Recentes e Favoritos integradas, usem os recursos integrados Não Perturbe e Bloqueiem, iniciem chamadas MonkeyCall da Siri e ofereçam a capacidade de os usuários atribuírem chamadas MonkeyCall a pessoas no aplicativo Contatos.
As seções a seguir abordarão a arquitetura do CallKit, os fluxos de chamadas de entrada e saída e a API do CallKit em detalhes.
A arquitetura do CallKit
No iOS 10, a Apple adotou o CallKit em todos os Serviços do Sistema, de modo que as chamadas feitas no CarPlay, por exemplo, são conhecidas pela interface do sistema via CallKit. No exemplo abaixo, como o MonkeyCall adota o CallKit, ele é conhecido pelo Sistema da mesma forma que esses Serviços do Sistema internos e obtém todos os mesmos recursos:
Dê uma olhada mais de perto no aplicativo MonkeyCall no diagrama acima. O aplicativo contém todo o seu código para se comunicar com sua própria rede e contém suas próprias interfaces de usuário. Ele se conecta no CallKit para se comunicar com o sistema:
Há duas interfaces principais no CallKit que o aplicativo usa:
CXProvider
- Isso permite que o aplicativo MonkeyCall informe o sistema sobre quaisquer notificações fora de banda que possam ocorrer.CXCallController
- Permite que o aplicativo MonkeyCall informe o sistema das ações locais do usuário.
O CXProvider
Como dito acima, CXProvider
permite que um aplicativo informe o sistema de quaisquer notificações fora de banda que possam ocorrer. Essas são notificações que não ocorrem devido a ações do usuário local, mas ocorrem devido a eventos externos, como chamadas de entrada.
Um aplicativo deve usar o CXProvider
para o seguinte:
- Relate uma chamada de entrada para o Sistema.
- Relate uma chamada de saída conectada ao Sistema.
- Relate o usuário remoto encerrando a chamada para o Sistema.
Quando o aplicativo deseja se comunicar com o sistema, ele usa a CXCallUpdate
classe e quando o sistema precisa se comunicar com o aplicativo, ele usa a CXAction
classe:
O CXCallController
O CXCallController
permite que um aplicativo informe o sistema de ações do usuário local, como o usuário iniciar uma chamada VOIP. Ao implementar um CXCallController
o aplicativo consegue interagir com outros tipos de chamadas no sistema. Por exemplo, se já houver uma chamada de telefonia ativa em andamento, CXCallController
pode permitir que o aplicativo VOIP coloque essa chamada em espera e inicie ou atenda uma chamada VOIP.
Um aplicativo deve usar o CXCallController
para o seguinte:
- Relate quando o usuário iniciou uma chamada de saída para o Sistema.
- Relate quando o usuário atende uma chamada de entrada para o Sistema.
- Relate quando o usuário termina uma chamada para o Sistema.
Quando o aplicativo deseja comunicar ações de usuário local ao sistema, ele usa a CXTransaction
classe:
Implementando o CallKit
As seções a seguir mostrarão como implementar o CallKit em um aplicativo VOIP Xamarin.iOS. Por exemplo, este documento usará o código do aplicativo fictício MonkeyCall VOIP. O código apresentado aqui representa várias classes de suporte, as partes específicas do CallKit serão abordadas em detalhes nas seções a seguir.
A classe ActiveCall
A ActiveCall
classe é usada pelo aplicativo MonkeyCall para armazenar todas as informações sobre uma chamada VOIP que está ativa no momento da seguinte maneira:
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
Contém várias propriedades que definem o estado da chamada e dois eventos que podem ser gerados quando o estado da chamada é alterado. Como este é apenas um exemplo, existem três métodos usados para simular iniciar, atender e terminar uma chamada.
A classe StartCallRequest
A StartCallRequest
classe estática, fornece alguns métodos auxiliares que serão usados ao trabalhar com chamadas de saída:
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;
}
}
}
}
As CallHandleFromURL
classes e CallHandleFromActivity
são usadas no AppDelegate para obter o identificador de contato da pessoa que está sendo chamada em uma chamada de saída. Para obter mais informações, consulte a seção Manipulando chamadas de saída abaixo.
A classe ActiveCallManager
A ActiveCallManager
classe lida com todas as chamadas abertas no aplicativo 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
}
}
Novamente, como se trata apenas de uma simulação, o ActiveCallManager
único mantém uma coleção de ActiveCall
objetos e tem uma rotina para encontrar uma determinada chamada por sua UUID
propriedade. Ele também inclui métodos para iniciar, terminar e alterar o estado em espera de uma chamada de saída. Para obter mais informações, consulte a seção Manipulando chamadas de saída abaixo.
A classe ProviderDelegate
Conforme discutido acima, um fornece comunicação CXProvider
bidirecional entre o aplicativo e o sistema para notificações fora de banda. O desenvolvedor precisa fornecer um personalizado CXProviderDelegate
e anexá-lo ao para que CXProvider
o aplicativo manipule eventos CallKit fora de banda. MonkeyCall usa o seguinte 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
}
}
Quando uma instância desse delegado é criada, ela passa o ActiveCallManager
que ela usará para manipular qualquer atividade de chamada. Em seguida, ele define os tipos de identificador (CXHandleType
) aos quais responderão CXProvider
:
// Define handle types
var handleTypes = new [] { (NSNumber)(int)CXHandleType.PhoneNumber };
E ele obtém a imagem do modelo que será aplicada ao ícone do aplicativo quando uma chamada estiver em andamento:
// Get Image Template
var templateImage = UIImage.FromFile ("telephone_receiver.png");
Esses valores são agrupados em um CXProviderConfiguration
que será usado para configurar o CXProvider
:
// Setup the initial configurations
Configuration = new CXProviderConfiguration ("MonkeyCall") {
MaximumCallsPerCallGroup = 1,
SupportedHandleTypes = new NSSet<NSNumber> (handleTypes),
IconTemplateImageData = templateImage.AsPNG(),
RingtoneSound = "musicloop01.wav"
};
O delegado, em seguida, cria um novo CXProvider
com estas configurações e anexa-se a ele:
// Create a new provider
Provider = new CXProvider (Configuration);
// Attach this delegate
Provider.SetDelegate (this, null);
Ao usar o CallKit, o aplicativo não criará mais e manipulará suas próprias sessões de áudio, em vez disso, precisará configurar e usar uma sessão de áudio que o sistema criará e manipulará para ele.
Se este fosse um aplicativo real, o DidActivateAudioSession
método seria usado para iniciar a chamada com uma pré-configuração AVAudioSession
que o sistema forneceu:
public override void DidActivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
// Start the call's audio session here...
}
Ele também usaria o DidDeactivateAudioSession
método para finalizar e liberar sua conexão com a sessão de áudio fornecida pelo sistema:
public override void DidDeactivateAudioSession (CXProvider provider, AVFoundation.AVAudioSession audioSession)
{
// End the calls audio session and restart any non-call
// releated audio
}
O restante do código será abordado em detalhes nas seções a seguir.
A classe AppDelegate
MonkeyCall usa o AppDelegate para armazenar instâncias do ActiveCallManager
e CXProviderDelegate
que serão usadas em todo o aplicativo:
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
}
}
Os OpenUrl
métodos e ContinueUserActivity
de substituição são usados quando o aplicativo está processando uma chamada de saída. Para obter mais informações, consulte a seção Manipulando chamadas de saída abaixo.
Manipulando chamadas recebidas
Há vários estados e processos pelos quais uma chamada VOIP de entrada pode passar durante um fluxo de trabalho típico de chamada de entrada, como:
- Informar ao usuário (e ao Sistema) que existe uma chamada de entrada.
- Receber notificação quando o usuário quiser atender a chamada e inicializar a chamada com o outro usuário.
- Informe o Sistema e a Rede de Comunicação quando o usuário quiser encerrar a chamada atual.
As seções a seguir examinarão detalhadamente como um aplicativo pode usar o CallKit para lidar com o fluxo de trabalho de chamadas de entrada, novamente usando o aplicativo VOIP MonkeyCall como exemplo.
Informando o usuário sobre a chamada recebida
Quando um usuário remoto iniciou uma conversa VOIP com o usuário local, ocorre o seguinte:
- O aplicativo recebe uma notificação de sua Rede de Comunicações de que há uma chamada VOIP de entrada.
- O aplicativo usa o
CXProvider
para enviar umCXCallUpdate
para o sistema informando sobre a chamada. - O Sistema publica a chamada para a interface do usuário do sistema, Serviços do Sistema e quaisquer outros aplicativos VOIP usando o CallKit.
Por exemplo, no 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);
}
});
}
Esse código cria uma nova CXCallUpdate
instância e anexa um identificador a ela que identificará o chamador. Em seguida, ele usa o ReportNewIncomingCall
CXProvider
método da classe para informar o sistema da chamada. Se for bem-sucedida, a chamada é adicionada à coleção de chamadas ativas do aplicativo, se não for, o erro precisa ser relatado ao usuário.
Usuário atendendo chamada recebida
Se o usuário quiser atender a chamada VOIP de entrada, ocorrerá o seguinte:
- A interface do usuário do sistema informa ao sistema que o usuário deseja atender à chamada VOIP.
- O Sistema envia um
CXAnswerCallAction
para o aplicativoCXProvider
informando a Intenção de Resposta. - O aplicativo informa à sua Rede de Comunicação que o usuário está atendendo a chamada e a chamada VOIP prossegue normalmente.
Por exemplo, no 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 ();
}
});
}
Esse código primeiro procura a chamada fornecida em sua lista de chamadas ativas. Se a chamada não for encontrada, o sistema será notificado e o método será encerrado. Se for encontrado, o AnswerCall
ActiveCall
método da classe é chamado para iniciar a chamada e o sistema é a informação se ele é bem-sucedido ou falha.
Usuário finalizando chamada de entrada
Se o usuário desejar encerrar a chamada de dentro da interface do usuário do aplicativo, ocorrerá o seguinte:
- O aplicativo cria
CXEndCallAction
que é empacotado em umCXTransaction
que é enviado ao sistema para informá-lo de que a chamada está terminando. - O Sistema verifica a Intenção de Finalizar Chamada e envia o
CXEndCallAction
retorno para o aplicativo através doCXProvider
. - Em seguida, o aplicativo informa à sua Rede de Comunicação que a chamada está terminando.
Por exemplo, no 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 ();
}
});
}
Esse código primeiro procura a chamada fornecida em sua lista de chamadas ativas. Se a chamada não for encontrada, o sistema será notificado e o método será encerrado. Se for encontrado, o EndCall
ActiveCall
método da classe é chamado para encerrar a chamada e o sistema é a informação se ele é bem-sucedido ou falha. Se for bem-sucedida, a chamada será removida da coleção de chamadas ativas.
Gerenciando várias chamadas
A maioria dos aplicativos VOIP pode lidar com várias chamadas ao mesmo tempo. Por exemplo, se houver atualmente uma chamada VOIP ativa e o aplicativo receber notificação de que há uma nova chamada de entrada, o usuário poderá pausar ou desligar na primeira chamada para atender a segunda.
Na situação acima, o Sistema enviará um CXTransaction
para o aplicativo que incluirá uma lista de várias ações (como o CXEndCallAction
e o CXAnswerCallAction
). Todas essas ações precisarão ser cumpridas individualmente, para que o sistema possa atualizar a interface do usuário adequadamente.
Lidando com chamadas de saída
Se o usuário tocar em uma entrada da lista Recentes (no aplicativo Telefone), por exemplo, que é de uma chamada pertencente ao aplicativo, será enviada uma Intenção de Iniciar Chamada pelo sistema:
- O aplicativo criará uma Ação de Iniciar Chamada com base na Intenção de Iniciar Chamada recebida do Sistema.
- O aplicativo usará o
CXCallController
para solicitar a Ação de Iniciar Chamada do sistema. - Se o Sistema aceitar a Ação, ela será devolvida ao aplicativo por meio do
XCProvider
delegado. - O aplicativo inicia a chamada de saída com sua Rede de Comunicação.
Para obter mais informações sobre Intents, consulte nossa documentação de Intents and Intents UI Extensions .
O ciclo de vida da chamada de saída
Ao trabalhar com o CallKit e uma chamada de saída, o aplicativo precisará informar ao Sistema os seguintes eventos do ciclo de vida:
- Início - Informe ao sistema que uma chamada de saída está prestes a começar.
- Iniciado - Informe ao sistema que uma chamada de saída foi iniciada.
- Conexão - Informe ao sistema que a chamada de saída está se conectando.
- Conectado - Informe que a chamada de saída se conectou e que ambas as partes podem conversar agora.
Por exemplo, o código a seguir iniciará uma chamada de saída:
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);
}
Ele cria um CXHandle
e o usa para configurar um CXStartCallAction
que é empacotado em um CXTransaction
que é enviado para o sistema usando o RequestTransaction
CXCallController
método da classe. Ao chamar o RequestTransaction
método, o sistema pode colocar todas as chamadas existentes em espera, independentemente da origem (aplicativo de telefone, FaceTime, VOIP, etc.), antes que a nova chamada seja iniciada.
A solicitação para iniciar uma chamada VOIP de saída pode vir de várias fontes diferentes, como Siri, uma entrada em um cartão de visita (no aplicativo Contatos) ou da lista Recentes (no aplicativo Telefone). Nessas situações, o aplicativo receberá uma Intenção de Iniciar Chamada dentro de um NSUserActivity
e o AppDelegate precisará lidar com isso:
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;
}
}
Aqui, o CallHandleFromActivity
método da classe StartCallRequest
auxiliar está sendo usado para obter o identificador para a pessoa que está sendo chamada (consulte A classe StartCallRequest acima).
O PerformStartCallAction
método da classe ProviderDelegate é usado para finalmente iniciar a chamada de saída real e informar o sistema de seu 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 ();
}
});
}
Ele cria uma instância da ActiveCall
classe (para armazenar informações sobre a chamada em andamento) e preenche com a pessoa que está sendo chamada. Os StartingConnectionChanged
eventos e ConnectedChanged
são usados para monitorar e relatar o ciclo de vida da chamada de saída. A ligação é iniciada e o Sistema informa que a ação foi cumprida.
Encerrando uma chamada de saída
Quando o usuário tiver terminado com uma chamada de saída e desejar terminá-la, o seguinte código pode ser usado:
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);
}
Se cria um CXEndCallAction
com o UUID da chamada a terminar, agrupa-o em um CXTransaction
que é enviado para o sistema usando o RequestTransaction
CXCallController
método da classe.
Detalhes adicionais do CallKit
Esta seção abordará alguns detalhes adicionais que o desenvolvedor precisará levar em consideração ao trabalhar com o CallKit, como:
- Configuração do provedor
- Erros de ação
- Restrições do sistema
- Áudio VOIP
Configuração do provedor
A configuração do provedor permite que um aplicativo VOIP do iOS 10 personalize a experiência do usuário (dentro da interface do usuário nativa de chamada) ao trabalhar com o CallKit.
Um aplicativo pode fazer os seguintes tipos de personalizações:
- Exibir um nome localizado.
- Habilite o suporte a chamadas de vídeo.
- Personalize os botões na interface do usuário em chamada apresentando seu próprio ícone de imagem de modelo. A interação do usuário com botões personalizados é enviada diretamente para o aplicativo a ser processado.
Erros de ação
Os aplicativos VOIP do iOS 10 que usam o CallKit precisam lidar com Ações falhando normalmente e manter o usuário informado sobre o estado de Ação o tempo todo.
Leve em consideração o exemplo a seguir:
- O aplicativo recebeu uma Ação de Iniciar Chamada e iniciou o processo de inicialização de uma nova chamada VOIP com sua Rede de Comunicação.
- Devido à capacidade de comunicação de rede limitada ou inexistente, essa conexão falha.
- O aplicativo deve enviar a mensagem de falha de volta para a Ação Iniciar Chamada (
Action.Fail()
) para informar o Sistema sobre a falha. - Isso permite que o Sistema informe ao usuário o status da chamada. Por exemplo, para exibir a interface do usuário de falha de chamada.
Além disso, um aplicativo VOIP do iOS 10 precisará responder a Erros de Tempo Limite que podem ocorrer quando uma Ação esperada não pode ser processada dentro de um determinado período de tempo. Cada Tipo de Ação fornecido pelo CallKit tem um valor máximo de Tempo Limite associado a ele. Esses valores de tempo limite garantem que qualquer ação do CallKit solicitada pelo usuário seja tratada de forma responsiva, mantendo o sistema operacional fluido e responsivo também.
Há vários métodos no Provider Delegate (CXProviderDelegate
) que devem ser substituídos para lidar normalmente com essas situações de tempo limite também.
Restrições do sistema
Com base no estado atual do dispositivo iOS que executa o aplicativo iOS 10 VOIP, certas restrições do sistema podem ser impostas.
Por exemplo, uma chamada VOIP de entrada pode ser restringida pelo sistema se:
- A pessoa que está chamando está na Lista de Chamadas Bloqueadas do usuário.
- O dispositivo iOS do usuário está no modo Não Perturbe.
Se uma chamada VOIP for restrita por qualquer uma dessas situações, use o seguinte código para manipulá-la:
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
}
}
});
}
}
Áudio VOIP
O CallKit oferece vários benefícios para lidar com os recursos de áudio que um aplicativo VOIP do iOS 10 exigirá durante uma chamada VOIP ao vivo. Um dos maiores benefícios é que a sessão de áudio do aplicativo terá prioridades elevadas ao ser executada no iOS 10. Este é o mesmo nível de prioridade que os aplicativos integrados de Telefone e FaceTime e esse nível de prioridade aprimorado impedirá que outros aplicativos em execução interrompam a sessão de áudio do aplicativo VOIP.
Além disso, o CallKit tem acesso a outras dicas de roteamento de áudio que podem melhorar o desempenho e rotear de forma inteligente o áudio VOIP para dispositivos de saída específicos durante uma chamada ao vivo com base nas preferências do usuário e nos estados do dispositivo. Por exemplo, com base em dispositivos conectados, como fones de ouvido bluetooth, uma conexão CarPlay ao vivo ou configurações de acessibilidade.
Durante o ciclo de vida de uma chamada VOIP típica usando o CallKit, o aplicativo precisará configurar o fluxo de áudio que o CallKit fornecerá. Dê uma olhada no exemplo a seguir:
- Uma Ação de Iniciar Chamada é recebida pelo aplicativo para atender a uma chamada de entrada.
- Antes que essa Ação seja cumprida pelo aplicativo, ela fornece a configuração necessária para seu
AVAudioSession
. - O aplicativo informa ao Sistema que a Ação foi cumprida.
- Antes da chamada se conectar, o CallKit fornece uma correspondência de alta prioridade
AVAudioSession
à configuração solicitada pelo aplicativo. O aplicativo será notificado através doDidActivateAudioSession
método de seuCXProviderDelegate
.
Trabalhando com extensões de diretório de chamadas
Ao trabalhar com o CallKit, as Extensões de Diretório de Chamadas fornecem uma maneira de adicionar números de chamada bloqueados e identificar números específicos de um determinado aplicativo VOIP aos contatos no aplicativo Contato no dispositivo iOS.
Implementando uma extensão de diretório de chamada
Para implementar uma extensão de diretório de chamadas em um aplicativo Xamarin.iOS, faça o seguinte:
Abra a solução do aplicativo no Visual Studio para Mac.
Clique com o botão direito do mouse no Nome da Solução no Gerenciador de Soluções e selecione Adicionar>Novo Projeto.
Selecione Extensões do iOS>Extensões>de diretório de chamadas e clique no botão Avançar:
Digite um Nome para a extensão e clique no botão Avançar :
Ajuste o Nome do Projeto e/ou Nome da Solução, se necessário, e clique no botão Criar:
Isso adicionará uma CallDirectoryHandler.cs
classe ao projeto que se parece com o seguinte:
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
}
}
O BeginRequest
método no manipulador de diretório de chamadas precisará ser modificado para fornecer a funcionalidade necessária. No caso do exemplo acima, ele tenta definir a lista de números bloqueados e disponíveis no banco de dados de contatos do aplicativo VOIP. Se qualquer uma das solicitações falhar por qualquer motivo, crie um NSError
para descrever a falha e passe-lhe o CancelRequest
método da CXCallDirectoryExtensionContext
classe.
Para definir os números bloqueados, use o AddBlockingEntry
CXCallDirectoryExtensionContext
método da classe. Os números fornecidos ao método devem estar em ordem numericamente crescente. Para obter o desempenho ideal e o uso de memória quando houver muitos números de telefone, considere carregar apenas um subconjunto de números em um determinado momento e usar pool(s) de liberação automática para liberar objetos alocados durante cada lote de números carregados.
Para informar ao aplicativo Contato os números de contato conhecidos pelo aplicativo VOIP, use o AddIdentificationEntry
CXCallDirectoryExtensionContext
método da classe e forneça o número e uma etiqueta de identificação. Novamente, os números fornecidos ao método devem estar em ordem numericamente crescente. Para obter o desempenho ideal e o uso de memória quando houver muitos números de telefone, considere carregar apenas um subconjunto de números em um determinado momento e usar pool(s) de liberação automática para liberar objetos alocados durante cada lote de números carregados.
Resumo
Este artigo abordou a nova API do CallKit que a Apple lançou no iOS 10 e como implementá-la nos aplicativos VOIP do Xamarin.iOS. Ele mostrou como o CallKit permite que um aplicativo se integre ao sistema iOS, como ele fornece paridade de recursos com aplicativos integrados (como telefone) e como aumenta a visibilidade de um aplicativo em todo o iOS em locais como as telas de bloqueio e início, por meio de interações com a Siri e por meio dos aplicativos de contatos.