Преглед на файлове

[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.
j8
Saúl Ibarra Corretgé преди 7 години
родител
ревизия
8d11b3024e

+ 12
- 0
ios/sdk/sdk.xcodeproj/project.pbxproj Целия файл

@@ -14,6 +14,9 @@
14 14
 		0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; };
15 15
 		0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */; };
16 16
 		0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */; };
17
+		0BB9AD771F5EC6CE001C08DB /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB9AD761F5EC6CE001C08DB /* CallKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
18
+		0BB9AD791F5EC6D7001C08DB /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB9AD781F5EC6D7001C08DB /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
19
+		0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */; };
17 20
 		0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BB9AD7C1F60356D001C08DB /* AppInfo.m */; };
18 21
 		0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495C1EC4B6C600B793EE /* AudioMode.m */; };
19 22
 		0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495D1EC4B6C600B793EE /* POSIX.m */; };
@@ -32,6 +35,9 @@
32 35
 		0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBridgeWrapper.h; sourceTree = "<group>"; };
33 36
 		0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeWrapper.m; sourceTree = "<group>"; };
34 37
 		0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExternalAPI.m; sourceTree = "<group>"; };
38
+		0BB9AD761F5EC6CE001C08DB /* CallKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CallKit.framework; path = System/Library/Frameworks/CallKit.framework; sourceTree = SDKROOT; };
39
+		0BB9AD781F5EC6D7001C08DB /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
40
+		0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallKit.m; sourceTree = "<group>"; };
35 41
 		0BB9AD7C1F60356D001C08DB /* AppInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppInfo.m; sourceTree = "<group>"; };
36 42
 		0BCA495C1EC4B6C600B793EE /* AudioMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioMode.m; sourceTree = "<group>"; };
37 43
 		0BCA495D1EC4B6C600B793EE /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = POSIX.m; sourceTree = "<group>"; };
@@ -50,6 +56,8 @@
50 56
 			isa = PBXFrameworksBuildPhase;
51 57
 			buildActionMask = 2147483647;
52 58
 			files = (
59
+				0BB9AD791F5EC6D7001C08DB /* Intents.framework in Frameworks */,
60
+				0BB9AD771F5EC6CE001C08DB /* CallKit.framework in Frameworks */,
53 61
 				0B93EF7B1EC608550030D24D /* CoreText.framework in Frameworks */,
54 62
 				0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */,
55 63
 			);
