CallKit в Xamarin.iOS

Новый API CallKit в iOS 10 позволяет приложениям VOIP интегрироваться с пользовательским интерфейсом i Телефон и предоставлять знакомый интерфейс и взаимодействие с конечным пользователем. С помощью этого API пользователи могут просматривать и взаимодействовать с вызовами VOIP с экрана блокировки устройства iOS и управлять контактами с помощью представлений избранного и последних приложений приложения Телефон.

Сведения о CallKit

Согласно Apple, CallKit — это новая платформа, которая позволит повысить уровень сторонних приложений Voice Over IP (VOIP) до 1-го стороннего интерфейса в iOS 10. API CallKit позволяет приложениям VOIP интегрироваться с пользовательским интерфейсом i Телефон и предоставлять знакомый интерфейс и взаимодействие с конечным пользователем. Как и встроенное приложение Телефон, пользователь может просматривать и взаимодействовать с вызовами VOIP с экрана блокировки устройства iOS и управлять контактами с помощью избранного и последних представлений приложения Телефон.

Кроме того, API CallKit предоставляет возможность создавать расширения приложений, которые могут связать номер телефона с именем (идентификатор вызывающего абонента) или сообщить системе, когда номер должен быть заблокирован (блокировка звонка).

Существующий интерфейс приложения VOIP

Прежде чем обсуждать новый API CallKit и его возможности, ознакомьтесь с текущим взаимодействием пользователя с сторонним приложением VOIP в iOS 9 (и меньше) с помощью вымышленного приложения VOIP под названием MonkeyCall. MonkeyCall — это простое приложение, которое позволяет пользователю отправлять и получать вызовы VOIP с помощью существующих API iOS.

В настоящее время, если пользователь получает входящие вызовы в MonkeyCall и их i Телефон заблокирован, уведомление, полученное на экране блокировки, неотличимо от любого другого типа уведомления (например, из приложений "Сообщения" или "Почта").

Если пользователь хотел ответить на звонок, им придется слайдировать уведомление MonkeyCall, чтобы открыть приложение и ввести свой секретный код (или user Touch ID), чтобы разблокировать телефон, прежде чем они смогут принять звонок и начать беседу.

При разблокировке телефона это не менее сложно. Опять же, входящий вызов MonkeyCall отображается в виде стандартного баннера уведомления, который скользит в верхней части экрана. Так как уведомление является временным, пользователь может легко пропустить его, чтобы открыть Центр уведомлений и найти определенное уведомление, чтобы ответить на вызов или найти и запустить приложение MonkeyCall вручную.

Интерфейс приложения VoIP CallKit

Реализуя новые API CallKit в приложении MonkeyCall, взаимодействие пользователя с входящим вызовом VOIP может быть значительно улучшено в iOS 10. Рассмотрим пример пользователя, получающего voIP-звонок, когда его телефон заблокирован выше. Реализуя CallKit, вызов появится на экране блокировки i Телефон так же, как и при получении вызова из встроенного приложения Телефон с полноэкранным интерфейсом, собственным пользовательским интерфейсом и стандартной функцией прокрутки к ответу.

Опять же, если i Телефон разблокируется при получении вызова VOIP MonkeyCall, то в собственном пользовательском интерфейсе и стандартном пальцем к ответу и нажатием на отклонение функциональных возможностей встроенного приложения Телефон представлено, и MonkeyCall имеет возможность воспроизводить настраиваемый мелодию звонка.

CallKit предоставляет дополнительные функциональные возможности для MonkeyCall, позволяя своим вызовам VOIP взаимодействовать с другими типами вызовов, отображаться в встроенных списках "Последние" и "Избранное", использовать встроенные функции "Не беспокоить" и "Блокировать", запускать вызовы MonkeyCall из Siri и предоставлять пользователям возможность назначать вызовы MonkeyCall пользователям в приложении "Контакты".

