共用方式為


Xamarin.iOS 中的 CallKit

iOS 10 中的新 CallKit API 提供一種方式,讓 VOIP 應用程式與 i 電話 UI 整合,並為終端使用者提供熟悉的介面和體驗。 透過此 API,使用者可以從 iOS 裝置的鎖定畫面檢視 VOIP 呼叫並與其互動,並使用 電話 應用程式的 [我的最愛] 和 [最近使用] 檢視來管理聯繫人。

關於 CallKit

根據 Apple,CallKit 是一個新的架構,將第三方 Voice Over IP (VOIP) 應用程式提升為 iOS 10 的第一方體驗。 CallKit API 可讓 VOIP 應用程式與 i 電話 UI 整合,並為使用者提供熟悉的介面和體驗。 就像內建 電話 應用程式一樣,使用者可以從 iOS 裝置的鎖定畫面檢視 VOIP 通話並與其互動,並使用 電話 應用程式的 [我的最愛] 和 [最近使用] 檢視來管理聯繫人。

此外,CallKit API 可讓您建立應用程式延伸模組,讓電話號碼與名稱(來電者標識符)產生關聯,或告訴系統何時應封鎖號碼(通話封鎖)。

現有的 VOIP 應用程式體驗

在討論新的 CallKit API 及其功能之前,請先看看目前在 iOS 9 中使用名為 MonkeyCall 的虛構 VOIP 應用程式來使用第三方 VOIP 應用程式的用戶體驗。 MonkeyCall 是一個簡單的應用程式,可讓使用者使用現有的 iOS API 來傳送和接收 VOIP 呼叫。

目前,如果使用者在 MonkeyCall 上收到來電,且其 i 電話 已鎖定,則鎖定畫面上收到的通知與任何其他通知類型無法區分(例如來自訊息或郵件應用程式的通知)。

如果使用者想要接聽電話,他們必須滑動 MonkeyCall 通知以開啟應用程式並輸入密碼(或使用者觸控標識符),才能解除鎖定電話,然後才能接受通話並啟動交談。

如果手機解除鎖定,體驗同樣麻煩。 同樣地,傳入的 MonkeyCall 呼叫會顯示為從畫面頂端滑入的標準通知橫幅。 由於通知是暫時的,因此使用者可以輕鬆地錯過通知中心,並尋找特定的通知接聽,然後呼叫或手動尋找並啟動 MonkeyCall 應用程式。

CallKit VOIP 應用程式體驗

藉由在 MonkeyCall 應用程式中實作新的 CallKit API,即可大幅改善 iOS 10 中傳入 VOIP 通話的用戶體驗。 以使用者從上方鎖定電話時收到 VOIP 通話的範例為例。 藉由實作 CallKit,呼叫會出現在 i 電話 的鎖定畫面上,就像從內建 電話 應用程式收到通話一樣,具有全螢幕、原生 UI 和標準撥動對接功能。

同樣地,如果在收到 MonkeyCall VOIP 呼叫時解除鎖定 i 電話,則會顯示內建 電話 應用程式的相同全螢幕、原生 UI 和標準撥動對接和點選拒絕功能,而 MonkeyCall 可以選擇播放自定義的鈴聲。

CallKit 為 MonkeyCall 提供額外的功能,允許其 VOIP 通話與其他類型的通話互動、出現在內建的 [最近] 和 [我的最愛] 清單中,以使用內建的 Do Not 打擾和封鎖功能、啟動 Siri 的 MonkeyCall 通話,並讓用戶能夠將 MonkeyCall 通話指派給聯繫人應用程式中的人員。

下列各節將詳細說明 CallKit 架構、傳入和傳出呼叫流程,以及 CallKit API。

CallKit 架構

在 iOS 10 中,Apple 已在所有系統服務中採用 CallKit,例如,透過 CallKit 對系統 UI 進行呼叫。 在下列範例中,由於 MonkeyCall 採用 CallKit,因此系統會以與這些內建系統服務相同的方式來得知系統,並取得所有相同的功能:

CallKit 服務堆疊

請仔細查看上圖中的 MonkeyCall App。 應用程式包含與其本身網路通訊的所有程序代碼,並包含自己的使用者介面。 它會連結 CallKit 中與系統通訊:

MonkeyCall 應用程式架構

CallKit 中有兩個主要介面可供應用程式使用:

  • CXProvider - 這可讓 MonkeyCall 應用程式通知系統任何可能發生的頻外通知。
  • CXCallController - 允許 MonkeyCall 應用程式通知系統本機用戶動作。

The CXProvider

如上所述, CXProvider 允許應用程式通知系統可能發生的任何頻外通知。 這些是不會因為本機用戶動作而發生的通知,但由於外部事件,例如來電而發生。

應用程式應該針對下列專案使用 CXProvider

  • 回報系統的來電。
  • 回報該連出通話已連線至系統。
  • 回報遠端用戶結束對系統的呼叫。

當應用程式想要與系統通訊時,它會使用 CXCallUpdate 類別,以及當系統需要與應用程式通訊時,它會使用 CXAction 類別:

透過 CXProvider 與系統通訊