@@ -90,6 +98,7 @@
90 98
 			children = (
91 99
 				0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
92 100
 				0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
101
+				0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
93 102
 				0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
94 103
 				0BD906E91EC0C00300C8C18E /* Info.plist */,
95 104
 				0BD906E81EC0C00300C8C18E /* JitsiMeet.h */,
@@ -107,6 +116,8 @@
107 116
 		9C3C6FA2341729836589B856 /* Frameworks */ = {
108 117
 			isa = PBXGroup;
109 118
 			children = (
119
+				0BB9AD781F5EC6D7001C08DB /* Intents.framework */,
120
+				0BB9AD761F5EC6CE001C08DB /* CallKit.framework */,
110 121
 				0B93EF7A1EC608550030D24D /* CoreText.framework */,
111 122
 				0BCA49631EC4B76D00B793EE /* WebRTC.framework */,
112 123
 				03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */,
@@ -256,6 +267,7 @@
256 267
 			isa = PBXSourcesBuildPhase;
257 268
 			buildActionMask = 2147483647;
258 269
 			files = (
270
+				0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
259 271
 				0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
260 272
 				0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
261 273
 				0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,

+ 334
- 0
ios/sdk/src/CallKit.m Целия файл

@@ -0,0 +1,334 @@
1
+//
2
+// Based on RNCallKit
3
+//
4
+// Original license:
5
+//
6
+// Copyright (c) 2016, Ian Yu-Hsun Lin
7
+//
8
+// Permission to use, copy, modify, and/or distribute this software for any
9
+// purpose with or without fee is hereby granted, provided that the above
10
+// copyright notice and this permission notice appear in all copies.
11
+//
12
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19
+//
20
+
21
+#import <AVFoundation/AVFoundation.h>
22
+#import <Foundation/Foundation.h>
23
+#import <UIKit/UIKit.h>
24
+
25
+#import <React/RCTBridge.h>
26
+#import <React/RCTConvert.h>
27
+#import <React/RCTEventEmitter.h>
28
+#import <React/RCTEventDispatcher.h>
29
+#import <React/RCTUtils.h>
30
+
31
+// Weakly load CallKit, because it's not available on iOS 9.
32
+@import CallKit;
33
+
34
+
35
+// Events we will emit.
36
+static NSString *const RNCallKitPerformAnswerCallAction = @"performAnswerCallAction";
37
+static NSString *const RNCallKitPerformEndCallAction = @"performEndCallAction";
38
+static NSString *const RNCallKitPerformSetMutedCallAction = @"performSetMutedCallAction";
39
+static NSString *const RNCallKitProviderDidReset = @"providerDidReset";
40
+
41
+
42
+@interface RNCallKit : RCTEventEmitter <CXProviderDelegate>
43
+@end
44
+
45
+@implementation RNCallKit
46
+{
47
+    CXCallController *callKitCallController;
48
+    CXProvider *callKitProvider;
49
+}
50
+
51
+RCT_EXPORT_MODULE()
52
+
53
+- (NSArray<NSString *> *)supportedEvents
54
+{
55
+    return @[
56
+        RNCallKitPerformAnswerCallAction,
57
+        RNCallKitPerformEndCallAction,
58
+        RNCallKitPerformSetMutedCallAction,
59
+        RNCallKitProviderDidReset
60
+    ];
61
+}
62
+
63
+// Configure CallKit
64
+RCT_EXPORT_METHOD(setup:(NSDictionary *)options)
65
+{
66
+#ifdef DEBUG
67
+    NSLog(@"[RNCallKit][setup] options = %@", options);
68
+#endif
69
+    callKitCallController = [[CXCallController alloc] init];
70
+    if (callKitProvider) {
71
+        [callKitProvider invalidate];
72
+    }
73
+    callKitProvider = [[CXProvider alloc] initWithConfiguration:[self getProviderConfiguration: options]];
74
+    [callKitProvider setDelegate:self queue:nil];
75
+}
76
+
77
+#pragma mark - CXCallController call actions
78
+
79
+// Display the incoming call to the user
80
+RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
81
+                               handle:(NSString *)handle
82
+                             hasVideo:(BOOL)hasVideo
83
+                              resolve:(RCTPromiseResolveBlock)resolve
84
+                               reject:(RCTPromiseRejectBlock)reject)
85
+{
86
+#ifdef DEBUG
87
+    NSLog(@"[RNCallKit][displayIncomingCall] uuidString = %@", uuidString);
88
+#endif
89
+    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
90
+    CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
91
+    callUpdate.remoteHandle
92
+        = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
93
+    callUpdate.supportsDTMF = NO;
94
+    callUpdate.supportsHolding = NO;
95
+    callUpdate.supportsGrouping = NO;
96
+    callUpdate.supportsUngrouping = NO;
97
+    callUpdate.hasVideo = hasVideo;
98
+    
99
+    [callKitProvider reportNewIncomingCallWithUUID:uuid
100
+                                            update:callUpdate
101
+                                        completion:^(NSError * _Nullable error) {
102
+        if (error == nil) {
103
+            resolve(nil);
104
+        } else {
105
+            reject(nil, @"Error reporting new incoming call", error);
106
+        }
107
+    }];
108
+}
109
+
110
+// End call
111
+RCT_EXPORT_METHOD(endCall:(NSString *)uuidString
112
+                  resolve:(RCTPromiseResolveBlock)resolve
113
+                  reject:(RCTPromiseRejectBlock)reject)
114
+{
115
+#ifdef DEBUG
116
+    NSLog(@"[RNCallKit][endCall] uuidString = %@", uuidString);
117
+#endif
118
+    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
119
+    CXEndCallAction *action = [[CXEndCallAction alloc] initWithCallUUID:uuid];
120
+    CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
121
+    [self requestTransaction:transaction resolve:resolve reject:reject];
122
+}
123
+
124
+// Mute / unmute (audio)
125
+RCT_EXPORT_METHOD(setMuted:(NSString *)uuidString
126
+                  muted:(BOOL) muted
127
+                  resolve:(RCTPromiseResolveBlock)resolve
128
+                  reject:(RCTPromiseRejectBlock)reject)
129
+{
130
+#ifdef DEBUG
131
+    NSLog(@"[RNCallKit][setMuted] uuidString = %@", uuidString);
132
+#endif
133
+    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
134
+    CXSetMutedCallAction *action
135
+    = [[CXSetMutedCallAction alloc] initWithCallUUID:uuid muted:muted];
136
+    CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
137
+    [self requestTransaction:transaction resolve:resolve reject:reject];
138
+}
139
+
140
+// Start outgoing call
141
+RCT_EXPORT_METHOD(startCall:(NSString *)uuidString
142
+                     handle:(NSString *)handle
143
+                      video:(BOOL)video
144
+                    resolve:(RCTPromiseResolveBlock)resolve
145
+                     reject:(RCTPromiseRejectBlock)reject)
146
+{
147
+#ifdef DEBUG
148
+    NSLog(@"[RNCallKit][startCall] uuidString = %@", uuidString);
149
+#endif
150
+    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
151
+    CXHandle *callHandle
152
+        = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
153
+    CXStartCallAction *action
154
+        = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle];
155
+    action.video = video;
156
+    CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
157
+    [self requestTransaction:transaction resolve:resolve reject:reject];
158
+}
159
+
160
+// Indicate call failed
161
+RCT_EXPORT_METHOD(reportCallFailed:(NSString *)uuidString
162
+                  resolve:(RCTPromiseResolveBlock)resolve
163
+                  reject:(RCTPromiseRejectBlock)reject)
164
+{
165
+    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
166
+    [callKitProvider reportCallWithUUID:uuid
167
+                            endedAtDate:[NSDate date]
168
+                                 reason:CXCallEndedReasonFailed];
169
+    resolve(nil);
170
+}
171
+
172
+// Indicate outgoing call connected
173
+RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)uuidString
174
+                  resolve:(RCTPromiseResolveBlock)resolve
175
+                  reject:(RCTPromiseRejectBlock)reject)
176
+{
177
+    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
178
+    [callKitProvider reportOutgoingCallWithUUID:uuid
179
+                                connectedAtDate:[NSDate date]];
180
+    resolve(nil);
181
+}
182
+
183
+// Update call in case we have a display name or video capability changes
184
+RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
185
+                     options:(NSDictionary *)options
186
+                     resolve:(RCTPromiseResolveBlock)resolve
187
+                      reject:(RCTPromiseRejectBlock)reject)
188
+{
189
+#ifdef DEBUG
190
+    NSLog(@"[RNCallKit][updateCall] uuidString = %@ options = %@", uuidString, options);
191
+#endif
192
+    NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
193
+    CXCallUpdate *update = [[CXCallUpdate alloc] init];
194
+    if (options[@"displayName"]) {
195
+        update.localizedCallerName = options[@"displayName"];
196
+    }
197
+    if (options[@"hasVideo"]) {
198
+        update.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
199
+    }
200
+    [callKitProvider reportCallWithUUID:uuid updated:update];
201
+    resolve(nil);
202
+}
203
+
204
+#pragma mark - Helper methods
205
+
206
+- (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary* )settings
207
+{
208
+#ifdef DEBUG
209
+    NSLog(@"[RNCallKit][getProviderConfiguration]");
210
+#endif
211
+    CXProviderConfiguration *providerConfiguration
212
+        = [[CXProviderConfiguration alloc] initWithLocalizedName:settings[@"appName"]];
213
+    providerConfiguration.supportsVideo = YES;
214
+    providerConfiguration.maximumCallGroups = 1;
215
+    providerConfiguration.maximumCallsPerCallGroup = 1;
216
+    providerConfiguration.supportedHandleTypes
217
+        = [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil];
218
+    if (settings[@"imageName"]) {
219
+        providerConfiguration.iconTemplateImageData
220
+            = UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]);
221
+    }
222
+    if (settings[@"ringtoneSound"]) {
223
+        providerConfiguration.ringtoneSound = settings[@"ringtoneSound"];
224
+    }
225
+    return providerConfiguration;
226
+}
227
+
228
+- (void)requestTransaction:(CXTransaction *)transaction
229
+                   resolve:(RCTPromiseResolveBlock)resolve
230
+                    reject:(RCTPromiseRejectBlock)reject
231
+{
232
+#ifdef DEBUG
233
+    NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction);
234
+#endif
235
+    [callKitCallController requestTransaction:transaction completion:^(NSError * _Nullable error) {
236
+        if (error == nil) {
237
+            resolve(nil);
238
+        } else {
239
+            NSLog(@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)", transaction.actions, error);
240
+            reject(nil, @"Error processing CallKit transaction", error);
241
+        }
242
+    }];
243
+}
244
+
245
+#pragma mark - CXProviderDelegate
246
+
247
+// Called when the provider has been reset. We should terminate all calls.
248
+- (void)providerDidReset:(CXProvider *)provider {
249
+#ifdef DEBUG
250
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:providerDidReset]");
251
+#endif
252
+    [self sendEventWithName:RNCallKitProviderDidReset body:nil];
253
+}
254
+
255
+// Answering incoming call
256
+- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action
257
+{
258
+#ifdef DEBUG
259
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction]");
260
+#endif
261
+    [self sendEventWithName:RNCallKitPerformAnswerCallAction
262
+                       body:@{ @"callUUID": action.callUUID.UUIDString }];
263
+    [action fulfill];
264
+}
265
+
266
+// Call ended, user request
267
+- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action
268
+{
269
+#ifdef DEBUG
270
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction]");
271
+#endif
272
+    [self sendEventWithName:RNCallKitPerformEndCallAction
273
+                       body:@{ @"callUUID": action.callUUID.UUIDString }];
274
+    [action fulfill];
275
+}
276
+
277
+// Handle audio mute from CallKit view
278
+- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action {
279
+#ifdef DEBUG
280
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction]");
281
+#endif
282
+    [self sendEventWithName:RNCallKitPerformSetMutedCallAction
283
+                       body:@{ @"callUUID": action.callUUID.UUIDString,
284
+                               @"muted": [NSNumber numberWithBool:action.muted]}];
285
+    [action fulfill];
286
+}
287
+
288
+// Starting outgoing call
289
+- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action
290
+{
291
+#ifdef DEBUG
292
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction]");
293
+#endif
294
+    [action fulfill];
295
+    
296
+    // Update call info
297
+    CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
298
+    callUpdate.remoteHandle = action.handle;
299
+    callUpdate.supportsDTMF = NO;
300
+    callUpdate.supportsHolding = NO;
301
+    callUpdate.supportsGrouping = NO;
302
+    callUpdate.supportsUngrouping = NO;
303
+    callUpdate.hasVideo = action.isVideo;
304
+    [callKitProvider reportCallWithUUID:action.callUUID updated:callUpdate];
305
+    
306
+    // Notify the system about the outgoing call
307
+    [callKitProvider reportOutgoingCallWithUUID:action.callUUID
308
+                        startedConnectingAtDate:[NSDate date]];
309
+}
310
+
311
+// These just help with debugging
312
+
313
+- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
314
+{
315
+#ifdef DEBUG
316
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession]");
317
+#endif
318
+}
319
+
320
+- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession
321
+{
322
+#ifdef DEBUG
323
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession]");
324
+#endif
325
+}
326
+
327
+- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action
328
+{
329
+#ifdef DEBUG
330
+    NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction]");
331
+#endif
332
+}
333
+
334
+@end

