Partager via


CallKit dans Xamarin.iOS

La nouvelle API CallKit dans iOS 10 permet aux applications VOIP de s’intégrer à l’interface utilisateur i Téléphone et de fournir une interface et une expérience familières à l’utilisateur final. Avec cette API, les utilisateurs peuvent afficher et interagir avec les appels VOIP à partir de l’écran de verrouillage de l’appareil iOS et gérer les contacts à l’aide des vues Favoris et Récents de l’application Téléphone.

À propos de CallKit

Selon Apple, CallKit est une nouvelle infrastructure qui élèvera les applications VoIP (Voice Over IP) tierces à une expérience tierce sur iOS 10. L’API CallKit permet aux applications VOIP de s’intégrer à l’interface utilisateur i Téléphone et de fournir une interface et une expérience familières à l’utilisateur final. Tout comme l’application intégrée Téléphone, un utilisateur peut afficher et interagir avec les appels VOIP à partir de l’écran de verrouillage de l’appareil iOS et gérer les contacts à l’aide des vues Favoris et Récents de l’application Téléphone.

En outre, l’API CallKit permet de créer des extensions d’application qui peuvent associer un numéro de téléphone à un nom (ID de l’appelant) ou indiquer au système quand un numéro doit être bloqué (blocage des appels).

Expérience d’application VOIP existante

Avant de discuter de la nouvelle API CallKit et de ses capacités, examinez l’expérience utilisateur actuelle avec une application VOIP tierce dans iOS 9 (et moins) à l’aide d’une application VOIP fictive appelée MonkeyCall. MonkeyCall est une application simple qui permet à l’utilisateur d’envoyer et de recevoir des appels VOIP à l’aide des API iOS existantes.

Actuellement, si l’utilisateur reçoit un appel entrant sur MonkeyCall et que son i Téléphone est verrouillé, la notification reçue sur l’écran de verrouillage est indistinguishable à partir de tout autre type de notification (comme ceux des applications Messages ou Courrier par exemple).

Si l’utilisateur voulait répondre à l’appel, il faudrait faire glisser la notification MonkeyCall pour ouvrir l’application et entrer son code secret (ou id tactile utilisateur) pour déverrouiller le téléphone avant qu’il puisse accepter l’appel et démarrer la conversation.

L’expérience est tout aussi fastidieuse si le téléphone est déverrouillé. Là encore, l’appel MonkeyCall entrant s’affiche sous la forme d’une bannière de notification standard qui se glisse en haut de l’écran. Étant donné que la notification est temporaire, il peut être facilement manqué par l’utilisateur de les forcer à ouvrir le Centre de notification et de trouver la notification spécifique pour répondre, puis appeler ou rechercher et lancer l’application MonkeyCall manuellement.

Expérience de l’application VoIP CallKit

En implémentant les nouvelles API CallKit dans l’application MonkeyCall, l’expérience de l’utilisateur avec un appel VOIP entrant peut être considérablement améliorée dans iOS 10. Prenons l’exemple de l’utilisateur qui reçoit un appel VOIP lorsque son téléphone est verrouillé à partir de ce qui précède. En implémentant CallKit, l’appel s’affiche sur l’écran de verrouillage de l’i Téléphone, tout comme si l’appel était reçu de l’application intégrée Téléphone, avec l’interface utilisateur native et la fonctionnalité de balayage standard pour répondre.

Là encore, si l’i Téléphone est déverrouillé lorsqu’un appel VOIP MonkeyCall est reçu, la même interface utilisateur native, l’interface utilisateur native et les fonctionnalités de balayage à réponse standard et appuyez pour refuser la fonctionnalité intégrée de l’application Téléphone intégrée est présentée et MonkeyCall a la possibilité de lire une sonnerie personnalisée.

CallKit fournit des fonctionnalités supplémentaires à MonkeyCall, ce qui permet à ses appels VOIP d’interagir avec d’autres types d’appels, d’apparaître dans les listes récentes et favorites intégrées, d’utiliser les fonctionnalités intégrées Do Not Disturb and Block, de démarrer les appels MonkeyCall à partir de Siri et offre la possibilité aux utilisateurs d’affecter des appels MonkeyCall à des personnes de l’application Contacts.

Les sections suivantes couvrent l’architecture CallKit, les flux d’appels entrants et sortants et l’API CallKit en détail.

Architecture CallKit