В следующих разделах подробно рассматриваются архитектура CallKit, входящие и исходящие потоки вызовов и API CallKit.

Архитектура CallKit

В iOS 10 Apple приняла CallKit во всех системных службах, таких как вызовы, выполненные в CarPlay, например, известны системный пользовательский интерфейс через CallKit. В приведенном ниже примере, так как MonkeyCall принимает CallKit, оно известно системе таким же образом, как эти встроенные системные службы и получают все те же функции:

The CallKit Service Stack

Взгляните на приложение MonkeyCall из приведенной выше схемы. Приложение содержит весь код для взаимодействия с собственной сетью и содержит собственные пользовательские интерфейсы. Он ссылается на CallKit для взаимодействия с системой:

MonkeyCall App Architecture

В CallKit используются два основных интерфейса:

  • CXProvider — Это позволяет приложению MonkeyCall сообщить о системе любых внеполосных уведомлений, которые могут возникнуть.
  • CXCallController — позволяет приложению MonkeyCall информировать систему действий локального пользователя.

The CXProvider

Как упоминалось выше, CXProvider приложение позволяет приложению информировать систему любых внеполосных уведомлений, которые могут возникнуть. Это уведомление, которое не происходит из-за действий локального пользователя, но происходит из-за внешних событий, таких как входящие вызовы.

Приложение должно использовать следующее CXProvider :

  • Сообщите о входном вызове системы.
  • Сообщите об исходящем вызове, подключенном к системе.
  • Сообщите удаленному пользователю о завершении вызова системы.

Когда приложение хочет взаимодействовать с системой, оно использует CXCallUpdate класс и когда системе нужно взаимодействовать с приложением, он использует CXAction класс:

Communicating with the system via a CXProvider

The CXCallController

Приложение CXCallController позволяет приложению информировать систему действий локальных пользователей, таких как пользователь, запускающий вызов VOIP. Реализация CXCallController приложения позволяет взаимодействовать с другими типами вызовов в системе. Например, если уже существует активный телефонный звонок, CXCallController приложение VOIP может разместить этот звонок на удержание и начать или ответить на вызов VOIP.

Приложение должно использовать следующее CXCallController :

  • Сообщите, когда пользователь начал исходящий вызов системы.
  • Сообщите, когда пользователь отвечает на входящий вызов системы.
  • Сообщите, когда пользователь завершает вызов системы.

Когда приложение хочет обмениваться действиями локального пользователя с системой, оно использует CXTransaction класс:

Reporting to the system using a CXCallController

Реализация CallKit

В следующих разделах показано, как реализовать CallKit в приложении VOIP Xamarin.iOS. В качестве примера этот документ будет использовать код из вымышленного приложения MonkeyCall VOIP. Приведенный здесь код представляет несколько вспомогательных классов, определенные части CallKit подробно описаны в следующих разделах.

Класс ActiveCall

Класс ActiveCall используется приложением MonkeyCall для хранения всех сведений о вызове VOIP, который в настоящее время активен следующим образом:

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 содержит несколько свойств, определяющих состояние вызова и два события, которые могут возникать при изменении состояния вызова. Так как это только пример, существует три метода, используемые для имитации запуска, ответа и завершения вызова.

Класс StartCallRequest

Статический StartCallRequest класс предоставляет несколько вспомогательных методов, которые будут использоваться при работе с исходящими вызовами:

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

CallHandleFromActivity Классы CallHandleFromURL используются в AppDelegate, чтобы получить дескриптор контакта вызываемого пользователя в исходящем вызове. Дополнительные сведения см. в разделе "Обработка исходящих вызовов " ниже.

Класс ActiveCallManager

Класс ActiveCallManager обрабатывает все открытые вызовы в приложении 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
    }
}

Опять же, так как это только имитация, единственное ActiveCallManager поддерживает коллекцию объектов и имеет подпрограмму для поиска заданного ActiveCall вызова его UUID свойством. Он также включает методы запуска, завершения и изменения состояния исходящего вызова. Дополнительные сведения см. в разделе "Обработка исходящих вызовов " ниже.

