Sdílet prostřednictvím


CallKit v Xamarin.iOS

Nové rozhraní CallKit API v iOSu 10 poskytuje způsob, jak integrovat aplikace VOIP s uživatelským rozhraním i Telefon a poskytnout koncovému uživateli známé rozhraní a prostředí. Díky tomuto rozhraní API můžou uživatelé rozhraní API zobrazovat a pracovat s voláními VOIP ze zamykací obrazovky zařízení s iOSem a spravovat kontakty pomocí zobrazení Oblíbené a Poslední položky aplikace Telefon.

Informace o CallKitu

Podle Společnosti Apple je CallKit novou architekturou, která zvýší úroveň aplikací Voice Over IP (VOIP) třetích stran na prostředí 1. strany v iOSu 10. Rozhraní CallKit API umožňuje, aby se aplikace VOIP integrují s uživatelským rozhraním i Telefon a koncovým uživatelům poskytovaly známé rozhraní a prostředí. Stejně jako integrovaná aplikace Telefon může uživatel zobrazovat a pracovat s voláními VOIP ze zamykací obrazovky zařízení s iOSem a spravovat kontakty pomocí zobrazení Oblíbené a Poslední položky aplikace Telefon.

Kromě toho rozhraní CallKit API poskytuje možnost vytvářet rozšíření aplikací, která můžou přidružit telefonní číslo ke jménu (ID volajícího) nebo říct systému, kdy má být číslo zablokováno (blokování volání).

Stávající prostředí aplikace VOIP

Než probereme nové rozhraní CALLKit API a jeho schopnosti, podívejte se na aktuální uživatelské prostředí s aplikací VOIP třetí strany v iOSu 9 (a menší) pomocí fiktivní aplikace VOIP s názvem MonkeyCall. MonkeyCall je jednoduchá aplikace, která uživateli umožňuje odesílat a přijímat volání VOIP pomocí stávajících rozhraní API pro iOS.

V současné době, pokud uživatel dostává příchozí hovor na MonkeyCall a jeho i Telefon je uzamčen, oznámení přijaté na zamykací obrazovce je nerozlišitelné od jakéhokoli jiného typu oznámení (například z aplikací Zprávy nebo Pošta).

Pokud chce uživatel hovor přijmout, bude muset posunout oznámení MonkeyCall, aby otevřel aplikaci a zadal heslo (nebo touch ID uživatele), aby telefon odemkl, než mohl hovor přijmout a zahájit konverzaci.

Prostředí je stejně těžkopádné, pokud je telefon odemknutý. Příchozí hovor MonkeyCall se znovu zobrazí jako standardní informační nápis, který se posune z horní části obrazovky. Vzhledem k tomu, že oznámení je dočasné, může ho uživatel snadno zmeškat, aby buď otevřel Centrum oznámení, a našel konkrétní oznámení, aby pak volal nebo našel a spustil aplikaci MonkeyCall ručně.

Prostředí aplikace CallKit VOIP

Díky implementaci nových rozhraní API CallKitu v aplikaci MonkeyCall může být prostředí uživatele s příchozím voláním VOIP výrazně vylepšeno v iOSu 10. Příklad uživatele, který přijímá hovor VOIP, když je telefon uzamčen od výše. Implementací CallKitu se volání zobrazí na zamykací obrazovce i Telefon stejně jako kdyby se hovor přijímal z integrované aplikace Telefon s nativním uživatelským rozhraním a standardní funkcí potáhnutí prstem na odpověď.

Opět platí, že pokud i Telefon je odemknutý při přijetí volání MonkeyCall VOIP, stejné celé obrazovky, nativní uživatelské rozhraní a standardní funkce potáhnutí prstem na odpověď a klepnutí na odmítnutí integrované aplikace Telefon je prezentována a MonkeyCall má možnost přehrávat vlastní vyzváněcí tón.

CallKit poskytuje pro MonkeyCall další funkce, které umožňují volání VOIP komunikovat s jinými typy volání, aby se objevila v předdefinovaných seznamech Poslední a Oblíbené, aby používala integrované funkce Nerušit a Blokovat, spusťte volání MonkeyCall ze Siri a nabízí uživatelům možnost přiřazovat volání MonkeyCall lidem v aplikaci Kontakty.