Dans iOS 10, Apple a adopté CallKit dans tous les services système tels que les appels effectués sur CarPlay, par exemple, sont connus de l’interface utilisateur système via CallKit. Dans l’exemple ci-dessous, étant donné que MonkeyCall adopte CallKit, il est connu du système de la même façon que ces services système intégrés et obtient toutes les mêmes fonctionnalités :

Pile du service CallKit

Examinez de plus près l’application MonkeyCall à partir du diagramme ci-dessus. L’application contient tout son code pour communiquer avec son propre réseau et contient ses propres interfaces utilisateur. Il établit des liens dans CallKit pour communiquer avec le système :

Architecture de l’application MonkeyCall

Il existe deux interfaces principales dans CallKit que l’application utilise :

  • CXProvider - Cela permet à l’application MonkeyCall d’informer le système de toutes les notifications hors bande susceptibles de se produire.
  • CXCallController - Permet à l’application MonkeyCall d’informer le système d’actions utilisateur locales.

The CXProvider

Comme indiqué ci-dessus, CXProvider permet à une application d’informer le système de toutes les notifications hors bande susceptibles de se produire. Il s’agit de notifications qui ne se produisent pas en raison d’actions d’utilisateur locales, mais qui se produisent en raison d’événements externes tels que les appels entrants.

Une application doit utiliser les CXProvider éléments suivants :

  • Signalez un appel entrant au système.
  • Signalez un appel sortant connecté au système.
  • Signalez à l’utilisateur distant la fin de l’appel au système.

Lorsque l’application souhaite communiquer avec le système, elle utilise la CXCallUpdate classe et quand le système doit communiquer avec l’application, elle utilise la CXAction classe :

Communication avec le système via un CXProvider

The CXCallController

L’application CXCallController permet à une application d’informer le système d’actions utilisateur locales telles que l’utilisateur qui démarre un appel VOIP. En implémentant une CXCallController application, vous pouvez interagir avec d’autres types d’appels dans le système. Par exemple, s’il existe déjà un appel téléphonique actif en cours, CXCallController peut autoriser l’application VOIP à placer cet appel en attente et à démarrer ou répondre à un appel VOIP.

Une application doit utiliser les CXCallController éléments suivants :

  • Signalez quand l’utilisateur a démarré un appel sortant au système.
  • Signalez quand l’utilisateur répond à un appel entrant au système.
  • Signalez quand l’utilisateur termine un appel au système.

Lorsque l’application souhaite communiquer les actions de l’utilisateur local au système, elle utilise la CXTransaction classe :

Création de rapports au système à l’aide d’un CXCallController

Implémentation de CallKit

Les sections suivantes montrent comment implémenter CallKit dans une application VOIP Xamarin.iOS. Par exemple, ce document utilise du code à partir de l’application fictive MonkeyCall VOIP. Le code présenté ici représente plusieurs classes de prise en charge, les parties spécifiques de CallKit seront décrites en détail dans les sections suivantes.

La classe ActiveCall

La ActiveCall classe est utilisée par l’application MonkeyCall pour contenir toutes les informations relatives à un appel VOIP actuellement actif comme suit :

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 contient plusieurs propriétés qui définissent l’état de l’appel et deux événements qui peuvent être déclenchés lorsque l’état de l’appel change. Étant donné qu’il s’agit d’un exemple uniquement, il existe trois méthodes utilisées pour simuler le démarrage, répondre et mettre fin à un appel.

La classe StartCallRequest

La StartCallRequest classe statique fournit quelques méthodes d’assistance qui seront utilisées lors de l’utilisation d’appels sortants :

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;
            }
        }
    }
}

Les CallHandleFromURL classes et CallHandleFromActivity les classes sont utilisées dans AppDelegate pour obtenir le handle de contact de la personne appelée dans un appel sortant. Pour plus d’informations, consultez la section Gestion des appels sortants ci-dessous.

La classe ActiveCallManager

La ActiveCallManager classe gère tous les appels ouverts dans l’application 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
    }
}

Là encore, étant donné qu’il s’agit d’une simulation uniquement, la ActiveCallManager seule conserve une collection d’objets ActiveCall et a une routine pour trouver un appel donné par sa UUID propriété. Il inclut également des méthodes permettant de démarrer, de terminer et de modifier l’état d’attente d’un appel sortant. Pour plus d’informations, consultez la section Gestion des appels sortants ci-dessous.