The CXCallController

CXCallController可讓應用程式通知系統本機用戶動作,例如用戶啟動 VOIP 通話。 藉由實作 CXCallController 應用程式,可以與系統中其他類型的呼叫互動。 例如,如果已經有進行中的電話語音通話, CXCallController 可以允許 VOIP 應用程式將通話放在保留並啟動或接聽 VOIP 通話。

應用程式應該針對下列專案使用 CXCallController

  • 當用戶開始對系統進行傳出呼叫時報告。
  • 當使用者接聽系統來電時回報。
  • 當用戶結束對系統的呼叫時報告。

當應用程式想要將本機使用者動作傳達給系統時,它會使用 類別 CXTransaction

使用 CXCallController 向系統報告

實作 CallKit

下列各節將示範如何在 Xamarin.iOS VOIP 應用程式中實作 CallKit。 為了範例,本檔將使用來自虛構的 MonkeyCall VOIP 應用程式的程式代碼。 此處呈現的程式代碼代表數個支持類別,CallKit 特定部分將在下列各節中詳細說明。

ActiveCall 類別

MonkeyCall 應用程式會使用 類別 ActiveCall 來保存目前作用中 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;
            }
        }
    }
}

CallHandleFromURLCallHandleFromActivity 類別用於 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 用來處理任何呼叫活動的 。 接下來,它會定義 將回應的句柄類型 (CXHandleTypeCXProvider

// 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 override 方法。 如需詳細資訊,請參閱下方的 處理傳出電話 一節。

處理來電

在一般來電工作流程期間,傳入 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 實例,並將句柄附加至它會識別呼叫端的句柄。 接下來,它會使用 ReportNewIncomingCall 類別的 CXProvider 方法,通知系統呼叫。 如果成功,則會將呼叫新增至應用程式的作用中呼叫集合,如果不是,則錯誤必須回報給使用者。

使用者接聽來電

如果使用者想要接聽連入 VOIP 通話,就會發生下列情況:

使用者接聽傳入 VOIP 通話

  1. 系統 UI 會通知系統使用者想要接聽 VOIP 通話。
  2. 系統會將 傳送 CXAnswerCallAction 給應用程式,通知應用程式 CXProvider 回應意圖。
  3. 應用程式會通知其通訊網路,使用者正在接聽通話,而 VOIP 通話會如往常般繼續。

例如,在中 CXProviderDelegate

public override void PerformAnswerCallAction (CXProvider provider, CXAnswerCallAction action)
{
    // Find requested call
    var call = CallManager.FindCall (action.CallUuid);

    // Found?
    if (call == null) {
        // No, inform system and exit
        action.Fail ();
        return;
    }

    // Attempt to answer call
    call.AnswerCall ((successful) => {
        // Was the call successfully answered?
        if (successful) {
            // Yes, inform system
            action.Fulfill ();
        } else {
            // No, inform system
            action.Fail ();
        }
    });
}

此程式代碼會先在其作用中呼叫清單中搜尋指定的呼叫。 如果找不到呼叫,系統會收到通知,而且方法會結束。 如果找到 AnswerCall ,則會呼叫 類別的 ActiveCall 方法來啟動呼叫,而且如果系統成功或失敗,則為資訊。

用戶結束來電

如果使用者想要從應用程式的 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 ();
        }
    });
}

此程式代碼會先在其作用中呼叫清單中搜尋指定的呼叫。 如果找不到呼叫,系統會收到通知,而且方法會結束。 如果找到,則會 EndCall 呼叫 類別的 ActiveCall 方法以結束呼叫,而且如果系統成功或失敗,則為資訊。 如果成功,就會從使用中呼叫的集合中移除呼叫。

管理多個呼叫

大部分的 VOIP 應用程式可以一次處理多個呼叫。 例如,如果目前有作用中的 VOIP 通話,且應用程式會收到有新來電的通知,則用戶可以在第一次通話上暫停或停止回應第二個通話。

在上述情況中,系統會將 傳送 CXTransaction 給應用程式,其中包含多個動作的清單(例如 CXEndCallActionCXAnswerCallAction)。 所有這些動作都必須個別完成,讓系統可以適當地更新UI。

處理傳出呼叫

例如,如果使用者從 [最近] 清單點選一個專案(在 電話 應用程式中),也就是從屬於應用程式的呼叫,系統會傳送啟動通話意圖

接收開始呼叫意圖

  1. 應用程式會根據從系統收到的啟動呼叫意圖來建立 啟動呼叫動作
  2. 應用程式會使用 CXCallController ,向系統要求啟動呼叫動作。
  3. 如果系統接受 Action,則會透過 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 使用 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 實例(以保存進行中呼叫的相關信息),並填入被呼叫的人員。 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);
}

如果建立CXEndCallAction具有結束呼叫 UUID 的 ,請使用 類別的 CXCallController 方法,將它組合在CXTransaction傳送至系統的 RequestTransaction 中。

其他 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 Action 都會以回應方式處理,進而讓 OS 流暢且回應。

提供者委派 (CXProviderDelegate) 上有數種方法應該覆寫,以正常處理此逾時情況。

