[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:
parent
2e2129fa44
commit
8d11b3024e
|
@ -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 */,
|
||||
|
|
|
@ -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
|
|
@ -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];
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
|
@ -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');
|
|
@ -0,0 +1,2 @@
|
|||
import './middleware';
|
||||
import './reducer';
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
});
|
Loading…
Reference in New Issue