V následujících částech najdete podrobnosti o architektuře CallKitu, příchozích a odchozích voláních a rozhraní API CallKitu.

Architektura CallKitu

V iOSu 10 společnost Apple přijala CallKit ve všech systémových službách tak, aby volání na CarPlay, například, jsou známé v uživatelském rozhraní systému prostřednictvím CallKit. V následujícím příkladu, protože MonkeyCall přijímá CallKit, je známý systémem stejným způsobem jako tyto integrované systémové služby a získá všechny stejné funkce:

Zásobník služby CallKit

Podívejte se blíže na aplikaci MonkeyCall z výše uvedeného diagramu. Aplikace obsahuje veškerý svůj kód pro komunikaci s vlastní sítí a obsahuje vlastní uživatelská rozhraní. Odkazuje v CallKitu na komunikaci se systémem:

Architektura aplikace MonkeyCall

V CallKitu existují dvě hlavní rozhraní, která aplikace používá:

  • CXProvider - To umožňuje aplikaci MonkeyCall informovat systém o všech nesmítěných oznámeních, ke kterým může dojít.
  • CXCallController - Umožňuje aplikaci MonkeyCall informovat systém místních uživatelských akcí.

The CXProvider

Jak je uvedeno výše, CXProvider umožňuje aplikaci informovat systém o případných nesrozuměných oznámeních. Jedná se o oznámení, ke kterým nedochází kvůli místním uživatelským akcím, ale dochází k nim kvůli externím událostem, jako jsou příchozí hovory.

Aplikace by měla používat CXProvider následující:

  • Nahlašte příchozí hovor do systému.
  • Nahlašte, že odchozí hovor je připojený k systému.
  • Nahlašte vzdáleného uživatele, který ukončí volání systému.

Když chce aplikace komunikovat se systémem, používá CXCallUpdate třídu a když systém potřebuje komunikovat s aplikací, používá třídu CXAction :

Komunikace se systémem prostřednictvím CXProvideru

The CXCallController

Umožňuje CXCallController aplikaci informovat systém o akcích místního uživatele, jako je například uživatel, který spouští volání VOIP. Implementace CXCallController aplikace se dostane k vzájemnému propojení s jinými typy volání v systému. Pokud už například probíhá aktivní telefonní hovor, CXCallController může aplikaci VOIP povolit, aby hovor přidržel a spustil nebo zvednul hovor VOIP.

Aplikace by měla používat CXCallController následující:

  • Nahlásit, když uživatel spustil odchozí hovor do systému.
  • Nahlásit, když uživatel odpoví na příchozí hovor do systému.
  • Nahlašte, když uživatel ukončí volání systému.

Když chce aplikace komunikovat s místním uživatelem akce systému, používá CXTransaction třídu:

Generování sestav systému pomocí CXCallController

Implementace CallKitu

Následující části ukazují, jak implementovat CallKit v aplikaci VOIP Xamarin.iOS. V zájmu příkladu bude tento dokument používat kód z fiktivní aplikace MonkeyCall VOIP. Zde uvedený kód představuje několik podpůrných tříd, konkrétní části CallKit budou podrobně popsány v následujících částech.

ActiveCall – třída

Třída je používána ActiveCall aplikací MonkeyCall k uložení všech informací o volání VOIP, které je aktuálně aktivní následujícím způsobem:

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 obsahuje několik vlastností, které definují stav volání a dvě události, které lze vyvolat při změně stavu volání. Vzhledem k tomu, že se jedná pouze o příklad, existují tři metody, které se používají k simulaci spuštění, odpovídání a ukončení volání.

StartCallRequest – třída

StartCallRequest Statická třída poskytuje několik pomocných metod, které se použijí při práci s odchozími voláními:

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

Třídy CallHandleFromURL a CallHandleFromActivity třídy se používají v AppDelegate k získání popisovače kontaktu osoby, která se volá v odchozím hovoru. Další informace najdete v části Zpracování odchozích hovorů níže.

Třída ActiveCallManager

Třída ActiveCallManager zpracovává všechna otevřená volání v aplikaci 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
    }
}