Класс ProviderDelegate

Как описано выше, предоставляет CXProvider двустороннее взаимодействие между приложением и системой для уведомлений вне диапазона. Разработчику необходимо предоставить пользовательский CXProviderDelegate и присоединить его к CXProvider приложению для обработки событий CallKit вне группы. MonkeyCall использует следующее 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
    }
}

При создании экземпляра этого делегата передается ActiveCallManager тот, который будет использоваться для обработки любого действия вызова. Далее он определяет типы дескрипторов (CXHandleType), которые CXProvider будут отвечать на следующие действия:

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

И получает образ шаблона, который будет применен к значку приложения при выполнении вызова:

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

Эти значения объединяются в объект CXProviderConfiguration , который будет использоваться для настройки CXProvider:

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

Затем делегат создает новый CXProvider с этими конфигурациями и подключается к нему:

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

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

При использовании CallKit приложение больше не создаст и обработает собственные звуковые сеансы, вместо этого потребуется настроить и использовать звуковой сеанс, который система создаст и обработает для него.

Если это было реальное приложение, DidActivateAudioSession метод будет использоваться для запуска вызова с предварительно настроенной AVAudioSession системой:

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

Он также будет использовать DidDeactivateAudioSession метод для завершения и выпуска его подключения к системе предоставленного звукового сеанса:

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

Остальная часть кода подробно рассматривается в следующих разделах.

Класс AppDelegate

MonkeyCall использует AppDelegate для хранения экземпляров ActiveCallManagerCXProviderDelegate и которые будут использоваться во всем приложении:

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

Методы OpenUrl и ContinueUserActivity переопределение используются при обработке исходящего вызова приложения. Дополнительные сведения см. в разделе "Обработка исходящих вызовов " ниже.

Обработка входящих вызовов

Существует несколько состояний и процессов, которые может пройти входящий вызов VOIP во время типичного рабочего процесса входящего вызова, например:

  • Уведомляя пользователя (и систему), что входящий вызов существует.
  • Получение уведомления, когда пользователь хочет ответить на звонок и инициализировать звонок с другим пользователем.
  • Сообщите системе и сети коммуникации, когда пользователь хочет завершить текущий вызов.

В следующих разделах вы узнаете, как приложение может использовать CallKit для обработки рабочего процесса входящего вызова, опять же с помощью приложения VoIP MonkeyCall в качестве примера.

Информирование пользователя о входящем вызове

Когда удаленный пользователь начал беседу VOIP с локальным пользователем, происходит следующее:

A remote user has started a VOIP conversation

  1. Приложение получает уведомление из сети связи, что есть входящий вызов VOIP.
  2. Приложение используется CXProvider для отправки CXCallUpdate в систему уведомления о вызове.
  3. Система публикует вызов системного пользовательского интерфейса, системных служб и других приложений VOIP с помощью CallKit.

Например, в :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);
        }
    });
}

Этот код создает новый CXCallUpdate экземпляр и присоединяет к нему дескриптор, который будет определять вызывающий объект. Затем он использует ReportNewIncomingCall метод CXProvider класса для информирования системы вызова. При успешном выполнении вызов добавляется в коллекцию активных вызовов приложения, если это не так, сообщение об ошибке необходимо сообщить пользователю.

Ответ пользователя на входящий вызов

Если пользователь хочет ответить на входящий вызов VOIP, происходит следующее:

The user answers the incoming VOIP call

  1. Системный пользовательский интерфейс сообщает системе, что пользователь хочет ответить на вызов VOIP.
  2. Система отправляет в CXAnswerCallAction приложение CXProvider уведомление о намерении ответа.
  3. Приложение сообщает своей коммуникационной сети, что пользователь отвечает на звонок, и вызов VOIP продолжается как обычно.

