Condividi tramite


CallKit in Xamarin.iOS

La nuova API CallKit in iOS 10 consente alle app VOIP di integrarsi con l'interfaccia utente i Telefono e offrire un'interfaccia e un'esperienza familiari all'utente finale. Con questa API gli utenti possono visualizzare e interagire con le chiamate VOIP dalla schermata di blocco del dispositivo iOS e gestire i contatti usando le visualizzazioni Preferiti e Recenti dell'app Telefono.

Informazioni su CallKit

Secondo Apple, CallKit è un nuovo framework che eleva le app Voice Over IP (VOIP) di terze parti a un'esperienza di prima parte in iOS 10. L'API CallKit consente alle app VOIP di integrarsi con l'interfaccia utente i Telefono e offrire un'interfaccia e un'esperienza familiari all'utente finale. Proprio come l'app di Telefono predefinita, un utente può visualizzare e interagire con le chiamate VOIP dalla schermata di blocco del dispositivo iOS e gestire i contatti usando le visualizzazioni Preferiti e Recenti dell'app Telefono.

Inoltre, l'API CallKit consente di creare estensioni dell'app che possono associare un numero di telefono a un nome (ID chiamante) o indicare al sistema quando un numero deve essere bloccato (blocco delle chiamate).

Esperienza dell'app VOIP esistente

Prima di discutere della nuova API CallKit e delle sue capacità, esaminare l'esperienza utente corrente con un'app VOIP di terze parti in iOS 9 (e minore) usando un'app VOIP fittizia denominata MonkeyCall. MonkeyCall è una semplice app che consente all'utente di inviare e ricevere chiamate VOIP usando le API iOS esistenti.

Attualmente, se l'utente riceve una chiamata in arrivo su MonkeyCall e il relativo i Telefono è bloccato, la notifica ricevuta nella schermata di blocco è indistinguibile da qualsiasi altro tipo di notifica (ad esempio quelli delle app Messaggi o Posta elettronica).

Se l'utente vuole rispondere alla chiamata, dovrà far scorrere la notifica MonkeyCall per aprire l'app e immettere il passcode (o l'ID tocco dell'utente) per sbloccare il telefono prima di poter accettare la chiamata e avviare la conversazione.

L'esperienza è ugualmente complessa se il telefono è sbloccato. Anche in questo caso, la chiamata MonkeyCall in ingresso viene visualizzata come banner di notifica standard che scorre dalla parte superiore dello schermo. Poiché la notifica è temporanea, può essere facilmente persa dall'utente forzandoli ad aprire il Centro notifiche e trovare la notifica specifica per rispondere, quindi chiamare o trovare e avviare manualmente l'app MonkeyCall.

Esperienza dell'app VOIP CallKit

Implementando le nuove API CallKit nell'app MonkeyCall, l'esperienza dell'utente con una chiamata VOIP in ingresso può essere notevolmente migliorata in iOS 10. Si prenda l'esempio dell'utente che riceve una chiamata VOIP quando il telefono è bloccato da sopra. Implementando CallKit, la chiamata verrà visualizzata nella schermata di blocco di i Telefono, proprio come se la chiamata venisse ricevuta dall'app Telefono predefinita, con l'interfaccia utente a schermo intero, l'interfaccia utente nativa e la funzionalità di scorrimento rapido a risposta standard.

Anche in questo caso, se l'i Telefono viene sbloccato quando viene ricevuta una chiamata VOIP MonkeyCall, la stessa interfaccia utente nativa a schermo intero e la funzionalità standard di scorrimento rapido alla risposta e tap-to-decline dell'app predefinita Telefono viene presentata e MonkeyCall ha la possibilità di riprodurre una suoneria personalizzata.

CallKit offre funzionalità aggiuntive a MonkeyCall, consentendo alle chiamate VOIP di interagire con altri tipi di chiamate, di apparire negli elenchi Recenti e Preferiti predefiniti, per usare le funzionalità predefinite Do Not Disturb e Block, avviare le chiamate MonkeyCall da Siri e offre agli utenti la possibilità di assegnare chiamate MonkeyCall alle persone nell'app Contatti.

Le sezioni seguenti illustrano in dettaglio l'architettura callkit, i flussi di chiamate in ingresso e in uscita e l'API CallKit.

Architettura di CallKit