+ 47
- 0
ios/sdk/src/JitsiMeetView.m Целия файл

@@ -21,9 +21,22 @@
21 21
 #import <React/RCTLinkingManager.h>
22 22
 #import <React/RCTRootView.h>
23 23
 
24
+#include <Availability.h>
25
+#import <Foundation/Foundation.h>
26
+
24 27
 #import "JitsiMeetView+Private.h"
25 28
 #import "RCTBridgeWrapper.h"
26 29
 
30
+// Weakly load the Intents framework since it's not available on iOS 9.
31
+@import Intents;
32
+
33
+// Constant describing iOS 10.0.0
34
+static const NSOperatingSystemVersion ios10 = {
35
+  .majorVersion = 10,
36
+  .minorVersion = 0,
37
+  .patchVersion = 0
38
+};
39
+
27 40
 /**
28 41
  * A <tt>RCTFatalHandler</tt> implementation which swallows JavaScript errors.
29 42
  * In the Release configuration, React Native will (intentionally) raise an
@@ -151,6 +164,40 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
151 164
         return YES;
152 165
     }
153 166
 
167
+      // Check for CallKit intents only on iOS >= 10
168
+      if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios10]) {
169
+          if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]
170
+              || [userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) {
171
+              INInteraction *interaction = [userActivity interaction];
172
+              INIntent *intent = interaction.intent;
173
+              NSString *handle;
174
+              BOOL isAudio = NO;
175
+
176
+              if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
177
+                  INStartAudioCallIntent *startCallIntent
178
+                      = (INStartAudioCallIntent *)intent;
179
+                  handle = startCallIntent.contacts.firstObject.personHandle.value;
180
+                  isAudio = YES;
181
+              } else {
182
+                  INStartVideoCallIntent *startCallIntent
183
+                      = (INStartVideoCallIntent *)intent;
184
+                  handle = startCallIntent.contacts.firstObject.personHandle.value;
185
+              }
186
+
187
+              if (handle) {
188
+              // Load the URL contained in the handle
189
+              [view loadURLObject:@{
190
+                                    @"url": handle,
191
+                                    @"configOverwrite": @{
192
+                                        @"startAudioOnly": @(isAudio)
193
+                                    }
194
+                                    }];
195
+
196
+              return YES;
197
+              }
198
+          }
199
+      }
200
+
154 201
     return [RCTLinkingManager application:application
155 202
                      continueUserActivity:userActivity
156 203
                        restorationHandler:restorationHandler];

+ 1
- 0
package.json Целия файл

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

+ 1
- 0
react/features/app/components/App.native.js Целия файл

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

+ 235
- 0
react/features/mobile/callkit/CallKit.js Целия файл

@@ -0,0 +1,235 @@
1
+import {
2
+    NativeModules,
3
+    NativeEventEmitter,
4
+    Platform
5
+} from 'react-native';
6
+
7
+const RNCallKit = NativeModules.RNCallKit;
8
+
9
+/**
10
+ * Thin wrapper around Apple's CallKit functionality.
11
+ *
12
+ * In CallKit requests are performed via actions (either user or system started)
13
+ * and async events are reported via dedicated methods. This class exposes that
14
+ * functionality in the form of methods and events. One important thing to note
15
+ * is that even if an action is started by the system (because the user pressed
16
+ * the "end call" button in the CallKit view, for example) the event will be
17
+ * emitted in the same way as it would if the action originated from calling
18
+ * the "endCall" method in this class, for example.
19
+ *
20
+ * Emitted events:
21
+ *  - performAnswerCallAction: The user pressed the answer button.
22
+ *  - performEndCallAction: The call should be ended.
23
+ *  - performSetMutedCallAction: The call muted state should change. The
24
+ *    ancillary `data` object contains a `muted` attribute.
25
+ *  - providerDidReset: The system has reset, all calls should be terminated.
26
+ *    This event gets no associated data.
27
+ *
28
+ * All events get a `data` object with a `callUUID` property, unless stated
29
+ * otherwise.
30
+ */
31
+class CallKit extends NativeEventEmitter {
32
+    /**
33
+     * Initializes a new {@code CallKit} instance.
34
+     */
35
+    constructor() {
36
+        super(RNCallKit);
37
+        this._setup = false;
38
+    }
39
+
40
+    /**
41
+     * Returns True if the current platform is supported, false otherwise. The
42
+     * supported platforms are: iOS >= 10.
43
+     *
44
+     * @private
45
+     * @returns {boolean}
46
+     */
47
+    static isSupported() {
48
+        return Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 10;
49
+    }
50
+
51
+    /**
52
+     * Checks if CallKit was setup, and throws an exception in that case.
53
+     *
54
+     * @private
55
+     * @returns {void}
56
+     */
57
+    _checkSetup() {
58
+        if (!this._setup) {
59
+            throw new Error('CallKit not initialized, call setup() first.');
60
+        }
61
+    }
62
+
63
+    /**
64
+     * Adds a listener for the given event.
65
+     *
66
+     * @param {string} event - Name of the event we are interested in.
67
+     * @param {Function} listener - Function which will be called when the
68
+     * desired event is emitted.
69
+     * @returns {void}
70
+     */
71
+    addEventListener(event, listener) {
72
+        this._checkSetup();
73
+        if (!CallKit.isSupported()) {
74
+            return;
75
+        }
76
+
77
+        this.addListener(event, listener);
78
+    }
79
+
80
+    /**
81
+     * Notifies CallKit about an incoming call. This will display the system
82
+     * incoming call view.
83
+     *
84
+     * @param {string} uuid - Unique identifier for the call.
85
+     * @param {string} handle - Call handle in CallKit's terms. The room URL.
86
+     * @param {boolean} hasVideo - True if it's a video call, false otherwise.
87
+     * @returns {Promise}
88
+     */
89
+    displayIncomingCall(uuid, handle, hasVideo = true) {
90
+        this._checkSetup();
91
+        if (!CallKit.isSupported()) {
92
+            return Promise.resolve();
93
+        }
94
+
95
+        return RNCallKit.displayIncomingCall(uuid, handle, hasVideo);
96
+    }
97
+
98
+    /**
99
+     * Request CallKit to end the call.
100
+     *
101
+     * @param {string} uuid - Unique identifier for the call.
102
+     * @returns {Promise}
103
+     */
104
+    endCall(uuid) {
105
+        this._checkSetup();
106
+        if (!CallKit.isSupported()) {
107
+            return Promise.resolve();
108
+        }
109
+
110
+        return RNCallKit.endCall(uuid);
111
+    }
112
+
113
+    /**
114
+     * Removes a listener for the given event.
115
+     *
116
+     * @param {string} event - Name of the event we are no longer interested in.
117
+     * @param {Function} listener - Function which used to be called when the
118
+     * desired event was emitted.
119
+     * @returns {void}
120
+     */
121
+    removeEventListener(event, listener) {
122
+        this._checkSetup();
123
+        if (!CallKit.isSupported()) {
124
+            return;
125
+        }
126
+
127
+        this.removeListener(event, listener);
128
+    }
129
+
130
+    /**
131
+     * Indicate CallKit that the outgoing call with the given UUID is now
132
+     * connected.
133
+     *
134
+     * @param {string} uuid - Unique identifier for the call.
135
+     * @returns {Promise}
136
+     */
137
+    reportConnectedOutgoingCall(uuid) {
138
+        this._checkSetup();
139
+        if (!CallKit.isSupported()) {
140
+            return Promise.resolve();
141
+        }
142
+
143
+        return RNCallKit.reportConnectedOutgoingCall(uuid);
144
+    }
145
+
146
+    /**
147
+     * Indicate CallKit that the call with the given UUID has failed.
148
+     *
149
+     * @param {string} uuid - Unique identifier for the call.
150
+     * @returns {Promise}
151
+     */
152
+    reportCallFailed(uuid) {
153
+        this._checkSetup();
154
+        if (!CallKit.isSupported()) {
155
+            return Promise.resolve();
156
+        }
157
+
158
+        return RNCallKit.reportCallFailed(uuid);
159
+    }
160
+
161
+    /**
162
+     * Tell CallKit about the audio muted state.
163
+     *
164
+     * @param {string} uuid - Unique identifier for the call.
165
+     * @param {boolean} muted - True if audio is muted, false otherwise.
166
+     * @returns {Promise}
167
+     */
168
+    setMuted(uuid, muted) {
169
+        this._checkSetup();
170
+        if (!CallKit.isSupported()) {
171
+            return Promise.resolve();
172
+        }
173
+
174
+        return RNCallKit.setMuted(uuid, muted);
175
+    }
176
+
177
+    /**
178
+     * Prepare / initialize CallKit. This method must be called before any
179
+     * other.
180
+     *
181
+     * @param {Object} options - Initialization options.
182
+     * @param {string} options.imageName - Image to be used in CallKit's
183
+     * application button..
184
+     * @param {string} options.ringtoneSound - Ringtone to be used for incoming
185
+     * calls.
186
+     * @returns {void}
187
+     */
188
+    setup(options = {}) {
189
+        if (CallKit.isSupported()) {
190
+            options.appName = NativeModules.AppInfo.name;
191
+            RNCallKit.setup(options);
192
+        }
193
+
194
+        this._setup = true;
195
+    }
196
+
197
+    /**
198
+     * Indicate CallKit about a new outgoing call.
199
+     *
200
+     * @param {string} uuid - Unique identifier for the call.
201
+     * @param {string} handle - Call handle in CallKit's terms. The room URL in
202
+     * our case.
203
+     * @param {boolean} hasVideo - True if it's a video call, false otherwise.
204
+     * @returns {Promise}
205
+     */
206
+    startCall(uuid, handle, hasVideo = true) {
207
+        this._checkSetup();
208
+        if (!CallKit.isSupported()) {
209
+            return Promise.resolve();
210
+        }
211
+
212
+        return RNCallKit.startCall(uuid, handle, hasVideo);
213
+    }
214
+
215
+    /**
216
+     * Updates an ongoing call's parameters.
217
+     *
218
+     * @param {string} uuid - Unique identifier for the call.
219
+     * @param {Object} options - Object with properties which should be updated.
220
+     * @param {string} options.displayName - Display name for the caller.
221
+     * @param {boolean} options.hasVideo - True if the call has video, false
222
+     * otherwise.
223
+     * @returns {Promise}
224
+     */
225
+    updateCall(uuid, options) {
226
+        this._checkSetup();
227
+        if (!CallKit.isSupported()) {
228
+            return Promise.resolve();
229
+        }
230
+
231
+        return RNCallKit.updateCall(uuid, options);
232
+    }
233
+}
234
+
235
+export default new CallKit();