Например, в :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 ();
        }
    });
}

Этот код сначала выполняет поиск заданного вызова в списке активных вызовов. Если вызов не удается найти, система уведомляется и метод завершает работу. Если он найден, метод ActiveCall класса вызывается для запуска вызова, и система содержит сведения, AnswerCall если оно успешно или завершается сбоем.

Конечный входящие вызовы пользователя

Если пользователь хочет завершить вызов из пользовательского интерфейса приложения, происходит следующее:

The user terminates the call from within the app's UI

  1. Приложение создает CXEndCallAction , которое упаковывается в CXTransaction систему, чтобы сообщить о завершении вызова.
  2. Система проверяет намерение конечного вызова и отправляет CXEndCallAction обратно в приложение через CXProviderприложение.
  3. Затем приложение сообщает своей коммуникационной сети, что вызов завершается.

Например, в :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 ();
        }
    });
}

Этот код сначала выполняет поиск заданного вызова в списке активных вызовов. Если вызов не удается найти, система уведомляется и метод завершает работу. Если он найден, метод ActiveCall класса вызывается для завершения вызова, а система — это информация, EndCall если она успешно или завершается ошибкой. При успешном выполнении вызов удаляется из коллекции активных вызовов.

Управление несколькими вызовами

Большинство приложений VOIP могут одновременно обрабатывать несколько вызовов. Например, если в настоящее время есть активный вызов VOIP и приложение получает уведомление о том, что есть новый входящий вызов, пользователь может приостановить или зависнуть на первом вызове, чтобы ответить на второй.

В приведенной выше ситуации система отправит CXTransaction приложение, включающее список нескольких действий (напримерCXEndCallAction, и).CXAnswerCallAction Все эти действия должны выполняться по отдельности, чтобы система может соответствующим образом обновить пользовательский интерфейс.

Обработка исходящих вызовов

Если пользователь касается записи из списка "Последние" (в приложении Телефон), например из вызова, относящегося к приложению, оно будет отправлено намерение начального вызова системой:

Receiving a Start Call Intent

  1. Приложение создаст действие запуска вызова на основе намерения запуска вызова, полученного из системы.
  2. Приложение будет использовать CXCallController запрос действия запуска вызова из системы.
  3. Если система принимает действие, оно будет возвращено приложению через XCProvider делегат.
  4. Приложение запускает исходящий вызов с его коммуникационной сетью.

Дополнительные сведения о намерениях см. в документации по расширениям пользовательского интерфейса "Намерения и намерения".

Жизненный цикл исходящего вызова

При работе с CallKit и исходящим вызовом приложению потребуется сообщить системе следующих событий жизненного цикла:

  1. Запуск — сообщите системе, что исходящий вызов начинается.
  2. Запущено — сообщите системе, что запущен исходящий вызов.
  3. Подключение . Сообщите системе, что исходящий вызов подключается.
  4. Подключение . Сообщите о подключении исходящего звонка и о том, что обе стороны могут говорить сейчас.

Например, следующий код запустит исходящий вызов:

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 и использует его для настройки CXStartCallAction пакета CXTransaction , который отправляется в систему с помощью RequestTransaction метода CXCallController класса. Вызывая RequestTransaction метод, система может размещать все существующие вызовы на удержании, независимо от источника (Телефон приложения, FaceTime, VOIP и т. д.), перед началом нового вызова.

Запрос на запуск исходящего вызова VOIP может поступать из нескольких различных источников, таких как Siri, запись в карта контакта (в приложении "Контакты") или из списка "Последние" (в приложении Телефон). В таких ситуациях приложение будет отправлено намерение начального вызова внутри приложения NSUserActivity , а AppDelegate потребуется обработать его:

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

CallHandleFromActivity Здесь используется метод вспомогательного класса StartCallRequest для получения дескриптора вызываемого пользователя (см. выше класс StartCallRequest).

