[iOS] Add initial CallKit support

This commit adds initial support for CallKit on supported platforms: iOS >= 10.

Since the call flow in Jitsi Meet is basically making outgoing calls, only
outgoing call support is currently handled via CallKit.

Features:
 - "Green bar" when in a call.
 - Native CallKit view when tapping on the call label on the lock screen.
 - Support for audio muting from the native CallKit view.
 - Support for recent calls (audio-only calls logged as Audio calls, others show
   as Video calls).
 - Call display name is room name.
 - Graceful downgrade on systems without CallKit support.

Limitations:
 - Native CallKit view cannot be shown for audio-only calls (this is a CallKit
   limitaion).
 - The video button in the CallKit view will start a new video call to the same
   room, and terminate the previous one.
 - No support for call hold.
This commit is contained in:
Saúl Ibarra Corretgé 2017-09-08 11:28:44 +02:00 committed by Lyubo Marinov
parent 2e2129fa44
commit 8d11b3024e
10 changed files with 854 additions and 0 deletions

View File

@ -14,6 +14,9 @@
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; };
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */; };
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */; };
0BB9AD771F5EC6CE001C08DB /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB9AD761F5EC6CE001C08DB /* CallKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
0BB9AD791F5EC6D7001C08DB /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB9AD781F5EC6D7001C08DB /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */; };
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BB9AD7C1F60356D001C08DB /* AppInfo.m */; };
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495C1EC4B6C600B793EE /* AudioMode.m */; };
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495D1EC4B6C600B793EE /* POSIX.m */; };
@ -32,6 +35,9 @@
0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBridgeWrapper.h; sourceTree = "<group>"; };
0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeWrapper.m; sourceTree = "<group>"; };
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExternalAPI.m; sourceTree = "<group>"; };
0BB9AD761F5EC6CE001C08DB /* CallKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CallKit.framework; path = System/Library/Frameworks/CallKit.framework; sourceTree = SDKROOT; };
0BB9AD781F5EC6D7001C08DB /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallKit.m; sourceTree = "<group>"; };
0BB9AD7C1F60356D001C08DB /* AppInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppInfo.m; sourceTree = "<group>"; };
0BCA495C1EC4B6C600B793EE /* AudioMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioMode.m; sourceTree = "<group>"; };
0BCA495D1EC4B6C600B793EE /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = POSIX.m; sourceTree = "<group>"; };
@ -50,6 +56,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0BB9AD791F5EC6D7001C08DB /* Intents.framework in Frameworks */,
0BB9AD771F5EC6CE001C08DB /* CallKit.framework in Frameworks */,
0B93EF7B1EC608550030D24D /* CoreText.framework in Frameworks */,
0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */,
);
@ -90,6 +98,7 @@
children = (
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
0BD906E91EC0C00300C8C18E /* Info.plist */,
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */,
@ -107,6 +116,8 @@
9C3C6FA2341729836589B856 /* Frameworks */ = {
isa = PBXGroup;
children = (
0BB9AD781F5EC6D7001C08DB /* Intents.framework */,
0BB9AD761F5EC6CE001C08DB /* CallKit.framework */,
0B93EF7A1EC608550030D24D /* CoreText.framework */,
0BCA49631EC4B76D00B793EE /* WebRTC.framework */,
03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */,
@ -256,6 +267,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,

334
ios/sdk/src/CallKit.m Normal file
View File

@ -0,0 +1,334 @@
//
// Based on RNCallKit
//
// Original license:
//
// Copyright (c) 2016, Ian Yu-Hsun Lin
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTUtils.h>
// Weakly load CallKit, because it's not available on iOS 9.
@import CallKit;
// Events we will emit.
static NSString *const RNCallKitPerformAnswerCallAction = @"performAnswerCallAction";
static NSString *const RNCallKitPerformEndCallAction = @"performEndCallAction";
static NSString *const RNCallKitPerformSetMutedCallAction = @"performSetMutedCallAction";
static NSString *const RNCallKitProviderDidReset = @"providerDidReset";
@interface RNCallKit : RCTEventEmitter <CXProviderDelegate>
@end
@implementation RNCallKit
{
CXCallController *callKitCallController;
CXProvider *callKitProvider;
}
RCT_EXPORT_MODULE()
- (NSArray<NSString *> *)supportedEvents
{
return @[
RNCallKitPerformAnswerCallAction,
RNCallKitPerformEndCallAction,
RNCallKitPerformSetMutedCallAction,
RNCallKitProviderDidReset
];
}
// Configure CallKit
RCT_EXPORT_METHOD(setup:(NSDictionary *)options)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][setup] options = %@", options);
#endif
callKitCallController = [[CXCallController alloc] init];
if (callKitProvider) {
[callKitProvider invalidate];
}
callKitProvider = [[CXProvider alloc] initWithConfiguration:[self getProviderConfiguration: options]];
[callKitProvider setDelegate:self queue:nil];
}
#pragma mark - CXCallController call actions
// Display the incoming call to the user
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
handle:(NSString *)handle
hasVideo:(BOOL)hasVideo
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][displayIncomingCall] uuidString = %@", uuidString);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
callUpdate.supportsDTMF = NO;
callUpdate.supportsHolding = NO;
callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = hasVideo;
[callKitProvider reportNewIncomingCallWithUUID:uuid
update:callUpdate
completion:^(NSError * _Nullable error) {
if (error == nil) {
resolve(nil);
} else {
reject(nil, @"Error reporting new incoming call", error);
}
}];
}
// End call
RCT_EXPORT_METHOD(endCall:(NSString *)uuidString
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][endCall] uuidString = %@", uuidString);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXEndCallAction *action = [[CXEndCallAction alloc] initWithCallUUID:uuid];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
}
// Mute / unmute (audio)
RCT_EXPORT_METHOD(setMuted:(NSString *)uuidString
muted:(BOOL) muted
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][setMuted] uuidString = %@", uuidString);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXSetMutedCallAction *action
= [[CXSetMutedCallAction alloc] initWithCallUUID:uuid muted:muted];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
}
// Start outgoing call
RCT_EXPORT_METHOD(startCall:(NSString *)uuidString
handle:(NSString *)handle
video:(BOOL)video
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][startCall] uuidString = %@", uuidString);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXHandle *callHandle
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
CXStartCallAction *action
= [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle];
action.video = video;
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
}
// Indicate call failed
RCT_EXPORT_METHOD(reportCallFailed:(NSString *)uuidString
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
[callKitProvider reportCallWithUUID:uuid
endedAtDate:[NSDate date]
reason:CXCallEndedReasonFailed];
resolve(nil);
}
// Indicate outgoing call connected
RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)uuidString
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
[callKitProvider reportOutgoingCallWithUUID:uuid
connectedAtDate:[NSDate date]];
resolve(nil);
}
// Update call in case we have a display name or video capability changes
RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
options:(NSDictionary *)options
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][updateCall] uuidString = %@ options = %@", uuidString, options);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXCallUpdate *update = [[CXCallUpdate alloc] init];
if (options[@"displayName"]) {
update.localizedCallerName = options[@"displayName"];
}
if (options[@"hasVideo"]) {
update.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
}
[callKitProvider reportCallWithUUID:uuid updated:update];
resolve(nil);
}
#pragma mark - Helper methods
- (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary* )settings
{
#ifdef DEBUG
NSLog(@"[RNCallKit][getProviderConfiguration]");
#endif
CXProviderConfiguration *providerConfiguration
= [[CXProviderConfiguration alloc] initWithLocalizedName:settings[@"appName"]];
providerConfiguration.supportsVideo = YES;
providerConfiguration.maximumCallGroups = 1;
providerConfiguration.maximumCallsPerCallGroup = 1;
providerConfiguration.supportedHandleTypes
= [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil];
if (settings[@"imageName"]) {
providerConfiguration.iconTemplateImageData
= UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]);
}
if (settings[@"ringtoneSound"]) {
providerConfiguration.ringtoneSound = settings[@"ringtoneSound"];
}
return providerConfiguration;
}
- (void)requestTransaction:(CXTransaction *)transaction
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
#ifdef DEBUG
NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction);
#endif
[callKitCallController requestTransaction:transaction completion:^(NSError * _Nullable error) {
if (error == nil) {
resolve(nil);
} else {
NSLog(@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)", transaction.actions, error);
reject(nil, @"Error processing CallKit transaction", error);
}
}];
}
#pragma mark - CXProviderDelegate
// Called when the provider has been reset. We should terminate all calls.
- (void)providerDidReset:(CXProvider *)provider {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:providerDidReset]");
#endif
[self sendEventWithName:RNCallKitProviderDidReset body:nil];
}
// Answering incoming call
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction]");
#endif
[self sendEventWithName:RNCallKitPerformAnswerCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }];
[action fulfill];
}
// Call ended, user request
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction]");
#endif
[self sendEventWithName:RNCallKitPerformEndCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }];
[action fulfill];
}
// Handle audio mute from CallKit view
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction]");
#endif
[self sendEventWithName:RNCallKitPerformSetMutedCallAction
body:@{ @"callUUID": action.callUUID.UUIDString,
@"muted": [NSNumber numberWithBool:action.muted]}];
[action fulfill];
}
// Starting outgoing call
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction]");
#endif
[action fulfill];
// Update call info
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle = action.handle;
callUpdate.supportsDTMF = NO;
callUpdate.supportsHolding = NO;
callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = action.isVideo;
[callKitProvider reportCallWithUUID:action.callUUID updated:callUpdate];
// Notify the system about the outgoing call
[callKitProvider reportOutgoingCallWithUUID:action.callUUID
startedConnectingAtDate:[NSDate date]];
}
// These just help with debugging
- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession]");
#endif
}
- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession]");
#endif
}
- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction]");
#endif
}
@end

