Compartilhar via


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:

A pilha de serviço do CallKit

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:

Arquitetura do aplicativo MonkeyCall

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:

Comunicação com o sistema através de um CXProvider

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:

Reportando ao sistema usando um CXCallController

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:

Um usuário remoto iniciou uma conversa VOIP

  1. O aplicativo recebe uma notificação de sua Rede de Comunicações de que há uma chamada VOIP de entrada.
  2. O aplicativo usa o CXProvider para enviar um CXCallUpdate para o sistema informando sobre a chamada.
  3. 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 ReportNewIncomingCallCXProvider 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:

O usuário responde à chamada VOIP de entrada

  1. A interface do usuário do sistema informa ao sistema que o usuário deseja atender à chamada VOIP.
  2. O Sistema envia um CXAnswerCallAction para o aplicativo CXProvider informando a Intenção de Resposta.
  3. 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 AnswerCallActiveCall 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 usuário encerra a chamada de dentro da interface do usuário do aplicativo

  1. O aplicativo cria CXEndCallAction que é empacotado em um CXTransaction que é enviado ao sistema para informá-lo de que a chamada está terminando.
  2. O Sistema verifica a Intenção de Finalizar Chamada e envia o CXEndCallAction retorno para o aplicativo através do CXProvider.
  3. 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 EndCallActiveCall 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:

Recebendo uma intenção de iniciar chamada

  1. O aplicativo criará uma Ação de Iniciar Chamada com base na Intenção de Iniciar Chamada recebida do Sistema.
  2. O aplicativo usará o CXCallController para solicitar a Ação de Iniciar Chamada do sistema.
  3. Se o Sistema aceitar a Ação, ela será devolvida ao aplicativo por meio do XCProvider delegado.
  4. 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:

  1. Início - Informe ao sistema que uma chamada de saída está prestes a começar.
  2. Iniciado - Informe ao sistema que uma chamada de saída foi iniciada.
  3. Conexão - Informe ao sistema que a chamada de saída está se conectando.
  4. 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 RequestTransactionCXCallController 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 RequestTransactionCXCallController 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:

  1. 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.
  2. Devido à capacidade de comunicação de rede limitada ou inexistente, essa conexão falha.
  3. 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.
  4. 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:

  1. A pessoa que está chamando está na Lista de Chamadas Bloqueadas do usuário.
  2. 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:

A sequência de ação Iniciar chamada

  1. Uma Ação de Iniciar Chamada é recebida pelo aplicativo para atender a uma chamada de entrada.
  2. Antes que essa Ação seja cumprida pelo aplicativo, ela fornece a configuração necessária para seu AVAudioSession.
  3. O aplicativo informa ao Sistema que a Ação foi cumprida.
  4. 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 do DidActivateAudioSession método de seu CXProviderDelegate.

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:

  1. Abra a solução do aplicativo no Visual Studio para Mac.

  2. Clique com o botão direito do mouse no Nome da Solução no Gerenciador de Soluções e selecione Adicionar>Novo Projeto.

  3. Selecione Extensões do iOS>Extensões>de diretório de chamadas e clique no botão Avançar:

    Criando uma nova extensão de diretório de chamadas

  4. Digite um Nome para a extensão e clique no botão Avançar :

    Inserindo um nome para a extensão

  5. Ajuste o Nome do Projeto e/ou Nome da Solução, se necessário, e clique no botão Criar:

    Criação do projeto

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 AddBlockingEntryCXCallDirectoryExtensionContext 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 AddIdentificationEntryCXCallDirectoryExtensionContext 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.