Метод PerformStartCallActionкласса ProviderDelegate используется для окончательного запуска фактического исходящего вызова и информирования системы о своем жизненном цикле:

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

Он создает экземпляр класса (для хранения сведений ActiveCall о вызове во время выполнения) и заполняет вызываемого пользователя. ConnectedChanged События StartingConnectionChanged используются для отслеживания и отчета о жизненном цикле исходящего вызова. Вызов запущен и система сообщила, что действие выполнено.

Завершение исходящего вызова

Завершив исходящий вызов и желая завершить его, можно использовать следующий код:

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

Если создается CXEndCallAction идентификатор UUID вызова к концу, пакетирует его в CXTransaction систему, используя RequestTransaction метод CXCallController класса.

Дополнительные сведения о CallKit

В этом разделе рассматриваются дополнительные сведения, которые разработчик должен учитывать при работе с CallKit, например:

  • Конфигурация поставщика
  • Ошибки действий
  • Ограничения системы
  • Звук VOIP

Конфигурация поставщика

Конфигурация поставщика позволяет приложению VOIP iOS 10 настраивать взаимодействие с пользователем (внутри собственного пользовательского интерфейса в вызове) при работе с CallKit.

Приложение может выполнять следующие типы настроек:

  • Отображение локализованного имени.
  • Включите поддержку видеозвонка.
  • Настройте кнопки в пользовательском интерфейсе in-Call, предоставив свой собственный значок изображения шаблона. Взаимодействие пользователя с настраиваемыми кнопками отправляется непосредственно в приложение для обработки.

Ошибки действий

Приложения iOS 10 VOIP с помощью CallKit должны обрабатывать действия сбоем и постоянно информировать пользователя о состоянии действия.

Рассмотрим следующий пример:

  1. Приложение получило действие запуска вызова и начало процесс инициализации нового вызова VOIP с помощью сети коммуникации.
  2. Из-за ограниченной или отсутствия сетевой связи это подключение завершается ошибкой.
  3. Приложение должно отправить сообщение о сбое обратно в действие запуска вызова (Action.Fail()), чтобы сообщить системе сбоя.
  4. Это позволяет системе информировать пользователя о состоянии вызова. Например, чтобы отобразить пользовательский интерфейс сбоя вызова.

Кроме того, приложению iOS 10 VOIP потребуется ответить на ошибки времени ожидания, которые могут возникать, когда ожидаемое действие не может быть обработано в течение заданного периода времени. Каждый тип действия, предоставляемый CallKit, имеет максимальное значение времени ожидания, связанное с ним. Эти значения времени ожидания гарантируют, что любое действие CallKit, запрошенное пользователем, обрабатывается в быстром режиме, таким образом, сохраняя жидкость ОС и реагировать.

Существует несколько методов делегата поставщика (CXProviderDelegate), которые следует переопределить для корректной обработки этих ситуаций тайм-аута.

Ограничения системы

В зависимости от текущего состояния устройства iOS под управлением приложения iOS 10 VOIP некоторые системные ограничения могут быть применены.

Например, входящий вызов VOIP может быть ограничен системой, если:

  1. Пользователь звонит в списке заблокированных абонентов пользователя.
  2. Устройство iOS пользователя находится в режиме Do-Not-Disturb.

Если вызов VOIP ограничен любой из этих ситуаций, используйте следующий код для его обработки:

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

}

Звук VOIP

CallKit предоставляет несколько преимуществ для обработки звуковых ресурсов, необходимых приложению iOS 10 VOIP во время динамического вызова VOIP. Одним из самых больших преимуществ является звуковой сеанс приложения будет иметь повышенные приоритеты при запуске в iOS 10. Это тот же уровень приоритета, что и встроенные Телефон и приложения FaceTime, и этот расширенный уровень приоритета не позволит другим запущенным приложениям прервать звуковой сеанс приложения VOIP.