Opět platí, že protože se jedná pouze o simulaci, ActiveCallManager udržuje jedinou kolekci ActiveCall objektů a má rutinu pro vyhledání daného volání podle jeho UUID vlastnosti. Zahrnuje také metody zahájení, ukončení a změny stavu blokování odchozího hovoru. Další informace najdete v části Zpracování odchozích hovorů níže.

ProviderDelegate – třída

Jak je popsáno výše, poskytuje obousměrnou CXProvider komunikaci mezi aplikací a systémem pro oznámení mimo pásmo. Vývojář musí poskytnout vlastní CXProviderDelegate a připojit ho k CXProvider aplikaci, aby mohla zpracovávat vzdálené události CallKit. MonkeyCall používá následující 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
    }
}

Když se vytvoří instance tohoto delegáta, předá ActiveCallManager se mu, že bude zpracovávat všechny aktivity volání. Dále definuje typy popisovačů (CXHandleType), na které CXProvider bude reagovat:

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

A načte obrázek šablony, který se použije na ikonu aplikace, když probíhá volání:

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

Tyto hodnoty se zabalí do CXProviderConfiguration sady, která se použije ke konfiguraci CXProvider:

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

Delegát pak vytvoří novou CXProvider s těmito konfiguracemi a připojí se k němu:

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

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

Když používáte CallKit, aplikace už nebude vytvářet a zpracovávat vlastní zvukové relace, místo toho bude muset nakonfigurovat a používat zvukovou relaci, kterou systém vytvoří a zpracuje pro ni.

Pokud by se jednalo o skutečnou aplikaci, DidActivateAudioSession metoda by se použila ke spuštění volání s předem nakonfigurovaným AVAudioSession systémem, který poskytl:

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

Metoda by také použila DidDeactivateAudioSession k dokončení a uvolnění jeho připojení k zadané zvukové relaci systému:

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

Zbývající část kódu bude podrobně popsána v následujících částech.

AppDelegate – třída

MonkeyCall používá AppDelegate k uložení instancí ActiveCallManager a CXProviderDelegate které se použijí v celé aplikaci:

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

Metody OpenUrl a ContinueUserActivity přepsání se používají, když aplikace zpracovává odchozí hovor. Další informace najdete v části Zpracování odchozích hovorů níže.

Zpracování příchozích hovorů

Existuje několik stavů a procesů, kterými může příchozí volání VOIP projít během typického pracovního postupu příchozího volání, například:

  • Informuje uživatele (a systém), že existuje příchozí hovor.
  • Když chce uživatel přijmout hovor a inicializovat hovor s jiným uživatelem, obdrží oznámení.
  • Informujte systém a komunikační síť, když chce uživatel ukončit aktuální volání.

V následujících částech se podrobně podíváme na to, jak může aplikace pomocí CallKitu zpracovat pracovní postup příchozího volání, a to znovu pomocí aplikace MonkeyCall VOIP jako příklad.

Informování uživatele o příchozím hovoru

Když vzdálený uživatel spustil konverzaci VOIP s místním uživatelem, dojde k následujícímu:

Vzdálený uživatel spustil konverzaci VOIP.

  1. Aplikace obdrží oznámení ze své komunikační sítě, že existuje příchozí volání VOIP.
  2. Aplikace používá CXProvider k odeslání CXCallUpdate do systému zprávu o volání.
  3. Systém publikuje volání do systémového uživatelského rozhraní, systémových služeb a všech dalších aplikací VOIP pomocí CallKitu.

Například v :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);
        }
    });
}

Tento kód vytvoří novou CXCallUpdate instanci a připojí k němu popisovač, který identifikuje volajícího. Dále používá ReportNewIncomingCall metodu CXProvider třídy k informování systému volání. Pokud je úspěšné, volání se přidá do kolekce aktivních volání aplikace, pokud ne, musí být uživateli nahlášena chyba.

Příjem příchozího hovoru uživatelem

Pokud chce uživatel přijmout příchozí volání VOIP, dojde k následujícímu:

Uživatel odpoví na příchozí hovor VOIP.

  1. Uživatelské rozhraní systému informuje systém, že uživatel chce přijmout volání VOIP.
  2. Systém pošle CXAnswerCallAction aplikaci CXProvider informace o záměru odpovědi.
  3. Aplikace informuje svou komunikační síť, že uživatel odpovídá na hovor a volání VOIP pokračuje obvyklým způsobem.

