Xamarin.iOS の CallKit
iOS 10 の新しい CallKit API を使うと、VOIP アプリを iPhone UI と統合し、使い慣れたインターフェイスとエクスペリエンスをエンド ユーザーに提供できるようになります。 この API を使うと、ユーザーは iOS デバイスのロック画面から VOIP 通話を表示および操作し、電話アプリの [お気に入り] や [最近] のビューを使って連絡先を管理することができます。
CallKit について
Apple によると、CallKit は、サード パーティの Voice Over IP (VOIP) アプリを iOS 10 のファースト パーティ エクスペリエンスに上げる新しいフレームワークです。 CallKit API を使用すると、VOIP アプリを iPhone UI と統合し、使い慣れたインターフェイスとエクスペリエンスをエンド ユーザーに提供できます。 組み込みの電話アプリのように、ユーザーは iOS デバイスのロック画面から VOIP 通話を表示および操作し、電話アプリの [お気に入り] や [最近] のビューを使って連絡先を管理することができます。
さらに、CallKit API では、電話番号を名前 (着信 ID) に関連付けたり、番号をブロックする必要があるとき (着信拒否設定) をシステムに通知したりできる App Extension を作成できます。
既存の VOIP アプリ エクスペリエンス
新しい CallKit API とその機能について説明する前に、MonkeyCall という架空の VOIP アプリを使用して、iOS 9 (以前) のサード パーティの VOIP アプリによる現在のユーザー エクスペリエンスを確認してください。 MonkeyCall は、ユーザーが既存の iOS API を使用して VOIP 通話を送受信できるようにする単純なアプリです。
現在、ユーザーが MonkeyCall で着信通話を受信していて、iPhone がロックされている場合、ロック画面で受信した通知は、他の種類の通知 (メッセージやメール アプリなど) と区別できません。
ユーザーが通話に応答する場合は、MonkeyCall 通知をスライドしてアプリを開き、パスコード (またはユーザー Touch ID) を入力して電話のロックを解除してから、通話を受け入れて会話を開始する必要があります。
携帯電話のロックが解除されている場合も、エクスペリエンスは同様に面倒です。 ここでも、着信した MonkeyCall 通話は、画面の上部からスライドインする標準通知バナーとして表示されます。 通知は一時的なものであるため簡単に見逃される可能性があり、その場合ユーザーは通知センターを開き、応答する特定の通知を見つけて通話したり、MonkeyCall アプリを手動で起動したりする必要があります。
CallKit VOIP アプリ エクスペリエンス
MonkeyCall アプリに新しい CallKit API を実装することで、iOS 10 で着信 VOIP 通話に対するユーザーのエクスペリエンスを大幅に向上させることができます。 電話が上からロックされているときに VOIP 通話を受信するユーザーの例を見てみましょう。 CallKit を実装すると、通話は iPhone のロック画面に表示され、組み込みの電話アプリから通話を受信した場合と同様に全画面表示、ネイティブ UI、標準のスワイプ対応答機能を備えています。
ここでも、MonkeyCall VOIP 通話の受信時に iPhone のロックが解除されると、組み込みの電話アプリと同じ全画面表示、ネイティブ UI、標準のスワイプから応答およびタップして拒否する機能が提供され、また MonkeyCall にはカスタム着信音を再生するオプションがあります。
CallKit は MonkeyCall に追加機能を提供し、VOIP 通話と他の種類の通話とのインタラクションを可能にしたり、組み込みの [履歴] や [よく使う項目] リストに表示したり、組み込みのおやすみモードやブロック機能を使用したり、Siri から MonkeyCall 通話を開始したり、ユーザーが連絡先アプリのユーザーに MonkeyCall 通話を割り当てたりできるようにします。
以降のセクションでは、CallKit アーキテクチャ、着信および発信通話フロー、および CallKit API について詳しく説明します。
CallKit アーキテクチャ
iOS 10 では、Apple は、たとえば CarPlay で行われた通話が CallKit を介してシステム UI に認識されるように、すべてのシステム サービスで CallKit を採用しています。 次に示す例では、MonkeyCall は CallKit を採用しているため、これらの組み込みのシステム サービスと同じ方法でシステムに認識され、同じ機能をすべて取得します。
上の図から MonkeyCall アプリを詳しく見てみましょう。 アプリには、独自のネットワークと通信するためのコードがすべて含まれており、独自のユーザー インターフェイスが含まれています。 CallKit にリンクして、システムと通信します。
CallKit には、アプリで使用される 2 つの主要なインターフェイスがあります。
CXProvider
- これにより、MonkeyCall アプリから、発生する可能性のある帯域外通知をシステムに通知できます。CXCallController
- MonkeyCall アプリからローカル ユーザーアクションをシステムに通知できるようにします。
CXProvider
前述のように、CXProvider
は発生する可能性のある帯域外通知をアプリがシステムに通知できるようにします。 これらは、ローカル ユーザーのアクションが原因では発生しませんが、着信通話などの外部イベントが原因で発生する通知です。
アプリでは、次の場合に CXProvider
を使用する必要があります。
- システムに着信通話を報告する。
- 発信通話がシステムに接続されたことを報告する。
- システムに通話を終了するリモート ユーザーを報告する。
アプリがシステムと通信する場合は、CXCallUpdate
クラスを使用し、システムがアプリと通信する必要があるときは、CXAction
クラスを使用します。
CXCallController
CXCallController
により、アプリは VOIP 通話を開始するユーザーなどのローカル ユーザー アクションをシステムに通知できます。 CXCallController
を実装することで、アプリはシステム内の他の種類の通話とインタラクションができるようになります。 たとえば、進行中のアクティブなテレフォニー通話が既にある場合は、CXCallController
は VOIP アプリがその通話を保留にして VOIP 通話を開始または応答できるようにします。
アプリでは、次の場合に CXCallController
を使用する必要があります。
- システムにユーザーが発信通話を開始したとき報告する。
- システムにユーザーが着信通話に応答したとき報告する。
- システムにユーザーが通話を終了したとき報告する。
アプリは、ローカル ユーザーアクションをシステムに伝えたい場合、CXTransaction
クラスを使用します。
CallKit の実装
次のセクションでは、Xamarin.iOS VOIP アプリで CallKit を実装する方法について説明します。 このドキュメントでは、例のために架空の MonkeyCall VOIP アプリのコードを使用します。 ここで示すコードは、いくつかのサポート クラスを表します。CallKit 固有の部分については、次のセクションで詳しく説明します。
ActiveCall クラス
ActiveCall
クラスは、次のように現在アクティブな VOIP 通話に関するすべての情報を保持するために MonkeyCall アプリによって使用されます。
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
には、通話の状態を定義するいくつかのプロパティと、通話の状態が変化したときに発生する可能性がある 2 つのイベントが保持されています。 これは例であるため、通話の開始、応答、終了をシミュレートするために使用される 3 つのメソッドだけがあります。
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;
}
}
}
}
AppDelegate では、発信通話で呼び出されるユーザーの連絡先ハンドルを取得するために、CallHandleFromURL
クラスと CallHandleFromActivity
クラスが使用されます。 詳細については、以下の「発信通話の処理」セクションを参照してください。
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
を提供し、それをアプリが帯域外の CallKit イベントを処理できるように CXProvider
にアタッチする必要があります。 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
が渡されます。 次に、CXProvider
が応答するハンドルの型 (CXHandleType
) を定義します。
// Define handle types
var handleTypes = new [] { (NSNumber)(int)CXHandleType.PhoneNumber };
また、通話の進行中にアプリのアイコンに適用されるテンプレート イメージを取得します。
// Get Image Template
var templateImage = UIImage.FromFile ("telephone_receiver.png");
これらの値は、CXProvider
の構成に使用される CXProviderConfiguration
にバンドルされます。
// 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 を使用する場合、アプリは独自のオーディオ セッションを作成して処理しなくなります。代わりに、システムが作成して処理するオーディオ セッションを構成して使用する必要があります。
実際のアプリの場合、システムが提供する事前構成済みの AVAudioSession
による呼び出しを開始するために DidActivateAudioSession
メソッドが使用されます。
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 を使用して着信通話ワークフローを処理する方法について詳しく説明します。もう一度、例として MonkeyCall VOIP アプリを使用します。
着信通話をユーザーに通知する
リモート ユーザーがローカル ユーザーとの VOIP 会話を開始すると、次の処理が行われます。
- アプリが着信 VOIP 通話があることを示す通知を通信ネットワークから取得する。
- アプリから、
CXProvider
を使用してCXCallUpdate
がシステムに送信され、通話が通知される。 - CallKit を使用して、システムからシステム UI、システム サービス、およびその他の VOIP アプリに通話が公開される。
たとえば、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
インスタンスを作成し、通話元を識別するハンドルをアタッチします。 次に、CXProvider
クラスの ReportNewIncomingCall
メソッドを使用して、呼び出しをシステムに通知します。 成功した場合、通話はアプリのアクティブな通話のコレクションに追加されます。そうでない場合は、エラーをユーザーに報告する必要があります。
着信通話に応答するユーザー
ユーザーが着信 VOIP 呼び出しに応答する場合は、次の処理が行われます。
- システム UI から、ユーザーが VOIP 通話に応答することがシステムに通知される。
- システムから、アプリの
CXProvider
にCXAnswerCallAction
が送信され、応答の意図が通知される。 - アプリから、ユーザーが通話に応答していることが通信ネットワークに通知され、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
メソッドが呼び出され、成功または失敗した場合はシステムが情報になります。
着信通話を終了するユーザー
ユーザーがアプリの UI 内から通話を終了する場合は、次の処理が行われます。
- アプリでは
CXEndCallAction
が作成され、通話が終了していることを通知するためにシステムに送信されるCXTransaction
にバンドルされます。 - システムは、通話終了の意図を検証し、
CXProvider
を介してCXEndCallAction
をアプリに戻します。 - その後、アプリから、通話が終了していることが通信ネットワークに通知されます。
たとえば、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 通話があり、アプリが新しい着信通話があることを示す通知を受け取った場合、ユーザーは最初の通話を一時停止または切って、2 番目の通話に応答できます。
上記の状況では、システムから複数のアクションの一覧を含む CXTransaction
がアプリに送信されます (例: CXEndCallAction
と CXAnswerCallAction
)。 システムが UI を適切に更新できるように、これらのアクションはすべて個別に実行する必要があります。
発信通話の処理
ユーザーが [履歴] の一覧 (電話アプリ内) のエントリをタップすると、アプリに属する通話からのエントリなど、システムによって "通話の開始の意図" が送信されます。
- アプリでは、システムから受信した通話の開始インテントに基づいて、通話開始アクションが作成される。
- アプリでは、システムから通話開始アクションを要求するために
CXCallController
が使用される。 - システムがアクションを受け入れると、
XCProvider
デリゲートを介してアプリに返される。 - アプリでは、通信ネットワークを使用して発信通話が開始される。
意図の詳細については、意図と意図 UI 拡張機能に関するドキュメントを参照してください。
発信通話のライフサイクル
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
にバンドル化され、CXCallController
クラスの RequestTransaction
メソッドを 使用してシステムに送信されます。 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;
}
}
ここでは、ヘルパー クラス StartCallRequest
の CallHandleFromActivity
メソッドを使用して、通話先のハンドルを取得しています (上記の StartCallRequest クラスを参照)。
ProviderDelegate クラスの PerformStartCallAction
メソッドは、最終的に実際の発信通話を開始し、システムにそのライフサイクルを通知するために使用されます。
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
クラスのインスタンスを作成し、通話先を設定します。 StartingConnectionChanged
と ConnectedChanged
イベントは、発信通話のライフサイクルを監視および報告するために使用されます。 通話が開始され、アクションが実行されたことがシステムに通知されました。
発信通話の終了
ユーザーが発信通話を終え、終了したい場合は、次のコードを使用できます。
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);
}
終了する呼び出しの UUID を使用して CXEndCallAction
を作成し、CXCallController
クラスの RequestTransaction
メソッドを使用してシステムに送信される CXTransaction
にバンドルします。
追加の CallKit の詳細
このセクションでは、CallKit を使用する場合に開発者が考慮する必要がある、次のような追加の詳細情報について説明します。
- プロバイダーの構成
- アクション エラー
- システムの制限
- VOIP オーディオ
プロバイダーの構成
プロバイダー構成を使用すると、iOS 10 VOIP アプリで CallKit を使用するときに (ネイティブの通話 UI 内の) ユーザー エクスペリエンスをカスタマイズできます。
アプリでは、次の種類のカスタマイズを行うことができます。
- ローカライズされた名前を表示する。
- ビデオ通話のサポートを有効にする。
- 独自のテンプレート イメージ アイコンを表示して、呼び出し中 UI のボタンをカスタマイズする。 カスタム ボタンを使用したユーザー操作は、処理対象のアプリに直接送信される。
アクション エラー
CallKit を使用する iOS 10 VOIP アプリでは、正常に失敗したアクションを処理し、ユーザーにアクションの状態を常に通知する必要があります。
次の例を考慮してください。
- アプリは通話開始アクションを受け取り、通信ネットワークを使用して新しい VOIP 通話を初期化するプロセスを開始した。
- ネットワーク通信機能が限られているか、ネットワーク通信機能がないため、この接続は失敗する。
- アプリは、エラーをシステムに通知するために、失敗メッセージを通話開始アクション (
Action.Fail()
) に送り返す"必要がある"。 - これにより、システムは呼び出しの状態をユーザーに通知できる。 たとえば、呼び出しエラー UI を表示するなど。
さらに、iOS 10 VOIP アプリは、特定の時間内に予期されるアクションを処理できない場合に発生する可能性がある "タイムアウト エラー" に応答する必要があります。 CallKit によって提供される各アクションの種類には、最大タイムアウト値が関連付けられています。 これらのタイムアウト値により、ユーザーによって要求されたすべての CallKit アクションが応答性の高い方法で処理されるため、OS の流動的さと応答性も維持されます。
プロバイダー デリゲート (CXProviderDelegate
) には、このタイムアウト状況を適切に処理するためにオーバーライドする必要があるメソッドがいくつかあります。
システムの制限
iOS 10 VOIP アプリを実行している iOS デバイスの現在の状態に基づいて、特定のシステム制限が適用される場合があります。
たとえば、次の場合、着信 VOIP 通話をシステムによって制限できます。
- 呼び出し元は、ユーザーのブロックされた呼び出し元リストにある。
- ユーザーの iOS デバイスが、おやすみモード中。
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 には、ライブ VOIP 通話中に iOS 10 VOIP アプリが必要とするオーディオ リソースを処理するための利点がいくつかあります。 最大の利点の 1 つは、iOS 10 で実行するときにアプリのオーディオ セッションの優先度が高まる点です。 これは、組み込みの電話アプリと FaceTime アプリと同じ優先度レベルであり、この強化された優先度レベルにより、実行中の他のアプリが VOIP アプリのオーディオ セッションを中断するのを防ぐことができます。
さらに、CallKit は、パフォーマンスを向上させ、ユーザー設定とデバイスの状態に基づいてライブ通話中に VOIP オーディオを特定の出力デバイスにインテリジェントにルーティングできる他のオーディオ ルーティング ヒントにアクセスできます。 たとえば、Bluetooth ヘッドフォン、ライブ CarPlay 接続、アクセシビリティ設定などの接続されたデバイスに基づきます。
CallKit を使用した一般的な VOIP 通話のライフサイクル中、アプリは CallKit が提供するオーディオ ストリームを構成する必要があります。 次の例を見てみましょう。
- 通話の開始アクションは、着信通話に応答するためにアプリによって受信される。
- このアクションがアプリによって実行される前に、
AVAudioSession
に必要な構成が提供される。 - アプリから、アクションが実行されたことがシステムに通知される。
- 通話が接続される前に、CallKit から、アプリが要求した構成と一致する高優先度の
AVAudioSession
が提供される。 アプリには、CXProviderDelegate
のDidActivateAudioSession
メソッドを介して通知される。
通話ディレクトリ拡張機能の操作
CallKit を使用する場合、"通話ディレクトリ拡張機能" は、ブロックされた通話番号を追加し、特定の VOIP アプリに固有の番号を iOS デバイスの連絡先アプリの連絡先で識別する方法を提供します。
通話ディレクトリ拡張機能の実装
Xamarin.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
を作成し、CXCallDirectoryExtensionContext
クラスの CancelRequest
メソッドを渡します。
ブロックされた数値を設定するには、CXCallDirectoryExtensionContext
クラスのメソッドを AddBlockingEntry
使用します。 メソッドに指定する数値は、数値の昇順にする "必要があります"。 電話番号が多い場合に最適なパフォーマンスとメモリ使用量を得るために、特定の時刻に番号のサブセットのみを読み込み、自動リリース プールを使用して、読み込まれる番号のバッチごとに割り当てられたオブジェクトを解放することを検討してください。
VOIP アプリに知られている連絡先番号を連絡先アプリに通知するには、CXCallDirectoryExtensionContext
クラスの AddIdentificationEntry
メソッドを使用し、番号と識別ラベルの両方を指定します。 ここでも、メソッドに指定する数値は、数値の昇順にする "必要があります"。 電話番号が多い場合に最適なパフォーマンスとメモリ使用量を得るために、特定の時刻に番号のサブセットのみを読み込み、自動リリース プールを使用して、読み込まれる番号のバッチごとに割り当てられたオブジェクトを解放することを検討してください。
まとめ
この記事では、Apple が iOS 10 でリリースした新しい CallKit API と、Xamarin.iOS VOIP アプリで実装する方法について説明しました。 CallKit を使用してアプリを iOS システムに統合する方法、組み込みのアプリ (電話など) との機能パリティを提供する方法、ロック画面やホーム画面などの場所で iOS 全体でアプリの可視性を向上させる方法、Siri を使った操作、連絡先アプリを使用した方法が示されています。