Кроме того, CallKit имеет доступ к другим указаниям маршрутизации звука, которые могут повысить производительность и интеллектуально направлять звук VOIP на определенные выходные устройства во время динамического вызова на основе пользовательских настроек и состояний устройства. Например, на основе подключенных устройств, таких как наушники Bluetooth, динамическое подключение CarPlay или параметры специальных возможностей.

В течение жизненного цикла типичного вызова VOIP с помощью CallKit приложение потребуется настроить аудиопоток, который будет предоставлять callKit. Ознакомьтесь со следующим примером:

The Start Call Action Sequence

  1. Действие запуска вызова получено приложением для ответа на входящий вызов.
  2. Перед выполнением этого действия приложением предоставляет конфигурацию, требуемую для нее AVAudioSession.
  3. Приложение сообщает системе о том, что действие выполнено.
  4. Перед подключением вызова CallKit предоставляет высокий приоритет AVAudioSession , соответствующий конфигурации, запрошенной приложению. Приложение будет уведомлено с помощью DidActivateAudioSession метода его CXProviderDelegate.

Работа с расширениями каталога вызовов

При работе с CallKit расширения каталога вызовов позволяют добавлять заблокированные номера звонков и определять номера, относящиеся к заданному приложению VOIP для контактов в приложении "Контакт" на устройстве iOS.

Реализация расширения каталога вызовов

Чтобы реализовать расширение каталога вызовов в приложении Xamarin.iOS, сделайте следующее:

  1. Откройте решение приложения в Visual Studio для Mac.

  2. Щелкните правой кнопкой мыши имя решения в Обозреватель решений и выберите "Добавить>новый проект".

  3. Выберите расширения каталога вызовов расширений>iOS>и нажмите кнопку "Далее":

    Creating a new Call Directory Extension

  4. Введите имя расширения и нажмите кнопку "Далее":

    Entering a name for the extension

  5. При необходимости измените имя проекта и (или) имя решения и нажмите кнопку "Создать ".

    Creating the project

Это добавит CallDirectoryHandler.cs класс в проект, который выглядит следующим образом:

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

Чтобы BeginRequest предоставить необходимые функциональные возможности, необходимо изменить метод в обработчике каталогов вызовов. В приведенном выше примере он пытается задать список заблокированных и доступных номеров в базе данных контактов приложения VOIP. Если любой запрос завершается сбоем по какой-либо причине, создайте описание NSError сбоя и передайте его CancelRequest метод CXCallDirectoryExtensionContext класса.

Чтобы задать заблокированные номера, используйте AddBlockingEntry метод CXCallDirectoryExtensionContext класса. Числа, предоставленные методу , должны находиться в числовом порядке возрастания. Для оптимальной производительности и использования памяти при наличии большого количества телефонных номеров рекомендуется загружать только подмножество чисел в определенное время и использовать пулы автовосписи для выпуска объектов, выделенных во время каждого пакета чисел, которые загружаются.

Чтобы сообщить приложению Contact о номерах контактов, известных приложению VOIP, используйте AddIdentificationEntry метод CXCallDirectoryExtensionContext класса и укажите как номер, так и метку идентификации. Опять же, числа, предоставленные методу , должны находиться в числовом порядке возрастания. Для оптимальной производительности и использования памяти при наличии большого количества телефонных номеров рекомендуется загружать только подмножество чисел в определенное время и использовать пулы автовосписи для выпуска объектов, выделенных во время каждого пакета чисел, которые загружаются.

Итоги

В этой статье рассматривается новый API CallKit, выпущенный Apple в iOS 10 и как реализовать его в приложениях VOIP Xamarin.iOS. В нем показано, как CallKit позволяет приложению интегрироваться в систему iOS, как она обеспечивает четность функций со встроенными приложениями (например, Телефон) и как это повышает видимость приложения в iOS в таких расположениях, как блокировка и домашние экраны, через взаимодействия Siri и через приложения "Контакты".