La classe ProviderDelegate

Comme indiqué ci-dessus, une CXProvider communication bidirectionnelle entre l’application et le système pour les notifications hors bande. Le développeur doit fournir un callKit personnalisé CXProviderDelegate et l’attacher à l’application CXProvider pour gérer les événements CallKit hors bande. MonkeyCall utilise les éléments suivants 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
    }
}

Lorsqu’une instance de ce délégué est créée, elle est passée à celle ActiveCallManager qu’elle utilisera pour gérer n’importe quelle activité d’appel. Ensuite, il définit les types de handles (CXHandleType) auxquels le CXProvider répond :

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

Il obtient l’image de modèle qui sera appliquée à l’icône de l’application lorsqu’un appel est en cours :

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

Ces valeurs sont regroupées dans un CXProviderConfiguration objet qui sera utilisé pour configurer les CXProvideréléments suivants :

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

Le délégué crée ensuite une nouvelle CXProvider configuration avec ces configurations et s’y attache :

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

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

Lorsque vous utilisez CallKit, l’application ne crée plus et ne gère plus ses propres sessions audio, au lieu de cela, elle doit configurer et utiliser une session audio que le système va créer et gérer pour elle.

S’il s’agissait d’une application réelle, la DidActivateAudioSession méthode serait utilisée pour démarrer l’appel avec une préconfigurée AVAudioSession fournie par le système :

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

Il utilise également la DidDeactivateAudioSession méthode pour finaliser et libérer sa connexion à la session audio fournie par le système :

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

Le reste du code sera abordé en détail dans les sections qui suivent.

Classe AppDelegate

MonkeyCall utilise l’AppDelegate pour contenir les instances de l’application ActiveCallManager et CXProviderDelegate qui seront utilisées dans l’application :

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
    }
}

Les OpenUrl méthodes de remplacement et ContinueUserActivity de substitution sont utilisées lorsque l’application traite un appel sortant. Pour plus d’informations, consultez la section Gestion des appels sortants ci-dessous.

Gestion des appels entrants

Il existe plusieurs états et processus qu’un appel VOIP entrant peut passer pendant un flux de travail d’appel entrant classique, par exemple :

  • Informer l’utilisateur (et le système) qu’un appel entrant existe.
  • Réception de notification lorsque l’utilisateur souhaite répondre à l’appel et initialiser l’appel avec l’autre utilisateur.
  • Informez le système et le réseau de communication lorsque l’utilisateur souhaite mettre fin à l’appel actuel.

Les sections suivantes examinent en détail comment une application peut utiliser CallKit pour gérer le flux de travail d’appel entrant, à nouveau à l’aide de l’application VoIP MonkeyCall comme exemple.

Informer l’utilisateur de l’appel entrant

Lorsqu’un utilisateur distant a démarré une conversation VOIP avec l’utilisateur local, les événements suivants se produisent :

Un utilisateur distant a démarré une conversation VOIP

  1. L’application reçoit une notification de son réseau de communications indiquant qu’il existe un appel VOIP entrant.
  2. L’application utilise la CXProvider commande pour envoyer un CXCallUpdate message au système qui l’informe de l’appel.
  3. Le système publie l’appel à l’interface utilisateur système, aux services système et à toutes les autres applications VOIP à l’aide de CallKit.

Par exemple, dans :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);
        }
    });
}

Ce code crée une instance CXCallUpdate et attache un handle à celui-ci qui identifie l’appelant. Ensuite, il utilise la ReportNewIncomingCall méthode de la CXProvider classe pour informer le système de l’appel. Si elle réussit, l’appel est ajouté à la collection d’appels actifs de l’application, si ce n’est pas le cas, l’erreur doit être signalée à l’utilisateur.

Utilisateur répondant à un appel entrant

Si l’utilisateur souhaite répondre à l’appel VOIP entrant, les événements suivants se produisent :

L’utilisateur répond à l’appel VOIP entrant

  1. L’interface utilisateur système informe le système que l’utilisateur souhaite répondre à l’appel VOIP.
  2. Le système envoie un CXAnswerCallAction message à l’application pour l’informer de l’intention CXProvider de réponse.
  3. L’application informe son réseau de communication que l’utilisateur répond à l’appel et que l’appel VOIP se poursuit comme d’habitude.

