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, оно известно системе таким же образом, как эти встроенные системные службы и получают все те же функции:
Взгляните на приложение MonkeyCall из приведенной выше схемы. Приложение содержит весь код для взаимодействия с собственной сетью и содержит собственные пользовательские интерфейсы. Он ссылается на CallKit для взаимодействия с системой:
В CallKit используются два основных интерфейса:
CXProvider
— Это позволяет приложению MonkeyCall сообщить о системе любых внеполосных уведомлений, которые могут возникнуть.CXCallController
— позволяет приложению MonkeyCall информировать систему действий локального пользователя.
The CXProvider
Как упоминалось выше, CXProvider
приложение позволяет приложению информировать систему любых внеполосных уведомлений, которые могут возникнуть. Это уведомление, которое не происходит из-за действий локального пользователя, но происходит из-за внешних событий, таких как входящие вызовы.
Приложение должно использовать следующее CXProvider
:
- Сообщите о входном вызове системы.
- Сообщите об исходящем вызове, подключенном к системе.
- Сообщите удаленному пользователю о завершении вызова системы.
Когда приложение хочет взаимодействовать с системой, оно использует CXCallUpdate
класс и когда системе нужно взаимодействовать с приложением, он использует CXAction
класс:
The CXCallController
Приложение CXCallController
позволяет приложению информировать систему действий локальных пользователей, таких как пользователь, запускающий вызов VOIP. Реализация CXCallController
приложения позволяет взаимодействовать с другими типами вызовов в системе. Например, если уже существует активный телефонный звонок, CXCallController
приложение VOIP может разместить этот звонок на удержание и начать или ответить на вызов VOIP.
Приложение должно использовать следующее CXCallController
:
- Сообщите, когда пользователь начал исходящий вызов системы.
- Сообщите, когда пользователь отвечает на входящий вызов системы.
- Сообщите, когда пользователь завершает вызов системы.
Когда приложение хочет обмениваться действиями локального пользователя с системой, оно использует CXTransaction
класс:
Реализация 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 для хранения экземпляров ActiveCallManager
CXProviderDelegate
и которые будут использоваться во всем приложении:
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 с локальным пользователем, происходит следующее:
- Приложение получает уведомление из сети связи, что есть входящий вызов VOIP.
- Приложение используется
CXProvider
для отправкиCXCallUpdate
в систему уведомления о вызове. - Система публикует вызов системного пользовательского интерфейса, системных служб и других приложений 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, происходит следующее:
- Системный пользовательский интерфейс сообщает системе, что пользователь хочет ответить на вызов VOIP.
- Система отправляет в
CXAnswerCallAction
приложениеCXProvider
уведомление о намерении ответа. - Приложение сообщает своей коммуникационной сети, что пользователь отвечает на звонок, и вызов 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
если оно успешно или завершается сбоем.
Конечный входящие вызовы пользователя
Если пользователь хочет завершить вызов из пользовательского интерфейса приложения, происходит следующее:
- Приложение создает
CXEndCallAction
, которое упаковывается вCXTransaction
систему, чтобы сообщить о завершении вызова. - Система проверяет намерение конечного вызова и отправляет
CXEndCallAction
обратно в приложение черезCXProvider
приложение. - Затем приложение сообщает своей коммуникационной сети, что вызов завершается.
Например, в :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
Все эти действия должны выполняться по отдельности, чтобы система может соответствующим образом обновить пользовательский интерфейс.
Обработка исходящих вызовов
Если пользователь касается записи из списка "Последние" (в приложении Телефон), например из вызова, относящегося к приложению, оно будет отправлено намерение начального вызова системой:
- Приложение создаст действие запуска вызова на основе намерения запуска вызова, полученного из системы.
- Приложение будет использовать
CXCallController
запрос действия запуска вызова из системы. - Если система принимает действие, оно будет возвращено приложению через
XCProvider
делегат. - Приложение запускает исходящий вызов с его коммуникационной сетью.
Дополнительные сведения о намерениях см. в документации по расширениям пользовательского интерфейса "Намерения и намерения".
Жизненный цикл исходящего вызова
При работе с CallKit и исходящим вызовом приложению потребуется сообщить системе следующих событий жизненного цикла:
- Запуск — сообщите системе, что исходящий вызов начинается.
- Запущено — сообщите системе, что запущен исходящий вызов.
- Подключение . Сообщите системе, что исходящий вызов подключается.
- Подключение . Сообщите о подключении исходящего звонка и о том, что обе стороны могут говорить сейчас.
Например, следующий код запустит исходящий вызов:
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 должны обрабатывать действия сбоем и постоянно информировать пользователя о состоянии действия.
Рассмотрим следующий пример:
- Приложение получило действие запуска вызова и начало процесс инициализации нового вызова VOIP с помощью сети коммуникации.
- Из-за ограниченной или отсутствия сетевой связи это подключение завершается ошибкой.
- Приложение должно отправить сообщение о сбое обратно в действие запуска вызова (
Action.Fail()
), чтобы сообщить системе сбоя. - Это позволяет системе информировать пользователя о состоянии вызова. Например, чтобы отобразить пользовательский интерфейс сбоя вызова.
Кроме того, приложению iOS 10 VOIP потребуется ответить на ошибки времени ожидания, которые могут возникать, когда ожидаемое действие не может быть обработано в течение заданного периода времени. Каждый тип действия, предоставляемый CallKit, имеет максимальное значение времени ожидания, связанное с ним. Эти значения времени ожидания гарантируют, что любое действие CallKit, запрошенное пользователем, обрабатывается в быстром режиме, таким образом, сохраняя жидкость ОС и реагировать.
Существует несколько методов делегата поставщика (CXProviderDelegate
), которые следует переопределить для корректной обработки этих ситуаций тайм-аута.
Ограничения системы
В зависимости от текущего состояния устройства iOS под управлением приложения iOS 10 VOIP некоторые системные ограничения могут быть применены.
Например, входящий вызов VOIP может быть ограничен системой, если:
- Пользователь звонит в списке заблокированных абонентов пользователя.
- Устройство 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. Ознакомьтесь со следующим примером:
- Действие запуска вызова получено приложением для ответа на входящий вызов.
- Перед выполнением этого действия приложением предоставляет конфигурацию, требуемую для нее
AVAudioSession
. - Приложение сообщает системе о том, что действие выполнено.
- Перед подключением вызова CallKit предоставляет высокий приоритет
AVAudioSession
, соответствующий конфигурации, запрошенной приложению. Приложение будет уведомлено с помощьюDidActivateAudioSession
метода егоCXProviderDelegate
.
Работа с расширениями каталога вызовов
При работе с CallKit расширения каталога вызовов позволяют добавлять заблокированные номера звонков и определять номера, относящиеся к заданному приложению VOIP для контактов в приложении "Контакт" на устройстве iOS.
Реализация расширения каталога вызовов
Чтобы реализовать расширение каталога вызовов в приложении Xamarin.iOS, сделайте следующее:
Откройте решение приложения в Visual Studio для Mac.
Щелкните правой кнопкой мыши имя решения в Обозреватель решений и выберите "Добавить>новый проект".
Выберите расширения каталога вызовов расширений>iOS>и нажмите кнопку "Далее":
Введите имя расширения и нажмите кнопку "Далее":
При необходимости измените имя проекта и (или) имя решения и нажмите кнопку "Создать ".
Это добавит 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 и через приложения "Контакты".