Například v :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 ();
        }
    });
}

Tento kód nejprve vyhledá dané volání v seznamu aktivních volání. Pokud se volání nenašlo, systém se upozorní a metoda se ukončí. Pokud je nalezena, AnswerCall metoda ActiveCall třídy je volána ke spuštění volání a systém je informace, pokud je úspěšná nebo neúspěšná.

Uživatel, který končí příchozí hovor

Pokud chce uživatel ukončit volání z uživatelského rozhraní aplikace, dojde k následujícímu:

Uživatel ukončí volání z uživatelského rozhraní aplikace.

  1. Aplikace vytvoří CXEndCallAction , která se spojí do CXTransaction systému, aby informovala, že volání končí.
  2. Systém ověří záměr koncového volání a přesměruje CXEndCallAction zpět do aplikace CXProvider.
  3. Aplikace pak informuje svou komunikační síť, že volání končí.

Například v :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 ();
        }
    });
}

Tento kód nejprve vyhledá dané volání v seznamu aktivních volání. Pokud se volání nenašlo, systém se upozorní a metoda se ukončí. Pokud je nalezena, EndCall metoda ActiveCall třídy je volána k ukončení volání a systém je informace, pokud je úspěšná nebo selže. V případě úspěchu se volání odebere z kolekce aktivních volání.

Správa více volání

Většina aplikací VOIP dokáže zpracovat více volání najednou. Pokud je aktuálně aktivní hovor VOIP a aplikace obdrží oznámení, že existuje nový příchozí hovor, může uživatel pozastavit nebo zavěsit při prvním hovoru, aby odpověděl na druhý hovor.

V situaci výše odešle CXTransaction systém aplikaci, která bude obsahovat seznam více akcí (například a CXEndCallActionCXAnswerCallAction). Všechny tyto akce musí být splněny jednotlivě, aby systém mohl odpovídajícím způsobem aktualizovat uživatelské rozhraní.

Zpracování odchozích hovorů

Pokud uživatel klepne na položku ze seznamu Poslední (v aplikaci Telefon), například z volání, které patří do aplikace, odešle systém záměr zahájení volání:

Příjem záměru počátečního volání

  1. Aplikace vytvoří akci zahájit volání na základě záměru zahájit volání, který přijal ze systému.
  2. Aplikace použije CXCallController k vyžádání akce zahájit volání ze systému.
  3. Pokud systém akce přijme, vrátí se do aplikace prostřednictvím delegáta XCProvider .
  4. Aplikace spustí odchozí hovor se svou komunikační sítí.

Další informace o záměrech najdete v naší dokumentaci k rozšířením záměrů a záměrů uživatelského rozhraní .

Životní cyklus odchozích hovorů

Při práci s CallKitem a odchozím voláním bude aplikace muset informovat systém o následujících událostech životního cyklu:

  1. Spuštění – Informujte systém, že odchozí hovor se chystá zahájit.
  2. Spuštěno – Informujte systém, že se spustil odchozí hovor.
  3. Připojení - Informujte systém, že se odchozí hovor připojuje.
  4. Připojení ed - Informujte odchozí hovor, který je připojený a že obě strany mohou mluvit nyní.

Například následující kód spustí odchozí hovor:

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

CXHandle Vytvoří a použije ho ke konfiguraciCXStartCallAction, která je součástí balíčku do CXTransaction systému, který je odeslán do systému pomocí RequestTransaction metody CXCallController třídy. Voláním RequestTransaction metody může systém umístit všechna existující volání do blokování, bez ohledu na zdroj (Telefon aplikace, FaceTime, VOIP atd.), před spuštěním nového volání.

Požadavek na zahájení odchozího hovoru VOIP může pocházet z několika různých zdrojů, jako je Siri, záznam na kartě kontaktu (v aplikaci Kontakty) nebo ze seznamu Poslední (v aplikaci Telefon). V těchto situacích se aplikaci odešle záměr zahájení volání uvnitř NSUserActivity a AppDelegate ji bude muset zpracovat:

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

Zde metoda CallHandleFromActivity pomocné třídy StartCallRequest se používá k získání popisovače pro osobu, která je volána (viz StartCallRequest Třída výše).