In iOS 10 Apple ha adottato CallKit in tutti i servizi di sistema, in modo che le chiamate effettuate su CarPlay, ad esempio, siano note all'interfaccia utente di sistema tramite CallKit. Nell'esempio riportato di seguito, poiché MonkeyCall adotta CallKit, è noto al sistema nello stesso modo di questi servizi di sistema predefiniti e ottiene tutte le stesse funzionalità:

Stack di servizi CallKit

Esaminare più in dettaglio l'app MonkeyCall del diagramma precedente. L'app contiene tutto il codice per comunicare con la propria rete e contiene le proprie interfacce utente. Collegamenti in CallKit per comunicare con il sistema:

Architettura dell'app MonkeyCall

Esistono due interfacce principali in CallKit usate dall'app:

  • CXProvider - Ciò consente all'app MonkeyCall di informare il sistema di eventuali notifiche fuori banda che potrebbero verificarsi.
  • CXCallController - Consente all'app MonkeyCall di informare il sistema delle azioni dell'utente locale.

The CXProvider

Come indicato in precedenza, CXProvider consente a un'app di informare il sistema di eventuali notifiche fuori banda che potrebbero verificarsi. Si tratta di una notifica che non si verifica a causa di azioni dell'utente locale, ma che si verificano a causa di eventi esterni, ad esempio le chiamate in ingresso.

Un'app deve usare CXProvider per quanto segue:

  • Segnalare una chiamata in ingresso al sistema.
  • Segnalare una chiamata in uscita connessa al sistema.
  • Segnalare all'utente remoto che termina la chiamata al sistema.

Quando l'app vuole comunicare con il sistema, usa la CXCallUpdate classe e quando il sistema deve comunicare con l'app, usa la CXAction classe :

Comunicazione con il sistema tramite CXProvider

The CXCallController

CXCallController consente a un'app di informare il sistema di azioni dell'utente locale, ad esempio l'utente che avvia una chiamata VOIP. Implementando un'app CXCallController viene eseguita l'interazione con altri tipi di chiamate nel sistema. Ad esempio, se è già in corso una chiamata telefonica attiva, CXCallController può consentire all'app VOIP di posizionare tale chiamata in attesa e avviare o rispondere a una chiamata VOIP.

Un'app deve usare CXCallController per quanto segue:

  • Segnalare quando l'utente ha avviato una chiamata in uscita al sistema.
  • Segnalare quando l'utente risponde a una chiamata in arrivo al sistema.
  • Segnalare quando l'utente termina una chiamata al sistema.

Quando l'app vuole comunicare le azioni dell'utente locale al sistema, usa la CXTransaction classe :

Creazione di report al sistema tramite CXCallController

Implementazione di CallKit

Le sezioni seguenti illustrano come implementare CallKit in un'app VOIP Xamarin.iOS. Per motivi di esempio, questo documento usa il codice dell'app VOIP fittizia MonkeyCall. Il codice presentato qui rappresenta diverse classi di supporto, le parti specifiche di CallKit verranno illustrate in dettaglio nelle sezioni seguenti.

Classe ActiveCall

La ActiveCall classe viene usata dall'app MonkeyCall per contenere tutte le informazioni su una chiamata VOIP attualmente attiva come indicato di seguito:

using System;
using CoreFoundation;
using Foundation;

namespace MonkeyCall
{
    public class ActiveCall
    {
        #region Private Variables
        private bool isConnecting;
        private bool isConnected;
        private bool isOnhold;
        #endregion

        #region Computed Properties
        public NSUuid UUID { get; set; }
        public bool isOutgoing { get; set; }
        public string Handle { get; set; }
        public DateTime StartedConnectingOn { get; set;}
        public DateTime ConnectedOn { get; set;}
        public DateTime EndedOn { get; set; }

        public bool IsConnecting {
            get { return isConnecting; }
            set {
                isConnecting = value;
                if (isConnecting) StartedConnectingOn = DateTime.Now;
                RaiseStartingConnectionChanged ();
            }
        }

        public bool IsConnected {
            get { return isConnected; }
            set {
                isConnected = value;
                if (isConnected) {
                    ConnectedOn = DateTime.Now;
                } else {
                    EndedOn = DateTime.Now;
                }
                RaiseConnectedChanged ();
            }
        }

        public bool IsOnHold {
            get { return isOnhold; }
            set {
                isOnhold = value;
            }
        }
        #endregion

