diff --git a/actions.js b/actions.js index 4e936360..82a61507 100644 --- a/actions.js +++ b/actions.js @@ -11,6 +11,7 @@ const RNCallKeepDidDeactivateAudioSession = 'RNCallKeepDidDeactivateAudioSession const RNCallKeepDidDisplayIncomingCall = 'RNCallKeepDidDisplayIncomingCall'; const RNCallKeepDidPerformSetMutedCallAction = 'RNCallKeepDidPerformSetMutedCallAction'; const RNCallKeepDidToggleHoldAction = 'RNCallKeepDidToggleHoldAction'; +const RNCallKeepPerformGroupCallAction = 'RNCallKeepPerformGroupCallAction'; const RNCallKeepDidPerformDTMFAction = 'RNCallKeepDidPerformDTMFAction'; const RNCallKeepProviderReset = 'RNCallKeepProviderReset'; const RNCallKeepCheckReachability = 'RNCallKeepCheckReachability'; @@ -63,6 +64,9 @@ const didPerformSetMutedCallAction = handler => const didToggleHoldCallAction = handler => eventEmitter.addListener(RNCallKeepDidToggleHoldAction, handler); +const performGroupCallAction = handler => + eventEmitter.addListener(RNCallKeepPerformGroupCallAction, handler); + const didPerformDTMFAction = handler => eventEmitter.addListener(RNCallKeepDidPerformDTMFAction, (data) => handler(data)); @@ -95,6 +99,7 @@ export const listeners = { didDisplayIncomingCall, didPerformSetMutedCallAction, didToggleHoldCallAction, + performGroupCallAction, didPerformDTMFAction, didResetProvider, checkReachability, diff --git a/android/build.gradle b/android/build.gradle index 3accf1c6..01c9b486 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,6 +16,7 @@ def safeExtGet(prop, fallback) { } android { + namespace 'io.wazo.callkeep' compileSdkVersion safeExtGet('compileSdkVersion', 28) defaultConfig { @@ -31,5 +32,6 @@ repositories { } dependencies { - implementation 'com.facebook.react:react-native:+' + implementation 'com.facebook.react:react-native:0.66.3' + implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0" } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index fe2b6cce..916b9907 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - + + static isAudioSessionActive(): Promise + static getCalls(): Promise static getAudioRoutes(): Promise @@ -175,6 +178,9 @@ declare module 'react-native-callkeep' { static toggleAudioRouteSpeaker(uuid: string, routeSpeaker: boolean): void static setOnHold(uuid: string, held: boolean): void + + static setGroupCall(activeUuid: string, heldUuid: string): void + static setGroupCallFulfilled(): void static setConnectionState(uuid: string, state: number): void @@ -199,5 +205,9 @@ declare module 'react-native-callkeep' { static setCurrentCallActive(callUUID: string): void static backToForeground(): void + + static configureVoiceAudioSession(): void + + static configureVideoAudioSession(): void } } diff --git a/index.js b/index.js index 3e1df0f0..23d159d4 100644 --- a/index.js +++ b/index.js @@ -27,6 +27,17 @@ class RNCallKeep { addEventListener = (type, handler) => { const listener = listeners[type](handler); + /** + * xxx https://idtjira.atlassian.net/browse/MOB-5329 + */ + if (type === 'didActivateAudioSession') { + this.isAudioSessionActive().then(isActive => { + if (isActive) { + handler() + } + }) + } + this._callkeepEventHandlers.set(type, listener); }; @@ -170,6 +181,8 @@ class RNCallKeep { isCallActive = async (uuid) => await RNCallKeepModule.isCallActive(uuid); + isAudioSessionActive = RNCallKeepModule.isAudioSessionActive; + getCalls = () => { if (isIOS) { return RNCallKeepModule.getCalls(); @@ -258,6 +271,9 @@ class RNCallKeep { }; setOnHold = (uuid, shouldHold) => RNCallKeepModule.setOnHold(uuid, shouldHold); + + setGroupCall = (activeUuid, heldUuid) => RNCallKeepModule.setGroupCall(activeUuid, heldUuid); + setGroupCallFulfilled = () => RNCallKeepModule.setGroupCallFulfilled(); setConnectionState = (uuid, state) => isIOS ? null : RNCallKeepModule.setConnectionState(uuid, state); @@ -347,6 +363,14 @@ class RNCallKeep { clearInitialEvents() { return RNCallKeepModule.clearInitialEvents(); } + + configureVideoAudioSession() { + return RNCallKeepModule.configureVideoAudioSession(); + } + + configureVoiceAudioSession() { + return RNCallKeepModule.configureVoiceAudioSession(); + } } export default new RNCallKeep(); diff --git a/ios/RNCallKeep/RNCallKeep.h b/ios/RNCallKeep/RNCallKeep.h index c8e7bbdf..2efb133d 100644 --- a/ios/RNCallKeep/RNCallKeep.h +++ b/ios/RNCallKeep/RNCallKeep.h @@ -16,8 +16,12 @@ @interface RNCallKeep : RCTEventEmitter +@property(nonatomic,retain) NSTimer *forceBluetoothTimer; +@property BOOL shouldForceBluetooth; + @property (nonatomic, strong) CXCallController *callKeepCallController; @property (nonatomic, strong) CXProvider *callKeepProvider; +@property (nonatomic, strong) CXSetGroupCallAction * callKeepGroupCallAction; + (BOOL)application:(UIApplication *)application openURL:(NSURL *)url @@ -44,7 +48,11 @@ continueUserActivity:(NSUserActivity *)userActivity reason:(int)reason; + (BOOL)isCallActive:(NSString *)uuidString; ++ (BOOL)isAudioSessionActive; + (void)setup:(NSDictionary *)options; ++ (void)configureVoiceAudioSession; ++ (void)configureVideoAudioSession; + @end diff --git a/ios/RNCallKeep/RNCallKeep.m b/ios/RNCallKeep/RNCallKeep.m index eff7dc2c..694ff9c4 100644 --- a/ios/RNCallKeep/RNCallKeep.m +++ b/ios/RNCallKeep/RNCallKeep.m @@ -33,6 +33,7 @@ static NSString *const RNCallKeepDidPerformSetMutedCallAction = @"RNCallKeepDidPerformSetMutedCallAction"; static NSString *const RNCallKeepPerformPlayDTMFCallAction = @"RNCallKeepDidPerformDTMFAction"; static NSString *const RNCallKeepDidToggleHoldAction = @"RNCallKeepDidToggleHoldAction"; +static NSString *const RNCallKeepPerformGroupCallAction = @"RNCallKeepPerformGroupCallAction"; static NSString *const RNCallKeepProviderReset = @"RNCallKeepProviderReset"; static NSString *const RNCallKeepCheckReachability = @"RNCallKeepCheckReachability"; static NSString *const RNCallKeepDidChangeAudioRoute = @"RNCallKeepDidChangeAudioRoute"; @@ -43,6 +44,7 @@ @implementation RNCallKeep NSOperatingSystemVersion _version; BOOL _isStartCallActionEventListenerAdded; bool _hasListeners; + bool _isAudioSessionActive; bool _isReachable; NSMutableArray *_delayedEvents; } @@ -59,6 +61,7 @@ - (instancetype)init NSLog(@"[RNCallKeep][init]"); #endif if (self = [super init]) { + _shouldForceBluetooth = TRUE; _isStartCallActionEventListenerAdded = NO; _isReachable = NO; if (_delayedEvents == nil) _delayedEvents = [NSMutableArray array]; @@ -112,6 +115,7 @@ - (void)dealloc RNCallKeepDidPerformSetMutedCallAction, RNCallKeepPerformPlayDTMFCallAction, RNCallKeepDidToggleHoldAction, + RNCallKeepPerformGroupCallAction, RNCallKeepProviderReset, RNCallKeepCheckReachability, RNCallKeepDidLoadWithEvents, @@ -147,6 +151,12 @@ - (void)onAudioRouteChange:(NSNotification *)notification @"output": output, @"reason": @(reason), }]; + + if (_shouldForceBluetooth) { + // Force Bluetooth as soon as possible once onAudioRouteChange event handler finishes + _forceBluetoothTimer = [NSTimer scheduledTimerWithTimeInterval:.1 target:self selector:@selector(forceBluetoothPreferredInput:) userInfo:nil repeats:YES]; + [_forceBluetoothTimer fire]; + } } - (void)sendEventWithNameWrapper:(NSString *)name body:(id)body { @@ -161,6 +171,13 @@ - (void)sendEventWithNameWrapper:(NSString *)name body:(id)body { nil ]; [_delayedEvents addObject:dictionary]; + + } + + if ([name isEqualToString:@"RNCallKeepDidReceiveStartCallAction"]) { + [_delayedEvents addObject: [NSDictionary dictionaryWithObjectsAndKeys: + @"RNCallKeepDidActivateAudioSession", @"name", nil]]; + _isAudioSessionActive = YES; } } @@ -178,7 +195,12 @@ + (void)initCallKitProvider { } + (NSString *) getAudioOutput { - return [AVAudioSession sharedInstance].currentRoute.outputs.count > 0 ? [AVAudioSession sharedInstance].currentRoute.outputs[0].portType : nil; + @try{ + return [AVAudioSession sharedInstance].currentRoute.outputs.count > 0 ? [AVAudioSession sharedInstance].currentRoute.outputs[0].portType : nil; + }@catch(NSException *e){ + NSLog(@"[RNCallKeep][getAudioOutput] exception: %@",e); + return nil; + } } + (void)setup:(NSDictionary *)options { @@ -353,7 +375,7 @@ + (void)setup:(NSDictionary *)options { NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid]; CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; - + _isAudioSessionActive = NO; [self requestTransaction:transaction]; } @@ -365,8 +387,12 @@ + (void)setup:(NSDictionary *)options { for (CXCall *call in self.callKeepCallController.callObserver.calls) { CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:call.UUID]; CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; + _isAudioSessionActive = NO; [self requestTransaction:transaction]; } + + // We are ready to force the Bluetooth for new calls + _shouldForceBluetooth = true; } RCT_EXPORT_METHOD(setOnHold:(NSString *)uuidString :(BOOL)shouldHold) @@ -382,6 +408,30 @@ + (void)setup:(NSDictionary *)options { [self requestTransaction:transaction]; } +RCT_EXPORT_METHOD(setGroupCall:(NSString *)activeUuidString :(NSString *)heldUuidString) +{ +#ifdef DEBUG + NSLog(@"[RNCallKeep][setGroupCall] activeUuidString = %@, heldUuidString = %d", activeUuidString, heldUuidString); +#endif + NSUUID *activeUuid = [[NSUUID alloc] initWithUUIDString:activeUuidString]; + NSUUID *heldUuid = [[NSUUID alloc] initWithUUIDString:heldUuidString]; + CXSetGroupCallAction *setGroupCallAction = [[CXSetGroupCallAction alloc] initWithCallUUID:heldUuid callUUIDToGroupWith:activeUuid]; + CXTransaction *transaction = [[CXTransaction alloc] init]; + [transaction addAction:setGroupCallAction]; + + [self requestTransaction:transaction]; +} + +RCT_EXPORT_METHOD(setGroupCallFulfilled) +{ +#ifdef DEBUG + NSLog(@"[RNCallKeep][setGroupCallFulfilled]"); +#endif + if (self.callKeepGroupCallAction != nil) { + [self.callKeepGroupCallAction fulfill]; + } +} + RCT_EXPORT_METHOD(_startCallActionEventListenerAdded) { _isStartCallActionEventListenerAdded = YES; @@ -460,6 +510,12 @@ + (void)setup:(NSDictionary *)options { [self requestTransaction:transaction]; } +RCT_EXPORT_METHOD(isAudioSessionActive:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + resolve([NSNumber numberWithBool:_isAudioSessionActive]); +} + RCT_EXPORT_METHOD(isCallActive:(NSString *)uuidString isCallActiveResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) @@ -497,6 +553,11 @@ + (void)setup:(NSDictionary *)options { AVAudioSession* myAudioSession = [AVAudioSession sharedInstance]; if ([inputName isEqualToString:@"Speaker"]) { BOOL isOverrided = [myAudioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&err]; + + if (isOverrided) { + _shouldForceBluetooth = FALSE; + } + if(!isOverrided){ [NSException raise:@"overrideOutputAudioPort failed" format:@"error: %@", err]; } @@ -505,9 +566,34 @@ + (void)setup:(NSDictionary *)options { } NSArray *ports = [RNCallKeep getAudioInputs]; + + BOOL isCategorySetted = [myAudioSession setCategory:AVAudioSessionCategoryPlayAndRecord mode:AVAudioSessionModeVoiceChat options: AVAudioSessionCategoryOptionAllowAirPlay | AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&err]; + if (!isCategorySetted) + { + NSLog(@"[RNCallKeep][setAudioRoute] setCategory failed"); + [NSException raise:@"setCategory failed" format:@"error: %@", err]; + } + + BOOL isCategoryActivated = [myAudioSession setActive:YES error:&err]; + if (!isCategoryActivated) + { + NSLog(@"[RNCallKeep][setAudioRoute] setActive failed"); + [NSException raise:@"setActive failed" format:@"error: %@", err]; + } + for (AVAudioSessionPortDescription *port in ports) { if ([port.portName isEqualToString:inputName]) { BOOL isSetted = [myAudioSession setPreferredInput:(AVAudioSessionPortDescription *)port error:&err]; + + if (isSetted) { + if ([port.portType isEqualToString:AVAudioSessionPortBluetoothHFP] || + [port.portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) { + _shouldForceBluetooth = TRUE; + } else { + _shouldForceBluetooth = FALSE; + } + } + if(!isSetted){ [NSException raise:@"setPreferredInput failed" format:@"error: %@", err]; } @@ -570,20 +656,6 @@ + (NSArray *) getAudioInputs AVAudioSession* myAudioSession = [AVAudioSession sharedInstance]; - BOOL isCategorySetted = [myAudioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:&err]; - if (!isCategorySetted) - { - NSLog(@"[RNCallKeep][getAudioInputs] setCategory failed"); - [NSException raise:@"setCategory failed" format:@"error: %@", err]; - } - - BOOL isCategoryActivated = [myAudioSession setActive:YES error:&err]; - if (!isCategoryActivated) - { - NSLog(@"[RNCallKeep][getAudioInputs] setActive failed"); - [NSException raise:@"setActive failed" format:@"error: %@", err]; - } - NSArray *inputs = [myAudioSession availableInputs]; return inputs; } @@ -754,9 +826,7 @@ + (void)reportNewIncomingCall:(NSString *)uuidString }]; if (error == nil) { // Workaround per https://forums.developer.apple.com/message/169511 - if ([callKeep lessThanIos10_2]) { - [callKeep configureAudioSession]; - } + [callKeep configureAudioSession:AVAudioSessionModeVoiceChat]; } if (completion != nil) { completion(); @@ -843,23 +913,72 @@ + (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary*)settings return providerConfiguration; } -- (void)configureAudioSession +- (void)configureAudioSession:(AVAudioSessionMode)mode { #ifdef DEBUG NSLog(@"[RNCallKeep][configureAudioSession] Activating audio session"); #endif + @try{ + NSError* err = nil; + + AVAudioSession* audioSession = [AVAudioSession sharedInstance]; + BOOL isConfigured = [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord mode:mode options: AVAudioSessionCategoryOptionAllowAirPlay | AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&err]; + if(!isConfigured){ + NSLog(@"[RNCallKeep][configureAudioSession][setCategory] failed"); + [NSException raise:@"audioSession#setCategory failed" format:@"error: %@", err]; + } + + double sampleRate = 44100.0; + BOOL sampleRateSetted = [audioSession setPreferredSampleRate:sampleRate error:&err]; + if(!sampleRateSetted){ + NSLog(@"[RNCallKeep][configureAudioSession][setPreferredSampleRate] failed"); + [NSException raise:@"audioSession#setPreferredSampleRate failed" format:@"error: %@", err]; + } + + NSTimeInterval bufferDuration = .005; + BOOL bufferSetted = [audioSession setPreferredIOBufferDuration:bufferDuration error:&err]; + if(!bufferSetted){ + NSLog(@"[RNCallKeep][configureAudioSession][setPreferredIOBufferDuration] failed"); + [NSException raise:@"audioSession#setPreferredIOBufferDuration failed" format:@"error: %@", err]; + } + + BOOL isActivated = [audioSession setActive:TRUE error:&err]; + if(!isActivated){ + NSLog(@"[RNCallKeep][configureAudioSession][setActive] failed"); + [NSException raise:@"audioSession#setActive failed" format:@"error: %@", err]; + } + } + @catch ( NSException *e ){ + NSLog(@"[RNCallKeep][configureAudioSession] exception: %@",e); + } + +} +- (void) forceBluetoothPreferredInput:(id)sender +{ + [_forceBluetoothTimer invalidate]; + _forceBluetoothTimer = nil; + AVAudioSession* audioSession = [AVAudioSession sharedInstance]; - [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil]; - - [audioSession setMode:AVAudioSessionModeDefault error:nil]; - - double sampleRate = 44100.0; - [audioSession setPreferredSampleRate:sampleRate error:nil]; - - NSTimeInterval bufferDuration = .005; - [audioSession setPreferredIOBufferDuration:bufferDuration error:nil]; - [audioSession setActive:TRUE error:nil]; + NSArray *ports = [RNCallKeep getAudioInputs]; + + for (AVAudioSessionPortDescription *port in ports) { + if ([port.portType isEqualToString:AVAudioSessionPortBluetoothHFP] || + [port.portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) { + @try { + NSError* err = nil; + BOOL isSetted = [audioSession setPreferredInput:(AVAudioSessionPortDescription *)port error:&err]; + + if (!isSetted) { + [NSException raise:@"forceBluetoothPreferredInput failed" format:@"error: %@", err]; + } + + break; + } @catch (NSException *e) { + NSLog(@"[RNCallKeep][forceBluetoothPreferredInput] exception: %@",e); + } + } + } } + (BOOL)application:(UIApplication *)application @@ -944,6 +1063,7 @@ + (BOOL)application:(UIApplication *)application }; RNCallKeep *callKeep = [RNCallKeep allocWithZone: nil]; + [callKeep sendEventWithNameWrapper:RNCallKeepDidReceiveStartCallAction body:userInfo]; return YES; } @@ -973,7 +1093,7 @@ - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallActio NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performStartCallAction]"); #endif //do this first, audio sessions are flakey - [self configureAudioSession]; + //[self configureAudioSession]; //tell the JS to actually make the call [self sendEventWithNameWrapper:RNCallKeepDidReceiveStartCallAction body:@{ @"callUUID": [action.callUUID.UUIDString lowercaseString], @"handle": action.handle.value }]; [action fulfill]; @@ -993,13 +1113,24 @@ - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallActio [self.callKeepProvider reportCallWithUUID:uuid updated:callUpdate]; } +RCT_EXPORT_METHOD(configureVoiceAudioSession) +{ + [self configureAudioSession:AVAudioSessionModeVoiceChat]; +} + + +RCT_EXPORT_METHOD(configureVideoAudioSession) +{ + [self configureAudioSession:AVAudioSessionModeVideoChat]; +} + // Answering incoming call - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { #ifdef DEBUG NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performAnswerCallAction]"); #endif - [self configureAudioSession]; + //[self configureAudioSession]; [self sendEventWithNameWrapper:RNCallKeepPerformAnswerCallAction body:@{ @"callUUID": [action.callUUID.UUIDString lowercaseString] }]; [action fulfill]; } @@ -1010,6 +1141,7 @@ - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *) #ifdef DEBUG NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performEndCallAction]"); #endif + _isAudioSessionActive = NO; [self sendEventWithNameWrapper:RNCallKeepPerformEndCallAction body:@{ @"callUUID": [action.callUUID.UUIDString lowercaseString] }]; [action fulfill]; } @@ -1024,6 +1156,17 @@ -(void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAc [action fulfill]; } +-(void)provider:(CXProvider *)provider performSetGroupCallAction:(CXSetGroupCallAction *)action +{ +#ifdef DEBUG + NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performSetGroupCallAction]"); +#endif + + [self sendEventWithNameWrapper:RNCallKeepPerformGroupCallAction body:@{ @"activeCallUUID": [action.callUUID.UUIDString lowercaseString], @"heldCallUUID": [action.callUUIDToGroupWith.UUIDString lowercaseString] }]; + + self.callKeepGroupCallAction = action; +} + - (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action { #ifdef DEBUG NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performPlayDTMFCallAction]"); @@ -1061,7 +1204,9 @@ - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession }; [[NSNotificationCenter defaultCenter] postNotificationName:AVAudioSessionInterruptionNotification object:nil userInfo:userInfo]; - [self configureAudioSession]; + //[self configureAudioSession]; + _isAudioSessionActive = YES; + [self sendEventWithNameWrapper:RNCallKeepDidActivateAudioSession body:nil]; } @@ -1070,6 +1215,7 @@ - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSessio #ifdef DEBUG NSLog(@"[RNCallKeep][CXProviderDelegate][provider:didDeactivateAudioSession]"); #endif + _isAudioSessionActive = NO; [self sendEventWithNameWrapper:RNCallKeepDidDeactivateAudioSession body:nil]; }