PerformStartCallAction Metoda ProviderDelegate Třída se používá k konečně spuštění skutečného odchozího volání a informovat systém o jeho životním cyklu:

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

Vytvoří instanci ActiveCall třídy (pro uchování informací o probíhajícím hovoru) a naplní volající osobu. ConnectedChanged Události StartingConnectionChanged se používají k monitorování a hlášení životního cyklu odchozích hovorů. Hovor se spustí a systém informuje, že akce byla splněna.

Ukončení odchozího hovoru

Jakmile uživatel dokončí odchozí hovor a chce ho ukončit, můžete použít následující kód:

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

Pokud vytvoří CXEndCallAction identifikátor UUID volání na konec, spojí ho do CXTransaction systému, který se odešle do systému pomocí RequestTransaction metody CXCallController třídy.

Další podrobnosti o CallKitu

Tato část se věnuje několika dalším podrobnostem, které bude vývojář muset vzít v úvahu při práci s CallKitem, například:

  • Konfigurace zprostředkovatele
  • Chyby akcí
  • Systémová omezení
  • Zvuk VOIP

Konfigurace zprostředkovatele

Konfigurace zprostředkovatele umožňuje aplikaci VOIP pro iOS 10 přizpůsobit uživatelské prostředí (v nativním uživatelském rozhraní pro volání) při práci s CallKitem.

Aplikace může provádět následující typy přizpůsobení:

  • Zobrazí lokalizovaný název.
  • Povolte podporu videohovorů.
  • Přizpůsobte tlačítka uživatelského rozhraní v uživatelském rozhraní pro volání tak, že předáte vlastní ikonu obrázku šablony. Interakce uživatele s vlastními tlačítky se odesílá přímo do aplikace, která se má zpracovat.

Chyby akcí

Aplikace VOIP pro iOS 10 využívající CallKit musí zpracovávat akce, které selhávají, a udržovat uživatele neustále informovaný o stavu akce.

Vezměte v úvahu následující příklad:

  1. Aplikace přijala akci zahájení volání a zahájila proces inicializace nového volání VOIP se svou komunikační sítí.
  2. Kvůli omezené nebo žádné možnosti síťové komunikace se toto připojení nezdaří.
  3. Aplikace musí odeslat zprávu o navrácení služeb po obnovení zpět do akce Zahájení volání (Action.Fail()), aby informovala systém o selhání.
  4. To umožňuje systému informovat uživatele o stavu volání. Pokud například chcete zobrazit uživatelské rozhraní selhání volání.

Aplikace IOS 10 VOIP navíc bude muset reagovat na chyby časového limitu, ke kterým může dojít v případě, že očekávanou akci nelze zpracovat v daném časovém intervalu. Každý typ akce, který poskytuje CallKit, má přidruženou maximální hodnotu časového limitu. Tyto hodnoty časového limitu zajišťují, aby se všechny akce CallKit požadované uživatelem zpracovávaly responzivním způsobem, a proto zachovávají tekutinu a odezvu operačního systému.

Existuje několik metod pro delegáta zprostředkovatele (CXProviderDelegate), které by se měly přepsat, aby se řádně zvládly i tyto situace časového limitu.

Systémová omezení

Na základě aktuálního stavu zařízení s iOSem, na kterém běží aplikace VOIP pro iOS 10, se můžou vynutit určitá systémová omezení.

Například příchozí volání VOIP může systém omezit, pokud:

  1. Volající je v seznamu blokovaných volajících uživatele.
  2. Zařízení s iOSem uživatele je v režimu Do-Not-Disturb.

Pokud je volání VOIP omezeno některým z těchto situací, použijte k jeho zpracování následující kód:

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

}

Zvuk VOIP

CallKit nabízí několik výhod pro zpracování zvukových prostředků, které bude aplikace VOIP pro iOS 10 vyžadovat během živého volání VOIP. Jednou z největších výhod je, že zvuková relace aplikace bude mít při spuštění v iOSu 10 zvýšenou prioritu. Tato úroveň priority je stejná jako předdefinovaná aplikace Telefon a FaceTime a tato rozšířená úroveň priority zabrání ostatním spuštěným aplikacím přerušit zvukovou relaci aplikace VOIP.

