31
loading...
This website collects cookies to deliver better user experience
CXProviderDelegate.provider(_:execute:)
native CallKit API requires synchronously returning a Bool value:optional func provider(_ provider: CXProvider,
execute transaction: CXTransaction) -> Bool
CXTransaction
needs to be processed. You can return true
to process the transaction yourself and notify the system about it. If you return false
(default behavior), the corresponding handler method in CXProviderDelegate
is called for each CXAction
contained in the transaction.true
in native code, we managed to move transaction control to the Dart code, where we perform manual or automatic CXTransaction
processing depending on the value received from the user.FlutterCallkitPlugin
iOS class) and several on the Flutter side (available via the FCXPlugin
Dart class).We declared additional features of the plugin in its class to separate the plugin interface from the CallKit interface.
When a VoIP push is received, one of the PKPushRegistryDelegate.pushRegistry(_: didReceiveIncomingPushWith:)
methods is called. Here you need to create a CXProvider
instance and call reportNewIncomingCall
to notify CallKit of the call. Since the same provider instance is required to further handle the call, we added the FlutterCallkitPlugin.reportNewIncomingCallWithUUID
method from the native side of the plugin. When the method is called, the plugin reports the call to the CXProvider
and also executes FCXPlugin.didDisplayIncomingCall
on the Dart side to continue working with the call.
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void
) {
// Retrieve the necessary data from the push
guard let uuidString = payload["UUID"] as? String,
let uuid = UUID(uuidString: uuidString),
let localizedName = payload["identifier"] as? String
else {
return
}
let callUpdate = CXCallUpdate()
callUpdate.localizedCallerName = localizedName
let configuration = CXProviderConfiguration(
localizedName: "ExampleLocalizedName"
)
// Report the call to the plugin and it will report it to CallKit
FlutterCallkitPlugin.sharedInstance.reportNewIncomingCall(
with: uuid,
callUpdate: callUpdate,
providerConfiguration: configuration,
pushProcessingCompletion: completion
)
}
The code presented in this article was simplified on purpose to make it easy to perceive. Find the full version of the code following the links at the end of the article. We used Objective-C to implement the plugin since it was chosen as the main language in our project earlier. The CallKit interfaces are written in Swift for simplicity.
extension CXCallDirectoryManager {
public enum EnabledStatus : Int {
case unknown = 0
case disabled = 1
case enabled = 2
}
}
open class CXCallDirectoryManager : NSObject {
open class var sharedInstance: CXCallDirectoryManager { get }
open func reloadExtension(
withIdentifier identifier: String,
completionHandler completion: ((Error?) -> Void)? = nil
)
open func getEnabledStatusForExtension(
withIdentifier identifier: String,
completionHandler completion: @escaping (CXCallDirectoryManager.EnabledStatus, Error?) -> Void
)
open func openSettings(
completionHandler completion: ((Error?) -> Void)? = nil
)
}
CXCallDirectoryManager.EnabledStatus
enum with Dart:enum FCXCallDirectoryManagerEnabledStatus {
unknown,
disabled,
enabled
}
sharedInstance
in our interface, so let's make a regular Dart class with static methods:class FCXCallDirectoryManager {
static Future<void> reloadExtension(String extensionIdentifier) async { }
static Future<FCXCallDirectoryManagerEnabledStatus> getEnabledStatus(
String extensionIdentifier,
) async { }
static Future<void> openSettings() async { }
}
Preserving the API is important, but it is just as important to consider the platform and language code style so that the interface is clear and convenient for plugin users.
For the API in the Dart, we used a shorter name (the long name was from objective-C) and replaced the completion block with Future. Future is the standard mechanism used to get the result of asynchronous methods in Dart. We also return Future from most Dart plugin methods because communication with native code is asynchronous.
Before – getEnabledStatusForExtension(withIdentifier:completionHandler:)
After – Future getEnabledStatus(extensionIdentifier)
FlutterMethodChannel
.MethodChannel
object:const MethodChannel _methodChannel =
const MethodChannel('plugins.voximplant.com/flutter_callkit');
FlutterPlugin
protocol to interact with Flutter:@interface FlutterCallkitPlugin : NSObject<FlutterPlugin>
@end
FlutterMethodChannel
with the same identifier we used above:+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel *channel
= [FlutterMethodChannel
methodChannelWithName:@"plugins.voximplant.com/flutter_callkit"
binaryMessenger:[registrar messenger]];
FlutterCallkitPlugin *instance
= [FlutterCallkitPlugin sharedPluginWithRegistrar:registrar];
[registrar addMethodCallDelegate:instance channel:channel];
}
getEnabledStatus
example.MethodChannel
API allows us to asynchronously get the result of a call from native code using Future
but imposes restrictions on the data types that we pass.
extensionIdentifier
argument to MethodChannel.invokeMethod
and then convert the result from the int type to FCXCallDirectoryManagerEnabledStatus
. We should also handle PlatformException
in case of an error in native code.static Future<FCXCallDirectoryManagerEnabledStatus> getEnabledStatus(
String extensionIdentifier,
) async {
try {
// Use MethodChannel with extensionIdentifier
// as an argument to call the corresponding
// method in the platform code
int index = await _methodChannel.invokeMethod(
'Plugin.getEnabledStatus',
extensionIdentifier,
);
// Convert the result to the
// FCXCallDirectoryManagerEnabledStatus enum
// and return its value to the user
return FCXCallDirectoryManagerEnabledStatus.values[index];
} on PlatformException catch (e) {
// If we get an error, we pass it to FCXException
// and then return it to the user in special type
throw FCXException(e.code, e.message);
}
}
Pay attention to the method identifier that we used:
Plugin.getEnabledStatus
The word before the dot is used to define the module responsible for a particular method.
getEnabledStatus
is equal to the name of the method in Flutter, not in iOS (or Android).
FlutterMethodChannel
go straight to the handleMethodCall:result:
method.- (void)handleMethodCall:(FlutterMethodCall*)call
result:(FlutterResult)result {
// Calls from Flutter can be initiated by name,
// which is passed to `FlutterMethodCall.method` property
if ([@"Plugin.getEnabledStatus" isEqualToString:call.method]) {
// When passing arguments with MethodChannel,
// they are packed to `FlutterMethodCall.arguments`.
// Extract extensionIdentifier, which we passed
// from the Flutter code earlier
NSString *extensionIdentifier = call.arguments;
if (isNull(extensionIdentifier)) {
// If the arguments are invalid, return an error
// using the `result` handler.
// The error should be packed to `FlutterError`.
// It’ll be thrown as PlatformException in the Dart code
result([FlutterError errorInvalidArguments:@"extensionIdentifier must not be null"]);
return;
}
// When the method is detected and the arguments
// are extracted and validated,
// we can write the logic
// To interact with this CallKit functionality
// we need the CallDirectoryManager instance
CXCallDirectoryManager *manager
= CXCallDirectoryManager.sharedInstance;
// Call the CallDirectoryManager method
// and wait for the result
[manager
getEnabledStatusForExtensionWithIdentifier:extensionIdentifier
completionHandler:^(CXCallDirectoryEnabledStatus status,
NSError * _Nullable error) {
// completion handler (containing the result of
// the CallDirectoryManager method) is executed,
// now we need to pass the result to Dart
// But first we convert it to the в suitable type,
// because only certain data types can be passed
// through MethodChannel
if (error) {
// Our errors are packed to `FlutterError`
result([FlutterError errorFromCallKitError:error]);
} else {
// Numbers are packed to `NSNumber`
// This enum is `NSInteger`, so we
// make the required conversion
result([self convertEnableStatusToNumber:enabledStatus]);
}
}];
}
}
FCXCallDirectoryManager
methods in the same waystatic Future<void> reloadExtension(String extensionIdentifier) async {
try {
// Set an identifier, pass the argument,
// and call the platform method
await _methodChannel.invokeMethod(
'Plugin.reloadExtension',
extensionIdentifier,
);
} on PlatformException catch (e) {
throw FCXException(e.code, e.message);
}
}
static Future<void> openSettings() async {
try {
// This method does not accept arguments
await _methodChannel.invokeMethod(
'Plugin.openSettings',
);
} on PlatformException catch (e) {
throw FCXException(e.code, e.message);
}
}
if ([@"Plugin.reloadExtension" isEqualToString:call.method]) {
NSString *extensionIdentifier = call.arguments;
if (isNull(extensionIdentifier)) {
result([FlutterError errorInvalidArguments:@"extensionIdentifier must not be null"]);
return;
}
CXCallDirectoryManager *manager
= CXCallDirectoryManager.sharedInstance;
[manager
reloadExtensionWithIdentifier:extensionIdentifier
completionHandler:^(NSError * _Nullable error) {
if (error) {
result([FlutterError errorFromCallKitError:error]);
} else {
result(nil);
}
}];
}
if ([@"Plugin.openSettings" isEqualToString:call.method]) {
if (@available(iOS 13.4, *)) {
CXCallDirectoryManager *manager
= CXCallDirectoryManager.sharedInstance;
[manager
openSettingsWithCompletionHandler:^(NSError * _Nullable error) {
if (error) {
result([FlutterError errorFromCallKitError:error]);
} else {
result(nil);
}
}];
} else {
result([FlutterError errorLowiOSVersionWithMinimal:@"13.4"]);
}
}
UserDefaults
, which we will wrap in propertyWrapper
.// Access to the storage from the iOS app
@UIApplicationMain
final class AppDelegate: FlutterAppDelegate {
@UserDefault("blockedNumbers", defaultValue: [])
private var blockedNumbers: [BlockableNumber]
@UserDefault("identifiedNumbers", defaultValue: [])
private var identifiedNumbers: [IdentifiableNumber]
}
// Access to the storage from the app extension
final class CallDirectoryHandler: CXCallDirectoryProvider {
@UserDefault("blockedNumbers", defaultValue: [])
private var blockedNumbers: [BlockableNumber]
@UserDefault("identifiedNumbers", defaultValue: [])
private var identifiedNumbers: [IdentifiableNumber]
@NullableUserDefault("lastUpdate")
private var lastUpdate: Date?
}
Note that the storage and extension samples are not part of the plugin, but rather part of the example application that comes with it.
FCXPlugin
(Flutter) and FlutterCallkitPlugin
(iOS) classes for the helpers. However, Call Directory is a highly specialized functionality that is not used in every project. That's why I want to put it in a separate file but leave the access through the FCXPlugin
class object. The extension will do this work:extension FCXPlugin_CallDirectoryExtension on FCXPlugin {
Future<List<FCXCallDirectoryPhoneNumber>> getBlockedPhoneNumbers()
async { }
Future<void> addBlockedPhoneNumbers(
List<FCXCallDirectoryPhoneNumber> numbers,
) async { }
Future<void> removeBlockedPhoneNumbers(
List<FCXCallDirectoryPhoneNumber> numbers,
) async { }
Future<void> removeAllBlockedPhoneNumbers() async { }
Future<List<FCXIdentifiablePhoneNumber>> getIdentifiablePhoneNumbers()
async { }
Future<void> addIdentifiablePhoneNumbers(
List<FCXIdentifiablePhoneNumber> numbers,
) async { }
Future<void> removeIdentifiablePhoneNumbers(
List<FCXCallDirectoryPhoneNumber> numbers,
) async { }
Future<void> removeAllIdentifiablePhoneNumbers() async { }
}
@interface FlutterCallkitPlugin : NSObject<FlutterPlugin>
@property(strong, nonatomic, nullable)
NSArray<FCXCallDirectoryPhoneNumber *> *(^getBlockedPhoneNumbers)(void);
@property(strong, nonatomic, nullable)
void(^didAddBlockedPhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);
@property(strong, nonatomic, nullable)
void(^didRemoveBlockedPhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);
@property(strong, nonatomic, nullable)
void(^didRemoveAllBlockedPhoneNumbers)(void);
@property(strong, nonatomic, nullable)
NSArray<FCXIdentifiablePhoneNumber *> *(^getIdentifiablePhoneNumbers)(void);
@property(strong, nonatomic, nullable)
void(^didAddIdentifiablePhoneNumbers)(NSArray<FCXIdentifiablePhoneNumber *> *numbers);
@property(strong, nonatomic, nullable)
void(^didRemoveIdentifiablePhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);
@property(strong, nonatomic, nullable)
void(^didRemoveAllIdentifiablePhoneNumbers)(void);
@end
Handlers are optional which allows you to use only some part of this functionality or use your own solution instead.
There are a lot of methods but they all work almost the same. That’s why we will focus on two of them, the ones with the opposite direction of data movement.
Future<List<FCXIdentifiablePhoneNumber>> getIdentifiablePhoneNumbers() async {
try {
// Call the platform method and save the result List<dynamic> numbers = await _methodChannel.invokeMethod(
'Plugin.getIdentifiablePhoneNumbers',
);
// Type the result and return it to the user
return numbers
.map(
(f) => FCXIdentifiablePhoneNumber(f['number'], label: f['label']))
.toList();
} on PlatformException catch (e) {
throw FCXException(e.code, e.message);
}
}
if ([@"Plugin.getIdentifiablePhoneNumbers" isEqualToString:call.method]) {
if (!self.getIdentifiablePhoneNumbers) {
// Check if the handler exists,
// if not, return an error
result([FlutterError errorHandlerIsNotRegistered:@"getIdentifiablePhoneNumbers"]);
return;
}
// Using the handler, request numbers from a user
NSArray<FCXIdentifiablePhoneNumber *> *identifiableNumbers
= self.getIdentifiablePhoneNumbers();
NSMutableArray<NSDictionary *> *phoneNumbers
= [NSMutableArray arrayWithCapacity:identifiableNumbers.count];
// Wrap each number in the dictionary type
// so we could pass them via MethodChannel
for (FCXIdentifiablePhoneNumber *identifiableNumber in identifiableNumbers) {
NSMutableDictionary *dictionary
= [NSMutableDictionary dictionary];
dictionary[@"number"]
= [NSNumber numberWithLongLong:identifiableNumber.number];
dictionary[@"label"]
= identifiableNumber.label;
[phoneNumbers addObject:dictionary];
}
// Pass the numbers to Flutter
result(phoneNumbers);
}
Future<void> addIdentifiablePhoneNumbers(
List<FCXIdentifiablePhoneNumber> numbers,
) async {
try {
// Prepare the numbers to be passed via MethodChannel
List<Map> arguments = numbers.map((f) => f._toMap()).toList();
// Pass the numbers to native code
await _methodChannel.invokeMethod(
'Plugin.addIdentifiablePhoneNumbers',
arguments
);
} on PlatformException catch (e) {
throw FCXException(e.code, e.message);
}
}
if ([@"Plugin.addIdentifiablePhoneNumbers" isEqualToString:call.method]) {
if (!self.didAddIdentifiablePhoneNumbers) {
// Check if the handler exists,
// if not, return an error
result([FlutterError errorHandlerIsNotRegistered:@"didAddIdentifiablePhoneNumbers"]);
return;
}
// Get the numbers passed as arguments
NSArray<NSDictionary *> *numbers = call.arguments;
if (isNull(numbers)) {
// Check if they’re valid
result([FlutterError errorInvalidArguments:@"numbers must not be null"]);
return;
}
NSMutableArray<FCXIdentifiablePhoneNumber *> *identifiableNumbers
= [NSMutableArray array];
// Type the numbers
for (NSDictionary *obj in numbers) {
NSNumber *number = obj[@"number"];
__auto_type identifiableNumber
= [[FCXIdentifiablePhoneNumber alloc] initWithNumber:number.longLongValue
label:obj[@"label"]];
[identifiableNumbers addObject:identifiableNumber];
}
// Pass the typed numbers to the handler
self.didAddIdentifiablePhoneNumbers(identifiableNumbers);
// Tell Flutter about the end of operation
result(nil);
}
reloadExtension(withIdentifier:completionHandler:)
method is used to reload the Call Directory extension. You may need it, for example, after adding new numbers to the storage so that they get into CallKit.FCXCallDirectoryManager
and request reload by the given extensionIdentifier
:final String _extensionID =
'com.voximplant.flutterCallkit.example.CallDirectoryExtension';
Future<void> reloadExtension() async {
await FCXCallDirectoryManager.reloadExtension(_extensionID);
}
final FCXPlugin _plugin = FCXPlugin();
Future<List<FCXIdentifiablePhoneNumber>> getIdentifiedNumbers() async {
return await _plugin.getIdentifiablePhoneNumbers();
}
getIdentifiablePhoneNumbers
handler, which the plugin uses to pass the specified numbers to Flutter. We will use it to pass the numbers from our identifiedNumbers
storage:private let callKitPlugin = FlutterCallkitPlugin.sharedInstance
@UserDefault("identifiedNumbers", defaultValue: [])
private var identifiedNumbers: [IdentifiableNumber]
// Add a phone number request event handler
callKitPlugin.getIdentifiablePhoneNumbers = { [weak self] in
guard let self = self else { return [] }
// Return the numbers from the storage to the handler
return self.identifiedNumbers.map {
FCXIdentifiablePhoneNumber(number: $0.number, label: $0.label)
}
}
final FCXPlugin _plugin = FCXPlugin();
Future<void> addIdentifiedNumber(String number, String id) async {
int num = int.parse(number);
var phone = FCXIdentifiablePhoneNumber(num, label: id);
await _plugin.addIdentifiablePhoneNumbers([phone]);
}
didAddIdentifiablePhoneNumbers
handler, which the plugin uses to notify the platform code about receiving new numbers from Flutter. In the handler, we save the received numbers to the number storage:private let callKitPlugin = FlutterCallkitPlugin.sharedInstance
@UserDefault("identifiedNumbers", defaultValue: [])
private var identifiedNumbers: [IdentifiableNumber]
// Add an event handler for adding numbers
callKitPlugin.didAddIdentifiablePhoneNumbers = { [weak self] numbers in
guard let self = self else { return }
// Save the numbers to the storage
self.identifiedNumbers.append(
contentsOf: numbers.map {
IdentifiableNumber(identifiableNumber: $0)
}
)
// The numbers in Call Directory must be sorted
self.identifiedNumbers.sort()
}