        #region Constructors
        public ActiveCall ()
        {
        }

        public ActiveCall (NSUuid uuid, string handle, bool outgoing)
        {
            // Initialize
            this.UUID = uuid;
            this.Handle = handle;
            this.isOutgoing = outgoing;
        }
        #endregion

        #region Public Methods
        public void StartCall (ActiveCallbackDelegate completionHandler)
        {
            // Simulate the call starting successfully
            completionHandler (true);

            // Simulate making a starting and completing a connection
            DispatchQueue.MainQueue.DispatchAfter (new DispatchTime(DispatchTime.Now, 3000), () => {
                // Note that the call is starting
                IsConnecting = true;

                // Simulate pause before connecting
                DispatchQueue.MainQueue.DispatchAfter (new DispatchTime (DispatchTime.Now, 1500), () => {
                    // Note that the call has connected
                    IsConnecting = false;
                    IsConnected = true;
                });
            });
        }

        public void AnswerCall (ActiveCallbackDelegate completionHandler)
        {
            // Simulate the call being answered
            IsConnected = true;
            completionHandler (true);
        }

        public void EndCall (ActiveCallbackDelegate completionHandler)
        {
            // Simulate the call ending
            IsConnected = false;
            completionHandler (true);
        }
        #endregion

        #region Events
        public delegate void ActiveCallbackDelegate (bool successful);
        public delegate void ActiveCallStateChangedDelegate (ActiveCall call);

        public event ActiveCallStateChangedDelegate StartingConnectionChanged;
        internal void RaiseStartingConnectionChanged ()
        {
            if (this.StartingConnectionChanged != null) this.StartingConnectionChanged (this);
        }

        public event ActiveCallStateChangedDelegate ConnectedChanged;
        internal void RaiseConnectedChanged ()
        {
            if (this.ConnectedChanged != null) this.ConnectedChanged (this);
        }
        #endregion
    }
}

ActiveCall contiene diverse proprietà che definiscono lo stato della chiamata e due eventi che possono essere generati quando lo stato della chiamata cambia. Poiché si tratta solo di un esempio, esistono tre metodi usati per simulare l'avvio, la risposta e la fine di una chiamata.

Classe StartCallRequest

La StartCallRequest classe statica fornisce alcuni metodi helper che verranno usati quando si usano le chiamate in uscita:

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

Le CallHandleFromURL classi e CallHandleFromActivity vengono usate in AppDelegate per ottenere l'handle di contatto della persona chiamata in una chiamata in uscita. Per altre informazioni, vedere la sezione Gestione delle chiamate in uscita di seguito.

Classe ActiveCallManager

La ActiveCallManager classe gestisce tutte le chiamate aperte nell'app 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
    }
}

Anche in questo caso, poiché si tratta solo di una simulazione, mantiene ActiveCallManager solo una raccolta di ActiveCall oggetti e dispone di una routine per trovare una determinata chiamata tramite la relativa UUID proprietà. Include anche metodi per avviare, terminare e modificare lo stato di attesa di una chiamata in uscita. Per altre informazioni, vedere la sezione Gestione delle chiamate in uscita di seguito.

Classe ProviderDelegate

Come illustrato in precedenza, un fornisce una CXProvider comunicazione bidirezionale tra l'app e il sistema per le notifiche fuori banda. Lo sviluppatore deve fornire un oggetto personalizzato CXProviderDelegate e collegarlo a CXProvider per consentire all'app di gestire gli eventi CallKit fuori banda. MonkeyCall usa quanto segue 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 viene creata un'istanza di questo delegato, viene passato a ActiveCallManager che verrà usato per gestire qualsiasi attività di chiamata. Definisce quindi i tipi di handle (CXHandleType) a cui CXProvider risponderà:

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

Ottiene l'immagine del modello che verrà applicata all'icona dell'app quando è in corso una chiamata:

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

Questi valori vengono aggregati in un oggetto CXProviderConfiguration che verrà usato per configurare :CXProvider

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

Il delegato crea quindi un nuovo CXProvider oggetto con queste configurazioni e si collega a esso:

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

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

Quando si usa CallKit, l'app non creerà e gestirà più le proprie sessioni audio, ma dovrà configurare e usare una sessione audio che verrà creata e gestita dal sistema.