Par exemple, dans :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 ();
        }
    });
}

Ce code recherche d’abord l’appel donné dans sa liste d’appels actifs. Si l’appel est introuvable, le système est averti et la méthode se ferme. Si elle est trouvée, la AnswerCall méthode de la ActiveCall classe est appelée pour démarrer l’appel et le système est des informations s’il réussit ou échoue.

Utilisateur terminant l’appel entrant

Si l’utilisateur souhaite arrêter l’appel à partir de l’interface utilisateur de l’application, les éléments suivants se produisent :

L’utilisateur termine l’appel à partir de l’interface utilisateur de l’application

  1. L’application crée CXEndCallAction un ensemble qui CXTransaction est envoyé au système pour l’informer que l’appel se termine.
  2. Le système vérifie l’intention d’appel final et renvoie l’application CXEndCallAction via le CXProvider.
  3. L’application informe ensuite son réseau de communication que l’appel se termine.

Par exemple, dans :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 ();
        }
    });
}

Ce code recherche d’abord l’appel donné dans sa liste d’appels actifs. Si l’appel est introuvable, le système est averti et la méthode se ferme. Si elle est trouvée, la EndCall méthode de la ActiveCall classe est appelée pour mettre fin à l’appel et le système est des informations s’il réussit ou échoue. En cas de réussite, l’appel est supprimé de la collection d’appels actifs.

Gestion de plusieurs appels

La plupart des applications VOIP peuvent gérer plusieurs appels à la fois. Par exemple, s’il existe actuellement un appel VOIP actif et que l’application reçoit une notification indiquant qu’il existe un nouvel appel entrant, l’utilisateur peut suspendre ou raccrocher sur le premier appel pour répondre au deuxième.

Dans la situation ci-dessus, le système envoie un CXTransaction message à l’application qui inclut une liste de plusieurs actions (telles que le CXEndCallAction et le CXAnswerCallAction). Toutes ces actions devront être remplies individuellement, afin que le système puisse mettre à jour l’interface utilisateur de manière appropriée.

Gestion des appels sortants

Si l’utilisateur appuie sur une entrée de la liste Récents (dans l’application Téléphone), par exemple, à partir d’un appel appartenant à l’application, il est envoyé une intention d’appel de démarrage par le système :

Réception d’une intention d’appel de début

  1. L’application crée une action d’appel de démarrage en fonction de l’intention d’appel de démarrage qu’elle a reçue du système.
  2. L’application utilisera l’application CXCallController pour demander l’action Démarrer l’appel à partir du système.
  3. Si le système accepte l’action, il est retourné à l’application via le XCProvider délégué.
  4. L’application démarre l’appel sortant avec son réseau de communication.

Pour plus d’informations sur les intentions, consultez notre documentation sur les extensions d’interface utilisateur intentions et intentions.

Cycle de vie des appels sortants

Lorsque vous utilisez CallKit et un appel sortant, l’application doit informer le système des événements de cycle de vie suivants :

  1. Démarrage : informez le système qu’un appel sortant est sur le point de démarrer.
  2. Démarré : informez le système qu’un appel sortant a démarré.
  3. Connecter ing : informez le système que l’appel sortant se connecte.
  4. Connecter ed - Informez l’appel sortant et que les deux parties peuvent parler maintenant.

Par exemple, le code suivant démarre un appel sortant :

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);
}

Il crée un CXHandle fichier et l’utilise pour configurer un CXStartCallAction élément qui est regroupé dans un CXTransaction système envoyé au système à l’aide de la RequestTransaction méthode de la CXCallController classe. En appelant la RequestTransaction méthode, le système peut placer tous les appels existants en attente, quelle que soit la source (Téléphone application, FaceTime, VOIP, etc.), avant le démarrage du nouvel appel.

La demande de démarrage d’un appel VOIP sortant peut provenir de plusieurs sources différentes, telles que Siri, une entrée sur un contact carte (dans l’application Contacts) ou à partir de la liste Récents (dans l’application Téléphone). Dans ces situations, l’application est envoyée à l’intérieur d’une NSUserActivity intention d’appel de démarrage et l’ApplicationDelegate doit la gérer :

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;
    }
}

Ici, la CallHandleFromActivity méthode de la classe StartCallRequest d’assistance est utilisée pour obtenir le handle à la personne appelée (voir la classe StartCallRequest ci-dessus).