系統限制

根據執行 iOS 10 VOIP 應用程式的 iOS 裝置目前狀態,可能會強制執行特定系統限制。

例如,如果下列狀況,系統可以限制連入 VOIP 通話:

  1. 通話人員位於使用者的封鎖來電者清單上。
  2. 使用者的 iOS 裝置處於 Do-Not-Disturb 模式。

如果 VOIP 呼叫受限於上述任何情況,請使用下列程式代碼來處理它:

public class ProviderDelegate : CXProviderDelegate
{
...

    public void ReportIncomingCall (NSUuid uuid, string handle)
    {
        // Create update to describe the incoming call and caller
        var update = new CXCallUpdate ();
        update.RemoteHandle = new CXHandle (CXHandleType.Generic, handle);

        // Report incoming call to system
        Provider.ReportNewIncomingCall (uuid, update, (error) => {
            // Was the call accepted
            if (error == null) {
                // Yes, report to call manager
                CallManager.Calls.Add (new ActiveCall (uuid, handle, false));
            } else {
                // Report error to user here
                if (error.Code == (int)CXErrorCodeIncomingCallError.CallUuidAlreadyExists) {
                    // Handle duplicate call ID
                } else if (error.Code == (int)CXErrorCodeIncomingCallError.FilteredByBlockList) {
                    // Handle call from blocked user
                } else if (error.Code == (int)CXErrorCodeIncomingCallError.FilteredByDoNotDisturb) {
                    // Handle call while in do-not-disturb mode
                } else {
                    // Handle unknown error
                }
            }
        });
    }

}

VOIP 音訊

CallKit 提供數個優點,可處理 iOS 10 VOIP 應用程式在即時 VOIP 通話期間所需的音訊資源。 其中一個最大的優點是應用程式音訊會話在iOS 10中執行時,會提高優先順序。 這與內建的 電話 和 FaceTime 應用程式相同,此增強的優先順序層級可防止其他執行中的應用程式中斷 VOIP 應用程式的音訊會話。

此外,CallKit 可以存取其他音訊路由提示,以根據使用者喜好設定和裝置狀態,在即時通話期間,以智慧方式將 VOIP 音訊路由傳送至特定輸出裝置。 例如,根據藍牙耳機、即時 CarPlay 連線或輔助功能設定等附加裝置。

在使用 CallKit 的一般 VOIP 通話生命週期中,應用程式必須設定 CallKit 將提供的音訊串流。 請檢視下列範例:

啟動呼叫動作順序

  1. 應用程式會收到啟動通話動作以接聽來電。
  2. 在應用程式完成此動作之前,它會提供其 AVAudioSession所需的設定。
  3. 應用程式會通知系統已完成動作。
  4. 呼叫連線之前,CallKit 會提供與應用程式所要求的設定相符的高優先順序 AVAudioSession 。 應用程式將會透過 DidActivateAudioSessionCXProviderDelegate的方法收到通知。

使用通話目錄延伸模組

使用 CallKit 時, 通話目錄延伸模組 提供一種方式,將封鎖的通話號碼新增,並識別指定 VOIP 應用程式專屬的號碼給 iOS 裝置上聯繫人中的聯繫人。

實作通話目錄擴充功能

若要在 Xamarin.iOS 應用程式中實作呼叫目錄擴充功能,請執行下列動作:

  1. 在 Visual Studio for Mac 中開啟應用程式的方案。

  2. 以滑鼠右鍵按兩下 方案總管中的 [方案名稱],然後選取 [新增>新增專案]。

  3. 選取 [iOS>擴充>功能呼叫目錄擴充功能],然後按兩下一步] 按鈕:

    建立新的通話目錄擴充功能

  4. 輸入延伸模組的 [名稱],然後按兩下一步[ 下一步] 按鈕:

    輸入延伸模組的名稱

  5. 視需要調整 [項目名稱] 和/或 [方案名稱],然後按兩下 [建立] 按鈕:

    建立專案

這會將類別新增 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 方法。 提供給方法 的數字必須 以數值遞增順序。 為了在有許多電話號碼時達到最佳效能和記憶體使用量,請考慮只在指定時間載入數位子集,並使用自動發行集區來釋放載入每個批次號碼期間所配置的物件。

若要通知聯繫人應用程式 VOIP 應用程式已知的聯繫人號碼,請使用 AddIdentificationEntry 類別的 CXCallDirectoryExtensionContext 方法,並提供號碼和識別標籤。 同樣地,提供給 方法 的數字必須 以數值遞增順序。 為了在有許多電話號碼時達到最佳效能和記憶體使用量,請考慮只在指定時間載入數位子集,並使用自動發行集區來釋放載入每個批次號碼期間所配置的物件。

摘要

本文涵蓋 Apple 在 iOS 10 中發行的新 CallKit API,以及如何在 Xamarin.iOS VOIP 應用程式中實作。 它已示範 CallKit 如何允許應用程式整合到 iOS 系統、它如何提供功能與內建應用程式(例如 電話)的同位,以及如何透過 Siri 互動和聯繫人應用程式等位置增加應用程式在 iOS 中的可見度。