Se si trattasse di un'app reale, il DidActivateAudioSession metodo verrà usato per avviare la chiamata con un preconfigurato AVAudioSession fornito dal sistema:

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

Userebbe anche il DidDeactivateAudioSession metodo per finalizzare e rilasciare la connessione alla sessione audio fornita dal sistema:

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

Il resto del codice verrà trattato in dettaglio nelle sezioni seguenti.

Classe AppDelegate

MonkeyCall usa AppDelegate per contenere le istanze di ActiveCallManager e CXProviderDelegate che verranno usate in tutta l'app:

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

I OpenUrl metodi di override e ContinueUserActivity vengono usati quando l'app elabora una chiamata in uscita. Per altre informazioni, vedere la sezione Gestione delle chiamate in uscita di seguito.

Gestione delle chiamate in ingresso

Esistono diversi stati e processi che una chiamata VOIP in ingresso può attraversare durante un tipico flusso di lavoro di chiamata in ingresso, ad esempio:

  • Informare l'utente (e il sistema) che esiste una chiamata in ingresso.
  • Ricezione di una notifica quando l'utente vuole rispondere alla chiamata e inizializzare la chiamata con l'altro utente.
  • Informare il sistema e la rete di comunicazione quando l'utente vuole terminare la chiamata corrente.

Le sezioni seguenti illustrano in dettaglio come un'app può usare CallKit per gestire il flusso di lavoro delle chiamate in ingresso, usando di nuovo l'app VOIP MonkeyCall come esempio.

Informare l'utente della chiamata in arrivo

Quando un utente remoto ha avviato una conversazione VOIP con l'utente locale, si verifica quanto segue:

Un utente remoto ha avviato una conversazione VOIP

  1. L'app riceve una notifica dalla rete di comunicazione in cui è presente una chiamata VOIP in ingresso.
  2. L'app usa per CXProvider inviare un CXCallUpdate oggetto al sistema informandolo della chiamata.
  3. Il sistema pubblica la chiamata all'interfaccia utente di sistema, ai servizi di sistema e a qualsiasi altra app VOIP usando CallKit.

Ad esempio, in 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);
        }
    });
}

Questo codice crea una nuova CXCallUpdate istanza e associa un handle a esso che identificherà il chiamante. Successivamente, usa il ReportNewIncomingCall metodo della CXProvider classe per informare il sistema della chiamata. Se ha esito positivo, la chiamata viene aggiunta alla raccolta di chiamate attive dell'app, in caso contrario, l'errore deve essere segnalato all'utente.

Utente che risponde alla chiamata in arrivo

Se l'utente vuole rispondere alla chiamata VOIP in ingresso, si verifica quanto segue:

L'utente risponde alla chiamata VOIP in ingresso

  1. L'interfaccia utente di sistema informa il sistema che l'utente vuole rispondere alla chiamata VOIP.
  2. Il sistema invia un oggetto CXAnswerCallAction all'app CXProvider informandolo della finalità di risposta.
  3. L'app informa la rete di comunicazione che l'utente sta rispondendo alla chiamata e la chiamata VOIP procede come di consueto.

Ad esempio, in 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 ();
        }
    });
}

Questo codice cerca prima di tutto la chiamata specificata nel relativo elenco di chiamate attive. Se la chiamata non viene trovata, il sistema riceve una notifica e il metodo viene chiuso. Se viene trovato, il AnswerCall metodo della ActiveCall classe viene chiamato per avviare la chiamata e il sistema è informativo se ha esito positivo o negativo.

Utente che termina la chiamata in arrivo

Se l'utente vuole terminare la chiamata dall'interfaccia utente dell'app, si verifica quanto segue:

L'utente termina la chiamata dall'interfaccia utente dell'app

  1. L'app crea CXEndCallAction che viene in bundle in un oggetto CXTransaction inviato al sistema per informarlo che la chiamata sta terminando.
  2. Il sistema verifica la finalità di chiamata finale e invia di CXEndCallAction nuovo all'app tramite .CXProvider
  3. L'app informa quindi la rete di comunicazione che la chiamata sta terminando.

Ad esempio, in 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 ();
        }
    });
}

Questo codice cerca prima di tutto la chiamata specificata nel relativo elenco di chiamate attive. Se la chiamata non viene trovata, il sistema riceve una notifica e il metodo viene chiuso. Se viene trovato, il EndCall metodo della classe viene chiamato per terminare la ActiveCall chiamata e il sistema è informazioni se ha esito positivo o negativo. In caso di esito positivo, la chiamata viene rimossa dalla raccolta di chiamate attive.