Kromě toho má CallKit přístup k dalším tipům směrování zvuku, které můžou zvýšit výkon a inteligentně směrovat zvuk VOIP na konkrétní výstupní zařízení během živého hovoru na základě uživatelských předvoleb a stavů zařízení. Například na základě připojených zařízení, jako jsou sluchátka Bluetooth, živé připojení CarPlay nebo nastavení přístupnosti.

Během životního cyklu typického volání VOIP pomocí CallKitu bude aplikace muset nakonfigurovat zvukový stream, který mu CallKit poskytne. Podívejte se na následující příklad:

Pořadí akcí zahájení volání

  1. Aplikace přijme akci zahájení hovoru, aby odpověděla na příchozí hovor.
  2. Před splněním této akce aplikace poskytuje konfiguraci, která bude pro svou AVAudioSessionaplikaci vyžadovat .
  3. Aplikace informuje systém, že akce byla splněna.
  4. Než se hovor připojí, CallKit poskytuje vysokou prioritu AVAudioSession odpovídající konfiguraci, kterou aplikace požadovala. Aplikace bude upozorněna metodou DidActivateAudioSession jeho CXProviderDelegate.

Práce s rozšířeními adresáře volání

Při práci s CallKitem poskytují rozšíření adresářů volání způsob, jak přidat blokovaná telefonní čísla a identifikovat čísla specifická pro danou aplikaci VOIP kontaktům v aplikaci Kontakt na zařízení s iOSem.

Implementace rozšíření adresáře volání

Pokud chcete implementovat rozšíření adresáře volání v aplikaci Xamarin.iOS, postupujte takto:

  1. Otevřete řešení aplikace v Visual Studio pro Mac.

  2. Klikněte pravým tlačítkem na název řešení v Průzkumník řešení a vyberte Přidat>nový projekt.

  3. Vyberte rozšíření rozšíření>pro rozšíření iOS>a klikněte na tlačítko Další:

    Vytvoření nového rozšíření adresáře volání

  4. Zadejte název rozšíření a klikněte na tlačítko Další:

    Zadání názvu rozšíření

  5. V případě potřeby upravte název projektu nebo název řešení a klikněte na tlačítko Vytvořit:

    Vytvoření projektu

Tím do projektu přidáte CallDirectoryHandler.cs třídu, která vypadá takto:

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

Metoda BeginRequest v obslužné rutině adresáře volání bude nutné upravit tak, aby poskytovala požadované funkce. V případě výše uvedeného vzorku se pokusí nastavit seznam blokovaných a dostupných čísel v databázi kontaktů aplikace VOIP. Pokud některý z těchto požadavků z nějakého důvodu selže, vytvořte popis NSError selhání a předejte ji CancelRequest metodu CXCallDirectoryExtensionContext třídy.

K nastavení blokovaných čísel použijte AddBlockingEntry metodu CXCallDirectoryExtensionContext třídy. Čísla zadaná metodě musí být v číselném vzestupném pořadí. Pokud chcete dosáhnout optimálního výkonu a využití paměti, pokud existuje mnoho telefonních čísel, zvažte načtení pouze podmnožinu čísel v daném okamžiku a použití automatických fondů k uvolnění objektů přidělených během každé dávky čísel, která jsou načtena.

Chcete-li informovat aplikaci Contact o kontaktních číslech známých pro aplikaci VOIP, použijte AddIdentificationEntry metodu CXCallDirectoryExtensionContext třídy a zadejte číslo i identifikační popisek. Opět musí být čísla zadaná metodě v číselném vzestupném pořadí. Pokud chcete dosáhnout optimálního výkonu a využití paměti, pokud existuje mnoho telefonních čísel, zvažte načtení pouze podmnožinu čísel v daném okamžiku a použití automatických fondů k uvolnění objektů přidělených během každé dávky čísel, která jsou načtena.

Shrnutí

Tento článek se zabývá novým rozhraním CallKit API, které apple vydal v iOS 10 a jak ho implementovat v aplikacích Xamarin.iOS VOIP. Ukázalo se, jak CallKit umožňuje integraci aplikace do systému iOS, jak poskytuje paritu funkcí s integrovanými aplikacemi (například Telefon) a jak zvyšuje viditelnost aplikace v celém iOSu v umístěních, jako jsou zamykací a domovské obrazovky, prostřednictvím interakcí Siri a aplikací Kontakty.