La PerformStartCallAction méthode de la classe ProviderDelegate est utilisée pour enfin démarrer l’appel sortant réel et informer le système de son cycle de vie :

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 ();
        }
    });
}

Il crée une instance de la ActiveCall classe (pour contenir des informations sur l’appel en cours) et remplit la personne appelée. Les StartingConnectionChanged événements et ConnectedChanged les événements sont utilisés pour surveiller et signaler le cycle de vie des appels sortants. L’appel est démarré et le système a informé que l’action a été exécutée.

Fin d’un appel sortant

Une fois l’utilisateur terminé avec un appel sortant et souhaite le terminer, le code suivant peut être utilisé :

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

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

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

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

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

Si vous créez un CXEndCallAction UUID de l’appel à la fin, le regroupe dans un CXTransaction élément envoyé au système à l’aide de la RequestTransaction méthode de la CXCallController classe.

Détails supplémentaires de CallKit

Cette section décrit quelques détails supplémentaires que le développeur devra prendre en compte lors de l’utilisation de CallKit, par exemple :

  • Configuration du fournisseur
  • Erreurs en relation avec les actions
  • Restrictions système
  • VOIP Audio

Configuration du fournisseur

La configuration du fournisseur permet à une application VOIP iOS 10 de personnaliser l’expérience utilisateur (à l’intérieur de l’interface utilisateur in-call native) lors de l’utilisation de CallKit.

Une application peut effectuer les types de personnalisations suivants :

  • Affichez un nom localisé.
  • Activer la prise en charge des appels vidéo.
  • Personnalisez les boutons de l’interface utilisateur in-call en présentant son propre icône d’image de modèle. L’interaction utilisateur avec des boutons personnalisés est envoyée directement à l’application à traiter.

Erreurs d’action

Les applications VOIP iOS 10 utilisant CallKit doivent gérer l’échec des actions correctement et garder l’utilisateur informé de l’état d’action à tout moment.

Prenez en compte l’exemple suivant :

  1. L’application a reçu une action Démarrer l’appel et a commencé le processus d’initialisation d’un nouvel appel VOIP avec son réseau de communication.
  2. En raison d’une fonctionnalité de communication réseau limitée ou inexistante, cette connexion échoue.
  3. L’application doit renvoyer le message de restauration automatique à l’action Démarrer l’appel (Action.Fail()) pour informer le système de l’échec.
  4. Cela permet au système d’informer l’utilisateur de l’état de l’appel. Par exemple, pour afficher l’interface utilisateur d’échec d’appel.

En outre, une application VOIP iOS 10 doit répondre aux erreurs de délai d’expiration qui peuvent se produire lorsqu’une action attendue ne peut pas être traitée dans un délai donné. Chaque type d’action fourni par CallKit a une valeur de délai d’expiration maximale associée. Ces valeurs de délai d’attente garantissent que toute action CallKit demandée par l’utilisateur est gérée de manière réactive, ce qui permet de maintenir le système d’exploitation fluide et réactif ainsi.

Il existe plusieurs méthodes sur le délégué du fournisseur (CXProviderDelegate) qui doivent également être remplacées pour gérer correctement ces situations de délai d’attente.

Restrictions système

En fonction de l’état actuel de l’appareil iOS exécutant l’application VOIP iOS 10, certaines restrictions système peuvent être appliquées.

Par exemple, un appel VOIP entrant peut être limité par le système si :

  1. L’appel de la personne se trouve dans la liste des appelants bloqués de l’utilisateur.
  2. L’appareil iOS de l’utilisateur est en mode Ne pas déranger.

Si un appel VOIP est limité par l’une de ces situations, utilisez le code suivant pour le gérer :

public class ProviderDelegate : CXProviderDelegate
{
...

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

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

}

Audio VOIP

CallKit offre plusieurs avantages pour gérer les ressources audio qu’une application VOIP iOS 10 nécessite lors d’un appel VOIP en direct. L’un des principaux avantages est que la session audio de l’application aura des priorités élevées lors de l’exécution dans iOS 10. Il s’agit du même niveau de priorité que les applications intégrées Téléphone et FaceTime, et ce niveau de priorité amélioré empêche les autres applications en cours d’exécution d’interrompre la session audio de l’application VOIP.

