次の方法で共有


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 を採用しているため、これらの組み込みのシステム サービスと同じ方法でシステムに認識され、同じ機能をすべて取得します。

CallKit サービス スタック

上の図から MonkeyCall アプリを詳しく見てみましょう。 アプリには、独自のネットワークと通信するためのコードがすべて含まれており、独自のユーザー インターフェイスが含まれています。 CallKit にリンクして、システムと通信します。

MonkeyCall アプリのアーキテクチャ

CallKit には、アプリで使用される 2 つの主要なインターフェイスがあります。

  • CXProvider - これにより、MonkeyCall アプリから、発生する可能性のある帯域外通知をシステムに通知できます。
  • CXCallController - MonkeyCall アプリからローカル ユーザーアクションをシステムに通知できるようにします。

CXProvider

前述のように、CXProvider は発生する可能性のある帯域外通知をアプリがシステムに通知できるようにします。 これらは、ローカル ユーザーのアクションが原因では発生しませんが、着信通話などの外部イベントが原因で発生する通知です。

アプリでは、次の場合に CXProvider を使用する必要があります。

  • システムに着信通話を報告する。
  • 発信通話がシステムに接続されたことを報告する。
  • システムに通話を終了するリモート ユーザーを報告する。

アプリがシステムと通信する場合は、CXCallUpdate クラスを使用し、システムがアプリと通信する必要があるときは、CXAction クラスを使用します。

CXProvider を介したシステムとの通信

CXCallController

CXCallController により、アプリは VOIP 通話を開始するユーザーなどのローカル ユーザー アクションをシステムに通知できます。 CXCallController を実装することで、アプリはシステム内の他の種類の通話とインタラクションができるようになります。 たとえば、進行中のアクティブなテレフォニー通話が既にある場合は、CXCallController は VOIP アプリがその通話を保留にして VOIP 通話を開始または応答できるようにします。

アプリでは、次の場合に CXCallController を使用する必要があります。

  • システムにユーザーが発信通話を開始したとき報告する。
  • システムにユーザーが着信通話に応答したとき報告する。
  • システムにユーザーが通話を終了したとき報告する。

アプリは、ローカル ユーザーアクションをシステムに伝えたい場合、CXTransaction クラスを使用します。

CXCallController を使用したシステムへのレポート

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 会話を開始しました

  1. アプリが着信 VOIP 通話があることを示す通知を通信ネットワークから取得する。
  2. アプリから、CXProvider を使用して CXCallUpdate がシステムに送信され、通話が通知される。
  3. 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 呼び出しに応答する場合は、次の処理が行われます。

ユーザーが着信 VOIP 通話に応答します

  1. システム UI から、ユーザーが VOIP 通話に応答することがシステムに通知される。
  2. システムから、アプリの CXProviderCXAnswerCallAction が送信され、応答の意図が通知される。
  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 メソッドが呼び出され、成功または失敗した場合はシステムが情報になります。

着信通話を終了するユーザー

ユーザーがアプリの UI 内から通話を終了する場合は、次の処理が行われます。

ユーザーがアプリの UI 内から通話を終了します

  1. アプリでは CXEndCallAction が作成され、通話が終了していることを通知するためにシステムに送信される CXTransaction にバンドルされます。
  2. システムは、通話終了の意図を検証し、CXProvider を介して CXEndCallAction をアプリに戻します。
  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 通話があり、アプリが新しい着信通話があることを示す通知を受け取った場合、ユーザーは最初の通話を一時停止または切って、2 番目の通話に応答できます。

上記の状況では、システムから複数のアクションの一覧を含む CXTransaction がアプリに送信されます (例: CXEndCallActionCXAnswerCallAction)。 システムが UI を適切に更新できるように、これらのアクションはすべて個別に実行する必要があります。

発信通話の処理

ユーザーが [履歴] の一覧 (電話アプリ内) のエントリをタップすると、アプリに属する通話からのエントリなど、システムによって "通話の開始の意図" が送信されます。

通話開始意図の受け取り

  1. アプリでは、システムから受信した通話の開始インテントに基づいて、通話開始アクションが作成される。
  2. アプリでは、システムから通話開始アクションを要求するために CXCallController が使用される。
  3. システムがアクションを受け入れると、XCProvider デリゲートを介してアプリに返される。
  4. アプリでは、通信ネットワークを使用して発信通話が開始される。