View File

@ -21,9 +21,22 @@
#import <React/RCTLinkingManager.h>
#import <React/RCTRootView.h>
#include <Availability.h>
#import <Foundation/Foundation.h>
#import "JitsiMeetView+Private.h"
#import "RCTBridgeWrapper.h"
// Weakly load the Intents framework since it's not available on iOS 9.
@import Intents;
// Constant describing iOS 10.0.0
static const NSOperatingSystemVersion ios10 = {
.majorVersion = 10,
.minorVersion = 0,
.patchVersion = 0
};
/**
* A <tt>RCTFatalHandler</tt> implementation which swallows JavaScript errors.
* In the Release configuration, React Native will (intentionally) raise an
@ -151,6 +164,40 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
return YES;
}
// Check for CallKit intents only on iOS >= 10
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios10]) {
if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]
|| [userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) {
INInteraction *interaction = [userActivity interaction];
INIntent *intent = interaction.intent;
NSString *handle;
BOOL isAudio = NO;
if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
INStartAudioCallIntent *startCallIntent
= (INStartAudioCallIntent *)intent;
handle = startCallIntent.contacts.firstObject.personHandle.value;
isAudio = YES;
} else {
INStartVideoCallIntent *startCallIntent
= (INStartVideoCallIntent *)intent;
handle = startCallIntent.contacts.firstObject.personHandle.value;
}
if (handle) {
// Load the URL contained in the handle
[view loadURLObject:@{
@"url": handle,
@"configOverwrite": @{
@"startAudioOnly": @(isAudio)
}
}];
return YES;
}
}
}
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];

View File

@ -73,6 +73,7 @@
"strophejs-plugins": "0.0.7",
"styled-components": "1.3.0",
"url-polyfill": "github/url-polyfill",
"uuid": "3.1.0",
"xmldom": "0.1.27"
},
"devDependencies": {

View File

@ -8,6 +8,7 @@ import '../../authentication';
import { Platform } from '../../base/react';
import '../../mobile/audio-mode';
import '../../mobile/background';
import '../../mobile/callkit';
import '../../mobile/external-api';
import '../../mobile/full-screen';
import '../../mobile/permissions';

View File

@ -0,0 +1,235 @@
import {
NativeModules,
NativeEventEmitter,
Platform
} from 'react-native';
const RNCallKit = NativeModules.RNCallKit;
/**
* Thin wrapper around Apple's CallKit functionality.
*
* In CallKit requests are performed via actions (either user or system started)
* and async events are reported via dedicated methods. This class exposes that
* functionality in the form of methods and events. One important thing to note
* is that even if an action is started by the system (because the user pressed
* the "end call" button in the CallKit view, for example) the event will be
* emitted in the same way as it would if the action originated from calling
* the "endCall" method in this class, for example.
*
* Emitted events:
* - performAnswerCallAction: The user pressed the answer button.
* - performEndCallAction: The call should be ended.
* - performSetMutedCallAction: The call muted state should change. The
* ancillary `data` object contains a `muted` attribute.
* - providerDidReset: The system has reset, all calls should be terminated.
* This event gets no associated data.
*
* All events get a `data` object with a `callUUID` property, unless stated
* otherwise.
*/
class CallKit extends NativeEventEmitter {
/**
* Initializes a new {@code CallKit} instance.
*/
constructor() {
super(RNCallKit);
this._setup = false;
}
/**
* Returns True if the current platform is supported, false otherwise. The
* supported platforms are: iOS >= 10.
*
* @private
* @returns {boolean}
*/
static isSupported() {
return Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 10;
}
/**
* Checks if CallKit was setup, and throws an exception in that case.
*
* @private
* @returns {void}
*/
_checkSetup() {
if (!this._setup) {
throw new Error('CallKit not initialized, call setup() first.');
}
}
/**
* Adds a listener for the given event.
*
* @param {string} event - Name of the event we are interested in.
* @param {Function} listener - Function which will be called when the
* desired event is emitted.
* @returns {void}
*/
addEventListener(event, listener) {
this._checkSetup();
if (!CallKit.isSupported()) {
return;
}
this.addListener(event, listener);
}
/**
* Notifies CallKit about an incoming call. This will display the system
* incoming call view.
*
* @param {string} uuid - Unique identifier for the call.
* @param {string} handle - Call handle in CallKit's terms. The room URL.
* @param {boolean} hasVideo - True if it's a video call, false otherwise.
* @returns {Promise}
*/
displayIncomingCall(uuid, handle, hasVideo = true) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.displayIncomingCall(uuid, handle, hasVideo);
}
/**
* Request CallKit to end the call.
*
* @param {string} uuid - Unique identifier for the call.
* @returns {Promise}
*/
endCall(uuid) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.endCall(uuid);
}
/**
* Removes a listener for the given event.
*
* @param {string} event - Name of the event we are no longer interested in.
* @param {Function} listener - Function which used to be called when the
* desired event was emitted.
* @returns {void}
*/
removeEventListener(event, listener) {
this._checkSetup();
if (!CallKit.isSupported()) {
return;
}
this.removeListener(event, listener);
}
/**
* Indicate CallKit that the outgoing call with the given UUID is now
* connected.
*
* @param {string} uuid - Unique identifier for the call.
* @returns {Promise}
*/
reportConnectedOutgoingCall(uuid) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.reportConnectedOutgoingCall(uuid);
}
/**
* Indicate CallKit that the call with the given UUID has failed.
*
* @param {string} uuid - Unique identifier for the call.
* @returns {Promise}
*/
reportCallFailed(uuid) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.reportCallFailed(uuid);
}
/**
* Tell CallKit about the audio muted state.
*
* @param {string} uuid - Unique identifier for the call.
* @param {boolean} muted - True if audio is muted, false otherwise.
* @returns {Promise}
*/
setMuted(uuid, muted) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.setMuted(uuid, muted);
}
/**
* Prepare / initialize CallKit. This method must be called before any
* other.
*
* @param {Object} options - Initialization options.
* @param {string} options.imageName - Image to be used in CallKit's
* application button..
* @param {string} options.ringtoneSound - Ringtone to be used for incoming
* calls.
* @returns {void}
*/
setup(options = {}) {
if (CallKit.isSupported()) {
options.appName = NativeModules.AppInfo.name;
RNCallKit.setup(options);
}
this._setup = true;
}
/**
* Indicate CallKit about a new outgoing call.
*
* @param {string} uuid - Unique identifier for the call.
* @param {string} handle - Call handle in CallKit's terms. The room URL in
* our case.
* @param {boolean} hasVideo - True if it's a video call, false otherwise.
* @returns {Promise}
*/
startCall(uuid, handle, hasVideo = true) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.startCall(uuid, handle, hasVideo);
}
/**
* Updates an ongoing call's parameters.
*
* @param {string} uuid - Unique identifier for the call.
* @param {Object} options - Object with properties which should be updated.
* @param {string} options.displayName - Display name for the caller.
* @param {boolean} options.hasVideo - True if the call has video, false
* otherwise.
* @returns {Promise}
*/
updateCall(uuid, options) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.updateCall(uuid, options);
}
}
export default new CallKit();