En outre, CallKit a accès à d’autres indicateurs de routage audio qui peuvent améliorer les performances et acheminer intelligemment l’audio VOIP vers des appareils de sortie spécifiques pendant un appel en direct en fonction des préférences utilisateur et des états des appareils. Par exemple, sur la base d’appareils attachés tels que des écouteurs bluetooth, une connexion CarPlay active ou des paramètres d’accessibilité.

Pendant le cycle de vie d’un appel VOIP classique à l’aide de CallKit, l’application doit configurer le flux audio fourni par CallKit. Examinez l’exemple suivant :

Séquence d’action Démarrer l’appel

  1. Une action Démarrer l’appel est reçue par l’application pour répondre à un appel entrant.
  2. Avant que cette action ne soit remplie par l’application, elle fournit la configuration requise pour son AVAudioSession.
  3. L’application informe le système que l’action a été remplie.
  4. Avant la connexion de l’appel, CallKit fournit une priorité AVAudioSession élevée correspondant à la configuration demandée par l’application. L’application sera avertie par le biais de la DidActivateAudioSession méthode de son CXProviderDelegate.

Utilisation des extensions d’annuaire d’appels

Lorsque vous utilisez CallKit, les extensions d’annuaire des appels permettent d’ajouter des numéros d’appel bloqués et d’identifier les numéros spécifiques à une application VOIP donnée aux contacts de l’application Contact sur l’appareil iOS.

Implémentation d’une extension d’annuaire d’appels

Pour implémenter une extension d’annuaire d’appels dans une application Xamarin.iOS, procédez comme suit :

  1. Ouvrez la solution de l’application dans Visual Studio pour Mac.

  2. Cliquez avec le bouton droit sur le nom de la solution dans le Explorateur de solutions, puis sélectionnez Ajouter>un nouveau projet.

  3. Sélectionnez Extensions>d’appel d’extensions iOS>, puis cliquez sur le bouton Suivant :

    Création d’une extension d’annuaire d’appels

  4. Entrez un nom pour l’extension, puis cliquez sur le bouton Suivant :

    Entrée d’un nom pour l’extension

  5. Ajustez le nom du projet et/ou le nom de la solution si nécessaire, puis cliquez sur le bouton Créer :

    Création du projet

Cela ajoute une CallDirectoryHandler.cs classe au projet qui ressemble à ce qui suit :

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
    }
}

La BeginRequest méthode du gestionnaire d’annuaires d’appels doit être modifiée pour fournir les fonctionnalités requises. Dans le cas de l’exemple ci-dessus, il tente de définir la liste des numéros bloqués et disponibles dans la base de données de contacts de l’application VOIP. Si l’une des demandes échoue pour une raison quelconque, créez-en une NSError pour décrire l’échec et transmettez-la à la CancelRequest méthode de la CXCallDirectoryExtensionContext classe.

Pour définir les nombres bloqués, utilisez la AddBlockingEntry méthode de la CXCallDirectoryExtensionContext classe. Les nombres fournis à la méthode doivent être dans un ordre croissant numérique. Pour optimiser les performances et l’utilisation de la mémoire lorsqu’il existe de nombreux numéros de téléphone, envisagez de charger uniquement un sous-ensemble de nombres à un moment donné et d’utiliser des pools de rééditions automatiques pour libérer des objets alloués pendant chaque lot de nombres chargés.

Pour informer l’application Contact des numéros de contact connus de l’application VOIP, utilisez la AddIdentificationEntry méthode de la CXCallDirectoryExtensionContext classe et fournissez le numéro et une étiquette d’identification. Là encore, les nombres fournis à la méthode doivent être dans un ordre croissant numérique. Pour optimiser les performances et l’utilisation de la mémoire lorsqu’il existe de nombreux numéros de téléphone, envisagez de charger uniquement un sous-ensemble de nombres à un moment donné et d’utiliser des pools de rééditions automatiques pour libérer des objets alloués pendant chaque lot de nombres chargés.

Résumé

Cet article a abordé la nouvelle API CallKit publiée par Apple dans iOS 10 et comment l’implémenter dans les applications VOIP Xamarin.iOS. Il a montré comment CallKit permet à une application d’intégrer dans le système iOS, comment elle fournit la parité des fonctionnalités avec les applications intégrées (telles que Téléphone) et comment elle augmente la visibilité d’une application dans iOS dans des emplacements tels que le verrouillage et les écrans d’accueil, via les interactions Siri et via les applications Contacts.