意図の詳細については、意図と意図 UI 拡張機能に関するドキュメントを参照してください。

発信通話のライフサイクル

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 にバンドル化され、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;
    }
}

ここでは、ヘルパー クラス StartCallRequestCallHandleFromActivity メソッドを使用して、通話先のハンドルを取得しています (上記の 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 クラスのインスタンスを作成し、通話先を設定します。 StartingConnectionChangedConnectedChanged イベントは、発信通話のライフサイクルを監視および報告するために使用されます。 通話が開始され、アクションが実行されたことがシステムに通知されました。

発信通話の終了

ユーザーが発信通話を終え、終了したい場合は、次のコードを使用できます。

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 アプリでは、正常に失敗したアクションを処理し、ユーザーにアクションの状態を常に通知する必要があります。

次の例を考慮してください。

  1. アプリは通話開始アクションを受け取り、通信ネットワークを使用して新しい VOIP 通話を初期化するプロセスを開始した。
  2. ネットワーク通信機能が限られているか、ネットワーク通信機能がないため、この接続は失敗する。
  3. アプリは、エラーをシステムに通知するために、失敗メッセージを通話開始アクション (Action.Fail()) に送り返す"必要がある"。
  4. これにより、システムは呼び出しの状態をユーザーに通知できる。 たとえば、呼び出しエラー UI を表示するなど。

さらに、iOS 10 VOIP アプリは、特定の時間内に予期されるアクションを処理できない場合に発生する可能性がある "タイムアウト エラー" に応答する必要があります。 CallKit によって提供される各アクションの種類には、最大タイムアウト値が関連付けられています。 これらのタイムアウト値により、ユーザーによって要求されたすべての CallKit アクションが応答性の高い方法で処理されるため、OS の流動的さと応答性も維持されます。

プロバイダー デリゲート (CXProviderDelegate) には、このタイムアウト状況を適切に処理するためにオーバーライドする必要があるメソッドがいくつかあります。

システムの制限

iOS 10 VOIP アプリを実行している iOS デバイスの現在の状態に基づいて、特定のシステム制限が適用される場合があります。

たとえば、次の場合、着信 VOIP 通話をシステムによって制限できます。

  1. 呼び出し元は、ユーザーのブロックされた呼び出し元リストにある。
  2. ユーザーの 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 が提供するオーディオ ストリームを構成する必要があります。 次の例を見てみましょう。

通話開始アクション シーケンス

  1. 通話の開始アクションは、着信通話に応答するためにアプリによって受信される。
  2. このアクションがアプリによって実行される前に、AVAudioSession に必要な構成が提供される。
  3. アプリから、アクションが実行されたことがシステムに通知される。
  4. 通話が接続される前に、CallKit から、アプリが要求した構成と一致する高優先度の AVAudioSession が提供される。 アプリには、CXProviderDelegateDidActivateAudioSession メソッドを介して通知される。

通話ディレクトリ拡張機能の操作

CallKit を使用する場合、"通話ディレクトリ拡張機能" は、ブロックされた通話番号を追加し、特定の VOIP アプリに固有の番号を iOS デバイスの連絡先アプリの連絡先で識別する方法を提供します。

通話ディレクトリ拡張機能の実装

Xamarin.iOS アプリに通話ディレクトリ拡張機能を実装するには、次の操作を行います。

  1. Visual Studio for Mac でアプリのソリューションを開きます。

  2. ソリューション エクスプローラーで、ソリューション名を右クリックし、[追加]>[新しいプロジェクトの追加] の順に選択します。

  3. [iOS]>[拡張機能]>[通話ディレクトリ拡張機能] を選択 し、[次へ] ボタンをクリックします。

    新しい通話ディレクトリ拡張の作成

  4. 拡張機能の名前を入力し、[Next] ボタンをクリックします。

    拡張の名前の入力

  5. 必要に応じてプロジェクト名ソリューション名 を調整し、[Create] ボタンをクリックします。

    プロジェクトの作成

これにより、次のような 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 を使った操作、連絡先アプリを使用した方法が示されています。