Gestione di più chiamate

La maggior parte delle app VOIP può gestire più chiamate contemporaneamente. Ad esempio, se è attualmente presente una chiamata VOIP attiva e l'app riceve una notifica che indica che è presente una nuova chiamata in arrivo, l'utente può sospendere o riattaccarsi alla prima chiamata per rispondere alla seconda.

Nella situazione precedente, il sistema invierà un all'app CXTransaction che includerà un elenco di più azioni ( ad esempio CXEndCallAction e ).CXAnswerCallAction Tutte queste azioni dovranno essere soddisfatte singolarmente, in modo che il sistema possa aggiornare l'interfaccia utente in modo appropriato.

Gestione delle chiamate in uscita

Se l'utente tocca una voce dall'elenco Recenti (nell'app Telefono), ad esempio da una chiamata appartenente all'app, verrà inviata una finalità start call dal sistema:

Ricezione di una finalità di chiamata di avvio

  1. L'app creerà un'azione start call in base alla finalità start call ricevuta dal sistema.
  2. L'app userà per CXCallController richiedere l'azione Avvia chiamata dal sistema.
  3. Se il sistema accetta l'azione, verrà restituito all'app tramite il XCProvider delegato.
  4. L'app avvia la chiamata in uscita con la relativa rete di comunicazione.

Per altre informazioni sulle finalità, vedere la documentazione sulle finalità e sulle estensioni dell'interfaccia utente delle finalità.

Ciclo di vita delle chiamate in uscita

Quando si usa CallKit e una chiamata in uscita, l'app dovrà informare il sistema degli eventi del ciclo di vita seguenti:

  1. Avvio : informare il sistema che una chiamata in uscita sta per essere avviata.
  2. Avviato : informare il sistema che è stata avviata una chiamata in uscita.
  3. Connessione: informare il sistema che la chiamata in uscita si sta connettendo.
  4. Connessione ed - Informare che la chiamata in uscita è connessa e che entrambe le parti possono parlare ora.

Ad esempio, il codice seguente avvierà una chiamata in uscita:

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

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

public void StartCall (string contact)
{
    // Build call action
    var handle = new CXHandle (CXHandleType.Generic, contact);
    var startCallAction = new CXStartCallAction (new NSUuid (), handle);

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

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

Crea un CXHandle oggetto e lo usa per configurare un CXStartCallAction oggetto che viene inserito in un CXTransaction oggetto inviato al sistema usando il RequestTransaction metodo della CXCallController classe . Chiamando il RequestTransaction metodo, il sistema può effettuare qualsiasi chiamata esistente in attesa, indipendentemente dall'origine (Telefono'app, FaceTime, VOIP e così via), prima dell'avvio della nuova chiamata.

La richiesta di avviare una chiamata VOIP in uscita può provenire da diverse origini, ad esempio Siri, una voce in una scheda contatto (nell'app Contatti) o dall'elenco Recenti (nell'app Telefono). In queste situazioni, l'app verrà inviata una finalità di chiamata start all'interno di un NSUserActivity e AppDelegate dovrà gestirla:

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

In questo caso viene usato il CallHandleFromActivity metodo della classe StartCallRequest helper per ottenere l'handle alla persona chiamata (vedere la classe StartCallRequest precedente).

Il PerformStartCallAction metodo della classe ProviderDelegate viene usato per avviare infine la chiamata in uscita effettiva e informare il sistema del ciclo di vita:

public override void PerformStartCallAction (CXProvider provider, CXStartCallAction action)
{
    // Create new call record
    var activeCall = new ActiveCall (action.CallUuid, action.CallHandle.Value, true);

    // Monitor state changes
    activeCall.StartingConnectionChanged += (call) => {
        if (call.IsConnecting) {
            // Inform system that the call is starting
            Provider.ReportConnectingOutgoingCall (call.UUID, call.StartedConnectingOn.ToNSDate());
        }
    };

    activeCall.ConnectedChanged += (call) => {
        if (call.IsConnected) {
            // Inform system that the call has connected
            Provider.ReportConnectedOutgoingCall (call.UUID, call.ConnectedOn.ToNSDate ());
        }
    };

    // Start call
    activeCall.StartCall ((successful) => {
        // Was the call able to be started?
        if (successful) {
            // Yes, inform the system
            action.Fulfill ();

            // Add call to manager
            CallManager.Calls.Add (activeCall);
        } else {
            // No, inform system
            action.Fail ();
        }
    });
}

Crea un'istanza della ActiveCall classe (per contenere informazioni sulla chiamata in corso) e popola con la persona chiamata. Gli StartingConnectionChanged eventi e ConnectedChanged vengono usati per monitorare e segnalare il ciclo di vita delle chiamate in uscita. La chiamata viene avviata e il sistema ha informato che l'azione è stata soddisfatta.

Fine di una chiamata in uscita

Quando l'utente ha terminato una chiamata in uscita e vuole terminarla, è possibile usare il codice seguente:

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 crea un CXEndCallAction oggetto con l'UUID della chiamata alla fine, lo aggrega in un oggetto CXTransaction inviato al sistema usando il RequestTransaction metodo della CXCallController classe .

Dettagli aggiuntivi sul CallKit

Questa sezione illustra alcuni dettagli aggiuntivi che lo sviluppatore dovrà prendere in considerazione quando si usa CallKit, ad esempio:

  • Configurazione del provider
  • Errori delle azioni
  • Restrizioni di sistema
  • VOIP Audio

Configurazione dei provider

La configurazione del provider consente a un'app VOIP iOS 10 di personalizzare l'esperienza utente (all'interno dell'interfaccia utente in chiamata nativa) quando si usa CallKit.

Un'app può effettuare i tipi di personalizzazioni seguenti:

  • Visualizzare un nome localizzato.
  • Abilitare il supporto di videochiamata.
  • Personalizzare i pulsanti nell'interfaccia utente in chiamata presentando la propria icona dell'immagine modello. L'interazione dell'utente con pulsanti personalizzati viene inviata direttamente all'app da elaborare.

Errori di azione

Le app VOIP iOS 10 che usano CallKit devono gestire le azioni con esito negativo e mantenere sempre informato l'utente dello stato azione.

Prendere in considerazione l'esempio seguente:

  1. L'app ha ricevuto un'azione di chiamata di avvio e ha iniziato il processo di inizializzazione di una nuova chiamata VOIP con la rete di comunicazione.
  2. A causa di funzionalità di comunicazione di rete limitate o non disponibili, la connessione non riesce.
  3. L'app deve inviare il messaggio Failback all'azione Avvia chiamata (Action.Fail()) per informare il sistema dell'errore.
  4. Ciò consente al sistema di informare l'utente dello stato della chiamata. Ad esempio, per visualizzare l'interfaccia utente dell'errore di chiamata.

Inoltre, un'app VOIP iOS 10 dovrà rispondere agli errori di timeout che possono verificarsi quando un'azione prevista non può essere elaborata entro un determinato periodo di tempo. A ogni tipo di azione fornito da CallKit è associato un valore di timeout massimo. Questi valori di timeout assicurano che qualsiasi azione CallKit richiesta dall'utente venga gestita in modo reattivo, mantenendo il sistema operativo fluido e reattivo.

Esistono diversi metodi nel delegato del provider (CXProviderDelegate) che devono essere sottoposti a override per gestire correttamente anche queste situazioni di timeout.

Restrizioni di sistema

In base allo stato corrente del dispositivo iOS che esegue l'app iOS 10 VOIP, è possibile applicare determinate restrizioni di sistema.

Ad esempio, una chiamata VOIP in ingresso può essere limitata dal sistema se:

  1. La persona che chiama è nell'elenco dei chiamanti bloccati dell'utente.
  2. Il dispositivo iOS dell'utente è in modalità Non disturbare.

Se una chiamata VOIP è limitata da una di queste situazioni, usare il codice seguente per gestirla:

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 diversi vantaggi per la gestione delle risorse audio necessarie per un'app VOIP iOS 10 durante una chiamata VOIP in tempo reale. Uno dei principali vantaggi è che la sessione audio dell'app avrà priorità elevate durante l'esecuzione in iOS 10. Questo è lo stesso livello di priorità delle app predefinite Telefono e FaceTime e questo livello di priorità avanzata impedirà ad altre app in esecuzione di interrompere la sessione audio dell'app VOIP.

Inoltre, CallKit ha accesso ad altri hint di routing audio che possono migliorare le prestazioni e instradare in modo intelligente l'audio VOIP a dispositivi di output specifici durante una chiamata live in base alle preferenze utente e agli stati del dispositivo. Ad esempio, in base a dispositivi collegati, ad esempio cuffie Bluetooth, una connessione CarPlay live o impostazioni di accessibilità.

Durante il ciclo di vita di una tipica chiamata VOIP tramite CallKit, l'app dovrà configurare il flusso audio che callkit fornirà. Esaminare l'esempio seguente:

Sequenza di azioni di chiamata di avvio

  1. Un'azione di chiamata di avvio viene ricevuta dall'app per rispondere a una chiamata in arrivo.
  2. Prima che questa azione venga soddisfatta dall'app, fornisce la configurazione necessaria per il relativo AVAudioSession.
  3. L'app informa il sistema che l'azione è stata soddisfatta.
  4. Prima che la chiamata si connetta, CallKit fornisce una priorità AVAudioSession elevata corrispondente alla configurazione richiesta dall'app. L'app riceverà una notifica tramite il DidActivateAudioSession metodo del relativo CXProviderDelegateoggetto .

Uso delle estensioni della directory delle chiamate

Quando si usa CallKit, le estensioni della directory di chiamata consentono di aggiungere numeri di chiamata bloccati e identificare i numeri specifici di una determinata app VOIP ai contatti nell'app Contatto nel dispositivo iOS.

Implementazione di un'estensione della directory di chiamata

Per implementare un'estensione directory di chiamata in un'app Xamarin.iOS, eseguire le operazioni seguenti:

  1. Aprire la soluzione dell'app in Visual Studio per Mac.

  2. Fare clic con il pulsante destro del mouse sul nome della soluzione nel Esplora soluzioni e scegliere Aggiungi>nuovo progetto.

  3. Selezionare Estensioni iOS>Chiama estensioni> directory e fare clic sul pulsante Avanti:

    Creazione di una nuova estensione della directory di chiamata

  4. Immettere un nome per l'estensione e fare clic sul pulsante Avanti :

    Immissione di un nome per l'estensione

  5. Modificare il nome del progetto e/o il nome della soluzione, se necessario, e fare clic sul pulsante Crea:

    Creazione del progetto

Verrà aggiunta una CallDirectoryHandler.cs classe al progetto simile alla seguente:

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

Il BeginRequest metodo nel gestore della directory di chiamata dovrà essere modificato per fornire la funzionalità necessaria. Nel caso dell'esempio precedente, tenta di impostare l'elenco di numeri bloccati e disponibili nel database dei contatti dell'app VOIP. Se una delle richieste ha esito negativo per qualsiasi motivo, creare un oggetto NSError per descrivere l'errore e passarlo al CancelRequest metodo della CXCallDirectoryExtensionContext classe .

Per impostare i numeri bloccati, utilizzare il AddBlockingEntry metodo della CXCallDirectoryExtensionContext classe . I numeri forniti al metodo devono essere in ordine numerico crescente. Per prestazioni ottimali e utilizzo della memoria quando sono presenti molti numeri di telefono, è consigliabile caricare solo un sottoinsieme di numeri in un determinato momento e usare pool di versioni automatica per rilasciare gli oggetti allocati durante ogni batch di numeri caricati.

Per informare l'app Contatto dei numeri di contatto noti all'app VOIP, usare il AddIdentificationEntry metodo della CXCallDirectoryExtensionContext classe e specificare sia il numero che un'etichetta di identificazione. Anche in questo caso, i numeri forniti al metodo devono essere in ordine numerico crescente. Per prestazioni ottimali e utilizzo della memoria quando sono presenti molti numeri di telefono, è consigliabile caricare solo un sottoinsieme di numeri in un determinato momento e usare pool di versioni automatica per rilasciare gli oggetti allocati durante ogni batch di numeri caricati.

Riepilogo

Questo articolo ha illustrato la nuova API CallKit rilasciata da Apple in iOS 10 e come implementarla nelle app VOIP Xamarin.iOS. Ha mostrato in che modo CallKit consente a un'app di integrarsi nel sistema iOS, come fornisce parità di funzionalità con le app predefinite (ad esempio Telefono) e come aumenta la visibilità di un'app in tutti iOS in posizioni come blocco e schermate home, tramite interazioni siri e tramite le app Contatti.