View File

@ -0,0 +1,11 @@
/**
* The type of redux action to set the CallKit event listeners.
*
* {
* type: _SET_CALLKIT_LISTENERS,
* listeners: Map|null
* }
*
* @protected
*/
export const _SET_CALLKIT_LISTENERS = Symbol('_SET_CALLKIT_LISTENERS');

View File

@ -0,0 +1,2 @@
import './middleware';
import './reducer';

View File

@ -0,0 +1,194 @@
/* @flow */
import uuid from 'uuid';
import {
APP_WILL_MOUNT,
APP_WILL_UNMOUNT,
appNavigate
} from '../../app';
import {
CONFERENCE_FAILED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_JOINED
} from '../../base/conference';
import { getInviteURL } from '../../base/connection';
import {
SET_AUDIO_MUTED,
SET_VIDEO_MUTED,
isVideoMutedByAudioOnly,
setAudioMuted
} from '../../base/media';
import { MiddlewareRegistry, toState } from '../../base/redux';
import { _SET_CALLKIT_LISTENERS } from './actionTypes';
import CallKit from './CallKit';
/**
* Middleware that captures several system actions and hooks up CallKit.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const result = next(action);
switch (action.type) {
case _SET_CALLKIT_LISTENERS: {
const { listeners } = getState()['features/callkit'];
if (listeners) {
for (const [ event, listener ] of listeners) {
CallKit.removeEventListener(event, listener);
}
}
if (action.listeners) {
for (const [ event, listener ] of action.listeners) {
CallKit.addEventListener(event, listener);
}
}
break;
}
case APP_WILL_MOUNT: {
CallKit.setup(); // TODO: set app icon.
const listeners = new Map();
const callEndListener = data => {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID === data.callUUID) {
// We arrive here when a call is ended by the system, for
// for example when another incoming call is received and the
// user selects "End & Accept".
delete conference.callUUID;
dispatch(appNavigate(undefined));
}
};
listeners.set('performEndCallAction', callEndListener);
// Set the same listener for providerDidReset. According to the docs,
// when the system resets we should terminate all calls.
listeners.set('providerDidReset', callEndListener);
const setMutedListener = data => {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID === data.callUUID) {
// Break the loop. Audio can be muted both from the CallKit
// interface and from the Jitsi Meet interface. We must keep
// them in sync, but at some point the loop needs to be broken.
// We are doing it here, on the CallKit handler.
const { muted } = getState()['features/base/media'].audio;
if (muted !== data.muted) {
dispatch(setAudioMuted(Boolean(data.muted)));
}
}
};
listeners.set('performSetMutedCallAction', setMutedListener);
dispatch({
type: _SET_CALLKIT_LISTENERS,
listeners
});
break;
}
case APP_WILL_UNMOUNT:
dispatch({
type: _SET_CALLKIT_LISTENERS,
listeners: null
});
break;
case CONFERENCE_FAILED: {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.reportCallFailed(callUUID);
}
break;
}
case CONFERENCE_LEFT: {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.endCall(callUUID);
}
break;
}
case CONFERENCE_JOINED: {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.reportConnectedOutgoingCall(callUUID);
}
break;
}
case CONFERENCE_WILL_JOIN: {
const conference = action.conference;
const url = getInviteURL(getState);
const hasVideo = !isVideoMutedByAudioOnly({ getState });
// When assigning the call UUID, do so in upper case, since iOS will
// return it upper cased.
conference.callUUID = uuid.v4().toUpperCase();
CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
.then(() => {
const { room } = getState()['features/base/conference'];
CallKit.updateCall(conference.callUUID, { displayName: room });
});
break;
}
case SET_AUDIO_MUTED: {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID) {
CallKit.setMuted(conference.callUUID, action.muted);
}
break;
}
case SET_VIDEO_MUTED: {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID) {
const hasVideo = !isVideoMutedByAudioOnly({ getState });
CallKit.updateCall(conference.callUUID, { hasVideo });
}
break;
}
}
return result;
});
/**
* Returns the currently active conference.
*
* @param {Function|Object} stateOrGetState - The redux state or redux's
* {@code getState} function.
* @returns {Conference|undefined}
*/
function getCurrentConference(stateOrGetState: Function | Object): ?Object {
const state = toState(stateOrGetState);
const { conference, joining } = state['features/base/conference'];
return conference || joining;
}

View File

@ -0,0 +1,17 @@
import { ReducerRegistry } from '../../base/redux';
import {
_SET_CALLKIT_LISTENERS
} from './actionTypes';
ReducerRegistry.register('features/callkit', (state = {}, action) => {
switch (action.type) {
case _SET_CALLKIT_LISTENERS:
return {
...state,
listeners: action.listeners
};
}
return state;
});