+ 11
- 0
react/features/mobile/callkit/actionTypes.js Целия файл

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

+ 2
- 0
react/features/mobile/callkit/index.js Целия файл

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

+ 194
- 0
react/features/mobile/callkit/middleware.js Целия файл

@@ -0,0 +1,194 @@
1
+/* @flow */
2
+
3
+import uuid from 'uuid';
4
+
5
+import {
6
+    APP_WILL_MOUNT,
7
+    APP_WILL_UNMOUNT,
8
+    appNavigate
9
+} from '../../app';
10
+import {
11
+    CONFERENCE_FAILED,
12
+    CONFERENCE_LEFT,
13
+    CONFERENCE_WILL_JOIN,
14
+    CONFERENCE_JOINED
15
+} from '../../base/conference';
16
+import { getInviteURL } from '../../base/connection';
17
+import {
18
+    SET_AUDIO_MUTED,
19
+    SET_VIDEO_MUTED,
20
+    isVideoMutedByAudioOnly,
21
+    setAudioMuted
22
+} from '../../base/media';
23
+import { MiddlewareRegistry, toState } from '../../base/redux';
24
+import { _SET_CALLKIT_LISTENERS } from './actionTypes';
25
+import CallKit from './CallKit';
26
+
27
+/**
28
+ * Middleware that captures several system actions and hooks up CallKit.
29
+ *
30
+ * @param {Store} store - The redux store.
31
+ * @returns {Function}
32
+ */
33
+MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
34
+    const result = next(action);
35
+
36
+    switch (action.type) {
37
+    case _SET_CALLKIT_LISTENERS: {
38
+        const { listeners } = getState()['features/callkit'];
39
+
40
+        if (listeners) {
41
+            for (const [ event, listener ] of listeners) {
42
+                CallKit.removeEventListener(event, listener);
43
+            }
44
+        }
45
+
46
+        if (action.listeners) {
47
+            for (const [ event, listener ] of action.listeners) {
48
+                CallKit.addEventListener(event, listener);
49
+            }
50
+        }
51
+
52
+        break;
53
+    }
54
+
55
+    case APP_WILL_MOUNT: {
56
+        CallKit.setup();  // TODO: set app icon.
57
+        const listeners = new Map();
58
+        const callEndListener = data => {
59
+            const conference = getCurrentConference(getState);
60
+
61
+            if (conference && conference.callUUID === data.callUUID) {
62
+                // We arrive here when a call is ended by the system, for
63
+                // for example when another incoming call is received and the
64
+                // user selects "End & Accept".
65
+                delete conference.callUUID;
66
+                dispatch(appNavigate(undefined));
67
+            }
68
+        };
69
+
70
+        listeners.set('performEndCallAction', callEndListener);
71
+
72
+        // Set the same listener for providerDidReset. According to the docs,
73
+        // when the system resets we should terminate all calls.
74
+        listeners.set('providerDidReset', callEndListener);
75
+
76
+        const setMutedListener = data => {
77
+            const conference = getCurrentConference(getState);
78
+
79
+            if (conference && conference.callUUID === data.callUUID) {
80
+                // Break the loop. Audio can be muted both from the CallKit
81
+                // interface and from the Jitsi Meet interface. We must keep
82
+                // them in sync, but at some point the loop needs to be broken.
83
+                // We are doing it here, on the CallKit handler.
84
+                const { muted } = getState()['features/base/media'].audio;
85
+
86
+                if (muted !== data.muted) {
87
+                    dispatch(setAudioMuted(Boolean(data.muted)));
88
+                }
89
+
90
+            }
91
+        };
92
+
93
+        listeners.set('performSetMutedCallAction', setMutedListener);
94
+
95
+        dispatch({
96
+            type: _SET_CALLKIT_LISTENERS,
97
+            listeners
98
+        });
99
+        break;
100
+    }
101
+
102
+    case APP_WILL_UNMOUNT:
103
+        dispatch({
104
+            type: _SET_CALLKIT_LISTENERS,
105
+            listeners: null
106
+        });
107
+        break;
108
+
109
+    case CONFERENCE_FAILED: {
110
+        const { callUUID } = action.conference;
111
+
112
+        if (callUUID) {
113
+            CallKit.reportCallFailed(callUUID);
114
+        }
115
+
116
+        break;
117
+    }
118
+
119
+    case CONFERENCE_LEFT: {
120
+        const { callUUID } = action.conference;
121
+
122
+        if (callUUID) {
123
+            CallKit.endCall(callUUID);
124
+        }
125
+
126
+        break;
127
+    }
128
+
129
+    case CONFERENCE_JOINED: {
130
+        const { callUUID } = action.conference;
131
+
132
+        if (callUUID) {
133
+            CallKit.reportConnectedOutgoingCall(callUUID);
134
+        }
135
+
136
+        break;
137
+    }
138
+
139
+    case CONFERENCE_WILL_JOIN: {
140
+        const conference = action.conference;
141
+        const url = getInviteURL(getState);
142
+        const hasVideo = !isVideoMutedByAudioOnly({ getState });
143
+
144
+        // When assigning the call UUID, do so in upper case, since iOS will
145
+        // return it upper cased.
146
+        conference.callUUID = uuid.v4().toUpperCase();
147
+        CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
148
+            .then(() => {
149
+                const { room } = getState()['features/base/conference'];
150
+
151
+                CallKit.updateCall(conference.callUUID, { displayName: room });
152
+            });
153
+        break;
154
+    }
155
+
156
+    case SET_AUDIO_MUTED: {
157
+        const conference = getCurrentConference(getState);
158
+
159
+        if (conference && conference.callUUID) {
160
+            CallKit.setMuted(conference.callUUID, action.muted);
161
+        }
162
+
163
+        break;
164
+    }
165
+
166
+    case SET_VIDEO_MUTED: {
167
+        const conference = getCurrentConference(getState);
168
+
169
+        if (conference && conference.callUUID) {
170
+            const hasVideo = !isVideoMutedByAudioOnly({ getState });
171
+
172
+            CallKit.updateCall(conference.callUUID, { hasVideo });
173
+        }
174
+
175
+        break;
176
+    }
177
+    }
178
+
179
+    return result;
180
+});
181
+
182
+/**
183
+ * Returns the currently active conference.
184
+ *
185
+ * @param {Function|Object} stateOrGetState - The redux state or redux's
186
+ * {@code getState} function.
187
+ * @returns {Conference|undefined}
188
+ */
189
+function getCurrentConference(stateOrGetState: Function | Object): ?Object {
190
+    const state = toState(stateOrGetState);
191
+    const { conference, joining } = state['features/base/conference'];
192
+
193
+    return conference || joining;
194
+}

+ 17
- 0
react/features/mobile/callkit/reducer.js Целия файл

@@ -0,0 +1,17 @@
1
+import { ReducerRegistry } from '../../base/redux';
2
+
3
+import {
4
+    _SET_CALLKIT_LISTENERS
5
+} from './actionTypes';
6
+
7
+ReducerRegistry.register('features/callkit', (state = {}, action) => {
8
+    switch (action.type) {
9
+    case _SET_CALLKIT_LISTENERS:
10
+        return {
11
+            ...state,
12
+            listeners: action.listeners
13
+        };
14
+    }
15
+
16
+    return state;
17
+});

Loading…
Отказ
Запис